Allow collection change display in activity (#1068)

Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
Andrey Sobolev 2022-02-28 11:32:44 +07:00 committed by GitHub
parent 101c8eb4c3
commit b1aa6c1132
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 171 additions and 78 deletions

View File

@ -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,

View File

@ -9,6 +9,7 @@
"Changed": "changed",
"To": "to",
"Unset": "unset",
"System": "System"
"System": "System",
"CollectionUpdated": "Update {collection}"
}
}

View File

@ -9,6 +9,7 @@
"Changed": "изменил(а)",
"To": "на",
"Unset": "сбросил",
"System": "Система"
"System": "Система",
"CollectionUpdated": "Обновлена {collection}"
}
}

View File

@ -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 {

View File

@ -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}

View File

@ -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>

View File

@ -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}

View File

@ -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 }

View File

@ -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,