// // Copyright © 2020, 2021 Anticrm Platform Contributors. // Copyright © 2021 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, { AttachedDoc, Class, ClassifierKind, Client, Collection, Doc, FindResult, Hierarchy, Lookup, matchQuery, Mixin, Obj, Ref, TxOperations } from '@anticrm/core' import type { IntlString } from '@anticrm/platform' import { getResource } from '@anticrm/platform' import { getAttributePresenterClass, KeyedAttribute } from '@anticrm/presentation' import { ErrorPresenter, getPlatformColorForText } from '@anticrm/ui' import type { Action, ActionTarget, BuildModelOptions, ObjectDDParticipant } from '@anticrm/view' import view, { AttributeModel, BuildModelKey } from '@anticrm/view' import plugin from './plugin' /** * Define some properties to be used to show component until data is properly loaded. */ export interface LoadingProps { length: number } /** * @public */ export async function getObjectPresenter ( client: Client, _class: Ref>, preserveKey: BuildModelKey ): Promise { const clazz = client.getHierarchy().getClass(_class) const presenterMixin = client.getHierarchy().as(clazz, view.mixin.AttributePresenter) if (presenterMixin.presenter === undefined) { if (clazz.extends !== undefined) { return await getObjectPresenter(client, clazz.extends, preserveKey) } else { throw new Error('object presenter not found for ' + JSON.stringify(preserveKey)) } } const presenter = await getResource(presenterMixin.presenter) const key = preserveKey.sortingKey ?? preserveKey.key const sortingKey = clazz.sortingKey !== undefined ? (key.length > 0 ? key + '.' + clazz.sortingKey : clazz.sortingKey) : key return { key: preserveKey.key, _class, label: preserveKey.label ?? clazz.label, presenter, sortingKey } } async function getAttributePresenter ( client: Client, _class: Ref>, key: string, preserveKey: BuildModelKey ): Promise { const attribute = client.getHierarchy().getAttribute(_class, key) let attrClass = getAttributePresenterClass(attribute) const clazz = client.getHierarchy().getClass(attrClass) let presenterMixin = client.getHierarchy().as(clazz, view.mixin.AttributePresenter) let parent = clazz.extends while (presenterMixin.presenter === undefined && parent !== undefined) { const pclazz = client.getHierarchy().getClass(parent) attrClass = parent presenterMixin = client.getHierarchy().as(pclazz, view.mixin.AttributePresenter) parent = pclazz.extends } if (presenterMixin.presenter === undefined) { throw new Error('attribute presenter not found for ' + JSON.stringify(preserveKey)) } const resultKey = preserveKey.sortingKey ?? preserveKey.key const sortingKey = attribute.type._class === core.class.ArrOf ? resultKey + '.length' : resultKey const presenter = await getResource(presenterMixin.presenter) return { key: preserveKey.key, sortingKey, _class: attrClass, label: preserveKey.label ?? attribute.label, presenter, icon: presenterMixin.icon, attribute } } async function getPresenter ( client: Client, _class: Ref>, key: BuildModelKey, preserveKey: BuildModelKey, lookup?: Lookup ): Promise { if (key.presenter !== undefined) { const { presenter, label, sortingKey } = key return { key: key.key ?? '', sortingKey: sortingKey ?? '', _class, label: label as IntlString, presenter: await getResource(presenter), props: key.props } } if (key.key.length === 0) { return await getObjectPresenter(client, _class, preserveKey) } else { if (key.key.startsWith('$lookup')) { if (lookup === undefined) { throw new Error(`lookup class does not provided for ${key.key}`) } return await getLookupPresenter(client, _class, key, preserveKey, lookup) } return await getAttributePresenter(client, _class, key.key, preserveKey) } } export async function buildModel (options: BuildModelOptions): Promise { console.log('building table model for', options) // eslint-disable-next-line array-callback-return const model = options.keys .map((key) => (typeof key === 'string' ? { key: key } : key)) .map(async (key) => { try { return await getPresenter(options.client, options._class, key, key, options.options?.lookup) } catch (err: any) { if (options.ignoreMissing ?? false) { return undefined } const stringKey = key.label ?? key.key console.error('Failed to find presenter for', key, err) const errorPresenter: AttributeModel = { key: '', sortingKey: '', presenter: ErrorPresenter, label: stringKey as IntlString, _class: core.class.TypeString, props: { error: err } } return errorPresenter } }) return (await Promise.all(model)).filter((a) => a !== undefined) as AttributeModel[] } function filterActions ( client: Client, doc: Doc, targets: ActionTarget[], derived: Ref> = core.class.Doc ): Array> { const result: Array> = [] const hierarchy = client.getHierarchy() const clazz = hierarchy.getClass(doc._class) const ignoreActions = hierarchy.as(clazz, view.mixin.IgnoreActions) const ignore = ignoreActions?.actions ?? [] for (const target of targets) { if (ignore.includes(target.action)) { continue } if (target.query !== undefined) { const r = matchQuery([doc], target.query) if (r.length === 0) { continue } } if (hierarchy.isDerived(doc._class, target.target) && client.getHierarchy().isDerived(target.target, derived)) { result.push(target.action) } } return result } /** * @public * * Find all action contributions applicable for specified _class. * If derivedFrom is specifie, only actions applicable to derivedFrom class will be used. * So if we have contribution for Doc, Space and we ask for SpaceWithStates and derivedFrom=Space, * we won't recieve Doc contribution but recieve Space ones. */ export async function getActions ( client: Client, doc: Doc, derived: Ref> = core.class.Doc ): Promise> { const targets = await client.findAll(view.class.ActionTarget, {}) return await client.findAll(view.class.Action, { _id: { $in: filterActions(client, doc, targets, derived) } }) } export async function deleteObject (client: TxOperations, object: Doc): Promise { const hierarchy = client.getHierarchy() const promises: Promise[] = [] if (client.getHierarchy().isDerived(object._class, core.class.AttachedDoc)) { const adoc = object as AttachedDoc promises.push(client.removeCollection(object._class, object.space, adoc._id, adoc.attachedTo, adoc.attachedToClass, adoc.collection).catch(err => console.error(err))) } else { promises.push(client.removeDoc(object._class, object.space, object._id).catch(err => console.error(err))) } promises.push(deleteClassCollections(client, object._class, object)) const mixins = getMixins(hierarchy, object._class, object) for (const mixin of mixins) { promises.push(deleteClassCollections(client, mixin, object)) } promises.push(deleteRelatedDocuments(hierarchy, object, client)) await Promise.all(promises) } async function deleteClassCollections (client: TxOperations, _class: Ref>, object: Doc): Promise { const hierarchy = client.getHierarchy() const attributes = hierarchy.getAllAttributes(_class) for (const [name, attribute] of attributes) { if (hierarchy.isDerived(attribute.type._class, core.class.Collection)) { const collection = attribute.type as Collection const allAttached = await client.findAll(collection.of, { attachedTo: object._id }) for (const attached of allAttached) { await deleteObject(client, attached).catch(err => console.log('failed to delete', name, err)) } } } } function getParentClass (hierarchy: Hierarchy, _class: Ref>): Ref> { const baseDomain = hierarchy.getDomain(_class) const ancestors = hierarchy.getAncestors(_class) let result: Ref> = _class for (const ancestor of ancestors) { try { const domain = hierarchy.getClass(ancestor).domain if (domain === baseDomain) { result = ancestor } } catch {} } return result } function getMixins (hierarchy: Hierarchy, _class: Ref>, object: Doc): Ref>[] { const parentClass = getParentClass(hierarchy, _class) const descendants = hierarchy.getDescendants(parentClass) return descendants.filter( (m) => hierarchy.getClass(m).kind === ClassifierKind.MIXIN && hierarchy.hasMixin(object, m) ) } async function deleteRelatedDocuments (hierarchy: Hierarchy, object: Doc, client: TxOperations): Promise { const objectClass = hierarchy.getClass(object._class) if (hierarchy.hasMixin(objectClass, view.mixin.ObjectDDParticipant)) { const removeParticipand: ObjectDDParticipant = hierarchy.as(objectClass, view.mixin.ObjectDDParticipant) const collector = await getResource(removeParticipand.collectDocs) const docs = await collector(object, client) for (const d of docs) { await deleteObject(client, d) } } } export function getMixinStyle (id: Ref>, selected: boolean): string { const color = getPlatformColorForText(id as string) return ` background: ${color + (selected ? 'ff' : '33')}; border: 1px solid ${color + (selected ? '0f' : '66')}; ` } async function getLookupPresenter (client: Client, _class: Ref>, key: BuildModelKey, preserveKey: BuildModelKey, lookup: Lookup): Promise { const lookupClass = getLookupClass(key.key, lookup, _class) const lookupProperty = getLookupProperty(key.key) const lookupKey = { ...key, key: lookupProperty[0] } const model = await getPresenter(client, lookupClass[0], lookupKey, preserveKey) model.label = getLookupLabel(client, lookupClass[1], lookupClass[0], lookupKey, lookupProperty[1]) return model } function getLookupLabel (client: Client, _class: Ref>, lookupClass: Ref>, key: BuildModelKey, attrib: string): IntlString { if (key.label !== undefined) return key.label if (key.key === '') { try { const attribute = client.getHierarchy().getAttribute(_class, attrib) return attribute.label } catch {} const clazz = client.getHierarchy().getClass(lookupClass) return clazz.label } else { const attribute = client.getHierarchy().getAttribute(lookupClass, key.key) return attribute.label } } function getLookupClass (key: string, lookup: Lookup, parent: Ref>): [Ref>, Ref>] { const _class = getLookup(key, lookup, parent) if (_class === undefined) { throw new Error('lookup class does not provided for ' + key) } return _class } function getLookupProperty (key: string): [string, string] { const parts = key.split('$lookup') const lastPart = parts[parts.length - 1] const split = lastPart.split('.').filter((p) => p.length > 0) const prev = split.shift() ?? '' const result = split.join('.') return [result, prev] } function getLookup (key: string, lookup: Lookup, parent: Ref>): [Ref>, Ref>] | undefined { const parts = key.split('$lookup.').filter((p) => p.length > 0) const currentKey = parts[0].split('.').filter((p) => p.length > 0)[0] const current = (lookup as any)[currentKey] const nestedKey = parts.slice(1).join('$lookup.') if (nestedKey.length > 0) { if (!Array.isArray(current)) { return } return getLookup(nestedKey, current[1], current[0]) } if (Array.isArray(current)) { return [current[0], parent] } if (current === undefined && lookup._id !== undefined) { const reverse = (lookup._id as any)[currentKey] return reverse !== undefined ? [reverse, parent] : undefined } return current !== undefined ? [current, parent] : undefined } export function getBooleanLabel (value: boolean | undefined): IntlString { if (value === true) return plugin.string.LabelYes if (value === false) return plugin.string.LabelNo return plugin.string.LabelNA } export function getCollectionCounter (hierarchy: Hierarchy, object: Doc, key: KeyedAttribute): number { if (hierarchy.isMixin(key.attr.attributeOf)) { return (hierarchy.as(object, key.attr.attributeOf) as any)[key.key] } return (object as any)[key.key] ?? 0 }