[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) <aab@odoo.com>
Co-authored-by: Aaron Bohy <aab@odoo.com>
This commit is contained in:
Jorge Pinna Puissant
2023-05-15 11:41:20 +02:00
committed by Aaron Bohy
co-authored by Aaron Bohy
parent d145b1e627
commit 58ca40b032
20 changed files with 515 additions and 416 deletions
@@ -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 };
}
}
@@ -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"];
@@ -2,7 +2,7 @@
<templates>
<t t-name="crm.CrmKanbanHeader" t-inherit="web.KanbanHeader" t-inherit-mode="primary" owl="1">
<xpath expr="//ColumnProgress" position="attributes">
<attribute name="progressAttributes">props.group.model.progressAttributes</attribute>
<attribute name="progressBarState">props.progressBarState</attribute>
</xpath>
</t>
</templates>
@@ -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,
};
@@ -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;
@@ -24,7 +24,7 @@
</div>
<div class="o_activity_counter d-flex align-items-center justify-content-between mb-3 h-0 mt24">
<t t-set="group" t-value="getGroupInfo(type)"/>
<ColumnProgress t-if="group.data.count" group="group.data" aggregate="group.aggregate" />
<ColumnProgress t-if="group.data.count" group="group.data" aggregate="group.aggregate" progressBar="group.data.progressBar" onBarClicked.bind="group.data.filterProgressValue" />
</div>
</th>
<th class="align-middle" style="width: 32px; min-width: 32px">
@@ -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) {}
@@ -49,6 +49,7 @@
scrollTop.bind="scrollTop"
canQuickCreate="canQuickCreate"
quickCreateState="quickCreateState"
progressBarState="progressBarState"
/>
</Layout>
</div>
@@ -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();
}
}
@@ -51,8 +51,8 @@
<i class="fa fa-caret-right lh-lg" role="img" aria-label="Unfold" title="Unfold" />
</button>
</div>
<div t-if="(env.isSmall or !group.isFolded) and group.progressBars.length" class="o_kanban_counter position-relative d-flex align-items-center justify-content-between" t-att-class="{ 'opacity-25': !group.count }">
<ColumnProgress group="group" aggregate="groupAggregate" onBarClicked="props.scrollTop" />
<div t-if="(env.isSmall or !group.isFolded) and progressBar" class="o_kanban_counter position-relative d-flex align-items-center justify-content-between" t-att-class="{ 'opacity-25': !group.count }">
<ColumnProgress group="group" progressBar="progressBar" aggregate="groupAggregate" onBarClicked.bind="onBarClicked" />
</div>
</div>
</t>
@@ -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<void>}
*/
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<void>}
*/
async updateProgressData(progressData) {
let value = this.displayName || this.value;
if (value === true) {
value = "True";
} else if (value === false) {
value = "False";
}
/** @type {Record<string, number>} */
const groupProgressData = progressData[value] || {};
/** @type {Map<string | symbol, number>} */
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<void>}
*/
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<Object>}
*/
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<any>} loadPromise
* @returns {Promise<void>}
*/
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
*/
@@ -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;
@@ -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);
}
@@ -26,6 +26,7 @@
quickCreateState="props.quickCreateState"
scrollTop="props.scrollTop"
tooltipInfo="props.archInfo.tooltipInfo"
progressBarState="props.progressBarState"
/>
<t t-if="!group.isFolded">
<t t-if="group.id === props.quickCreateState.groupId">
@@ -50,6 +51,7 @@
readonly="props.readonly"
record="record"
templates="props.archInfo.templateDocs"
progressBarState="props.progressBarState"
/>
</t>
<t t-set="unloadedCount" t-value="getGroupUnloadedCount(group)" />
@@ -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);
}
@@ -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;
+4
View File
@@ -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)) {
@@ -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);
}
}
@@ -3,19 +3,19 @@
<t t-name="web.ColumnProgress" owl="1">
<div class="o_column_progress progress bg-300 w-75">
<t t-foreach="props.group.progressBars" t-as="progressBar" t-key="progressBar.value">
<div t-if="progressBar.count"
<t t-foreach="props.progressBar.bars" t-as="bar" t-key="bar.value">
<div t-if="bar.count"
role="progressbar"
class="progress-bar o_bar_has_records cursor-pointer"
t-att-class="{ 'progress-bar-animated progress-bar-striped': props.group.progressValue.active === progressBar.value, 'border border-white': !props.group.isFolded and props.group.hasActiveProgressValue }"
t-attf-class="bg-{{ progressBar.color }}"
t-attf-style="width: {{ progressBar.count / (props.group.count or 1) * 100 }}%;"
t-att-class="{ 'progress-bar-animated progress-bar-striped': props.progressBar.activeBar === bar.value, 'border border-white': !props.group.isFolded and props.progressBar.activeBar }"
t-attf-class="bg-{{ bar.color }}"
t-attf-style="width: {{ bar.count / (props.group.count or 1) * 100 }}%;"
aria-valuemin="0"
t-att-aria-valuemax="props.group.count"
t-att-aria-valuenow="progressBar.count"
t-attf-data-tooltip="{{ progressBar.count }} {{ progressBar.string }}"
t-att-aria-valuenow="bar.count"
t-attf-data-tooltip="{{ bar.count }} {{ bar.string }}"
data-tooltip-delay="0"
t-on-click="() => this.onBarClick(progressBar)"
t-on-click="() => this.onBarClick(bar)"
/>
</t>
</div>
@@ -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",
]);