[IMP] web, mass_mailing: make domain field foldable
This commit allows the domain field to become foldable such that it will display its domain in the form of facets (similar to the search bar's ones) when it is folded. Also applies this functionality directly to the mass_mailing form view. task-3339512 closes odoo/odoo#126789 Signed-off-by: Mathieu Duckerts-Antoine (dam) <dam@odoo.com>
This commit is contained in:
@@ -232,7 +232,7 @@
|
||||
<field name="mailing_on_mailing_list" invisible="1"/>
|
||||
<field name="mailing_model_real" invisible="1"/>
|
||||
<div attrs="{'invisible': [('mailing_on_mailing_list', '=', True)]}">
|
||||
<field name="mailing_domain" widget="domain" options="{'model': 'mailing_model_real'}"
|
||||
<field name="mailing_domain" widget="domain" options="{'model': 'mailing_model_real', 'foldable': true}"
|
||||
attrs="{'readonly': [('state', 'in', ('sending', 'done'))]}"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,10 +3,19 @@
|
||||
import { Domain } from "@web/core/domain";
|
||||
import { getDefaultValue, getDefaultOperator } from "./domain_selector_fields";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
import { Expression, toValue, toDomain, toTree } from "@web/core/domain_tree";
|
||||
import { _t } from "@web/core/l10n/translation";
|
||||
import { unique, zip } from "@web/core/utils/arrays";
|
||||
import { sprintf } from "@web/core/utils/strings";
|
||||
import {
|
||||
Expression,
|
||||
toValue,
|
||||
toDomain,
|
||||
toTree,
|
||||
normalizeValue,
|
||||
formatValue as toSring,
|
||||
} from "@web/core/domain_tree";
|
||||
import { useLoadFieldInfo, useLoadPathDescription } from "@web/core/model_field_selector/utils";
|
||||
import { getOperatorInfo } from "@web/core/domain_selector/domain_selector_operators";
|
||||
|
||||
/**
|
||||
* @param {import("@web/core/domain_tree").Tree} tree
|
||||
@@ -276,3 +285,143 @@ export function useGetDefaultLeafDomain() {
|
||||
return getDefaultDomain(fieldDefs);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Tree} tree
|
||||
* @returns {tree}
|
||||
*/
|
||||
export function simplifyTree(tree, isRoot = true) {
|
||||
if (tree.type === "condition") {
|
||||
return tree;
|
||||
}
|
||||
const processedChildren = tree.children.map((c) => simplifyTree(c, false));
|
||||
if (tree.value === "&") {
|
||||
return { ...tree, children: processedChildren };
|
||||
}
|
||||
const children = [];
|
||||
const childrenByPath = {};
|
||||
for (const child of processedChildren) {
|
||||
if (
|
||||
child.type === "connector" ||
|
||||
typeof child.path !== "string" ||
|
||||
!["=", "in"].includes(child.operator)
|
||||
) {
|
||||
children.push(child);
|
||||
} else {
|
||||
if (!childrenByPath[child.path]) {
|
||||
childrenByPath[child.path] = [];
|
||||
}
|
||||
childrenByPath[child.path].push(child);
|
||||
}
|
||||
}
|
||||
for (const path in childrenByPath) {
|
||||
if (childrenByPath[path].length === 1) {
|
||||
children.push(childrenByPath[path][0]);
|
||||
continue;
|
||||
}
|
||||
const value = [];
|
||||
for (const child of childrenByPath[path]) {
|
||||
if (child.operator === "=") {
|
||||
value.push(child.value);
|
||||
} else {
|
||||
value.push(...child.value);
|
||||
}
|
||||
}
|
||||
children.push({
|
||||
type: "condition",
|
||||
negate: false,
|
||||
operator: "in",
|
||||
path: path,
|
||||
value: normalizeValue(value),
|
||||
});
|
||||
}
|
||||
if (children.length === 1 && !isRoot) {
|
||||
return { ...children[0] };
|
||||
}
|
||||
return { ...tree, children };
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Tree} tree
|
||||
* @param {Function} getDescription
|
||||
* @param {boolean} [isSubExpression=true]
|
||||
* @returns {string}
|
||||
*/
|
||||
export function getDomainTreeDescription(
|
||||
tree,
|
||||
getFieldDef,
|
||||
getDescription,
|
||||
displayNames,
|
||||
isSubExpression = true
|
||||
) {
|
||||
if (tree.type === "connector") {
|
||||
// we assume that the domain tree is normalized (--> there is at least two children)
|
||||
const childDescriptions = tree.children.map((c) =>
|
||||
getDomainTreeDescription(c, getFieldDef, getDescription, displayNames)
|
||||
);
|
||||
const separator = tree.value === "&" ? _t("and") : _t("or");
|
||||
let description = childDescriptions.join(` ${separator} `);
|
||||
if (isSubExpression || tree.negate) {
|
||||
description = `( ${description} )`;
|
||||
}
|
||||
if (tree.negate) {
|
||||
description = `! ${description}`;
|
||||
}
|
||||
return description;
|
||||
}
|
||||
const { negate, operator, path, value } = tree;
|
||||
const fieldDef = getFieldDef(path);
|
||||
const operatorInfo = getOperatorInfo(operator, negate);
|
||||
const { operatorDescription, valueDescription } = leafToString(
|
||||
fieldDef,
|
||||
operatorInfo,
|
||||
value,
|
||||
displayNames[fieldDef?.relation]
|
||||
);
|
||||
let description = `${getDescription(path)} ${operatorDescription} `;
|
||||
if (valueDescription) {
|
||||
const { values, join, addParenthesis } = valueDescription;
|
||||
const jointedValues = values.join(` ${join} `);
|
||||
description += addParenthesis ? `( ${jointedValues} )` : jointedValues;
|
||||
}
|
||||
return description;
|
||||
}
|
||||
|
||||
export function useGetDomainTreeDescription(fieldService, nameService) {
|
||||
fieldService ||= useService("field");
|
||||
nameService ||= useService("name");
|
||||
const loadFieldInfo = useLoadFieldInfo(fieldService);
|
||||
const loadPathDescription = useLoadPathDescription(fieldService);
|
||||
const loadDisplayNames = useLoadDisplayNames(nameService);
|
||||
return async (resModel, tree) => {
|
||||
tree = simplifyTree(tree);
|
||||
const domain = toDomain(tree);
|
||||
const paths = extractPathsFromDomain(domain);
|
||||
const promises = [];
|
||||
const pathFieldDefs = {};
|
||||
const pathDescriptions = {};
|
||||
for (const path of paths) {
|
||||
promises.push(
|
||||
loadPathDescription(resModel, path).then(({ displayNames }) => {
|
||||
pathDescriptions[toSring(path)] = displayNames.join(" 🠒 ");
|
||||
}),
|
||||
loadFieldInfo(resModel, path).then(({ fieldDef }) => {
|
||||
pathFieldDefs[toSring(path)] = fieldDef;
|
||||
})
|
||||
);
|
||||
}
|
||||
await Promise.all(promises);
|
||||
const getFieldDef = (path) => pathFieldDefs[toSring(path)];
|
||||
const getDescription = (path) => pathDescriptions[toSring(path)];
|
||||
const idsByModel = extractIdsFromDomain(domain, getFieldDef);
|
||||
const treeWithVirtualOperators = createVirtualOperators(tree, getFieldDef);
|
||||
const displayNames = await loadDisplayNames(idsByModel);
|
||||
return getDomainTreeDescription(
|
||||
treeWithVirtualOperators,
|
||||
getFieldDef,
|
||||
getDescription,
|
||||
displayNames,
|
||||
false
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -19,17 +19,9 @@ import {
|
||||
import { FACET_ICONS, FACET_COLORS } from "./utils/misc";
|
||||
|
||||
import { EventBus, toRaw } from "@odoo/owl";
|
||||
import { toDomain, toTree, formatValue, normalizeValue } from "@web/core/domain_tree";
|
||||
import {
|
||||
createVirtualOperators,
|
||||
extractIdsFromDomain,
|
||||
extractPathsFromDomain,
|
||||
leafToString,
|
||||
useLoadDisplayNames,
|
||||
} from "@web/core/domain_selector/utils";
|
||||
import { useLoadFieldInfo, useLoadPathDescription } from "@web/core/model_field_selector/utils";
|
||||
import { toDomain, toTree } from "@web/core/domain_tree";
|
||||
import { _t } from "@web/core/l10n/translation";
|
||||
import { getOperatorInfo } from "@web/core/domain_selector/domain_selector_operators";
|
||||
import { useGetDomainTreeDescription } from "@web/core/domain_selector/utils";
|
||||
|
||||
const { DateTime } = luxon;
|
||||
|
||||
@@ -159,144 +151,6 @@ function execute(op, source, target) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Tree} tree
|
||||
* @returns {tree}
|
||||
*/
|
||||
function simplifyTree(tree, isRoot = true) {
|
||||
if (tree.type === "condition") {
|
||||
return tree;
|
||||
}
|
||||
const processedChildren = tree.children.map((c) => simplifyTree(c, false));
|
||||
if (tree.value === "&") {
|
||||
return { ...tree, children: processedChildren };
|
||||
}
|
||||
const children = [];
|
||||
const childrenByPath = {};
|
||||
for (const child of processedChildren) {
|
||||
if (
|
||||
child.type === "connector" ||
|
||||
typeof child.path !== "string" ||
|
||||
!["=", "in"].includes(child.operator)
|
||||
) {
|
||||
children.push(child);
|
||||
} else {
|
||||
if (!childrenByPath[child.path]) {
|
||||
childrenByPath[child.path] = [];
|
||||
}
|
||||
childrenByPath[child.path].push(child);
|
||||
}
|
||||
}
|
||||
for (const path in childrenByPath) {
|
||||
if (childrenByPath[path].length === 1) {
|
||||
children.push(childrenByPath[path][0]);
|
||||
continue;
|
||||
}
|
||||
const value = [];
|
||||
for (const child of childrenByPath[path]) {
|
||||
if (child.operator === "=") {
|
||||
value.push(child.value);
|
||||
} else {
|
||||
value.push(...child.value);
|
||||
}
|
||||
}
|
||||
children.push({
|
||||
type: "condition",
|
||||
negate: false,
|
||||
operator: "in",
|
||||
path: path,
|
||||
value: normalizeValue(value),
|
||||
});
|
||||
}
|
||||
if (children.length === 1 && !isRoot) {
|
||||
return { ...children[0] };
|
||||
}
|
||||
return { ...tree, children };
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Tree} tree
|
||||
* @param {Function} getDescription
|
||||
* @param {boolean} [isSubExpression=true]
|
||||
* @returns {string}
|
||||
*/
|
||||
function getDomainTreeDescription(
|
||||
tree,
|
||||
getFieldDef,
|
||||
getDescription,
|
||||
displayNames,
|
||||
isSubExpression = true
|
||||
) {
|
||||
if (tree.type === "connector") {
|
||||
// we assume that the domain tree is normalized (--> there is at least two children)
|
||||
const childDescriptions = tree.children.map((c) =>
|
||||
getDomainTreeDescription(c, getFieldDef, getDescription, displayNames)
|
||||
);
|
||||
const separator = tree.value === "&" ? _t("and") : _t("or");
|
||||
let description = childDescriptions.join(` ${separator} `);
|
||||
if (isSubExpression || tree.negate) {
|
||||
description = `( ${description} )`;
|
||||
}
|
||||
if (tree.negate) {
|
||||
description = `! ${description}`;
|
||||
}
|
||||
return description;
|
||||
}
|
||||
const { negate, operator, path, value } = tree;
|
||||
const fieldDef = getFieldDef(path);
|
||||
const operatorInfo = getOperatorInfo(operator, negate);
|
||||
const { operatorDescription, valueDescription } = leafToString(
|
||||
fieldDef,
|
||||
operatorInfo,
|
||||
value,
|
||||
displayNames[fieldDef?.relation]
|
||||
);
|
||||
let description = `${getDescription(path)} ${operatorDescription} `;
|
||||
if (valueDescription) {
|
||||
const { values, join, addParenthesis } = valueDescription;
|
||||
const jointedValues = values.join(` ${join} `);
|
||||
description += addParenthesis ? `( ${jointedValues} )` : jointedValues;
|
||||
}
|
||||
return description;
|
||||
}
|
||||
|
||||
function useGetDomainTreeDescription(fieldService, nameService) {
|
||||
const loadFieldInfo = useLoadFieldInfo(fieldService);
|
||||
const loadPathDescription = useLoadPathDescription(fieldService);
|
||||
const loadDisplayNames = useLoadDisplayNames(nameService);
|
||||
return async (resModel, tree) => {
|
||||
tree = simplifyTree(tree);
|
||||
const domain = toDomain(tree);
|
||||
const paths = extractPathsFromDomain(domain);
|
||||
const promises = [];
|
||||
const pathFieldDefs = {};
|
||||
const pathDescriptions = {};
|
||||
for (const path of paths) {
|
||||
promises.push(
|
||||
loadPathDescription(resModel, path).then(({ displayNames }) => {
|
||||
pathDescriptions[formatValue(path)] = displayNames.join(" 🠒 ");
|
||||
}),
|
||||
loadFieldInfo(resModel, path).then(({ fieldDef }) => {
|
||||
pathFieldDefs[formatValue(path)] = fieldDef;
|
||||
})
|
||||
);
|
||||
}
|
||||
await Promise.all(promises);
|
||||
const getFieldDef = (path) => pathFieldDefs[formatValue(path)];
|
||||
const getDescription = (path) => pathDescriptions[formatValue(path)];
|
||||
const idsByModel = extractIdsFromDomain(domain, getFieldDef);
|
||||
const treeWithVirtualOperators = createVirtualOperators(tree, getFieldDef);
|
||||
const displayNames = await loadDisplayNames(idsByModel);
|
||||
return getDomainTreeDescription(
|
||||
treeWithVirtualOperators,
|
||||
getFieldDef,
|
||||
getDescription,
|
||||
displayNames,
|
||||
false
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Global constants/variables
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
@@ -10,6 +10,8 @@ import { registry } from "@web/core/registry";
|
||||
import { SelectCreateDialog } from "@web/views/view_dialogs/select_create_dialog";
|
||||
import { standardFieldProps } from "../standard_field_props";
|
||||
import { useBus, useService, useOwnedDialogs } from "@web/core/utils/hooks";
|
||||
import { toTree } from "@web/core/domain_tree";
|
||||
import { useGetDomainTreeDescription } from "@web/core/domain_selector/utils";
|
||||
|
||||
export class DomainField extends Component {
|
||||
static template = "web.DomainField";
|
||||
@@ -21,30 +23,41 @@ export class DomainField extends Component {
|
||||
context: { type: Object, optional: true },
|
||||
editInDialog: { type: Boolean, optional: true },
|
||||
resModel: { type: String, optional: true },
|
||||
isFoldable: { type: Boolean, optional: true },
|
||||
};
|
||||
static defaultProps = {
|
||||
editInDialog: false,
|
||||
isFoldable: false,
|
||||
};
|
||||
|
||||
setup() {
|
||||
this.rpc = useService("rpc");
|
||||
this.orm = useService("orm");
|
||||
this.getDomainTreeDescription = useGetDomainTreeDescription();
|
||||
this.addDialog = useOwnedDialogs();
|
||||
|
||||
this.state = useState({
|
||||
isValid: null,
|
||||
recordCount: null,
|
||||
folded: this.props.isFoldable,
|
||||
facets: [],
|
||||
});
|
||||
|
||||
this.isDebugEdited = false;
|
||||
onWillStart(() => {
|
||||
this.checkProps(); // not awaited
|
||||
if (this.props.isFoldable) {
|
||||
this.loadFacets();
|
||||
}
|
||||
});
|
||||
onWillUpdateProps((nextProps) => {
|
||||
this.isDebugEdited = this.isDebugEdited && this.props.readonly === nextProps.readonly;
|
||||
if (!this.isDebugEdited) {
|
||||
this.checkProps(nextProps); // not awaited
|
||||
}
|
||||
if (nextProps.isFoldable) {
|
||||
this.loadFacets(nextProps);
|
||||
}
|
||||
});
|
||||
|
||||
useBus(this.props.record.model.bus, "NEED_LOCAL_CHANGES", async (ev) => {
|
||||
@@ -98,6 +111,40 @@ export class DomainField extends Component {
|
||||
return resModel;
|
||||
}
|
||||
|
||||
async loadFacets(props = this.props) {
|
||||
const resModel = this.getResModel(props);
|
||||
|
||||
if (!resModel) {
|
||||
this.state.facets = [];
|
||||
this.state.folded = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof resModel !== "string") {
|
||||
// we don't want to support invalid models
|
||||
throw new Error(`Invalid model: ${resModel}`);
|
||||
}
|
||||
|
||||
let promises;
|
||||
const domain = this.getDomain(props);
|
||||
try {
|
||||
const tree = toTree(domain, { distributeNot: !this.env.debug });
|
||||
const trees = !tree.negate && tree.value === "&" ? tree.children : [tree];
|
||||
promises = trees.map(async (tree) => {
|
||||
const description = await this.getDomainTreeDescription(resModel, tree);
|
||||
return description;
|
||||
});
|
||||
} catch (error) {
|
||||
if (error.data?.name === "builtins.KeyError" && error.data.message === resModel) {
|
||||
// we don't want to support invalid models
|
||||
throw new Error(`Invalid model: ${resModel}`);
|
||||
}
|
||||
this.state.facets = [];
|
||||
this.state.folded = false;
|
||||
}
|
||||
this.state.facets = await Promise.all(promises);
|
||||
}
|
||||
|
||||
async checkProps(props = this.props) {
|
||||
const resModel = this.getResModel(props);
|
||||
if (!resModel) {
|
||||
@@ -179,6 +226,10 @@ export class DomainField extends Component {
|
||||
return this.props.record.update({ [this.props.name]: domain });
|
||||
}
|
||||
|
||||
fold() {
|
||||
this.state.folded = true;
|
||||
}
|
||||
|
||||
updateState(params = {}) {
|
||||
Object.assign(this.state, {
|
||||
isValid: "isValid" in params ? params.isValid : null,
|
||||
@@ -207,6 +258,7 @@ export const domainField = {
|
||||
extractProps({ options }, dynamicInfo) {
|
||||
return {
|
||||
editInDialog: options.in_dialog,
|
||||
isFoldable: options.foldable,
|
||||
resModel: options.model,
|
||||
context: dynamicInfo.context,
|
||||
};
|
||||
|
||||
@@ -5,45 +5,80 @@
|
||||
<div t-att-class="{ o_inline_mode: !props.editInDialog }">
|
||||
<t t-set="resModel" t-value="getResModel()"/>
|
||||
<t t-if="resModel">
|
||||
<DomainSelector
|
||||
resModel="resModel"
|
||||
domain="getDomain()"
|
||||
readonly="props.readonly or props.editInDialog"
|
||||
update.bind="update"
|
||||
isDebugMode="!!env.debug"
|
||||
className="props.readonly ? 'o_read_mode' : 'o_edit_mode'"
|
||||
/>
|
||||
<div class="o_field_domain_panel d-flex align-items-center gap-2 mt-1">
|
||||
<i class="oi oi-arrow-right" role="img" aria-label="Domain" title="Domain" />
|
||||
<t t-if="state.isValid === null and state.recordCount === null">
|
||||
<i class="fa fa-circle-o-notch fa-spin ms-2" role="img" aria-label="Loading" title="Loading" />
|
||||
</t>
|
||||
<t t-else="">
|
||||
<t t-if="state.isValid">
|
||||
<button class="btn btn-sm btn-link o_domain_show_selection_button" type="button" t-on-click.stop="onButtonClick">
|
||||
<t t-esc="state.recordCount" /> record(s)
|
||||
</button>
|
||||
<t t-if="props.isFoldable and state.folded">
|
||||
<div class="d-flex align-items-center" t-on-click="() => state.folded = false">
|
||||
<i data-tooltip="Modify filter" class="fa fa-lg fa-caret-right pe-2"/>
|
||||
<div class="d-print-contents">
|
||||
<t t-if="!state.facets.length">
|
||||
<span>Match <strong>all records</strong></span>
|
||||
</t>
|
||||
<t t-foreach="state.facets" t-as="facet" t-key="facet_index">
|
||||
<div class="d-inline-flex align-items-stretch text-nowrap px-1 position-relative"
|
||||
role="listitem"
|
||||
tabindex="0"
|
||||
>
|
||||
<div class="o_searchview_facet_label rounded-start-2 btn btn-primary rounded-end-0 p-0" role="button">
|
||||
<i class="px-1 small fa fa-filter" role="image"/>
|
||||
</div>
|
||||
<div class="o_facet_values d-flex align-items-center px-2 bg-200 rounded-end-2">
|
||||
<small class="o_facet_value" t-esc="facet"/>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
<t t-if="state.isValid">
|
||||
<button class="btn btn-sm btn-link o_domain_show_selection_button" data-tooltip="Show matching records" type="button" t-on-click.stop="onButtonClick">
|
||||
<t t-esc="state.recordCount" /> record(s)
|
||||
</button>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<div class="d-flex">
|
||||
<a t-if="props.isFoldable" t-on-click="fold">
|
||||
<i class="fa fa-lg fa-caret-down pe-2"></i>
|
||||
</a>
|
||||
<DomainSelector
|
||||
resModel="resModel"
|
||||
domain="getDomain()"
|
||||
readonly="props.readonly or props.editInDialog"
|
||||
update.bind="update"
|
||||
isDebugMode="!!env.debug"
|
||||
className="props.readonly ? 'o_read_mode' : 'o_edit_mode'"
|
||||
/>
|
||||
</div>
|
||||
<div class="o_field_domain_panel d-flex align-items-center gap-2 mt-1">
|
||||
<i class="oi oi-arrow-right" role="img" aria-label="Domain" title="Domain" />
|
||||
<t t-if="state.isValid === null and state.recordCount === null">
|
||||
<i class="fa fa-circle-o-notch fa-spin ms-2" role="img" aria-label="Loading" title="Loading" />
|
||||
</t>
|
||||
<t t-else="">
|
||||
<span class="text-warning" role="alert">
|
||||
<i class="fa fa-exclamation-triangle" role="img" aria-label="Warning" title="Warning" /> Invalid domain
|
||||
</span>
|
||||
</t>
|
||||
<t t-if="!!env.debug and !props.readonly">
|
||||
<button
|
||||
class="btn btn-sm btn-icon fa fa-refresh o_refresh_count"
|
||||
role="img"
|
||||
aria-label="Refresh"
|
||||
title="Refresh"
|
||||
t-on-click="() => this.checkProps()"
|
||||
/>
|
||||
</t>
|
||||
<t t-if="state.isValid">
|
||||
<button class="btn btn-sm btn-link o_domain_show_selection_button" type="button" t-on-click.stop="onButtonClick">
|
||||
<t t-esc="state.recordCount" /> record(s)
|
||||
</button>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<span class="text-warning" role="alert">
|
||||
<i class="fa fa-exclamation-triangle" role="img" aria-label="Warning" title="Warning" /> Invalid domain
|
||||
</span>
|
||||
</t>
|
||||
<t t-if="!!env.debug and !props.readonly">
|
||||
<button
|
||||
class="btn btn-sm btn-icon fa fa-refresh o_refresh_count"
|
||||
role="img"
|
||||
aria-label="Refresh"
|
||||
title="Refresh"
|
||||
t-on-click="() => this.checkProps()"
|
||||
/>
|
||||
</t>
|
||||
|
||||
<t t-if="props.editInDialog and !props.readonly">
|
||||
<button class="btn btn-sm btn-primary o_field_domain_dialog_button" t-on-click.prevent="onEditDialogBtnClick">Edit Domain</button>
|
||||
<t t-if="props.editInDialog and !props.readonly">
|
||||
<button class="btn btn-sm btn-primary o_field_domain_dialog_button" t-on-click.prevent="onEditDialogBtnClick">Edit Domain</button>
|
||||
</t>
|
||||
</t>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<div>Select a model to add a filter.</div>
|
||||
|
||||
@@ -994,4 +994,75 @@ QUnit.module("Fields", (hooks) => {
|
||||
);
|
||||
}
|
||||
);
|
||||
QUnit.test("domain field can be foldable", async function (assert) {
|
||||
serverData.models.partner.records[0].foo = "[]";
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="foo" widget="domain" options="{'model': 'partner_type', 'foldable': true}" />
|
||||
</group>
|
||||
</sheet>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
// As the domain is empty, the "Match all records" span should be visible
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_domain span").textContent,
|
||||
"Match all records"
|
||||
);
|
||||
|
||||
// Unfold the domain
|
||||
await click(target, ".o_field_domain > div > div");
|
||||
|
||||
// As the domain is empty, there should be a button to add the first
|
||||
// domain part
|
||||
assert.containsOnce(target, ".o_domain_add_first_node_button");
|
||||
|
||||
// Clicking on the button should add the [["id", "=", "1"]] domain, so
|
||||
// there should be a field selector in the DOM
|
||||
await click(target, ".o_domain_add_first_node_button");
|
||||
assert.containsOnce(target, ".o_model_field_selector");
|
||||
|
||||
// Focusing the field selector input should open the field selector
|
||||
// popover
|
||||
await click(target, ".o_model_field_selector");
|
||||
assert.containsOnce(document.body, ".o_model_field_selector_popover");
|
||||
assert.containsOnce(document.body, ".o_model_field_selector_popover_search input");
|
||||
|
||||
// The popover should contain the list of partner_type fields and so
|
||||
// there should be the "Color index" field
|
||||
assert.strictEqual(
|
||||
document.body.querySelector(".o_model_field_selector_popover_item_name").textContent,
|
||||
"Color index"
|
||||
);
|
||||
|
||||
// Clicking on this field should close the popover, then changing the
|
||||
// associated value should reveal one matched record
|
||||
await click(document.body.querySelector(".o_model_field_selector_popover_item_name"));
|
||||
|
||||
await editInput(target, ".o_domain_leaf_value_input", 2);
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_domain_show_selection_button").textContent.trim().substr(0, 2),
|
||||
"1 ",
|
||||
"changing color value to 2 should reveal only one record"
|
||||
);
|
||||
|
||||
// Saving the form view should show a readonly domain containing the
|
||||
// "color" field
|
||||
await clickSave(target);
|
||||
assert.ok(target.querySelector(".o_field_domain").textContent.includes("Color index"));
|
||||
|
||||
// Fold domain selector
|
||||
await click(target, ".o_field_domain a i");
|
||||
|
||||
assert.containsOnce(target, ".o_field_domain .o_facet_values:contains('Color index = 2')");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user