summaryrefslogtreecommitdiff
path: root/addons
diff options
context:
space:
mode:
authorAaron Bohy <aab@odoo.com>2018-01-02 16:25:28 +0100
committerAaron Bohy <aab@odoo.com>2018-01-26 19:26:03 +0100
commitd18d08f01a589053e40c7af1fd4dd59c13aaa625 (patch)
tree63a243bccee505c4263fe441357946401cf7985e /addons
parentaf2686fee55b20d74ce1799aea84bf1910b20051 (diff)
[IMP] web: Kanban: quick create record with more fields
This rev. improves the QuickCreate widget for grouped Kanban views. It allows specifying in the Kanban arch a form view xmlid, e.g.: <kanban quick_create_view="some_form_view_xmlid"> ... </kanban> When this attribute is set, the referenced form view is loaded and rendered inside the QuickCreate widget. A real FormView widget is instantiated, meaning that all form view mechanisms are available in the QuickCreate (modifiers, default_get, onchanges, widgets...). Task #27475.
Diffstat (limited to 'addons')
-rw-r--r--addons/web/static/src/js/views/basic/basic_controller.js11
-rw-r--r--addons/web/static/src/js/views/form/form_renderer.js11
-rw-r--r--addons/web/static/src/js/views/kanban/kanban_column.js38
-rw-r--r--addons/web/static/src/js/views/kanban/kanban_controller.js59
-rw-r--r--addons/web/static/src/js/views/kanban/kanban_model.js40
-rw-r--r--addons/web/static/src/js/views/kanban/kanban_quick_create.js411
-rw-r--r--addons/web/static/src/js/views/kanban/kanban_renderer.js43
-rw-r--r--addons/web/static/src/js/views/kanban/kanban_renderer_mobile.js4
-rw-r--r--addons/web/static/src/js/views/kanban/kanban_view.js1
-rw-r--r--addons/web/static/src/js/views/kanban/quick_create_form_view.js110
-rw-r--r--addons/web/static/src/less/form_view.less51
-rw-r--r--addons/web/static/src/less/form_view_extra.less7
-rw-r--r--addons/web/static/src/less/kanban_view.less11
-rw-r--r--addons/web/static/src/xml/kanban.xml5
-rw-r--r--addons/web/static/tests/helpers/mock_server.js6
-rw-r--r--addons/web/static/tests/views/kanban_tests.js626
-rw-r--r--addons/web/views/webclient_templates.xml1
17 files changed, 1155 insertions, 280 deletions
diff --git a/addons/web/static/src/js/views/basic/basic_controller.js b/addons/web/static/src/js/views/basic/basic_controller.js
index 2c7c1c40210..c4dff8fad2e 100644
--- a/addons/web/static/src/js/views/basic/basic_controller.js
+++ b/addons/web/static/src/js/views/basic/basic_controller.js
@@ -68,7 +68,7 @@ var BasicController = AbstractController.extend(FieldManagerMixin, {
* rejected otherwise
*/
canBeDiscarded: function (recordID) {
- if (!this.model.isDirty(recordID || this.handle)) {
+ if (!this.isDirty(recordID)) {
return $.when(false);
}
@@ -121,6 +121,15 @@ var BasicController = AbstractController.extend(FieldManagerMixin, {
return [];
},
/**
+ * Returns true iff the given recordID (or the main recordID) is dirty.
+ *
+ * @param {string} [recordID] - default to main recordID
+ * @returns {boolean}
+ */
+ isDirty: function (recordID) {
+ return this.model.isDirty(recordID || this.handle);
+ },
+ /**
* @override
*/
renderPager: function ($node, options) {
diff --git a/addons/web/static/src/js/views/form/form_renderer.js b/addons/web/static/src/js/views/form/form_renderer.js
index 7d9c3cd2d78..b289663fb19 100644
--- a/addons/web/static/src/js/views/form/form_renderer.js
+++ b/addons/web/static/src/js/views/form/form_renderer.js
@@ -18,10 +18,19 @@ var FormRenderer = BasicRenderer.extend({
/**
* @override
*/
- init: function (parent, state, params) {
+ init: function () {
this._super.apply(this, arguments);
this.idsForLabels = {};
},
+ /**
+ * @override
+ */
+ start: function () {
+ if (config.device.size_class <= config.device.SIZES.XXS) {
+ this.$el.addClass('o_xxs_form_view');
+ }
+ return this._super.apply(this, arguments);
+ },
//--------------------------------------------------------------------------
// Public
diff --git a/addons/web/static/src/js/views/kanban/kanban_column.js b/addons/web/static/src/js/views/kanban/kanban_column.js
index bde447870bb..563df7a4630 100644
--- a/addons/web/static/src/js/views/kanban/kanban_column.js
+++ b/addons/web/static/src/js/views/kanban/kanban_column.js
@@ -47,6 +47,8 @@ var KanbanColumn = Widget.extend({
this.modelName = data.model;
this.quick_create = options.quick_create;
+ this.quickCreateView = options.quickCreateView;
+ this.groupedBy = options.groupedBy;
this.grouped_by_m2o = options.grouped_by_m2o;
this.editable = options.editable;
this.deletable = options.deletable;
@@ -156,6 +158,18 @@ var KanbanColumn = Widget.extend({
return $.when.apply($, defs);
},
+ /**
+ * Called when a record has been quick created, as a new column is rendered
+ * and appended into a fragment, before replacing the old column in the DOM.
+ * When this happens, the quick create widget is inserted into the new
+ * column directly, and it should be focused. However, as it is rendered
+ * into a fragment, the focus has to be set manually once in the DOM.
+ */
+ on_attach_callback: function () {
+ if (this.quickCreateWidget) {
+ this.quickCreateWidget.on_attach_callback();
+ }
+ },
//--------------------------------------------------------------------------
// Public
@@ -163,6 +177,8 @@ var KanbanColumn = Widget.extend({
/**
* Adds the quick create record to the top of the column.
+ *
+ * @returns {Deferred}
*/
addQuickCreate: function () {
if (this.folded) {
@@ -173,11 +189,25 @@ var KanbanColumn = Widget.extend({
return;
}
if (this.quickCreateWidget) {
- return;
+ return $.Deferred().reject();
+ }
+ this.trigger_up('close_quick_create'); // close other quick create widgets
+ var context = this.data.getContext();
+ context['default_' + this.groupedBy] = this.id;
+ this.quickCreateWidget = new RecordQuickCreate(this, {
+ context: context,
+ formViewRef: this.quickCreateView,
+ model: this.modelName,
+ });
+ return this.quickCreateWidget.insertAfter(this.$header);
+ },
+ /**
+ * Closes the quick create widget if it isn't dirty.
+ */
+ cancelQuickCreate: function () {
+ if (this.quickCreateWidget) {
+ this.quickCreateWidget.cancel();
}
- var width = this.records.length ? this.records[0].$el.innerWidth() : this.$el.width() - 8;
- this.quickCreateWidget = new RecordQuickCreate(this, width);
- this.quickCreateWidget.insertAfter(this.$header);
},
/**
* @returns {Boolean} true iff the column is empty
diff --git a/addons/web/static/src/js/views/kanban/kanban_controller.js b/addons/web/static/src/js/views/kanban/kanban_controller.js
index 2ddd5577330..ca2999b05b0 100644
--- a/addons/web/static/src/js/views/kanban/kanban_controller.js
+++ b/addons/web/static/src/js/views/kanban/kanban_controller.js
@@ -297,52 +297,51 @@ var KanbanController = BasicController.extend({
/**
* @private
* @param {OdooEvent} event
+ * @param {KanbanColumn} event.target the column in which the record should
+ * be added
+ * @param {Object} event.data.values the field values of the record to
+ * create; if values only contains the value of the 'display_name', a
+ * 'name_create' is performed instead of 'create'
*/
_onQuickCreateRecord: function (event) {
var self = this;
+ var values = event.data.values;
var column = event.target;
- var name = event.data.value;
- var state = this.model.get(this.handle, {raw: true});
- var columnState = this.model.get(column.db_id, {raw: true});
- var context = columnState.getContext();
- context['default_' + state.groupedBy[0]] = columnState.res_id;
- this._rpc({
- model: state.model,
- method: 'name_create',
- args: [name],
- context: context,
- })
- .then(add_record)
+ // function that updates the kanban view once the record has been added
+ // it receives the local id of the created record in arguments
+ var update = function (db_id) {
+ self._updateEnv();
+
+ var columnState = self.model.getColumn(db_id);
+ return self.renderer
+ .updateColumn(columnState.id, columnState, {openQuickCreate: true})
+ .then(function () {
+ if (event.data.openRecord) {
+ self.trigger_up('open_record', {id: db_id, mode: 'edit'});
+ }
+ });
+ };
+
+ this.model.createRecordInGroup(column.db_id, values)
+ .then(update)
.fail(function (error, event) {
event.preventDefault();
+ var columnState = self.model.get(column.db_id, {raw: true});
+ var context = columnState.getContext();
+ var state = self.model.get(self.handle, {raw: true});
+ context['default_' + state.groupedBy[0]] = columnState.res_id;
new view_dialogs.FormViewDialog(self, {
res_model: state.model,
context: _.extend({default_name: name}, context),
title: _t("Create"),
disable_multiple_selection: true,
on_saved: function (record) {
- add_record([record.res_id]);
+ self.model.addRecordToGroup(column.db_id, record.res_id)
+ .then(update);
},
}).open();
});
-
- function add_record(records) {
- return self.model
- .addRecordToGroup(columnState.id, records[0])
- .then(function (db_id) {
- self._updateEnv();
-
- var columnState = self.model.getColumn(db_id);
- return self.renderer
- .updateColumn(columnState.id, columnState, {openQuickCreate: true})
- .then(function () {
- if (event.data.openRecord) {
- self.trigger_up('open_record', {id: db_id, mode: 'edit'});
- }
- });
- });
- }
},
/**
* @private
diff --git a/addons/web/static/src/js/views/kanban/kanban_model.js b/addons/web/static/src/js/views/kanban/kanban_model.js
index df6172f0d8b..ba6b2ce3dbd 100644
--- a/addons/web/static/src/js/views/kanban/kanban_model.js
+++ b/addons/web/static/src/js/views/kanban/kanban_model.js
@@ -33,7 +33,7 @@ var KanbanModel = BasicModel.extend({
parentID: groupID,
});
- return this._fetchRecord(new_record).then(function (result) {
+ var def = this._fetchRecord(new_record).then(function (result) {
group.data.unshift(new_record.id);
group.res_ids.unshift(resId);
group.count++;
@@ -44,6 +44,7 @@ var KanbanModel = BasicModel.extend({
return result.id;
});
+ return this._reloadProgressBarGroupFromRecord(new_record.id, def);
},
/**
* Creates a new group from a name (performs a name_create).
@@ -94,6 +95,43 @@ var KanbanModel = BasicModel.extend({
});
},
/**
+ * Creates a new record from the given value, and add it to the given group.
+ *
+ * @param {string} groupID
+ * @param {Object} values
+ * @returns {Deferred} resolved with the local id of the created record
+ */
+ createRecordInGroup: function (groupID, values) {
+ var self = this;
+ var group = this.localData[groupID];
+ var context = this._getContext(group);
+ var parent = this.localData[group.parentID];
+ context['default_' + parent.groupedBy[0]] = group.res_id;
+ var def;
+ if (Object.keys(values).length === 1 && 'display_name' in values) {
+ // only 'display_name is given, perform a 'name_create'
+ def = this._rpc({
+ model: parent.model,
+ method: 'name_create',
+ args: [values.display_name],
+ context: context,
+ }).then(function (records) {
+ return records[0];
+ });
+ } else {
+ // other fields are specified, perform a classical 'create'
+ def = this._rpc({
+ model: parent.model,
+ method: 'create',
+ args: [values],
+ context: context,
+ });
+ }
+ return def.then(function (resID) {
+ return self.addRecordToGroup(group.id, resID);
+ });
+ },
+ /**
* Add the key `tooltipData` (kanban specific) when performing a `geĊ§`.
*
* @override
diff --git a/addons/web/static/src/js/views/kanban/kanban_quick_create.js b/addons/web/static/src/js/views/kanban/kanban_quick_create.js
index 9c317552329..66c6a2676f1 100644
--- a/addons/web/static/src/js/views/kanban/kanban_quick_create.js
+++ b/addons/web/static/src/js/views/kanban/kanban_quick_create.js
@@ -6,229 +6,263 @@ odoo.define('web.kanban_quick_create', function (require) {
* kanban records, and one to quick create kanban columns.
*/
+var core = require('web.core');
+var QuickCreateFormView = require('web.QuickCreateFormView');
var Widget = require('web.Widget');
-var AbstractQuickCreate = Widget.extend({
+var qweb = core.qweb;
+
+var RecordQuickCreate = Widget.extend({
+ className: 'o_kanban_quick_create',
+ custom_events: {
+ add: '_onAdd',
+ cancel: '_onCancel',
+ },
events: {
'click .o_kanban_add': '_onAddClicked',
+ 'click .o_kanban_edit': '_onEditClicked',
'click .o_kanban_cancel': '_onCancelClicked',
- 'keydown': '_onKeydown',
- 'keypress input': '_onKeypress',
- 'mousedown .o_kanban_add': '_onMousedown',
- 'mousedown .o_kanban_cancel': '_onMousedown',
- 'focusout': '_onFocusOut',
},
- //--------------------------------------------------------------------------
- // Private
- //--------------------------------------------------------------------------
-
/**
- * Clears the input value and notify the environment that a quick create
- * has been done
- *
- * @private
- * @param {Object} [options] dict of options to pass to call to '_notifyAdd'
+ * @override
+ * @param {Widget} parent
+ * @param {Object} options
+ * @param {Object} options.context
+ * @param {string|null} options.formViewRef
+ * @param {string} options.model
*/
- _add: function (options) {
- var value = this.$input.val();
- this.$input.val('');
- if (/^\s*$/.test(value)) {
- return;
- }
- this._notifyAdd(value, options);
- this.$input.focus();
+ init: function (parent, options) {
+ this._super.apply(this, arguments);
+ this.context = options.context;
+ this.formViewRef = options.formViewRef;
+ this.model = options.model;
},
/**
- * Should implement what to do when the quick creation has been cancelled
+ * Loads the form fieldsView (if not provided), instantiates the form view
+ * and starts the form controller.
*
- * @abstract
- * @private
+ * @override
*/
- _cancel: function () {
+ willStart: function () {
+ var self = this;
+ var def1 = this._super.apply(this, arguments);
+ var def2;
+ if (this.formViewRef) {
+ var views = [[false, 'form']];
+ var context = _.extend({}, this.context, {
+ form_view_ref: this.formViewRef,
+ });
+ def2 = this.loadViews(this.model, context, views);
+ } else {
+ var fieldsView = {};
+ fieldsView.arch = '<form>' +
+ '<field name="display_name" placeholder="Title" modifiers=\'{"required": true}\'/>' +
+ '</form>';
+ var fields = {
+ display_name: {string: 'Display name', type: 'char'},
+ };
+ fieldsView.fields = fields;
+ fieldsView.viewFields = fields;
+ def2 = $.when({form: fieldsView});
+ }
+ def2 = def2.then(function (fieldsViews) {
+ var formView = new QuickCreateFormView(fieldsViews.form, {
+ context: self.context,
+ modelName: self.model,
+ userContext: self.getSession().user_context,
+ });
+ return formView.getController(self).then(function (controller) {
+ self.controller = controller;
+ return self.controller.appendTo(document.createDocumentFragment());
+ });
+ });
+ return $.when(def1, def2);
},
/**
- * Should implement what to do to notify the environment that a quick
- * create has been done
- *
- * @abstract
- * @private
+ * @override
*/
- _notifyAdd: function (name, options) {
- },
+ start: function () {
+ this.$el.append(this.controller.$el);
+ this.$el.append(qweb.render('KanbanView.RecordQuickCreate.buttons'));
- //--------------------------------------------------------------------------
- // Handlers
- //--------------------------------------------------------------------------
+ // focus the first field
+ this.controller.autofocus();
- /**
- * @private
- * @param {MouseEvent} event
- */
- _onAddClicked: function (event) {
- event.stopPropagation();
- this._add();
- },
- /**
- * @private
- */
- _onCancelClicked: function () {
- this._cancel();
+ // destroy the quick create when the user clicks outside
+ core.bus.on('click', this, this._onWindowClicked);
+
+ return this._super.apply(this, arguments);
},
/**
- * Cancels quick creation on focusout input event
- *
- * @private
- * @param {KeyEvent} ev
+ * Called when the quick create is appended into the DOM.
*/
- _onFocusOut: function (ev) {
- if (!this.$input.val()) {
- this._cancel();
+ on_attach_callback: function () {
+ if (this.controller) {
+ this.controller.autofocus();
}
},
+
+ //--------------------------------------------------------------------------
+ // Public
+ //--------------------------------------------------------------------------
+
/**
- * Cancels quick creation on escape keydown event
+ * Cancels the quick creation if the record isn't dirty, i.e. if no changes
+ * have been made yet
*
* @private
- * @param {KeyEvent} event
+ * @returns {Deferred}
*/
- _onKeydown: function (event) {
- if (event.keyCode === 27) { // escape
- this._cancel();
- }
+ cancel: function () {
+ var self = this;
+ return this.controller.commitChanges().then(function () {
+ if (!self.controller.isDirty()) {
+ self._cancel();
+ }
+ });
},
+
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+
/**
- * Validates quick creation on enter keypress event
- *
+ * @override
* @private
- * @param {KeyEvent} event
+ * @param {Object} [options]
+ * @param {boolean} [options.openRecord] set to true to directly open the
+ * newly created record in a form view (in edit mode)
+ * @returns {Deferred}
*/
- _onKeypress: function (event) {
- if (event.keyCode === 13) { // enter
- this._add();
- }
+ _add: function (options) {
+ var self = this;
+ return this.controller.commitChanges().then(function () {
+ var canBeSaved = self.controller.canBeSaved();
+ if (canBeSaved) {
+ self.trigger_up('quick_create_add_record', {
+ openRecord: options && options.openRecord || false,
+ values: self.controller.getChanges(),
+ });
+ }
+ });
},
/**
- * In all browsers the 'focus/blur' event is triggered before a button's
- * 'click' (it's actually triggered by mousedown).
- * The quick create is destroyed on blur of its input but still needs
- * to create the data when the "Add" button is clicked.
- * This problem is adressed by suppressing the 'focus/blur' event from the
- * relevant buttons (Add and Cancel) entirely by preventing the browser's
- * default mousedown handler.
+ * Notifies the environment that the quick creation must be cancelled
*
* @private
- * @param {MouseEvent} event
- */
- _onMousedown: function (event) {
- event.preventDefault();
- },
-});
-
-var RecordQuickCreate = AbstractQuickCreate.extend({
- template: "KanbanView.QuickCreate",
- events: _.extend({}, AbstractQuickCreate.prototype.events, {
- 'click .o_kanban_edit': '_onEditClicked',
- 'mousedown .o_kanban_edit': '_onMousedown',
- }),
- /**
- * @override
- * @param {Widget} parent
- * @param {Object} options
- * @param {string|number} options.width defines the element's width
- * @param {string} [options.defaultName] the record's default name
- */
- init: function (parent, options) {
- this._super.apply(this, arguments);
- this.width = options.width;
- this.defaultName = options.defaultName;
- },
- /**
- * @override
+ * @returns {Deferred}
*/
- start: function () {
- this.$el.css({width: this.width});
- this.$input = this.$('input');
- this._addDefaultName();
- this.$input.focus();
- return this._super.apply(this, arguments);
+ _cancel: function () {
+ this.trigger_up('cancel_quick_create');
},
//--------------------------------------------------------------------------
- // Private
+ // Handlers
//--------------------------------------------------------------------------
/**
- * Set the default value for the record name.
- *
* @private
+ * @param {OdooEvent} ev
*/
- _addDefaultName: function () {
- if (this.defaultName) {
- this.$input.val(this.defaultName);
- }
+ _onAdd: function (ev) {
+ ev.stopPropagation();
+ this._add();
},
/**
- * Triggers up an event to cancel the quick creation
- *
- * @override
* @private
+ * @param {MouseEvent} ev
*/
- _cancel: function () {
- this.trigger_up('cancel_quick_create');
+ _onAddClicked: function (ev) {
+ ev.stopPropagation();
+ this._add();
},
/**
- * Triggers up an event to quick create a record with the given value
- *
- * @override
* @private
- * @param {string} value
- * @param {Object} [options]
- * @param {boolean} [options.openRecord] set to true to directly open the
- * newly created record in a form view (in edit mode)
+ * @param {OdooEvent} ev
*/
- _notifyAdd: function (value, options) {
- this.trigger_up('quick_create_add_record', {
- value: value,
- openRecord: options && options.openRecord || false,
- });
+ _onCancel: function (ev) {
+ ev.stopPropagation();
+ this._cancel();
},
-
- //--------------------------------------------------------------------------
- // Handlers
- //--------------------------------------------------------------------------
-
/**
- * Add the default value for the record name again after creating
- * the previous record.
- *
- * @override
* @private
+ * @param {MouseEvent} ev
*/
- _onAddClicked: function () {
- this._super.apply(this, arguments);
- this._addDefaultName();
+ _onCancelClicked: function (ev) {
+ ev.stopPropagation();
+ this._cancel();
},
/**
* Validates the quick creation and directly opens the record in a form
* view in edit mode.
*
* @private
- * @param {MouseEvent} event
+ * @param {MouseEvent} ev
*/
- _onEditClicked: function (event) {
- event.stopPropagation();
+ _onEditClicked: function (ev) {
+ ev.stopPropagation();
this._add({openRecord: true});
},
+ /**
+ * When a click happens outside the quick create, we want to close the quick
+ * create.
+ *
+ * This is quite tricky, because in some cases a click is performed outside
+ * the quick create, but is still related to it (e.g. click in a dialog
+ * opened from the quick create).
+ *
+ * @param {MouseEvent} ev
+ */
+ _onWindowClicked: function (ev) {
+ // ignore clicks if the quick create is not in the dom
+ if (!document.contains(this.el)) {
+ return;
+ }
+
+ // ignore clicks on elements that open the quick create widget, to
+ // prevent from closing quick create widget that has just been opened
+ if ($(ev.target).closest('.o-kanban-button-new, .o_kanban_quick_add').length) {
+ return;
+ }
+
+ // ignore clicks in autocomplete dropdowns
+ if ($(ev.target).parents('.ui-autocomplete').length) {
+ return;
+ }
+
+ // ignore clicks in modals
+ if ($(ev.target).closest('.modal').length) {
+ return;
+ }
+
+ // ignore clicks if target is no longer in dom (e.g., a click on the
+ // 'delete' trash icon of a m2m tag)
+ if (!document.contains(ev.target)) {
+ return;
+ }
+
+ // ignore clicks if target is inside the quick create
+ if (this.el.contains(ev.target) && this.el !== ev.target) {
+ return;
+ }
+
+ this.cancel();
+ },
});
-var ColumnQuickCreate = AbstractQuickCreate.extend({
+var ColumnQuickCreate = Widget.extend({
template: 'KanbanView.ColumnQuickCreate',
- events: _.extend({}, AbstractQuickCreate.prototype.events, {
+ events: {
'click': 'toggleFold',
'click input': '_onInputClicked',
+ 'click .o_kanban_add': '_onAddClicked',
'focusout': '_onFocusout',
- }),
+ 'keydown': '_onKeydown',
+ 'keypress input': '_onKeypress',
+ 'mousedown .o_kanban_add': '_onMousedown',
+ },
+
/**
* @override
*/
@@ -251,7 +285,7 @@ var ColumnQuickCreate = AbstractQuickCreate.extend({
//--------------------------------------------------------------------------
/**
- * Toggle fold/unfold the Column quick create widget
+ * Folds/unfolds the Column quick create widget
*/
toggleFold: function () {
this.folded = !this.folded;
@@ -267,26 +301,31 @@ var ColumnQuickCreate = AbstractQuickCreate.extend({
//--------------------------------------------------------------------------
/**
- * @override
+ * Clears the input value and notify the environment to create a column
+ *
* @private
*/
- _cancel: function () {
- this.folded = true;
+ _add: function () {
+ var value = this.$input.val().trim();
+ if (!value.length) {
+ return;
+ }
this.$input.val('');
- this._update();
+ this.trigger_up('quick_create_add_column', {value: value});
+ this.$input.focus();
},
/**
- * Triggers up an event to quick create a column with the given value
+ * Cancels the quick creation
*
- * @override
* @private
- * @param {string} value
*/
- _notifyAdd: function (value) {
- this.trigger_up('quick_create_add_column', {value: value});
+ _cancel: function () {
+ this.folded = true;
+ this.$input.val('');
+ this._update();
},
/**
- * Updates the rendering according to the current state
+ * Updates the rendering according to the current state (folded/unfolded)
*
* @private
*/
@@ -301,6 +340,16 @@ var ColumnQuickCreate = AbstractQuickCreate.extend({
/**
* @private
+ * @param {MouseEvent} event
+ */
+ _onAddClicked: function (event) {
+ event.stopPropagation();
+ this._add();
+ },
+ /**
+ * Cancels the quick creation when the input loses the focus
+ *
+ * @private
*/
_onFocusout: function () {
var hasFocus = this.$(':focus').length > 0;
@@ -319,6 +368,42 @@ var ColumnQuickCreate = AbstractQuickCreate.extend({
_onInputClicked: function (event) {
event.stopPropagation();
},
+ /**
+ * Cancels quick creation on escape keydown event
+ *
+ * @private
+ * @param {KeyEvent} event
+ */
+ _onKeydown: function (event) {
+ if (event.keyCode === 27) { // escape
+ this._cancel();
+ }
+ },
+ /**
+ * Validates quick creation on enter keypress event
+ *
+ * @private
+ * @param {KeyEvent} event
+ */
+ _onKeypress: function (event) {
+ if (event.keyCode === 13) { // enter
+ this._add();
+ }
+ },
+ /**
+ * In all browsers the 'focus/blur' event is triggered before a button's
+ * 'click' (it's actually triggered by mousedown).
+ * The quick create is hidden on blur of its input but still needs to create
+ * the data when the "Add" button is clicked.
+ * This problem is adressed by suppressing the 'focus/blur' event from the
+ * Add button entirely by preventing the browser's default mousedown handler.
+ *
+ * @private
+ * @param {MouseEvent} event
+ */
+ _onMousedown: function (event) {
+ event.preventDefault();
+ },
});
return {
diff --git a/addons/web/static/src/js/views/kanban/kanban_renderer.js b/addons/web/static/src/js/views/kanban/kanban_renderer.js
index 1dad8e9c35d..52f57287296 100644
--- a/addons/web/static/src/js/views/kanban/kanban_renderer.js
+++ b/addons/web/static/src/js/views/kanban/kanban_renderer.js
@@ -84,7 +84,8 @@ function transformQwebTemplate(node, fields) {
var KanbanRenderer = BasicRenderer.extend({
className: 'o_kanban_view',
custom_events: _.extend({}, BasicRenderer.prototype.custom_events || {}, {
- 'set_progress_bar_state': '_onSetProgressBarState',
+ close_quick_create: '_onCloseQuickCreate',
+ set_progress_bar_state: '_onSetProgressBarState',
}),
/**
@@ -117,9 +118,11 @@ var KanbanRenderer = BasicRenderer.extend({
/**
* Displays the quick create record in the first column.
+ *
+ * @returns {Deferred}
*/
addQuickCreate: function () {
- this.widgets[0].addQuickCreate();
+ return this.widgets[0].addQuickCreate();
},
/**
* Toggle fold/unfold the Column quick create widget
@@ -152,11 +155,21 @@ var KanbanRenderer = BasicRenderer.extend({
var index = _.findIndex(this.widgets, {db_id: localID});
var column = this.widgets[index];
this.widgets[index] = newColumn;
- return newColumn.insertAfter(column.$el).then(function () {
+ return newColumn.appendTo(document.createDocumentFragment()).then(function () {
+ var def;
if (options && options.openQuickCreate) {
- newColumn.addQuickCreate();
+ def = newColumn.addQuickCreate();
}
- column.destroy();
+ return $.when(def).then(function () {
+ newColumn.$el.insertAfter(column.$el);
+ // When a record has been quick created, the new column directly
+ // renders the quick create widget (to allow quick creating several
+ // records in a row). However, as we render this column in a
+ // fragment, the quick create widget can't be correctly focused. So
+ // we manually call on_attach_callback to focus it once in the DOM.
+ newColumn.on_attach_callback();
+ column.destroy();
+ });
});
},
/**
@@ -334,8 +347,9 @@ var KanbanRenderer = BasicRenderer.extend({
_setState: function (state) {
this.state = state;
- var groupByFieldAttrs = state.fields[state.groupedBy[0]];
- var groupByFieldInfo = state.fieldsInfo.kanban[state.groupedBy[0]];
+ var groupByField = state.groupedBy[0];
+ var groupByFieldAttrs = state.fields[groupByField];
+ var groupByFieldInfo = state.fieldsInfo.kanban[groupByField];
// Deactivate the drag'n'drop if the groupedBy field:
// - is a date or datetime since we group by month or
// - is readonly (on the field attrs or in the view)
@@ -353,13 +367,14 @@ var KanbanRenderer = BasicRenderer.extend({
}
}
this.groupedByM2O = groupByFieldAttrs && (groupByFieldAttrs.type === 'many2one');
- var grouped_by_field = this.groupedByM2O && groupByFieldAttrs.relation;
+ var relation = this.groupedByM2O && groupByFieldAttrs.relation;
var groupByTooltip = groupByFieldInfo && groupByFieldInfo.options.group_by_tooltip;
this.columnOptions = _.extend(this.columnOptions, {
draggable: draggable,
group_by_tooltip: groupByTooltip,
+ groupedBy: groupByField,
grouped_by_m2o: this.groupedByM2O,
- relation: grouped_by_field,
+ relation: relation,
});
this.createColumnEnabled = this.groupedByM2O && this.columnOptions.group_creatable;
},
@@ -369,6 +384,16 @@ var KanbanRenderer = BasicRenderer.extend({
//--------------------------------------------------------------------------
/**
+ * Closes the opened quick create widgets in columns
+ *
+ * @private
+ */
+ _onCloseQuickCreate: function () {
+ if (this.state.groupedBy.length) {
+ _.invoke(this.widgets, 'cancelQuickCreate');
+ }
+ },
+ /**
* Updates progressbar internal states (necessary for animations) with
* received data.
*
diff --git a/addons/web/static/src/js/views/kanban/kanban_renderer_mobile.js b/addons/web/static/src/js/views/kanban/kanban_renderer_mobile.js
index 7614d6bca95..c609e45fc60 100644
--- a/addons/web/static/src/js/views/kanban/kanban_renderer_mobile.js
+++ b/addons/web/static/src/js/views/kanban/kanban_renderer_mobile.js
@@ -38,9 +38,11 @@ KanbanRenderer.include({
/**
* Displays the quick create record in the active column
+ *
+ * @returns {Deferred}
*/
addQuickCreate: function () {
- this.widgets[this.activeColumnIndex].addQuickCreate();
+ return this.widgets[this.activeColumnIndex].addQuickCreate();
},
/**
* Overrides to restore the left property and the scrollTop on the updated
diff --git a/addons/web/static/src/js/views/kanban/kanban_view.js b/addons/web/static/src/js/views/kanban/kanban_view.js
index 87b4a366502..c5ebbf5243f 100644
--- a/addons/web/static/src/js/views/kanban/kanban_view.js
+++ b/addons/web/static/src/js/views/kanban/kanban_view.js
@@ -62,6 +62,7 @@ var KanbanView = BasicView.extend({
deletable: activeActions.group_delete,
group_creatable: activeActions.group_create && !config.device.isMobile,
quick_create: params.isQuickCreateEnabled || this._isQuickCreateEnabled(),
+ quickCreateView: this.arch.attrs.quick_create_view || null,
hasProgressBar: !!progressBar,
};
this.rendererParams.record_options = {
diff --git a/addons/web/static/src/js/views/kanban/quick_create_form_view.js b/addons/web/static/src/js/views/kanban/quick_create_form_view.js
new file mode 100644
index 00000000000..fe0684a9ada
--- /dev/null
+++ b/addons/web/static/src/js/views/kanban/quick_create_form_view.js
@@ -0,0 +1,110 @@
+odoo.define('web.QuickCreateFormView', function (require) {
+"use strict";
+
+/**
+ * This file defines the QuickCreateFormView, an extension of the FormView that
+ * is used by the RecordQuickCreate in Kanban views.
+ */
+
+var FormController = require('web.FormController');
+var BasicModel = require('web.BasicModel');
+var FormRenderer = require('web.FormRenderer');
+var FormView = require('web.FormView');
+
+var QuickCreateFormRenderer = FormRenderer.extend({
+ /**
+ * @override
+ */
+ start: function () {
+ this.$el.addClass('o_xxs_form_view');
+ return this._super.apply(this, arguments);
+ },
+
+ //--------------------------------------------------------------------------
+ // Handlers
+ //--------------------------------------------------------------------------
+
+ /**
+ * @override
+ * @private
+ * @param {OdooEvent} ev
+ */
+ _onNavigationMove: function (ev) {
+ var direction = ev.data.direction;
+ if (direction === 'cancel' || direction === 'next_line') {
+ ev.stopPropagation();
+ this.trigger_up(direction === 'cancel' ? 'cancel' : 'add');
+ } else {
+ this._super.apply(this, arguments);
+ }
+ },
+});
+
+var QuickCreateFormModel = BasicModel.extend({
+ //--------------------------------------------------------------------------
+ // Public
+ //--------------------------------------------------------------------------
+
+ /**
+ * @returns {Object} the changes of the given resource (server commands for
+ * x2manys)
+ */
+ getChanges: function (localID) {
+ var record = this.localData[localID];
+ return this._generateChanges(record, {changesOnly: false});
+ },
+});
+
+var QuickCreateFormController = FormController.extend({
+ custom_events: _.extend({}, FormController.prototype.custom_events, {
+ env_updated: '_onEnvUpdated',
+ }),
+
+ //--------------------------------------------------------------------------
+ // Public
+ //--------------------------------------------------------------------------
+
+ /**
+ * Asks all field widgets to notify the environment with their current value
+ * (useful for instance for input fields that still have the focus and that
+ * could have not notified the environment of their changes yet).
+ *
+ * @return {Deferred}
+ */
+ commitChanges: function () {
+ return this.mutex
+ .exec(this.renderer.commitChanges.bind(this.renderer, this.handle));
+ },
+ /**
+ * @returns {Object} the changes done on the current record
+ */
+ getChanges: function () {
+ return this.model.getChanges(this.handle);
+ },
+
+ //--------------------------------------------------------------------------
+ // Handlers
+ //--------------------------------------------------------------------------
+
+ /**
+ * Stops the propagation of the 'env_updated' event to prevent interferences
+ * with the kanban controller.
+ *
+ * @private
+ */
+ _onEnvUpdated: function (ev) {
+ ev.stopPropagation();
+ },
+});
+
+var QuickCreateFormView = FormView.extend({
+ config: _.extend({}, FormView.prototype.config, {
+ Model: QuickCreateFormModel,
+ Renderer: QuickCreateFormRenderer,
+ Controller: QuickCreateFormController,
+ }),
+});
+
+return QuickCreateFormView;
+
+});
diff --git a/addons/web/static/src/less/form_view.less b/addons/web/static/src/less/form_view.less
index 8a053a8cca3..505f62ac42f 100644
--- a/addons/web/static/src/less/form_view.less
+++ b/addons/web/static/src/less/form_view.less
@@ -649,6 +649,57 @@ label.o_light_label, .o_light_label label {
font-weight: normal;
}
+// XXS form view specific rules
+.form-break-table() {
+ display: block;
+ margin-bottom: @odoo-form-spacing-unit * 4;
+
+ > tbody {
+ display: block;
+
+ > tr {
+ .o-flex-display();
+ .o-flex-flow(row, wrap);
+
+ > td {
+ .o-flex(1, 0, auto);
+ display: block;
+ max-width: 100%;
+ padding: 0;
+
+ width: auto!important; // !important is required to override the width computed in JS
+ &.o_td_label {
+ width: 94%!important; // The label must be on its own line except if the form field is small enough (checkbox)
+ line-height: @odoo-label-font-size-factor;
+ }
+
+ .o_field_widget {
+ margin-bottom: @odoo-form-spacing-unit * 2;
+ &.o_field_boolean {
+ margin-right: 0;
+ }
+ }
+
+ .o_input_dropdown {
+ width: auto;
+ max-width: 100%;
+ }
+ }
+ }
+ }
+}
+.o_form_view.o_xxs_form_view {
+ .oe_title {
+ word-break: break-all;
+ }
+ .o_group {
+ &.o_inner_group {
+ .form-break-table();
+ }
+ }
+}
+
+// Settings form views
.o_settings_container {
.o-flex-display();
.o-flex(0, 1, auto);
diff --git a/addons/web/static/src/less/form_view_extra.less b/addons/web/static/src/less/form_view_extra.less
index 2349ee5c0b0..95da6a2b35b 100644
--- a/addons/web/static/src/less/form_view_extra.less
+++ b/addons/web/static/src/less/form_view_extra.less
@@ -181,3 +181,10 @@
}
}
}
+
+// XXS form view specific rules
+.o_form_view.o_xxs_form_view {
+ .o_group .o_td_label {
+ border: none;
+ }
+}
diff --git a/addons/web/static/src/less/kanban_view.less b/addons/web/static/src/less/kanban_view.less
index 9158aa22df6..2b5482e36ff 100644
--- a/addons/web/static/src/less/kanban_view.less
+++ b/addons/web/static/src/less/kanban_view.less
@@ -161,12 +161,11 @@
box-shadow: 0 0 20px -10px;
margin: 0 @o-kanban-record-margin -1px @o-kanban-record-margin;
- input {
- width: 100%;
- margin: 2px 0 8px;
- padding: 3px 6px;
- color: @headings-color;
- font-weight: 500;
+ .o_form_view {
+ padding: 0;
+ .o_group.o_inner_group {
+ margin: 0;
+ }
}
}
diff --git a/addons/web/static/src/xml/kanban.xml b/addons/web/static/src/xml/kanban.xml
index dd8427a7643..97ac827df2a 100644
--- a/addons/web/static/src/xml/kanban.xml
+++ b/addons/web/static/src/xml/kanban.xml
@@ -54,9 +54,8 @@
</div>
</t>
-<t t-name="KanbanView.QuickCreate">
- <div class="o_kanban_quick_create">
- <input type="text" class="o_input" placeholder="Title"/>
+<t t-name="KanbanView.RecordQuickCreate.buttons">
+ <div>
<button class="btn btn-sm btn-primary o_kanban_add">Add</button>
<button class="btn btn-sm btn-primary o_kanban_edit">Edit</button>
<button class="btn btn-sm btn-default o_kanban_cancel ml8">Discard</button>
diff --git a/addons/web/static/tests/helpers/mock_server.js b/addons/web/static/tests/helpers/mock_server.js
index 8a44e1eb1e7..a80aa2ff05b 100644
--- a/addons/web/static/tests/helpers/mock_server.js
+++ b/addons/web/static/tests/helpers/mock_server.js
@@ -485,6 +485,12 @@ var MockServer = Class.extend({
_.each(kwargs.views, function (view_descr) {
var viewID = view_descr[0] || false;
var viewType = view_descr[1];
+ if (!viewID) {
+ var contextKey = viewType + '_view_ref';
+ if (contextKey in kwargs.context) {
+ viewID = kwargs.context[contextKey];
+ }
+ }
var key = [model, viewID, viewType].join(',');
var arch = self.archs[key];
if (!arch) {
diff --git a/addons/web/static/tests/views/kanban_tests.js b/addons/web/static/tests/views/kanban_tests.js
index d4054e9f7af..b899fee7a64 100644
--- a/addons/web/static/tests/views/kanban_tests.js
+++ b/addons/web/static/tests/views/kanban_tests.js
@@ -329,7 +329,367 @@ QUnit.module('Views', {
kanban.destroy();
});
- QUnit.test('quick create and change state in grouped mode', function (assert) {
+ QUnit.test('quick create record without quick_create_view', function (assert) {
+ assert.expect(16);
+
+ var kanban = createView({
+ View: KanbanView,
+ model: 'partner',
+ data: this.data,
+ arch: '<kanban on_create="quick_create">' +
+ '<field name="bar"/>' +
+ '<templates><t t-name="kanban-box">' +
+ '<div><field name="foo"/></div>' +
+ '</t></templates></kanban>',
+ groupBy: ['bar'],
+ mockRPC: function (route, args) {
+ assert.step(args.method || route);
+ if (args.method === 'name_create') {
+ assert.strictEqual(args.args[0], 'new partner',
+ "should send the correct value");
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ assert.strictEqual(kanban.$('.o_kanban_group:first .o_kanban_record').length, 1,
+ "first column should contain one record");
+
+ // click on 'Create' -> should open the quick create in the first column
+ kanban.$buttons.find('.o-kanban-button-new').click();
+ var $quickCreate = kanban.$('.o_kanban_group:first .o_kanban_quick_create');
+
+ assert.strictEqual($quickCreate.length, 1,
+ "should have a quick create element in the first column");
+ assert.strictEqual($quickCreate.find('.o_form_view.o_xxs_form_view').length, 1,
+ "should have rendered an XXS form view");
+ assert.strictEqual($quickCreate.find('input').length, 1,
+ "should have only one input");
+ assert.ok($quickCreate.find('input').hasClass('o_required_modifier'),
+ "the field should be required");
+ assert.strictEqual($quickCreate.find('input[placeholder=Title]').length, 1,
+ "input placeholder should be 'Title'");
+
+ // fill the quick create and validate
+ $quickCreate.find('input').val('new partner').trigger('input');
+ $quickCreate.find('button.o_kanban_add').click();
+
+ assert.strictEqual(kanban.$('.o_kanban_group:first .o_kanban_record').length, 2,
+ "first column should contain two records");
+
+ assert.verifySteps([
+ 'read_group', // initial read_group
+ '/web/dataset/search_read', // initial search_read (first column)
+ '/web/dataset/search_read', // initial search_read (second column)
+ 'default_get', // quick create
+ 'name_create', // should perform a name_create to create the record
+ 'read', // read the created record
+ 'default_get', // reopen the quick create automatically
+ ]);
+
+ kanban.destroy();
+ });
+
+ QUnit.test('quick create record with quick_create_view', function (assert) {
+ assert.expect(17);
+
+ var kanban = createView({
+ View: KanbanView,
+ model: 'partner',
+ data: this.data,
+ arch: '<kanban on_create="quick_create" quick_create_view="some_view_ref">' +
+ '<field name="bar"/>' +
+ '<templates><t t-name="kanban-box">' +
+ '<div><field name="foo"/></div>' +
+ '</t></templates></kanban>',
+ archs: {
+ 'partner,some_view_ref,form': '<form>' +
+ '<field name="foo"/>' +
+ '<field name="int_field"/>' +
+ '<field name="state" widget="priority"/>' +
+ '</form>',
+ },
+ groupBy: ['bar'],
+ mockRPC: function (route, args) {
+ assert.step(args.method || route);
+ if (args.method === 'create') {
+ assert.deepEqual(args.args[0], {
+ foo: 'new partner',
+ int_field: 4,
+ state: 'def',
+ }, "should send the correct values");
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ assert.strictEqual(kanban.$('.o_kanban_group:first .o_kanban_record').length, 1,
+ "first column should contain one record");
+
+ // click on 'Create' -> should open the quick create in the first column
+ kanban.$buttons.find('.o-kanban-button-new').click();
+ var $quickCreate = kanban.$('.o_kanban_group:first .o_kanban_quick_create');
+
+ assert.strictEqual($quickCreate.length, 1,
+ "should have a quick create element in the first column");
+ assert.strictEqual($quickCreate.find('.o_form_view.o_xxs_form_view').length, 1,
+ "should have rendered an XXS form view");
+ assert.strictEqual($quickCreate.find('input').length, 2,
+ "should have two inputs");
+ assert.strictEqual($quickCreate.find('.o_field_widget').length, 3,
+ "should have rendered three widgets");
+
+ // fill the quick create and validate
+ $quickCreate.find('.o_field_widget[name=foo]').val('new partner').trigger('input');
+ $quickCreate.find('.o_field_widget[name=int_field]').val('4').trigger('input');
+ $quickCreate.find('.o_field_widget[name=state] .o_priority_star:first').click();
+ $quickCreate.find('button.o_kanban_add').click();
+
+ assert.strictEqual(kanban.$('.o_kanban_group:first .o_kanban_record').length, 2,
+ "first column should contain two records");
+
+ assert.verifySteps([
+ 'read_group', // initial read_group
+ '/web/dataset/search_read', // initial search_read (first column)
+ '/web/dataset/search_read', // initial search_read (second column)
+ 'load_views', // form view in quick create
+ 'default_get', // quick create
+ 'create', // should perform a create to create the record
+ 'read', // read the created record
+ 'load_views', // form view in quick create (is actually in cache)
+ 'default_get', // reopen the quick create automatically
+ ]);
+
+ kanban.destroy();
+ });
+
+ QUnit.test('quick create record in grouped on m2o (no quick_create_view)', function (assert) {
+ assert.expect(12);
+
+ var kanban = createView({
+ View: KanbanView,
+ model: 'partner',
+ data: this.data,
+ arch: '<kanban on_create="quick_create">' +
+ '<field name="product_id"/>' +
+ '<templates><t t-name="kanban-box">' +
+ '<div><field name="foo"/></div>' +
+ '</t></templates>' +
+ '</kanban>',
+ groupBy: ['product_id'],
+ mockRPC: function (route, args) {
+ assert.step(args.method || route);
+ if (args.method === 'name_create') {
+ assert.strictEqual(args.args[0], 'new partner',
+ "should send the correct value");
+ assert.deepEqual(args.kwargs.context, {
+ default_product_id: 3,
+ default_qux: 2.5,
+ }, "should send the correct context");
+ }
+ return this._super.apply(this, arguments);
+ },
+ viewOptions: {
+ context: {default_qux: 2.5},
+ },
+ });
+
+ assert.strictEqual(kanban.$('.o_kanban_group:first .o_kanban_record').length, 2,
+ "first column should contain two records");
+
+ // click on 'Create', fill the quick create and validate
+ kanban.$buttons.find('.o-kanban-button-new').click();
+ var $quickCreate = kanban.$('.o_kanban_group:first .o_kanban_quick_create');
+ $quickCreate.find('input').val('new partner').trigger('input');
+ $quickCreate.find('button.o_kanban_add').click();
+
+ assert.strictEqual(kanban.$('.o_kanban_group:first .o_kanban_record').length, 3,
+ "first column should contain three records");
+
+ assert.verifySteps([
+ 'read_group', // initial read_group
+ '/web/dataset/search_read', // initial search_read (first column)
+ '/web/dataset/search_read', // initial search_read (second column)
+ 'default_get', // quick create
+ 'name_create', // should perform a name_create to create the record
+ 'read', // read the created record
+ 'default_get', // reopen the quick create automatically
+ ]);
+
+ kanban.destroy();
+ });
+
+ QUnit.test('quick create record in grouped on m2o (with quick_create_view)', function (assert) {
+ assert.expect(14);
+
+ var kanban = createView({
+ View: KanbanView,
+ model: 'partner',
+ data: this.data,
+ arch: '<kanban on_create="quick_create" quick_create_view="some_view_ref">' +
+ '<field name="product_id"/>' +
+ '<templates><t t-name="kanban-box">' +
+ '<div><field name="foo"/></div>' +
+ '</t></templates></kanban>',
+ archs: {
+ 'partner,some_view_ref,form': '<form>' +
+ '<field name="foo"/>' +
+ '<field name="int_field"/>' +
+ '<field name="state" widget="priority"/>' +
+ '</form>',
+ },
+ groupBy: ['product_id'],
+ mockRPC: function (route, args) {
+ assert.step(args.method || route);
+ if (args.method === 'create') {
+ assert.deepEqual(args.args[0], {
+ foo: 'new partner',
+ int_field: 4,
+ state: 'def',
+ }, "should send the correct values");
+ assert.deepEqual(args.kwargs.context, {
+ default_product_id: 3,
+ default_qux: 2.5,
+ }, "should send the correct context");
+ }
+ return this._super.apply(this, arguments);
+ },
+ viewOptions: {
+ context: {default_qux: 2.5},
+ },
+ });
+
+ assert.strictEqual(kanban.$('.o_kanban_group:first .o_kanban_record').length, 2,
+ "first column should contain two records");
+
+ // click on 'Create', fill the quick create and validate
+ kanban.$buttons.find('.o-kanban-button-new').click();
+ var $quickCreate = kanban.$('.o_kanban_group:first .o_kanban_quick_create');
+ $quickCreate.find('.o_field_widget[name=foo]').val('new partner').trigger('input');
+ $quickCreate.find('.o_field_widget[name=int_field]').val('4').trigger('input');
+ $quickCreate.find('.o_field_widget[name=state] .o_priority_star:first').click();
+ $quickCreate.find('button.o_kanban_add').click();
+
+ assert.strictEqual(kanban.$('.o_kanban_group:first .o_kanban_record').length, 3,
+ "first column should contain three records");
+
+ assert.verifySteps([
+ 'read_group', // initial read_group
+ '/web/dataset/search_read', // initial search_read (first column)
+ '/web/dataset/search_read', // initial search_read (second column)
+ 'load_views', // form view in quick create
+ 'default_get', // quick create
+ 'create', // should perform a create to create the record
+ 'read', // read the created record
+ 'load_views', // form view in quick create (is actually in cache)
+ 'default_get', // reopen the quick create automatically
+ ]);
+
+ kanban.destroy();
+ });
+
+ QUnit.test('quick create record with default values and onchanges', function (assert) {
+ assert.expect(11);
+
+ this.data.partner.fields.int_field.default = 4;
+ this.data.partner.onchanges = {
+ foo: function (obj) {
+ if (obj.foo) {
+ obj.int_field = 8;
+ }
+ },
+ };
+
+ var kanban = createView({
+ View: KanbanView,
+ model: 'partner',
+ data: this.data,
+ arch: '<kanban on_create="quick_create" quick_create_view="some_view_ref">' +
+ '<field name="bar"/>' +
+ '<templates><t t-name="kanban-box">' +
+ '<div><field name="foo"/></div>' +
+ '</t></templates></kanban>',
+ archs: {
+ 'partner,some_view_ref,form': '<form>' +
+ '<field name="foo"/>' +
+ '<field name="int_field"/>' +
+ '</form>',
+ },
+ groupBy: ['bar'],
+ mockRPC: function (route, args) {
+ assert.step(args.method || route);
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ // click on 'Create' -> should open the quick create in the first column
+ kanban.$buttons.find('.o-kanban-button-new').click();
+ var $quickCreate = kanban.$('.o_kanban_group:first .o_kanban_quick_create');
+
+ assert.strictEqual($quickCreate.length, 1,
+ "should have a quick create element in the first column");
+ assert.strictEqual($quickCreate.find('.o_field_widget[name=int_field]').val(), '4',
+ "default value should be set");
+
+ // fill the 'foo' field -> should trigger the onchange
+ $quickCreate.find('.o_field_widget[name=foo]').val('new partner').trigger('input');
+
+ assert.strictEqual($quickCreate.find('.o_field_widget[name=int_field]').val(), '8',
+ "onchange should have been triggered");
+
+ assert.verifySteps([
+ 'read_group', // initial read_group
+ '/web/dataset/search_read', // initial search_read (first column)
+ '/web/dataset/search_read', // initial search_read (second column)
+ 'load_views', // form view in quick create
+ 'default_get', // quick create
+ 'onchange', // default_get's onchange
+ 'onchange', // onchange due to 'foo' field change
+ ]);
+
+ kanban.destroy();
+ });
+
+ QUnit.test('quick create record with quick_create_view: modifiers', function (assert) {
+ assert.expect(3);
+
+ var kanban = createView({
+ View: KanbanView,
+ model: 'partner',
+ data: this.data,
+ arch: '<kanban quick_create_view="some_view_ref">' +
+ '<field name="bar"/>' +
+ '<templates><t t-name="kanban-box">' +
+ '<div><field name="foo"/></div>' +
+ '</t></templates></kanban>',
+ archs: {
+ 'partner,some_view_ref,form': '<form>' +
+ '<field name="foo" required="1"/>' +
+ '<field name="int_field" attrs=\'{"invisible": [["foo", "=", false]]}\'/>' +
+ '</form>',
+ },
+ groupBy: ['bar'],
+ });
+
+ // create a new record
+ kanban.$('.o_kanban_group:first .o_kanban_quick_add').click();
+ var $quickCreate = kanban.$('.o_kanban_group:first .o_kanban_quick_create');
+
+ assert.ok($quickCreate.find('.o_field_widget[name=foo]').hasClass('o_required_modifier'),
+ "foo field should be required");
+ assert.ok($quickCreate.find('.o_field_widget[name=int_field]').hasClass('o_invisible_modifier'),
+ "int_field should be invisible");
+
+ // fill 'foo' field
+ $quickCreate.find('.o_field_widget[name=foo]').val('new partner').trigger('input');
+
+ assert.notOk($quickCreate.find('.o_field_widget[name=int_field]').hasClass('o_invisible_modifier'),
+ "int_field should now be visible");
+
+ kanban.destroy();
+ });
+
+ QUnit.test('quick create record and change state in grouped mode', function (assert) {
assert.expect(1);
this.data.partner.fields.kanban_state = {
@@ -367,7 +727,7 @@ QUnit.module('Views', {
kanban.destroy();
});
- QUnit.test('quick create in grouped mode', function (assert) {
+ QUnit.test('quick create record: cancel and validate without using the buttons', function (assert) {
assert.expect(8);
var nbRecords = 4;
@@ -375,7 +735,7 @@ QUnit.module('Views', {
View: KanbanView,
model: 'partner',
data: this.data,
- arch: '<kanban class="o_kanban_test" on_create="quick_create">' +
+ arch: '<kanban on_create="quick_create">' +
'<field name="bar"/>' +
'<templates><t t-name="kanban-box">' +
'<div><field name="foo"/></div>' +
@@ -389,34 +749,41 @@ QUnit.module('Views', {
}
});
- // click to add an element and cancel the quick creation
+ // click to add an element and cancel the quick creation by pressing ESC
kanban.$('.o_kanban_header .o_kanban_quick_add i').first().click();
var $quickCreate = kanban.$('.o_kanban_quick_create');
assert.strictEqual($quickCreate.length, 1, "should have a quick create element");
- $quickCreate.find('input').trigger($.Event('keydown', {keyCode: $.ui.keyCode.ESCAPE}));
+ $quickCreate.find('input').trigger($.Event('keydown', {
+ keyCode: $.ui.keyCode.ESCAPE,
+ which: $.ui.keyCode.ESCAPE,
+ }));
assert.strictEqual(kanban.$('.o_kanban_quick_create').length, 0,
"should have destroyed the quick create element");
- //click to add and element and focus out the blank input, should cancel the quick creation
+ // click to add and element and click outside, should cancel the quick creation
kanban.$('.o_kanban_header .o_kanban_quick_add i').first().click();
- $quickCreate = kanban.$('.o_kanban_quick_create');
- $quickCreate.find('input').blur();
+ kanban.$('.o_kanban_group .o_kanban_record:first').click();
assert.strictEqual(kanban.$('.o_kanban_quick_create').length, 0,
- "Blur should have destroyed the quick create element");
+ "the quick create should be destroyed when the user clicks outside");
// click to really add an element
kanban.$('.o_kanban_header .o_kanban_quick_add i').first().click();
$quickCreate = kanban.$('.o_kanban_quick_create');
- $quickCreate.find('input').val('new partner');
- $quickCreate.find('input').blur();
+ $quickCreate.find('input').val('new partner').trigger('input');
- // When focus out the input containing value, should not delete the Quick Creation
+ // clicking outside should no longer destroy the quick create as it is dirty
+ kanban.$('.o_kanban_group .o_kanban_record:first').click();
assert.strictEqual(kanban.$('.o_kanban_quick_create').length, 1,
- "Blur should not have destroyed the quick create element");
+ "the quick create should not have been destroyed");
+
+ // confirm by pressing ENTER
nbRecords = 5;
- $quickCreate.find('button.o_kanban_add').click();
+ $quickCreate.find('input').trigger($.Event('keydown', {
+ keyCode: $.ui.keyCode.ENTER,
+ which: $.ui.keyCode.ENTER,
+ }));
assert.strictEqual(this.data.partner.records.length, 5,
"should have created a partner");
@@ -470,7 +837,128 @@ QUnit.module('Views', {
kanban.destroy();
});
- QUnit.test('quick create and edit in grouped mode', function (assert) {
+ QUnit.test('quick create record: cancel when not dirty', function (assert) {
+ assert.expect(9);
+
+ var kanban = createView({
+ View: KanbanView,
+ model: 'partner',
+ data: this.data,
+ arch: '<kanban>' +
+ '<field name="bar"/>' +
+ '<templates><t t-name="kanban-box">' +
+ '<div><field name="foo"/></div>' +
+ '</t></templates></kanban>',
+ groupBy: ['bar'],
+ });
+
+ assert.strictEqual(kanban.$('.o_kanban_group:first .o_kanban_record').length, 1,
+ "first column should contain one record");
+
+ // click to add an element
+ kanban.$('.o_kanban_header .o_kanban_quick_add i').first().click();
+ assert.strictEqual(kanban.$('.o_kanban_quick_create').length, 1,
+ "should have open the quick create widget");
+
+ // click again to add an element -> should have kept the quick create open
+ kanban.$('.o_kanban_header .o_kanban_quick_add i').first().click();
+ assert.strictEqual(kanban.$('.o_kanban_quick_create').length, 1,
+ "should have kept the quick create open");
+
+ // click outside: should remove the quick create
+ kanban.$('.o_kanban_group .o_kanban_record:first').click();
+ assert.strictEqual(kanban.$('.o_kanban_quick_create').length, 0,
+ "the quick create should not have been destroyed");
+
+ // click to reopen the quick create
+ kanban.$('.o_kanban_header .o_kanban_quick_add i').first().click();
+ assert.strictEqual(kanban.$('.o_kanban_quick_create').length, 1,
+ "should have open the quick create widget");
+
+ // press ESC: should remove the quick create
+ kanban.$('.o_kanban_quick_create input').trigger($.Event('keydown', {
+ keyCode: $.ui.keyCode.ESCAPE,
+ which: $.ui.keyCode.ESCAPE,
+ }));
+ assert.strictEqual(kanban.$('.o_kanban_quick_create').length, 0,
+ "quick create widget should have been removed");
+
+ // click to reopen the quick create
+ kanban.$('.o_kanban_header .o_kanban_quick_add i').first().click();
+ assert.strictEqual(kanban.$('.o_kanban_quick_create').length, 1,
+ "should have open the quick create widget");
+
+ // click on 'Discard': should remove the quick create
+ kanban.$('.o_kanban_header .o_kanban_quick_add i').first().click();
+ kanban.$('.o_kanban_group .o_kanban_record:first').click();
+ assert.strictEqual(kanban.$('.o_kanban_quick_create').length, 0,
+ "the quick create should be destroyed when the user clicks outside");
+
+ assert.strictEqual(kanban.$('.o_kanban_group:first .o_kanban_record').length, 1,
+ "first column should still contain one record");
+
+ kanban.destroy();
+ });
+
+ QUnit.test('quick create record: cancel when dirty', function (assert) {
+ assert.expect(7);
+
+ var kanban = createView({
+ View: KanbanView,
+ model: 'partner',
+ data: this.data,
+ arch: '<kanban>' +
+ '<field name="bar"/>' +
+ '<templates><t t-name="kanban-box">' +
+ '<div><field name="foo"/></div>' +
+ '</t></templates></kanban>',
+ groupBy: ['bar'],
+ });
+
+ assert.strictEqual(kanban.$('.o_kanban_group:first .o_kanban_record').length, 1,
+ "first column should contain one record");
+
+ // click to add an element and edit it
+ kanban.$('.o_kanban_header .o_kanban_quick_add i').first().click();
+ assert.strictEqual(kanban.$('.o_kanban_quick_create').length, 1,
+ "should have open the quick create widget");
+
+ var $quickCreate = kanban.$('.o_kanban_quick_create');
+ $quickCreate.find('input').val('some value').trigger('input');
+
+ // click outside: should not remove the quick create
+ kanban.$('.o_kanban_group .o_kanban_record:first').click();
+ assert.strictEqual(kanban.$('.o_kanban_quick_create').length, 1,
+ "the quick create should not have been destroyed");
+
+ // press ESC: should remove the quick create
+ $quickCreate.find('input').trigger($.Event('keydown', {
+ keyCode: $.ui.keyCode.ESCAPE,
+ which: $.ui.keyCode.ESCAPE,
+ }));
+ assert.strictEqual(kanban.$('.o_kanban_quick_create').length, 0,
+ "quick create widget should have been removed");
+
+ // click to reopen quick create and edit it
+ kanban.$('.o_kanban_header .o_kanban_quick_add i').first().click();
+ assert.strictEqual(kanban.$('.o_kanban_quick_create').length, 1,
+ "should have open the quick create widget");
+
+ $quickCreate = kanban.$('.o_kanban_quick_create');
+ $quickCreate.find('input').val('some value').trigger('input');
+
+ // click on 'Discard': should remove the quick create
+ kanban.$('.o_kanban_quick_create .o_kanban_cancel').click();
+ assert.strictEqual(kanban.$('.o_kanban_quick_create').length, 0,
+ "the quick create should be destroyed when the user clicks outside");
+
+ assert.strictEqual(kanban.$('.o_kanban_group:first .o_kanban_record').length, 1,
+ "first column should still contain one record");
+
+ kanban.destroy();
+ });
+
+ QUnit.test('quick create record and edit in grouped mode', function (assert) {
assert.expect(6);
var newRecordID;
@@ -510,7 +998,7 @@ QUnit.module('Views', {
var $quickCreate = kanban.$('.o_kanban_quick_create');
kanban.$('.o_kanban_header .o_kanban_quick_add i').first().click();
$quickCreate = kanban.$('.o_kanban_quick_create');
- $quickCreate.find('input').val('new partner');
+ $quickCreate.find('input').val('new partner').trigger('input');
$quickCreate.find('button.o_kanban_edit').click();
assert.strictEqual(this.data.partner.records.length, 5,
@@ -549,7 +1037,8 @@ QUnit.module('Views', {
kanban.$('.o_kanban_quick_create input')
.val('new partner 1')
- .trigger($.Event('keypress', {
+ .trigger('input')
+ .trigger($.Event('keydown', {
which: $.ui.keyCode.ENTER,
keyCode: $.ui.keyCode.ENTER,
}));
@@ -562,7 +1051,8 @@ QUnit.module('Views', {
// create a second element in a row
kanban.$('.o_kanban_quick_create input')
.val('new partner 2')
- .trigger($.Event('keypress', {
+ .trigger('input')
+ .trigger($.Event('keydown', {
which: $.ui.keyCode.ENTER,
keyCode: $.ui.keyCode.ENTER,
}));
@@ -621,7 +1111,8 @@ QUnit.module('Views', {
def = $.Deferred();
kanban.$('.o_kanban_quick_create input')
.val('new partner 1')
- .trigger($.Event('keypress', {
+ .trigger('input')
+ .trigger($.Event('keydown', {
which: $.ui.keyCode.ENTER,
keyCode: $.ui.keyCode.ENTER,
}));
@@ -629,7 +1120,8 @@ QUnit.module('Views', {
def = null;
kanban.$('.o_kanban_quick_create input')
.val('new partner 2')
- .trigger($.Event('keypress', {
+ .trigger('input')
+ .trigger($.Event('keydown', {
which: $.ui.keyCode.ENTER,
keyCode: $.ui.keyCode.ENTER,
}));
@@ -649,7 +1141,7 @@ QUnit.module('Views', {
kanban.destroy();
});
- QUnit.test('quick create fail in grouped', function (assert) {
+ QUnit.test('quick create record fail in grouped', function (assert) {
assert.expect(7);
var kanban = createView({
@@ -690,7 +1182,11 @@ QUnit.module('Views', {
kanban.$('.o_kanban_quick_create input')
.val('test')
- .trigger($.Event('keypress', {keyCode: $.ui.keyCode.ENTER}));
+ .trigger('input')
+ .trigger($.Event('keydown', {
+ keyCode: $.ui.keyCode.ENTER,
+ which: $.ui.keyCode.ENTER,
+ }));
assert.strictEqual($('.modal .o_form_view.o_form_editable').length, 1,
"a form view dialog should have been opened (in edit)");
@@ -1181,37 +1677,6 @@ QUnit.module('Views', {
kanban.destroy();
});
- QUnit.test('quick create record & column in grouped on m2o', function (assert) {
- assert.expect(2);
-
- var kanban = createView({
- View: KanbanView,
- model: 'partner',
- data: this.data,
- arch: '<kanban on_create="quick_create">' +
- '<field name="product_id"/>' +
- '<templates><t t-name="kanban-box">' +
- '<div><field name="foo"/></div>' +
- '</t></templates>' +
- '</kanban>',
- groupBy: ['product_id'],
- });
- kanban.$('.o_kanban_group:first .o_kanban_quick_add').click();
- var $quickCreate = kanban.$('.o_kanban_quick_create');
- $quickCreate.find('input').val('new partner');
- $quickCreate.find('button.o_kanban_add').click();
- assert.strictEqual(this.data.partner.records.length, 5,
- "should have created a partner");
-
- kanban.$('.o_column_quick_create').click();
- kanban.$('.o_column_quick_create input').val('new column');
- kanban.$('.o_column_quick_create button.o_kanban_add').click();
-
- assert.strictEqual(kanban.$('.o_kanban_group:last span:contains(new column)').length, 1,
- "the last column should be the newly created one");
- kanban.destroy();
- });
-
QUnit.test('delete a column in grouped on m2o', function (assert) {
assert.expect(28);
@@ -2172,7 +2637,7 @@ QUnit.module('Views', {
kanban.$('.o_kanban_group:eq(1) .o_kanban_quick_add i').click();
var $quickCreate = kanban.$('.o_kanban_group:eq(1) .o_kanban_quick_create');
- $quickCreate.find('input').val('new partner');
+ $quickCreate.find('input').val('new partner').trigger('input');
$quickCreate.find('button.o_kanban_add').click();
assert.strictEqual(kanban.$('.o_kanban_group:eq(0) .o_kanban_record').length, 0,
@@ -2232,12 +2697,12 @@ QUnit.module('Views', {
kanban.$('.o_kanban_group:eq(0) .o_kanban_quick_add i').click();
var $quickCreate = kanban.$('.o_kanban_group:eq(0) .o_kanban_quick_create');
- $quickCreate.find('input').val('record1');
+ $quickCreate.find('input').val('record1').trigger('input');
$quickCreate.find('button.o_kanban_add').click();
kanban.$('.o_kanban_group:eq(0) .o_kanban_quick_add i').click();
$quickCreate = kanban.$('.o_kanban_group:eq(0) .o_kanban_quick_create');
- $quickCreate.find('input').val('record2');
+ $quickCreate.find('input').val('record2').trigger('input');
$quickCreate.find('button.o_kanban_add').click();
assert.strictEqual(kanban.$('.o_kanban_group:eq(0) .o_kanban_record').length, 2,
@@ -2289,7 +2754,7 @@ QUnit.module('Views', {
// add a new column
kanban.$('.o_kanban_group:first .o_kanban_quick_add').click();
var $quickCreate = kanban.$('.o_kanban_quick_create');
- $quickCreate.find('input').val('new partner');
+ $quickCreate.find('input').val('new partner').trigger('input');
$quickCreate.find('button.o_kanban_add').click();
assert.strictEqual(this.data.partner.records.length, 5,
"should have created a 'new partner' column");
@@ -2414,11 +2879,11 @@ QUnit.module('Views', {
groupBy: ['bar'],
});
- var initialCount = parseInt(kanban.$('.o_kanban_counter_side').eq(1).text());
- kanban.$('.o_kanban_quick_add').eq(1).click();
- kanban.$('.o_input').val('Test');
+ var initialCount = parseInt(kanban.$('.o_kanban_counter_side:first').text());
+ kanban.$('.o_kanban_quick_add:first').click();
+ kanban.$('.o_kanban_quick_create input').val('Test').trigger('input');
kanban.$('.o_kanban_add').click();
- var lastCount = parseInt(kanban.$('.o_kanban_counter_side').eq(1).text());
+ var lastCount = parseInt(kanban.$('.o_kanban_counter_side:first').text());
assert.strictEqual(lastCount, initialCount + 1,
"kanban counters should have updated on quick create");
@@ -2455,6 +2920,45 @@ QUnit.module('Views', {
kanban.destroy();
});
+ QUnit.test('column progressbars on quick create with quick_create_view are updated', function (assert) {
+ assert.expect(1);
+
+ var kanban = createView({
+ View: KanbanView,
+ model: 'partner',
+ data: this.data,
+ arch: '<kanban on_create="quick_create" quick_create_view="some_view_ref">' +
+ '<field name="int_field"/>' +
+ '<progressbar field="foo" colors=\'{"yop": "success", "gnap": "warning", "blip": "danger"}\' sum_field="int_field"/>' +
+ '<templates><t t-name="kanban-box">' +
+ '<div>' +
+ '<field name="name"/>' +
+ '</div>' +
+ '</t></templates>' +
+ '</kanban>',
+ archs: {
+ 'partner,some_view_ref,form': '<form>' +
+ '<field name="int_field"/>' +
+ '</form>',
+ },
+ groupBy: ['bar'],
+ });
+
+ var initialCount = parseInt(kanban.$('.o_kanban_counter_side:first').text());
+
+ // click on 'Create', fill the quick create and validate
+ kanban.$buttons.find('.o-kanban-button-new').click();
+ var $quickCreate = kanban.$('.o_kanban_group:first .o_kanban_quick_create');
+ $quickCreate.find('.o_field_widget[name=int_field]').val('44').trigger('input');
+ $quickCreate.find('button.o_kanban_add').click();
+
+ var lastCount = parseInt(kanban.$('.o_kanban_counter_side:first').text());
+ assert.strictEqual(lastCount, initialCount + 44,
+ "kanban counters should have been updated on quick create");
+
+ kanban.destroy();
+ });
+
QUnit.test('keep adding quickcreate in first column after a record from this column was moved', function (assert) {
assert.expect(2);
diff --git a/addons/web/views/webclient_templates.xml b/addons/web/views/webclient_templates.xml
index f6e337a8731..44836526157 100644
--- a/addons/web/views/webclient_templates.xml
+++ b/addons/web/views/webclient_templates.xml
@@ -221,6 +221,7 @@
<script type="text/javascript" src="/web/static/src/js/views/kanban/kanban_renderer.js"></script>
<script type="text/javascript" src="/web/static/src/js/views/kanban/kanban_renderer_mobile.js"></script>
<script type="text/javascript" src="/web/static/src/js/views/kanban/kanban_view.js"></script>
+ <script type="text/javascript" src="/web/static/src/js/views/kanban/quick_create_form_view.js"></script>
<script type="text/javascript" src="/web/static/src/js/views/list/list_editable_renderer.js"></script>
<script type="text/javascript" src="/web/static/src/js/views/list/list_renderer.js"></script>
<script type="text/javascript" src="/web/static/src/js/views/list/list_view.js"></script>