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", ]);