Includes: - Odoo 19 framework (odoo/) - 27 custom EnCoach addons (new_project/custom_addons/) - encoach_core, encoach_api, encoach_lms_api, encoach_adaptive_api - encoach_exam, encoach_taxonomy, encoach_adaptive, encoach_assignment - encoach_ai, encoach_ai_grading, encoach_ai_generation, encoach_ai_media - encoach_courseware, encoach_communication, encoach_subscription - encoach_notification, encoach_approval, encoach_branding - encoach_classroom, encoach_registration, encoach_stats - encoach_faq, encoach_ticket, encoach_training, encoach_resources - encoach_adaptive_ai, encoach_sis - 21 OpenEduCat Enterprise modules (new_project/enterprise-19/) - 14 OpenEduCat Community modules (new_project/openeducat_erp-19.0/) - Configuration: odoo.conf, requirements.txt, scripts - 200+ REST API endpoints with JWT authentication - SRS and test documentation Made-with: Cursor
251 lines
8.3 KiB
JavaScript
251 lines
8.3 KiB
JavaScript
import { App, EventBus } from "@odoo/owl";
|
|
import { SERVICES_METADATA } from "@web/core/utils/hooks";
|
|
import { registry } from "@web/core/registry";
|
|
import { getTemplate } from "@web/core/templates";
|
|
import { appTranslateFn } from "@web/core/l10n/translation";
|
|
import { session } from "@web/session";
|
|
import { isMacOS } from "@web/core/browser/feature_detection";
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// Types
|
|
// -----------------------------------------------------------------------------
|
|
|
|
/**
|
|
* @typedef {{
|
|
* bus: EventBus;
|
|
* debug: string;
|
|
* services: import("services").Services;
|
|
* readonly isSmall: boolean;
|
|
* }} OdooEnv
|
|
*/
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// makeEnv
|
|
// -----------------------------------------------------------------------------
|
|
|
|
/**
|
|
* Return a value Odoo Env object
|
|
*
|
|
* @returns {OdooEnv}
|
|
*/
|
|
export function makeEnv() {
|
|
const bus = new EventBus();
|
|
const prom = new Promise((resolve) => {
|
|
bus.addEventListener("SERVICES-LOADED", resolve, { once: true });
|
|
});
|
|
return {
|
|
bus,
|
|
isReady: prom,
|
|
services: {},
|
|
debug: odoo.debug,
|
|
get isSmall() {
|
|
throw new Error("UI service not initialized!");
|
|
},
|
|
};
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// Service Launcher
|
|
// -----------------------------------------------------------------------------
|
|
|
|
const serviceRegistry = registry.category("services");
|
|
|
|
serviceRegistry.addValidation({
|
|
start: Function,
|
|
dependencies: { type: Array, element: String, optional: true },
|
|
async: { type: [{ type: Array, element: String }, { value: true }], optional: true },
|
|
"*": true,
|
|
});
|
|
|
|
let startServicesPromise = null;
|
|
|
|
/**
|
|
* Start all services registered in the service registry, while making sure
|
|
* each service dependencies are properly fulfilled.
|
|
*
|
|
* @param {OdooEnv} env
|
|
* @returns {Promise<void>}
|
|
*/
|
|
export async function startServices(env) {
|
|
// Wait for all synchronous code so that if new services that depend on
|
|
// one another are added to the registry, they're all present before we
|
|
// start them regardless of the order they're added to the registry.
|
|
await Promise.resolve();
|
|
|
|
const toStart = new Map();
|
|
serviceRegistry.addEventListener("UPDATE", async (ev) => {
|
|
// Wait for all synchronous code so that if new services that depend on
|
|
// one another are added to the registry, they're all present before we
|
|
// start them regardless of the order they're added to the registry.
|
|
await Promise.resolve();
|
|
const { operation, key: name, value: service } = ev.detail;
|
|
if (operation === "delete") {
|
|
// We hardly see why it would be usefull to remove a service.
|
|
// Furthermore we could encounter problems with dependencies.
|
|
// Keep it simple!
|
|
return;
|
|
}
|
|
if (toStart.size) {
|
|
const namedService = Object.assign(Object.create(service), { name });
|
|
toStart.set(name, namedService);
|
|
} else {
|
|
await _startServices(env, toStart);
|
|
}
|
|
});
|
|
await _startServices(env, toStart);
|
|
}
|
|
|
|
async function _startServices(env, toStart) {
|
|
if (startServicesPromise) {
|
|
return startServicesPromise.then(() => _startServices(env, toStart));
|
|
}
|
|
const services = env.services;
|
|
for (const [name, service] of serviceRegistry.getEntries()) {
|
|
if (!(name in services)) {
|
|
const namedService = Object.assign(Object.create(service), { name });
|
|
toStart.set(name, namedService);
|
|
}
|
|
}
|
|
|
|
// start as many services in parallel as possible
|
|
async function start() {
|
|
let service = null;
|
|
const proms = [];
|
|
while ((service = findNext())) {
|
|
const name = service.name;
|
|
toStart.delete(name);
|
|
const entries = (service.dependencies || []).map((dep) => [dep, services[dep]]);
|
|
const dependencies = Object.fromEntries(entries);
|
|
if (name in services) {
|
|
continue;
|
|
}
|
|
const value = service.start(env, dependencies);
|
|
if ("async" in service) {
|
|
SERVICES_METADATA[name] = service.async;
|
|
}
|
|
proms.push(
|
|
Promise.resolve(value).then((val) => {
|
|
services[name] = val || null;
|
|
})
|
|
);
|
|
}
|
|
await Promise.all(proms);
|
|
if (proms.length) {
|
|
return start();
|
|
}
|
|
}
|
|
startServicesPromise = start().finally(() => {
|
|
startServicesPromise = null;
|
|
});
|
|
await startServicesPromise;
|
|
env.bus.trigger("SERVICES-LOADED");
|
|
if (toStart.size) {
|
|
const missingDeps = new Set();
|
|
for (const service of toStart.values()) {
|
|
for (const dependency of service.dependencies) {
|
|
if (!(dependency in services) && !toStart.has(dependency)) {
|
|
missingDeps.add(dependency);
|
|
}
|
|
}
|
|
}
|
|
const depNames = [...missingDeps].join(", ");
|
|
throw new Error(
|
|
`Some services could not be started: ${[
|
|
...toStart.keys(),
|
|
]}. Missing dependencies: ${depNames}`
|
|
);
|
|
}
|
|
|
|
function findNext() {
|
|
for (const s of toStart.values()) {
|
|
if (s.dependencies) {
|
|
if (s.dependencies.every((d) => d in services)) {
|
|
return s;
|
|
}
|
|
} else {
|
|
return s;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export const customDirectives = {
|
|
// t-custom-click="handler"
|
|
// This custom directive will add two even listeners ("click"; "auxclick") and call the global value "click".
|
|
// The global value "click" will call the handler with two parameters :
|
|
// - ev (the original event)
|
|
// - isMiddleClick (a boolean that says if the user middle clicked, or if he did a ctrl+click)
|
|
//
|
|
click: (node, value, modifiers) => {
|
|
let mods = "";
|
|
if (modifiers.includes("synthetic")) {
|
|
mods += ".synthetic";
|
|
}
|
|
if (modifiers.includes("capture")) {
|
|
mods += ".capture";
|
|
}
|
|
const handlerFunction = `(ev) => __globals__.click(ev, (${value}).bind(this), '${JSON.stringify(
|
|
modifiers
|
|
)}')`;
|
|
node.setAttribute(`t-on-click${mods}`, handlerFunction);
|
|
node.setAttribute(`t-on-auxclick${mods}`, handlerFunction);
|
|
},
|
|
};
|
|
|
|
export const globalValues = {
|
|
click: (ev, value, modifiers) => {
|
|
if (ev.button === 0 || ev.button === 1) {
|
|
modifiers = JSON.parse(modifiers);
|
|
for (const modifier of modifiers) {
|
|
if (modifier === "stop") {
|
|
ev.stopPropagation();
|
|
}
|
|
if (modifier === "prevent") {
|
|
ev.preventDefault();
|
|
}
|
|
}
|
|
const ctrlKey = isMacOS() ? ev.metaKey : ev.ctrlKey;
|
|
const isMiddleClick = (ctrlKey && ev.button === 0) || ev.button === 1;
|
|
value(ev, isMiddleClick);
|
|
}
|
|
},
|
|
};
|
|
|
|
/**
|
|
* Create an application with a given component as root and mount it. If no env
|
|
* is provided, the application will be treated as a "root": an env will be
|
|
* created and the services will be started, it will also be set as the root
|
|
* in `__WOWL_DEBUG__`
|
|
*
|
|
* @param {import("@odoo/owl").Component} component the component to mount
|
|
* @param {HTMLElement} target the HTML element in which to mount the app
|
|
* @param {Partial<ConstructorParameters<typeof App>[1]>} [appConfig] object
|
|
* containing a (partial) config for the app.
|
|
*/
|
|
export async function mountComponent(component, target, appConfig = {}) {
|
|
let { env } = appConfig;
|
|
const isRoot = !env;
|
|
if (isRoot) {
|
|
env = await makeEnv();
|
|
await startServices(env);
|
|
}
|
|
const app = new App(component, {
|
|
env,
|
|
getTemplate,
|
|
dev: env.debug || session.test_mode,
|
|
warnIfNoStaticProps: !session.test_mode,
|
|
name: component.constructor.name,
|
|
translatableAttributes: ["data-tooltip"],
|
|
translateFn: appTranslateFn,
|
|
customDirectives,
|
|
globalValues,
|
|
...appConfig,
|
|
});
|
|
const root = await app.mount(target);
|
|
if (isRoot) {
|
|
odoo.__WOWL_DEBUG__ = { root };
|
|
}
|
|
return app;
|
|
}
|