From 4ca2b973455acbca0ff6fcb911abeaa8eb3f25ae Mon Sep 17 00:00:00 2001 From: Denis Bykhov Date: Wed, 7 Dec 2022 17:48:55 +0600 Subject: [PATCH] Activity change attached fix (#2428) Signed-off-by: Denis Bykhov --- models/tracker/src/index.ts | 3 + models/tracker/src/plugin.ts | 3 +- plugins/activity-resources/src/activity.ts | 257 ++++++++++-------- .../src/components/TxView.svelte | 2 +- .../src/components/utils.ts | 57 ++-- .../src/components/NotificationView.svelte | 6 +- server/core/src/storage.ts | 29 +- 7 files changed, 201 insertions(+), 156 deletions(-) diff --git a/models/tracker/src/index.ts b/models/tracker/src/index.ts index beaf0b9954..9b2778c187 100644 --- a/models/tracker/src/index.ts +++ b/models/tracker/src/index.ts @@ -193,6 +193,9 @@ export function TypeReportedTime (): Type { @Model(tracker.class.Issue, core.class.AttachedDoc, DOMAIN_TRACKER) @UX(tracker.string.Issue, tracker.icon.Issue, tracker.string.Issue) export class TIssue extends TAttachedDoc implements Issue { + @Prop(TypeRef(tracker.class.Issue), tracker.string.Parent) + declare attachedTo: Ref + @Prop(TypeString(), tracker.string.Title) @Index(IndexKind.FullText) title!: string diff --git a/models/tracker/src/plugin.ts b/models/tracker/src/plugin.ts index ded36b4648..03450215b7 100644 --- a/models/tracker/src/plugin.ts +++ b/models/tracker/src/plugin.ts @@ -34,7 +34,8 @@ export default mergeIds(trackerId, tracker, { GotoProjects: '' as IntlString, GotoTrackerApplication: '' as IntlString, SearchIssue: '' as IntlString, - NewRelatedIssue: '' as IntlString + NewRelatedIssue: '' as IntlString, + Parent: '' as IntlString }, component: { // Required to pass build without errorsF diff --git a/plugins/activity-resources/src/activity.ts b/plugins/activity-resources/src/activity.ts index 9cb73d509c..e096053110 100644 --- a/plugins/activity-resources/src/activity.ts +++ b/plugins/activity-resources/src/activity.ts @@ -96,24 +96,29 @@ export interface Activity { } class ActivityImpl implements Activity { - private readonly txQuery1: LiveQuery - private readonly txQuery2: LiveQuery + private readonly ownTxQuery: LiveQuery + private readonly attachedTxQuery: LiveQuery + private readonly attachedChangeTxQuery: LiveQuery private readonly hiddenAttributes: Set private editable: Map>, boolean> | undefined - private txes1: Array> = [] - private txes2: Array> = [] + private ownTxes: Array> = [] + private attachedTxes: Array> = [] + private attacheChangedTxes: Array> = [] + private readonly hierarchy: Hierarchy constructor (readonly client: Client, attributes: Map) { + this.hierarchy = client.getHierarchy() this.hiddenAttributes = new Set( [...attributes.entries()].filter(([, value]) => value.hidden === true).map(([key]) => key) ) - this.txQuery1 = createQuery() - this.txQuery2 = createQuery() + this.ownTxQuery = createQuery() + this.attachedTxQuery = createQuery() + this.attachedChangeTxQuery = createQuery() } private notify (object: Doc, listener: DisplayTxListener, sort: SortingOrder): void { if (this.editable != null) { - this.combineTransactions(object, this.txes1, this.txes2, this.editable).then( + this.combineTransactions(object, this.ownTxes, this.attachedTxes, this.attacheChangedTxes, this.editable).then( (result) => { const sorted = result.sort((a, b) => (a.tx.modifiedOn - b.tx.modifiedOn) * sort) listener(sorted) @@ -128,11 +133,11 @@ class ActivityImpl implements Activity { update (object: Doc, listener: DisplayTxListener, sort: SortingOrder, editable: Map>, boolean>): void { let isAttached = false - isAttached = this.client.getHierarchy().isDerived(object._class, core.class.AttachedDoc) + isAttached = this.hierarchy.isDerived(object._class, core.class.AttachedDoc) this.editable = editable - this.txQuery1.query>( + this.ownTxQuery.query>( isAttached ? core.class.TxCollectionCUD : core.class.TxCUD, isAttached ? { 'tx.objectId': object._id as Ref } @@ -143,54 +148,106 @@ class ActivityImpl implements Activity { } }, (result) => { - this.txes1 = result + this.ownTxes = result this.notify(object, listener, sort) }, - { sort: { modifiedOn: SortingOrder.Descending } } + { sort: { modifiedOn: SortingOrder.Ascending } } ) - this.txQuery2.query>( + this.attachedTxQuery.query>( core.class.TxCollectionCUD, { objectId: object._id, 'tx._class': { $in: [core.class.TxCreateDoc, core.class.TxUpdateDoc, core.class.TxRemoveDoc] } }, (result) => { - this.txes2 = result + this.attachedTxes = result this.notify(object, listener, sort) }, - { sort: { modifiedOn: SortingOrder.Descending } } + { sort: { modifiedOn: SortingOrder.Ascending } } + ) + + this.attachedChangeTxQuery.query>( + core.class.TxCollectionCUD, + { + 'tx.operations.attachedTo': object._id, + 'tx._class': core.class.TxUpdateDoc + }, + (result) => { + this.attacheChangedTxes = result + this.notify(object, listener, sort) + }, + { sort: { modifiedOn: SortingOrder.Ascending } } ) // In case editable is changed this.notify(object, listener, sort) } async combineTransactions ( - object: Doc, - txes1: Array>, - txes2: Array>, + doc: Doc, + ownTxes: Array>, + attachedTxes: Array>, + attachedChangeTxes: Array>, editable: Map>, boolean> ): Promise { - const hierarchy = this.client.getHierarchy() - - // We need to sort with with natural order, to build a proper doc values. - const allTx = Array.from(txes1).concat(txes2).sort(this.sortByLastModified) - const txCUD: Array> = this.filterTxCUD(allTx, hierarchy) - const parents = new Map, DisplayTx>() - let results: DisplayTx[] = [] + let ownResults: DisplayTx[] = [] + let attachedResults: DisplayTx[] = [] - for (const tx of txCUD) { - const { collectionCUD, updateCUD, mixinCUD, result, tx: ntx } = this.createDisplayTx(tx, parents) - // We do not need collection object updates, in main list of displayed transactions. - if (this.isDisplayTxRequired(collectionCUD, updateCUD || mixinCUD, ntx, object)) { + for (const tx of ownTxes) { + if (!this.filterUpdateTx(tx)) continue + const result = this.createDisplayTx(tx, parents) + // Combine previous update transaction for same field and if same operation and time treshold is ok + ownResults = this.integrateTxWithResults(ownResults, result, editable) + this.updateRemovedState(result, ownResults) + } + + for (let tx of Array.from(attachedTxes) + .concat(attachedChangeTxes) + .sort((a, b) => a.modifiedOn - b.modifiedOn)) { + if (!this.filterUpdateTx(tx)) continue + const changeAttached = this.isChangeAttachedTx(tx) + if (changeAttached || this.isDisplayTxRequired(tx)) { + if (changeAttached) { + tx = await this.createFakeTx(doc, tx) + } + const result = this.createDisplayTx(tx, parents) // Combine previous update transaction for same field and if same operation and time treshold is ok - results = this.integrateTxWithResults(results, result, editable) - this.updateRemovedState(result, results) + attachedResults = this.integrateTxWithResults(attachedResults, result, editable) + this.updateRemovedState(result, attachedResults) } } - return Array.from(results) + return Array.from(ownResults).concat(attachedResults) + } + + private async createFakeTx ( + doc: Doc, + cltx: TxCollectionCUD + ): Promise> { + if (doc._id === cltx.objectId) { + cltx.tx._class = core.class.TxRemoveDoc + } else { + const createTx = await this.client.findOne(core.class.TxCollectionCUD, { + 'tx.objectId': cltx.tx.objectId, + 'tx._class': core.class.TxCreateDoc + }) + if (createTx !== undefined) { + cltx.tx = createTx.tx + cltx.tx.modifiedBy = cltx.modifiedBy + cltx.tx.modifiedOn = cltx.modifiedOn + } + } + return cltx + } + + private isChangeAttachedTx (cltx: TxCollectionCUD): boolean { + const tx = TxProcessor.extractTx(cltx) + if (this.hierarchy.isDerived(tx._class, core.class.TxUpdateDoc)) { + const utx = tx as TxUpdateDoc + return utx.operations.attachedTo !== undefined + } + return false } private updateRemovedState (result: DisplayTx, results: DisplayTx[]): void { @@ -204,73 +261,55 @@ class ActivityImpl implements Activity { } } - sortByLastModified (a: TxCUD, b: TxCUD): number { - return a.modifiedOn - b.modifiedOn - } - - isDisplayTxRequired (collectionCUD: boolean, cudOp: boolean, ntx: TxCUD, object: Doc): boolean { - return !(collectionCUD && cudOp) || ntx.objectId === object._id + private isDisplayTxRequired (cltx: TxCollectionCUD): boolean { + // Check if collection attribute is hidden + if (this.hiddenAttributes.has(cltx.collection)) { + return false + } + const tx = TxProcessor.extractTx(cltx) + if ([core.class.TxCreateDoc, core.class.TxRemoveDoc].includes(tx._class)) return true + return false } private readonly getUpdateTx = (tx: TxCUD): TxUpdateDoc | undefined => { - if (tx._class !== core.class.TxCollectionCUD) { - return undefined - } - - const colTx = tx as TxCollectionCUD - - if (colTx.tx._class !== core.class.TxUpdateDoc) { - return undefined - } - - return colTx.tx as TxUpdateDoc - } - - filterTxCUD (allTx: Array>, hierarchy: Hierarchy): Array> { - return allTx - .filter((tx) => hierarchy.isDerived(tx._class, core.class.TxCUD)) - .filter((tx) => { - const utx = this.getUpdateTx(tx) - - if (hierarchy.isDerived(tx._class, core.class.TxCollectionCUD)) { - // Check if collection attribute is hidden - const txColl = tx as TxCollectionCUD - if (this.hiddenAttributes.has(txColl.collection)) { - return false - } - } - - if (utx === undefined) { - return true - } - - const ops = Object.keys(utx.operations) - - if (ops.length > 1) { - return true - } - - return !this.hiddenAttributes.has(ops[0]) - }) - } - - createDisplayTx ( - tx: TxCUD, - parents: Map, DisplayTx> - ): { collectionCUD: boolean, updateCUD: boolean, mixinCUD: boolean, result: DisplayTx, tx: TxCUD } { - let collectionCUD = false - let updateCUD = false - let mixinCUD = false - const hierarchy = this.client.getHierarchy() - let collectionAttribute: Attribute> | undefined - if (hierarchy.isDerived(tx._class, core.class.TxCollectionCUD)) { + if (this.hierarchy.isDerived(tx._class, core.class.TxCollectionCUD)) { const cltx = tx as TxCollectionCUD - tx = getCollectionTx(cltx) + tx = TxProcessor.extractTx(cltx) as TxCUD + } + + if (tx._class !== core.class.TxUpdateDoc) { + return undefined + } + + return tx as TxUpdateDoc + } + + filterUpdateTx (tx: TxCUD): boolean { + const utx = this.getUpdateTx(tx) + + if (utx === undefined) { + return true + } + + const ops = Object.keys(utx.operations) + + if (ops.length > 1) { + return true + } + + return !this.hiddenAttributes.has(ops[0]) + } + + createDisplayTx (tx: TxCUD, parents: Map, DisplayTx>): DisplayTx { + let collectionAttribute: Attribute> | undefined + if (this.hierarchy.isDerived(tx._class, core.class.TxCollectionCUD)) { + const cltx = tx as TxCollectionCUD + tx = TxProcessor.extractTx(cltx) as TxCUD // Check mixin classes for desired attribute - for (const cl of hierarchy.getDescendants(cltx.objectClass)) { + for (const cl of this.hierarchy.getDescendants(cltx.objectClass)) { try { - collectionAttribute = hierarchy.getAttribute(cl, cltx.collection) as Attribute> + collectionAttribute = this.hierarchy.getAttribute(cl, cltx.collection) as Attribute> if (collectionAttribute !== undefined) { break } @@ -278,10 +317,9 @@ class ActivityImpl implements Activity { // Ignore } } - 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) + const result: DisplayTx = newDisplayTx(tx, this.hierarchy) result.collectionAttribute = collectionAttribute result.doc = firstTx?.doc ?? result.doc @@ -290,22 +328,22 @@ class ActivityImpl implements Activity { parents.set(tx.objectId, firstTx) // If we have updates also apply them all. - updateCUD = this.checkUpdateState(result, firstTx) - mixinCUD = this.checkMixinState(result, firstTx) + this.checkUpdateState(result, firstTx) + this.checkMixinState(result, firstTx) - this.checkRemoveState(hierarchy, tx, firstTx, result) - return { collectionCUD, updateCUD, mixinCUD, result, tx } + this.checkRemoveState(tx, firstTx, result) + return result } - private checkRemoveState (hierarchy: Hierarchy, tx: TxCUD, firstTx: DisplayTx, result: DisplayTx): void { - if (hierarchy.isDerived(tx._class, core.class.TxRemoveDoc)) { + private checkRemoveState (tx: TxCUD, firstTx: DisplayTx, result: DisplayTx): void { + if (this.hierarchy.isDerived(tx._class, core.class.TxRemoveDoc)) { firstTx.removed = true result.removed = true } } checkUpdateState (result: DisplayTx, firstTx: DisplayTx): boolean { - if (this.client.getHierarchy().isDerived(result.tx._class, core.class.TxUpdateDoc) && result.doc !== undefined) { + if (this.hierarchy.isDerived(result.tx._class, core.class.TxUpdateDoc) && result.doc !== undefined) { firstTx.doc = TxProcessor.updateDoc2Doc(result.doc, result.tx as TxUpdateDoc) firstTx.updated = true result.updated = true @@ -315,7 +353,7 @@ class ActivityImpl implements Activity { } checkMixinState (result: DisplayTx, firstTx: DisplayTx): boolean { - if (this.client.getHierarchy().isDerived(result.tx._class, core.class.TxMixin) && result.doc !== undefined) { + if (this.hierarchy.isDerived(result.tx._class, core.class.TxMixin) && result.doc !== undefined) { const mix = result.tx as TxMixin firstTx.doc = TxProcessor.updateMixin4Doc(result.doc, mix) firstTx.mixin = true @@ -339,7 +377,7 @@ class ActivityImpl implements Activity { const newResult = results.filter((prevTx) => { const prevUpdate: any = getCombineOpFromTx(prevTx) // If same tx or same collection - if (this.isSameKindTx(prevTx, result, result.tx._class) || prevUpdate === curUpdate) { + if (this.isSameKindTx(prevTx, result) || prevUpdate === curUpdate) { if (result.tx.modifiedOn - prevTx.tx.modifiedOn < combineThreshold && isEqualOps(prevUpdate, curUpdate)) { // we have same keys, // Remember previous transactions @@ -363,11 +401,10 @@ class ActivityImpl implements Activity { return newResult } - isSameKindTx (prevTx: DisplayTx, result: DisplayTx, _class: Ref>): boolean { + isSameKindTx (prevTx: DisplayTx, result: DisplayTx): boolean { return ( prevTx.tx.objectId === result.tx.objectId && // Same document id prevTx.tx._class === result.tx._class && // Same transaction class - result.tx._class === _class && prevTx.tx.modifiedBy === result.tx.modifiedBy // Same user ) } @@ -402,18 +439,6 @@ export function newDisplayTx (tx: TxCUD, hierarchy: Hierarchy): DisplayTx { } } -export function getCollectionTx (cltx: TxCollectionCUD): TxCUD { - if (cltx.tx._class === core.class.TxCreateDoc) { - // We need to update tx to contain attachedDoc, attachedClass & collection - const create = cltx.tx as TxCreateDoc - create.attributes.attachedTo = cltx.objectId - create.attributes.attachedToClass = cltx.objectClass - create.attributes.collection = cltx.collection - return create - } - return cltx.tx -} - /** * Construct an new activity, to listend for displayed transactions in UI. * @param client diff --git a/plugins/activity-resources/src/components/TxView.svelte b/plugins/activity-resources/src/components/TxView.svelte index cd1776d96c..d5dbceb881 100644 --- a/plugins/activity-resources/src/components/TxView.svelte +++ b/plugins/activity-resources/src/components/TxView.svelte @@ -200,7 +200,7 @@ {#if viewlet === undefined && model.length > 0 && tx.updateTx} {#each model as m, i} {#await getValue(client, m, tx) then value} - {#if value.set === null} + {#if value.set === null || value.set === undefined} {:else if value.added.length} diff --git a/plugins/activity-resources/src/components/utils.ts b/plugins/activity-resources/src/components/utils.ts index d95d9827fd..6695a56608 100644 --- a/plugins/activity-resources/src/components/utils.ts +++ b/plugins/activity-resources/src/components/utils.ts @@ -192,19 +192,28 @@ function getModifiedAttributes (tx: DisplayTx): any[] { return [{}] } -async function buildRemovedDoc (client: TxOperations, objectId: Ref): Promise { - 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 - > - createTx = collectionTxes.find((p) => p.tx._class === core.class.TxCreateDoc) - } +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 - doc = TxProcessor.createDoc2Doc(createTx as TxCreateDoc) - for (const tx of txes) { + 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) { @@ -216,7 +225,8 @@ async function buildRemovedDoc (client: TxOperations, objectId: Ref): Promi } async function getAllRealValues (client: TxOperations, values: any[], _class: Ref>): Promise { - if (!client.getHierarchy().isDerived(_class, core.class.Doc) || values.some((value) => typeof value !== 'string')) { + if (values.length === 0) return [] + if (values.some((value) => typeof value !== 'string')) { return values } const realValues = await client.findAll(_class, { _id: { $in: values } }) @@ -226,21 +236,22 @@ async function getAllRealValues (client: TxOperations, values: any[], _class: Re ...(await Promise.all( values .filter((value) => !realValuesIds.includes(value)) - .map(async (value) => await buildRemovedDoc(client, value)) + .map(async (value) => await buildRemovedDoc(client, value, _class)) )) ].filter((v) => v != null) } -export async function getValue (client: TxOperations, m: AttributeModel, tx: DisplayTx): Promise { - 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] - ) +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) - } + ) + ).filter((v) => v != null) +} + +export async function getValue (client: TxOperations, m: AttributeModel, tx: DisplayTx): Promise { const utxs = getModifiedAttributes(tx) const value = { set: utxs[0][m.key], diff --git a/plugins/notification-resources/src/components/NotificationView.svelte b/plugins/notification-resources/src/components/NotificationView.svelte index 30d7cfab9b..07de538080 100644 --- a/plugins/notification-resources/src/components/NotificationView.svelte +++ b/plugins/notification-resources/src/components/NotificationView.svelte @@ -15,8 +15,8 @@ -->