diff --git a/models/recruit/src/migration.ts b/models/recruit/src/migration.ts index 1217608846..d680ddae1e 100644 --- a/models/recruit/src/migration.ts +++ b/models/recruit/src/migration.ts @@ -137,25 +137,9 @@ export const recruitOperation: MigrateOperation = { // Rename other const categories = await client.find(DOMAIN_TAGS, { _class: tags.class.TagCategory }) - let prefix = 'tags:category:Category' + const prefix = 'tags:category:Category' for (const c of categories) { if (c._id.startsWith(prefix) || c._id === 'tags:category:Other') { - let newCID = c._id.replace(prefix, recruit.category.Category + '.') as Ref<TagCategory> - if (c._id === 'tags:category:Other') { - newCID = recruit.category.Other - } - await client.delete(DOMAIN_TAGS, c._id) - await client.create(DOMAIN_TAGS, { ...c, _id: newCID, targetClass: recruit.mixin.Candidate }) - await client.update(DOMAIN_TAGS, { _class: tags.class.TagElement, category: c._id }, { - category: newCID, - targetClass: recruit.mixin.Candidate - }) - } - } - - prefix = 'recruit:category:Category' - for (const c of categories) { - if ((c._id.startsWith(prefix) && !c._id.startsWith(prefix + '.')) || c._id === 'tags:category:Other') { let newCID = c._id.replace(prefix, recruit.category.Category + '.') as Ref<TagCategory> if (c._id === 'tags:category:Other') { newCID = recruit.category.Other @@ -164,7 +148,7 @@ export const recruitOperation: MigrateOperation = { try { await client.create(DOMAIN_TAGS, { ...c, _id: newCID, targetClass: recruit.mixin.Candidate }) } catch (err: any) { - // Ignore + // ignore } await client.update(DOMAIN_TAGS, { _class: tags.class.TagElement, category: c._id }, { category: newCID, diff --git a/plugins/activity-assets/lang/en.json b/plugins/activity-assets/lang/en.json index 25b0ad23b2..b99a8ece81 100644 --- a/plugins/activity-assets/lang/en.json +++ b/plugins/activity-assets/lang/en.json @@ -9,6 +9,7 @@ "Changed": "changed", "To": "to", "Unset": "unset", - "System": "System" + "System": "System", + "CollectionUpdated": "Update {collection}" } } \ No newline at end of file diff --git a/plugins/activity-assets/lang/ru.json b/plugins/activity-assets/lang/ru.json index 0b83745d5d..04b7d82ae8 100644 --- a/plugins/activity-assets/lang/ru.json +++ b/plugins/activity-assets/lang/ru.json @@ -9,6 +9,7 @@ "Changed": "изменил(а)", "To": "на", "Unset": "сбросил", - "System": "Система" + "System": "Система", + "CollectionUpdated": "Обновлена {collection}" } } \ No newline at end of file diff --git a/plugins/activity-resources/src/activity.ts b/plugins/activity-resources/src/activity.ts index c5c3ea54f6..082dadb62d 100644 --- a/plugins/activity-resources/src/activity.ts +++ b/plugins/activity-resources/src/activity.ts @@ -6,7 +6,6 @@ import core, { Client, Collection, Doc, - DocumentUpdate, Hierarchy, Ref, SortingOrder, @@ -32,7 +31,13 @@ export function activityKey (objectClass: Ref<Class<Doc>>, txClass: Ref<Class<Tx return objectClass + ':' + txClass } -function isEqualOps (op1: DocumentUpdate<Doc>, op2: DocumentUpdate<Doc>): boolean { +function isEqualOps (op1: any, op2: any): boolean { + if (typeof op1 === 'string' && typeof op2 === 'string') { + return op1 === op2 + } + if (typeof op1 !== typeof op2) { + return false + } const o1 = Object.keys(op1).sort().join('-') const o2 = Object.keys(op2).sort().join('-') return o1 === o2 @@ -47,7 +52,7 @@ export interface DisplayTx { tx: TxCUD<Doc> // A set of collapsed transactions. - txes: Array<TxCUD<Doc>> + txes: DisplayTx[] // type check for createTx createTx?: TxCreateDoc<Doc> @@ -81,7 +86,7 @@ const combineThreshold = 5 * 60 * 1000 * Allow to recieve a list of transactions and notify client about it. */ export interface Activity { - update: (object: Doc, listener: DisplayTxListener, sort: SortingOrder) => void + update: (object: Doc, listener: DisplayTxListener, sort: SortingOrder, editable: Map<Ref<Class<Doc>>, boolean>) => void } class ActivityImpl implements Activity { @@ -99,8 +104,8 @@ class ActivityImpl implements Activity { this.txQuery2 = createQuery() } - private notify (object: Doc, listener: DisplayTxListener, sort: SortingOrder): void { - this.combineTransactions(object, this.txes1, this.txes2).then( + private notify (object: Doc, listener: DisplayTxListener, sort: SortingOrder, editable: Map<Ref<Class<Doc>>, boolean>): void { + this.combineTransactions(object, this.txes1, this.txes2, editable).then( (result) => { const sorted = result.sort((a, b) => (a.tx.modifiedOn - b.tx.modifiedOn) * sort) listener(sorted) @@ -111,7 +116,7 @@ class ActivityImpl implements Activity { ) } - update (object: Doc, listener: DisplayTxListener, sort: SortingOrder): void { + update (object: Doc, listener: DisplayTxListener, sort: SortingOrder, editable: Map<Ref<Class<Doc>>, boolean>): void { let isAttached = false isAttached = this.client.getHierarchy().isDerived(object._class, core.class.AttachedDoc) @@ -128,7 +133,7 @@ class ActivityImpl implements Activity { }, (result) => { this.txes1 = result - this.notify(object, listener, sort) + this.notify(object, listener, sort, editable) }, { sort: { modifiedOn: SortingOrder.Descending } } ) @@ -141,13 +146,13 @@ class ActivityImpl implements Activity { }, (result) => { this.txes2 = result - this.notify(object, listener, sort) + this.notify(object, listener, sort, editable) }, { sort: { modifiedOn: SortingOrder.Descending } } ) } - async combineTransactions (object: Doc, txes1: Array<TxCUD<Doc>>, txes2: Array<TxCUD<Doc>>): Promise<DisplayTx[]> { + async combineTransactions (object: Doc, txes1: Array<TxCUD<Doc>>, txes2: Array<TxCUD<Doc>>, editable: Map<Ref<Class<Doc>>, boolean>): Promise<DisplayTx[]> { const hierarchy = this.client.getHierarchy() // We need to sort with with natural order, to build a proper doc values. @@ -163,7 +168,7 @@ class ActivityImpl implements Activity { // We do not need collection object updates, in main list of displayed transactions. if (this.isDisplayTxRequired(collectionCUD, updateCUD || mixinCUD, ntx, object)) { // Combine previous update transaction for same field and if same operation and time treshold is ok - results = this.integrateTxWithResults(results, result) + results = this.integrateTxWithResults(results, result, editable) this.updateRemovedState(result, results) } } @@ -248,7 +253,7 @@ class ActivityImpl implements Activity { // Ignore } } - collectionCUD = true + collectionCUD = (cltx.tx._class === core.class.TxUpdateDoc) || (cltx.tx._class === core.class.TxMixin) } let firstTx = parents.get(tx.objectId) const result: DisplayTx = newDisplayTx(tx, hierarchy) @@ -295,22 +300,21 @@ class ActivityImpl implements Activity { return false } - integrateTxWithResults (results: DisplayTx[], result: DisplayTx): DisplayTx[] { - const curUpdate: any = - result.tx._class === core.class.TxUpdateDoc - ? (result.tx as unknown as TxUpdateDoc<Doc>).operations - : (result.tx as unknown as TxMixin<Doc, Doc>).attributes + integrateTxWithResults (results: DisplayTx[], result: DisplayTx, editable: Map<Ref<Class<Doc>>, boolean>): DisplayTx[] { + const curUpdate: any = getCombineOpFromTx(result) + if (curUpdate === undefined || (result.doc !== undefined && editable.has(result.doc._class))) { + results.push(result) + return results + } const newResult = results.filter((prevTx) => { - if (this.isSameKindTx(prevTx, result, result.tx._class)) { - const prevUpdate: any = - prevTx.tx._class === core.class.TxUpdateDoc - ? (prevTx.tx as unknown as TxUpdateDoc<Doc>).operations - : (prevTx.tx as unknown as TxMixin<Doc, Doc>).attributes + const prevUpdate: any = getCombineOpFromTx(prevTx) + // If same tx or same collection + if (this.isSameKindTx(prevTx, result, result.tx._class) || (prevUpdate === curUpdate)) { if (result.tx.modifiedOn - prevTx.tx.modifiedOn < combineThreshold && isEqualOps(prevUpdate, curUpdate)) { // we have same keys, // Remember previous transactions - result.txes.push(...prevTx.txes, prevTx.tx) + result.txes.push(...prevTx.txes, prevTx) return false } } @@ -331,6 +335,20 @@ class ActivityImpl implements Activity { } } +function getCombineOpFromTx (result: DisplayTx): any { + let curUpdate: any + if (result.tx._class === core.class.TxUpdateDoc) { + curUpdate = (result.tx as unknown as TxUpdateDoc<Doc>).operations + } + if (result.tx._class === core.class.TxMixin) { + curUpdate = (result.tx as unknown as TxMixin<Doc, Doc>).attributes + } + if (result.collectionAttribute !== undefined) { + curUpdate = result.collectionAttribute.attributeOf + '.' + result.collectionAttribute.name + } + return curUpdate +} + export function newDisplayTx (tx: TxCUD<Doc>, hierarchy: Hierarchy): DisplayTx { const createTx = hierarchy.isDerived(tx._class, core.class.TxCreateDoc) ? (tx as TxCreateDoc<Doc>) : undefined return { diff --git a/plugins/activity-resources/src/components/Activity.svelte b/plugins/activity-resources/src/components/Activity.svelte index 58227a2210..ab7ac92ba3 100644 --- a/plugins/activity-resources/src/components/Activity.svelte +++ b/plugins/activity-resources/src/components/Activity.svelte @@ -16,7 +16,7 @@ <script lang="ts"> import activity, { TxViewlet } from '@anticrm/activity' import chunter from '@anticrm/chunter' - import { Doc, SortingOrder } from '@anticrm/core' + import { Class, Doc, Ref, SortingOrder } from '@anticrm/core' import { createQuery, getClient } from '@anticrm/presentation' import { Component, Grid, IconActivity, Label, Scroller } from '@anticrm/ui' import { ActivityKey, activityKey, DisplayTx, newActivity } from '../activity' @@ -33,20 +33,25 @@ const activityQuery = newActivity(client, attrs) + let viewlets: Map<ActivityKey, TxViewlet> + let editable: Map<Ref<Class<Doc>>, boolean> = new Map() + + const descriptors = createQuery() + $: descriptors.query(activity.class.TxViewlet, {}, (result) => { + viewlets = new Map(result.map((r) => [activityKey(r.objectClass, r.txClass), r])) + + editable = new Map(result.map(it => [it.objectClass, it.editable ?? false])) + }) + $: activityQuery.update( object, (result) => { txes = result }, - SortingOrder.Descending + SortingOrder.Descending, + editable ) - let viewlets: Map<ActivityKey, TxViewlet> - - const descriptors = createQuery() - $: descriptors.query(activity.class.TxViewlet, {}, (result) => { - viewlets = new Map(result.map((r) => [activityKey(r.objectClass, r.txClass), r])) - }) </script> {#if fullSize || transparent} diff --git a/plugins/activity-resources/src/components/TxView.svelte b/plugins/activity-resources/src/components/TxView.svelte index 03a6aa850f..9ceff3a6b6 100644 --- a/plugins/activity-resources/src/components/TxView.svelte +++ b/plugins/activity-resources/src/components/TxView.svelte @@ -15,15 +15,25 @@ --> <script lang="ts"> import type { TxViewlet } from '@anticrm/activity' - import activity from '../plugin' import contact, { EmployeeAccount, formatName } from '@anticrm/contact' import core, { AnyAttribute, Doc, getCurrentAccount, Ref } from '@anticrm/core' import { Asset, getResource } from '@anticrm/platform' import { getClient } from '@anticrm/presentation' - import { Component, Icon, IconEdit, IconMoreH, Label, Menu, ShowMore, showPopup, TimeSince } from '@anticrm/ui' + import { + Component, + Icon, IconEdit, + IconMoreH, + Label, + Menu, + ShowMore, + showPopup, + TimeSince + } from '@anticrm/ui' import type { AttributeModel } from '@anticrm/view' import { getActions } from '@anticrm/view-resources' import { ActivityKey, DisplayTx } from '../activity' + import activity from '../plugin' + import TxViewTx from './TxViewTx.svelte' import { getValue, TxDisplayViewlet, updateViewlet } from './utils' export let tx: DisplayTx @@ -50,12 +60,16 @@ const client = getClient() + function getProps (props: any, edit: boolean): any { + return { ...props, edit } + } + $: updateViewlet(client, viewlets, tx).then((result) => { if (result.id === tx.tx._id) { viewlet = result.viewlet model = result.model modelIcon = result.modelIcon - props = { ...result.props, edit } + props = getProps(result.props, edit) } }) @@ -76,7 +90,7 @@ icon: IconEdit, action: () => { edit = true - props = { ...props, edit } + props = getProps(props, edit) } }, ...actions.map((a) => ({ @@ -94,7 +108,7 @@ } const onCancelEdit = () => { edit = false - props = { ...props, edit } + props = getProps(props, edit) } function isMessageType (attr?: AnyAttribute): boolean { return attr?.type._class === core.class.TypeMarkup @@ -147,10 +161,7 @@ <Label label={viewlet.label} params={viewlet.labelParams ?? {}} /> </span> {#if viewlet.labelComponent} - <Component - is={viewlet.labelComponent} - {props} - /> + <Component is={viewlet.labelComponent} {props} /> {/if} </div> {/if} @@ -160,7 +171,11 @@ {#if value === null} <span class="lower"><Label label={activity.string.Unset} /> <Label label={m.label} /></span> {:else} - <span class="lower" class:flex-grow={hasMessageType}><Label label={activity.string.Changed} /> <Label label={m.label} /> <Label label={activity.string.To} /></span> + <span class="lower" class:flex-grow={hasMessageType} + ><Label label={activity.string.Changed} /> + <Label label={m.label} /> + <Label label={activity.string.To} /></span + > {#if hasMessageType} <div class="time"><TimeSince value={tx.tx.modifiedOn} /></div> {/if} @@ -180,9 +195,15 @@ {#each model as m} {#await getValue(client, m, tx.mixinTx.attributes) then value} {#if value === null} - <span><Label label={activity.string.Unset} /> <span class="lower"><Label label={m.label} /></span></span> + <span> + <Label label={activity.string.Unset} /> <span class="lower"><Label label={m.label} /></span> + </span> {:else} - <span><Label label={activity.string.Changed} /> <span class="lower"><Label label={m.label} /></span> <Label label={activity.string.To} /></span> + <span> + <Label label={activity.string.Changed} /> + <span class="lower"><Label label={m.label} /></span> + <Label label={activity.string.To} /> + </span> {#if isMessageType(m.attribute)} <div class="strong message emphasized"> <svelte:component this={m.presenter} {value} /> @@ -196,10 +217,18 @@ {/await} {/each} {:else if viewlet && viewlet.display === 'inline' && viewlet.component} - {#if typeof viewlet.component === 'string'} - <Component is={viewlet.component} {props} on:close={onCancelEdit} /> - {:else} - <svelte:component this={viewlet.component} {...props} on:close={onCancelEdit} /> + {#if tx.collectionAttribute !== undefined && tx.txes.length > 0} + <ShowMore ignore={edit}> + <div class="flex-row-center flex-grow flex-wrap"> + <TxViewTx {tx} {onCancelEdit} {edit} {viewlet}/> + </div> + </ShowMore> + {:else} + {#if typeof viewlet.component === 'string'} + <Component is={viewlet.component} {props} on:close={onCancelEdit} /> + {:else} + <svelte:component this={viewlet.component} {...props} on:close={onCancelEdit} /> + {/if} {/if} {/if} </div> @@ -211,10 +240,16 @@ {#if viewlet && viewlet.component && viewlet.display !== 'inline'} <div class={viewlet.display}> <ShowMore ignore={viewlet.display !== 'content' || edit}> - {#if typeof viewlet.component === 'string'} - <Component is={viewlet.component} {props} on:close={onCancelEdit} /> - {:else} - <svelte:component this={viewlet.component} {...props} on:close={onCancelEdit} /> + {#if tx.collectionAttribute !== undefined && tx.txes.length > 0} + <div class="flex-row-center flex-grow flex-wrap"> + <TxViewTx {tx} {onCancelEdit} {edit} {viewlet}/> + </div> + {:else} + {#if typeof viewlet.component === 'string'} + <Component is={viewlet.component} {props} on:close={onCancelEdit} /> + {:else} + <svelte:component this={viewlet.component} {...props} on:close={onCancelEdit} /> + {/if} {/if} </ShowMore> </div> diff --git a/plugins/activity-resources/src/components/TxViewTx.svelte b/plugins/activity-resources/src/components/TxViewTx.svelte new file mode 100644 index 0000000000..ecf4cc5b47 --- /dev/null +++ b/plugins/activity-resources/src/components/TxViewTx.svelte @@ -0,0 +1,43 @@ +<script lang="ts"> + import core, { Class, Doc, Ref } from '@anticrm/core' + import { Component, IconAdd, IconDelete } from '@anticrm/ui' + import { DisplayTx } from '../activity' + import { getDTxProps, TxDisplayViewlet } from './utils' + + export let tx: DisplayTx + export let viewlet: TxDisplayViewlet + export let edit: boolean + export let onCancelEdit: () => void + + function filterTx (dtx: DisplayTx[], _class: Ref<Class<Doc>>): DisplayTx[] { + return dtx.filter((it) => it.tx._class === _class) + } + function getProps (props: any, edit: boolean): any { + return { ...props, edit } + } +</script> + +{#each filterTx([tx, ...tx.txes], core.class.TxCreateDoc) as ctx, i} + {#if i === 0} + <div class='mr-2'> + <IconAdd size={'small'} /> + </div> + {/if} + {#if typeof viewlet?.component === 'string'} + <Component is={viewlet?.component} props={getProps(getDTxProps(ctx), edit)} on:close={onCancelEdit} /> + {:else} + <svelte:component this={viewlet?.component} {...getProps(getDTxProps(ctx), edit)} on:close={onCancelEdit} /> + {/if} +{/each} +{#each filterTx([tx, ...tx.txes], core.class.TxRemoveDoc) as ctx, i} + {#if i === 0} + <div class='mr-2'> + <IconDelete size={'small'} /> + </div> + {/if} + {#if typeof viewlet?.component === 'string'} + <Component is={viewlet?.component} props={getProps(getDTxProps(ctx), edit)} on:close={onCancelEdit} /> + {:else} + <svelte:component this={viewlet?.component} {...getProps(getDTxProps(ctx), edit)} on:close={onCancelEdit} /> + {/if} +{/each} diff --git a/plugins/activity-resources/src/components/utils.ts b/plugins/activity-resources/src/components/utils.ts index 14ffb9d816..4f00425a87 100644 --- a/plugins/activity-resources/src/components/utils.ts +++ b/plugins/activity-resources/src/components/utils.ts @@ -39,12 +39,16 @@ async function createPseudoViewlet ( display: 'inline', icon: docClass.icon ?? activity.icon.Activity, label: label, - labelParams: { _class: trLabel }, + labelParams: { _class: trLabel, collection: dtx.collectionAttribute?.label !== undefined ? await translate(dtx.collectionAttribute?.label, {}) : '' }, component: presenter.presenter } } } +export function getDTxProps (dtx: DisplayTx): any { + return { tx: dtx.tx, value: dtx.doc, dtx } +} + export async function updateViewlet ( client: TxOperations, viewlets: Map<ActivityKey, TxViewlet>, @@ -59,7 +63,7 @@ export async function updateViewlet ( const key = activityKey(dtx.tx.objectClass, dtx.tx._class) let viewlet: TxDisplayViewlet = viewlets.get(key) - const props = { tx: dtx.tx, value: dtx.doc, dtx } + const props = getDTxProps(dtx) let model: AttributeModel[] = [] let modelIcon: Asset | undefined @@ -84,14 +88,15 @@ async function checkInlineViewlets ( client: TxOperations, model: AttributeModel[] ): Promise<{ viewlet: TxDisplayViewlet, model: AttributeModel[] }> { - if (dtx.tx._class === core.class.TxCreateDoc) { + if (dtx.collectionAttribute !== undefined && dtx.txes.length > 0) { + // 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) + } 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, activity.string.DocCreated) - } - if (dtx.tx._class === core.class.TxRemoveDoc) { + } else if (dtx.tx._class === core.class.TxRemoveDoc) { viewlet = await createPseudoViewlet(client, dtx, activity.string.DocDeleted) - } - if (dtx.tx._class === core.class.TxUpdateDoc) { + } else if (dtx.tx._class === core.class.TxUpdateDoc) { model = await createUpdateModel(dtx, client, model) } return { viewlet, model } diff --git a/plugins/activity-resources/src/plugin.ts b/plugins/activity-resources/src/plugin.ts index 05d42ffcd5..fd4508585f 100644 --- a/plugins/activity-resources/src/plugin.ts +++ b/plugins/activity-resources/src/plugin.ts @@ -21,6 +21,7 @@ export default mergeIds(activityId, activity, { string: { DocCreated: '' as IntlString, DocDeleted: '' as IntlString, + CollectionUpdated: '' as IntlString, Changed: '' as IntlString, To: '' as IntlString, Unset: '' as IntlString,