OWL
overlay
Model Field Selector Popover
Odoo 19 OWL component — Model Field Selector Popover (core)
Live preview
Interactive
Source excerpt
web/static/src/core/model_field_selector/model_field_selector_popover.js
import { Component, onWillStart, useEffect, useRef, useState } from "@odoo/owl";
import { _t } from "@web/core/l10n/translation";
import { sortBy } from "@web/core/utils/arrays";
import { KeepLast } from "@web/core/utils/concurrency";
import { useService } from "@web/core/utils/hooks";
import { fuzzyLookup } from "@web/core/utils/search";
import { debounce } from "@web/core/utils/timing";
class Page {
constructor(resModel, fieldDefs, options = {}) {
this.resModel = resModel;
this.fieldDefs = fieldDefs;
const {
previousPage = null,
selectedName = null,
isDebugMode,
readProperty = false,
sortFn = (fieldDefs) => sortBy(Object.keys(fieldDefs), (key) => fieldDefs[key].string),
} = options;
this.previousPage = previousPage;
this.selectedName = selectedName;
this.isDebugMode = isDebugMode;
this.readProperty = readProperty;
this.sortedFieldNames = sortFn(fieldDefs);
this.fieldNames = this.sortedFieldNames;
this.query = "";
this.focusedFieldName = null;
this.resetFocusedFieldName();
}
get path() {
const previousPath = this.previousPage?.path || "";
const name = this.selectedName;
if (this.readProperty && this.selectedField && this.selectedField.is_property) {
if (this.selectedField.relation) {
return `${previousPath}.get('${name}', env['${this.selectedField.relation}'])`;
}
return `${previousPath}.get('${name}')`;
}
if (name) {
if (previousPath) {
return `${previousPath}.${name}`;
}
return name;
}
return previousPath;
}
get selectedField() {
return this.fieldDefs[this.selectedName];
}
get title() {
const prefix = this.previousPage?.previousPage ? "... > " : "";
const title = this.previousPage?.selectedField?.string || "";
if (prefix.length || title.length) {
return `${prefix}${title}`;
}
return _t("Select a field");
}
focus(direction) {
if (!this.fieldNames.length) {
return;
}
const index = this.fieldNames.indexOf(this.focusedFieldName);
if (direction === "previous") {
if (index === 0) {
this.focusedFieldName = this.fieldNames[this.fieldNames.length - 1];
} else {
this.focusedFieldName = this.fieldNames[index - 1];
}
} else {
if (index === this.fieldNames.length - 1) {
this.focusedFieldName = this.fieldNames[0];
} else {
this.focusedFieldName = this.fieldNames[index + 1];
}
}
}
resetFocusedFieldName() {
if (this.selectedName && this.fieldNames.includes(this.selectedName)) {
this.focusedFieldName = this.selectedName;
} else {
this.focusedFieldName = this.fieldNames.length ? this.fieldNames[0] : null;
}
}
searchFields(query = "") {
this.query = query;
this.fieldNames = this.sortedFieldNames;
if (query) {
this.fieldNames = fuzzyLookup(query, this.fieldNames, (key) => {
const vals = [this.fieldDefs[key].string];
if (this.isDebugMode) {
vals.push(key);
}
return vals;
});
}
this.resetFocusedFieldName();
}
}
export class ModelFieldSelectorPopover extends Component {
static template = "web.ModelFieldSelectorPopover";
static props = {
close: Function,
filter: { type: Function, optional: true },
sort: { type: Function, optional: true },
followRelations: { type: Boolean, optional: true },
showDebugInput: { type: Boolean, optional: true },
isDebugMode: { type: Boolean, optional: true },
path: { optional: true },
readProperty: { type: Boolean, optional: true },
resModel: String,
showSearchInput: { type: Boolean, optional: true },
update: Function,
Registry / API
- Registry name
ModelFieldSelectorPopover- Category
—- Module
web- Slug
model-field-selector-popover- Nav group
overlay