From 58ca40b03215ef4c6c575267494dc8bccc30a033 Mon Sep 17 00:00:00 2001 From: Jorge Pinna Puissant Date: Thu, 11 May 2023 12:09:05 +0000 Subject: [PATCH] [REF] web, *: move progress bar to kanban view The aim of this commit is to remove the specific complexity of the kanban's progress bar from the relational model, and put it into the kanban view itself. This commit prepares and is part of the task that aim to refactor the relational model and migrate it to owl. part-of task-id 3179751 closes odoo/odoo#119781 Signed-off-by: Aaron Bohy (aab) Co-authored-by: Aaron Bohy --- .../views/crm_kanban/crm_column_progress.js | 13 +- .../src/views/crm_kanban/crm_kanban_model.js | 33 -- .../views/crm_kanban/crm_kanban_renderer.xml | 2 +- .../src/views/crm_kanban/crm_kanban_view.js | 11 +- .../src/views/activity/activity_renderer.js | 13 +- .../src/views/activity/activity_renderer.xml | 2 +- .../src/views/kanban/kanban_controller.js | 38 +- .../src/views/kanban/kanban_controller.xml | 1 + .../static/src/views/kanban/kanban_header.js | 23 +- .../static/src/views/kanban/kanban_header.xml | 4 +- .../static/src/views/kanban/kanban_model.js | 331 +---------------- .../static/src/views/kanban/kanban_record.js | 11 +- .../src/views/kanban/kanban_renderer.js | 21 +- .../src/views/kanban/kanban_renderer.xml | 2 + .../src/views/kanban/progress_bar_hook.js | 350 ++++++++++++++++++ .../web/static/src/views/relational_model.js | 34 +- addons/web/static/src/views/utils.js | 4 + .../views/view_components/column_progress.js | 6 +- .../views/view_components/column_progress.xml | 16 +- .../static/tests/views/kanban_view_tests.js | 16 +- 20 files changed, 515 insertions(+), 416 deletions(-) create mode 100644 addons/web/static/src/views/kanban/progress_bar_hook.js diff --git a/addons/crm/static/src/views/crm_kanban/crm_column_progress.js b/addons/crm/static/src/views/crm_kanban/crm_column_progress.js index 2452bf91624..514d747ef2a 100644 --- a/addons/crm/static/src/views/crm_kanban/crm_column_progress.js +++ b/addons/crm/static/src/views/crm_kanban/crm_column_progress.js @@ -9,7 +9,7 @@ import { getCurrency } from "@web/core/currency"; export class CrmColumnProgress extends ColumnProgress { static props = { ...ColumnProgress.props, - progressAttributes: true, + progressBarState: { type: Object }, }; static template = "crm.ColumnProgress"; setup() { @@ -18,20 +18,19 @@ export class CrmColumnProgress extends ColumnProgress { this.showRecurringRevenue = false; onWillStart(async () => { - if (this.props.progressAttributes.recurring_revenue_sum_field) { + if (this.props.progressBarState.progressAttributes.recurring_revenue_sum_field) { this.showRecurringRevenue = await this.user.hasGroup("crm.group_use_recurring_revenues"); } }); } getRecurringRevenueGroupAggregate(group) { - const rrField = this.props.progressAttributes.recurring_revenue_sum_field; - const value = group.getAggregates(rrField.name); - const title = rrField.string || this.env._t("Count"); + const rrField = this.props.progressBarState.progressAttributes.recurring_revenue_sum_field; + const aggregatedValue = this.props.progressBarState.getAggregateValue(group, rrField); let currency = false; - if (value && rrField.currency_field) { + if (aggregatedValue.value && rrField.currency_field) { currency = getCurrency(session.company_currency_id); } - return { value, currency, title }; + return { ...aggregatedValue, currency }; } } diff --git a/addons/crm/static/src/views/crm_kanban/crm_kanban_model.js b/addons/crm/static/src/views/crm_kanban/crm_kanban_model.js index a006a7c0976..b3a32cfd4dd 100644 --- a/addons/crm/static/src/views/crm_kanban/crm_kanban_model.js +++ b/addons/crm/static/src/views/crm_kanban/crm_kanban_model.js @@ -11,19 +11,6 @@ export class CrmKanbanModel extends KanbanModel { } export class CrmKanbanDynamicGroupList extends CrmKanbanModel.DynamicGroupList { - /** - * @override - * - * Add the RRM field to the sumfields to fetch in read_group calls - */ - get sumFields() { - const result = super.sumFields; - if (this.model.progressAttributes.recurring_revenue_sum_field) { - result.push(this.model.progressAttributes.recurring_revenue_sum_field.name); - } - return result; - } - /** * @override * @@ -49,25 +36,5 @@ export class CrmKanbanDynamicGroupList extends CrmKanbanModel.DynamicGroupList { } } -export class CrmKanbanGroup extends CrmKanbanModel.Group { - /** - * This is called whenever the progress bar is changed, for example - * when filtering on a certain stage from the progress bar and is meant - * to update `sum_field` aggregated value. - * We also want to update the recurring revenue aggregate. - */ - updateAggregates(groupData) { - if (this.model.progressAttributes.recurring_revenue_sum_field) { - const rrField = this.model.progressAttributes.recurring_revenue_sum_field; - const group = groupData.find(g => this.valueEquals(g[this.groupByField.name])); - if (rrField) { - this.aggregates[rrField.name] = group ? group[rrField.name] : 0; - } - } - return super.updateAggregates(...arguments); - } -} - CrmKanbanModel.DynamicGroupList = CrmKanbanDynamicGroupList; -CrmKanbanModel.Group = CrmKanbanGroup; CrmKanbanModel.services = [...KanbanModel.services, "effect"]; diff --git a/addons/crm/static/src/views/crm_kanban/crm_kanban_renderer.xml b/addons/crm/static/src/views/crm_kanban/crm_kanban_renderer.xml index 776b13d7775..301cbdab7bf 100644 --- a/addons/crm/static/src/views/crm_kanban/crm_kanban_renderer.xml +++ b/addons/crm/static/src/views/crm_kanban/crm_kanban_renderer.xml @@ -2,7 +2,7 @@ - props.group.model.progressAttributes + props.progressBarState diff --git a/addons/crm/static/src/views/crm_kanban/crm_kanban_view.js b/addons/crm/static/src/views/crm_kanban/crm_kanban_view.js index c328dbf1efe..7900b671b76 100644 --- a/addons/crm/static/src/views/crm_kanban/crm_kanban_view.js +++ b/addons/crm/static/src/views/crm_kanban/crm_kanban_view.js @@ -10,7 +10,16 @@ export const crmKanbanView = { ...kanbanView, ArchParser: CrmKanbanArchParser, // Makes it easier to patch - Controller: class extends kanbanView.Controller {}, + Controller: class extends kanbanView.Controller { + get progressBarAggregateFields() { + const res = super.progressBarAggregateFields; + const progressAttributes = this.props.archInfo.progressAttributes; + if (progressAttributes && progressAttributes.recurring_revenue_sum_field) { + res.push(progressAttributes.recurring_revenue_sum_field); + } + return res; + } + }, Model: CrmKanbanModel, Renderer: CrmKanbanRenderer, }; diff --git a/addons/mail/static/src/views/activity/activity_renderer.js b/addons/mail/static/src/views/activity/activity_renderer.js index 9a0dbf051e1..b2a2c7510ee 100644 --- a/addons/mail/static/src/views/activity/activity_renderer.js +++ b/addons/mail/static/src/views/activity/activity_renderer.js @@ -75,7 +75,7 @@ export class ActivityRenderer extends Component { }; const typeId = group[0]; const isColumnFiltered = this.activeFilter.activityTypeId === group[0]; - const progressValue = isColumnFiltered ? this.activeFilter.progressValue : {active: null}; + const progressValue = isColumnFiltered ? this.activeFilter.progressValue : { active: null }; let totalCount = 0; for (const activities of Object.values(this.props.groupedActivities)) { @@ -85,9 +85,11 @@ export class ActivityRenderer extends Component { } } - const progressBars = []; + const progressBar = { + bars: [], + }; for (const [value, count] of Object.entries(types)) { - progressBars.push({ + progressBar.bars.push({ count: count.value, value, string: this.props.fields.activity_state.selection.find((e) => e[0] === value)[1], @@ -103,7 +105,7 @@ export class ActivityRenderer extends Component { data: { count: totalCount, filterProgressValue: (name) => this.onSetProgressBarState(typeId, name), - progressBars, + progressBar, progressValue, }, }; @@ -113,7 +115,8 @@ export class ActivityRenderer extends Component { return this.props.records.find((r) => r.resId === resId); } - onSetProgressBarState(typeId, name) { + onSetProgressBarState(typeId, bar) { + const name = bar.value; if (this.activeFilter.progressValue.active === name) { this.activeFilter.progressValue.active = null; this.activeFilter.activityTypeId = null; diff --git a/addons/mail/static/src/views/activity/activity_renderer.xml b/addons/mail/static/src/views/activity/activity_renderer.xml index c819a453ad7..2cbee5ab561 100644 --- a/addons/mail/static/src/views/activity/activity_renderer.xml +++ b/addons/mail/static/src/views/activity/activity_renderer.xml @@ -24,7 +24,7 @@ diff --git a/addons/web/static/src/views/kanban/kanban_controller.js b/addons/web/static/src/views/kanban/kanban_controller.js index 37926cf9ce3..12160785848 100644 --- a/addons/web/static/src/views/kanban/kanban_controller.js +++ b/addons/web/static/src/views/kanban/kanban_controller.js @@ -9,11 +9,13 @@ import { MultiRecordViewButton } from "@web/views/view_button/multi_record_view_ import { useViewButtons } from "@web/views/view_button/view_button_hook"; import { useSetupView } from "@web/views/view_hook"; import { KanbanRenderer } from "./kanban_renderer"; +import { useProgressBar } from "./progress_bar_hook"; import { SearchBar } from "@web/search/search_bar/search_bar"; import { useSearchBarToggler } from "@web/search/search_bar/search_bar_toggler"; import { CogMenu } from "@web/search/cog_menu/cog_menu"; import { Component, reactive, useRef } from "@odoo/owl"; +import { addDependencies } from "../utils"; const QUICK_CREATE_FIELD_TYPES = ["char", "boolean", "many2one", "selection", "many2many"]; @@ -24,6 +26,15 @@ export class KanbanController extends Component { this.actionService = useService("action"); const { Model, archInfo } = this.props; this.model = useModel(Model, this.modelParams); + if (archInfo.progressAttributes) { + const { activeBars } = this.props.state || {}; + this.progressBarState = useProgressBar( + archInfo.progressAttributes, + this.model, + this.progressBarAggregateFields, + activeBars + ); + } this.headerButtons = archInfo.headerButtons; const self = this; @@ -56,6 +67,7 @@ export class KanbanController extends Component { }, getLocalState: () => { return { + activeBars: this.progressBarState?.activeBars, rootState: this.model.root.exportState(), }; }, @@ -85,9 +97,12 @@ export class KanbanController extends Component { get modelParams() { const { resModel, fields, archInfo, limit, defaultGroupBy, state } = this.props; const { rootState } = state || {}; + + const activeFields = archInfo.activeFields; + addDependencies(this.progressBarAggregateFields, activeFields, fields); + return { - activeFields: archInfo.activeFields, - progressAttributes: archInfo.progressAttributes, + activeFields, fields: { ...fields }, resModel, handleField: archInfo.handleField, @@ -97,10 +112,20 @@ export class KanbanController extends Component { defaultOrder: archInfo.defaultOrder, viewMode: "kanban", openGroupsByDefault: true, + onRecordSaved: this.onRecordSaved.bind(this), rootState, }; } + get progressBarAggregateFields() { + const res = []; + const { progressAttributes } = this.props.archInfo; + if (progressAttributes && progressAttributes.sumField) { + res.push(progressAttributes.sumField); + } + return res; + } + get className() { if (this.env.isSmall && this.model.root.isGrouped) { const classList = (this.props.className || "").split(" "); @@ -162,6 +187,15 @@ export class KanbanController extends Component { return this.isQuickCreateField(list.groupByField); } + onRecordSaved(record) { + if (this.model.root.isGrouped) { + const group = this.model.root.groups.find((l) => + l.records.find((r) => r.id === record.id) + ); + this.progressBarState?.updateCounts(group); + } + } + async beforeExecuteActionButton(clickParams) {} async afterExecuteActionButton(clickParams) {} diff --git a/addons/web/static/src/views/kanban/kanban_controller.xml b/addons/web/static/src/views/kanban/kanban_controller.xml index d948992be94..7162b8c677c 100644 --- a/addons/web/static/src/views/kanban/kanban_controller.xml +++ b/addons/web/static/src/views/kanban/kanban_controller.xml @@ -49,6 +49,7 @@ scrollTop.bind="scrollTop" canQuickCreate="canQuickCreate" quickCreateState="quickCreateState" + progressBarState="progressBarState" /> diff --git a/addons/web/static/src/views/kanban/kanban_header.js b/addons/web/static/src/views/kanban/kanban_header.js index 2e885ba6a4b..db150abf726 100644 --- a/addons/web/static/src/views/kanban/kanban_header.js +++ b/addons/web/static/src/views/kanban/kanban_header.js @@ -30,6 +30,7 @@ export class KanbanHeader extends Component { quickCreateState: { type: Object }, scrollTop: { type: Function }, tooltipInfo: { type: Object }, + progressBarState: { type: true, optional: true }, }; setup() { @@ -59,6 +60,10 @@ export class KanbanHeader extends Component { // Getters // ------------------------------------------------------------------------ + get progressBar() { + return this.props.progressBarState?.getGroupInfo(this.group); + } + get group() { return this.props.group; } @@ -82,10 +87,9 @@ export class KanbanHeader extends Component { } get groupAggregate() { - const { sumField } = this.group.model.progressAttributes; - const value = this.group.getAggregates(sumField && sumField.name); - const title = sumField ? sumField.string : this.env._t("Count"); - return { value, title }; + const { group, progressBarState } = this.props; + const { sumField } = progressBarState.progressAttributes; + return progressBarState.getAggregateValue(group, sumField); } // ------------------------------------------------------------------------ @@ -121,13 +125,17 @@ export class KanbanHeader extends Component { body: this.env._t( "Are you sure that you want to archive all the records from this column?" ), - confirm: () => this.group.list.archive(), + confirm: async () => { + await this.group.list.archive(); + this.props.progressBarState?.updateCounts(this.group); + }, cancel: () => {}, }); } unarchiveGroup() { this.group.list.unarchive(); + this.props.progressBarState?.updateCounts(this.group); } deleteGroup() { @@ -190,4 +198,9 @@ export class KanbanHeader extends Component { canQuickCreate() { return this.props.canQuickCreate; } + + async onBarClicked(value) { + await this.props.progressBarState.selectBar(this.props.group.id, value); + this.props.scrollTop(); + } } diff --git a/addons/web/static/src/views/kanban/kanban_header.xml b/addons/web/static/src/views/kanban/kanban_header.xml index 8c4005cd7bd..6eb6fb6269a 100644 --- a/addons/web/static/src/views/kanban/kanban_header.xml +++ b/addons/web/static/src/views/kanban/kanban_header.xml @@ -51,8 +51,8 @@ -
- +
+
diff --git a/addons/web/static/src/views/kanban/kanban_model.js b/addons/web/static/src/views/kanban/kanban_model.js index 8ea5227b482..a673a63211b 100644 --- a/addons/web/static/src/views/kanban/kanban_model.js +++ b/addons/web/static/src/views/kanban/kanban_model.js @@ -1,6 +1,5 @@ /** @odoo-module **/ -import { Domain } from "@web/core/domain"; import { DynamicGroupList, DynamicRecordList, @@ -9,17 +8,7 @@ import { } from "@web/views/relational_model"; import { isRelational } from "@web/views/utils"; -/** - * @typedef ProgressBar - * @property {number} count - * @property {any} value - * @property {string} color - * @property {string} string - */ - -import { EventBus, markRaw } from "@odoo/owl"; - -const FALSE = Symbol("false"); +import { EventBus } from "@odoo/owl"; class TransactionInProgress extends Error {} @@ -65,207 +54,18 @@ class KanbanGroup extends Group { setup(_params, state = {}) { super.setup(...arguments); - /** @type {ProgressBar[]} */ - this.progressBars = this._generateProgressBars(); - this.progressValue = markRaw(state.progressValue || { active: null }); - this.list.domain = this.getProgressBarDomain(); this.tooltip = []; this.model.transaction.register({ onStart: () => ({ count: this.count, - progressBars: [...this.progressBars], records: [...this.list.records], }), - onAbort: ({ count, progressBars, records }) => { + onAbort: ({ count, records }) => { this.count = count; - this.progressBars = progressBars; this.list.records = records; }, }); - - this.model.bus.addEventListener("record-updated", ({ detail }) => { - const { record, relatedRecords } = detail; - const localIds = this.records.map((r) => r.id); - const updatedIds = [record, ...relatedRecords].map((r) => r.id); - if (localIds.some((id) => updatedIds.includes(id))) { - this.model.bus.trigger("group-updated", { - group: this, - withProgressBars: true, - }); - } - }); - } - - get activeProgressBar() { - return ( - this.hasActiveProgressValue && - this.progressBars.find((pv) => pv.value === this.progressValue.active) - ); - } - - get hasActiveProgressValue() { - return this.model.hasProgressBars && this.progressValue.active !== null; - } - - /** - * @override - */ - async deleteRecords() { - const records = await super.deleteRecords(...arguments); - this.model.bus.trigger("group-updated", { - group: this, - withProgressBars: true, - }); - return records; - } - - /** - * @override - */ - empty() { - super.empty(); - - this.progressValue.active = null; - for (const progressBar of this.progressBars) { - progressBar.count = 0; - } - } - - /** - * @override - */ - exportState() { - return { - ...super.exportState(), - progressValue: this.progressValue, - }; - } - - /** - * Checks if the current active progress bar value contains records, and - * deactivates it if not. - * @returns {Promise} - */ - async checkActiveValue() { - if (!this.hasActiveProgressValue) { - return; - } - if (this.activeProgressBar.count === 0) { - await this.filterProgressValue(null); - } - } - - async filterProgressValue(value) { - this.progressValue.active = this.progressValue.active === value ? null : value; - this.list.domain = this.getProgressBarDomain(); - - // Do not update progress bars data when filtering on them. - await Promise.all([this.list.load()]); - this.model.bus.trigger("group-updated", { group: this, withProgressBars: false }); - } - - /** - * @param {Object} record - * @returns {ProgressBar} - */ - findProgressValueFromRecord(record) { - const { fieldName } = this.model.progressAttributes; - const value = record.data[fieldName]; - return ( - this.progressBars.find((pv) => pv.value === value) || - this.progressBars.find((pv) => pv.value === FALSE) - ); - } - - /** - * @override - */ - getAggregates(fieldName) { - if (!this.hasActiveProgressValue) { - return super.getAggregates(...arguments); - } - return fieldName ? this.aggregates[fieldName] : this.activeProgressBar.count; - } - - getProgressBarDomain() { - const { fieldName } = this.model.progressAttributes; - const domains = [this.groupDomain]; - if (this.hasActiveProgressValue) { - if (this.progressValue.active === FALSE) { - const values = this.progressBars - .map((pv) => pv.value) - .filter((val) => val !== this.progressValue.active); - domains.push(["!", [fieldName, "in", values]]); - } else { - domains.push([[fieldName, "=", this.progressValue.active]]); - } - } - return Domain.and(domains).toList(); - } - - updateAggregates(groupData) { - const fname = this.groupByField.name; - const { sumField } = this.model.progressAttributes; - const group = groupData.find((g) => this.valueEquals(g[fname])); - if (sumField) { - this.aggregates[sumField.name] = group ? group[sumField.name] : 0; - } - } - - /** - * @param {Object} [progressData] - * @returns {Promise} - */ - async updateProgressData(progressData) { - let value = this.displayName || this.value; - if (value === true) { - value = "True"; - } else if (value === false) { - value = "False"; - } - /** @type {Record} */ - const groupProgressData = progressData[value] || {}; - /** @type {Map} */ - const counts = new Map( - groupProgressData ? Object.entries(groupProgressData) : [[FALSE, this.count]] - ); - const total = [...counts.values()].reduce((acc, c) => acc + c, 0); - counts.set(FALSE, this.count - total); - for (const pv of this.progressBars) { - pv.count = counts.get(pv.value) || 0; - } - await this.checkActiveValue(); - } - - // ------------------------------------------------------------------------ - // Protected - // ------------------------------------------------------------------------ - - /** - * @returns {ProgressBar[]} - */ - _generateProgressBars() { - if (!this.model.hasProgressBars) { - return []; - } - const { colors, fieldName } = this.model.progressAttributes; - const { selection: fieldSelection } = this.fields[fieldName]; - /** @type {[string | typeof FALSE, string][]} */ - const colorEntries = Object.entries(colors); - const selection = fieldSelection && Object.fromEntries(fieldSelection); - colorEntries.push([FALSE, "200"]); - return colorEntries.map(([value, color]) => { - let string; - if (value === FALSE) { - string = this.model.env._t("Other"); - } else if (selection) { - string = selection[value]; - } else { - string = String(value); - } - return { count: 0, value, string, color }; - }); } } @@ -276,25 +76,6 @@ export class KanbanDynamicGroupList extends DynamicGroupList { this.groupBy = this.groupBy.slice(0, 1); this.limit = null; - - this.model.bus.addEventListener("group-updated", async ({ detail }) => { - if (this.groups.some((g) => g.id === detail.group.id)) { - await this.updateGroupProgressData([detail.group], detail.withProgressBars); - this.model.notify(); - } - }); - } - - get sumFields() { - const { sumField } = this.model.progressAttributes; - return sumField ? [sumField.name] : []; - } - - /** - * @override - */ - get fieldNames() { - return [...super.fieldNames, ...this.sumFields]; } get currentParams() { @@ -316,68 +97,16 @@ export class KanbanDynamicGroupList extends DynamicGroupList { * @override */ async load() { - const load = async () => { - const previousGroups = this.groups.map((g, i) => [g, i]); - await super.load(); - if (this.previousParams === this.currentParams) { - for (const [group, index] of previousGroups) { - const newGroup = this.groups.find((g) => group.valueEquals(g.value)); - if (!group.deleted && !newGroup) { - group.empty(); - this.groups.splice(index, 0, group); - } + const previousGroups = this.groups.map((g, i) => [g, i]); + await super.load(); + if (this.previousParams === this.currentParams) { + for (const [group, index] of previousGroups) { + const newGroup = this.groups.find((g) => group.valueEquals(g.value)); + if (!group.deleted && !newGroup) { + group.empty(); + this.groups.splice(index, 0, group); } } - }; - await this._loadWithProgressData(load()); - } - - /** - * @param {KanbanGroup[]} groups - * @param {boolean} withProgressBars - * @returns {Promise} - */ - async updateGroupProgressData(groups, withProgressBars) { - if (!this.model.hasProgressBars) { - return; - } - - const gbFieldName = this.groupByField.name; - const promises = {}; - - if (withProgressBars) { - const domain = Domain.or(groups.map((g) => g.groupDomain)).toList(); - promises.readProgressBar = this._fetchProgressData(domain); - } - // If we have a sumField, the aggregates must be re-fetched - if (this.sumFields.length) { - const domain = Domain.or(groups.map((g) => g.getProgressBarDomain())).toList(); - promises.webReadGroup = this.model.orm.webReadGroup( - this.resModel, - domain, - this.sumFields, - this.groupBy, - { lazy: true } - ); - } - - await Promise.all(Object.values(promises)); - - // Update the aggregates for each group - if (promises.webReadGroup) { - const result = await promises.webReadGroup; - const groupData = result.groups.map((group) => ({ - ...group, - [gbFieldName]: this._getValueFromGroupData(group, this.firstGroupBy), - })); - for (const group of groups) { - group.updateAggregates(groupData); - } - } - // Update the progress bar data for each group - if (promises.readProgressBar) { - const result = await promises.readProgressBar; - await Promise.all(groups.map((group) => group.updateProgressData(result))); } } @@ -439,7 +168,6 @@ export class KanbanDynamicGroupList extends DynamicGroupList { groupsToReload.push(targetGroup); promises.push(record.load()); } - promises.push(this.updateGroupProgressData(groupsToReload, true)); await Promise.all(promises); } @@ -453,40 +181,6 @@ export class KanbanDynamicGroupList extends DynamicGroupList { return true; } - - // ------------------------------------------------------------------------ - // Protected - // ------------------------------------------------------------------------ - - /** - * @param {any[]} [domain] - * @returns {Promise} - */ - async _fetchProgressData(domain) { - const { colors, fieldName, help } = this.model.progressAttributes; - return this.model.orm.call(this.resModel, "read_progress_bar", [], { - domain, - group_by: this.firstGroupBy, - progress_bar: { colors, field: fieldName, help }, - context: this.context, - }); - } - - /** - * @param {Promise} loadPromise - * @returns {Promise} - */ - async _loadWithProgressData(loadPromise) { - if (!this.model.hasProgressBars) { - // No progress attributes : normal load - return loadPromise; - } - const [progressData] = await Promise.all([ - this._fetchProgressData(this.domain), - loadPromise, - ]); - await Promise.all(this.groups.map((group) => group.updateProgressData(progressData))); - } } export class KanbanDynamicRecordList extends DynamicRecordList { @@ -511,14 +205,9 @@ export class KanbanModel extends RelationalModel { setup(params) { super.setup(...arguments); - this.progressAttributes = params.progressAttributes; this.transaction = makeTransactionManager(); } - get hasProgressBars() { - return Boolean(this.progressAttributes); - } - /** * @override */ diff --git a/addons/web/static/src/views/kanban/kanban_record.js b/addons/web/static/src/views/kanban/kanban_record.js index e56f971666d..533f26175f2 100644 --- a/addons/web/static/src/views/kanban/kanban_record.js +++ b/addons/web/static/src/views/kanban/kanban_record.js @@ -234,7 +234,7 @@ export class KanbanRecord extends Component { } getRecordClasses() { - const { archInfo, canResequence, forceGlobalClick, group, record } = this.props; + const { archInfo, canResequence, forceGlobalClick, record, progressBarState } = this.props; const classes = ["o_kanban_record d-flex"]; if (canResequence) { classes.push("o_draggable"); @@ -242,9 +242,11 @@ export class KanbanRecord extends Component { if (forceGlobalClick || archInfo.openAction) { classes.push("oe_kanban_global_click"); } - if (group && record.model.hasProgressBars) { - const progressBar = group.findProgressValueFromRecord(record); - classes.push(`oe_kanban_card_${progressBar.color}`); + if (progressBarState) { + const { fieldName, colors } = progressBarState.progressAttributes; + const value = record.data[fieldName]; + const color = colors[value]; + classes.push(`oe_kanban_card_${color}`); } if (archInfo.cardColorField) { const value = record.data[archInfo.cardColorField]; @@ -399,6 +401,7 @@ KanbanRecord.props = [ "readonly?", "record", "templates", + "progressBarState?", ]; KanbanRecord.Compiler = KanbanCompiler; KanbanRecord.KANBAN_BOX_ATTRIBUTE = KANBAN_BOX_ATTRIBUTE; diff --git a/addons/web/static/src/views/kanban/kanban_renderer.js b/addons/web/static/src/views/kanban/kanban_renderer.js index 54ec4c84952..00e9d22f81a 100644 --- a/addons/web/static/src/views/kanban/kanban_renderer.js +++ b/addons/web/static/src/views/kanban/kanban_renderer.js @@ -56,6 +56,7 @@ export class KanbanRenderer extends Component { "scrollTop?", "canQuickCreate?", "quickCreateState?", + "progressBarState?", ]; static defaultProps = { @@ -297,10 +298,12 @@ export class KanbanRenderer extends Component { if (!this.env.isSmall && group.isFolded) { classes.push("o_column_folded", "flex-basis-0"); } - if (group.progressBars.length) { - classes.push("o_kanban_has_progressbar"); - if (!group.isFolded && group.hasActiveProgressValue) { - const progressBar = group.activeProgressBar; + if (this.props.progressBarState && !group.isFolded) { + const progressBarInfo = this.props.progressBarState.getGroupInfo(group); + if (progressBarInfo.activeBar) { + const progressBar = progressBarInfo.bars.find( + (b) => b.value === progressBarInfo.activeBar + ); classes.push("o_kanban_group_show", `o_kanban_group_show_${progressBar.color}`); } } @@ -308,9 +311,9 @@ export class KanbanRenderer extends Component { } getGroupUnloadedCount(group) { - const progressBar = group.activeProgressBar; const records = group.list.records.filter((r) => !r.isInQuickCreation); - return (progressBar ? progressBar.count : group.count) - records.length; + const count = this.props.progressBarState?.getGroupCount(group) || group.count; + return count - records.length; } generateGhostColumns() { @@ -367,6 +370,8 @@ export class KanbanRenderer extends Component { }); if (mode === "edit") { await this.props.openRecord(record, "edit"); + } else { + this.props.progressBarState?.updateCounts(group); } } @@ -457,6 +462,10 @@ export class KanbanRenderer extends Component { const refId = previous ? previous.dataset.id : null; const targetGroupId = parent?.dataset.id; await this.props.list.moveRecord(dataRecordId, dataGroupId, refId, targetGroupId); + if (dataGroupId !== targetGroupId) { + const group = this.props.list.groups.find((g) => g.id === dataGroupId); + this.props.progressBarState?.updateAggreagteGroup(group); + } this.toggleProcessing(dataRecordId, false); } diff --git a/addons/web/static/src/views/kanban/kanban_renderer.xml b/addons/web/static/src/views/kanban/kanban_renderer.xml index bc4c84414da..feb5e0b7f78 100644 --- a/addons/web/static/src/views/kanban/kanban_renderer.xml +++ b/addons/web/static/src/views/kanban/kanban_renderer.xml @@ -26,6 +26,7 @@ quickCreateState="props.quickCreateState" scrollTop="props.scrollTop" tooltipInfo="props.archInfo.tooltipInfo" + progressBarState="props.progressBarState" /> @@ -50,6 +51,7 @@ readonly="props.readonly" record="record" templates="props.archInfo.templateDocs" + progressBarState="props.progressBarState" /> diff --git a/addons/web/static/src/views/kanban/progress_bar_hook.js b/addons/web/static/src/views/kanban/progress_bar_hook.js new file mode 100644 index 00000000000..f156e90388d --- /dev/null +++ b/addons/web/static/src/views/kanban/progress_bar_hook.js @@ -0,0 +1,350 @@ +/** @odoo-module **/ + +import { onWillStart, onWillUpdateProps, reactive, useComponent } from "@odoo/owl"; +import { Domain } from "@web/core/domain"; +import { _t } from "@web/core/l10n/translation"; +const FALSE = Symbol("False"); + +/** + * + * @param {*} groups: returned by web_read_group + * @param {*} groupByField + * @param {*} value + * @returns + */ + +function _findGroup(groups, groupByField, value) { + return ( + groups.find((s) => { + if (Array.isArray(s[groupByField.name])) { + return s[groupByField.name][0] === value; + } else { + return s[groupByField.name] === value; + } + }) || {} + ); +} + +function _createFilterDomain(fieldName, bars, value) { + let filterDomain = undefined; + if (value === FALSE) { + const keys = bars.filter((x) => x.value !== FALSE).map((x) => x.value); + filterDomain = ["!", [fieldName, "in", keys]]; + } else { + filterDomain = [[fieldName, "=", value]]; + } + return filterDomain; +} + +class ProgressBarState { + constructor(progressAttributes, model, aggregateFields, activeBars = {}) { + this.progressAttributes = progressAttributes; + this.model = model; + this._groupsInfo = {}; + this._aggregateFields = aggregateFields; + this.activeBars = activeBars; + this._aggregateValues = []; + this._pbCounts = null; + } + + getGroupInfo(group) { + if (!this._groupsInfo[group.id]) { + if ( + !Object.keys(_findGroup(this._aggregateValues, group.groupByField, group.value)) + .length + ) { + this._aggregateValues.push({ + ...group.aggregates, + [group.groupByField.name]: group.value, + }); + } + let groupValue = group.displayName || group.value; + if (groupValue === true) { + groupValue = "True"; + } else if (groupValue === false) { + groupValue = "False"; + } + const pbCount = this._pbCounts[groupValue]; + const { fieldName, colors } = this.progressAttributes; + const { selection: fieldSelection } = this.model.root.fields[fieldName]; + const selection = fieldSelection && Object.fromEntries(fieldSelection); + const bars = Object.entries(colors).map(([value, color]) => { + let string; + if (selection) { + string = selection[value]; + } else { + string = String(value); + } + return { + count: (pbCount && pbCount[value]) || 0, + value, + string, + color, + }; + }); + bars.push({ + count: group.count - bars.map((r) => r.count).reduce((a, b) => a + b, 0), + value: FALSE, + string: _t("Other"), + color: "200", + }); + + if ( + this.activeBars[group.value] && + bars.find((b) => b.value === this.activeBars[group.value].value).count === 0 + ) { + group.applyFilter(undefined).then(() => { + delete this.activeBars[group.value]; + group.model.notify(); + }); + } + + const self = this; + const progressBar = { + get activeBar() { + return self.activeBars[group.value]?.value || null; + }, + bars, + }; + + this._groupsInfo[group.id] = progressBar; + } + return this._groupsInfo[group.id]; + } + + getAggregateValue(group, aggregateField) { + const title = aggregateField ? aggregateField.string : _t("Count"); + let value = 0; + if (!this.activeBars[group.value]) { + value = group.count; + if (aggregateField) { + value = + _findGroup(this._aggregateValues, group.groupByField, group.value)[ + aggregateField.name + ] || 0; + } + } else { + value = this.activeBars[group.value].count; + if (aggregateField) { + value = + (this.activeBars[group.value]?.aggregates && + this.activeBars[group.value]?.aggregates[aggregateField.name]) || + 0; + } + } + return { title, value }; + } + + async selectBar(groupId, bar) { + const group = this.model.root.groups.find((group) => group.id === groupId); + const progressBar = this._groupsInfo[groupId]; + const nextActiveBar = {}; + if (bar.value && this.activeBars[group.value]?.value !== bar.value) { + nextActiveBar.value = bar.value; + } else { + group.applyFilter(undefined).then(() => { + delete this.activeBars[group.value]; + group.model.notify(); + }); + return; + } + const { bars } = progressBar; + const filterDomain = _createFilterDomain( + this.progressAttributes.fieldName, + bars, + nextActiveBar.value + ); + const proms = []; + proms.push( + group.applyFilter(filterDomain).then((res) => { + const groupInfo = this._groupsInfo[group.id]; + nextActiveBar.count = groupInfo.bars.find( + (x) => x.value === nextActiveBar.value + ).count; + }) + ); + if (this._aggregateFields.length) { + proms.push(this._updateAggregateGroup(group, nextActiveBar)); + } + await Promise.all(proms); + this.activeBars[group.value] = nextActiveBar; + } + + _updateAggregateGroup(group, activeBar) { + const groupInfo = this._groupsInfo[group.id]; + const filterDomain = _createFilterDomain( + this.progressAttributes.fieldName, + groupInfo.bars, + activeBar.value + ); + const { context, groupBy, resModel } = this.model.root; + const kwargs = { context }; + const fieldNames = this._aggregateFields.map((f) => f.name); + const fields = [...fieldNames, group.groupByField.name]; + const domain = filterDomain + ? Domain.and([group.groupDomain, filterDomain]).toList() + : group.groupDomain; + return this.model.orm + .webReadGroup(resModel, domain, fields, groupBy, kwargs) + .then((res) => { + if (res.length) { + const resGroup = _findGroup(res.groups, group.groupByField, group.value); + activeBar.aggregates = { + ...resGroup, + [group.groupByField.name]: group.value, + }; + } + }); + } + + updateCounts(group) { + this._updateProgressBar(); + if (this._aggregateFields.length) { + this._updateAggregates(); + this.updateAggreagteGroup(group); + } + + // If the selected bar is empty, remove the selection + for (const group of this.model.root.groups) { + if (this.activeBars[group.value] && group.list.count === 0) { + this.selectBar(group.id, { value: null }); + } + } + } + + updateAggreagteGroup(group) { + if (group && this.activeBars[group.value]) { + this._updateAggregateGroup(group, this.activeBars[group.value]); + } + } + + async updateCountsArchive(group) { + const groupInfo = this._groupsInfo[group.id]; + for (const bar of groupInfo.bars) { + bar.count = 0; + } + if (this._aggregateFields.length) { + const key = Object.keys(this._aggregateValues).find( + (key) => this._aggregateValues[key][group.groupByField.name] === group.value + ); + for (const sumField of this._aggregateFields) { + this._aggregateValues[key][sumField.name] = 0; + } + } + if (this.activeBars[group.value] && group.list.count === 0) { + // If the selected bar is empty, remove the selection + this.selectBar(group.id, { value: null }); + } + } + + async _updateAggregates() { + const { context, groupBy, domain, resModel, firstGroupBy } = this.model.root; + const fieldsName = this._aggregateFields.map((f) => f.name); + const firstGroupByName = firstGroupBy.split(":")[0]; + const kwargs = { context }; + const res = await this.model.orm.webReadGroup( + resModel, + domain, + [...fieldsName, firstGroupByName], + groupBy, + kwargs + ); + this._aggregateValues = res.groups; + } + + async _updateProgressBar() { + const groupBy = this.model.root.groupBy; + const defaultGroupBy = this.model.root.defaultGroupBy; + if (groupBy.length || defaultGroupBy) { + const resModel = this.model.root.resModel; + const domain = this.model.root.domain; + const context = this.model.root.context; + const { colors, fieldName: field, help } = this.progressAttributes; + const groupsId = this.model.root.groups.map((g) => g.id).join(); + const res = await this.model.orm.call(resModel, "read_progress_bar", [], { + domain, + group_by: groupBy.length ? groupBy[0] : defaultGroupBy, + progress_bar: { colors, field, help }, + context, + }); + if (groupsId !== this.model.root.groups.map((g) => g.id).join()) { + return; + } + for (const group of this.model.root.groups) { + if (!group.isFolded) { + const groupInfo = this._groupsInfo[group.id]; + let groupValue = group.displayName || group.value; + if (groupValue === true) { + groupValue = "True"; + } else if (groupValue === false) { + groupValue = "False"; + } + const counts = res[groupValue]; + for (const bar of groupInfo.bars) { + bar.count = (counts && counts[bar.value]) || 0; + } + groupInfo.bars.find((b) => b.value === FALSE).count = counts + ? group.count - Object.values(counts).reduce((a, b) => a + b, 0) + : group.count; + + if (this.activeBars[group.value]) { + this.activeBars[group.value].count = groupInfo.bars.find( + (x) => x.value === this.activeBars[group.value].value + ).count; + } + } + } + } + } + + async loadProgressBar(props = {}) { + const groupBy = props.groupBy || this.model.root.groupBy; + const defaultGroupBy = + props.defaultGroupBy || (this.model.root && this.model.root.defaultGroupBy); + if (groupBy.length || defaultGroupBy) { + const resModel = props.resModel || this.model.root.resModel; + const domain = props.domain || this.model.root.domain; + const context = props.context || this.model.root.context; + const { colors, fieldName: field, help } = this.progressAttributes; + const res = await this.model.orm.call(resModel, "read_progress_bar", [], { + domain, + group_by: groupBy.length ? groupBy[0] : defaultGroupBy, + progress_bar: { colors, field, help }, + context, + }); + this._groupsInfo = {}; + this._pbCounts = res; + } + } + + getGroupCount(group) { + const progressBarInfo = this._groupsInfo[group.id]; + if (progressBarInfo.activeBar) { + const progressBar = progressBarInfo.bars.find( + (b) => b.value === progressBarInfo.activeBar + ); + return progressBar.count; + } + } +} + +export function useProgressBar(progressAttributes, model, aggregateFields, activeBars) { + const component = useComponent(); + + const progressBarState = new ProgressBarState( + progressAttributes, + model, + aggregateFields, + activeBars + ); + + // FIXME: maybe this can be do directly on the readGroup + onWillStart(() => { + return progressBarState.loadProgressBar(component.props); + }); + onWillUpdateProps((nextProps) => { + return progressBarState.loadProgressBar(nextProps); + }); + + return reactive(progressBarState); +} diff --git a/addons/web/static/src/views/relational_model.js b/addons/web/static/src/views/relational_model.js index ebd627c7bf7..13ce1edf9b7 100644 --- a/addons/web/static/src/views/relational_model.js +++ b/addons/web/static/src/views/relational_model.js @@ -330,9 +330,13 @@ class DataPoint { const commands = []; for (const command of value) { const code = command[0]; - if(code === CREATE){ - commands.push([code, command[1], this._cache[field.name]._parseServerValues(command[2])]); - }else{ + if (code === CREATE) { + commands.push([ + code, + command[1], + this._cache[field.name]._parseServerValues(command[2]), + ]); + } else { commands.push(command); } } @@ -2041,6 +2045,7 @@ export class DynamicRecordList extends DynamicList { async load(params = {}) { this.limit = params.limit === undefined ? this.limit : params.limit; this.offset = params.offset === undefined ? this.offset : params.offset; + this.domain = params.domain === undefined ? this.domain : params.domain; this.records = await this._loadRecords(); await this._adjustOffset(); } @@ -2557,6 +2562,9 @@ export class Group extends DataPoint { } else { this.isFolded = true; } + if ("groupFilterDomain" in state) { + this.groupFilterDomain = state.groupFilterDomain; + } if (isRelational(this.groupByField)) { // If the groupBy field is a relational field, the group model must // then be the relation of that field. @@ -2568,7 +2576,9 @@ export class Group extends DataPoint { } const listParams = { data: params.data, - domain: this.groupDomain, + domain: this.groupFilterDomain + ? Domain.and([this.groupDomain, this.groupFilterDomain]).toList() + : this.groupDomain, groupBy: params.groupBy, rawContext: params.rawContext, orderBy: params.orderBy, @@ -2601,6 +2611,17 @@ export class Group extends DataPoint { // Public // ------------------------------------------------------------------------ + applyFilter(filter) { + this.groupFilterDomain = filter; + if (this.groupFilterDomain) { + return this.list.load({ + domain: Domain.and([this.groupDomain, this.groupFilterDomain]).toList(), + }); + } else { + return this.list.load({ domain: this.groupDomain }); + } + } + /** * @see DynamicRecordList.addRecord */ @@ -2649,13 +2670,10 @@ export class Group extends DataPoint { return { isFolded: this.isFolded, listState: this.list.exportState(), + groupFilterDomain: this.groupFilterDomain, }; } - getAggregates(fieldName) { - return fieldName ? this.aggregates[fieldName] || 0 : this.count; - } - getServerValue() { const { name, selection, type, granularity } = this.groupByField; diff --git a/addons/web/static/src/views/utils.js b/addons/web/static/src/views/utils.js index c5101fe3df7..2a9c8a5acdf 100644 --- a/addons/web/static/src/views/utils.js +++ b/addons/web/static/src/views/utils.js @@ -53,6 +53,10 @@ export function addFieldDependencies(info, activeFields, fields) { const { fieldDependencies } = info.field || info.widget; const deps = typeof fieldDependencies === "function" ? fieldDependencies(info) : fieldDependencies; + addDependencies(deps, activeFields, fields); +} + +export function addDependencies(deps, activeFields, fields) { for (const dependency of deps || []) { const { name } = dependency; if (!(name in activeFields)) { diff --git a/addons/web/static/src/views/view_components/column_progress.js b/addons/web/static/src/views/view_components/column_progress.js index 0f918bcc0ea..4f83cf4ccaf 100644 --- a/addons/web/static/src/views/view_components/column_progress.js +++ b/addons/web/static/src/views/view_components/column_progress.js @@ -12,13 +12,13 @@ export class ColumnProgress extends Component { aggregate: { type: Object }, group: { type: Object }, onBarClicked: { type: Function, optional: true }, + progressBar: { type: Object }, }; static defaultProps = { onBarClicked: () => {}, }; - async onBarClick(progressBar) { - await this.props.group.filterProgressValue(progressBar.value); - this.props.onBarClicked(); + async onBarClick(bar) { + await this.props.onBarClicked(bar); } } diff --git a/addons/web/static/src/views/view_components/column_progress.xml b/addons/web/static/src/views/view_components/column_progress.xml index 509b5a59080..34fbc5d5aab 100644 --- a/addons/web/static/src/views/view_components/column_progress.xml +++ b/addons/web/static/src/views/view_components/column_progress.xml @@ -3,19 +3,19 @@
- -
+
diff --git a/addons/web/static/tests/views/kanban_view_tests.js b/addons/web/static/tests/views/kanban_view_tests.js index 871168d572b..9f18080867d 100644 --- a/addons/web/static/tests/views/kanban_view_tests.js +++ b/addons/web/static/tests/views/kanban_view_tests.js @@ -6718,7 +6718,6 @@ QUnit.module("Views", (hooks) => { "read_progress_bar", "web_read_group", "onchange", - "onchange", ]); } ); @@ -9270,7 +9269,6 @@ QUnit.module("Views", (hooks) => { "read", "read_progress_bar", "onchange", - "onchange", ]); }); @@ -9411,8 +9409,9 @@ QUnit.module("Views", (hooks) => { "web_search_read", "action_archive", "web_read_group", - "read_progress_bar", "web_search_read", + "read_progress_bar", + "web_read_group", ]); }); @@ -9465,8 +9464,9 @@ QUnit.module("Views", (hooks) => { "web_search_read", "action_archive", "web_read_group", - "read_progress_bar", "web_search_read", + "read_progress_bar", + "web_read_group", ]); } ); @@ -9552,8 +9552,6 @@ QUnit.module("Views", (hooks) => { // activate another filter (switching) "web_read_group", // recomputes aggregates "web_search_read", - // deactivate active filter - "web_read_group", // recomputes aggregates ]); }); @@ -9802,7 +9800,6 @@ QUnit.module("Views", (hooks) => { "read_progress_bar", "web_read_group", "onchange", - "onchange", ]); } ); @@ -9877,13 +9874,14 @@ QUnit.module("Views", (hooks) => { "read", "read_progress_bar", "web_read_group", + "web_read_group", "onchange", "onchange", "create", "read", "read_progress_bar", "web_read_group", - "onchange", + "web_read_group", "onchange", ]); } @@ -11135,8 +11133,8 @@ QUnit.module("Views", (hooks) => { assert.verifySteps([ "write", "read_progress_bar", - "read", // read happens is delayed by the ORM batcher "web_search_read", + "read", // read happens is delayed by the ORM batcher "/web/dataset/resequence", "read", ]);