summaryrefslogtreecommitdiff
path: root/addons/web/static/src/js/views/view_dialogs.js
diff options
context:
space:
mode:
authorGéry Debongnie <ged@odoo.com>2017-03-05 10:05:56 +0100
committerAaron Bohy <aab@odoo.com>2017-04-11 19:44:38 +0200
commit905e01921f3c3ef43ace6fc537d1d8c0d280002c (patch)
tree265c43ab21c5401a5d9f0f3d64d86f1ac417451c /addons/web/static/src/js/views/view_dialogs.js
parent54d4c73af4ad05c7640d28c1892247c9538b8017 (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.js376
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,
+};
+
+});