flectra/addons/web/static/src/js/views/abstract_controller.js

303 lines
10 KiB
JavaScript

flectra.define('web.AbstractController', function (require) {
"use strict";
/**
* The Controller class is the class coordinating the model and the renderer.
* It is the C in MVC, and is what was formerly known in Flectra as a View.
*
* Its role is to listen to events bubbling up from the model/renderer, and call
* the appropriate methods if necessary. It also render control panel buttons,
* and react to changes in the search view. Basically, all interactions from
* the renderer/model with the outside world (meaning server/reading in session/
* reading localstorage, ...) has to go through the controller.
*/
var concurrency = require('web.concurrency');
var Widget = require('web.Widget');
var AbstractController = Widget.extend({
custom_events: {
open_record: '_onOpenRecord',
},
events: {
'click a[type="action"]': '_onActionClicked',
},
/**
* @constructor
* @param {Widget} parent
* @param {AbstractModel} model
* @param {AbstractRenderer} renderer
* @param {object} params
* @param {string} params.modelName
* @param {any} [params.handle] a handle that will be given to the model (some id)
* @param {any} params.initialState the initialState
*/
init: function (parent, model, renderer, params) {
this._super.apply(this, arguments);
this.model = model;
this.renderer = renderer;
this.modelName = params.modelName;
this.handle = params.handle;
this.activeActions = params.activeActions;
this.initialState = params.initialState;
// use a DropPrevious to correctly handle concurrent updates
this.dp = new concurrency.DropPrevious();
},
/**
* Simply renders and updates the url.
*
* @returns {Deferred}
*/
start: function () {
return $.when(
this._super.apply(this, arguments),
this.renderer.appendTo(this.$el)
).then(this._update.bind(this, this.initialState));
},
/**
* @override
*/
destroy: function () {
if (this.$buttons) {
this.$buttons.off();
}
return this._super.apply(this, arguments);
},
/**
* Called each time the controller is attached into the DOM.
*/
on_attach_callback: function () {
this.renderer.on_attach_callback();
},
/**
* Called each time the controller is detached from the DOM.
*/
on_detach_callback: function () {
this.renderer.on_detach_callback();
},
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
/**
* 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
* this method on the active view to make sure it is ok to open the home
* screen (and lose all current state).
*
* Note that it returns a deferred, because the view could choose to ask the
* user if he agrees to discard.
*
* @param {string} [recordID]
* if not given, we consider all the changes made by the controller
* @returns {Deferred} resolved if properly discarded, rejected otherwise
*/
discardChanges: function (recordID) {
return $.when();
},
/**
* Returns any special keys that may be useful when reloading the view to
* get the same effect. This is necessary for saving the current view in
* the favorites. For example, a graph view might want to add a key to
* save the current graph type.
*
* @returns {Object}
*/
getContext: function () {
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.
*
* @returns {string}
*/
getTitle: function () {
return this.display_name;
},
/**
* The use of this method is discouraged. It is still snakecased, because
* it currently is used in many templates, but we will move to a simpler
* mechanism as soon as we can.
*
* @deprecated
* @param {string} action type of action, such as 'create', 'read', ...
* @returns {boolean}
*/
is_action_enabled: function (action) {
return this.activeActions[action];
},
/**
* Short helper method to reload the view
*
* @param {Object} [params] This object will simply be given to the update
* @returns {Deferred}
*/
reload: function (params) {
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 (flectra frontend = public website)
*
* @param {jQuery Node} $node
*/
renderButtons: function ($node) {
},
/**
* For views that require a pager, this method will be called to allow the
* controller to instantiate and render a pager. Note that in theory, the
* 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
*/
renderPager: function ($node) {
},
/**
* 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
*/
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
* that something has changed.
*
* The update method is responsible for fetching necessary data, then
* updating the renderer and wait for the rendering to complete.
*
* @param {Object} params will be given to the model and to the renderer
* @param {Object} [options]
* @param {boolean} [options.reload=true] if true, the model will reload data
*
* @returns {Deferred}
*/
update: function (params, options) {
var self = this;
var shouldReload = (options && 'reload' in options) ? options.reload : true;
var def = shouldReload ? this.model.reload(this.handle, params) : $.when();
return this.dp.add(def).then(function (handle) {
self.handle = handle || self.handle; // update handle if we reloaded
var state = self.model.get(self.handle);
var localState = self.renderer.getLocalState();
return self.dp.add(self.renderer.updateState(state, params)).then(function () {
self.renderer.setLocalState(localState);
self._update(state);
});
});
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* This method is the way a view can notifies the outside world that
* something has changed. The main use for this is to update the url, for
* example with a new id.
*
* @private
* @param {Object} [state] information that will be pushed to the outside
* world
*/
_pushState: function (state) {
this.trigger_up('push_state', state || {});
},
/**
* This method is called after each update or when the start method is
* completed.
*
* Its primary use is to be used as a hook to update all parts of the UI,
* besides the renderer. For example, it may be used to enable/disable
* some buttons in the control panel, such as the current graph type for a
* graph view.
*
* @private
* @param {Object} state the state given by the model
* @returns {Deferred}
*/
_update: function (state) {
this._pushState();
return $.when();
},
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
/**
* When an Flectra event arrives requesting a record to be opened, this method
* gets the res_id, and request a switch view in the appropriate mode
*
* 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
*
* @private
* @param {FlectraEvent} event
* @param {number} event.data.id The local model ID for the record to be
* opened
* @param {string} [event.data.mode='readonly']
*/
_onOpenRecord: function (event) {
event.stopPropagation();
var record = this.model.get(event.data.id, {raw: true});
this.trigger_up('switch_view', {
view_type: 'form',
res_id: record.res_id,
mode: event.data.mode || 'readonly',
model: this.modelName,
});
},
/**
* When a user clicks on an <a> link with type="action", we need to actually
* do the action. This kind of links is used a lot in no-content helpers.
*
* @private
* @param {FlectraEvent} event
*/
_onActionClicked: function (event) {
event.preventDefault();
this.do_action(event.target.name);
},
});
return AbstractController;
});