Skip to Content
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