diff options
| author | Géry Debongnie <ged@odoo.com> | 2017-03-05 10:05:56 +0100 |
|---|---|---|
| committer | Aaron Bohy <aab@odoo.com> | 2017-04-11 19:44:38 +0200 |
| commit | 905e01921f3c3ef43ace6fc537d1d8c0d280002c (patch) | |
| tree | 265c43ab21c5401a5d9f0f3d64d86f1ac417451c /addons/web/static/src/js/views/view_dialogs.js | |
| parent | 54d4c73af4ad05c7640d28c1892247c9538b8017 (diff) | |
[REF] web, *: redesign all JS views
This commit introduce a full redesign of all JS views. We started
basically from scratch. The goal was to unify all the various views
under a common framework, to make them testable, to make then usable in
different conditions (in studio, or in the frontend), and to make our
lives easier.
Some important points are:
- we introduced new coding guidelines (camelCase, 80 chars width, ...)
- we have a brand new testing framework (still QUnit based)
- kanban view moved to the web addon
- calendar view (formerly web_calender) moved to web as well
- the tree view was removed
- all new code should be documented
We hope that this code is the start of a new era for the Odoo web
client, we want to have a high quality codebase, well documented, well
tested, well designed.
Work done by the framework team: mostly aab, ged, chm, dmo, qsm
Diffstat (limited to 'addons/web/static/src/js/views/view_dialogs.js')
| -rw-r--r-- | addons/web/static/src/js/views/view_dialogs.js | 376 |
1 files changed, 376 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 00000000000..0310656cd37 --- /dev/null +++ b/addons/web/static/src/js/views/view_dialogs.js @@ -0,0 +1,376 @@ +odoo.define('web.view_dialogs', function (require) { +"use strict"; + +var core = require('web.core'); +var data = require('web.data'); +var Dialog = require('web.Dialog'); +var ListController = require('web.ListController'); +var ListView = require('web.ListView'); +var pyeval = require('web.pyeval'); +var SearchView = require('web.SearchView'); +var view_registry = require('web.view_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.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 || {}); + + // FIXME: remove this once a dataset won't be necessary anymore to interact + // with data_manager and instantiate views + this.dataset = new data.DataSet(this, this.res_model, this.context); + }, + + //-------------------------------------------------------------------------- + // 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 {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 {function} [options.on_save] callback to execute when clicking on + * 'Save' (form view's 'saveRecord' by default) + * @param {function} [options.on_saved] callback executed after on_save + * @param {BasicModel} [options.model] if given, it will be used instead of + * a new form view model + */ + init: function (parent, options) { + var self = this; + + this.res_id = options.res_id || null; + this.on_saved = options.on_saved || (function () {}); + this.context = options.context; + this.model = options.model; + + var multi_select = !_.isNumber(options.res_id) && !options.disable_multiple_selection; + var readonly = _.isNumber(options.res_id) && options.readonly; + + if (!options || !options.buttons) { + options = options || {}; + options.buttons = [{ + text: (readonly ? _t("Close") : _t("Discard")), + classes: "btn-default o_form_button_cancel", + close: true, + click: function () { + if (!readonly) { + self.form_view.model.discardChanges(self.form_view.handle); + } + }, + }]; + + if (!readonly) { + options.buttons.unshift({ + text: _t("Save") + ((multi_select)? " " + _t(" & Close") : ""), + classes: "btn-primary", + click: function () { + this._save().then(self.close.bind(self)); + } + }); + + if (multi_select) { + options.buttons.splice(1, 0, { + text: _t("Save & New"), + classes: "btn-primary", + click: function () { + this._save().then(self.form_view.createRecord.bind(self.form_view)); + }, + }); + } + } + } + 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 = $.when(this.options.fields_view); + } else { + fields_view_def = this.loadFieldView(this.dataset, this.options.view_id, 'form'); + } + + fields_view_def.then(function (fields_view) { + var formview = new FormView(fields_view.arch, fields_view.fields, { + modelName: self.res_model, + context: self.context, + ids: self.res_id ? [self.res_id] : [], + currentId: self.res_id || undefined, + index: 0, + mode: self.res_id && self.options.readonly ? 'readonly' : 'edit', + footer_to_buttons: true, + default_buttons: false, + model: self.model, + }); + return formview.createController(self); + }).then(function (formView) { + self.form_view = formView; + var fragment = document.createDocumentFragment(); + self.form_view.appendTo(fragment) + .then(function () { + var $buttons = $('<div>'); + self.form_view.renderButtons($buttons); + if ($buttons.children().length) { + self.$footer.empty().append($buttons.contents()); + } + _super().$el.append(fragment); + self.form_view.autofocus(); + }); + }); + + return this; + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + _save: function () { + var self = this; + var def; + if (this.options.on_save) { + def = this.options.on_save(this.form_view.model.get(this.form_view.handle)); + } else { + def = this.form_view.saveRecord({stayInEdit: true, reload: false}); + } + return $.when(def).then(function () { + // 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 + self.on_saved(self.form_view.model.get(self.form_view.handle)); + }); + }, +}); + +var SelectCreateListController = ListController.extend({ + // Override the ListView to handle the custom events 'open_record' (triggered when clicking on a + // row of the list) such that it triggers up 'select_record' with its res_id. + custom_events: _.extend({}, ListController.prototype.custom_events, { + open_record: function (event) { + var selected_record = this.model.get(event.data.id); + this.trigger_up('select_record', {id: selected_record.res_id}); + }, + }), +}); + +/** + * 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.id]); + this.close(); + } + }, + selection_changed: function (event) { + this.$footer.find(".o_select_button").prop('disabled', !event.data.selection.length); + }, + search: function (event) { + event.stopPropagation(); // prevent this event from bubbling up to the view manager + var d = event.data; + var searchData = this._process_search_data(d.domains, d.contexts, d.groupbys); + this.list_controller.reload(searchData); + }, + }), + + /** + * 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 + */ + init: function () { + this._super.apply(this, arguments); + _.defaults(this.options, { initial_view: 'search' }); + this.on_selected = this.options.on_selected || (function () {}); + this.initial_ids = this.options.initial_ids; + }, + + open: function () { + if(this.options.initial_view !== "search") { + return this.create_edit_record(); + } + var user_context = this.getSession().user_context; + + var _super = this._super.bind(this); + var context = pyeval.eval_domains_and_contexts({ + domains: [], + contexts: [user_context, this.context] + }).context; + var search_defaults = {}; + _.each(context, function (value_, key) { + var match = /^search_default_(.*)$/.exec(key); + if (match) { + search_defaults[match[1]] = value_; + } + }); + this.loadViews(this.dataset.model, this.dataset.get_context(), [[false, 'list'], [false, 'search']], {}) + .then(this.setup.bind(this, search_defaults)) + .then(function (fragment) { + _super().$el.append(fragment); + }); + return this; + }, + + setup: function (search_defaults, fields_views) { + var self = this; + var fragment = document.createDocumentFragment(); + + var searchDef = $.Deferred(); + + // Set the dialog's header and its search view + var $header = $('<div/>').addClass('o_modal_header').appendTo(fragment); + var $pager = $('<div/>').addClass('o_pager').appendTo($header); + var options = { + $buttons: $('<div/>').addClass('o_search_options').appendTo($header), + search_defaults: search_defaults, + }; + var searchview = new SearchView(this, this.dataset, fields_views.search, options); + searchview.prependTo($header).done(function () { + var d = searchview.build_search_data(); + d.domains = d.domains.concat([self.domain]); + if (self.initial_ids) { + d.domains.push([["id", "in", self.initial_ids]]); + self.initial_ids = undefined; + } + var searchData = self._process_search_data(d.domains, d.contexts, d.groupbys); + searchDef.resolve(searchData); + }); + + return $.when(searchDef).then(function (searchResult) { + // Set the list view + var listView = new ListView(fields_views.list.arch, fields_views.list.fields, _.extend({ + context: searchResult.context, + domain: searchResult.domain, + groupBy: searchResult.groupBy, + modelName: self.dataset.model, + hasSelectors: !self.options.disable_multiple_selection, + }, self.options.list_view_options)); + listView.setController(SelectCreateListController); + return listView.createController(self); + }).then(function (controller) { + self.list_controller = controller; + // Set the dialog's buttons + var buttons = [{ + text: _t("Cancel"), + classes: "btn-default o_form_button_cancel", + close: true, + }]; + if (!self.options.no_create) { + buttons.unshift({ + text: _t("Create"), + classes: "btn-primary", + click: self.create_edit_record.bind(self) + }); + } + if (!self.options.disable_multiple_selection) { + buttons.unshift({ + text: _t("Select"), + classes: "btn-primary o_select_button", + disabled: true, + close: true, + click: function () { + self.on_selected(self.list_controller.getSelectedIds()); + }, + }); + } + self.set_buttons(buttons); + return self.list_controller.appendTo(fragment); + }).then(function () { + searchview.toggle_visibility(true); + self.list_controller.do_show(); + self.list_controller.renderPager($pager); + return fragment; + }); + }, + _process_search_data: function (domains, contexts, groupbys) { + var user_context = this.getSession().user_context; + contexts = [user_context].concat(contexts); + var results = pyeval.eval_domains_and_contexts({ + domains: domains || [], + contexts: contexts || [], + group_by_seq: groupbys || [] + }); + return { + context: results.context, + domain: results.domain, + groupBy: results.group_by, + }; + }, + create_edit_record: function () { + var self = this; + var dialog = new FormViewDialog(this, _.extend({}, this.options, { + on_saved: function (record) { + self.on_selected([record.res_id]); + }, + })).open(); + dialog.on('closed', this, this.close.bind(this)); + }, +}); + +return { + FormViewDialog: FormViewDialog, + SelectCreateDialog: SelectCreateDialog, +}; + +}); |
