2022-09-21 08:08:25 +00:00
|
|
|
import type { TxViewlet } from '@hcengineering/activity'
|
2022-05-13 06:42:14 +00:00
|
|
|
import core, {
|
|
|
|
AttachedDoc,
|
|
|
|
Class,
|
|
|
|
Collection,
|
|
|
|
Doc,
|
|
|
|
Ref,
|
|
|
|
TxCollectionCUD,
|
|
|
|
TxCreateDoc,
|
|
|
|
TxCUD,
|
|
|
|
TxMixin,
|
|
|
|
TxOperations,
|
|
|
|
TxProcessor,
|
|
|
|
TxUpdateDoc
|
2022-09-21 08:08:25 +00:00
|
|
|
} from '@hcengineering/core'
|
|
|
|
import { Asset, IntlString, translate } from '@hcengineering/platform'
|
|
|
|
import { AnyComponent, AnySvelteComponent } from '@hcengineering/ui'
|
|
|
|
import { AttributeModel } from '@hcengineering/view'
|
|
|
|
import { buildModel, getObjectPresenter } from '@hcengineering/view-resources'
|
2022-01-11 09:09:52 +00:00
|
|
|
import { ActivityKey, activityKey, DisplayTx } from '../activity'
|
2022-02-16 09:02:31 +00:00
|
|
|
import activity from '../plugin'
|
2022-01-11 09:09:52 +00:00
|
|
|
|
|
|
|
export type TxDisplayViewlet =
|
2022-02-08 09:05:12 +00:00
|
|
|
| (Pick<TxViewlet, 'icon' | 'label' | 'display' | 'editable' | 'hideOnRemove' | 'labelComponent' | 'labelParams'> & {
|
2022-01-11 09:09:52 +00:00
|
|
|
component?: AnyComponent | AnySvelteComponent
|
2022-04-24 05:18:03 +00:00
|
|
|
pseudo: boolean
|
2022-01-11 09:09:52 +00:00
|
|
|
})
|
|
|
|
| undefined
|
|
|
|
|
|
|
|
async function createPseudoViewlet (
|
2022-01-13 09:06:50 +00:00
|
|
|
client: TxOperations,
|
2022-01-11 09:09:52 +00:00
|
|
|
dtx: DisplayTx,
|
2022-03-09 09:41:14 +00:00
|
|
|
label: IntlString,
|
|
|
|
display: 'inline' | 'content' | 'emphasized' = 'inline'
|
2022-01-11 09:09:52 +00:00
|
|
|
): Promise<TxDisplayViewlet> {
|
|
|
|
const doc = dtx.doc
|
|
|
|
if (doc === undefined) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
const docClass: Class<Doc> = client.getModel().getObject(doc._class)
|
|
|
|
|
2022-02-16 09:02:31 +00:00
|
|
|
let trLabel = await translate(docClass.label, {})
|
|
|
|
if (dtx.collectionAttribute !== undefined) {
|
|
|
|
const itemLabel = (dtx.collectionAttribute.type as Collection<AttachedDoc>).itemLabel
|
|
|
|
if (itemLabel !== undefined) {
|
|
|
|
trLabel = await translate(itemLabel, {})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Check if it is attached doc and collection have title override.
|
2022-01-11 09:09:52 +00:00
|
|
|
const presenter = await getObjectPresenter(client, doc._class, { key: 'doc-presenter' })
|
|
|
|
if (presenter !== undefined) {
|
2022-05-05 14:50:28 +00:00
|
|
|
let collection = ''
|
|
|
|
if (dtx.collectionAttribute?.label !== undefined) {
|
|
|
|
collection = await translate(dtx.collectionAttribute.label, {})
|
|
|
|
}
|
2022-01-11 09:09:52 +00:00
|
|
|
return {
|
2022-03-09 09:41:14 +00:00
|
|
|
display,
|
2022-01-11 09:09:52 +00:00
|
|
|
icon: docClass.icon ?? activity.icon.Activity,
|
2022-11-02 08:50:14 +00:00
|
|
|
label,
|
2022-04-29 05:27:17 +00:00
|
|
|
labelParams: {
|
|
|
|
_class: trLabel,
|
2022-05-05 14:50:28 +00:00
|
|
|
collection
|
2022-04-29 05:27:17 +00:00
|
|
|
},
|
2022-04-24 05:18:03 +00:00
|
|
|
component: presenter.presenter,
|
|
|
|
pseudo: true
|
2022-01-11 09:09:52 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-02-28 04:32:44 +00:00
|
|
|
export function getDTxProps (dtx: DisplayTx): any {
|
2022-04-24 05:18:03 +00:00
|
|
|
return { tx: dtx.tx, value: dtx.doc }
|
2022-02-28 04:32:44 +00:00
|
|
|
}
|
|
|
|
|
2022-03-26 17:34:06 +00:00
|
|
|
function getViewlet (viewlets: Map<ActivityKey, TxViewlet>, dtx: DisplayTx): 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)
|
|
|
|
}
|
2022-04-24 05:18:03 +00:00
|
|
|
const vl = viewlets.get(key)
|
|
|
|
if (vl !== undefined) {
|
|
|
|
return { ...vl, pseudo: false }
|
|
|
|
}
|
2022-03-26 17:34:06 +00:00
|
|
|
}
|
|
|
|
|
2022-01-11 09:09:52 +00:00
|
|
|
export async function updateViewlet (
|
2022-01-13 09:06:50 +00:00
|
|
|
client: TxOperations,
|
2022-01-11 09:09:52 +00:00
|
|
|
viewlets: Map<ActivityKey, TxViewlet>,
|
|
|
|
dtx: DisplayTx
|
|
|
|
): Promise<{
|
|
|
|
viewlet: TxDisplayViewlet
|
|
|
|
id: Ref<TxCUD<Doc>>
|
|
|
|
model: AttributeModel[]
|
|
|
|
props: any
|
|
|
|
modelIcon: Asset | undefined
|
|
|
|
}> {
|
2022-03-26 17:34:06 +00:00
|
|
|
let viewlet = getViewlet(viewlets, dtx)
|
2022-01-11 09:09:52 +00:00
|
|
|
|
2022-04-24 05:18:03 +00:00
|
|
|
let props = getDTxProps(dtx)
|
2022-01-11 09:09:52 +00:00
|
|
|
let model: AttributeModel[] = []
|
|
|
|
let modelIcon: Asset | undefined
|
|
|
|
|
|
|
|
if (viewlet === undefined) {
|
|
|
|
;({ viewlet, model } = await checkInlineViewlets(dtx, viewlet, client, model))
|
2022-04-24 05:18:03 +00:00
|
|
|
// Only value is necessary for inline viewlets
|
|
|
|
props = { value: dtx.doc }
|
2022-01-11 09:09:52 +00:00
|
|
|
if (model !== undefined) {
|
|
|
|
// Check for State attribute
|
|
|
|
for (const a of model) {
|
|
|
|
if (a.icon !== undefined) {
|
|
|
|
modelIcon = a.icon
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return { viewlet, id: dtx.tx._id, model, props, modelIcon }
|
|
|
|
}
|
|
|
|
|
|
|
|
async function checkInlineViewlets (
|
|
|
|
dtx: DisplayTx,
|
|
|
|
viewlet: TxDisplayViewlet,
|
2022-01-13 09:06:50 +00:00
|
|
|
client: TxOperations,
|
2022-01-11 09:09:52 +00:00
|
|
|
model: AttributeModel[]
|
|
|
|
): Promise<{ viewlet: TxDisplayViewlet, model: AttributeModel[] }> {
|
2022-06-02 17:42:44 +00:00
|
|
|
if (dtx.collectionAttribute !== undefined && (dtx.txDocIds?.size ?? 0) > 1) {
|
2022-02-28 04:32:44 +00:00
|
|
|
// Check if we have a class presenter we could have a pseudo viewlet based on class presenter.
|
2022-03-09 09:41:14 +00:00
|
|
|
viewlet = await createPseudoViewlet(client, dtx, activity.string.CollectionUpdated, 'content')
|
2022-02-28 04:32:44 +00:00
|
|
|
} else if (dtx.tx._class === core.class.TxCreateDoc) {
|
2022-01-11 09:09:52 +00:00
|
|
|
// Check if we have a class presenter we could have a pseudo viewlet based on class presenter.
|
2022-02-08 09:05:12 +00:00
|
|
|
viewlet = await createPseudoViewlet(client, dtx, activity.string.DocCreated)
|
2022-02-28 04:32:44 +00:00
|
|
|
} else if (dtx.tx._class === core.class.TxRemoveDoc) {
|
2022-02-08 09:05:12 +00:00
|
|
|
viewlet = await createPseudoViewlet(client, dtx, activity.string.DocDeleted)
|
2022-05-11 05:36:35 +00:00
|
|
|
} else if (dtx.tx._class === core.class.TxUpdateDoc || dtx.tx._class === core.class.TxMixin) {
|
2022-01-11 09:09:52 +00:00
|
|
|
model = await createUpdateModel(dtx, client, model)
|
|
|
|
}
|
|
|
|
return { viewlet, model }
|
|
|
|
}
|
|
|
|
|
|
|
|
async function createUpdateModel (
|
|
|
|
dtx: DisplayTx,
|
2022-01-13 09:06:50 +00:00
|
|
|
client: TxOperations,
|
2022-01-11 09:09:52 +00:00
|
|
|
model: AttributeModel[]
|
|
|
|
): Promise<AttributeModel[]> {
|
|
|
|
if (dtx.updateTx !== undefined) {
|
|
|
|
const _class = dtx.updateTx.objectClass
|
|
|
|
const ops = {
|
|
|
|
client,
|
|
|
|
_class,
|
2022-05-13 06:42:14 +00:00
|
|
|
keys: Object.entries(dtx.updateTx.operations)
|
|
|
|
.flatMap(([id, val]) => (['$push', '$pull'].includes(id) ? Object.keys(val) : id))
|
|
|
|
.filter((id) => !id.startsWith('$')),
|
2022-01-11 09:09:52 +00:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2022-01-13 09:06:50 +00:00
|
|
|
function getHiddenAttrs (client: TxOperations, _class: Ref<Class<Doc>>): Set<string> {
|
2022-01-11 09:09:52 +00:00
|
|
|
return new Set(
|
|
|
|
[...client.getHierarchy().getAllAttributes(_class).entries()]
|
|
|
|
.filter(([, attr]) => attr.hidden === true)
|
|
|
|
.map(([k]) => k)
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2022-05-13 06:42:14 +00:00
|
|
|
function getModifiedAttributes (tx: DisplayTx): any[] {
|
|
|
|
if (tx.tx._class === core.class.TxUpdateDoc) {
|
|
|
|
return ([tx.tx, ...tx.txes.map(({ tx }) => tx)] as unknown as Array<TxUpdateDoc<Doc>>).map(
|
|
|
|
({ operations }) => operations
|
|
|
|
)
|
|
|
|
}
|
|
|
|
if (tx.tx._class === core.class.TxMixin) {
|
|
|
|
return ([tx.tx, ...tx.txes.map(({ tx }) => tx)] as unknown as Array<TxMixin<Doc, Doc>>).map(
|
|
|
|
({ attributes }) => attributes
|
|
|
|
)
|
|
|
|
}
|
|
|
|
return [{}]
|
|
|
|
}
|
2022-01-11 09:09:52 +00:00
|
|
|
|
2022-05-13 06:42:14 +00:00
|
|
|
async function buildRemovedDoc (client: TxOperations, objectId: Ref<Doc>): Promise<Doc | undefined> {
|
|
|
|
const txes = await client.findAll(core.class.TxCUD, { objectId }, { sort: { modifiedOn: 1 } })
|
|
|
|
let doc: Doc
|
|
|
|
let createTx = txes.find((tx) => tx._class === core.class.TxCreateDoc)
|
|
|
|
if (createTx === undefined) {
|
|
|
|
const collectionTxes = txes.filter((tx) => tx._class === core.class.TxCollectionCUD) as Array<
|
|
|
|
TxCollectionCUD<Doc, AttachedDoc>
|
|
|
|
>
|
|
|
|
createTx = collectionTxes.find((p) => p.tx._class === core.class.TxCreateDoc)
|
|
|
|
}
|
|
|
|
if (createTx === undefined) return
|
|
|
|
doc = TxProcessor.createDoc2Doc(createTx as TxCreateDoc<Doc>)
|
|
|
|
for (const tx of txes) {
|
|
|
|
if (tx._class === core.class.TxUpdateDoc) {
|
|
|
|
doc = TxProcessor.updateDoc2Doc(doc, tx as TxUpdateDoc<Doc>)
|
|
|
|
} else if (tx._class === core.class.TxMixin) {
|
|
|
|
const mixinTx = tx as TxMixin<Doc, Doc>
|
2022-06-19 16:26:05 +00:00
|
|
|
doc = TxProcessor.updateMixin4Doc(doc, mixinTx)
|
2022-05-13 06:42:14 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
return doc
|
|
|
|
}
|
|
|
|
|
|
|
|
async function getAllRealValues (client: TxOperations, values: any[], _class: Ref<Class<Doc>>): Promise<any[]> {
|
|
|
|
if (!client.getHierarchy().isDerived(_class, core.class.Doc) || values.some((value) => typeof value !== 'string')) {
|
|
|
|
return values
|
|
|
|
}
|
|
|
|
const realValues = await client.findAll(_class, { _id: { $in: values } })
|
|
|
|
const realValuesIds = realValues.map(({ _id }) => _id)
|
|
|
|
return [
|
|
|
|
...realValues,
|
|
|
|
...(await Promise.all(
|
|
|
|
values
|
|
|
|
.filter((value) => !realValuesIds.includes(value))
|
|
|
|
.map(async (value) => await buildRemovedDoc(client, value))
|
|
|
|
))
|
|
|
|
].filter((v) => v != null)
|
|
|
|
}
|
|
|
|
|
|
|
|
export async function getValue (client: TxOperations, m: AttributeModel, tx: DisplayTx): Promise<any> {
|
|
|
|
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)
|
|
|
|
}
|
|
|
|
const utxs = getModifiedAttributes(tx)
|
|
|
|
const value = {
|
|
|
|
set: utxs[0][m.key],
|
|
|
|
added: await getAllRealValues(client, combineAttributes(utxs, m.key, '$push', '$each'), m._class),
|
|
|
|
removed: await getAllRealValues(client, combineAttributes(utxs, m.key, '$pull', '$in'), m._class)
|
|
|
|
}
|
|
|
|
if (value.set !== undefined) {
|
|
|
|
;[value.set] = await getAllRealValues(client, [value.set], m._class)
|
2022-01-11 09:09:52 +00:00
|
|
|
}
|
2022-05-13 06:42:14 +00:00
|
|
|
return value
|
2022-01-11 09:09:52 +00:00
|
|
|
}
|
2022-06-03 04:23:51 +00:00
|
|
|
|
|
|
|
export function filterCollectionTxes (txes: DisplayTx[]): DisplayTx[] {
|
|
|
|
return txes.map(filterCollectionTx).filter(Boolean) as DisplayTx[]
|
|
|
|
}
|
|
|
|
|
|
|
|
function filterCollectionTx (tx: DisplayTx): DisplayTx | undefined {
|
|
|
|
if (tx.collectionAttribute === undefined) return tx
|
|
|
|
const txes = tx.txes.reduceRight(
|
|
|
|
(txes, ctx) => {
|
|
|
|
const filtredTxes = txes.filter(
|
|
|
|
({ tx: { _class }, doc }) => doc?._id !== ctx.doc?._id || _class === core.class.TxUpdateDoc
|
|
|
|
)
|
|
|
|
return ctx.tx._class === core.class.TxUpdateDoc || filtredTxes.length === txes.length
|
|
|
|
? [ctx, ...txes]
|
|
|
|
: filtredTxes
|
|
|
|
},
|
|
|
|
[tx]
|
|
|
|
)
|
|
|
|
const txDocIds = txes.map(({ doc }) => doc?._id).filter(Boolean) as Array<Ref<Doc>>
|
|
|
|
const ctx = txes.pop()
|
|
|
|
if (ctx !== undefined) {
|
|
|
|
ctx.txes = txes
|
|
|
|
ctx.txDocIds = new Set(txDocIds)
|
|
|
|
}
|
|
|
|
return ctx
|
|
|
|
}
|