diff options
Diffstat (limited to 'addons/web/static/src/js/control_panel/control_panel_model.js')
| -rw-r--r-- | addons/web/static/src/js/control_panel/control_panel_model.js | 1712 |
1 files changed, 0 insertions, 1712 deletions
diff --git a/addons/web/static/src/js/control_panel/control_panel_model.js b/addons/web/static/src/js/control_panel/control_panel_model.js deleted file mode 100644 index d438e5175cb..00000000000 --- a/addons/web/static/src/js/control_panel/control_panel_model.js +++ /dev/null @@ -1,1712 +0,0 @@ -odoo.define('web.ControlPanelModel', function (require) { - "use strict"; - - const Domain = require('web.Domain'); - const { Model } = require('web.model'); - const { parseArch } = require('web.viewUtils'); - const pyUtils = require('web.py_utils'); - - const { DEFAULT_INTERVAL, DEFAULT_PERIOD, FACET_ICONS, - getComparisonOptions, getIntervalOptions, getPeriodOptions, - constructDateDomain, rankInterval, yearSelected } = require('web.searchUtils'); - - const FAVORITE_PRIVATE_GROUP = 1; - const FAVORITE_SHARED_GROUP = 2; - - let filterId = 1; - let groupId = 1; - let groupNumber = 0; - - /** - * Control panel model - * - * The control panel model state is an object structured in the following way: - * - * { - * filters: Object{}, - * query: Object[], - * } - * - *------------------------------------------------------------------------- - * Filters - *------------------------------------------------------------------------- - * - * The keys are stringified numbers called 'filter ids'. - * The values are objects called 'filters'. - * - * Each filter has the following properties: - * @prop {number} id unique identifier, also the filter's corresponding key - * @prop {number} groupId the id of some group, actually the group itself, - * the (active) 'groups' are reconstructed in _getGroups. - * @prop {string} description the description of the filter - * @prop {string} type 'filter'|'groupBy'|'comparison'|'field'|'favorite' - * - * Other properties can be present according to the corresponding filter type: - * - * • type 'comparison': - * @prop {string} comparisonOptionId option identifier (@see COMPARISON_OPTIONS). - * @prop {string} dateFilterId the id of a date filter (filter of type 'filter' - * with isDateFilter=true) - * - * • type 'filter': - * @prop {number} groupNumber used to separate items in the 'Filters' menu - * @prop {string} [context] context - * @prop {boolean} [invisible] determine if the filter is accessible in the interface - * @prop {boolean} [isDefault] - * @if isDefault = true: - * > @prop {number} [defaultRank=-5] used to determine the order of - * > activation of default filters - * @prop {boolean} [isDateFilter] true if the filter comes from an arch node - * with a valid 'date' attribute. - * @if isDateFilter = true - * > @prop {boolean} [hasOptions=true] - * > @prop {string} defaultOptionId option identifier determined by - * > default_period attribute (@see PERIOD_OPTIONS). - * > Default set to DEFAULT_PERIOD. - * > @prop {string} fieldName determined by the value of 'date' attribute - * > @prop {string} fieldType 'date' or 'datetime', type of the corresponding field - * @else - * > @prop {string} domain - * - * • type 'groupBy': - * @prop {string} fieldName - * @prop {string} fieldType - * @prop {number} groupNumber used to separate items in the 'Group by' menu - * @prop {boolean} [isDefault] - * @if isDefault = true: - * > @prop {number} defaultRank used to determine the order of activation - * > of default filters - * @prop {boolean} [invisible] determine if the filter is accessible in the interface - * @prop {boolean} [hasOptions] true if field type is 'date' or 'datetime' - * @if hasOptions=true - * > @prop {string} defaultOptionId option identifier (see INTERVAL_OPTIONS) - * default set to DEFAULT_INTERVAL. - * - * • type 'field': - * @prop {string} fieldName - * @prop {string} fieldType - * @prop {string} [context] - * @prop {string} [domain] - * @prop {string} [filterDomain] - * @prop {boolean} [invisible] determine if the filter is accessible in the interface - * @prop {boolean} [isDefault] - * @prop {string} [operator] - * @if isDefault = true: - * > @prop {number} [defaultRank=-10] used to determine the order of - * > activation of filters - * > @prop {Object} defaultAutocompleteValue of the form { value, label, operator } - * - * • type: 'favorite': - * @prop {Object} [comparison] of the form {comparisonId, fieldName, fieldDescription, - * range, rangeDescription, comparisonRange, comparisonRangeDescription, } - * @prop {Object} context - * @prop {string} domain - * @prop {string[]} groupBys - * @prop {number} groupNumber 1 | 2, 2 if the favorite is shared - * @prop {string[]} orderedBy - * @prop {boolean} [removable=true] indicates that the favorite can be deleted - * @prop {number} serverSideId - * @prop {number} userId - * @prop {boolean} [isDefault] - * - *------------------------------------------------------------------------- - * Query - *------------------------------------------------------------------------- - * - * The query elements are objects called 'query elements'. - * - * Each query element has the following properties: - * @prop {number} filterId the id of some filter - * @prop {number} groupId the id of some group (actually the group itself) - * - * Other properties must be defined according to the corresponding filter type. - * - * • type 'comparison': - * @prop {string} dateFilterId the id of a date filter (filter of type 'filter' - * with hasOptions=true) - * @prop {string} type 'comparison', help when searching if a comparison is active - * - * • type 'filter' with hasOptions=true: - * @prop {string} optionId option identifier (@see PERIOD_OPTIONS) - * - * • type 'groupBy' with hasOptions=true: - * @prop {string} optionId option identifier (@see INTERVAL_OPTIONS) - * - * • type 'field': - * @prop {string} label description put in the facet (can be temporarilly missing) - * @prop {(string|number)} value used as the value of the generated domain - * @prop {string} operator used as the operator of the generated domain - * - * The query elements indicates what are the active filters and 'how' they are active. - * The key groupId has been added for simplicity. It could have been removed from query elements - * since the information is available on the corresponding filters. - * @extends Model - */ - class ControlPanelModel extends Model { - /** - * @param {Object} config - * @param {(string|number)} config.actionId - * @param {Object} config.env - * @param {string} config.modelName - * @param {Object} [config.importedState] - * @param {Array[]} [config.actionDomain=[]] - * @param {Object} [config.actionContext={}] - * @param {Object[]} [config.dynamicFilters=[]] - * @param {string[]} [config.searchMenuTypes=[]] - * @param {Object} [config.viewInfo={}] - * @param {boolean} [config.withSearchBar=true] - * - */ - constructor(config) { - super(); - - this._setProperties(config); - - if (this.withSearchBar) { - if (config.importedState) { - this.importState(config.importedState); - } else { - this._prepareInitialState(); - } - } - - this.isReady = Promise.all(this.labelPromises); - } - - //--------------------------------------------------------------------- - // Getters - //--------------------------------------------------------------------- - - /** - * @returns {(Object | undefined)} - */ - get activeComparison() { - return this.state.query.find(queryElem => queryElem.type === 'comparison'); - } - - //--------------------------------------------------------------------- - // Public - //--------------------------------------------------------------------- - - /** - * Activate a filter of type 'field' with given filterId with - * 'autocompleteValues' value, label, and operator. - * @param {Object} - */ - addAutoCompletionValues({ filterId, label, value, operator }) { - const queryElem = this.state.query.find(queryElem => - queryElem.filterId === filterId && - queryElem.value === value && - queryElem.operator === operator - ); - if (!queryElem) { - const { groupId } = this.state.filters[filterId]; - this.state.query.push({ filterId, groupId, label, value, operator }); - } else { - queryElem.label = label; - } - } - - /** - * Remove all the query elements from query. - */ - clearQuery() { - this.state.query = []; - } - - /** - * Create a new filter of type 'favorite' and activate it. - * A new group containing only that filter is created. - * The query is emptied before activating the new favorite. - * @param {Object} preFilter - * @returns {Promise} - */ - async createNewFavorite(preFilter) { - const preFavorite = await this._saveQuery(preFilter); - this.clearQuery(); - const filter = Object.assign(preFavorite, { - groupId, - id: filterId, - }); - this.state.filters[filterId] = filter; - this.state.query.push({ groupId, filterId }); - groupId++; - filterId++; - } - - /** - * Create new filters of type 'filter' and activate them. - * A new group containing only those filters is created. - * @param {Object[]} filters - * @returns {number[]} - */ - createNewFilters(prefilters) { - if (!prefilters.length) { - return []; - } - const newFilterIdS = []; - prefilters.forEach(preFilter => { - const filter = Object.assign(preFilter, { - groupId, - groupNumber, - id: filterId, - type: 'filter', - }); - this.state.filters[filterId] = filter; - this.state.query.push({ groupId, filterId }); - newFilterIdS.push(filterId); - filterId++; - }); - groupId++; - groupNumber++; - return newFilterIdS; - } - - /** - * Create a new filter of type 'groupBy' and activate it. - * It is added to the unique group of groupbys. - * @param {Object} field - */ - createNewGroupBy(field) { - const groupBy = Object.values(this.state.filters).find(f => f.type === 'groupBy'); - const filter = { - description: field.string || field.name, - fieldName: field.name, - fieldType: field.type, - groupId: groupBy ? groupBy.groupId : groupId++, - groupNumber, - id: filterId, - type: 'groupBy', - }; - this.state.filters[filterId] = filter; - if (['date', 'datetime'].includes(field.type)) { - filter.hasOptions = true; - filter.defaultOptionId = DEFAULT_INTERVAL; - this.toggleFilterWithOptions(filterId); - } else { - this.toggleFilter(filterId); - } - groupNumber++; - filterId++; - } - - /** - * Deactivate a group with provided groupId, i.e. delete the query elements - * with given groupId. - * @param {number} groupId - */ - deactivateGroup(groupId) { - this.state.query = this.state.query.filter( - queryElem => queryElem.groupId !== groupId - ); - this._checkComparisonStatus(); - } - - /** - * Delete a filter of type 'favorite' with given filterId server side and - * in control panel model. Of course the filter is also removed - * from the search query. - * @param {number} filterId - */ - async deleteFavorite(filterId) { - const { serverSideId } = this.state.filters[filterId]; - await this.env.dataManager.delete_filter(serverSideId); - const index = this.state.query.findIndex( - queryElem => queryElem.filterId === filterId - ); - delete this.state.filters[filterId]; - if (index >= 0) { - this.state.query.splice(index, 1); - } - } - - /** - * Return the state of the control panel model (the filters and the - * current query). This state can then be used in an other control panel - * model (with same key modelName). See importedState. - * @returns {Object} - */ - exportState() { - return JSON.parse(JSON.stringify(this.state)); - } - - /** - * Called by search bar to render the facets - * @returns {Object[]} - */ - getFacets() { - const accept = type => { - if (['groupBy', 'comparison'].includes(type) && !this.searchMenuTypes.includes(type)) { - return false; - } - return true; - }; - - const groups = this._getGroups(); - const facets = []; - for (const group of groups) { - const { activities, type, id } = group; - if (accept(type)) { - const facet = { - groupId: id, - separator: type === 'groupBy' ? ">" : this.env._t("or"), - values: this._getFacetDescriptions(activities, type), - }; - const icon = FACET_ICONS[type]; - if (icon) { - facet.icon = icon; - } else { - facet.title = activities[0].filter.description; - } - facets.push(facet); - } - } - return facets; - } - - /** - * Return an array containing enriched copies of the filters of the provided type. - * @param {string} type - * @returns {Object[]} - */ - getFiltersOfType(type) { - const filters = Object.values(this.state.filters).reduce( - (filters, filter) => { - if (filter.type === type && !filter.invisible) { - const filterQueryElements = this.state.query.filter( - queryElem => queryElem.filterId === filter.id - ); - const enrichedFilter = this._enrichFilterCopy(filter, filterQueryElements); - if (enrichedFilter) { - filters.push(enrichedFilter); - } - } - return filters; - }, - [] - ); - if (type === 'favorite') { - filters.sort((f1, f2) => f1.groupNumber - f2.groupNumber); - } - return filters; - } - - /** - * Principal objects used by controllers/models to fetch data. - * @returns {Object} An object called search query with keys domain, groupBy, - * context, orderedBy, and (optionally) timeRanges. - */ - getQuery() { - const requireEvaluation = true; - const groups = this._getGroups(); - const query = { - context: this._getContext(groups), - domain: this._getDomain(groups, requireEvaluation), - orderedBy: this._getOrderedBy(groups) - }; - if (this.searchMenuTypes.includes('groupBy')) { - const groupBy = this._getGroupBy(groups); - query.groupBy = groupBy; - } else { - query.groupBy = []; - } - if (this.searchMenuTypes.includes('comparison')) { - const timeRanges = this._getTimeRanges(requireEvaluation); - query.timeRanges = timeRanges || {}; - } - return query; - } - - /** - * Allow to reuse the state of a previous control panel model with same modelName. - * This is mainly used when switching views. - * @param {Object} state - */ - importState(state) { - Object.assign(this.state, state); - } - - /** - * This function won't do anything: its purpose is to call the dispatch - * method to trigger a 'search' event + reload the components. - */ - search() { } - - /** - * Activate/Deactivate a filter of type 'comparison' with provided id. - * At most one filter of type 'comparison' can be activated at every time. - * @param {string} filterId - */ - toggleComparison(filterId) { - const { groupId, dateFilterId } = this.state.filters[filterId]; - const queryElem = this.state.query.find(queryElem => - queryElem.type === 'comparison' && - queryElem.filterId === filterId - ); - // make sure only one comparison can be active - this.state.query = this.state.query.filter(queryElem => queryElem.type !== 'comparison'); - if (!queryElem) { - this.state.query.push({ groupId, filterId, dateFilterId, type: 'comparison', }); - } - } - - /** - * Activate or deactivate the simple filter with given filterId, i.e. - * add or remove a corresponding query element. - * @param {string} filterId - */ - toggleFilter(filterId) { - const index = this.state.query.findIndex( - queryElem => queryElem.filterId === filterId - ); - if (index >= 0) { - this.state.query.splice(index, 1); - } else { - const { groupId, type } = this.state.filters[filterId]; - if (type === 'favorite') { - this.state.query = []; - } - this.state.query.push({ groupId, filterId }); - } - } - - /** - * Used to toggle a query element { filterId, optionId, (groupId) }. - * This can impact the query in various form, e.g. add/remove other query elements - * in case the filter is of type 'filter'. - * @param {string} filterId - * @param {string} [optionId] - */ - toggleFilterWithOptions(filterId, optionId) { - const filter = this.state.filters[filterId]; - optionId = optionId || filter.defaultOptionId; - const option = this.optionGenerators.find(o => o.id === optionId); - - const index = this.state.query.findIndex( - queryElem => queryElem.filterId === filterId && queryElem.optionId === optionId - ); - - if (index >= 0) { - this.state.query.splice(index, 1); - if (filter.type === 'filter' && !yearSelected(this._getSelectedOptionIds(filterId))) { - // This is the case where optionId was the last option - // of type 'year' to be there before being removed above. - // Since other options of type 'month' or 'quarter' do - // not make sense without a year we deactivate all options. - this.state.query = this.state.query.filter( - queryElem => queryElem.filterId !== filterId - ); - } - } else { - this.state.query.push({ groupId: filter.groupId, filterId, optionId }); - if (filter.type === 'filter' && !yearSelected(this._getSelectedOptionIds(filterId))) { - // Here we add 'this_year' as options if no option of type - // year is already selected. - this.state.query.push({ - groupId: filter.groupId, - filterId, - optionId: option.defaultYearId, - }); - } - } - if (filter.type === 'filter') { - this._checkComparisonStatus(); - } - } - - /** - * TODO: the way it is done could be improved, but the actual state of the - * searchView doesn't allow to do much better. - * - * Update the domain of the search view by adding and/or removing filters. - * @param {Object[]} newFilters list of filters to add, described by - * objects with keys domain (the domain as an Array), description (the text - * to display in the facet) and type with value 'filter'. - * @param {number[]} filtersToRemove list of filter ids to remove - * (previously added ones) - * @returns {number[]} list of added filter ids (to pass as filtersToRemove - * for a further call to this function) - */ - updateFilters(newFilters, filtersToRemove) { - const newFilterIdS = this.createNewFilters(newFilters); - this.state.query = this.state.query.filter( - queryElem => !filtersToRemove.includes(queryElem.filterId) - ); - return newFilterIdS; - } - - //--------------------------------------------------------------------- - // Private - //--------------------------------------------------------------------- - - /** - * Activate the default favorite (if any) or all default filters. - * @private - */ - _activateDefaultFilters() { - if (this.defaultFavoriteId) { - // Activate default favorite - this.toggleFilter(this.defaultFavoriteId); - } else { - // Activate default filters - Object.values(this.state.filters) - .filter((f) => f.isDefault && f.type !== 'favorite') - .sort((f1, f2) => (f1.defaultRank || 100) - (f2.defaultRank || 100)) - .forEach(f => { - if (f.hasOptions) { - this.toggleFilterWithOptions(f.id); - } else if (f.type === 'field') { - let { operator, label, value } = f.defaultAutocompleteValue; - this.addAutoCompletionValues({ - filterId: f.id, - value, - operator, - label, - }); - } else { - this.toggleFilter(f.id); - } - }); - } - } - - /** - * This function populates the 'filters' object at initialization. - * The filters come from: - * - config.viewInfo.arch (types 'comparison', 'filter', 'groupBy', 'field'), - * - config.dynamicFilters (type 'filter'), - * - config.viewInfo.favoriteFilters (type 'favorite'), - * @private - */ - _addFilters() { - this._createGroupOfFavorites(); - this._createGroupOfFiltersFromArch(); - this._createGroupOfDynamicFilters(); - } - - /** - * If a comparison is active, check if it should become inactive. - * The comparison should become inactive if the corresponding date filter has become - * inactive. - * @private - */ - _checkComparisonStatus() { - const activeComparison = this.activeComparison; - if (!activeComparison) { - return; - } - const { dateFilterId } = activeComparison; - const dateFilterIsActive = this.state.query.some( - queryElem => queryElem.filterId === dateFilterId - ); - if (!dateFilterIsActive) { - this.state.query = this.state.query.filter( - queryElem => queryElem.type !== 'comparison' - ); - } - } - - /** - * Returns the active comparison timeRanges object. - * @private - * @param {Object} comparisonFilter - * @returns {Object | undefined} - */ - _computeTimeRanges(comparisonFilter) { - const { filterId } = this.activeComparison; - if (filterId !== comparisonFilter.id) { - return; - } - const { dateFilterId, comparisonOptionId } = comparisonFilter; - const { - fieldName, - fieldType, - description: dateFilterDescription, - } = this.state.filters[dateFilterId]; - - const selectedOptionIds = this._getSelectedOptionIds(dateFilterId); - - // compute range and range description - const { domain: range, description: rangeDescription } = constructDateDomain( - this.referenceMoment, fieldName, fieldType, selectedOptionIds, - ); - - // compute comparisonRange and comparisonRange description - const { - domain: comparisonRange, - description: comparisonRangeDescription, - } = constructDateDomain( - this.referenceMoment, fieldName, fieldType, selectedOptionIds, comparisonOptionId - ); - - return { - comparisonId: comparisonOptionId, - fieldName, - fieldDescription: dateFilterDescription, - range, - rangeDescription, - comparisonRange, - comparisonRangeDescription, - }; - } - - /** - * Starting from the array of date filters, create the filters of type - * 'comparison'. - * @private - * @param {Object[]} dateFilters - */ - _createGroupOfComparisons(dateFilters) { - const preFilters = []; - for (const dateFilter of dateFilters) { - for (const comparisonOption of this.comparisonOptions) { - const { id: dateFilterId, description } = dateFilter; - const preFilter = { - type: 'comparison', - comparisonOptionId: comparisonOption.id, - description: `${description}: ${comparisonOption.description}`, - dateFilterId, - }; - preFilters.push(preFilter); - } - } - this._createGroupOfFilters(preFilters); - } - - /** - * Add filters of type 'filter' determined by the key array this.dynamicFilters. - * @private - */ - _createGroupOfDynamicFilters() { - const pregroup = this.dynamicFilters.map(filter => { - return { - description: filter.description, - domain: JSON.stringify(filter.domain), - isDefault: true, - type: 'filter', - }; - }); - this._createGroupOfFilters(pregroup); - } - - /** - * Add filters of type 'favorite' determined by array this.favoriteFilters. - * @private - */ - _createGroupOfFavorites() { - this.favoriteFilters.forEach(irFilter => { - const favorite = this._irFilterToFavorite(irFilter); - this._createGroupOfFilters([favorite]); - if (this.activateDefaultFavorite && favorite.isDefault) { - this.defaultFavoriteId = favorite.id; - } - }); - } - - /** - * Using a list (a 'pregroup') of 'prefilters', create new filters in `state.filters` - * for each prefilter. The new filters belong to a same new group. - * @private - * @param {Object[]} pregroup, list of 'prefilters' - * @param {string} type - */ - _createGroupOfFilters(pregroup) { - pregroup.forEach(preFilter => { - const filter = Object.assign(preFilter, { groupId, id: filterId }); - this.state.filters[filterId] = filter; - if (!this.defaultFavoriteId && filter.isDefault && filter.type === 'field') { - this._prepareDefaultLabel(filter); - } - filterId++; - }); - groupId++; - } - - /** - * Parse the arch of a 'search' view and create corresponding filters and groups. - * - * A searchview arch may contain a 'searchpanel' node, but this isn't - * the concern of the ControlPanel (the SearchPanel will handle it). - * Ideally, this code should whitelist the tags to take into account - * instead of blacklisting the others, but with the current (messy) - * structure of a searchview arch, it's way simpler to do it that way. - * @private - */ - _createGroupOfFiltersFromArch() { - const children = this.parsedArch.children.filter( - child => child instanceof Object && child.tag !== 'searchpanel' - ); - const preFilters = children.reduce( - (preFilters, child) => { - if (child.tag === 'group') { - return [...preFilters, ...child.children.map(c => this._evalArchChild(c))]; - } else { - return [...preFilters, this._evalArchChild(child)]; - } - }, - [] - ); - preFilters.push({ tag: 'separator' }); - - // create groups and filters - let currentTag; - let currentGroup = []; - let pregroupOfGroupBys = []; - - preFilters.forEach(preFilter => { - if ( - preFilter.tag !== currentTag || - ['separator', 'field'].includes(preFilter.tag) - ) { - if (currentGroup.length) { - if (currentTag === 'groupBy') { - pregroupOfGroupBys = [...pregroupOfGroupBys, ...currentGroup]; - } else { - this._createGroupOfFilters(currentGroup); - } - } - currentTag = preFilter.tag; - currentGroup = []; - groupNumber++; - } - if (preFilter.tag !== 'separator') { - const filter = { - type: preFilter.tag, - // we need to codify here what we want to keep from attrs - // and how, for now I put everything. - // In some sence, some filter are active (totally determined, given) - // and others are passive (require input(s) to become determined) - // What is the right place to process the attrs? - }; - if (preFilter.attrs && JSON.parse(preFilter.attrs.modifiers || '{}').invisible) { - filter.invisible = true; - - var preFilterFieldName = null; - if (preFilter.tag == 'filter' && preFilter.attrs.date) { - preFilterFieldName = preFilter.attrs.date; - } else if (preFilter.tag == 'groupBy') { - preFilterFieldName = preFilter.attrs.fieldName; - } - if (preFilterFieldName && !this.fields[preFilterFieldName]) { - // In some case when a field is limited to specific groups - // on the model, we need to ensure to discard related filter - // as it may still be present in the view (in 'invisible' state) - return; - } - } - if (filter.type === 'filter' || filter.type === 'groupBy') { - filter.groupNumber = groupNumber; - } - this._extractAttributes(filter, preFilter.attrs); - currentGroup.push(filter); - } - }); - - if (pregroupOfGroupBys.length) { - this._createGroupOfFilters(pregroupOfGroupBys); - } - const dateFilters = Object.values(this.state.filters).filter( - (filter) => filter.isDateFilter - ); - if (dateFilters.length) { - this._createGroupOfComparisons(dateFilters); - } - } - - /** - * The parent of the control panel (an action or a controller) is not - * necessarily a component. So we need to notify it through a - * mechanism different from __notifyComponents. Here we use the fact - * that the controlPanelModel is an (owl) EventBus to communicate with - * the parent. - * @private - */ - _dispatch() { - this.trigger('search', this.getQuery()); - } - - /** - * Returns undefined or a copy of the provided filter with additional information - * used only outside of the control panel model, like in search bar or in the - * various menus. The value undefined is returned if the filter should not appear - * for some reason. - * @private - * @param {Object} filter - * @param {Object[]} filterQueryElements - * @returns {Object|undefined} - */ - _enrichFilterCopy(filter, filterQueryElements) { - const isActive = Boolean(filterQueryElements.length); - const f = Object.assign({ isActive }, filter); - - function _enrichOptions(options) { - return options.map(o => { - const { description, id, groupNumber } = o; - const isActive = filterQueryElements.some(a => a.optionId === id); - return { description, id, groupNumber, isActive }; - }); - } - - switch (f.type) { - case 'comparison': { - const { dateFilterId } = filter; - const dateFilterIsActive = this.state.query.some( - queryElem => queryElem.filterId === dateFilterId - ); - if (!dateFilterIsActive) { - return; - } - break; - } - case 'filter': - if (f.hasOptions) { - f.options = _enrichOptions(this.optionGenerators); - } - break; - case 'groupBy': - if (f.hasOptions) { - f.options = _enrichOptions(this.intervalOptions); - } - break; - case 'field': - f.autoCompleteValues = filterQueryElements.map( - ({ label, value, operator }) => ({ label, value, operator }) - ); - break; - } - return f; - } - - /** - * Process a given arch node and enrich it. - * @private - * @param {Object} child - * @returns {Object} - */ - _evalArchChild(child) { - if (child.attrs.context) { - try { - const context = pyUtils.eval('context', child.attrs.context); - child.attrs.context = context; - if (context.group_by) { - // let us extract basic data since we just evaluated context - // and use a correct tag! - child.attrs.fieldName = context.group_by.split(':')[0]; - child.attrs.defaultInterval = context.group_by.split(':')[1]; - child.tag = 'groupBy'; - } - } catch (e) { } - } - if (this.searchDefaults.hasOwnProperty(child.attrs.name)) { - child.attrs.isDefault = true; - let value = this.searchDefaults[child.attrs.name]; - if (child.tag === 'field') { - if (value instanceof Array) { - value = value[0]; - } - child.attrs.defaultAutocompleteValue = { value, operator: '=' }; - } else if (child.tag === 'groupBy') { - child.attrs.defaultRank = typeof value === 'number' ? value : 100; - } - } - return child; - } - - /** - * Process the attributes set on an arch node and adds various keys to - * the given filter. - * @private - * @param {Object} filter - * @param {Object} attrs - */ - _extractAttributes(filter, attrs) { - if (attrs.isDefault) { - filter.isDefault = attrs.isDefault; - } - filter.description = attrs.string || attrs.help || attrs.name || attrs.domain || 'Ω'; - switch (filter.type) { - case 'filter': - if (attrs.context) { - filter.context = attrs.context; - } - if (attrs.date) { - filter.isDateFilter = true; - filter.hasOptions = true; - filter.fieldName = attrs.date; - filter.fieldType = this.fields[attrs.date].type; - filter.defaultOptionId = attrs.default_period || DEFAULT_PERIOD; - } else { - filter.domain = attrs.domain || '[]'; - } - if (filter.isDefault) { - filter.defaultRank = -5; - } - break; - case 'groupBy': - filter.fieldName = attrs.fieldName; - filter.fieldType = this.fields[attrs.fieldName].type; - if (['date', 'datetime'].includes(filter.fieldType)) { - filter.hasOptions = true; - filter.defaultOptionId = attrs.defaultInterval || DEFAULT_INTERVAL; - } - if (filter.isDefault) { - filter.defaultRank = attrs.defaultRank; - } - break; - case 'field': { - const field = this.fields[attrs.name]; - filter.fieldName = attrs.name; - filter.fieldType = field.type; - if (attrs.domain) { - filter.domain = attrs.domain; - } - if (attrs.filter_domain) { - filter.filterDomain = attrs.filter_domain; - } else if (attrs.operator) { - filter.operator = attrs.operator; - } - if (attrs.context) { - filter.context = attrs.context; - } - if (filter.isDefault) { - filter.defaultRank = -10; - filter.defaultAutocompleteValue = attrs.defaultAutocompleteValue; - } - if (attrs.widget) { // FIXME: drop support in master - filter.widget = attrs.widget; - } - break; - } - } - if (filter.fieldName && !attrs.string) { - const { string } = this.fields[filter.fieldName]; - filter.description = string; - } - } - - /** - * Returns an object irFilter serving to create an ir_filte in db - * starting from a filter of type 'favorite'. - * @private - * @param {Object} favorite - * @returns {Object} - */ - _favoriteToIrFilter(favorite) { - const irFilter = { - action_id: this.actionId, - model_id: this.modelName, - }; - - // ir.filter fields - if ('description' in favorite) { - irFilter.name = favorite.description; - } - if ('domain' in favorite) { - irFilter.domain = favorite.domain; - } - if ('isDefault' in favorite) { - irFilter.is_default = favorite.isDefault; - } - if ('orderedBy' in favorite) { - const sort = favorite.orderedBy.map( - ob => ob.name + (ob.asc === false ? " desc" : "") - ); - irFilter.sort = JSON.stringify(sort); - } - if ('serverSideId' in favorite) { - irFilter.id = favorite.serverSideId; - } - if ('userId' in favorite) { - irFilter.user_id = favorite.userId; - } - - // Context - const context = Object.assign({}, favorite.context); - if ('groupBys' in favorite) { - context.group_by = favorite.groupBys; - } - if ('comparison' in favorite) { - context.comparison = favorite.comparison; - } - if (Object.keys(context).length) { - irFilter.context = context; - } - - return irFilter; - } - - /** - * Return the domain resulting from the combination of the auto-completion - * values of a filter of type 'field'. - * @private - * @param {Object} filter - * @param {Object[]} filterQueryElements - * @returns {string} - */ - _getAutoCompletionFilterDomain(filter, filterQueryElements) { - const domains = filterQueryElements.map(({ label, value, operator }) => { - let domain; - if (filter.filterDomain) { - domain = Domain.prototype.stringToArray( - filter.filterDomain, - { - self: label, - raw_value: value, - } - ); - } else { - // Create new domain - domain = [[filter.fieldName, operator, value]]; - } - return Domain.prototype.arrayToString(domain); - }); - return pyUtils.assembleDomains(domains, 'OR'); - } - - /** - * Construct a single context from the contexts of - * filters of type 'filter', 'favorite', and 'field'. - * @private - * @returns {Object} - */ - _getContext(groups, withActionContext = true) { - const types = ['filter', 'favorite', 'field']; - const contexts = groups.reduce( - (contexts, group) => { - if (types.includes(group.type)) { - contexts.push(...this._getGroupContexts(group)); - } - return contexts; - }, - [] - ); - if (withActionContext) { - contexts.unshift(this.actionContext); - } - const evaluationContext = this.env.session.user_context; - try { - return pyUtils.eval('contexts', contexts, evaluationContext); - } catch (err) { - throw new Error( - this.env._t("Failed to evaluate search context") + ":\n" + - JSON.stringify(err) - ); - } - } - - /** - * Compute the string representation or the description of the current domain associated - * with a date filter starting from its corresponding query elements. - * @private - * @param {Object} filter - * @param {Object[]} filterQueryElements - * @param {'domain'|'description'} [key='domain'] - * @returns {string} - */ - _getDateFilterDomain(filter, filterQueryElements, key = 'domain') { - const { fieldName, fieldType } = filter; - const selectedOptionIds = filterQueryElements.map(queryElem => queryElem.optionId); - const dateFilterRange = constructDateDomain( - this.referenceMoment, fieldName, fieldType, selectedOptionIds, - ); - return dateFilterRange[key]; - } - - /** - * Return the string or array representation of a domain created by combining - * appropriately (with an 'AND') the domains coming from the active groups - * of type 'filter', 'favorite', and 'field'. - * @private - * @param {Object[]} groups - * @param {boolean} [evaluation=true] - * @returns {string} - */ - _getDomain(groups, evaluation = true) { - const types = ['filter', 'favorite', 'field']; - const domains = groups.reduce( - (domains, group) => { - if (types.includes(group.type)) { - domains.push(this._getGroupDomain(group)); - } - return domains; - }, - [] - ); - let filterDomain = pyUtils.assembleDomains(domains, 'AND'); - - if (evaluation) { - const userContext = this.env.session.user_context; - try { - return pyUtils.eval('domains', [this.actionDomain, filterDomain], userContext); - } catch (err) { - throw new Error( - this.env._t("Failed to evaluate search domain") + ":\n" + - JSON.stringify(err) - ); - } - } else { - return filterDomain; - } - } - - /** - * Return the context of the provided (active) filter. - * @private - * @param {Object} filter - * @param {Object[]} filterQueryElements - * @returns {Object} - */ - _getFilterContext(filter, filterQueryElements) { - let context = filter.context || {}; - // for <field> nodes, a dynamic context (like context="{'field1': self}") - // should set {'field1': [value1, value2]} in the context - if (filter.type === 'field' && filter.context) { - context = pyUtils.eval('context', - filter.context, - { self: filterQueryElements.map(({ value }) => value) }, - ); - } - // the following code aims to remodel this: - // https://github.com/odoo/odoo/blob/12.0/addons/web/static/src/js/views/search/search_inputs.js#L498 - // this is required for the helpdesk tour to pass - // this seems weird to only do that for m2o fields, but a test fails if - // we do it for other fields (my guess being that the test should simply - // be adapted) - if (filter.type === 'field' && filter.isDefault && filter.fieldType === 'many2one') { - context[`default_${filter.fieldName}`] = filter.defaultAutocompleteValue.value; - } - return context; - } - - /** - * Return the domain of the provided filter. - * @private - * @param {Object} filter - * @param {Object[]} filterQueryElements - * @returns {string} domain, string representation of a domain - */ - _getFilterDomain(filter, filterQueryElements) { - if (filter.type === 'filter' && filter.hasOptions) { - const { dateFilterId } = this.activeComparison || {}; - if (this.searchMenuTypes.includes('comparison') && dateFilterId === filter.id) { - return "[]"; - } - return this._getDateFilterDomain(filter, filterQueryElements); - } else if (filter.type === 'field') { - return this._getAutoCompletionFilterDomain(filter, filterQueryElements); - } - return filter.domain; - } - - /** - * Return the groupBys of the provided filter. - * @private - * @param {Object} filter - * @param {Object[]} filterQueryElements - * @returns {string[]} groupBys - */ - _getFilterGroupBys(filter, filterQueryElements) { - if (filter.type === 'groupBy') { - const fieldName = filter.fieldName; - if (filter.hasOptions) { - return filterQueryElements.map( - ({ optionId }) => `${fieldName}:${optionId}` - ); - } else { - return [fieldName]; - } - } else { - return filter.groupBys; - } - } - - /** - * Return the concatenation of groupBys comming from the active filters of - * type 'favorite' and 'groupBy'. - * The result respects the appropriate logic: the groupBys - * coming from an active favorite (if any) come first, then come the - * groupBys comming from the active filters of type 'groupBy' in the order - * defined in this.state.query. If no groupBys are found, one tries to - * find some grouBys in the action context. - * @private - * @param {Object[]} groups - * @returns {string[]} - */ - _getGroupBy(groups) { - const groupBys = groups.reduce( - (groupBys, group) => { - if (['groupBy', 'favorite'].includes(group.type)) { - groupBys.push(...this._getGroupGroupBys(group)); - } - return groupBys; - }, - [] - ); - const groupBy = groupBys.length ? groupBys : (this.actionContext.group_by || []); - return typeof groupBy === 'string' ? [groupBy] : groupBy; - } - - /** - * Return the list of the contexts of the filters active in the given - * group. - * @private - * @param {Object} group - * @returns {Object[]} - */ - _getGroupContexts(group) { - const contexts = group.activities.reduce( - (ctx, qe) => [...ctx, this._getFilterContext(qe.filter, qe.filterQueryElements)], - [] - ); - return contexts; - } - - /** - * Return the string representation of a domain created by combining - * appropriately (with an 'OR') the domains coming from the filters - * active in the given group. - * @private - * @param {Object} group - * @returns {string} string representation of a domain - */ - _getGroupDomain(group) { - const domains = group.activities.map(({ filter, filterQueryElements }) => { - return this._getFilterDomain(filter, filterQueryElements); - }); - return pyUtils.assembleDomains(domains, 'OR'); - } - - /** - * Return the groupBys coming form the filters active in the given group. - * @private - * @param {Object} group - * @returns {string[]} - */ - _getGroupGroupBys(group) { - const groupBys = group.activities.reduce( - (gb, qe) => [...gb, ...this._getFilterGroupBys(qe.filter, qe.filterQueryElements)], - [] - ); - return groupBys; - } - - /** - * Reconstruct the (active) groups from the query elements. - * @private - * @returns {Object[]} - */ - _getGroups() { - const groups = this.state.query.reduce( - (groups, queryElem) => { - const { groupId, filterId } = queryElem; - let group = groups.find(group => group.id === groupId); - const filter = this.state.filters[filterId]; - if (!group) { - const { type } = filter; - group = { - id: groupId, - type, - activities: [] - }; - groups.push(group); - } - group.activities.push(queryElem); - return groups; - }, - [] - ); - groups.forEach(g => this._mergeActivities(g)); - return groups; - } - - /** - * Used to get the key orderedBy of the active favorite. - * @private - * @param {Object[]} groups - * @returns {string[]} orderedBy - */ - _getOrderedBy(groups) { - return groups.reduce( - (orderedBy, group) => { - if (group.type === 'favorite') { - const favoriteOrderedBy = group.activities[0].filter.orderedBy; - if (favoriteOrderedBy) { - // Group order is reversed but inner order is kept - orderedBy = [...favoriteOrderedBy, ...orderedBy]; - } - } - return orderedBy; - }, - [] - ); - } - - /** - * Starting from the id of a date filter, returns the array of option ids currently selected - * for the corresponding filter. - * @private - * @param {string} dateFilterId - * @returns {string[]} - */ - _getSelectedOptionIds(dateFilterId) { - const selectedOptionIds = []; - for (const queryElem of this.state.query) { - if (queryElem.filterId === dateFilterId) { - selectedOptionIds.push(queryElem.optionId); - } - } - return selectedOptionIds; - } - - /** - * Returns a filter of type 'favorite' starting from an ir_filter comming from db. - * @private - * @param {Object} irFilter - * @returns {Object} - */ - _irFilterToFavorite(irFilter) { - let userId = irFilter.user_id || false; - if (Array.isArray(userId)) { - userId = userId[0]; - } - const groupNumber = userId ? FAVORITE_PRIVATE_GROUP : FAVORITE_SHARED_GROUP; - const context = pyUtils.eval('context', irFilter.context, this.env.session.user_context); - let groupBys = []; - if (context.group_by) { - groupBys = context.group_by; - delete context.group_by; - } - let comparison; - if (context.comparison) { - comparison = context.comparison; - delete context.comparison; - } - let sort; - try { - sort = JSON.parse(irFilter.sort); - } catch (err) { - if (err instanceof SyntaxError) { - sort = []; - } else { - throw err; - } - } - const orderedBy = sort.map(order => { - let fieldName; - let asc; - const sqlNotation = order.split(' '); - if (sqlNotation.length > 1) { - // regex: \fieldName (asc|desc)?\ - fieldName = sqlNotation[0]; - asc = sqlNotation[1] === 'asc'; - } else { - // legacy notation -- regex: \-?fieldName\ - fieldName = order[0] === '-' ? order.slice(1) : order; - asc = order[0] === '-' ? false : true; - } - return { - asc: asc, - name: fieldName, - }; - }); - const favorite = { - context, - description: irFilter.name, - domain: irFilter.domain, - groupBys, - groupNumber, - orderedBy, - removable: true, - serverSideId: irFilter.id, - type: 'favorite', - userId, - }; - if (irFilter.is_default) { - favorite.isDefault = irFilter.is_default; - } - if (comparison) { - favorite.comparison = comparison; - } - return favorite; - } - - /** - * Group the query elements in group.activities by qe -> qe.filterId - * and changes the form of group.activities to make it more suitable for further - * computations. - * @private - * @param {Object} group - */ - _mergeActivities(group) { - const { activities, type } = group; - let res = []; - switch (type) { - case 'filter': - case 'groupBy': { - for (const activity of activities) { - const { filterId } = activity; - let a = res.find(({ filter }) => filter.id === filterId); - if (!a) { - a = { - filter: this.state.filters[filterId], - filterQueryElements: [] - }; - res.push(a); - } - a.filterQueryElements.push(activity); - } - break; - } - case 'favorite': - case 'field': - case 'comparison': { - // all activities in the group have same filterId - const { filterId } = group.activities[0]; - const filter = this.state.filters[filterId]; - res.push({ - filter, - filterQueryElements: group.activities - }); - break; - } - } - if (type === 'groupBy') { - res.forEach(activity => { - activity.filterQueryElements.sort( - (qe1, qe2) => rankInterval(qe1.optionId) - rankInterval(qe2.optionId) - ); - }); - } - group.activities = res; - } - - /** - * Set the key label in defaultAutocompleteValue used by default filters of - * type 'field'. - * @private - * @param {Object} filter - */ - _prepareDefaultLabel(filter) { - const { id, fieldType, fieldName, defaultAutocompleteValue } = filter; - const { selection, context, relation } = this.fields[fieldName]; - if (fieldType === 'selection') { - defaultAutocompleteValue.label = selection.find( - sel => sel[0] === defaultAutocompleteValue.value - )[1]; - } else if (fieldType === 'many2one') { - const updateLabel = label => { - const queryElem = this.state.query.find(({ filterId }) => filterId === id); - queryElem.label = label; - defaultAutocompleteValue.label = label; - }; - const promise = this.env.services.rpc({ - args: [defaultAutocompleteValue.value], - context: context, - method: 'name_get', - model: relation, - }) - .then(results => updateLabel(results[0][1])) - .guardedCatch(() => updateLabel(defaultAutocompleteValue.value)); - this.labelPromises.push(promise); - } else { - defaultAutocompleteValue.label = defaultAutocompleteValue.value; - } - } - - /** - * @private - */ - _prepareInitialState() { - this._addFilters(); - this._activateDefaultFilters(); - } - - /** - * Compute the search Query and save it as an ir_filter in db. - * No evaluation of domains is done in order to keep them dynamic. - * If the operation is successful, a new filter of type 'favorite' is - * created and activated. - * @private - * @param {Object} preFilter - * @returns {Promise<Object>} - */ - async _saveQuery(preFilter) { - const groups = this._getGroups(); - - const userContext = this.env.session.user_context; - let controllerQueryParams; - this.trigger( - 'get-controller-query-params', - p => { - controllerQueryParams = p; - } - ); - controllerQueryParams = controllerQueryParams || {}; - controllerQueryParams.context = controllerQueryParams.context || {}; - - const withoutActiveContext = false; - const queryContext = this._getContext(groups, withoutActiveContext); - const context = pyUtils.eval( - 'contexts', - [userContext, controllerQueryParams.context, queryContext] - ); - for (const key in userContext) { - delete context[key]; - } - - const requireEvaluation = false; - const domain = this._getDomain(groups, requireEvaluation); - const groupBys = this._getGroupBy(groups); - const timeRanges = this._getTimeRanges(requireEvaluation); - const orderedBy = controllerQueryParams.orderedBy ? - controllerQueryParams.orderedBy : - (this._getOrderedBy(groups) || []); - - const userId = preFilter.isShared ? false : this.env.session.uid; - delete preFilter.isShared; - - Object.assign(preFilter, { - context, - domain, - groupBys, - groupNumber: userId ? FAVORITE_PRIVATE_GROUP : FAVORITE_SHARED_GROUP, - orderedBy, - removable: true, - userId, - }); - if (timeRanges) { - preFilter.comparison = timeRanges; - } - const irFilter = this._favoriteToIrFilter(preFilter); - const serverSideId = await this.env.dataManager.create_filter(irFilter); - - preFilter.serverSideId = serverSideId; - - return preFilter; - } - - /** - * Get the filter description to use in the search bar as a facet. - * @private - * @param {Object} activity - * @param {Object} activity.filter - * @param {Object[]} activity.filterQueryElements - * @returns {string} - */ - _getFacetDescriptions(activities, type) { - const facetDescriptions = []; - if (type === 'field') { - for (const queryElem of activities[0].filterQueryElements) { - facetDescriptions.push(queryElem.label); - } - } else if (type === 'groupBy') { - for (const { filter, filterQueryElements } of activities) { - if (filter.hasOptions) { - for (const queryElem of filterQueryElements) { - const option = this.intervalOptions.find( - o => o.id === queryElem.optionId - ); - facetDescriptions.push(filter.description + ': ' + option.description); - } - } else { - facetDescriptions.push(filter.description); - } - } - } else { - let facetDescription; - for (const { filter, filterQueryElements } of activities) { - // filter, favorite and comparison - facetDescription = filter.description; - if (filter.isDateFilter) { - const description = this._getDateFilterDomain( - filter, filterQueryElements, 'description' - ); - facetDescription += `: ${description}`; - } - facetDescriptions.push(facetDescription); - } - } - return facetDescriptions; - } - - /** - * Returns the last timeRanges object found in the query. - * TimeRanges objects can be associated with filters of type 'favorite' - * or 'comparison'. - * @private - * @param {boolean} [evaluation=false] - * @returns {(Object|undefined)} - */ - _getTimeRanges(evaluation) { - let timeRanges; - for (const queryElem of this.state.query.slice().reverse()) { - const filter = this.state.filters[queryElem.filterId]; - if (filter.type === 'comparison') { - timeRanges = this._computeTimeRanges(filter); - break; - } else if (filter.type === 'favorite' && filter.comparison) { - timeRanges = filter.comparison; - break; - } - } - if (timeRanges) { - if (evaluation) { - timeRanges.range = Domain.prototype.stringToArray(timeRanges.range); - timeRanges.comparisonRange = Domain.prototype.stringToArray(timeRanges.comparisonRange); - } - return timeRanges; - } - } - - /** - * Using the constructor parameter object config, set most of the properties - * of the control panel model. - * @private - * @param {Object} config - */ - _setProperties(config) { - this.state = { - filters: {}, - query: [], - }; - this.env = config.env; - this.modelName = config.modelName; - this.actionDomain = config.actionDomain || []; - this.actionContext = config.actionContext || {}; - this.actionId = config.actionId; - this.withSearchBar = 'withSearchBar' in config ? config.withSearchBar : true; - this.searchMenuTypes = config.searchMenuTypes || []; - - this.searchDefaults = []; - for (const key in this.actionContext) { - const match = /^search_default_(.*)$/.exec(key); - if (match) { - const val = this.actionContext[key]; - if (val) { - this.searchDefaults[match[1]] = val; - } - delete this.actionContext[key]; - } - } - this.labelPromises = []; - - const viewInfo = config.viewInfo || {}; - - this.parsedArch = parseArch(viewInfo.arch || '<search/>'); - this.fields = viewInfo.fields || {}; - this.favoriteFilters = viewInfo.favoriteFilters || []; - this.activateDefaultFavorite = 'search_disable_custom_filters' in this.actionContext ? - !this.actionContext.search_disable_custom_filters : - true; - - this.dynamicFilters = config.dynamicFilters || []; - - this.referenceMoment = moment(); - this.optionGenerators = getPeriodOptions(this.referenceMoment); - this.intervalOptions = getIntervalOptions(); - this.comparisonOptions = getComparisonOptions(); - } - } - - return ControlPanelModel; -}); |
