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); } @@ -120,6 +120,15 @@ var BasicController = AbstractController.extend(FieldManagerMixin, { getSelectedIds: function () { 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 */ 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). @@ -93,6 +94,43 @@ var KanbanModel = BasicModel.extend({ return newGroup.id; }); }, + /** + * 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Ċ§`. * 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,17 +6,121 @@ 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', + }, + + /** + * @override + * @param {Widget} parent + * @param {Object} options + * @param {Object} options.context + * @param {string|null} options.formViewRef + * @param {string} options.model + */ + init: function (parent, options) { + this._super.apply(this, arguments); + this.context = options.context; + this.formViewRef = options.formViewRef; + this.model = options.model; + }, + /** + * Loads the form fieldsView (if not provided), instantiates the form view + * and starts the form controller. + * + * @override + */ + 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 = '
' + + '' + + ''; + 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); + }, + /** + * @override + */ + start: function () { + this.$el.append(this.controller.$el); + this.$el.append(qweb.render('KanbanView.RecordQuickCreate.buttons')); + + // focus the first field + this.controller.autofocus(); + + // destroy the quick create when the user clicks outside + core.bus.on('click', this, this._onWindowClicked); + + return this._super.apply(this, arguments); + }, + /** + * Called when the quick create is appended into the DOM. + */ + on_attach_callback: function () { + if (this.controller) { + this.controller.autofocus(); + } + }, + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + /** + * Cancels the quick creation if the record isn't dirty, i.e. if no changes + * have been made yet + * + * @private + * @returns {Deferred} + */ + cancel: function () { + var self = this; + return this.controller.commitChanges().then(function () { + if (!self.controller.isDirty()) { + self._cancel(); + } + }); }, //-------------------------------------------------------------------------- @@ -24,37 +128,210 @@ var AbstractQuickCreate = Widget.extend({ //-------------------------------------------------------------------------- /** - * Clears the input value and notify the environment that a quick create - * has been done - * + * @override * @private - * @param {Object} [options] dict of options to pass to call to '_notifyAdd' + * @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} */ _add: function (options) { - var value = this.$input.val(); - this.$input.val(''); - if (/^\s*$/.test(value)) { + 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(), + }); + } + }); + }, + /** + * Notifies the environment that the quick creation must be cancelled + * + * @private + * @returns {Deferred} + */ + _cancel: function () { + this.trigger_up('cancel_quick_create'); + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * @private + * @param {OdooEvent} ev + */ + _onAdd: function (ev) { + ev.stopPropagation(); + this._add(); + }, + /** + * @private + * @param {MouseEvent} ev + */ + _onAddClicked: function (ev) { + ev.stopPropagation(); + this._add(); + }, + /** + * @private + * @param {OdooEvent} ev + */ + _onCancel: function (ev) { + ev.stopPropagation(); + this._cancel(); + }, + /** + * @private + * @param {MouseEvent} ev + */ + _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} ev + */ + _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; } - this._notifyAdd(value, options); + + // 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 = Widget.extend({ + template: 'KanbanView.ColumnQuickCreate', + events: { + 'click': 'toggleFold', + 'click input': '_onInputClicked', + 'click .o_kanban_add': '_onAddClicked', + 'focusout': '_onFocusout', + 'keydown': '_onKeydown', + 'keypress input': '_onKeypress', + 'mousedown .o_kanban_add': '_onMousedown', + }, + + /** + * @override + */ + init: function () { + this._super.apply(this, arguments); + this.folded = true; + }, + /** + * @override + */ + start: function () { + this.$header = this.$('.o_column_header'); + this.$quick_create = this.$('.o_kanban_quick_create'); + this.$input = this.$('input'); + return this._super.apply(this, arguments); + }, + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + /** + * Folds/unfolds the Column quick create widget + */ + toggleFold: function () { + this.folded = !this.folded; + this._update(); + if (!this.folded) { + this.$input.focus(); + this.trigger_up('scrollTo', {selector: '.o_column_quick_create'}); + } + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * Clears the input value and notify the environment to create a column + * + * @private + */ + _add: function () { + var value = this.$input.val().trim(); + if (!value.length) { + return; + } + this.$input.val(''); + this.trigger_up('quick_create_add_column', {value: value}); this.$input.focus(); }, /** - * Should implement what to do when the quick creation has been cancelled + * Cancels the quick creation * - * @abstract * @private */ _cancel: function () { + this.folded = true; + this.$input.val(''); + this._update(); }, /** - * Should implement what to do to notify the environment that a quick - * create has been done + * Updates the rendering according to the current state (folded/unfolded) * - * @abstract * @private */ - _notifyAdd: function (name, options) { + _update: function () { + this.$header.toggle(this.folded); + this.$quick_create.toggle(!this.folded); }, //-------------------------------------------------------------------------- @@ -70,21 +347,26 @@ var AbstractQuickCreate = Widget.extend({ this._add(); }, /** + * Cancels the quick creation when the input loses the focus + * * @private */ - _onCancelClicked: function () { + _onFocusout: function () { + var hasFocus = this.$(':focus').length > 0; + if (hasFocus) { + return; + } this._cancel(); }, /** - * Cancels quick creation on focusout input event + * Stops the propagation of the event to prevent the quick create from + * toggling when the user clicks in the input * * @private - * @param {KeyEvent} ev + * @param {MouseEvent} event */ - _onFocusOut: function (ev) { - if (!this.$input.val()) { - this._cancel(); - } + _onInputClicked: function (event) { + event.stopPropagation(); }, /** * Cancels quick creation on escape keydown event @@ -111,11 +393,10 @@ var AbstractQuickCreate = Widget.extend({ /** * 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. + * 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 - * relevant buttons (Add and Cancel) entirely by preventing the browser's - * default mousedown handler. + * Add button entirely by preventing the browser's default mousedown handler. * * @private * @param {MouseEvent} event @@ -125,202 +406,6 @@ var AbstractQuickCreate = Widget.extend({ }, }); -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 - */ - start: function () { - this.$el.css({width: this.width}); - this.$input = this.$('input'); - this._addDefaultName(); - this.$input.focus(); - return this._super.apply(this, arguments); - }, - - //-------------------------------------------------------------------------- - // Private - //-------------------------------------------------------------------------- - - /** - * Set the default value for the record name. - * - * @private - */ - _addDefaultName: function () { - if (this.defaultName) { - this.$input.val(this.defaultName); - } - }, - /** - * Triggers up an event to cancel the quick creation - * - * @override - * @private - */ - _cancel: function () { - this.trigger_up('cancel_quick_create'); - }, - /** - * 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) - */ - _notifyAdd: function (value, options) { - this.trigger_up('quick_create_add_record', { - value: value, - openRecord: options && options.openRecord || false, - }); - }, - - //-------------------------------------------------------------------------- - // Handlers - //-------------------------------------------------------------------------- - - /** - * Add the default value for the record name again after creating - * the previous record. - * - * @override - * @private - */ - _onAddClicked: function () { - this._super.apply(this, arguments); - this._addDefaultName(); - }, - /** - * Validates the quick creation and directly opens the record in a form - * view in edit mode. - * - * @private - * @param {MouseEvent} event - */ - _onEditClicked: function (event) { - event.stopPropagation(); - this._add({openRecord: true}); - }, -}); - -var ColumnQuickCreate = AbstractQuickCreate.extend({ - template: 'KanbanView.ColumnQuickCreate', - events: _.extend({}, AbstractQuickCreate.prototype.events, { - 'click': 'toggleFold', - 'click input': '_onInputClicked', - 'focusout': '_onFocusout', - }), - /** - * @override - */ - init: function () { - this._super.apply(this, arguments); - this.folded = true; - }, - /** - * @override - */ - start: function () { - this.$header = this.$('.o_column_header'); - this.$quick_create = this.$('.o_kanban_quick_create'); - this.$input = this.$('input'); - return this._super.apply(this, arguments); - }, - - //-------------------------------------------------------------------------- - // Public - //-------------------------------------------------------------------------- - - /** - * Toggle fold/unfold the Column quick create widget - */ - toggleFold: function () { - this.folded = !this.folded; - this._update(); - if (!this.folded) { - this.$input.focus(); - this.trigger_up('scrollTo', {selector: '.o_column_quick_create'}); - } - }, - - //-------------------------------------------------------------------------- - // Private - //-------------------------------------------------------------------------- - - /** - * @override - * @private - */ - _cancel: function () { - this.folded = true; - this.$input.val(''); - this._update(); - }, - /** - * Triggers up an event to quick create a column with the given value - * - * @override - * @private - * @param {string} value - */ - _notifyAdd: function (value) { - this.trigger_up('quick_create_add_column', {value: value}); - }, - /** - * Updates the rendering according to the current state - * - * @private - */ - _update: function () { - this.$header.toggle(this.folded); - this.$quick_create.toggle(!this.folded); - }, - - //-------------------------------------------------------------------------- - // Handlers - //-------------------------------------------------------------------------- - - /** - * @private - */ - _onFocusout: function () { - var hasFocus = this.$(':focus').length > 0; - if (hasFocus) { - return; - } - this._cancel(); - }, - /** - * Stops the propagation of the event to prevent the quick create from - * toggling when the user clicks in the input - * - * @private - * @param {MouseEvent} event - */ - _onInputClicked: function (event) { - event.stopPropagation(); - }, -}); - return { RecordQuickCreate: RecordQuickCreate, ColumnQuickCreate: ColumnQuickCreate, 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; }, @@ -368,6 +383,16 @@ var KanbanRenderer = BasicRenderer.extend({ // Handlers //-------------------------------------------------------------------------- + /** + * 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 @@ - -
- + +
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: '' + + '' + + '' + + '
' + + '
', + 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: '' + + '' + + '' + + '
' + + '
', + archs: { + 'partner,some_view_ref,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: '' + + '' + + '' + + '
' + + '
' + + '
', + 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: '' + + '' + + '' + + '
' + + '
', + archs: { + 'partner,some_view_ref,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: '' + + '' + + '' + + '
' + + '
', + archs: { + 'partner,some_view_ref,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: '' + + '' + + '' + + '
' + + '
', + archs: { + 'partner,some_view_ref,form': '
' + + '' + + '' + + arch: '' + '' + '' + '
' + @@ -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: '' + + '' + + '' + + '
' + + '
', + 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: '' + + '' + + '' + + '
' + + '
', + 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: '' + - '' + - '' + - '
' + - '
' + - '
', - 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: '' + + '' + + '' + + '' + + '
' + + '' + + '
' + + '
' + + '
', + archs: { + 'partner,some_view_ref,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 @@ +