// // Copyright © 2023 Hardcore Engineering Inc. // // Licensed under the Eclipse Public License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. You may // obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // // See the License for the specific language governing permissions and // limitations under the License. // import core, { AnyAttribute, Attribute, Class, Client, Doc, DocumentQuery, FindOptions, FindResult, generateId, Hierarchy, Ref, RefTo, SortingOrder, SortingRules, Status, StatusManager, Tx, TxResult } from '@hcengineering/core' import { LiveQuery } from '@hcengineering/query' import { writable } from 'svelte/store' import { BasePresentationMiddleware, PresentationMiddleware } from './pipeline' // Issue status live query export const statusStore = writable(new StatusManager([])) interface StatusSubscriber { attributes: Array> _class: Ref> query: DocumentQuery options?: FindOptions refresh: () => void } /** * @public */ export class StatusMiddleware extends BasePresentationMiddleware implements PresentationMiddleware { mgr: StatusManager | Promise | undefined status: Status[] | undefined statusQuery: (() => void) | undefined lq: LiveQuery subscribers: Map = new Map() private constructor (client: Client, next?: PresentationMiddleware) { super(client, next) this.lq = new LiveQuery(client) } async notifyTx (tx: Tx): Promise { await this.lq.tx(tx) await this.provideNotifyTx(tx) } async close (): Promise { this.statusQuery?.() return await this.provideClose() } async getManager (): Promise { if (this.mgr !== undefined) { if (this.mgr instanceof Promise) { this.mgr = await this.mgr } return this.mgr } this.mgr = new Promise((resolve) => { this.statusQuery = this.lq.query( core.class.Status, {}, (res) => { const first = this.status === undefined this.status = res this.mgr = new StatusManager(res) statusStore.set(this.mgr) if (!first) { this.refreshSubscribers() } resolve(this.mgr) }, { lookup: { category: core.class.StatusCategory }, sort: { rank: SortingOrder.Ascending } } ) }) return await this.mgr } private refreshSubscribers (): void { for (const s of this.subscribers.values()) { // TODO: Do something more smart and track if used status field is changed. s.refresh() } } async subscribe( _class: Ref>, query: DocumentQuery, options: FindOptions | undefined, refresh: () => void ): Promise<{ unsubscribe: () => void query?: DocumentQuery options?: FindOptions }> { const ret = await this.provideSubscribe(_class, query, options, refresh) const h = this.client.getHierarchy() const id = generateId() const s: StatusSubscriber = { _class, query, refresh, options, attributes: [] } const statusFields: Array> = [] const allAttrs = h.getAllAttributes(_class) const updatedQuery: DocumentQuery = { ...(ret.query ?? query) } const finalOptions = { ...(ret.options ?? options ?? {}) } await this.updateQueryOptions(allAttrs, h, statusFields, updatedQuery, finalOptions) if (statusFields.length > 0) { this.subscribers.set(id, s) return { unsubscribe: () => { ret.unsubscribe() this.subscribers.delete(id) }, query: updatedQuery, options: finalOptions } } return { unsubscribe: (await ret).unsubscribe } } static create (client: Client, next?: PresentationMiddleware): StatusMiddleware { return new StatusMiddleware(client, next) } async findAll( _class: Ref>, query: DocumentQuery, options?: FindOptions | undefined ): Promise> { const statusFields: Array> = [] const h = this.client.getHierarchy() const allAttrs = h.getAllAttributes(_class) const finalOptions = options ?? {} await this.updateQueryOptions(allAttrs, h, statusFields, query, finalOptions) const result = await this.provideFindAll(_class, query, finalOptions) // We need to add $ if (statusFields.length > 0) { // We need to update $lookup for status fields and provide $status group fields. for (const attr of statusFields) { for (const r of result) { const resultDoc = Hierarchy.toDoc(r) if (resultDoc.$lookup === undefined) { resultDoc.$lookup = {} } // TODO: Check for mixin? const stateValue = (r as any)[attr.name] const status = (await this.getManager()).byId.get(stateValue) if (status !== undefined) { ;(resultDoc.$lookup as any)[attr.name] = status } } } } return result } private async updateQueryOptions( allAttrs: Map, h: Hierarchy, statusFields: Array>, query: DocumentQuery, finalOptions: FindOptions ): Promise { for (const attr of allAttrs.values()) { try { if (attr.type._class === core.class.RefTo && h.isDerived((attr.type as RefTo).to, core.class.Status)) { const mgr = await this.getManager() let target: Array> = [] statusFields.push(attr) const v = (query as any)[attr.name] if (v !== undefined) { // Only add filter if we have filer inside. if (v?.$in !== undefined) { target.push(...v.$in) } else { target.push(v) } // Find all similar name statues for same attribute name. for (const sid of [...target]) { const s = mgr.byId.get(sid) if (s !== undefined) { const statuses = mgr.statuses.filter( (it) => it.ofAttribute === attr._id && it.name.toLowerCase().trim() === s.name.toLowerCase().trim() && it._id !== s._id ) if (statuses !== undefined) { target.push(...statuses.map((it) => it._id)) } } } target = target.filter((it, idx, arr) => arr.indexOf(it) === idx) ;(query as any)[attr.name] = { $in: target } if (finalOptions.lookup !== undefined) { // Remove lookups by status field if ((finalOptions.lookup as any)[attr.name] !== undefined) { const { [attr.name]: _, ...newLookup } = finalOptions.lookup as any finalOptions.lookup = newLookup } } } // Update sorting if defined. this.updateCustomSorting(finalOptions, attr, mgr) } } catch (err: any) { console.error(err) } } } private updateCustomSorting( finalOptions: FindOptions, attr: AnyAttribute, mgr: StatusManager ): void { const attrSort = finalOptions.sort?.[attr.name] if (attrSort !== undefined && typeof attrSort !== 'object') { // Fill custom sorting. const statuses = mgr.statuses.filter((it) => it.ofAttribute === attr._id) statuses.sort((a, b) => { let ret = 0 if (a.category !== undefined && b.category !== undefined) { ret = (a.$lookup?.category?.order ?? 0) - (b.$lookup?.category?.order ?? 0) } if (ret === 0) { if (a.name.toLowerCase().trim() === b.name.toLowerCase().trim()) { return 0 } ret = a.rank.localeCompare(b.rank) } return ret }) if (finalOptions.sort === undefined) { finalOptions.sort = {} } const rules: SortingRules = { order: attrSort, cases: statuses.map((it, idx) => ({ query: it._id, index: idx })), default: statuses.length + 1 } ;(finalOptions.sort as any)[attr.name] = rules } } async tx (tx: Tx): Promise { return await this.provideTx(tx) } }