diff options
| author | Aaron Bohy <aab@odoo.com> | 2017-11-17 13:14:02 +0100 |
|---|---|---|
| committer | Aaron Bohy <aab@odoo.com> | 2018-01-16 20:49:19 +0100 |
| commit | 32b8cec5361174c92c45a4975d4e02f1fb448aa2 (patch) | |
| tree | 110bd6986c2da1ed5047002b7ea40c91ea9fa5ed /addons/web/static/src/js/views | |
| parent | 9bee25a9df65f657fef0f2f32a7ad823fdc7d4e1 (diff) | |
[REF] web,*: ActionManager/ViewManager refactoring
This rev. is the first part (out of 3) of the refactoring of the
ActionManager and ViewManager layer of the webclient.
The main changes are:
- there is no ViewManager anymore, its work is now handled by the
ActionManager itself, but isolated in a specific file ; this
eases a lot of things, as the breadcrumbs handling for example.
- the ActionManager code is converted to the new coding principles
and guidelines ; mainly, children widgets communicate with it by
triggering events up, and not by function calls anymore.
- the ActionManager layer is now testable, and a lot of tests have
already been written.
- the code in other addons has been adapted consequently.
What's coming next:
- introduce the 'AbstractAction' Widget, and make client actions
and view controllers inherit from it ; this widget will
implement a common API that could be used uniformly by the
ActionManager (e.g. restore(), canBeLeft(), renderButtons(),
getTitle()...).
- move the ControlPanel handling from the ActionManager to the
AbstractAction.
Diffstat (limited to 'addons/web/static/src/js/views')
8 files changed, 184 insertions, 62 deletions
diff --git a/addons/web/static/src/js/views/abstract_controller.js b/addons/web/static/src/js/views/abstract_controller.js index 3bdf8ab08ca..150b4803a7a 100644 --- a/addons/web/static/src/js/views/abstract_controller.js +++ b/addons/web/static/src/js/views/abstract_controller.js @@ -12,12 +12,16 @@ odoo.define('web.AbstractController', function (require) { * reading localstorage, ...) has to go through the controller. */ +var ControlPanelMixin = require('web.ControlPanelMixin'); +var core = require('web.core'); var Widget = require('web.Widget'); +var QWeb = core.qweb; -var AbstractController = Widget.extend({ +var AbstractController = Widget.extend(ControlPanelMixin, { custom_events: { open_record: '_onOpenRecord', + switch_view: '_onSwitchView', }, events: { 'click a[type="action"]': '_onActionClicked', @@ -30,8 +34,15 @@ var AbstractController = Widget.extend({ * @param {AbstractRenderer} renderer * @param {object} params * @param {string} params.modelName + * @param {string} [params.controllerID] an id to ease the communication + * with upstream components * @param {any} [params.handle] a handle that will be given to the model (some id) * @param {any} params.initialState the initialState + * @param {boolean} params.isMultiRecord + * @param {Object[]} params.actionViews + * @param {string} params.viewType + * @param {boolean} params.withControlPanel set to false to hide the + * ControlPanel */ init: function (parent, model, renderer, params) { this._super.apply(this, arguments); @@ -40,7 +51,23 @@ var AbstractController = Widget.extend({ this.modelName = params.modelName; this.handle = params.handle; this.activeActions = params.activeActions; + this.controllerID = params.controllerID; this.initialState = params.initialState; + + // those arguments are temporary, they won't be necessary as soon as the + // ControlPanel will be handled by the View + this.displayName = params.displayName; + this.isMultiRecord = params.isMultiRecord; + this.searchable = params.searchable; + this.searchView = params.searchView; + this.searchViewHidden = params.searchViewHidden; + this.actionViews = params.actionViews; + this.viewType = params.viewType; + this.withControlPanel = params.withControlPanel !== false; + // override this.need_control_panel so that the ActionManager doesn't + // update the control panel when it isn't visible (this is a temporary + // hack that can be removed as soon as the CP'll be handled by the view) + this.need_control_panel = this.withControlPanel; }, /** * Simply renders and updates the url. @@ -48,10 +75,19 @@ var AbstractController = Widget.extend({ * @returns {Deferred} */ start: function () { + var self = this; + + this.$el.addClass('o_view_controller'); + + // render the ControlPanel elements (buttons, pager, sidebar...) + this.controlPanelElements = this._renderControlPanelElements(); + return $.when( this._super.apply(this, arguments), this.renderer.appendTo(this.$el) - ).then(this._update.bind(this, this.initialState)); + ).then(function () { + self._update(self.initialState); + }); }, /** * @override @@ -60,6 +96,9 @@ var AbstractController = Widget.extend({ if (this.$buttons) { this.$buttons.off(); } + if (this.controlPanelElements && this.controlPanelElements.$switch_buttons) { + this.controlPanelElements.$switch_buttons.off(); + } return this._super.apply(this, arguments); }, @@ -70,7 +109,7 @@ var AbstractController = Widget.extend({ /** * Discards the changes made on the record associated to the given ID, or * all changes made by the current controller if no recordID is given. For - * example, when the user open the 'home' screen, the view manager will call + * example, when the user opens the 'home' screen, the action manager calls * this method on the active view to make sure it is ok to open the home * screen (and lose all current state). * @@ -96,23 +135,15 @@ var AbstractController = Widget.extend({ return {}; }, /** - * @see setScrollTop - * @returns {number} - */ - getScrollTop: function () { - return this.scrollTop; - }, - /** * Returns a title that may be displayed in the breadcrumb area. For * example, the name of the record. * - * Note: this seems wrong right now, it should not be implemented, we have - * no guarantee that there is a display_name variable in a controller. + * note: this will be moved to AbstractAction * * @returns {string} */ getTitle: function () { - return this.display_name; + return this.displayName; }, /** * The use of this method is discouraged. It is still snakecased, because @@ -136,12 +167,9 @@ var AbstractController = Widget.extend({ return this.update(params || {}); }, /** - * Most likely called by the view manager, this method is responsible for - * adding buttons in the control panel (buttons such as save/discard/...) - * - * Note that there is no guarantee that this method will be called. The - * controller is supposed to work even without a view manager, for example - * in the frontend (odoo frontend = public website) + * Renders the buttons to append, in most cases, to the control panel (in + * the bottom left corner). When the view is rendered in a dialog, those + * buttons might be moved to the dialog's footer. * * @param {jQuery Node} $node */ @@ -153,7 +181,7 @@ var AbstractController = Widget.extend({ * controller can actually render whatever he wants in the pager zone. If * your view does not want a pager, just let this method empty. * - * @param {Query Node} $node + * @param {jQuery Node} $node */ renderPager: function ($node) { }, @@ -161,21 +189,11 @@ var AbstractController = Widget.extend({ * Same as renderPager, but for the 'sidebar' zone (the zone with the menu * dropdown in the control panel next to the buttons) * - * @param {Query Node} $node + * @param {jQuery Node} $node */ renderSidebar: function ($node) { }, /** - * Not sure about this one, it probably needs to be reworked, maybe merged - * in get/set local state methods. - * - * @see getScrollTop - * @param {number} scrollTop - */ - setScrollTop: function (scrollTop) { - this.scrollTop = scrollTop; - }, - /** * This is the main entry point for the controller. Changes from the search * view arrive in this method, and internal changes can sometimes also call * this method. It is basically the way everything notifies the controller @@ -219,7 +237,68 @@ var AbstractController = Widget.extend({ * world */ _pushState: function (state) { - this.trigger_up('push_state', state || {}); + this.trigger_up('push_state', { + controllerID: this.controllerID, + state: state || {}, + }); + }, + /** + * Renders the control elements (buttons, pager and sidebar) of the current + * view. + * + * @private + * @returns {Object} an object containing the control panel jQuery elements + */ + _renderControlPanelElements: function () { + var elements = {}; + + if (this.withControlPanel) { + elements = { + $buttons: $('<div>'), + $sidebar: $('<div>'), + $pager: $('<div>'), + }; + + this.renderButtons(elements.$buttons); + this.renderSidebar(elements.$sidebar); + this.renderPager(elements.$pager); + // remove the unnecessary outer div + elements = _.mapObject(elements, function($node) { + return $node && $node.contents(); + }); + elements.$switch_buttons = this._renderSwitchButtons(); + } + + return elements; + }, + /** + * Renders the switch buttons and binds listeners on them. + * + * @private + * @returns {jQuery} + */ + _renderSwitchButtons: function () { + var self = this; + var views = _.filter(this.actionViews, {multiRecord: this.isMultiRecord}); + + if (views.length <= 1) { + return $(); + } + + var $switchButtons = $(QWeb.render('ControlPanel.SwitchButtons', { + views: views, + })); + // create bootstrap tooltips + _.each(views, function (view) { + $switchButtons.filter('.o_cp_switch_' + view.type).tooltip(); + }); + // add onclick event listener + $switchButtons.filter('button').click(_.debounce(function (event) { + var viewType = $(event.target).data('view-type'); + self.trigger_up('switch_view', {view_type: viewType}); + }, 200, true)); + + return $switchButtons; }, /** * This method is called after each update or when the start method is @@ -235,6 +314,22 @@ var AbstractController = Widget.extend({ * @returns {Deferred} */ _update: function (state) { + // AAB: update the control panel -> this will be moved elsewhere at some point + var cpContent = _.extend({}, this.controlPanelElements); + if (this.searchView) { + _.extend(cpContent, { + $searchview: this.searchView.$el, + $searchview_buttons: this.searchView.$buttons.contents(), + }); + } + this.update_control_panel({ + active_view_selector: '.o_cp_switch_' + this.viewType, + cp_content: cpContent, + hidden: !this.withControlPanel, + searchview: this.searchView, + search_view_hidden: !this.searchable || this.searchviewHidden, + }); + this._pushState(); return $.when(); }, @@ -249,7 +344,7 @@ var AbstractController = Widget.extend({ * * Note: this method seems wrong, it relies on the model being a basic model, * to get the res_id. It should receive the res_id in the event data - * @todo move this to basic controller? or view manager + * @todo move this to basic controller? * * @private * @param {OdooEvent} event @@ -278,6 +373,15 @@ var AbstractController = Widget.extend({ event.preventDefault(); this.do_action(event.target.name); }, + /** + * Intercepts the 'switch_view' event to add the controllerID into the data, + * and lets the event bubble up. + * + * @param {OdooEvent} event + */ + _onSwitchView: function (event) { + event.data.controllerID = this.controllerID; + }, }); diff --git a/addons/web/static/src/js/views/abstract_view.js b/addons/web/static/src/js/views/abstract_view.js index 2a1ba16a33e..58b2c85a670 100644 --- a/addons/web/static/src/js/views/abstract_view.js +++ b/addons/web/static/src/js/views/abstract_view.js @@ -67,11 +67,18 @@ var AbstractView = Class.extend({ * @param {string} params.modelName The actual model name * @param {Object} params.context * @param {number} [params.count] + * @param {string} [params.controllerID] * @param {string[]} params.domain * @param {string[]} params.groupBy * @param {number} [params.currentId] * @param {number[]} [params.ids] + * @param {boolean} [params.withControlPanel] + * @param {boolean} [params.action.flags.headless] + * @param {string} [params.action.display_name] + * @param {string} [params.action.name] * @param {string} [params.action.help] + * @param {string} [params.action.jsID] + * @param {boolean} [params.action.views] */ init: function (viewInfo, params) { this.rendererParams = { @@ -87,7 +94,23 @@ var AbstractView = Class.extend({ delete: viewInfo.arch.attrs.delete ? JSON.parse(viewInfo.arch.attrs.delete) : true, duplicate: viewInfo.arch.attrs.duplicate ? JSON.parse(viewInfo.arch.attrs.duplicate) : true, }, + controllerID: params.controllerID, }; + // AAB: these params won't be necessary as soon as the ControlPanel will + // be instantiated by the View + this.controllerParams.displayName = params.action && (params.action.display_name || params.action.name); + this.controllerParams.isMultiRecord = this.multi_record; + this.controllerParams.searchable = this.searchable; + this.controllerParams.searchView = params.action && params.action.searchView; + this.controllerParams.searchViewHidden = this.searchview_hidden; // AAB: use searchable instead where it is used? + this.controllerParams.actionViews = params.action ? params.action.views : []; + this.controllerParams.viewType = this.viewType; + this.controllerParams.withControlPanel = true; + if (params.action && params.action.flags) { + this.controllerParams.withControlPanel = !params.action.flags.headless; + } else if ('withControlPanel' in params) { + this.controllerParams.withControlPanel = params.withControlPanel; + } this.loadParams = { context: params.context, @@ -133,7 +156,7 @@ var AbstractView = Class.extend({ * load itself, and the renderer needs the data in its constructor. * * @param {Widget} parent The parent of the resulting Controller (most - * likely a view manager) + * likely an action manager) * @returns {Deferred} The deferred resolves to a controller */ getController: function (parent) { 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 1c8060a6fe1..2c7c1c40210 100644 --- a/addons/web/static/src/js/views/basic/basic_controller.js +++ b/addons/web/static/src/js/views/basic/basic_controller.js @@ -200,7 +200,7 @@ var BasicController = AbstractController.extend(FieldManagerMixin, { _abandonRecord: function (recordID) { recordID = recordID || this.handle; if (recordID === this.handle) { - this.trigger_up('switch_to_previous_view'); + this.trigger_up('history_back'); } else { this.model.removeLine(recordID); } @@ -244,17 +244,11 @@ var BasicController = AbstractController.extend(FieldManagerMixin, { model: record.model, resIDs: record.res_ids, }, - on_closed: function (reason) { - if (!_.isObject(reason)) { - reload(reason); - } - }, - on_fail: function (reason) { - reload().always(function() { - def.reject(reason); - }); - }, on_success: def.resolve.bind(def), + on_fail: function () { + reload().always(def.reject.bind(def)); + }, + on_closed: reload, }); return this.alive(def); }, @@ -313,13 +307,13 @@ var BasicController = AbstractController.extend(FieldManagerMixin, { * @param {Object} [options] * @param {boolean} [options.readonlyIfRealDiscard=false] * After discarding record changes, the usual option is to make the - * record readonly. However, the view manager calls this function + * record readonly. However, the action manager calls this function * at inappropriate times in the current code and in that case, we * don't want to go back to readonly if there is nothing to discard * (e.g. when switching record in edit mode in form view, we expect * the new record to be in edit mode too, but the view manager calls * this function as the URL changes...) @todo get rid of this when - * the view manager is improved. + * the webclient/action_manager's hashchange mechanism is improved. * @returns {Deferred} */ _discardChanges: function (recordID, options) { @@ -467,7 +461,7 @@ var BasicController = AbstractController.extend(FieldManagerMixin, { var sidebarEnv = this._getSidebarEnv(); this.sidebar.updateEnv(sidebarEnv); } - this.trigger_up('env_updated', env); + this.trigger_up('env_updated', {controllerID: this.controllerID, env: env}); }, /** * Helper method, to make sure the information displayed by the pager is up diff --git a/addons/web/static/src/js/views/form/form_controller.js b/addons/web/static/src/js/views/form/form_controller.js index 2820dad448f..7b279d44bcd 100644 --- a/addons/web/static/src/js/views/form/form_controller.js +++ b/addons/web/static/src/js/views/form/form_controller.js @@ -149,7 +149,7 @@ var FormController = BasicController.extend({ * inserted **/ renderSidebar: function ($node) { - if (!this.sidebar && this.hasSidebar) { + if (this.hasSidebar) { var otherItems = []; if (this.is_action_enabled('delete')) { otherItems.push({ @@ -280,7 +280,7 @@ var FormController = BasicController.extend({ _onDeletedRecords: function () { var state = this.model.get(this.handle, {raw: true}); if (!state.res_ids.length) { - this.do_action('history_back'); + this.trigger_up('history_back'); } else { this._super.apply(this, arguments); } diff --git a/addons/web/static/src/js/views/form/form_view.js b/addons/web/static/src/js/views/form/form_view.js index 42cf5de12e4..2851d3ac72a 100644 --- a/addons/web/static/src/js/views/form/form_view.js +++ b/addons/web/static/src/js/views/form/form_view.js @@ -2,7 +2,6 @@ odoo.define('web.FormView', function (require) { "use strict"; var BasicView = require('web.BasicView'); -var Context = require('web.Context'); var core = require('web.core'); var FormController = require('web.FormController'); var FormRenderer = require('web.FormRenderer'); @@ -25,15 +24,16 @@ var FormView = BasicView.extend({ init: function (viewInfo, params) { this._super.apply(this, arguments); - var mode = params.mode || (params.currentId ? 'readonly' : 'edit'); + var mode = params.mode || params.context.form_view_initial_mode || + (params.currentId ? 'readonly' : 'edit'); this.loadParams.type = 'record'; this.controllerParams.disableAutofocus = params.disable_autofocus; - this.controllerParams.hasSidebar = params.sidebar; + this.controllerParams.hasSidebar = params.hasSidebar; this.controllerParams.toolbarActions = viewInfo.toolbar; - this.controllerParams.footerToButtons = params.footer_to_buttons; + this.controllerParams.footerToButtons = params.footerToButtons; if ('action' in params && 'flags' in params.action) { - this.controllerParams.footerToButtons = params.action.flags.footer_to_buttons; + this.controllerParams.footerToButtons = params.action.flags.footerToButtons; } var defaultButtons = 'default_buttons' in params ? params.default_buttons : true; this.controllerParams.defaultButtons = defaultButtons; diff --git a/addons/web/static/src/js/views/list/list_controller.js b/addons/web/static/src/js/views/list/list_controller.js index 982d5456bca..33c1d234c7a 100644 --- a/addons/web/static/src/js/views/list/list_controller.js +++ b/addons/web/static/src/js/views/list/list_controller.js @@ -128,7 +128,7 @@ var ListController = BasicController.extend({ * @param {jQuery Node} $node */ renderSidebar: function ($node) { - if (this.hasSidebar && !this.sidebar) { + if (this.hasSidebar) { var other = [{ label: _t("Export"), callback: this._onExportData.bind(this) diff --git a/addons/web/static/src/js/views/list/list_view.js b/addons/web/static/src/js/views/list/list_view.js index 8a989dad35e..e970f825846 100644 --- a/addons/web/static/src/js/views/list/list_view.js +++ b/addons/web/static/src/js/views/list/list_view.js @@ -30,7 +30,7 @@ var ListView = BasicView.extend({ * * @param {Object} viewInfo * @param {Object} params - * @param {boolean} params.sidebar + * @param {boolean} params.hasSidebar * @param {boolean} [params.hasSelectors=true] */ init: function (viewInfo, params) { @@ -40,7 +40,7 @@ var ListView = BasicView.extend({ var mode = arch.attrs.editable && !params.readonly ? "edit" : "readonly"; this.controllerParams.editable = arch.attrs.editable; - this.controllerParams.hasSidebar = params.sidebar; + this.controllerParams.hasSidebar = params.hasSidebar; this.controllerParams.toolbarActions = viewInfo.toolbar; this.controllerParams.noLeaf = !!this.loadParams.context.group_by_no_leaf; this.controllerParams.mode = mode; diff --git a/addons/web/static/src/js/views/view_dialogs.js b/addons/web/static/src/js/views/view_dialogs.js index aea29d96ee2..72e215f9c24 100644 --- a/addons/web/static/src/js/views/view_dialogs.js +++ b/addons/web/static/src/js/views/view_dialogs.js @@ -175,8 +175,9 @@ var FormViewDialog = ViewDialog.extend({ currentId: self.res_id || undefined, index: 0, mode: self.res_id && self.options.readonly ? 'readonly' : 'edit', - footer_to_buttons: true, + footerToButtons: true, default_buttons: false, + withControlPanel: false, model: self.model, parentID: self.parentID, recordID: self.recordID, @@ -190,9 +191,9 @@ var FormViewDialog = ViewDialog.extend({ } self.form_view.appendTo(fragment) .then(function () { - var $buttons = $('<div>'); - self.form_view.renderButtons($buttons); self.opened().always(function () { + var $buttons = $('<div>'); + self.form_view.renderButtons($buttons); if ($buttons.children().length) { self.$footer.empty().append($buttons.contents()); } @@ -257,7 +258,7 @@ var SelectCreateDialog = ViewDialog.extend({ 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 + event.stopPropagation(); // prevent this event from bubbling up to the action manager var d = event.data; var searchData = this._process_search_data(d.domains, d.contexts, d.groupbys); this.list_controller.reload(searchData); |
