summaryrefslogtreecommitdiff
path: root/addons/web/static/src/js/views
diff options
context:
space:
mode:
authorAaron Bohy <aab@odoo.com>2017-11-17 13:14:02 +0100
committerAaron Bohy <aab@odoo.com>2018-01-16 20:49:19 +0100
commit32b8cec5361174c92c45a4975d4e02f1fb448aa2 (patch)
tree110bd6986c2da1ed5047002b7ea40c91ea9fa5ed /addons/web/static/src/js/views
parent9bee25a9df65f657fef0f2f32a7ad823fdc7d4e1 (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')
-rw-r--r--addons/web/static/src/js/views/abstract_controller.js170
-rw-r--r--addons/web/static/src/js/views/abstract_view.js25
-rw-r--r--addons/web/static/src/js/views/basic/basic_controller.js22
-rw-r--r--addons/web/static/src/js/views/form/form_controller.js4
-rw-r--r--addons/web/static/src/js/views/form/form_view.js10
-rw-r--r--addons/web/static/src/js/views/list/list_controller.js2
-rw-r--r--addons/web/static/src/js/views/list/list_view.js4
-rw-r--r--addons/web/static/src/js/views/view_dialogs.js9
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);