[REF] web: simplify popover hook usage

**Before this commit**
Almost all of the use cases of the `usePopover` hook are dealing
with an abnormal amount of complexity:
- they have to store the closing function of the popover, and sometimes
  keep track to its opening state,
- you can find a duplication of a useUniquePopover hook, which is
  anyway the behavior that is always wanted for popover users.

This is due to the fact that usePopover is just some kind of wrapper
to the lower level popover service.

**After the commit**
- The usePopover now has a new API, which simplifies its usage.
- The useUniquePopover hook implementations have been removed and
  this "unique" behavior is now merged into usePopover
  and has become the standard one (not configurable).
- All of the use cases have been adapted, even the cases where
  components used directly the popover service (through `useService()`).

See the adapted jsDoc around the hook in popover_hook.js

closes odoo/odoo#118066

Related: odoo/enterprise#39654
Signed-off-by: Michaël Mattiello <mcm@odoo.com>
This commit is contained in:
Bruno Boi
2023-04-13 16:40:19 +02:00
parent e9aced9d53
commit b4b834c0a9
27 changed files with 268 additions and 477 deletions
@@ -15,7 +15,8 @@ AccountPaymentPopOver.template = "account.AccountPaymentPopOver";
export class AccountPaymentField extends Component {
setup() {
this.popover = usePopover();
const position = localization.direction === "rtl" ? "bottom" : "left";
this.popover = usePopover(AccountPaymentPopOver, { position });
this.orm = useService("orm");
this.action = useService("action");
@@ -45,28 +46,12 @@ export class AccountPaymentField extends Component {
}
onInfoClick(ev, idx) {
if (this.popoverCloseFn) {
this.closePopover();
}
this.popoverCloseFn = this.popover.add(
ev.currentTarget,
AccountPaymentPopOver,
{
title: this.env._t("Journal Entry Info"),
...this.lines[idx],
_onRemoveMoveReconcile: this.removeMoveReconcile.bind(this),
_onOpenMove: this.openMove.bind(this),
onClose: this.closePopover,
},
{
position: localization.direction === "rtl" ? "bottom" : "left",
},
);
}
closePopover() {
this.popoverCloseFn();
this.popoverCloseFn = null;
this.popover.open(ev.currentTarget, {
title: this.env._t("Journal Entry Info"),
...this.lines[idx],
_onRemoveMoveReconcile: this.removeMoveReconcile.bind(this),
_onOpenMove: this.openMove.bind(this),
});
}
async assignOutstandingCredit(id) {
@@ -76,7 +61,7 @@ export class AccountPaymentField extends Component {
}
async removeMoveReconcile(moveId, partialId) {
this.closePopover();
this.popover.close();
await this.orm.call(this.props.record.resModel, 'js_remove_outstanding_partial', [moveId, partialId], {});
await this.props.record.model.root.load();
this.props.record.model.notify();
@@ -7,23 +7,6 @@ import { onEmployeeSubRedirect } from './hooks';
const { Component, onWillStart, onWillRender, useState } = owl;
function useUniquePopover() {
const popover = usePopover();
let remove = null;
return Object.assign(Object.create(popover), {
add(target, component, props, options) {
if (remove) {
remove();
}
remove = popover.add(target, component, props, options);
return () => {
remove();
remove = null;
};
},
});
}
class HrOrgChartPopover extends Component {
async setup() {
super.setup();
@@ -56,7 +39,7 @@ export class HrOrgChart extends Component {
this.orm = useService('orm');
this.actionService = useService("action");
this.user = useService("user");
this.popover = useUniquePopover();
this.popover = usePopover(HrOrgChartPopover);
this.state = useState({'employee_id': null});
this._onEmployeeSubRedirect = onEmployeeSubRedirect();
@@ -109,12 +92,7 @@ export class HrOrgChart extends Component {
}
_onOpenPopover(event, employee) {
this.popover.add(
event.currentTarget,
HrOrgChartPopover,
{employee},
{closeOnClickAway: true}
);
this.popover.open(event.currentTarget, { employee });
}
/**
+3 -7
View File
@@ -58,6 +58,7 @@ export class Message extends Component {
MessageReactions,
MessageSeenIndicator,
ImStatus,
Popover: MessageNotificationPopover,
RelativeTime,
};
static defaultProps = {
@@ -78,7 +79,7 @@ export class Message extends Component {
static template = "mail.Message";
setup() {
this.popover = usePopover();
this.popover = usePopover(this.constructor.components.Popover, { position: "top" });
this.state = useState({
isEditing: false,
isHovered: false,
@@ -456,12 +457,7 @@ export class Message extends Component {
}
onClickNotificationIcon(ev) {
this.popover.add(
ev.target,
MessageNotificationPopover,
{ message: this.message },
{ position: "top" }
);
this.popover.open(ev.target, { message: this.message });
}
onClickFailure(ev) {
+6 -17
View File
@@ -60,8 +60,9 @@ export class Discuss extends Component {
this.messageEdition = useMessageEdition();
this.messageToReplyTo = useMessageToReplyTo();
this.contentRef = useRef("content");
this.popover = usePopover();
this.closePopover = null;
this.popover = usePopover(ChannelInvitation, {
onClose: () => (this.state.isAddingUsers = false),
});
this.addUsersRef = useRef("addUsers");
this.state = useState({
activeMode: this.MODES.NONE,
@@ -120,23 +121,11 @@ export class Discuss extends Component {
}
toggleInviteForm() {
if (this.closePopover) {
this.closePopover();
this.closePopover = null;
if (this.popover.isOpen) {
this.popover.close();
} else {
const el = this.addUsersRef.el;
this.state.isAddingUsers = true;
this.closePopover = this.popover.add(
el,
ChannelInvitation,
{ thread: this.thread },
{
onClose: () => {
this.state.isAddingUsers = false;
this.closePopover = null;
},
}
);
this.popover.open(this.addUsersRef.el, { thread: this.thread });
}
}
@@ -30,8 +30,7 @@ import { escapeRegExp } from "@web/core/utils/strings";
*/
export function useEmojiPicker(ref, props, options = {}) {
const targets = [];
const popover = usePopover();
let closePopover = false;
const popover = usePopover(EmojiPicker, { ...options, popoverClass: "o-fast-popover" });
props.storeScroll = {
scrollValue: 0,
set: (value) => {
@@ -59,20 +58,10 @@ export function useEmojiPicker(ref, props, options = {}) {
}
function toggle(ref, onSelect = props.onSelect) {
if (closePopover) {
closePopover();
closePopover = false;
if (popover.isOpen) {
popover.close();
} else {
closePopover = popover.add(
ref.el,
EmojiPicker,
{ ...props, onSelect },
{
...options,
onClose: () => (closePopover = false),
popoverClass: "o-fast-popover",
}
);
popover.open(ref.el, { ...props, onSelect });
}
}
@@ -109,7 +98,7 @@ export function useEmojiPicker(ref, props, options = {}) {
return {
add,
get isOpen() {
return Boolean(closePopover);
return popover.isOpen;
},
};
}
@@ -17,11 +17,9 @@ export class CallParticipantCard extends Component {
static components = { CallParticipantVideo };
static template = "mail.CallParticipantCard";
closePopover;
setup() {
this.contextMenuAnchorRef = useRef("contextMenuAnchor");
this.popover = usePopover();
this.popover = usePopover(CallContextMenu);
this.rpc = useService("rpc");
this.rtc = useRtc();
this.store = useStore();
@@ -125,24 +123,15 @@ export class CallParticipantCard extends Component {
onContextMenu(ev) {
ev.preventDefault();
markEventHandled(ev, "CallParticipantCard.clickVolumeAnchor");
if (this.closePopover) {
this.closePopover();
this.closePopover = undefined;
if (this.popover.isOpen) {
this.popover.close();
return;
}
if (!this.contextMenuAnchorRef?.el) {
return;
}
this.closePopover = this.popover.add(
this.contextMenuAnchorRef.el,
CallContextMenu,
{
rtcSession: this.rtcSession,
},
{
onClose: () => (this.closePopover = undefined),
position: "bottom",
}
);
this.popover.open(this.contextMenuAnchorRef.el, {
rtcSession: this.rtcSession,
});
}
}
@@ -41,7 +41,7 @@ export class Activity extends Component {
showDetails: false,
delay: computeDelay(this.props.data.date_deadline),
});
this.popover = usePopover();
this.popover = usePopover(ActivityMarkAsDone, { position: "right" });
onMounted(() => {
this.updateDelayAtNight();
});
@@ -70,21 +70,15 @@ export class Activity extends Component {
}
async onClickMarkAsDone(ev) {
if (this.closePopover) {
this.closePopover();
this.closePopover = undefined;
if (this.popover.isOpen) {
this.popover.close();
return;
}
this.closePopover = this.popover.add(
ev.currentTarget,
ActivityMarkAsDone,
{
activity: this.props.data,
hasHeader: true,
reload: this.props.onUpdate,
},
{ position: "right" }
);
this.popover.open(ev.currentTarget, {
activity: this.props.data,
hasHeader: true,
reload: this.props.onUpdate,
});
}
async onFileUploaded(data) {
@@ -11,9 +11,8 @@ export class ActivityButton extends Component {
static template = "mail.ActivityButton";
setup() {
this.popover = usePopover();
this.popover = usePopover(ActivityListPopover, { position: "bottom-start" });
this.buttonRef = useRef("button");
this.closePopover = undefined;
}
get buttonClass() {
@@ -53,26 +52,17 @@ export class ActivityButton extends Component {
}
onClick() {
if (this.closePopover) {
this.closePopover();
this.closePopover = undefined;
if (this.popover.isOpen) {
this.popover.close();
} else {
this.closePopover = this.popover.add(
this.buttonRef.el,
ActivityListPopover,
{
activityIds: this.props.record.data.activity_ids.currentIds,
onActivityChanged: () => {
this.props.record.model.load({ resId: this.props.record.resId });
},
resId: this.props.record.resId,
resModel: this.props.record.resModel,
this.popover.open(this.buttonRef.el, {
activityIds: this.props.record.data.activity_ids.currentIds,
onActivityChanged: () => {
this.props.record.model.load({ resId: this.props.record.resId });
},
{
onClose: () => (this.closePopover = undefined),
position: "bottom", // should be "bottom-start" but not supported for some reason
}
);
resId: this.props.record.resId,
resModel: this.props.record.resModel,
});
}
}
}
@@ -21,7 +21,7 @@ export class ActivityCell extends Component {
static template = "mail.ActivityCell";
setup() {
this.popover = usePopover();
this.popover = usePopover(ActivityListPopover, { position: "bottom-start" });
this.contentRef = useRef("content");
}
@@ -43,27 +43,18 @@ export class ActivityCell extends Component {
}
onClick() {
if (this.closePopover) {
this.closePopover();
this.closePopover = undefined;
if (this.popover.isOpen) {
this.popover.close();
} else {
this.closePopover = this.popover.add(
this.contentRef.el,
ActivityListPopover,
{
activityIds: this.props.activityIds,
defaultActivityTypeId: this.props.activityTypeId,
onActivityChanged: () => {
this.props.reloadFunc();
},
resId: this.props.resId,
resModel: this.props.resModel,
this.popover.open(this.contentRef.el, {
activityIds: this.props.activityIds,
defaultActivityTypeId: this.props.activityTypeId,
onActivityChanged: () => {
this.props.reloadFunc();
},
{
onClose: () => (this.closePopover = undefined),
position: "bottom", // should be "bottom-start" but not supported for some reason
}
);
resId: this.props.resId,
resModel: this.props.resModel,
});
}
}
}
@@ -6,7 +6,7 @@ import { registry } from "@web/core/registry";
import { useService } from "@web/core/utils/hooks";
import { usePopover } from "@web/core/popover/popover_hook";
const { Component, EventBus, onWillRender } = owl;
const { Component, onWillRender } = owl;
export class QtyAtDatePopover extends Component {
setup() {
@@ -30,9 +30,7 @@ QtyAtDatePopover.template = "sale_stock.QtyDetailPopOver";
export class QtyAtDateWidget extends Component {
setup() {
this.bus = new EventBus();
this.popover = usePopover();
this.closePopover = null;
this.popover = usePopover(this.constructor.components.Popover, { position: "top" });
this.calcData = {};
onWillRender(() => {
this.initCalcData();
@@ -74,15 +72,10 @@ export class QtyAtDateWidget extends Component {
showPopup(ev) {
this.updateCalcData();
this.closePopover = this.popover.add(
ev.currentTarget,
this.constructor.components.Popover,
{bus: this.bus, record: this.props.record, calcData: this.calcData},
{
position: 'top',
}
);
this.bus.addEventListener('close-popover', this.closePopover);
this.popover.open(ev.currentTarget, {
record: this.props.record,
calcData: this.calcData,
});
}
}
@@ -54,22 +54,10 @@ patch(Message.prototype, "snailmail", {
},
});
},
onClickNotificationIcon(ev) {
if (this.message.type === "snailmail") {
this.popover.add(
ev.target,
SnailmailNotificationPopover,
{ message: this.message },
{ position: "top" }
);
} else {
this._super(...arguments);
}
},
});
Message.components = {
...Message.components,
Popover: SnailmailNotificationPopover,
SnailmailError,
};
@@ -1,11 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="snailmail.SnailmailNotificationPopover" owl="1">
<div class="o-snailmail-SnailmailNotificationPopover m-2" t-attf-class="{{ className }}" t-ref="root">
<i class="me-2" t-att-class="props.message.notifications[0].statusIcon" role="img"/>
<span t-esc="props.message.notifications[0].statusTitle"/>
</div>
<t t-name="snailmail.SnailmailNotificationPopover" t-inherit="mail.MessageNotificationPopover" t-inherit-mode="primary" owl="1">
<xpath expr="//div[hasclass('o-mail-MessageNotificationPopover')]" position="replace">
<t t-if="props.message.type === 'snailmail'">
<div class="o-snailmail-SnailmailNotificationPopover m-2" t-attf-class="{{ className }}">
<i class="me-2" t-att-class="props.message.notifications[0].statusIcon" role="img"/>
<span t-esc="props.message.notifications[0].statusTitle"/>
</div>
</t>
<t t-else="">$0</t>
</xpath>
</t>
</templates>
@@ -26,23 +26,16 @@ PopoverComponent.template = 'stock.popoverContent';
export class PopoverWidgetField extends Component {
setup(){
this.popover = usePopover();
this.closePopover = null;
let fieldValue = this.props.record.data[this.props.name];
this.jsonValue = JSON.parse(fieldValue || "{}");
const position = this.jsonValue.position || "top";
this.popover = usePopover(this.constructor.components.Popover, { position });
this.color = this.jsonValue.color || 'text-primary';
this.icon = this.jsonValue.icon || 'fa-info-circle';
}
showPopup(ev){
this.closePopover = this.popover.add(
ev.currentTarget,
this.constructor.components.Popover,
{...this.jsonValue, record: this.props.record},
{
position: this.jsonValue.position || 'top',
}
);
this.popover.open(ev.currentTarget, { ...this.jsonValue, record: this.props.record });
}
}
@@ -1,14 +1,21 @@
/** @odoo-module **/
import { useModelField } from "./model_field_hook";
import { useUniquePopover } from "./unique_popover_hook";
import { usePopover } from "@web/core/popover/popover_hook";
import { ModelFieldSelectorPopover } from "./model_field_selector_popover";
import { Component, onWillStart, onWillUpdateProps, toRaw, useState } from "@odoo/owl";
export class ModelFieldSelector extends Component {
setup() {
this.popover = useUniquePopover();
this.popover = usePopover(this.constructor.components.Popover, {
popoverClass: "o_popover_field_selector",
onClose: () => {
if (this.state.isDirty) {
this.props.update(this.state.fieldName, toRaw(this.state.chain));
}
},
});
this.modelField = useModelField();
this.state = useState({
chain: [],
@@ -54,28 +61,15 @@ export class ModelFieldSelector extends Component {
if (this.props.readonly) {
return;
}
this.popover.add(
ev.currentTarget,
this.constructor.components.Popover,
{
chain: this.state.chain,
update: this.update.bind(this),
showSearchInput: this.props.showSearchInput,
isDebugMode: this.props.isDebugMode,
loadChain: this.loadChain.bind(this),
filter: this.props.filter,
followRelations: this.props.followRelations,
},
{
closeOnClickAway: true,
popoverClass: "o_popover_field_selector",
onClose: () => {
if (this.state.isDirty) {
this.props.update(this.state.fieldName, toRaw(this.state.chain));
}
},
}
);
this.popover.open(ev.currentTarget, {
chain: this.state.chain,
update: this.update.bind(this),
showSearchInput: this.props.showSearchInput,
isDebugMode: this.props.isDebugMode,
loadChain: this.loadChain.bind(this),
filter: this.props.filter,
followRelations: this.props.followRelations,
});
}
}
@@ -1,20 +0,0 @@
/** @odoo-module **/
import { usePopover } from "@web/core/popover/popover_hook";
export function useUniquePopover() {
const popover = usePopover();
let remove = null;
return Object.assign(Object.create(popover), {
add(target, component, props, options) {
if (remove) {
remove();
}
remove = popover.add(target, component, props, options);
return () => {
remove();
remove = null;
};
},
});
}
@@ -4,39 +4,47 @@ import { useService } from "@web/core/utils/hooks";
import { onWillUnmount, status, useComponent } from "@odoo/owl";
export function usePopover() {
const removeFns = new Set();
const service = useService("popover");
const component = useComponent();
/**
* @typedef PopoverHookReturnType
* @property {(target: string | HTMLElement, props: object) => void} open
* - Signals the manager to open the configured popover
* component on the target, with the given props.
* @property {() => void} close
* - Signals the manager to remove the popover.
* @property {boolean} isOpen
* - Whether the popover is currently open.
*/
onWillUnmount(function () {
for (const removeFn of removeFns) {
removeFn();
}
removeFns.clear();
});
/**
* Manages a component to be used as a popover.
*
* @param {typeof import("@odoo/owl").Component} component
* @param {import("@web/core/popover/popover_service").PopoverServiceAddOptions} [options]
* @returns {PopoverHookReturnType}
*/
export function usePopover(component, options = {}) {
let removeFn = null;
const popover = useService("popover");
const owner = useComponent();
function close() {
removeFn?.();
}
onWillUnmount(close);
return {
/**
* Signals the manager to add a popover.
*
* @param {string | HTMLElement} target
* @param {typeof import("@odoo/owl").Component} Component
* @param {object} props
* @param {import("@web/core/popover/popover_service").PopoverServiceAddOptions} [options]
* @returns {() => void}
*/
add(target, Component, props, options = {}) {
open(target, props) {
close();
const newOptions = Object.create(options);
newOptions.onClose = function () {
removeFns.delete(removeFn);
if (options.onClose && status(component) !== "destroyed") {
options.onClose();
removeFn = null;
if (status(owner) !== "destroyed") {
options.onClose?.();
}
};
const removeFn = service.add(target, Component, props, newOptions);
removeFns.add(removeFn);
return removeFn;
removeFn = popover.add(target, component, props, newOptions);
},
close,
get isOpen() {
return Boolean(removeFn);
},
};
}
+13 -14
View File
@@ -17,9 +17,12 @@ import {
export function useCalendarPopover(component) {
const owner = useComponent();
const popover = usePopover();
let popoverClass = "";
const popoverOptions = { position: "right", onClose: cleanup };
Object.defineProperty(popoverOptions, "popoverClass", { get: () => popoverClass });
const popover = usePopover(component, popoverOptions);
const dialog = useService("dialog");
let remove = null;
let removeDialog = null;
let fcPopover;
useExternalListener(
window,
@@ -34,27 +37,23 @@ export function useCalendarPopover(component) {
);
function cleanup() {
fcPopover = null;
remove = null;
removeDialog = null;
}
function close() {
if (remove) {
remove();
}
removeDialog?.();
popover.close();
cleanup();
}
return {
close,
open(target, props, popoverClass) {
close();
open(target, props, popoverClassToUse) {
fcPopover = target.closest(".fc-popover");
if (owner.env.isSmall) {
remove = dialog.add(component, props, { onClose: cleanup });
close();
removeDialog = dialog.add(component, props, { onClose: cleanup });
} else {
remove = popover.add(target, component, props, {
popoverClass,
position: "right",
onClose: cleanup,
});
popoverClass = popoverClassToUse;
popover.open(target, props);
}
},
};
@@ -2,9 +2,9 @@
import { browser } from "@web/core/browser/browser";
import { Tooltip } from "@web/core/tooltip/tooltip";
import { useService } from "@web/core/utils/hooks";
import { usePopover } from "@web/core/popover/popover_hook";
import { Component, useRef } from "@odoo/owl";
export class CopyButton extends Component {
static template = "web.CopyButton";
static props = {
@@ -17,16 +17,12 @@ export class CopyButton extends Component {
setup() {
this.button = useRef("button");
this.popover = useService("popover");
this.popover = usePopover(Tooltip);
}
showTooltip() {
const closeTooltip = this.popover.add(this.button.el, Tooltip, {
tooltip: this.props.successText,
});
browser.setTimeout(() => {
closeTooltip();
}, 800);
this.popover.open(this.button.el, { tooltip: this.props.successText });
browser.setTimeout(this.popover.close, 800);
}
async onClick() {
@@ -43,7 +39,7 @@ export class CopyButton extends Component {
}
try {
await write(this.props.content);
} catch(error) {
} catch (error) {
return browser.console.warn(error);
}
this.showTooltip();
@@ -1,6 +1,6 @@
/** @odoo-module **/
import { useUniquePopover } from "@web/core/model_field_selector/unique_popover_hook";
import { usePopover } from "@web/core/popover/popover_hook";
import { useModelField } from "@web/core/model_field_selector/model_field_hook";
import { useService } from "@web/core/utils/hooks";
import { ModelFieldSelectorPopover } from "@web/core/model_field_selector/model_field_selector_popover";
@@ -10,7 +10,12 @@ export function useDynamicPlaceholder(elementRef) {
const TRIGGER_KEY = "#";
const ownerField = useComponent();
const triggerKeyReplaceRegex = new RegExp(`${TRIGGER_KEY}$`);
const popover = useUniquePopover();
let closeCallback;
let positionCallback;
const popoverOptions = {};
Object.defineProperty(popoverOptions, "onClose", { get: () => closeCallback });
Object.defineProperty(popoverOptions, "onPositioned", { get: () => positionCallback });
const popover = usePopover(ModelFieldSelectorPopover, popoverOptions);
const modelField = useModelField();
const notification = useService("notification");
@@ -54,12 +59,12 @@ export function useDynamicPlaceholder(elementRef) {
* placeholder string in the Input with or without a default text value.
*
* @public
* @param {Object} options
* @param {Object} opts
* @param {function} options.validateCallback
* @param {function} options.closeCallback
* @param {function} [options.positionCallback]
*/
async function open(options) {
async function open(opts) {
if (!model) {
return notification.add(
ownerField.env._t(
@@ -70,25 +75,18 @@ export function useDynamicPlaceholder(elementRef) {
}
dynamicPlaceholderChain = await modelField.loadChain(model, "");
popover.add(
elementRef?.el,
ModelFieldSelectorPopover,
{
chain: dynamicPlaceholderChain,
update: (chain) => { dynamicPlaceholderChain = chain; },
validate: options.validateCallback,
showSearchInput: true,
isDebugMode: true,
needDefaultValue: true,
loadChain: modelField.loadChain,
filter: (model) => !["one2many", "boolean", "many2many"].includes(model.type),
},
{
closeOnClickAway: true,
onClose: options.closeCallback,
onPositioned: options.positionCallback,
}
);
closeCallback = opts.closeCallback;
positionCallback = opts.positionCallback;
popover.open(elementRef?.el, {
chain: dynamicPlaceholderChain,
update: (chain) => (dynamicPlaceholderChain = chain),
validate: opts.validateCallback,
showSearchInput: true,
isDebugMode: true,
needDefaultValue: true,
loadChain: modelField.loadChain,
filter: (model) => !["one2many", "boolean", "many2many"].includes(model.type),
});
}
async function onKeydown(ev) {
const element = elementRef?.el;
@@ -59,7 +59,7 @@ export class Many2ManyTagsField extends Component {
setup() {
this.orm = useService("orm");
this.previousColorsMap = {};
this.popover = usePopover();
this.popover = usePopover(this.constructor.components.Popover);
this.dialog = useService("dialog");
this.dialogClose = [];
@@ -312,37 +312,22 @@ export class Many2ManyTagsFieldColorEditable extends Many2ManyTagsField {
return props;
}
closePopover() {
this.popoverCloseFn();
this.popoverCloseFn = null;
}
onBadgeClick(ev, record) {
if (!this.props.canEditColor) {
return;
}
const isClosed = !document.querySelector(".o_tag_popover");
if (isClosed) {
this.currentPopoverEl = null;
}
if (this.popoverCloseFn) {
this.closePopover();
}
if (isClosed || this.currentPopoverEl !== ev.currentTarget) {
this.currentPopoverEl = ev.currentTarget;
this.popoverCloseFn = this.popover.add(
ev.currentTarget,
this.constructor.components.Popover,
{
colors: this.constructor.RECORD_COLORS,
tag: {
id: record.id,
colorIndex: record.data[this.props.colorField],
},
switchTagColor: this.switchTagColor.bind(this),
onTagVisibilityChange: this.onTagVisibilityChange.bind(this),
}
);
if (this.popover.isOpen) {
this.popover.close();
} else {
this.popover.open(ev.currentTarget, {
colors: this.constructor.RECORD_COLORS,
tag: {
id: record.id,
colorIndex: record.data[this.props.colorField],
},
switchTagColor: this.switchTagColor.bind(this),
onTagVisibilityChange: this.onTagVisibilityChange.bind(this),
});
}
}
@@ -357,7 +342,7 @@ export class Many2ManyTagsFieldColorEditable extends Many2ManyTagsField {
[this.props.colorField]: isHidden ? 0 : this.previousColorsMap[tagRecord.resId] || 1,
});
tagRecord.save();
this.closePopover();
this.popover.close();
}
switchTagColor(colorIndex, tag) {
@@ -366,7 +351,7 @@ export class Many2ManyTagsFieldColorEditable extends Many2ManyTagsField {
);
tagRecord.update({ [this.props.colorField]: colorIndex });
tagRecord.save();
this.closePopover();
this.popover.close();
}
}
@@ -101,39 +101,26 @@ export class KanbanMany2ManyTagsAvatarFieldTagsList extends TagsList {
};
setup() {
super.setup();
this.popover = usePopover();
this.popover = usePopover(Many2ManyTagsAvatarFieldPopover, {
popoverClass: "o_m2m_tags_avatar_field_popover",
closeOnClickAway: (target) => !target.closest(".modal"),
});
}
get visibleTagsCount() {
return this.props.itemsVisible;
}
closePopover() {
this.closePopoverFn();
this.closePopoverFn = null;
}
openPopover(ev) {
if (this.props.readonly) {
return;
}
if (this.closePopoverFn) {
this.closePopover();
}
this.closePopoverFn = this.popover.add(
ev.currentTarget.parentElement,
Many2ManyTagsAvatarFieldPopover,
{
...this.props.popoverProps,
readonly: false,
canCreate: false,
canCreateEdit: false,
canQuickCreate: false,
placeholder: this.env._t("Search users..."),
},
{
position: "bottom",
popoverClass: "o_m2m_tags_avatar_field_popover",
closeOnClickAway: (target) => !target.closest(".modal"),
}
);
this.popover.open(ev.currentTarget.parentElement, {
...this.props.popoverProps,
readonly: false,
canCreate: false,
canCreateEdit: false,
canQuickCreate: false,
placeholder: this.env._t("Search users..."),
});
}
get canDisplayQuickAssignAvatar() {
return !this.props.readonly && !(this.props.tags && this.otherTags.length);
@@ -63,12 +63,10 @@ export class KanbanMany2OneAvatarField extends Many2OneAvatarField {
};
setup() {
super.setup();
this.popover = usePopover();
}
closePopover() {
this.closePopoverFn();
this.closePopoverFn = null;
this.popover = usePopover(Many2OneFieldPopover, {
popoverClass: "o_m2o_tags_avatar_field_popover",
closeOnClickAway: (target) => !target.closest(".modal"),
});
}
get popoverProps() {
const props = {
@@ -82,25 +80,13 @@ export class KanbanMany2OneAvatarField extends Many2OneAvatarField {
if (!this.props.isEditable) {
return;
}
if (this.closePopoverFn) {
this.closePopover();
}
this.closePopoverFn = this.popover.add(
ev.currentTarget,
Many2OneFieldPopover,
{
...this.popoverProps,
canCreate: false,
canCreateEdit: false,
canQuickCreate: false,
placeholder: this.env._t("Search user..."),
},
{
position: "bottom",
popoverClass: "o_m2o_tags_avatar_field_popover",
closeOnClickAway: (target) => !target.closest(".modal"),
}
);
this.popover.open(ev.currentTarget, {
...this.popoverProps,
canCreate: false,
canCreateEdit: false,
canQuickCreate: false,
placeholder: this.env._t("Search user..."),
});
}
}
@@ -37,7 +37,12 @@ export class PropertiesField extends Component {
this.orm = useService("orm");
this.user = useService("user");
this.dialogService = useService("dialog");
this.popover = usePopover();
this.popover = usePopover(PropertyDefinition, {
closeOnClickAway: this.checkPopoverClose,
popoverClass: "o_property_field_popover",
position: "top",
onClose: () => this.onCloseCurrentPopover?.(),
});
this.propertiesRef = useRef("properties");
this.state = useState({
@@ -261,10 +266,7 @@ export class PropertiesField extends Component {
),
confirmLabel: _lt("Delete"),
confirm: () => {
if (this.popoverCloseFn) {
this.popoverCloseFn();
this.popoverCloseFn = null;
}
this.popover.close();
const propertiesDefinitions = this.propertiesList;
propertiesDefinitions.find(
(property) => property.name === propertyName
@@ -459,38 +461,31 @@ export class PropertiesField extends Component {
return propertyName;
};
this.popoverCloseFn = this.popover.add(
target,
PropertyDefinition,
{
readonly: this.props.readonly || !this.state.canChangeDefinition,
canChangeDefinition: this.state.canChangeDefinition,
propertyDefinition: this.propertiesList.find(
(property) => property.name === currentName(propertyName)
),
context: this.props.context,
onChange: this.onPropertyDefinitionChange.bind(this),
onDelete: () => this.onPropertyDelete(currentName(propertyName)),
onPropertyMove: (direction) =>
this.onPropertyMove(currentName(propertyName), direction),
isNewlyCreated: isNewlyCreated,
propertyIndex: propertyIndex,
propertiesSize: propertiesList.length,
hideKanbanOption: this.props.hideKanbanOption,
},
{
closeOnClickAway: this.checkPopoverClose,
popoverClass: "o_property_field_popover",
position: "top",
onClose: () => {
this.state.movedPropertyName = null;
target.classList.remove("disabled");
if (isNewlyCreated) {
this._setDefaultPropertyValue(currentName(propertyName));
}
},
this.onCloseCurrentPopover = () => {
this.onCloseCurrentPopover = null;
this.state.movedPropertyName = null;
target.classList.remove("disabled");
if (isNewlyCreated) {
this._setDefaultPropertyValue(currentName(propertyName));
}
);
};
this.popover.open(target, {
readonly: this.props.readonly || !this.state.canChangeDefinition,
canChangeDefinition: this.state.canChangeDefinition,
propertyDefinition: this.propertiesList.find(
(property) => property.name === currentName(propertyName)
),
context: this.props.context,
onChange: this.onPropertyDefinitionChange.bind(this),
onDelete: () => this.onPropertyDelete(currentName(propertyName)),
onPropertyMove: (direction) =>
this.onPropertyMove(currentName(propertyName), direction),
isNewlyCreated: isNewlyCreated,
propertyIndex: propertyIndex,
propertiesSize: propertiesList.length,
hideKanbanOption: this.props.hideKanbanOption,
});
}
/**
@@ -24,7 +24,7 @@ Object.assign(PropertyTagAutoComplete, { timeout: 0 });
export class PropertyTags extends Component {
setup() {
this.notification = useService("notification");
this.popover = usePopover();
this.popover = usePopover(this.constructor.components.Popover);
}
/* --------------------------------------------------------
@@ -251,18 +251,11 @@ export class PropertyTags extends Component {
event.currentTarget.blur();
return;
}
this.popoverCloseFn = this.popover.add(
event.currentTarget,
this.constructor.components.Popover,
{
colors: [...Array(ColorList.COLORS.length).keys()],
tag: { id: tagId, colorIndex: tagColor },
switchTagColor: this.onTagColorSwitch.bind(this),
},
{
closeOnClickAway: true,
}
);
this.popover.open(event.currentTarget, {
colors: [...Array(ColorList.COLORS.length).keys()],
tag: { id: tagId, colorIndex: tagColor },
switchTagColor: this.onTagColorSwitch.bind(this),
});
}
/**
@@ -277,8 +270,7 @@ export class PropertyTags extends Component {
this.props.onTagsChange(availableTags);
// close the color popover
this.popoverCloseFn();
this.popoverCloseFn = null;
this.popover.close();
}
}
@@ -35,33 +35,24 @@ export class KanbanHeader extends Component {
setup() {
this.dialog = useService("dialog");
this.orm = useService("orm");
this.rootRef = useRef("root");
const titleRef = useRef("title");
this.popover = usePopover(KanbanHeaderTooltip);
this.onTitleMouseEnter = useDebounced(this.onTitleMouseEnter, 400);
}
const popover = usePopover();
async onTitleMouseEnter(ev) {
if (!this.hasTooltip) {
return;
}
const tooltip = await this.loadTooltip();
if (tooltip.length) {
this.popover.open(ev.target, { tooltip });
}
}
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();
};
onTitleMouseLeave() {
this.onTitleMouseEnter.cancel();
this.popover.close();
}
// ------------------------------------------------------------------------
@@ -3,7 +3,7 @@
<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()">
<div class="o_kanban_header_title position-relative d-flex align-items-baseline lh-lg text-900" t-on-mouseenter="onTitleMouseEnter" t-on-mouseleave="onTitleMouseLeave">
<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">
@@ -48,17 +48,17 @@ QUnit.test("close popover when component is unmounted", async (assert) => {
class CompWithPopover extends Component {
setup() {
this.popover = usePopover();
this.popover = usePopover(Comp);
}
}
CompWithPopover.template = xml`<div />`;
const comp1 = await mount(CompWithPopover, target, { env });
comp1.popover.add(popoverTarget, Comp, { id: "comp1" });
comp1.popover.open(popoverTarget, { id: "comp1" });
await nextTick();
const comp2 = await mount(CompWithPopover, target, { env });
comp2.popover.add(popoverTarget, Comp, { id: "comp2" });
comp2.popover.open(popoverTarget, { id: "comp2" });
await nextTick();
assert.containsN(target, ".o_popover", 2);