import { get } from 'svelte/store' import type { DisplayTx, TxViewlet } from '@hcengineering/activity' import core, { type AttachedDoc, type Class, type Client, type Collection, type Doc, type Hierarchy, type Obj, type Ref, type TxCUD, type TxCollectionCUD, type TxCreateDoc, type TxMixin, type TxOperations, TxProcessor, type TxUpdateDoc, matchQuery } from '@hcengineering/core' import { type Asset, type IntlString, getResource, translate } from '@hcengineering/platform' import { getAttributePresenterClass } from '@hcengineering/presentation' import { type AnyComponent, type AnySvelteComponent, ErrorPresenter, themeStore } from '@hcengineering/ui' import view, { type AttributeModel, type BuildModelKey, type BuildModelOptions } from '@hcengineering/view' import { getObjectPresenter } from '@hcengineering/view-resources' import { type ActivityKey, activityKey } from './activity' import activity from './plugin' const valueTypes: ReadonlyArray>> = [ core.class.TypeString, core.class.EnumOf, core.class.TypeNumber, core.class.TypeDate, core.class.TypeMarkup ] export type TxDisplayViewlet = | (Pick & { component?: AnyComponent | AnySvelteComponent pseudo: boolean }) | undefined async function createPseudoViewlet ( client: TxOperations, dtx: DisplayTx, label: IntlString, display: 'inline' | 'content' | 'emphasized' = 'inline' ): Promise { const docClass: Class = client.getModel().getObject(dtx.tx.objectClass) const language = get(themeStore).language let trLabel = docClass.label !== undefined ? await translate(docClass.label, {}, language) : undefined if (dtx.collectionAttribute !== undefined) { const itemLabel = (dtx.collectionAttribute.type as Collection).itemLabel if (itemLabel !== undefined) { trLabel = await translate(itemLabel, {}, language) } } // Check if it is attached doc and collection have title override. const presenter = await getObjectPresenter(client, dtx.tx.objectClass, { key: 'doc-presenter' }, false, false) if (presenter !== undefined) { let collection = '' if (dtx.collectionAttribute?.label !== undefined) { collection = await translate(dtx.collectionAttribute.label, {}, language) } return { display, icon: docClass.icon ?? activity.icon.Activity, label, labelParams: { _class: trLabel, collection }, component: presenter.presenter, pseudo: true } } } export function getDTxProps (dtx: DisplayTx): any { return { tx: dtx.tx, value: dtx.doc, isOwnTx: dtx.isOwnTx, prevValue: dtx.prevDoc } } function getViewlet ( viewlets: Map, dtx: DisplayTx, hierarchy: Hierarchy ): TxDisplayViewlet | undefined { let key: string if (dtx.mixinTx?.mixin !== undefined && dtx.tx._id === dtx.mixinTx._id) { key = activityKey(dtx.mixinTx.mixin, dtx.tx._class) } else { key = activityKey(dtx.tx.objectClass, dtx.tx._class) } const vl = viewlets.get(key) if (vl !== undefined) { for (const viewlet of vl) { if (viewlet.match === undefined) { return { ...viewlet, pseudo: false } } const res = matchQuery([dtx.tx], viewlet.match, dtx.tx._class, hierarchy) if (res.length > 0) { return { ...viewlet, pseudo: false } } } } } export async function updateViewlet ( client: TxOperations, viewlets: Map, dtx: DisplayTx ): Promise<{ viewlet: TxDisplayViewlet id: Ref> model: AttributeModel[] props: any modelIcon: Asset | undefined iconComponent: AnyComponent | undefined }> { let viewlet = getViewlet(viewlets, dtx, client.getHierarchy()) const props = getDTxProps(dtx) let model: AttributeModel[] = [] let modelIcon: Asset | undefined let iconComponent: AnyComponent | undefined if (viewlet === undefined) { ;({ viewlet, model } = await checkInlineViewlets(dtx, viewlet, client, model, dtx.isOwnTx)) if (model !== undefined) { // Check for State attribute for (const a of model) { if (a.icon !== undefined) { modelIcon = a.icon break } } for (const a of model) { if (a.attribute?.iconComponent !== undefined) { iconComponent = a.attribute?.iconComponent break } } } } return { viewlet, id: dtx.tx._id, model, props, modelIcon, iconComponent } } async function checkInlineViewlets ( dtx: DisplayTx, viewlet: TxDisplayViewlet, client: TxOperations, model: AttributeModel[], isOwn: boolean ): Promise<{ viewlet: TxDisplayViewlet, model: AttributeModel[] }> { if (dtx.collectionAttribute !== undefined && (dtx.txDocIds?.size ?? 0) > 1) { // Check if we have a class presenter we could have a pseudo viewlet based on class presenter. viewlet = await createPseudoViewlet(client, dtx, activity.string.CollectionUpdated, 'inline') } else if (dtx.tx._class === core.class.TxCreateDoc) { // Check if we have a class presenter we could have a pseudo viewlet based on class presenter. viewlet = await createPseudoViewlet(client, dtx, isOwn ? activity.string.DocCreated : activity.string.DocAdded) } else if (dtx.tx._class === core.class.TxRemoveDoc) { viewlet = await createPseudoViewlet(client, dtx, activity.string.DocDeleted) } else if (dtx.tx._class === core.class.TxUpdateDoc || dtx.tx._class === core.class.TxMixin) { model = await createUpdateModel(dtx, client, model) } return { viewlet, model } } async function getAttributePresenter ( client: Client, _class: Ref>, key: string, preserveKey: BuildModelKey ): Promise { const hierarchy = client.getHierarchy() const attribute = hierarchy.getAttribute(_class, key) const presenterClass = getAttributePresenterClass(hierarchy, attribute) const isCollectionAttr = presenterClass.category === 'collection' const mixin = isCollectionAttr ? view.mixin.CollectionPresenter : view.mixin.ActivityAttributePresenter let presenterMixin = hierarchy.classHierarchyMixin(presenterClass.attrClass, mixin) if (presenterMixin?.presenter === undefined && mixin === view.mixin.ActivityAttributePresenter) { presenterMixin = hierarchy.classHierarchyMixin(presenterClass.attrClass, view.mixin.AttributePresenter) if (presenterMixin?.presenter === undefined) { throw new Error('attribute presenter not found for ' + JSON.stringify(preserveKey)) } } else if (presenterMixin?.presenter === undefined) { throw new Error('attribute presenter not found for ' + JSON.stringify(preserveKey)) } const resultKey = preserveKey.sortingKey ?? preserveKey.key const sortingKey = Array.isArray(resultKey) ? resultKey : attribute.type._class === core.class.ArrOf ? resultKey + '.length' : resultKey const presenter = await getResource(presenterMixin.presenter) return { key: preserveKey.key, sortingKey, _class: presenterClass.attrClass, label: preserveKey.label ?? attribute.shortLabel ?? attribute.label, presenter, props: preserveKey.props, icon: presenterMixin.icon, attribute, collectionAttr: isCollectionAttr, isLookup: false } } async function buildModel (options: BuildModelOptions): Promise { // eslint-disable-next-line array-callback-return const model = options.keys .map((key) => (typeof key === 'string' ? { key } : key)) .map(async (key) => { try { return await getAttributePresenter(options.client, options._class, key.key, key) } 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 }, collectionAttr: false, isLookup: false } return errorPresenter } }) return (await Promise.all(model)).filter((a) => a !== undefined) as AttributeModel[] } async function createUpdateModel ( dtx: DisplayTx, client: TxOperations, model: AttributeModel[] ): Promise { if (dtx.updateTx !== undefined) { const _class = dtx.updateTx.objectClass const ops = { client, _class, keys: Object.entries(dtx.updateTx.operations) .flatMap(([id, val]) => (['$push', '$pull'].includes(id) ? Object.keys(val) : id)) .filter((id) => !id.startsWith('$')), ignoreMissing: true } const hiddenAttrs = getHiddenAttrs(client, _class) model = (await buildModel(ops)).filter((x) => !hiddenAttrs.has(x.key)) } else if (dtx.mixinTx !== undefined) { const _class = dtx.mixinTx.mixin const ops = { client, _class, keys: Object.keys(dtx.mixinTx.attributes).filter((id) => !id.startsWith('$')), ignoreMissing: true } const hiddenAttrs = getHiddenAttrs(client, _class) model = (await buildModel(ops)).filter((x) => !hiddenAttrs.has(x.key)) } return model } function getHiddenAttrs (client: TxOperations, _class: Ref>): Set { return new Set( [...client.getHierarchy().getAllAttributes(_class).entries()] .filter(([, attr]) => attr.hidden === true) .map(([k]) => k) ) } function getModifiedAttributes (tx: DisplayTx): any[] { if (tx.tx._class === core.class.TxUpdateDoc) { return ([tx.tx, ...tx.txes.map(({ tx }) => tx)] as unknown as Array>).map( ({ operations }) => operations ) } if (tx.tx._class === core.class.TxMixin) { return ([tx.tx, ...tx.txes.map(({ tx }) => tx)] as unknown as Array>).map( ({ attributes }) => attributes ) } return [{}] } async function buildRemovedDoc ( client: TxOperations, objectId: Ref, _class: Ref> ): Promise { const isAttached = client.getHierarchy().isDerived(_class, core.class.AttachedDoc) const txes = await client.findAll>( isAttached ? core.class.TxCollectionCUD : core.class.TxCUD, isAttached ? { 'tx.objectId': objectId as Ref } : { objectId }, { sort: { modifiedOn: 1 } } ) const createTx = isAttached ? txes.find((tx) => (tx as TxCollectionCUD).tx._class === core.class.TxCreateDoc) : txes.find((tx) => tx._class === core.class.TxCreateDoc) if (createTx === undefined) return let doc = TxProcessor.createDoc2Doc(createTx as TxCreateDoc) for (let tx of txes) { tx = TxProcessor.extractTx(tx) as TxCUD if (tx._class === core.class.TxUpdateDoc) { doc = TxProcessor.updateDoc2Doc(doc, tx as TxUpdateDoc) } else if (tx._class === core.class.TxMixin) { const mixinTx = tx as TxMixin doc = TxProcessor.updateMixin4Doc(doc, mixinTx) } } return doc } async function getAllRealValues ( client: TxOperations, values: any[], _class: Ref> ): Promise<[any[], boolean]> { if (values.length === 0) return [[], false] if (values.some((value) => typeof value !== 'string')) { return [values, false] } if (valueTypes.includes(_class)) { return [values, false] } const realValues = await client.findAll(_class, { _id: { $in: values } }) const realValuesIds = realValues.map(({ _id }) => _id) const res = [ ...realValues, ...(await Promise.all( values .filter((value) => !realValuesIds.includes(value)) .map(async (value) => await buildRemovedDoc(client, value, _class)) )) ].filter((v) => v != null) return [res, true] } function combineAttributes (attributes: any[], key: string, operator: string, arrayKey: string): any[] { return Array.from( new Set( attributes.flatMap((attr) => Array.isArray(attr[operator]?.[key]?.[arrayKey]) ? attr[operator]?.[key]?.[arrayKey] : attr[operator]?.[key] ) ) ).filter((v) => v != null) } interface TxAttributeValue { set: any isObjectSet: boolean added: any[] isObjectAdded: boolean removed: any[] isObjectRemoved: boolean } export async function getValue (client: TxOperations, m: AttributeModel, tx: DisplayTx): Promise { const utxs = getModifiedAttributes(tx) const added = await getAllRealValues(client, combineAttributes(utxs, m.key, '$push', '$each'), m._class) const removed = await getAllRealValues(client, combineAttributes(utxs, m.key, '$pull', '$in'), m._class) const value: TxAttributeValue = { set: utxs[0][m.key], isObjectSet: false, added: added[0], isObjectAdded: added[1], removed: removed[0], isObjectRemoved: removed[1] } if (value.set !== undefined) { const res = await getAllRealValues(client, [value.set], m._class) value.set = res[0][0] value.isObjectSet = res[1] } return value }