diff options
| author | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
|---|---|---|
| committer | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
| commit | 3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch) | |
| tree | a44932296ef4a9b71d5f010906253d8c53727726 /addons/web/static/src/js/views/view_dialogs.js | |
| parent | 0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff) | |
initial commit 2
Diffstat (limited to 'addons/web/static/src/js/views/view_dialogs.js')
| -rw-r--r-- | addons/web/static/src/js/views/view_dialogs.js | 484 |
1 files changed, 484 insertions, 0 deletions
diff --git a/addons/web/static/src/js/views/view_dialogs.js b/addons/web/static/src/js/views/view_dialogs.js new file mode 100644 index 00000000..21004ed9 --- /dev/null +++ b/addons/web/static/src/js/views/view_dialogs.js @@ -0,0 +1,484 @@ +odoo.define('web.view_dialogs', function (require) { +"use strict"; + +var config = require('web.config'); +var core = require('web.core'); +var Dialog = require('web.Dialog'); +var dom = require('web.dom'); +var view_registry = require('web.view_registry'); +var select_create_controllers_registry = require('web.select_create_controllers_registry'); + +var _t = core._t; + +/** + * Class with everything which is common between FormViewDialog and + * SelectCreateDialog. + */ +var ViewDialog = Dialog.extend({ + custom_events: _.extend({}, Dialog.prototype.custom_events, { + push_state: '_onPushState', + }), + /** + * @constructor + * @param {Widget} parent + * @param {options} [options] + * @param {string} [options.dialogClass=o_act_window] + * @param {string} [options.res_model] the model of the record(s) to open + * @param {any[]} [options.domain] + * @param {Object} [options.context] + */ + init: function (parent, options) { + options = options || {}; + options.fullscreen = config.device.isMobile; + options.dialogClass = options.dialogClass || '' + ' o_act_window'; + + this._super(parent, $.extend(true, {}, options)); + + this.res_model = options.res_model || null; + this.domain = options.domain || []; + this.context = options.context || {}; + this.options = _.extend(this.options || {}, options || {}); + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * We stop all push_state events from bubbling up. It would be weird to + * change the url because a dialog opened. + * + * @param {OdooEvent} event + */ + _onPushState: function (event) { + event.stopPropagation(); + }, +}); + +/** + * Create and edit dialog (displays a form view record and leave once saved) + */ +var FormViewDialog = ViewDialog.extend({ + /** + * @param {Widget} parent + * @param {Object} [options] + * @param {string} [options.parentID] the id of the parent record. It is + * useful for situations such as a one2many opened in a form view dialog. + * In that case, we want to be able to properly evaluate domains with the + * 'parent' key. + * @param {integer} [options.res_id] the id of the record to open + * @param {Object} [options.form_view_options] dict of options to pass to + * the Form View @todo: make it work + * @param {Object} [options.fields_view] optional form fields_view + * @param {boolean} [options.readonly=false] only applicable when not in + * creation mode + * @param {boolean} [options.deletable=false] whether or not the record can + * be deleted + * @param {boolean} [options.disable_multiple_selection=false] set to true + * to remove the possibility to create several records in a row + * @param {function} [options.on_saved] callback executed after saving a + * record. It will be called with the record data, and a boolean which + * indicates if something was changed + * @param {function} [options.on_remove] callback executed when the user + * clicks on the 'Remove' button + * @param {BasicModel} [options.model] if given, it will be used instead of + * a new form view model + * @param {string} [options.recordID] if given, the model has to be given as + * well, and in that case, it will be used without loading anything. + * @param {boolean} [options.shouldSaveLocally] if true, the view dialog + * will save locally instead of actually saving (useful for one2manys) + * @param {function} [options._createContext] function to get context for name field + * useful for many2many_tags widget where we want to removed default_name field + * context. + */ + init: function (parent, options) { + var self = this; + options = options || {}; + + this.res_id = options.res_id || null; + this.on_saved = options.on_saved || (function () {}); + this.on_remove = options.on_remove || (function () {}); + this.context = options.context; + this._createContext = options._createContext; + this.model = options.model; + this.parentID = options.parentID; + this.recordID = options.recordID; + this.shouldSaveLocally = options.shouldSaveLocally; + this.readonly = options.readonly; + this.deletable = options.deletable; + this.disable_multiple_selection = options.disable_multiple_selection; + var oBtnRemove = 'o_btn_remove'; + + var multi_select = !_.isNumber(options.res_id) && !options.disable_multiple_selection; + var readonly = _.isNumber(options.res_id) && options.readonly; + + if (!options.buttons) { + options.buttons = [{ + text: options.close_text || (readonly ? _t("Close") : _t("Discard")), + classes: "btn-secondary o_form_button_cancel", + close: true, + click: function () { + if (!readonly) { + self.form_view.model.discardChanges(self.form_view.handle, { + rollback: self.shouldSaveLocally, + }); + } + }, + }]; + + if (!readonly) { + options.buttons.unshift({ + text: options.save_text || (multi_select ? _t("Save & Close") : _t("Save")), + classes: "btn-primary", + click: function () { + self._save().then(self.close.bind(self)); + } + }); + + if (multi_select) { + options.buttons.splice(1, 0, { + text: _t("Save & New"), + classes: "btn-primary", + click: function () { + self._save() + .then(function () { + // reset default name field from context when Save & New is clicked, pass additional + // context so that when getContext is called additional context resets it + var additionalContext = self._createContext && self._createContext(false) || {}; + self.form_view.createRecord(self.parentID, additionalContext); + }) + .then(function () { + if (!self.deletable) { + return; + } + self.deletable = false; + self.buttons = self.buttons.filter(function (button) { + return button.classes.split(' ').indexOf(oBtnRemove) < 0; + }); + self.set_buttons(self.buttons); + self.set_title(_t("Create ") + _.str.strRight(self.title, _t("Open: "))); + }); + }, + }); + } + + var multi = options.disable_multiple_selection; + if (!multi && this.deletable) { + this._setRemoveButtonOption(options, oBtnRemove); + } + } + } + this._super(parent, options); + }, + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + /** + * Open the form view dialog. It is necessarily asynchronous, but this + * method returns immediately. + * + * @returns {FormViewDialog} this instance + */ + open: function () { + var self = this; + var _super = this._super.bind(this); + var FormView = view_registry.get('form'); + var fields_view_def; + if (this.options.fields_view) { + fields_view_def = Promise.resolve(this.options.fields_view); + } else { + fields_view_def = this.loadFieldView(this.res_model, this.context, this.options.view_id, 'form'); + } + + fields_view_def.then(function (viewInfo) { + var refinedContext = _.pick(self.context, function (value, key) { + return key.indexOf('_view_ref') === -1; + }); + var formview = new FormView(viewInfo, { + modelName: self.res_model, + context: refinedContext, + ids: self.res_id ? [self.res_id] : [], + currentId: self.res_id || undefined, + index: 0, + mode: self.res_id && self.options.readonly ? 'readonly' : 'edit', + footerToButtons: true, + default_buttons: false, + withControlPanel: false, + model: self.model, + parentID: self.parentID, + recordID: self.recordID, + isFromFormViewDialog: true, + }); + return formview.getController(self); + }).then(function (formView) { + self.form_view = formView; + var fragment = document.createDocumentFragment(); + if (self.recordID && self.shouldSaveLocally) { + self.model.save(self.recordID, {savePoint: true}); + } + return self.form_view.appendTo(fragment) + .then(function () { + self.opened().then(function () { + var $buttons = $('<div>'); + self.form_view.renderButtons($buttons); + if ($buttons.children().length) { + self.$footer.empty().append($buttons.contents()); + } + dom.append(self.$el, fragment, { + callbacks: [{widget: self.form_view}], + in_DOM: true, + }); + self.form_view.updateButtons(); + }); + return _super(); + }); + }); + + return this; + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * @override + */ + _focusOnClose: function() { + var isFocusSet = false; + this.trigger_up('form_dialog_discarded', { + callback: function (isFocused) { + isFocusSet = isFocused; + }, + }); + return isFocusSet; + }, + + /** + * @private + */ + _remove: function () { + return Promise.resolve(this.on_remove()); + }, + + /** + * @private + * @returns {Promise} + */ + _save: function () { + var self = this; + return this.form_view.saveRecord(this.form_view.handle, { + stayInEdit: true, + reload: false, + savePoint: this.shouldSaveLocally, + viewType: 'form', + }).then(function (changedFields) { + // record might have been changed by the save (e.g. if this was a new record, it has an + // id now), so don't re-use the copy obtained before the save + var record = self.form_view.model.get(self.form_view.handle); + return self.on_saved(record, !!changedFields.length); + }); + }, + + /** + * Set the "remove" button into the options' buttons list + * + * @private + * @param {Object} options The options object to modify + * @param {string} btnClasses The classes for the remove button + */ + _setRemoveButtonOption(options, btnClasses) { + const self = this; + options.buttons.push({ + text: _t("Remove"), + classes: 'btn-secondary ' + btnClasses, + click: function() { + self._remove().then(self.close.bind(self)); + } + }); + }, +}); + +/** + * Search dialog (displays a list of records and permits to create a new one by switching to a form view) + */ +var SelectCreateDialog = ViewDialog.extend({ + custom_events: _.extend({}, ViewDialog.prototype.custom_events, { + select_record: function (event) { + if (!this.options.readonly) { + this.on_selected([event.data]); + this.close(); + } + }, + selection_changed: function (event) { + event.stopPropagation(); + this.$footer.find(".o_select_button").prop('disabled', !event.data.selection.length); + }, + }), + + /** + * options: + * - initial_ids + * - initial_view: form or search (default search) + * - list_view_options: dict of options to pass to the List View + * - on_selected: optional callback to execute when records are selected + * - disable_multiple_selection: true to allow create/select multiple records + * - dynamicFilters: filters to add to the searchview + */ + init: function () { + this._super.apply(this, arguments); + _.defaults(this.options, { initial_view: 'search' }); + this.on_selected = this.options.on_selected || (function () {}); + this.on_closed = this.options.on_closed || (function () {}); + this.initialIDs = this.options.initial_ids; + this.viewType = 'list'; + }, + + open: function () { + if (this.options.initial_view !== "search") { + return this.create_edit_record(); + } + var self = this; + var _super = this._super.bind(this); + var viewRefID = this.viewType === 'kanban' ? + (this.options.kanban_view_ref && JSON.parse(this.options.kanban_view_ref) || false) : false; + return this.loadViews(this.res_model, this.context, [[viewRefID, this.viewType], [false, 'search']], {load_filters: true}) + .then(this.setup.bind(this)) + .then(function (fragment) { + self.opened().then(function () { + dom.append(self.$el, fragment, { + callbacks: [{widget: self.viewController}], + in_DOM: true, + }); + self.set_buttons(self.__buttons); + }); + return _super(); + }); + }, + + setup: function (fieldsViews) { + var self = this; + var fragment = document.createDocumentFragment(); + + var domain = this.domain; + if (this.initialIDs) { + domain = domain.concat([['id', 'in', this.initialIDs]]); + } + var ViewClass = view_registry.get(this.viewType); + var viewOptions = {}; + var selectCreateController; + if (this.viewType === 'list') { // add listview specific options + _.extend(viewOptions, { + hasSelectors: !this.options.disable_multiple_selection, + readonly: true, + + }, this.options.list_view_options); + selectCreateController = select_create_controllers_registry.SelectCreateListController; + } + if (this.viewType === 'kanban') { + _.extend(viewOptions, { + noDefaultGroupby: true, + selectionMode: this.options.selectionMode || false, + }); + selectCreateController = select_create_controllers_registry.SelectCreateKanbanController; + } + var view = new ViewClass(fieldsViews[this.viewType], _.extend(viewOptions, { + action: { + controlPanelFieldsView: fieldsViews.search, + help: _.str.sprintf("<p>%s</p>", _t("No records found!")), + }, + action_buttons: false, + dynamicFilters: this.options.dynamicFilters, + context: this.context, + domain: domain, + modelName: this.res_model, + withBreadcrumbs: false, + withSearchPanel: false, + })); + view.setController(selectCreateController); + return view.getController(this).then(function (controller) { + self.viewController = controller; + // render the footer buttons + self._prepareButtons(); + return self.viewController.appendTo(fragment); + }).then(function () { + return fragment; + }); + }, + close: function () { + this._super.apply(this, arguments); + this.on_closed(); + }, + create_edit_record: function () { + var self = this; + var dialog = new FormViewDialog(this, _.extend({}, this.options, { + on_saved: function (record) { + var values = [{ + id: record.res_id, + display_name: record.data.display_name || record.data.name, + }]; + self.on_selected(values); + }, + })).open(); + dialog.on('closed', this, this.close); + return dialog; + }, + /** + * @override + */ + _focusOnClose: function() { + var isFocusSet = false; + this.trigger_up('form_dialog_discarded', { + callback: function (isFocused) { + isFocusSet = isFocused; + }, + }); + return isFocusSet; + }, + /** + * prepare buttons for dialog footer based on options + * + * @private + */ + _prepareButtons: function () { + this.__buttons = [{ + text: _t("Cancel"), + classes: 'btn-secondary o_form_button_cancel', + close: true, + }]; + if (!this.options.no_create) { + this.__buttons.unshift({ + text: _t("Create"), + classes: 'btn-primary', + click: this.create_edit_record.bind(this) + }); + } + if (!this.options.disable_multiple_selection) { + this.__buttons.unshift({ + text: _t("Select"), + classes: 'btn-primary o_select_button', + disabled: true, + close: true, + click: function () { + var records = this.viewController.getSelectedRecords(); + var values = _.map(records, function (record) { + return { + id: record.res_id, + display_name: record.data.display_name, + }; + }); + this.on_selected(values); + }, + }); + } + }, +}); + +return { + FormViewDialog: FormViewDialog, + SelectCreateDialog: SelectCreateDialog, +}; + +}); |
