[REF] mail: make store service a model

This simplifies managing relations on the store, and gives
incentive to define them there instead of another prop that
is a record, e.g. `discuss`.

Part-of: odoo/odoo#136539
This commit is contained in:
Alexandre Kühn
2023-10-02 11:42:55 +00:00
parent ab93449323
commit 485bcb82bf
@@ -8,7 +8,12 @@ import { registry } from "@web/core/registry";
import { debounce } from "@web/core/utils/timing";
import { modelRegistry, Record, RecordInverses, RecordList } from "./record";
export class Store {
export class Store extends Record {
/** @returns {import("models").Store} */
static insert() {
return this.get() ?? this.new();
}
/** @type {typeof import("@mail/core/web/activity_model").Activity} */
Activity;
/** @type {typeof import("@mail/core/common/attachment_model").Attachment} */
@@ -44,6 +49,56 @@ export class Store {
/** @type {typeof import("@mail/core/common/thread_model").Thread} */
Thread;
lastChannelSubscription = "";
/**
* This is the current logged partner
*
* @type {import("models").Persona}
*/
user = null;
/**
* This is the current logged guest
*
* @type {import("models").Persona}
*/
guest = null;
/**
* The last id of bus notification at the time for fetch init_messaging.
* When receiving a notification:
* - if id greater than this value: the notification is newer than init_messaging state.
* - if same id or lower: the notification is older than init_messaging state.
* This is useful to determine whether we should increment or decrement a counter based
* on init_messaging state.
*/
initBusId = 0;
/**
* Indicates whether the current user is using the application through the
* public page.
*/
inPublicPage = false;
companyName = "";
/** @type {import("models").Persona} */
odoobot = null;
odoobotOnboarding;
users = {};
internalUserGroupId = null;
registeredImStatusPartners = null;
hasLinkPreviewFeature = true;
// messaging menu
menu = { counter: 0 };
discuss = Record.one("DiscussApp");
activityCounter = 0;
isMessagingReady = false;
get self() {
return this.guest ?? this.user;
}
setup() {
super.setup();
this.updateBusSubscription = debounce(this.updateBusSubscription, 0); // Wait for thread fully inserted.
}
/**
* @param {string} localId
* @returns {Record}
@@ -56,22 +111,6 @@ export class Store {
return this[modelName].records[localId];
}
/**
* @param {import("@web/env").OdooEnv} env
*/
constructor(env) {
this.setup(env);
this.lastChannelSubscription = "";
this.updateBusSubscription = debounce(this.updateBusSubscription, 0); // Wait for thread fully inserted.
}
/**
* @param {import("@web/env").OdooEnv} env
*/
setup(env) {
this.env = env;
}
updateBusSubscription() {
const channelIds = [];
const ids = Object.keys(this.Thread.records).sort(); // Ensure channels processed in same order.
@@ -87,64 +126,8 @@ export class Store {
}
this.lastChannelSubscription = channels;
}
get self() {
return this.guest ?? this.user;
}
// base data
/**
* This is the current logged partner
*
* @type {import("models").Persona}
*/
user = null;
/**
* This is the current logged guest
*
* @type {import("models").Persona}
*/
guest = null;
/**
* The last id of bus notification at the time for fetch init_messaging.
* When receiving a notification:
* - if id greater than this value: the notification is newer than init_messaging state.
* - if same id or lower: the notification is older than init_messaging state.
* This is useful to determine whether we should increment or decrement a counter based
* on init_messaging state.
*/
initBusId = 0;
/**
* Indicates whether the current user is using the application through the
* public page.
*/
inPublicPage = false;
companyName = "";
/** @type {import("models").Persona} */
odoobot = null;
odoobotOnboarding;
users = {};
internalUserGroupId = null;
registeredImStatusPartners = null;
hasLinkPreviewFeature = true;
// messaging menu
menu = {
counter: 0,
};
discuss = Record.one("DiscussApp");
activityCounter = 0;
isMessagingReady = false;
}
Store.register();
export const storeService = {
dependencies: ["bus_service", "ui"],
@@ -153,11 +136,19 @@ export const storeService = {
* @param {Partial<import("services").Services>} services
*/
start(env, services) {
const res = reactive(new Store(env, services));
const res = {
// fake store for now, until it becomes a model
/** @type {Store} */
store: {
env,
get: (...args) => Store.prototype.get.call(this, ...args),
},
};
const Models = {};
for (const [name, _Model] of modelRegistry.getEntries()) {
/** @type {typeof Record} */
const Model = _Model;
if (res[name]) {
if (res.store[name]) {
throw new Error(
`There must be no duplicated Model Names (duplicate found: ${name})`
);
@@ -165,7 +156,7 @@ export const storeService = {
// classes cannot be made reactive because they are functions and they are not supported.
// work-around: make an object whose prototype is the class, so that static props become
// instance props.
const entry = Object.assign(Object.create(Model), { env, store: res });
const entry = Object.assign(Object.create(Model), { env, store: res.store });
// Produce another class with changed prototype, so that there are automatic get/set on relational fields
let detecting = true;
const Class = {
@@ -187,7 +178,7 @@ export const storeService = {
}
if (this[name] === Record.many()) {
newVal = new RecordList();
newVal.__store__ = res;
newVal.__store__ = res.store;
newVal.name = name;
newVal.owner = this;
}
@@ -203,7 +194,7 @@ export const storeService = {
if (l1 instanceof RecordList) {
return l1;
}
return res.get(l1);
return res.store.get(l1);
}
return Reflect.get(target, name, receiver);
},
@@ -211,7 +202,7 @@ export const storeService = {
if (name !== "__rels__" && target.__rels__.has(name)) {
const r1 = target;
const l1 = r1.__rels__.get(name);
const r2 = res.get(l1);
const r2 = res.store.get(l1);
if (r2) {
r2.__invs__.delete(r1.localId, name);
}
@@ -242,7 +233,7 @@ export const storeService = {
} else {
const r1 = receiver;
const l1 = r1.__rels__.get(name);
const r2 = res.get(l1);
const r2 = res.store.get(l1);
/** @type {Record} */
const r3 = val;
if (r2 && r2.notEq(r3)) {
@@ -269,7 +260,8 @@ export const storeService = {
}[Model.name];
entry.Class = Class;
entry.records = JSON.parse(JSON.stringify(Model.records));
res[name] = entry;
Models[name] = entry;
res.store[name] = entry;
// Detect relational fields with a dummy record and setup getter/setters on them
const obj = new Model();
detecting = false;
@@ -280,17 +272,25 @@ export const storeService = {
Class.__rels__.add(name);
}
}
res.discuss = res.DiscussApp.insert();
res.discuss.activeTab = env.services.ui.isSmall ? "mailbox" : "all";
onChange(res.Thread, "records", () => res.updateBusSubscription());
// Make true store (as a model)
res.store = reactive(res.store.Store.insert());
res.store.env = env;
for (const Model of Object.values(Models)) {
Model.store = res.store;
res.store[Model.name] = Model;
}
const store = res.store;
store.discuss = store.DiscussApp.insert();
store.discuss.activeTab = env.services.ui.isSmall ? "mailbox" : "all";
onChange(store.Thread, "records", () => store.updateBusSubscription());
services.ui.bus.addEventListener("resize", () => {
if (!services.ui.isSmall) {
res.discuss.activeTab = "all";
store.discuss.activeTab = "all";
} else {
res.discuss.activeTab = res.discuss.thread?.type ?? "all";
store.discuss.activeTab = store.discuss.thread?.type ?? "all";
}
});
return res;
return store;
},
};