oca-ocb-core/odoo-bringout-oca-ocb-web/web/static/src/views/model.js
2025-08-29 15:20:45 +02:00

193 lines
5.8 KiB
JavaScript

/** @odoo-module **/
import { useBus, useService } from "@web/core/utils/hooks";
import { SEARCH_KEYS } from "@web/search/with_search/with_search";
import { buildSampleORM } from "@web/views/sample_server";
import { useSetupView } from "@web/views/view_hook";
import { EventBus, onMounted, onWillStart, onWillUpdateProps, status, useComponent } from "@odoo/owl";
/**
* @typedef {import("@web/search/search_model").SearchParams} SearchParams
*/
export class Model extends EventBus {
/**
* @param {Object} env
* @param {Object} services
*/
constructor(env, params, services) {
super();
this.env = env;
this.orm = services.orm;
this.useSampleModel = false; // will be set to true by the "useModel" hook if necessary
this.setup(params, services);
}
/**
* @param {Object} params
* @param {Object} services
*/
setup(/* params, services */) {}
/**
* @param {SearchParams} searchParams
*/
async load(/* searchParams */) {}
/**
* This function is meant to be overriden by models that want to implement
* the sample data feature. It should return true iff the last loaded state
* actually contains data. If not, another load will be done (if the sample
* feature is enabled) with the orm service substituted by another using the
* SampleServer, to have sample data to display instead of an empty screen.
*
* @returns {boolean}
*/
hasData() {
return true;
}
/**
* This function is meant to be overriden by models that want to combine
* sample data with real groups that exist on the server.
*
* @returns {boolean}
*/
getGroups() {
return null;
}
notify() {
this.trigger("update");
}
}
Model.services = [];
/**
* @param {Object} props
* @returns {SearchParams}
*/
function getSearchParams(props) {
const params = {};
for (const key of SEARCH_KEYS) {
params[key] = props[key];
}
return params;
}
/**
* @template {Model} T
* @param {new (env: Object, params: Object, services: Object) => T} ModelClass
* @param {Object} params
* @param {Object} [options]
* @param {Function} [options.onUpdate]
* @returns {T}
*/
export function useModel(ModelClass, params, options = {}) {
const component = useComponent();
if (!(ModelClass.prototype instanceof Model)) {
throw new Error(`the model class should extend Model`);
}
const services = {};
for (const key of ModelClass.services) {
services[key] = useService(key);
}
services.orm = services.orm || useService("orm");
if (services.dialog) {
services.dialog = Object.create(services.dialog);
const dialogAddOrigin = services.dialog.add;
let dialogRequests = [];
services.dialog.add = (...args) => {
const index = dialogRequests.push(args);
return () => {
dialogRequests[index] = null;
}
}
onMounted(() => {
services.dialog.add = dialogAddOrigin;
for (const req of dialogRequests) {
if (req) {
dialogAddOrigin(...req);
}
}
dialogRequests = null;
});
}
if (!("isAlive" in params)) {
params.isAlive = () => status(component) !== "destroyed";
}
const model = new ModelClass(component.env, params, services);
useBus(
model,
"update",
options.onUpdate ||
(() => {
component.render(true); // FIXME WOWL reactivity
})
);
const globalState = component.props.globalState || {};
let useSampleModel =
component.props.useSampleModel &&
(!("useSampleModel" in globalState) || globalState.useSampleModel);
model.useSampleModel = !options.ignoreUseSampleModel ? useSampleModel : false;
const orm = model.orm;
let sampleORM = globalState.sampleORM;
const user = useService("user");
let started = false;
async function load(props) {
const searchParams = getSearchParams(props);
await model.load(searchParams);
if (!options.ignoreUseSampleModel) {
if (useSampleModel && !model.hasData()) {
sampleORM =
sampleORM ||
buildSampleORM(component.props.resModel, component.props.fields, user);
sampleORM.setGroups(model.getGroups());
// Load data with sampleORM then restore real ORM.
model.orm = sampleORM;
await model.load(searchParams);
model.orm = orm;
} else {
useSampleModel = false;
model.useSampleModel = useSampleModel;
}
}
if (started) {
model.notify();
}
}
onWillStart(async () => {
// FIXME: we have a problem here: in the view, we have two onWillStart:
// - 1) to load the subviews that aren't inline
// - 2) to load the data
// 2) must be done after 1), but we can't sync two onWillStarts
// The problem is also there with the relational model, but it isn't visible
// in the tests because the load the sub views in a tick, and we look inside
// the fieldsInfo after a tick as well. Here, we look into fieldsInfo directly.
if (params.beforeLoadProm) {
await params.beforeLoadProm;
}
await load(component.props);
started = true;
});
onWillUpdateProps((nextProps) => {
if (!options.ignoreUseSampleModel) {
useSampleModel = false;
}
load(nextProps);
});
useSetupView({
getGlobalState() {
if (component.props.useSampleModel) {
return { sampleORM, useSampleModel };
}
},
});
return model;
}