[IMP] web: remove read in kanban grouped with group_by_tooltip
This commit introduces an optimization to reduce the number of read operations performed in kanban views that are grouped by a field with a group_by_tooltip. Specifically, we now fetch the required values from group_by_tooltip only when the tooltip is first opened, instead of performing a read every time the view is loaded. Part of Task: 3179751 closes odoo/odoo#117758 Related: odoo/enterprise#39349 Signed-off-by: Aaron Bohy (aab) <aab@odoo.com>
This commit is contained in:
@@ -2,10 +2,18 @@
|
||||
|
||||
import { CrmColumnProgress } from "./crm_column_progress";
|
||||
import { KanbanRenderer } from "@web/views/kanban/kanban_renderer";
|
||||
import { KanbanHeader } from "@web/views/kanban/kanban_header";
|
||||
|
||||
class CrmKanbanHeader extends KanbanHeader {
|
||||
static template = "crm.CrmKanbanHeader";
|
||||
static components = {
|
||||
...KanbanHeader.components,
|
||||
ColumnProgress: CrmColumnProgress,
|
||||
};
|
||||
}
|
||||
|
||||
export class CrmKanbanRenderer extends KanbanRenderer {}
|
||||
CrmKanbanRenderer.template = "crm.CrmKanbanRenderer";
|
||||
CrmKanbanRenderer.components = {
|
||||
...KanbanRenderer.components,
|
||||
CrmColumnProgress,
|
||||
KanbanHeader: CrmKanbanHeader,
|
||||
};
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates>
|
||||
<t t-name="crm.CrmKanbanRenderer" t-inherit="web.KanbanRenderer" t-inherit-mode="primary" owl="1">
|
||||
<xpath expr="//ColumnProgress" position="replace">
|
||||
<CrmColumnProgress group="group" aggregate="getGroupAggregate(group)" onBarClicked="props.scrollTop" progressAttributes="props.list.model.progressAttributes"/>
|
||||
<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>
|
||||
</xpath>
|
||||
</t>
|
||||
</templates>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates>
|
||||
<t t-name="crm.ForecastKanbanRenderer" t-inherit="crm.CrmKanbanRenderer" t-inherit-mode="primary" owl="1">
|
||||
<t t-name="crm.ForecastKanbanRenderer" t-inherit="web.KanbanRenderer" t-inherit-mode="primary" owl="1">
|
||||
<KanbanColumnQuickCreate position="replace">
|
||||
<t t-if="isGroupedByForecastField()">
|
||||
<ForecastKanbanColumnQuickCreate
|
||||
|
||||
@@ -33,7 +33,6 @@ export class KanbanController extends Component {
|
||||
defaultOrder: archInfo.defaultOrder,
|
||||
viewMode: "kanban",
|
||||
openGroupsByDefault: true,
|
||||
tooltipInfo: archInfo.tooltipInfo,
|
||||
rootState,
|
||||
});
|
||||
this.headerButtons = archInfo.headerButtons;
|
||||
|
||||
@@ -0,0 +1,201 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { Component, useRef } from "@odoo/owl";
|
||||
import { ConfirmationDialog } from "@web/core/confirmation_dialog/confirmation_dialog";
|
||||
import { Dropdown } from "@web/core/dropdown/dropdown";
|
||||
import { DropdownItem } from "@web/core/dropdown/dropdown_item";
|
||||
import { usePopover } from "@web/core/popover/popover_hook";
|
||||
import { memoize } from "@web/core/utils/functions";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
import { sprintf } from "@web/core/utils/strings";
|
||||
import { useDebounced } from "@web/core/utils/timing";
|
||||
import { isNull, isRelational } from "@web/views/utils";
|
||||
import { ColumnProgress } from "@web/views/view_components/column_progress";
|
||||
import { FormViewDialog } from "@web/views/view_dialogs/form_view_dialog";
|
||||
|
||||
class KanbanHeaderTooltip extends Component {
|
||||
static template = "web.KanbanGroupTooltip";
|
||||
}
|
||||
|
||||
export class KanbanHeader extends Component {
|
||||
static template = "web.KanbanHeader";
|
||||
static components = { ColumnProgress, Dropdown, DropdownItem };
|
||||
static props = {
|
||||
activeActions: { type: Object },
|
||||
canQuickCreate: { type: Boolean },
|
||||
deleteGroup: { type: Function },
|
||||
dialogClose: { type: Array },
|
||||
group: { type: Object },
|
||||
list: { type: Object },
|
||||
quickCreateState: { type: Object },
|
||||
scrollTop: { type: Function },
|
||||
tooltipInfo: { type: Object },
|
||||
};
|
||||
|
||||
setup() {
|
||||
this.dialog = useService("dialog");
|
||||
this.orm = useService("orm");
|
||||
|
||||
this.rootRef = useRef("root");
|
||||
const titleRef = useRef("title");
|
||||
|
||||
const popover = usePopover();
|
||||
|
||||
let shouldOpenTooltip;
|
||||
const debouncedOpenTooltip = useDebounced(async () => {
|
||||
shouldOpenTooltip = true;
|
||||
if (this.hasTooltip) {
|
||||
const tooltip = await this.loadTooltip();
|
||||
if (tooltip.length && shouldOpenTooltip) {
|
||||
this.closeTooltip = popover.add(titleRef.el, KanbanHeaderTooltip, {
|
||||
tooltip: tooltip,
|
||||
});
|
||||
shouldOpenTooltip = false;
|
||||
}
|
||||
}
|
||||
}, 400);
|
||||
|
||||
this.openTooltip = () => {
|
||||
this.closeTooltip = () => {
|
||||
shouldOpenTooltip = false;
|
||||
debouncedOpenTooltip.cancel();
|
||||
};
|
||||
debouncedOpenTooltip();
|
||||
};
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Getters
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
get group() {
|
||||
return this.props.group;
|
||||
}
|
||||
|
||||
get groupName() {
|
||||
const { groupByField, count, displayName, isFolded } = this.group;
|
||||
let name = displayName;
|
||||
if (groupByField.type === "boolean") {
|
||||
name = name ? this.env._t("Yes") : this.env._t("No");
|
||||
} else if (!name) {
|
||||
if (
|
||||
isRelational(groupByField) ||
|
||||
groupByField.type === "date" ||
|
||||
groupByField.type === "datetime" ||
|
||||
isNull(name)
|
||||
) {
|
||||
name = this.env._t("None");
|
||||
}
|
||||
}
|
||||
return !this.env.isSmall && isFolded ? `${name} (${count})` : name;
|
||||
}
|
||||
|
||||
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 };
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Tooltip methods
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
get hasTooltip() {
|
||||
const { name, type } = this.group.groupByField;
|
||||
return type === "many2one" && this.group.value && name in this.props.tooltipInfo;
|
||||
}
|
||||
|
||||
loadTooltip = memoize(async () => {
|
||||
const { name, relation: resModel } = this.group.groupByField;
|
||||
const tooltipInfo = this.props.tooltipInfo[name];
|
||||
const fieldNames = Object.keys(tooltipInfo);
|
||||
const [values] = await this.orm.silent.read(
|
||||
resModel,
|
||||
[this.group.value],
|
||||
["display_name", ...fieldNames]
|
||||
);
|
||||
|
||||
return fieldNames
|
||||
.filter((fieldName) => values[fieldName])
|
||||
.map((fieldName) => ({ title: tooltipInfo[fieldName], value: values[fieldName] }));
|
||||
});
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Edition methods
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
archiveGroup() {
|
||||
this.dialog.add(ConfirmationDialog, {
|
||||
body: this.env._t(
|
||||
"Are you sure that you want to archive all the records from this column?"
|
||||
),
|
||||
confirm: () => this.group.list.archive(),
|
||||
cancel: () => {},
|
||||
});
|
||||
}
|
||||
|
||||
unarchiveGroup() {
|
||||
this.group.list.unarchive();
|
||||
}
|
||||
|
||||
deleteGroup() {
|
||||
this.dialog.add(ConfirmationDialog, {
|
||||
body: this.env._t("Are you sure you want to delete this column?"),
|
||||
confirm: async () => {
|
||||
this.props.deleteGroup(this.group);
|
||||
},
|
||||
cancel: () => {},
|
||||
});
|
||||
}
|
||||
|
||||
editGroup() {
|
||||
const { context, displayName, resModel, value } = this.group;
|
||||
this.props.dialogClose.push(
|
||||
this.dialog.add(FormViewDialog, {
|
||||
context,
|
||||
resId: value,
|
||||
resModel,
|
||||
title: sprintf(this.env._t("Edit: %s"), displayName),
|
||||
onRecordSaved: async () => {
|
||||
await this.props.list.load();
|
||||
this.props.list.model.notify();
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
quickCreate(group) {
|
||||
this.props.quickCreateState.groupId = this.group.id;
|
||||
}
|
||||
|
||||
toggleGroup() {
|
||||
return this.group.toggle();
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Permissions
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
canArchiveGroup() {
|
||||
const { archiveGroup } = this.props.activeActions;
|
||||
const hasActiveField = "active" in this.group.fields;
|
||||
return archiveGroup && hasActiveField && this.group.groupByField.type !== "many2many";
|
||||
}
|
||||
|
||||
canDeleteGroup() {
|
||||
const { deleteGroup } = this.props.activeActions;
|
||||
const { groupByField, value } = this.group;
|
||||
return deleteGroup && isRelational(groupByField) && value;
|
||||
}
|
||||
|
||||
canEditGroup() {
|
||||
const { editGroup } = this.props.activeActions;
|
||||
const { groupByField, value } = this.group;
|
||||
return editGroup && isRelational(groupByField) && value;
|
||||
}
|
||||
|
||||
canQuickCreate() {
|
||||
return this.props.canQuickCreate;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="web.KanbanHeader" owl="1">
|
||||
<div class="o_kanban_header top-0" t-ref="root" t-attf-class="{{ !env.isSmall and group.isFolded ? 'pt-3' : 'py-3' }}">
|
||||
<div t-ref="title" class="o_kanban_header_title position-relative d-flex align-items-baseline lh-lg text-900" t-on-mouseenter="() => this.openTooltip()" t-on-mouseleave="(ev) => this.closeTooltip()">
|
||||
<span class="o_column_title flex-grow-1 d-inline-block mw-100 text-truncate fs-4 fw-bold align-top" t-att-class="{ 'overflow-visible': !env.isSmall and group.isFolded }" t-esc="groupName"/>
|
||||
<t t-if="env.isSmall or !group.isFolded">
|
||||
<Dropdown class="'o_kanban_config'" togglerClass="'btn p-0 border-0'" position="'bottom-end'" container="() => this.rootRef.el">
|
||||
<t t-set-slot="toggler">
|
||||
<i class="fa fa-gear fa-stack d-block text-center text-700 small opacity-50 opacity-100-hover cursor-pointer" role="img" aria-label="Settings" title="Settings" />
|
||||
</t>
|
||||
<DropdownItem t-if="!env.isSmall" class="{
|
||||
'o_kanban_toggle_fold': true,
|
||||
'disabled': props.list.model.useSampleModel,
|
||||
}" onSelected="() => this.toggleGroup()">
|
||||
Fold
|
||||
</DropdownItem>
|
||||
<t t-if="canEditGroup()">
|
||||
<DropdownItem class="'o_column_edit'" onSelected="() => this.editGroup()">
|
||||
Edit
|
||||
</DropdownItem>
|
||||
</t>
|
||||
<t t-if="canDeleteGroup()">
|
||||
<DropdownItem class="'o_column_delete'" onSelected="() => this.deleteGroup()">
|
||||
Delete
|
||||
</DropdownItem>
|
||||
</t>
|
||||
<t t-if="canArchiveGroup()">
|
||||
<DropdownItem class="'o_column_archive_records'" onSelected="() => this.archiveGroup()">
|
||||
Archive All
|
||||
</DropdownItem>
|
||||
<DropdownItem class="'o_column_unarchive_records'" onSelected="() => this.unarchiveGroup()">
|
||||
Unarchive All
|
||||
</DropdownItem>
|
||||
</t>
|
||||
</Dropdown>
|
||||
<button t-if="canQuickCreate()" class="o_kanban_quick_add btn p-0 border-0 fs-6 shadow-none" t-on-click="() => this.quickCreate()">
|
||||
<i class="fa fa-plus fa-stack d-block small text-end text-700 opacity-50 opacity-100-hover cursor-pointer" role="img" aria-label="Quick add" title="Quick add" />
|
||||
</button>
|
||||
</t>
|
||||
<button t-else="" class="o_column_unfold btn btn-link d-block mt-0 mt-md-2 p-0 border-0 small text-center text-700 cursor-pointer">
|
||||
<i class="fa fa-arrows-h" 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>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<t t-name="web.KanbanGroupTooltip" owl="1">
|
||||
<div class="o-tooltip px-2 py-1">
|
||||
<t t-foreach="props.tooltip" t-as="entry" t-key="entry_index">
|
||||
<t t-esc="entry.title"/>
|
||||
<br/>
|
||||
<t t-esc="entry.value"/>
|
||||
<br t-if="!tooltip_last"/>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
@@ -1,14 +1,13 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { Domain } from "@web/core/domain";
|
||||
import { isRelational } from "@web/views/utils";
|
||||
import {
|
||||
DynamicGroupList,
|
||||
DynamicRecordList,
|
||||
Group,
|
||||
RelationalModel,
|
||||
} from "@web/views/relational_model";
|
||||
import { KeepLast } from "@web/core/utils/concurrency";
|
||||
import { isRelational } from "@web/views/utils";
|
||||
|
||||
/**
|
||||
* @typedef ProgressBar
|
||||
@@ -143,38 +142,6 @@ class KanbanGroup extends Group {
|
||||
};
|
||||
}
|
||||
|
||||
async load() {
|
||||
this.loadTooltip();
|
||||
await super.load();
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests the groups tooltips to the server and store them in this.tooltip
|
||||
*
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async loadTooltip() {
|
||||
const groupName = this.groupByField.name;
|
||||
if (
|
||||
this.groupByField.type === "many2one" &&
|
||||
this.value &&
|
||||
groupName in this.model.tooltipInfo
|
||||
) {
|
||||
this.tooltipKeepLast = this.tooltipKeepLast || new KeepLast();
|
||||
const resModel = this.groupByField.relation;
|
||||
const tooltipInfo = this.model.tooltipInfo[groupName];
|
||||
const fieldNames = Object.keys(tooltipInfo);
|
||||
// This read will be batched for all groups
|
||||
const [values] = await this.tooltipKeepLast.add(
|
||||
this.model.orm.silent.read(resModel, [this.value], ["display_name", ...fieldNames])
|
||||
);
|
||||
this.tooltip = fieldNames
|
||||
.filter((fieldName) => values[fieldName])
|
||||
.map((fieldName) => ({ title: tooltipInfo[fieldName], value: values[fieldName] }));
|
||||
this.model.notify();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the current active progress bar value contains records, and
|
||||
* deactivates it if not.
|
||||
@@ -544,7 +511,6 @@ export class KanbanModel extends RelationalModel {
|
||||
super.setup(...arguments);
|
||||
|
||||
this.progressAttributes = params.progressAttributes;
|
||||
this.tooltipInfo = params.tooltipInfo;
|
||||
this.transaction = makeTransactionManager();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { ConfirmationDialog } from "@web/core/confirmation_dialog/confirmation_dialog";
|
||||
import { Dropdown } from "@web/core/dropdown/dropdown";
|
||||
import { DropdownItem } from "@web/core/dropdown/dropdown_item";
|
||||
import { useHotkey } from "@web/core/hotkeys/hotkey_hook";
|
||||
@@ -8,11 +7,11 @@ import { registry } from "@web/core/registry";
|
||||
import { useBus, useService } from "@web/core/utils/hooks";
|
||||
import { useSortable } from "@web/core/utils/sortable";
|
||||
import { sprintf } from "@web/core/utils/strings";
|
||||
import { isNull, isRelational } from "@web/views/utils";
|
||||
import { isNull } from "@web/views/utils";
|
||||
import { ColumnProgress } from "@web/views/view_components/column_progress";
|
||||
import { FormViewDialog } from "@web/views/view_dialogs/form_view_dialog";
|
||||
import { useBounceButton } from "@web/views/view_hook";
|
||||
import { KanbanColumnQuickCreate } from "./kanban_column_quick_create";
|
||||
import { KanbanHeader } from "./kanban_header";
|
||||
import { KanbanRecord } from "./kanban_record";
|
||||
import { KanbanRecordQuickCreate } from "./kanban_record_quick_create";
|
||||
|
||||
@@ -42,6 +41,7 @@ export class KanbanRenderer extends Component {
|
||||
DropdownItem,
|
||||
ColumnProgress,
|
||||
KanbanColumnQuickCreate,
|
||||
KanbanHeader,
|
||||
KanbanRecord,
|
||||
KanbanRecordQuickCreate,
|
||||
};
|
||||
@@ -57,9 +57,11 @@ export class KanbanRenderer extends Component {
|
||||
"canQuickCreate?",
|
||||
"quickCreateState?",
|
||||
];
|
||||
|
||||
static defaultProps = {
|
||||
scrollTop: () => {},
|
||||
quickCreateState: { groupId: false },
|
||||
tooltipInfo: {},
|
||||
};
|
||||
|
||||
setup() {
|
||||
@@ -280,23 +282,6 @@ export class KanbanRenderer extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
getGroupName({ groupByField, count, displayName, isFolded }) {
|
||||
let name = displayName;
|
||||
if (groupByField.type === "boolean") {
|
||||
name = name ? this.env._t("Yes") : this.env._t("No");
|
||||
} else if (!name) {
|
||||
if (
|
||||
isRelational(groupByField) ||
|
||||
groupByField.type === "date" ||
|
||||
groupByField.type === "datetime" ||
|
||||
isNull(name)
|
||||
) {
|
||||
name = this.env._t("None");
|
||||
}
|
||||
}
|
||||
return !this.env.isSmall && isFolded ? `${name} (${count})` : name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {RelationalGroup} group
|
||||
* @param {boolean} isGroupProcessing
|
||||
@@ -332,13 +317,6 @@ export class KanbanRenderer extends Component {
|
||||
return (progressBar ? progressBar.count : group.count) - records.length;
|
||||
}
|
||||
|
||||
getGroupAggregate(group) {
|
||||
const { sumField } = this.props.list.model.progressAttributes;
|
||||
const value = group.getAggregates(sumField && sumField.name);
|
||||
const title = sumField ? sumField.string : this.env._t("Count");
|
||||
return { value, title };
|
||||
}
|
||||
|
||||
generateGhostColumns() {
|
||||
let colNames;
|
||||
if (this.exampleData && this.exampleData.ghostColumns) {
|
||||
@@ -368,42 +346,11 @@ export class KanbanRenderer extends Component {
|
||||
// Permissions
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
canArchiveGroup(group) {
|
||||
const { activeActions } = this.props.archInfo;
|
||||
const hasActiveField = "active" in group.fields;
|
||||
return (
|
||||
activeActions.archiveGroup &&
|
||||
hasActiveField &&
|
||||
this.props.list.groupByField.type !== "many2many"
|
||||
);
|
||||
}
|
||||
|
||||
canCreateGroup() {
|
||||
const { activeActions } = this.props.archInfo;
|
||||
return activeActions.createGroup && this.props.list.groupByField.type === "many2one";
|
||||
}
|
||||
|
||||
canDeleteGroup(group) {
|
||||
const { activeActions } = this.props.archInfo;
|
||||
const { groupByField } = this.props.list;
|
||||
return activeActions.deleteGroup && isRelational(groupByField) && group.value;
|
||||
}
|
||||
|
||||
canDeleteRecord() {
|
||||
const { activeActions } = this.props.archInfo;
|
||||
return activeActions.delete && this.props.list.groupByField.type !== "many2many";
|
||||
}
|
||||
|
||||
canEditGroup(group) {
|
||||
const { activeActions } = this.props.archInfo;
|
||||
const { groupByField } = this.props.list;
|
||||
return activeActions.editGroup && isRelational(groupByField) && group.value;
|
||||
}
|
||||
|
||||
canEditRecord() {
|
||||
return this.props.archInfo.activeActions.edit;
|
||||
}
|
||||
|
||||
canQuickCreate() {
|
||||
return this.props.canQuickCreate;
|
||||
}
|
||||
@@ -412,10 +359,6 @@ export class KanbanRenderer extends Component {
|
||||
// Edition methods
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
quickCreate(group) {
|
||||
this.props.quickCreateState.groupId = group.id;
|
||||
}
|
||||
|
||||
async validateQuickCreate(recordId, mode, group) {
|
||||
this.props.quickCreateState.groupId = false;
|
||||
if (mode === "add") {
|
||||
@@ -435,6 +378,13 @@ export class KanbanRenderer extends Component {
|
||||
this.props.quickCreateState.groupId = false;
|
||||
}
|
||||
|
||||
async deleteGroup(group) {
|
||||
await this.props.list.deleteGroups([group]);
|
||||
if (this.props.list.groups.length === 0) {
|
||||
this.state.columnQuickCreateIsFolded = false;
|
||||
}
|
||||
}
|
||||
|
||||
toggleGroup(group) {
|
||||
return group.toggle();
|
||||
}
|
||||
@@ -443,53 +393,6 @@ export class KanbanRenderer extends Component {
|
||||
return group.list.loadMore();
|
||||
}
|
||||
|
||||
editGroup(group) {
|
||||
this.dialogClose.push(
|
||||
this.dialog.add(FormViewDialog, {
|
||||
context: group.context,
|
||||
resId: group.value,
|
||||
resModel: group.resModel,
|
||||
title: sprintf(this.env._t("Edit: %s"), group.displayName),
|
||||
|
||||
onRecordSaved: async () => {
|
||||
await this.props.list.load();
|
||||
this.props.list.model.notify();
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
archiveGroup(group) {
|
||||
this.dialog.add(ConfirmationDialog, {
|
||||
body: this.env._t(
|
||||
"Are you sure that you want to archive all the records from this column?"
|
||||
),
|
||||
confirm: () => group.list.archive(),
|
||||
cancel: () => {},
|
||||
});
|
||||
}
|
||||
|
||||
unarchiveGroup(group) {
|
||||
group.list.unarchive();
|
||||
}
|
||||
|
||||
deleteGroup(group) {
|
||||
this.dialog.add(ConfirmationDialog, {
|
||||
body: this.env._t("Are you sure you want to delete this column?"),
|
||||
confirm: async () => {
|
||||
await this.props.list.deleteGroups([group]);
|
||||
if (this.props.list.groups.length === 0) {
|
||||
this.state.columnQuickCreateIsFolded = false;
|
||||
}
|
||||
},
|
||||
cancel: () => {},
|
||||
});
|
||||
}
|
||||
|
||||
getGroupEl(groupId) {
|
||||
return this.rootRef.el.querySelector(`.o_kanban_group[data-id="${groupId}"]`);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} id
|
||||
* @param {boolean} isProcessing
|
||||
@@ -657,14 +560,4 @@ export class KanbanRenderer extends Component {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
tooltipAttributes(group) {
|
||||
if (!group.tooltip.length) {
|
||||
return {};
|
||||
}
|
||||
return {
|
||||
"data-tooltip-template": "web.KanbanGroupTooltip",
|
||||
"data-tooltip-info": JSON.stringify({ entries: group.tooltip }),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,51 +15,17 @@
|
||||
t-att-data-id="group.id"
|
||||
t-on-click="(ev) => this.onGroupClick(group, ev)"
|
||||
>
|
||||
<div class="o_kanban_header top-0" t-attf-class="{{ !env.isSmall and group.isFolded ? 'pt-3' : 'py-3' }}">
|
||||
<div class="o_kanban_header_title position-relative d-flex align-items-baseline lh-lg text-900" t-att="tooltipAttributes(group)">
|
||||
<span class="o_column_title flex-grow-1 d-inline-block mw-100 text-truncate fs-4 fw-bold align-top" t-att-class="{ 'overflow-visible': !env.isSmall and group.isFolded }" t-esc="getGroupName(group)"/>
|
||||
<t t-if="env.isSmall or !group.isFolded">
|
||||
<Dropdown class="'o_kanban_config'" togglerClass="'btn p-0 border-0'" position="'bottom-end'" container="() => this.getGroupEl(group.id)">
|
||||
<t t-set-slot="toggler">
|
||||
<i class="fa fa-gear fa-stack d-block text-center text-700 small opacity-50 opacity-100-hover cursor-pointer" role="img" aria-label="Settings" title="Settings" />
|
||||
</t>
|
||||
<DropdownItem t-if="!env.isSmall" class="{
|
||||
'o_kanban_toggle_fold': true,
|
||||
'disabled': props.list.model.useSampleModel,
|
||||
}" onSelected="() => this.toggleGroup(group)">
|
||||
Fold
|
||||
</DropdownItem>
|
||||
<t t-if="canEditGroup(group)">
|
||||
<DropdownItem class="'o_column_edit'" onSelected="() => this.editGroup(group)">
|
||||
Edit
|
||||
</DropdownItem>
|
||||
</t>
|
||||
<t t-if="canDeleteGroup(group)">
|
||||
<DropdownItem class="'o_column_delete'" onSelected="() => this.deleteGroup(group)">
|
||||
Delete
|
||||
</DropdownItem>
|
||||
</t>
|
||||
<t t-if="canArchiveGroup(group)">
|
||||
<DropdownItem class="'o_column_archive_records'" onSelected="() => this.archiveGroup(group)">
|
||||
Archive All
|
||||
</DropdownItem>
|
||||
<DropdownItem class="'o_column_unarchive_records'" onSelected="() => this.unarchiveGroup(group)">
|
||||
Unarchive All
|
||||
</DropdownItem>
|
||||
</t>
|
||||
</Dropdown>
|
||||
<button t-if="canQuickCreate()" class="o_kanban_quick_add btn p-0 border-0 fs-6 shadow-none" t-on-click="() => this.quickCreate(group)">
|
||||
<i class="fa fa-plus fa-stack d-block small text-end text-700 opacity-50 opacity-100-hover cursor-pointer" role="img" aria-label="Quick add" title="Quick add" />
|
||||
</button>
|
||||
</t>
|
||||
<button t-else="" class="o_column_unfold btn btn-link d-block mt-0 mt-md-2 p-0 border-0 small text-center text-700 cursor-pointer">
|
||||
<i class="fa fa-arrows-h" 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="getGroupAggregate(group)" onBarClicked="props.scrollTop" />
|
||||
</div>
|
||||
</div>
|
||||
<KanbanHeader
|
||||
activeActions="props.archInfo.activeActions"
|
||||
canQuickCreate="props.canQuickCreate"
|
||||
deleteGroup="(group) => this.deleteGroup(group)"
|
||||
dialogClose="dialogClose"
|
||||
group="group"
|
||||
list="props.list"
|
||||
quickCreateState="props.quickCreateState"
|
||||
scrollTop="props.scrollTop"
|
||||
tooltipInfo="props.archInfo.tooltipInfo"
|
||||
/>
|
||||
<t t-if="!group.isFolded">
|
||||
<t t-if="group.id === props.quickCreateState.groupId">
|
||||
<KanbanRecordQuickCreate
|
||||
@@ -168,17 +134,6 @@
|
||||
</ul>
|
||||
</t>
|
||||
|
||||
<t t-name="web.KanbanGroupTooltip" owl="1">
|
||||
<div>
|
||||
<t t-foreach="entries" t-as="entry" t-key="entry_index">
|
||||
<t t-esc="entry.title"/>
|
||||
<br/>
|
||||
<t t-esc="entry.value"/>
|
||||
<br t-if="!field_last"/>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<t t-name="web.KanbanDropdownMenuWrapper" owl="1">
|
||||
<div t-on-click="onClick" style="display:contents">
|
||||
<t t-slot="default"/>
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
getNodesTextContent,
|
||||
makeDeferred,
|
||||
mockAnimationFrame,
|
||||
mouseEnter,
|
||||
nextTick,
|
||||
patchWithCleanup,
|
||||
selectDropdownItem,
|
||||
@@ -8459,7 +8460,9 @@ QUnit.module("Views", (hooks) => {
|
||||
});
|
||||
|
||||
QUnit.test("group_by_tooltip option when grouping on a many2one", async (assert) => {
|
||||
assert.expect(14);
|
||||
patchWithCleanup(browser, {
|
||||
setTimeout: (fn) => fn(),
|
||||
});
|
||||
delete serverData.models.partner.records[3].product_id;
|
||||
const kanban = await makeView({
|
||||
type: "kanban",
|
||||
@@ -8477,7 +8480,7 @@ QUnit.module("Views", (hooks) => {
|
||||
</kanban>`,
|
||||
async mockRPC(route, args) {
|
||||
if (route === "/web/dataset/call_kw/product/read") {
|
||||
assert.strictEqual(args.args[0].length, 2, "read on two groups");
|
||||
assert.step("read: product");
|
||||
assert.deepEqual(
|
||||
args.args[1],
|
||||
["display_name", "name"],
|
||||
@@ -8518,38 +8521,38 @@ QUnit.module("Views", (hooks) => {
|
||||
"None",
|
||||
"first column should have a default title for when no value is provided"
|
||||
);
|
||||
assert.ok(
|
||||
!target.querySelector(".o_kanban_group:first-child .o_kanban_header_title").dataset
|
||||
.tooltipInfo,
|
||||
|
||||
const groupsTitle = [...target.querySelectorAll(".o_kanban_group .o_kanban_header_title")];
|
||||
await mouseEnter(groupsTitle[0]);
|
||||
assert.containsNone(
|
||||
target,
|
||||
".o-tooltip",
|
||||
"tooltip of first column should not defined, since group_by_tooltip title and the many2one field has no value"
|
||||
);
|
||||
assert.ok(
|
||||
!target.querySelector(".o_kanban_group:first-child .o_kanban_header_title").dataset
|
||||
.tooltipTemplate,
|
||||
"tooltip of first column should not defined, since group_by_tooltip title and the many2one field has no value"
|
||||
assert.verifySteps([], "should not have done any read on product because no value");
|
||||
|
||||
await mouseEnter(groupsTitle[1]);
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o-tooltip",
|
||||
"second column should have a tooltip with the group_by_tooltip title and many2one field value"
|
||||
);
|
||||
assert.strictEqual(target.querySelector(".o-tooltip").textContent, "Kikouhello");
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_kanban_group:nth-child(2) span.o_column_title").textContent,
|
||||
"hello",
|
||||
"second column should have a title with a value from the many2one"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_kanban_group:nth-child(2) .o_kanban_header_title").dataset
|
||||
.tooltipInfo,
|
||||
`{"entries":[{"title":"Kikou","value":"hello"}]}`,
|
||||
"second column should have a tooltip with the group_by_tooltip title and many2one field value"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_kanban_group:nth-child(2) .o_kanban_header_title").dataset
|
||||
.tooltipTemplate,
|
||||
"web.KanbanGroupTooltip",
|
||||
"second column should have a tooltip with the group_by_tooltip title and many2one field value"
|
||||
assert.verifySteps(
|
||||
["read: product"],
|
||||
"should have done one read on product for the second column tooltip"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("asynchronous tooltips when grouped", async (assert) => {
|
||||
assert.expect(10);
|
||||
serviceRegistry.add("tooltip", tooltipService);
|
||||
patchWithCleanup(browser, {
|
||||
setTimeout: (fn) => fn(),
|
||||
});
|
||||
const prom = makeDeferred();
|
||||
await makeView({
|
||||
type: "kanban",
|
||||
@@ -8567,6 +8570,7 @@ QUnit.module("Views", (hooks) => {
|
||||
</kanban>`,
|
||||
async mockRPC(route, args) {
|
||||
if (route === "/web/dataset/call_kw/product/read") {
|
||||
assert.step("read: product");
|
||||
await prom;
|
||||
}
|
||||
},
|
||||
@@ -8574,56 +8578,32 @@ QUnit.module("Views", (hooks) => {
|
||||
|
||||
assert.hasClass(target.querySelector(".o_kanban_renderer"), "o_kanban_grouped");
|
||||
assert.containsN(target, ".o_column_title", 2);
|
||||
assert.strictEqual(
|
||||
target
|
||||
.querySelectorAll(".o_kanban_header_title")[0]
|
||||
.getAttribute("data-tooltip-template"),
|
||||
null
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelectorAll(".o_kanban_header_title")[0].getAttribute("data-tooltip-info"),
|
||||
null
|
||||
);
|
||||
assert.strictEqual(
|
||||
target
|
||||
.querySelectorAll(".o_kanban_header_title")[1]
|
||||
.getAttribute("data-tooltip-template"),
|
||||
null
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelectorAll(".o_kanban_header_title")[1].getAttribute("data-tooltip-info"),
|
||||
null
|
||||
|
||||
await mouseEnter(target.querySelectorAll(".o_kanban_group .o_kanban_header_title")[0]);
|
||||
assert.containsNone(target, ".o-tooltip");
|
||||
|
||||
await triggerEvent(
|
||||
target.querySelectorAll(".o_kanban_group .o_kanban_header_title")[0],
|
||||
null,
|
||||
"mouseleave"
|
||||
);
|
||||
assert.containsNone(target, ".o-tooltip");
|
||||
|
||||
await mouseEnter(target.querySelectorAll(".o_kanban_group .o_kanban_header_title")[0]);
|
||||
assert.containsNone(target, ".o-tooltip");
|
||||
|
||||
prom.resolve();
|
||||
await nextTick();
|
||||
|
||||
assert.strictEqual(
|
||||
target
|
||||
.querySelectorAll(".o_kanban_header_title")[0]
|
||||
.getAttribute("data-tooltip-template"),
|
||||
"web.KanbanGroupTooltip"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target
|
||||
.querySelectorAll(".o_kanban_header_title")[1]
|
||||
.getAttribute("data-tooltip-template"),
|
||||
"web.KanbanGroupTooltip"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelectorAll(".o_kanban_header_title")[0].getAttribute("data-tooltip-info"),
|
||||
'{"entries":[{"title":"Name","value":"hello"}]}'
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelectorAll(".o_kanban_header_title")[1].getAttribute("data-tooltip-info"),
|
||||
'{"entries":[{"title":"Name","value":"xmo"}]}'
|
||||
);
|
||||
assert.containsOnce(target, ".o-tooltip");
|
||||
assert.strictEqual(target.querySelector(".o-tooltip").textContent.trim(), "Namehello");
|
||||
assert.verifySteps(["read: product"]);
|
||||
});
|
||||
|
||||
QUnit.test("concurrency asynchronous tooltips when grouped", async (assert) => {
|
||||
assert.expect(2);
|
||||
serviceRegistry.add("tooltip", tooltipService);
|
||||
const prom = makeDeferred();
|
||||
let rpcCount = 0;
|
||||
QUnit.test("loads data tooltips only when first opening", async (assert) => {
|
||||
patchWithCleanup(browser, {
|
||||
setTimeout: (fn) => fn(),
|
||||
});
|
||||
await makeView({
|
||||
type: "kanban",
|
||||
resModel: "partner",
|
||||
@@ -8638,48 +8618,29 @@ QUnit.module("Views", (hooks) => {
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>`,
|
||||
searchViewArch: `
|
||||
<search>
|
||||
<filter name="product_id" string="product" context="{'group_by': 'product_id', 'group_by_tooltip': {'name': 'Name'}}}"/>
|
||||
</search>
|
||||
`,
|
||||
async mockRPC(route, args) {
|
||||
if (route === "/web/dataset/call_kw/product/read") {
|
||||
if (rpcCount++ == 0) {
|
||||
await prom;
|
||||
} else {
|
||||
return [
|
||||
{
|
||||
id: 3,
|
||||
display_name: "hello",
|
||||
name: "hello",
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
display_name: "xmo",
|
||||
name: "xm",
|
||||
},
|
||||
];
|
||||
}
|
||||
assert.step("read: product");
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// The first tooltip rpc request is blocked and user changes the group by
|
||||
await click(target, ".o_group_by_menu > .dropdown-toggle");
|
||||
await click(target, ".o_group_by_menu > div > span");
|
||||
// The first tooltip request arrives after the second request
|
||||
prom.resolve();
|
||||
await nextTick();
|
||||
await mouseEnter(target.querySelectorAll(".o_kanban_group .o_kanban_header_title")[0]);
|
||||
assert.containsOnce(target, ".o-tooltip");
|
||||
assert.strictEqual(target.querySelector(".o-tooltip").textContent.trim(), "Namehello");
|
||||
assert.verifySteps(["read: product"]);
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelectorAll(".o_kanban_header_title")[0].getAttribute("data-tooltip-info"),
|
||||
'{"entries":[{"title":"Name","value":"hello"}]}'
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelectorAll(".o_kanban_header_title")[1].getAttribute("data-tooltip-info"),
|
||||
'{"entries":[{"title":"Name","value":"xm"}]}'
|
||||
await triggerEvent(
|
||||
target.querySelectorAll(".o_kanban_group .o_kanban_header_title")[0],
|
||||
null,
|
||||
"mouseleave"
|
||||
);
|
||||
assert.containsNone(target, ".o-tooltip", "tooltip should be closed");
|
||||
|
||||
await mouseEnter(target.querySelectorAll(".o_kanban_group .o_kanban_header_title")[0]);
|
||||
assert.containsOnce(target, ".o-tooltip");
|
||||
assert.strictEqual(target.querySelector(".o-tooltip").textContent.trim(), "Namehello");
|
||||
assert.verifySteps([]);
|
||||
});
|
||||
|
||||
QUnit.test("move a record then put it again in the same column", async (assert) => {
|
||||
|
||||
Reference in New Issue
Block a user