[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:
FrancoisGe
2023-04-07 07:28:09 +02:00
parent b9f0758350
commit afbdce664f
10 changed files with 363 additions and 317 deletions
@@ -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) => {