diff options
| author | Aaron Bohy <aab@odoo.com> | 2019-02-05 12:36:17 +0000 |
|---|---|---|
| committer | Mathieu Duckerts-Antoine <dam@odoo.com> | 2019-02-13 12:23:28 +0000 |
| commit | 3a5f6bfa9f74a0f038df4f22b97061c436c01281 (patch) | |
| tree | 72a9fa8362ac46fdbceb59fe9d53756c9bbd0884 /addons/web/static/src/js | |
| parent | 33207ed452cb1f3f2dc05a447a137b8cc8cee9ed (diff) | |
[IMP] web: add kanban searchPanel
This rev. defines a searchPanel widget used in Kanban views to
refine search according to specific dimensions. This wigdet is
displayed as a sidebar to the left of the kanban view.
Part of task 1892462
Co-authored-by: Aaron Bohy <aab@odoo.com>
Co-authored-by: Mathieu Duckerts-Antoine <dam@odoo.com>
Co-authored-by: Raphael Collet <rco@odoo.com>
Diffstat (limited to 'addons/web/static/src/js')
6 files changed, 745 insertions, 11 deletions
diff --git a/addons/web/static/src/js/views/abstract_view.js b/addons/web/static/src/js/views/abstract_view.js index 085571a200f..72108e3c8ad 100644 --- a/addons/web/static/src/js/views/abstract_view.js +++ b/addons/web/static/src/js/views/abstract_view.js @@ -192,10 +192,6 @@ var AbstractView = Factory.extend({ } var _super = this._super.bind(this); return $.when(def).then(function (controlPanel) { - if (controlPanel) { - var searchQuery = controlPanel.getSearchQuery(); - self._updateMVCParams(searchQuery); - } // get the parent of the model if it already exists, as _super will // set the new controller as parent, which we don't want var modelParent = self.model && self.model.getParent(); @@ -249,6 +245,7 @@ var AbstractView = Factory.extend({ return controlPanelView.getController(parent).then(function (controlPanel) { self.controllerParams.controlPanel = controlPanel; return controlPanel.appendTo(document.createDocumentFragment()).then(function () { + self._updateMVCParams(controlPanel.getSearchQuery()); return controlPanel; }); }); diff --git a/addons/web/static/src/js/views/basic/basic_view.js b/addons/web/static/src/js/views/basic/basic_view.js index 39444fdfd75..313bed44708 100644 --- a/addons/web/static/src/js/views/basic/basic_view.js +++ b/addons/web/static/src/js/views/basic/basic_view.js @@ -393,7 +393,9 @@ var BasicView = AbstractView.extend({ for (var dependency_name in deps) { var dependency_dict = {name: dependency_name, type: deps[dependency_name].type}; if (!(dependency_name in fieldsInfo)) { - fieldsInfo[dependency_name] = _.extend({}, dependency_dict, {options: deps[dependency_name].options || {}}); + fieldsInfo[dependency_name] = _.extend({}, dependency_dict, { + options: deps[dependency_name].options || {}, + }); } if (!(dependency_name in fields)) { fields[dependency_name] = dependency_dict; diff --git a/addons/web/static/src/js/views/kanban/kanban_controller.js b/addons/web/static/src/js/views/kanban/kanban_controller.js index b51c32aa770..8c6525981f2 100644 --- a/addons/web/static/src/js/views/kanban/kanban_controller.js +++ b/addons/web/static/src/js/views/kanban/kanban_controller.js @@ -32,6 +32,7 @@ var KanbanController = BasicController.extend({ kanban_load_records: '_onLoadColumnRecords', column_toggle_fold: '_onToggleColumn', kanban_column_records_toggle_active: '_onToggleActiveRecords', + search_panel_domain_updated: '_onSearchPanelDomainUpdated', }), events: _.extend({}, BasicController.prototype.events, { click: '_onClick', @@ -41,13 +42,31 @@ var KanbanController = BasicController.extend({ * @param {Object} params * @param {boolean} params.quickCreateEnabled set to false to disable the * quick create feature + * @param {SearchPanel} [params.searchPanel] + * @param {Array[]} [params.controlPanelDomain=[]] initial domain coming + * from the controlPanel */ init: function (parent, model, renderer, params) { this._super.apply(this, arguments); - this.on_create = params.on_create; this.hasButtons = params.hasButtons; this.quickCreateEnabled = params.quickCreateEnabled; + + // the following attributes are used when there is a searchPanel + this._searchPanel = params.searchPanel; + this.controlPanelDomain = params.controlPanelDomain || []; + this.searchPanelDomain = this._searchPanel ? this._searchPanel.getDomain() : []; + }, + /** + * @override + */ + start: function () { + if (this._searchPanel) { + this.$('.o_content') + .addClass('o_kanban_with_searchpanel') + .prepend(this._searchPanel.$el); + } + return this._super.apply(this, arguments); }, //-------------------------------------------------------------------------- @@ -64,11 +83,37 @@ var KanbanController = BasicController.extend({ widget: this, })); this.$buttons.on('click', 'button.o-kanban-button-new', this._onButtonNew.bind(this)); - this.$buttons.on('keydown',this._onButtonsKeyDown.bind(this)); + this.$buttons.on('keydown', this._onButtonsKeyDown.bind(this)); this._updateButtons(); this.$buttons.appendTo($node); } }, + /** + * Override to add the domain coming from the searchPanel (if any) to the + * domain coming from the controlPanel. + * + * @override + */ + update: function (params) { + if (!this._searchPanel) { + return this._super.apply(this, arguments); + } + var self = this; + if (params.domain) { + this.controlPanelDomain = params.domain; + } + // do not re-render the view as soon as records have been fetched, but + // wait for the searchPanel to be ready as well, such that the view + // isn't re-rendered before the searchPanel + params.noRender = true; + params.domain = this.controlPanelDomain.concat(this.searchPanelDomain); + var superProm = this._super.apply(this, arguments); + var searchPanelProm = this._updateSearchPanel(); + return $.when(superProm, searchPanelProm).then(function () { + // searchPanel has been re-rendered, so re-render the view + return self.renderer.render(); + }); + }, //-------------------------------------------------------------------------- // Private @@ -198,6 +243,13 @@ var KanbanController = BasicController.extend({ this.$buttons.find('.o-kanban-button-new').toggleClass('o_hidden', createHidden); } }, + /** + * @private + * @returns {$.Promise} + */ + _updateSearchPanel: function () { + return this._searchPanel.update({searchDomain: this.controlPanelDomain}); + }, //-------------------------------------------------------------------------- // Handlers @@ -456,6 +508,15 @@ var KanbanController = BasicController.extend({ /** * @private * @param {OdooEvent} ev + * @param {Array[]} ev.data.domain the current domain of the searchPanel + */ + _onSearchPanelDomainUpdated: function (ev) { + this.searchPanelDomain = ev.data.domain; + this.reload({offset: 0}); + }, + /** + * @private + * @param {OdooEvent} ev * @param {boolean} [ev.data.openQuickCreate=false] if true, opens the * QuickCreate in the toggled column (it assumes that we are opening it) */ diff --git a/addons/web/static/src/js/views/kanban/kanban_renderer.js b/addons/web/static/src/js/views/kanban/kanban_renderer.js index 51d75a167c7..252b4587af6 100644 --- a/addons/web/static/src/js/views/kanban/kanban_renderer.js +++ b/addons/web/static/src/js/views/kanban/kanban_renderer.js @@ -167,6 +167,17 @@ var KanbanRenderer = BasicRenderer.extend({ this._toggleNoContentHelper(); }, /** + * Allow the rendering to be triggered from outside. This is used for kanban + * views with a searchPanel, to synchronize updates (the view is updated + * with param 'noRender', so that it is not re-rendered before the + * searchPanel). + * + * @returns {$.Promise} + */ + render: function () { + return this._render(); + }, + /** * Updates a given column with its new state. * * @param {string} localID the column id diff --git a/addons/web/static/src/js/views/kanban/kanban_view.js b/addons/web/static/src/js/views/kanban/kanban_view.js index fd40ca7df93..b7484e7d409 100644 --- a/addons/web/static/src/js/views/kanban/kanban_view.js +++ b/addons/web/static/src/js/views/kanban/kanban_view.js @@ -2,12 +2,14 @@ odoo.define('web.KanbanView', function (require) { "use strict"; var BasicView = require('web.BasicView'); -var core = require('web.core'); var config = require('web.config'); -var KanbanModel = require('web.KanbanModel'); -var KanbanRenderer = require('web.KanbanRenderer'); +var core = require('web.core'); var KanbanController = require('web.KanbanController'); var kanbanExamplesRegistry = require('web.kanban_examples_registry'); +var KanbanModel = require('web.KanbanModel'); +var KanbanRenderer = require('web.KanbanRenderer'); +var pyUtils = require('web.py_utils'); +var SearchPanel = require('web.SearchPanel'); var utils = require('web.utils'); var _lt = core._lt; @@ -21,6 +23,7 @@ var KanbanView = BasicView.extend({ Model: KanbanModel, Controller: KanbanController, Renderer: KanbanRenderer, + SearchPanel: SearchPanel, }, jsLibs: [], viewType: 'kanban', @@ -29,7 +32,10 @@ var KanbanView = BasicView.extend({ * @constructor */ init: function (viewInfo, params) { + this.searchPanelSections = Object.create(null); + this._super.apply(this, arguments); + this.loadParams.limit = this.loadParams.limit || 40; // in mobile, columns are lazy-loaded, so set 'openGroupByDefault' to // false so that they will won't be loaded by the initial load @@ -81,9 +87,31 @@ var KanbanView = BasicView.extend({ this.controllerParams.hasButtons = true; this.controllerParams.quickCreateEnabled = this.rendererParams.quickCreateEnabled; + if (config.device.isMobile) { this.jsLibs.push('/web/static/lib/jquery.touchSwipe/jquery.touchSwipe.js'); } + + this.hasSearchPanel = !_.isEmpty(this.searchPanelSections); + }, + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + /** + * Override to set the controller as parent of the optional searchPanel + * + * @override + */ + getController: function () { + var self = this; + return this._super.apply(this, arguments).then(function (controller) { + if (self.hasSearchPanel) { + self.controllerParams.searchPanel.setParent(controller); + } + return controller; + }); }, //-------------------------------------------------------------------------- @@ -91,6 +119,53 @@ var KanbanView = BasicView.extend({ //-------------------------------------------------------------------------- /** + * Override to create the searchPanel (if necessary) with the domain coming + * from the controlPanel + * + * @override + * @private + */ + _createControlPanel: function (parent) { + if (!this.hasSearchPanel) { + return this._super.apply(this, arguments); + } + var self = this; + return this._super.apply(this, arguments).then(function (controlPanel) { + return self._createSearchPanel(parent).then(function () { + return controlPanel; + }); + }); + }, + /** + * @private + * @param {Widget} parent + * @returns {$.Promise} resolved when the searchPanel is ready + */ + _createSearchPanel: function (parent) { + var self = this; + var defaultCategoryValues = {}; + Object.keys(this.loadParams.context).forEach(function (key) { + var match = /^searchpanel_default_(.*)$/.exec(key); + if (match) { + defaultCategoryValues[match[1]] = self.loadParams.context[key]; + } + }); + var controlPanelDomain = this.loadParams.domain; + var searchPanel = new this.config.SearchPanel(parent, { + defaultCategoryValues: defaultCategoryValues, + fields: this.fields, + model: this.loadParams.modelName, + searchDomain: controlPanelDomain, + sections: this.searchPanelSections, + }); + this.controllerParams.searchPanel = searchPanel; + this.controllerParams.controlPanelDomain = controlPanelDomain; + return searchPanel.appendTo(document.createDocumentFragment()).then(function () { + var searchPanelDomain = searchPanel.getDomain(); + self.loadParams.domain = controlPanelDomain.concat(searchPanelDomain); + }); + }, + /** * @private * @param {Object} viewInfo * @returns {boolean} true iff the quick create feature is not explicitely @@ -105,10 +180,71 @@ var KanbanView = BasicView.extend({ } return true; }, + /** + * Override to handle nodes with tagname 'searchpanel'. + * + * @override + * @private + */ + _processNode: function (node, fv) { + if (node.tag === 'searchpanel') { + if (!config.device.isMobile) { + this._processSearchPanelNode(node, fv); + } + return false; + } + return this._super.apply(this, arguments); + }, + /** + * Populate this.searchPanelSections with category/filter descriptions. + * + * @private + * @param {Object} node + * @param {Object} fv + */ + _processSearchPanelNode: function (node, fv) { + var self = this; + node.children.forEach(function (childNode, index) { + if (childNode.tag !== 'field') { + return; + } + if (childNode.attrs.invisible === "1") { + return; + } + var fieldName = childNode.attrs.name; + var type = childNode.attrs.select === 'multi' ? 'filter' : 'category'; + + var sectionId = _.uniqueId('section_'); + var section = { + color: childNode.attrs.color, + description: childNode.attrs.string || fv.fields[fieldName].string, + fieldName: fieldName, + icon: childNode.attrs.icon, + id: sectionId, + index: index, + type: type, + }; + if (section.type === 'category') { + section.icon = section.icon || 'fa-folder'; + } else if (section.type === 'filter') { + section.disableCounters = !!pyUtils.py_eval(childNode.attrs.disable_counters || '0'); + section.domain = childNode.attrs.domain || '[]'; + section.groupBy = childNode.attrs.groupby; + section.icon = section.icon || 'fa-filter'; + } + self.searchPanelSections[sectionId] = section; + }); + }, + /** + * @override + * @private + */ _updateMVCParams: function () { this._super.apply(this, arguments); var defaultGroupBy = this.arch.attrs.default_group_by; - this.loadParams.groupBy = defaultGroupBy ? [defaultGroupBy] : (this.loadParams.groupedBy || []); + this.loadParams.groupBy = defaultGroupBy ? + [defaultGroupBy] : + (this.loadParams.groupedBy || []); }, }); diff --git a/addons/web/static/src/js/views/kanban/search_panel.js b/addons/web/static/src/js/views/kanban/search_panel.js new file mode 100644 index 00000000000..9062714e0c1 --- /dev/null +++ b/addons/web/static/src/js/views/kanban/search_panel.js @@ -0,0 +1,527 @@ +odoo.define('web.SearchPanel', function (require) { +"use strict"; + +/** + * This file defines the SearchPanel widget for Kanban. It allows to + * filter/manage data easily. + */ + +var core = require('web.core'); +var Domain = require('web.Domain'); +var Widget = require('web.Widget'); + +var qweb = core.qweb; + +var SearchPanel = Widget.extend({ + className: 'o_search_panel', + events: { + 'click .o_search_panel_category_value header': '_onCategoryValueClicked', + 'click .o_search_panel_category_value .o_toggle_fold': '_onToggleFoldCategory', + 'click .o_search_panel_filter_group .o_toggle_fold': '_onToggleFoldFilterGroup', + 'change .o_search_panel_filter_value > div > input': '_onFilterValueChanged', + 'change .o_search_panel_filter_group > div > input': '_onFilterGroupChanged', + }, + + /** + * @override + * @param {Object} params + * @param {Object} [params.defaultCategoryValues={}] the category value to + * activate by default, for each category + * @param {Object} params.fields + * @param {string} params.model + * @param {Object} params.sections + * @param {Array[]} params.searchDomain domain coming from controlPanel + */ + init: function (parent, params) { + this._super.apply(this, arguments); + + this.categories = _.pick(params.sections, function (section) { + return section.type === 'category'; + }); + this.filters = _.pick(params.sections, function (section) { + return section.type === 'filter'; + }); + + this.defaultCategoryValues = params.defaultCategoryValues || {}; + this.fields = params.fields; + this.model = params.model; + this.searchDomain = params.searchDomain; + }, + /** + * @override + */ + willStart: function () { + var self = this; + var loadProm = this._fetchCategories().then(function () { + return self._fetchFilters(); + }); + return $.when(loadProm, this._super.apply(this, arguments)); + }, + /** + * @override + */ + start: function () { + this._render(); + return this._super.apply(this, arguments); + }, + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + /** + * @returns {Array[]} the current searchPanel domain based on active + * categories and checked filters + */ + getDomain: function () { + return this._getCategoryDomain().concat(this._getFilterDomain()); + }, + /** + * Reload the filters and re-render. Note that we only reload the filters if + * the controlPanel domain or searchPanel domain has changed. + * + * @param {Object} params + * @param {Array[]} params.searchDomain domain coming from controlPanel + * @returns {$.Promise} + */ + update: function (params) { + var currentSearchDomainStr = JSON.stringify(this.searchDomain); + var newSearchDomainStr = JSON.stringify(params.searchDomain); + var def; + if (this.needReload || (currentSearchDomainStr !== newSearchDomainStr)) { + this.needReload = false; + this.searchDomain = params.searchDomain; + def = this._fetchFilters(); + } + return $.when(def).then(this._render.bind(this)); + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * @private + * @param {string} categoryId + * @param {Object[]} values + */ + _createCategoryTree: function (categoryId, values) { + var category = this.categories[categoryId]; + var parentField = category.parentField; + + category.values = {}; + values.forEach(function (value) { + category.values[value.id] = _.extend({}, value, { + childrenIds: [], + folded: true, + parentId: value[parentField] && value[parentField][0] || false, + }); + }); + Object.keys(category.values).forEach(function (valueId) { + var value = category.values[valueId]; + if (value.parentId) { + category.values[value.parentId].childrenIds.push(value.id); + } + }); + category.rootIds = Object.keys(category.values).filter(function (valueId) { + var value = category.values[valueId]; + return value.parentId === false; + }); + + // set active value + var validValues = _.pluck(category.values, 'id').concat([false]); + // set active value from context + var value = this.defaultCategoryValues[category.fieldName]; + // if not set in context, or set to an unknown value, set active value + // from localStorage + if (!_.contains(validValues, value)) { + var storageKey = this._getLocalStorageKey(category); + value = this.call('local_storage', 'getItem', storageKey); + } + // if not set in localStorage either, select 'All' + category.activeValueId = _.contains(validValues, value) ? value : false; + + // unfold ancestor values of active value to make it is visible + if (category.activeValueId) { + var parentValueIds = this._getAncestorValueIds(category, category.activeValueId); + parentValueIds.forEach(function (parentValue) { + category.values[parentValue].folded = false; + }); + } + }, + /** + * @private + * @param {string} filterId + * @param {Object[]} values + */ + _createFilterTree: function (filterId, values) { + var filter = this.filters[filterId]; + + // restore checked property + values.forEach(function (value) { + var oldValue = filter.values && filter.values[value.id]; + value.checked = oldValue && oldValue.checked || false; + }); + + filter.values = {}; + var groupIds = []; + if (filter.groupBy) { + var groups = {}; + values.forEach(function (value) { + var groupId = value.group_id; + if (!groups[groupId]) { + if (groupId) { + groupIds.push(groupId); + } + groups[groupId] = { + folded: false, + id: groupId, + name: value.group_name, + values: {}, + tooltip: value.group_tooltip, + sequence: value.group_sequence, + sortedValueIds: [], + }; + // restore former checked and folded state + var oldGroup = filter.groups && filter.groups[groupId]; + groups[groupId].state = oldGroup && oldGroup.state || false; + groups[groupId].folded = oldGroup && oldGroup.folded || false; + } + groups[groupId].values[value.id] = value; + groups[groupId].sortedValueIds.push(value.id); + }); + filter.groups = groups; + filter.sortedGroupIds = _.sortBy(groupIds, function (groupId) { + return groups[groupId].sequence || groups[groupId].name; + }); + Object.keys(filter.groups).forEach(function (groupId) { + filter.values = _.extend(filter.values, filter.groups[groupId].values); + }); + } else { + values.forEach(function (value) { + filter.values[value.id] = value; + }); + filter.sortedValueIds = values.map(function (value) { + return value.id; + }); + } + }, + /** + * Fetch values for each category. This is done only once, at startup. + * + * @private + * @returns {$.Promise} resolved when all categories have been fetched + */ + _fetchCategories: function () { + var self = this; + var defs = Object.keys(this.categories).map(function (categoryId) { + var category = self.categories[categoryId]; + var field = self.fields[category.fieldName]; + var def; + if (field.type === 'selection') { + var values = field.selection.map(function (value) { + return {id: value[0], display_name: value[1]}; + }); + def = $.when(values); + } else { + def = self._rpc({ + method: 'search_panel_select_range', + model: self.model, + args: [category.fieldName], + }).then(function (result) { + category.parentField = result.parent_field; + return result.values; + }); + } + return def.then(function (values) { + self._createCategoryTree(categoryId, values); + }); + }); + return $.when.apply($, defs); + }, + /** + * Fetch values for each filter. This is done at startup, and at each reload + * (when the controlPanel or searchPanel domain changes). + * + * @private + * @returns {$.Promise} resolved when all filters have been fetched + */ + _fetchFilters: function () { + var self = this; + var evalContext = {}; + Object.keys(this.categories).forEach(function (categoryId) { + var category = self.categories[categoryId]; + evalContext[category.fieldName] = category.activeValueId; + }); + var categoryDomain = this._getCategoryDomain(); + var filterDomain = this._getFilterDomain(); + var defs = Object.keys(this.filters).map(function (filterId) { + var filter = self.filters[filterId]; + return self._rpc({ + method: 'search_panel_select_multi_range', + model: self.model, + args: [filter.fieldName], + kwargs: { + category_domain: categoryDomain, + comodel_domain: Domain.prototype.stringToArray(filter.domain, evalContext), + disable_counters: filter.disableCounters, + filter_domain: filterDomain, + group_by: filter.groupBy || false, + search_domain: self.searchDomain, + }, + }).then(function (values) { + self._createFilterTree(filterId, values); + }); + }); + return $.when.apply($, defs); + }, + /** + * Compute and return the domain based on the current active categories. + * + * @private + * @returns {Array[]} + */ + _getCategoryDomain: function () { + var self = this; + function categoryToDomain(domain, categoryId) { + var category = self.categories[categoryId]; + if (category.activeValueId) { + domain.push([category.fieldName, '=', category.activeValueId]); + } + return domain; + } + return Object.keys(this.categories).reduce(categoryToDomain, []); + }, + /** + * Compute and return the domain based on the current checked filters. + * The values of a single filter are combined using a simple rule: checked values within + * a same group are combined with an 'OR' (this is expressed as single condition using a list) + * and groups are combined with an 'AND' (expressed by concatenation of conditions). + * If a filter has no groups, its checked values are implicitely considered as forming + * a group (and grouped using an 'OR'). + * + * @private + * @returns {Array[]} + */ + _getFilterDomain: function () { + var self = this; + function getCheckedValueIds(values) { + return Object.keys(values).reduce(function (checkedValues, valueId) { + if (values[valueId].checked) { + checkedValues.push(values[valueId].id); + } + return checkedValues; + }, []); + } + function filterToDomain(domain, filterId) { + var filter = self.filters[filterId]; + if (filter.groups) { + Object.keys(filter.groups).forEach(function (groupId) { + var group = filter.groups[groupId]; + var checkedValues = getCheckedValueIds(group.values); + if (checkedValues.length) { + domain.push([filter.fieldName, 'in', checkedValues]); + } + }); + } else if (filter.values) { + var checkedValues = getCheckedValueIds(filter.values); + if (checkedValues.length) { + domain.push([filter.fieldName, 'in', checkedValues]); + } + } + return domain; + } + return Object.keys(this.filters).reduce(filterToDomain, []); + }, + /** + * The active id of each category is stored in the localStorage, s.t. it + * can be restored afterwards (when the action is reloaded, for instance). + * This function returns the key in the sessionStorage for a given category. + * + * @param {Object} category + * @returns {string} + */ + _getLocalStorageKey: function (category) { + return 'searchpanel_' + this.model + '_' + category.fieldName; + }, + /** + * @private + * @param {Object} category + * @param {integer} categoryValueId + * @returns {integer[]} list of ids of the ancestors of the given value in + * the given category + */ + _getAncestorValueIds: function (category, categoryValueId) { + var categoryValue = category.values[categoryValueId]; + var parentId = categoryValue.parentId; + if (parentId) { + return [parentId].concat(this._getAncestorValueIds(category, parentId)); + } + return []; + }, + /** + * @private + */ + _render: function () { + var self = this; + this.$el.empty(); + + // sort categories and filters according to their index + var categories = Object.keys(this.categories).map(function (categoryId) { + return self.categories[categoryId]; + }); + var filters = Object.keys(this.filters).map(function (filterId) { + return self.filters[filterId]; + }); + var sections = categories.concat(filters).sort(function (s1, s2) { + return s1.index - s2.index; + }); + + sections.forEach(function (section) { + if (Object.keys(section.values).length) { + if (section.type === 'category') { + self.$el.append(self._renderCategory(section)); + } else { + self.$el.append(self._renderFilter(section)); + } + } + }); + }, + /** + * @private + * @param {Object} category + * @returns {string} + */ + _renderCategory: function (category) { + return qweb.render('SearchPanel.Category', {category: category}); + }, + /** + * @private + * @param {Object} filter + * @returns {jQuery} + */ + _renderFilter: function (filter) { + var $filter = $(qweb.render('SearchPanel.Filter', {filter: filter})); + + // set group inputs in indeterminate state when necessary + Object.keys(filter.groups || {}).forEach(function (groupId) { + var state = filter.groups[groupId].state; + // group 'false' is not displayed + if (groupId !== 'false' && state === 'indeterminate') { + $filter + .find('.o_search_panel_filter_group[data-group-id=' + groupId + '] input') + .get(0) + .indeterminate = true; + } + }); + + return $filter; + }, + /** + * Compute the current searchPanel domain based on categories and filters, + * and notify environment of the domain change. + * + * Note that this assumes that the environment will update the searchPanel. + * This is done as such to ensure the coordination between the reloading of + * the searchPanel and the reloading of the data. + * + * @private + */ + _notifyDomainUpdated: function () { + this.needReload = true; + this.trigger_up('search_panel_domain_updated', { + domain: this.getDomain(), + }); + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * @private + * @param {MouseEvent} ev + */ + _onCategoryValueClicked: function (ev) { + ev.stopPropagation(); + var $item = $(ev.currentTarget).closest('.o_search_panel_category_value'); + var category = this.categories[$item.data('categoryId')]; + var valueId = $item.data('id') || false; + category.activeValueId = valueId; + var storageKey = this._getLocalStorageKey(category); + this.call('local_storage', 'setItem', storageKey, valueId); + this._notifyDomainUpdated(); + }, + /** + * @private + * @param {MouseEvent} ev + */ + _onFilterGroupChanged: function (ev) { + ev.stopPropagation(); + var $item = $(ev.target).closest('.o_search_panel_filter_group'); + var filter = this.filters[$item.data('filterId')]; + var groupId = $item.data('groupId'); + var group = filter.groups[groupId]; + group.state = group.state === 'checked' ? 'unchecked' : 'checked'; + Object.keys(group.values).forEach(function (valueId) { + group.values[valueId].checked = group.state === 'checked'; + }); + this._notifyDomainUpdated(); + }, + /** + * @private + * @param {MouseEvent} ev + */ + _onFilterValueChanged: function (ev) { + ev.stopPropagation(); + var $item = $(ev.target).closest('.o_search_panel_filter_value'); + var valueId = $item.data('valueId'); + var filter = this.filters[$item.data('filterId')]; + var value = filter.values[valueId]; + value.checked = !value.checked; + var group = filter.groups && filter.groups[value.group_id]; + if (group) { + var valuePartition = _.partition(Object.keys(group.values), function (valueId) { + return group.values[valueId].checked; + }); + if (valuePartition[0].length && valuePartition[1].length) { + group.state = 'indeterminate'; + } else if (valuePartition[0].length) { + group.state = 'checked'; + } else { + group.state = 'unchecked'; + } + } + this._notifyDomainUpdated(); + }, + /** + * @private + * @param {MouseEvent} ev + */ + _onToggleFoldCategory: function (ev) { + ev.preventDefault(); + ev.stopPropagation(); + var $item = $(ev.currentTarget).closest('.o_search_panel_category_value'); + var category = this.categories[$item.data('categoryId')]; + var valueId = $item.data('id'); + category.values[valueId].folded = !category.values[valueId].folded; + this._render(); + }, + /** + * @private + * @param {MouseEvent} ev + */ + _onToggleFoldFilterGroup: function (ev) { + ev.preventDefault(); + ev.stopPropagation(); + var $item = $(ev.currentTarget).closest('.o_search_panel_filter_group'); + var filter = this.filters[$item.data('filterId')]; + var groupId = $item.data('groupId'); + filter.groups[groupId].folded = !filter.groups[groupId].folded; + this._render(); + }, +}); + +return SearchPanel; + +}); |
