// // 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, { AggregateValue, AggregateValueData, AnyAttribute, Attribute, Class, Client, Doc, DocumentQuery, FindOptions, Hierarchy, IdMap, Ref, SortingOrder, SortingRules, Space, Status, StatusCategory, StatusManager, StatusValue, Tx, WithLookup, matchQuery, toIdMap } from '@hcengineering/core' import { LiveQuery } from '@hcengineering/query' import { AggregationManager, GrouppingManager } from '@hcengineering/view' import { get, writable } from 'svelte/store' // Issue status live query export const statusStore = writable(new StatusManager([])) /** * @public */ export class StatusAggregationManager implements AggregationManager { docs: Doc[] | undefined docsByName: Map = new Map() mgr: StatusManager | Promise | undefined statusCategory: IdMap = new Map() query: (() => void) | undefined categoryQuery: (() => void) | undefined categoryPromise!: Promise lq: LiveQuery lqCallback: () => void private constructor (client: Client, lqCallback: () => void) { this.lq = new LiveQuery(client) this.lqCallback = lqCallback ?? (() => {}) } static create (client: Client, lqCallback: () => void): StatusAggregationManager { return new StatusAggregationManager(client, lqCallback) } private async getManager (): Promise { if (this.mgr !== undefined) { if (this.mgr instanceof Promise) { this.mgr = await this.mgr } return this.mgr } this.categoryPromise = new Promise((resolve) => { this.categoryQuery = this.lq.query(core.class.StatusCategory, {}, (res) => { this.statusCategory = toIdMap(res) resolve() }) }) this.mgr = new Promise((resolve) => { this.query = this.lq.query( core.class.Status, {}, (res) => { const first = this.docs === undefined this.docs = res const newMap = new Map() for (const d of this.docs as Array>) { const n = d.name.toLowerCase().trim() newMap.set(n, [...(newMap.get(n) ?? []), d]) } this.mgr = new StatusManager(res) this.docsByName = newMap statusStore.set(this.mgr) if (!first) { this.lqCallback() } resolve(this.mgr) }, { sort: { rank: SortingOrder.Ascending } } ) }) return await this.mgr } close (): void { this.query?.() this.categoryQuery?.() } async notifyTx (tx: Tx): Promise { await this.lq.tx(tx) } getAttrClass (): Ref> { return core.class.Status } async categorize (target: Array>, attr: AnyAttribute): Promise>> { const mgr = await this.getManager() const idMap = mgr.getIdMap() for (const sid of [...target]) { const s = idMap.get(sid as Ref) as WithLookup if (s !== undefined) { const statuses = (this.docsByName.get(s.name.toLowerCase().trim()) ?? []).filter( (it) => it.ofAttribute === attr._id && it._id !== s._id ) target.push(...statuses.map((it) => it._id)) } } return target.filter((it, idx, arr) => arr.indexOf(it) === idx) } async updateLookup (resultDoc: WithLookup, attr: Attribute): Promise { const value = (resultDoc as any)[attr.name] const doc = (await this.getManager()).getIdMap().get(value) if (doc !== undefined) { ;(resultDoc.$lookup as any)[attr.name] = doc } } async updateSorting(finalOptions: FindOptions, attr: AnyAttribute): Promise { const attrSort = finalOptions.sort?.[attr.name] if (attrSort !== undefined && typeof attrSort !== 'object') { // Fill custom sorting. let statuses = (await this.getManager()).getDocs() statuses = statuses.filter((it) => it.ofAttribute === attr._id) await this.categoryPromise statuses.sort((a, b) => { let ret = 0 if (a.category !== undefined && b.category !== undefined) { ret = (this.statusCategory.get(a.category)?.order ?? 0) - (this.statusCategory.get(b.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 } } } /** * @public */ export const grouppingStatusManager: GrouppingManager = { groupByCategories: groupByStatusCategories, groupValues: groupStatusValues, groupValuesWithEmpty: groupStatusValuesWithEmpty, hasValue: hasStatusValue } /** * @public */ export function groupByStatusCategories (categories: any[]): AggregateValue[] { const mgr = get(statusStore) const existingCategories: AggregateValue[] = [] const statusMap = new Map() const usedSpaces = new Set>() const statusesList: Array> = [] for (const v of categories) { const status = mgr.getIdMap().get(v) if (status !== undefined) { statusesList.push(status) usedSpaces.add(status.space) } } for (const status of statusesList) { if (status !== undefined) { let fst = statusMap.get(status.name.toLowerCase().trim()) if (fst === undefined) { const statuses = mgr .getDocs() .filter( (it) => it.ofAttribute === status.ofAttribute && it.name.toLowerCase().trim() === status.name.toLowerCase().trim() && (categories.includes(it._id) || usedSpaces.has(it.space)) ) .sort((a, b) => a.rank.localeCompare(b.rank)) .map((it) => new AggregateValueData(it.name, it._id, it.space, it.rank, it.category)) fst = new StatusValue(status.name, status.color, statuses) statusMap.set(status.name.toLowerCase().trim(), fst) existingCategories.push(fst) } } } return existingCategories } /** * @public */ export function groupStatusValues (val: Doc[], targets: Set): Doc[] { const values = val const result: Doc[] = [] const unique = [...new Set(val.map((v) => (v as Status).name.trim().toLocaleLowerCase()))] unique.forEach((label, i) => { let exists = false values.forEach((value) => { if ((value as Status).name.trim().toLocaleLowerCase() === label) { if (!exists) { result[i] = value exists = targets.has(value?._id) } } }) }) return result } /** * @public */ export function hasStatusValue (value: Doc | undefined | null, values: any[]): boolean { const mgr = get(statusStore) const statusSet = new Set( mgr .filter((it) => it.name.trim().toLocaleLowerCase() === (value as Status)?.name?.trim()?.toLocaleLowerCase()) .map((it) => it._id) ) return values.some((it) => statusSet.has(it)) } /** * @public */ export function groupStatusValuesWithEmpty ( hierarchy: Hierarchy, _class: Ref>, key: string, query: DocumentQuery | undefined ): Array> { const mgr = get(statusStore) const attr = hierarchy.getAttribute(_class, key) // We do not need extensions for all status categories. let statusList = mgr.filter((it) => { return it.ofAttribute === attr._id }) if (query !== undefined) { const { [key]: st, space } = query const resQuery: DocumentQuery = {} if (space !== undefined) { resQuery.space = space } if (st !== undefined) { resQuery._id = st } statusList = matchQuery(statusList, resQuery, _class, hierarchy) as unknown as Array> } return statusList.map((it) => it._id) }