[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 { CrmColumnProgress } from "./crm_column_progress";
|
||||||
import { KanbanRenderer } from "@web/views/kanban/kanban_renderer";
|
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 {}
|
export class CrmKanbanRenderer extends KanbanRenderer {}
|
||||||
CrmKanbanRenderer.template = "crm.CrmKanbanRenderer";
|
|
||||||
CrmKanbanRenderer.components = {
|
CrmKanbanRenderer.components = {
|
||||||
...KanbanRenderer.components,
|
...KanbanRenderer.components,
|
||||||
CrmColumnProgress,
|
KanbanHeader: CrmKanbanHeader,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<templates>
|
<templates>
|
||||||
<t t-name="crm.CrmKanbanRenderer" t-inherit="web.KanbanRenderer" t-inherit-mode="primary" owl="1">
|
<t t-name="crm.CrmKanbanHeader" t-inherit="web.KanbanHeader" t-inherit-mode="primary" owl="1">
|
||||||
<xpath expr="//ColumnProgress" position="replace">
|
<xpath expr="//ColumnProgress" position="attributes">
|
||||||
<CrmColumnProgress group="group" aggregate="getGroupAggregate(group)" onBarClicked="props.scrollTop" progressAttributes="props.list.model.progressAttributes"/>
|
<attribute name="progressAttributes">props.group.model.progressAttributes</attribute>
|
||||||
</xpath>
|
</xpath>
|
||||||
</t>
|
</t>
|
||||||
</templates>
|
</templates>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<templates>
|
<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">
|
<KanbanColumnQuickCreate position="replace">
|
||||||
<t t-if="isGroupedByForecastField()">
|
<t t-if="isGroupedByForecastField()">
|
||||||
<ForecastKanbanColumnQuickCreate
|
<ForecastKanbanColumnQuickCreate
|
||||||
|
|||||||
@@ -33,7 +33,6 @@ export class KanbanController extends Component {
|
|||||||
defaultOrder: archInfo.defaultOrder,
|
defaultOrder: archInfo.defaultOrder,
|
||||||
viewMode: "kanban",
|
viewMode: "kanban",
|
||||||
openGroupsByDefault: true,
|
openGroupsByDefault: true,
|
||||||
tooltipInfo: archInfo.tooltipInfo,
|
|
||||||
rootState,
|
rootState,
|
||||||
});
|
});
|
||||||
this.headerButtons = archInfo.headerButtons;
|
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 **/
|
/** @odoo-module **/
|
||||||
|
|
||||||
import { Domain } from "@web/core/domain";
|
import { Domain } from "@web/core/domain";
|
||||||
import { isRelational } from "@web/views/utils";
|
|
||||||
import {
|
import {
|
||||||
DynamicGroupList,
|
DynamicGroupList,
|
||||||
DynamicRecordList,
|
DynamicRecordList,
|
||||||
Group,
|
Group,
|
||||||
RelationalModel,
|
RelationalModel,
|
||||||
} from "@web/views/relational_model";
|
} from "@web/views/relational_model";
|
||||||
import { KeepLast } from "@web/core/utils/concurrency";
|
import { isRelational } from "@web/views/utils";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef ProgressBar
|
* @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
|
* Checks if the current active progress bar value contains records, and
|
||||||
* deactivates it if not.
|
* deactivates it if not.
|
||||||
@@ -544,7 +511,6 @@ export class KanbanModel extends RelationalModel {
|
|||||||
super.setup(...arguments);
|
super.setup(...arguments);
|
||||||
|
|
||||||
this.progressAttributes = params.progressAttributes;
|
this.progressAttributes = params.progressAttributes;
|
||||||
this.tooltipInfo = params.tooltipInfo;
|
|
||||||
this.transaction = makeTransactionManager();
|
this.transaction = makeTransactionManager();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
/** @odoo-module **/
|
/** @odoo-module **/
|
||||||
|
|
||||||
import { ConfirmationDialog } from "@web/core/confirmation_dialog/confirmation_dialog";
|
|
||||||
import { Dropdown } from "@web/core/dropdown/dropdown";
|
import { Dropdown } from "@web/core/dropdown/dropdown";
|
||||||
import { DropdownItem } from "@web/core/dropdown/dropdown_item";
|
import { DropdownItem } from "@web/core/dropdown/dropdown_item";
|
||||||
import { useHotkey } from "@web/core/hotkeys/hotkey_hook";
|
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 { useBus, useService } from "@web/core/utils/hooks";
|
||||||
import { useSortable } from "@web/core/utils/sortable";
|
import { useSortable } from "@web/core/utils/sortable";
|
||||||
import { sprintf } from "@web/core/utils/strings";
|
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 { 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 { useBounceButton } from "@web/views/view_hook";
|
||||||
import { KanbanColumnQuickCreate } from "./kanban_column_quick_create";
|
import { KanbanColumnQuickCreate } from "./kanban_column_quick_create";
|
||||||
|
import { KanbanHeader } from "./kanban_header";
|
||||||
import { KanbanRecord } from "./kanban_record";
|
import { KanbanRecord } from "./kanban_record";
|
||||||
import { KanbanRecordQuickCreate } from "./kanban_record_quick_create";
|
import { KanbanRecordQuickCreate } from "./kanban_record_quick_create";
|
||||||
|
|
||||||
@@ -42,6 +41,7 @@ export class KanbanRenderer extends Component {
|
|||||||
DropdownItem,
|
DropdownItem,
|
||||||
ColumnProgress,
|
ColumnProgress,
|
||||||
KanbanColumnQuickCreate,
|
KanbanColumnQuickCreate,
|
||||||
|
KanbanHeader,
|
||||||
KanbanRecord,
|
KanbanRecord,
|
||||||
KanbanRecordQuickCreate,
|
KanbanRecordQuickCreate,
|
||||||
};
|
};
|
||||||
@@ -57,9 +57,11 @@ export class KanbanRenderer extends Component {
|
|||||||
"canQuickCreate?",
|
"canQuickCreate?",
|
||||||
"quickCreateState?",
|
"quickCreateState?",
|
||||||
];
|
];
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
scrollTop: () => {},
|
scrollTop: () => {},
|
||||||
quickCreateState: { groupId: false },
|
quickCreateState: { groupId: false },
|
||||||
|
tooltipInfo: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
setup() {
|
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 {RelationalGroup} group
|
||||||
* @param {boolean} isGroupProcessing
|
* @param {boolean} isGroupProcessing
|
||||||
@@ -332,13 +317,6 @@ export class KanbanRenderer extends Component {
|
|||||||
return (progressBar ? progressBar.count : group.count) - records.length;
|
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() {
|
generateGhostColumns() {
|
||||||
let colNames;
|
let colNames;
|
||||||
if (this.exampleData && this.exampleData.ghostColumns) {
|
if (this.exampleData && this.exampleData.ghostColumns) {
|
||||||
@@ -368,42 +346,11 @@ export class KanbanRenderer extends Component {
|
|||||||
// Permissions
|
// 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() {
|
canCreateGroup() {
|
||||||
const { activeActions } = this.props.archInfo;
|
const { activeActions } = this.props.archInfo;
|
||||||
return activeActions.createGroup && this.props.list.groupByField.type === "many2one";
|
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() {
|
canQuickCreate() {
|
||||||
return this.props.canQuickCreate;
|
return this.props.canQuickCreate;
|
||||||
}
|
}
|
||||||
@@ -412,10 +359,6 @@ export class KanbanRenderer extends Component {
|
|||||||
// Edition methods
|
// Edition methods
|
||||||
// ------------------------------------------------------------------------
|
// ------------------------------------------------------------------------
|
||||||
|
|
||||||
quickCreate(group) {
|
|
||||||
this.props.quickCreateState.groupId = group.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
async validateQuickCreate(recordId, mode, group) {
|
async validateQuickCreate(recordId, mode, group) {
|
||||||
this.props.quickCreateState.groupId = false;
|
this.props.quickCreateState.groupId = false;
|
||||||
if (mode === "add") {
|
if (mode === "add") {
|
||||||
@@ -435,6 +378,13 @@ export class KanbanRenderer extends Component {
|
|||||||
this.props.quickCreateState.groupId = false;
|
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) {
|
toggleGroup(group) {
|
||||||
return group.toggle();
|
return group.toggle();
|
||||||
}
|
}
|
||||||
@@ -443,53 +393,6 @@ export class KanbanRenderer extends Component {
|
|||||||
return group.list.loadMore();
|
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 {string} id
|
||||||
* @param {boolean} isProcessing
|
* @param {boolean} isProcessing
|
||||||
@@ -657,14 +560,4 @@ export class KanbanRenderer extends Component {
|
|||||||
return true;
|
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-att-data-id="group.id"
|
||||||
t-on-click="(ev) => this.onGroupClick(group, ev)"
|
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' }}">
|
<KanbanHeader
|
||||||
<div class="o_kanban_header_title position-relative d-flex align-items-baseline lh-lg text-900" t-att="tooltipAttributes(group)">
|
activeActions="props.archInfo.activeActions"
|
||||||
<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)"/>
|
canQuickCreate="props.canQuickCreate"
|
||||||
<t t-if="env.isSmall or !group.isFolded">
|
deleteGroup="(group) => this.deleteGroup(group)"
|
||||||
<Dropdown class="'o_kanban_config'" togglerClass="'btn p-0 border-0'" position="'bottom-end'" container="() => this.getGroupEl(group.id)">
|
dialogClose="dialogClose"
|
||||||
<t t-set-slot="toggler">
|
group="group"
|
||||||
<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" />
|
list="props.list"
|
||||||
</t>
|
quickCreateState="props.quickCreateState"
|
||||||
<DropdownItem t-if="!env.isSmall" class="{
|
scrollTop="props.scrollTop"
|
||||||
'o_kanban_toggle_fold': true,
|
tooltipInfo="props.archInfo.tooltipInfo"
|
||||||
'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>
|
|
||||||
<t t-if="!group.isFolded">
|
<t t-if="!group.isFolded">
|
||||||
<t t-if="group.id === props.quickCreateState.groupId">
|
<t t-if="group.id === props.quickCreateState.groupId">
|
||||||
<KanbanRecordQuickCreate
|
<KanbanRecordQuickCreate
|
||||||
@@ -168,17 +134,6 @@
|
|||||||
</ul>
|
</ul>
|
||||||
</t>
|
</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">
|
<t t-name="web.KanbanDropdownMenuWrapper" owl="1">
|
||||||
<div t-on-click="onClick" style="display:contents">
|
<div t-on-click="onClick" style="display:contents">
|
||||||
<t t-slot="default"/>
|
<t t-slot="default"/>
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
getNodesTextContent,
|
getNodesTextContent,
|
||||||
makeDeferred,
|
makeDeferred,
|
||||||
mockAnimationFrame,
|
mockAnimationFrame,
|
||||||
|
mouseEnter,
|
||||||
nextTick,
|
nextTick,
|
||||||
patchWithCleanup,
|
patchWithCleanup,
|
||||||
selectDropdownItem,
|
selectDropdownItem,
|
||||||
@@ -8459,7 +8460,9 @@ QUnit.module("Views", (hooks) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
QUnit.test("group_by_tooltip option when grouping on a many2one", async (assert) => {
|
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;
|
delete serverData.models.partner.records[3].product_id;
|
||||||
const kanban = await makeView({
|
const kanban = await makeView({
|
||||||
type: "kanban",
|
type: "kanban",
|
||||||
@@ -8477,7 +8480,7 @@ QUnit.module("Views", (hooks) => {
|
|||||||
</kanban>`,
|
</kanban>`,
|
||||||
async mockRPC(route, args) {
|
async mockRPC(route, args) {
|
||||||
if (route === "/web/dataset/call_kw/product/read") {
|
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(
|
assert.deepEqual(
|
||||||
args.args[1],
|
args.args[1],
|
||||||
["display_name", "name"],
|
["display_name", "name"],
|
||||||
@@ -8518,38 +8521,38 @@ QUnit.module("Views", (hooks) => {
|
|||||||
"None",
|
"None",
|
||||||
"first column should have a default title for when no value is provided"
|
"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
|
const groupsTitle = [...target.querySelectorAll(".o_kanban_group .o_kanban_header_title")];
|
||||||
.tooltipInfo,
|
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"
|
"tooltip of first column should not defined, since group_by_tooltip title and the many2one field has no value"
|
||||||
);
|
);
|
||||||
assert.ok(
|
assert.verifySteps([], "should not have done any read on product because no value");
|
||||||
!target.querySelector(".o_kanban_group:first-child .o_kanban_header_title").dataset
|
|
||||||
.tooltipTemplate,
|
await mouseEnter(groupsTitle[1]);
|
||||||
"tooltip of first column should not defined, since group_by_tooltip title and the many2one field has no value"
|
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(
|
assert.strictEqual(
|
||||||
target.querySelector(".o_kanban_group:nth-child(2) span.o_column_title").textContent,
|
target.querySelector(".o_kanban_group:nth-child(2) span.o_column_title").textContent,
|
||||||
"hello",
|
"hello",
|
||||||
"second column should have a title with a value from the many2one"
|
"second column should have a title with a value from the many2one"
|
||||||
);
|
);
|
||||||
assert.strictEqual(
|
assert.verifySteps(
|
||||||
target.querySelector(".o_kanban_group:nth-child(2) .o_kanban_header_title").dataset
|
["read: product"],
|
||||||
.tooltipInfo,
|
"should have done one read on product for the second column tooltip"
|
||||||
`{"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"
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
QUnit.test("asynchronous tooltips when grouped", async (assert) => {
|
QUnit.test("asynchronous tooltips when grouped", async (assert) => {
|
||||||
assert.expect(10);
|
patchWithCleanup(browser, {
|
||||||
serviceRegistry.add("tooltip", tooltipService);
|
setTimeout: (fn) => fn(),
|
||||||
|
});
|
||||||
const prom = makeDeferred();
|
const prom = makeDeferred();
|
||||||
await makeView({
|
await makeView({
|
||||||
type: "kanban",
|
type: "kanban",
|
||||||
@@ -8567,6 +8570,7 @@ QUnit.module("Views", (hooks) => {
|
|||||||
</kanban>`,
|
</kanban>`,
|
||||||
async mockRPC(route, args) {
|
async mockRPC(route, args) {
|
||||||
if (route === "/web/dataset/call_kw/product/read") {
|
if (route === "/web/dataset/call_kw/product/read") {
|
||||||
|
assert.step("read: product");
|
||||||
await prom;
|
await prom;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -8574,56 +8578,32 @@ QUnit.module("Views", (hooks) => {
|
|||||||
|
|
||||||
assert.hasClass(target.querySelector(".o_kanban_renderer"), "o_kanban_grouped");
|
assert.hasClass(target.querySelector(".o_kanban_renderer"), "o_kanban_grouped");
|
||||||
assert.containsN(target, ".o_column_title", 2);
|
assert.containsN(target, ".o_column_title", 2);
|
||||||
assert.strictEqual(
|
|
||||||
target
|
await mouseEnter(target.querySelectorAll(".o_kanban_group .o_kanban_header_title")[0]);
|
||||||
.querySelectorAll(".o_kanban_header_title")[0]
|
assert.containsNone(target, ".o-tooltip");
|
||||||
.getAttribute("data-tooltip-template"),
|
|
||||||
null
|
await triggerEvent(
|
||||||
);
|
target.querySelectorAll(".o_kanban_group .o_kanban_header_title")[0],
|
||||||
assert.strictEqual(
|
null,
|
||||||
target.querySelectorAll(".o_kanban_header_title")[0].getAttribute("data-tooltip-info"),
|
"mouseleave"
|
||||||
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
|
|
||||||
);
|
);
|
||||||
|
assert.containsNone(target, ".o-tooltip");
|
||||||
|
|
||||||
|
await mouseEnter(target.querySelectorAll(".o_kanban_group .o_kanban_header_title")[0]);
|
||||||
|
assert.containsNone(target, ".o-tooltip");
|
||||||
|
|
||||||
prom.resolve();
|
prom.resolve();
|
||||||
await nextTick();
|
await nextTick();
|
||||||
|
|
||||||
assert.strictEqual(
|
assert.containsOnce(target, ".o-tooltip");
|
||||||
target
|
assert.strictEqual(target.querySelector(".o-tooltip").textContent.trim(), "Namehello");
|
||||||
.querySelectorAll(".o_kanban_header_title")[0]
|
assert.verifySteps(["read: product"]);
|
||||||
.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"}]}'
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
QUnit.test("concurrency asynchronous tooltips when grouped", async (assert) => {
|
QUnit.test("loads data tooltips only when first opening", async (assert) => {
|
||||||
assert.expect(2);
|
patchWithCleanup(browser, {
|
||||||
serviceRegistry.add("tooltip", tooltipService);
|
setTimeout: (fn) => fn(),
|
||||||
const prom = makeDeferred();
|
});
|
||||||
let rpcCount = 0;
|
|
||||||
await makeView({
|
await makeView({
|
||||||
type: "kanban",
|
type: "kanban",
|
||||||
resModel: "partner",
|
resModel: "partner",
|
||||||
@@ -8638,48 +8618,29 @@ QUnit.module("Views", (hooks) => {
|
|||||||
</t>
|
</t>
|
||||||
</templates>
|
</templates>
|
||||||
</kanban>`,
|
</kanban>`,
|
||||||
searchViewArch: `
|
|
||||||
<search>
|
|
||||||
<filter name="product_id" string="product" context="{'group_by': 'product_id', 'group_by_tooltip': {'name': 'Name'}}}"/>
|
|
||||||
</search>
|
|
||||||
`,
|
|
||||||
async mockRPC(route, args) {
|
async mockRPC(route, args) {
|
||||||
if (route === "/web/dataset/call_kw/product/read") {
|
if (route === "/web/dataset/call_kw/product/read") {
|
||||||
if (rpcCount++ == 0) {
|
assert.step("read: product");
|
||||||
await prom;
|
|
||||||
} else {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
id: 3,
|
|
||||||
display_name: "hello",
|
|
||||||
name: "hello",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 5,
|
|
||||||
display_name: "xmo",
|
|
||||||
name: "xm",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// The first tooltip rpc request is blocked and user changes the group by
|
await mouseEnter(target.querySelectorAll(".o_kanban_group .o_kanban_header_title")[0]);
|
||||||
await click(target, ".o_group_by_menu > .dropdown-toggle");
|
assert.containsOnce(target, ".o-tooltip");
|
||||||
await click(target, ".o_group_by_menu > div > span");
|
assert.strictEqual(target.querySelector(".o-tooltip").textContent.trim(), "Namehello");
|
||||||
// The first tooltip request arrives after the second request
|
assert.verifySteps(["read: product"]);
|
||||||
prom.resolve();
|
|
||||||
await nextTick();
|
|
||||||
|
|
||||||
assert.strictEqual(
|
await triggerEvent(
|
||||||
target.querySelectorAll(".o_kanban_header_title")[0].getAttribute("data-tooltip-info"),
|
target.querySelectorAll(".o_kanban_group .o_kanban_header_title")[0],
|
||||||
'{"entries":[{"title":"Name","value":"hello"}]}'
|
null,
|
||||||
);
|
"mouseleave"
|
||||||
assert.strictEqual(
|
|
||||||
target.querySelectorAll(".o_kanban_header_title")[1].getAttribute("data-tooltip-info"),
|
|
||||||
'{"entries":[{"title":"Name","value":"xm"}]}'
|
|
||||||
);
|
);
|
||||||
|
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) => {
|
QUnit.test("move a record then put it again in the same column", async (assert) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user