From 21eaf38daa6ebe8de5ed42c5262f5916069ada90 Mon Sep 17 00:00:00 2001 From: Denis Bykhov Date: Sat, 15 Mar 2025 18:52:31 +0500 Subject: [PATCH] Fix master tag remove (#8239) --- models/card/src/index.ts | 6 +- models/server-card/src/index.ts | 14 +++- packages/core/src/hierarchy.ts | 16 ++++- packages/query/src/index.ts | 32 ++++----- .../src/components/EditCard.svelte | 20 +++++- .../card-resources/src/components/Main.svelte | 38 ++++++---- .../src/components/Navigator.svelte | 22 ++---- .../components/settings/GeneralSection.svelte | 8 ++- .../settings/ManageMasterTags.svelte | 12 +--- plugins/card-resources/src/utils.ts | 40 +++++------ plugins/card/src/index.ts | 4 +- .../src/components/RelationsEditor.svelte | 5 ++ server-plugins/card-resources/src/index.ts | 72 +++++++++++++------ server-plugins/card/src/index.ts | 1 + server/middleware/src/domainTx.ts | 3 +- server/middleware/src/triggers.ts | 13 ++-- server/postgres/src/storage.ts | 4 +- 17 files changed, 191 insertions(+), 119 deletions(-) diff --git a/models/card/src/index.ts b/models/card/src/index.ts index ac2883ded3..d5e83db772 100644 --- a/models/card/src/index.ts +++ b/models/card/src/index.ts @@ -59,7 +59,9 @@ import card from './plugin' export { cardId } from '@hcengineering/card' @Model(card.class.MasterTag, core.class.Class) -export class TMasterTag extends TClass implements MasterTag {} +export class TMasterTag extends TClass implements MasterTag { + removed?: boolean +} @Model(card.class.Tag, core.class.Mixin) export class TTag extends TMixin implements Tag {} @@ -120,7 +122,7 @@ export function createModel (builder: Builder): void { ) builder.mixin(card.types.File, card.class.MasterTag, setting.mixin.Editable, { - value: true + value: false }) builder.createDoc(view.class.Viewlet, core.space.Model, { diff --git a/models/server-card/src/index.ts b/models/server-card/src/index.ts index 9bfe6c7114..010088488f 100644 --- a/models/server-card/src/index.ts +++ b/models/server-card/src/index.ts @@ -42,11 +42,19 @@ export function createModel (builder: Builder): void { }) builder.createDoc(serverCore.class.Trigger, core.space.Model, { - trigger: serverCard.trigger.OnMasterTagRemove, - isAsync: true, + trigger: serverCard.trigger.OnTagRemove, txMatch: { _class: core.class.TxRemoveDoc, - objectClass: { $in: [card.class.MasterTag, card.class.Tag] } + objectClass: card.class.Tag + } + }) + + builder.createDoc(serverCore.class.Trigger, core.space.Model, { + trigger: serverCard.trigger.OnMasterTagRemove, + txMatch: { + _class: core.class.TxUpdateDoc, + objectClass: card.class.MasterTag, + 'operations.removed': true } }) diff --git a/packages/core/src/hierarchy.ts b/packages/core/src/hierarchy.ts index 6a7efb168b..778d154cf1 100644 --- a/packages/core/src/hierarchy.ts +++ b/packages/core/src/hierarchy.ts @@ -183,6 +183,14 @@ export class Hierarchy { return data } + findClass(_class: Ref>): Class | undefined { + const data = this.classifiers.get(_class) + if (data === undefined || this.isInterface(data)) { + return undefined + } + return data + } + hasClass(_class: Ref>): boolean { const data = this.classifiers.get(_class) @@ -214,14 +222,16 @@ export class Hierarchy { } public findDomain (_class: Ref>): Domain | undefined { - const klazz = this.getClass(_class) + const klazz = this.findClass(_class) + if (klazz === undefined) return if (klazz.domain !== undefined) { return klazz.domain } - let _klazz = klazz + let _klazz: Class | undefined = klazz while (_klazz.extends !== undefined) { - _klazz = this.getClass(_klazz.extends) + _klazz = this.findClass(_klazz.extends) + if (_klazz === undefined) return if (_klazz.domain !== undefined) { // Cache for next requests klazz.domain = _klazz.domain diff --git a/packages/query/src/index.ts b/packages/query/src/index.ts index d94fa7dde0..5d8a3600cd 100644 --- a/packages/query/src/index.ts +++ b/packages/query/src/index.ts @@ -1196,26 +1196,24 @@ export class LiveQuery implements WithTx, Client { private async handleDocRemove (q: Query, tx: TxRemoveDoc): Promise { const h = this.client.getHierarchy() - if (q.result instanceof Promise) { - q.result = await q.result - } - const index = q.result.getDocs().find((p) => p._id === tx.objectId && h.isDerived(p._class, tx.objectClass)) - if (index !== undefined) { - if ( - q.options?.limit !== undefined && - q.options.limit === q.result.length && - h.isDerived(q._class, tx.objectClass) - ) { - await this.refresh(q) - return + if (q._class === tx.objectClass || h.isDerived(q._class, tx.objectClass) || h.isDerived(tx.objectClass, q._class)) { + if (q.result instanceof Promise) { + q.result = await q.result } - q.result.delete(index._id) - this.refs.updateDocuments(q, [index], true) + const index = q.result.getDocs().find((p) => p._id === tx.objectId) + if (index !== undefined) { + if (q.options?.limit !== undefined && q.options.limit === q.result.length && q.query._id !== tx.objectId) { + await this.refresh(q) + return + } + q.result.delete(index._id) + this.refs.updateDocuments(q, [index], true) - if (q.options?.total === true) { - q.total-- + if (q.options?.total === true) { + q.total-- + } + await this.callback(q, true) } - await this.callback(q, true) } await this.handleDocRemoveLookup(q, tx) await this.handleDocRemoveRelation(q, tx) diff --git a/plugins/card-resources/src/components/EditCard.svelte b/plugins/card-resources/src/components/EditCard.svelte index 60fda234c3..81b6596f05 100644 --- a/plugins/card-resources/src/components/EditCard.svelte +++ b/plugins/card-resources/src/components/EditCard.svelte @@ -22,7 +22,15 @@ import { Panel } from '@hcengineering/panel' import { getResource } from '@hcengineering/platform' import { createQuery, getClient } from '@hcengineering/presentation' - import { Button, EditBox, FocusHandler, IconMoreH, createFocusManager } from '@hcengineering/ui' + import { + Button, + EditBox, + FocusHandler, + IconMoreH, + createFocusManager, + getCurrentLocation, + navigate + } from '@hcengineering/ui' import view from '@hcengineering/view' import { ParentsNavigator, RelationsEditor, getDocMixins, showMenu } from '@hcengineering/view-resources' import { createEventDispatcher, onDestroy, onMount } from 'svelte' @@ -70,8 +78,14 @@ $: _id !== undefined && query.query(card.class.Card, { _id }, async (result) => { - ;[doc] = result - title = doc?.title ?? '' + if (result.length > 0) { + ;[doc] = result + title = doc?.title ?? '' + } else { + const loc = getCurrentLocation() + loc.path.length = 3 + navigate(loc) + } }) $: canSave = title.trim().length > 0 diff --git a/plugins/card-resources/src/components/Main.svelte b/plugins/card-resources/src/components/Main.svelte index 16081e2a9e..c17748bc81 100644 --- a/plugins/card-resources/src/components/Main.svelte +++ b/plugins/card-resources/src/components/Main.svelte @@ -13,28 +13,38 @@ // limitations under the License. -->
@@ -95,6 +99,8 @@ }} />
- + {#if isEditable} + + {/if} diff --git a/plugins/card-resources/src/components/settings/ManageMasterTags.svelte b/plugins/card-resources/src/components/settings/ManageMasterTags.svelte index 7f03819caa..6d1f4dabf2 100644 --- a/plugins/card-resources/src/components/settings/ManageMasterTags.svelte +++ b/plugins/card-resources/src/components/settings/ManageMasterTags.svelte @@ -41,15 +41,9 @@ let tags: MasterTag[] = [] const tagsQuery = createQuery() - $: tagsQuery.query( - card.class.MasterTag, - { - _class: card.class.MasterTag - }, - (result) => { - tags = result.sort((a, b) => a.label.localeCompare(b.label)) - } - ) + $: tagsQuery.query(card.class.MasterTag, {}, (result) => { + tags = result.filter((p) => p.removed !== true).sort((a, b) => a.label.localeCompare(b.label)) + }) function selectProjectType (id: string): void { clearSettingsStore() diff --git a/plugins/card-resources/src/utils.ts b/plugins/card-resources/src/utils.ts index 9b4758b33e..b46cca4aab 100644 --- a/plugins/card-resources/src/utils.ts +++ b/plugins/card-resources/src/utils.ts @@ -39,30 +39,24 @@ import card from './plugin' export async function deleteMasterTag (tag: MasterTag | undefined, onDelete?: () => void): Promise { if (tag !== undefined) { const client = getClient() - const objects = await client.findOne(tag._id, {}) - if (objects !== undefined) { - if (tag._class === card.class.MasterTag) { - showPopup(MessageBox, { - label: card.string.DeleteMasterTag, - message: card.string.DeleteMasterTagConfirm, - action: async () => { - onDelete?.() - await client.remove(tag) - } - }) - } else { - showPopup(MessageBox, { - label: card.string.DeleteTag, - message: card.string.DeleteTagConfirm, - action: async () => { - onDelete?.() - await client.remove(tag) - } - }) - } + if (tag._class === card.class.MasterTag) { + showPopup(MessageBox, { + label: card.string.DeleteMasterTag, + message: card.string.DeleteMasterTagConfirm, + action: async () => { + onDelete?.() + await client.update(tag, { removed: true }) + } + }) } else { - onDelete?.() - await client.remove(tag) + showPopup(MessageBox, { + label: card.string.DeleteTag, + message: card.string.DeleteTagConfirm, + action: async () => { + onDelete?.() + await client.remove(tag) + } + }) } } } diff --git a/plugins/card/src/index.ts b/plugins/card/src/index.ts index dc704aa774..e4a41b5822 100644 --- a/plugins/card/src/index.ts +++ b/plugins/card/src/index.ts @@ -17,7 +17,9 @@ import type { AnyComponent } from '@hcengineering/ui' export * from './analytics' -export interface MasterTag extends Class {} +export interface MasterTag extends Class { + removed?: boolean +} export interface Tag extends MasterTag, Mixin {} diff --git a/plugins/view-resources/src/components/RelationsEditor.svelte b/plugins/view-resources/src/components/RelationsEditor.svelte index 7be498a3aa..737d6accc8 100644 --- a/plugins/view-resources/src/components/RelationsEditor.svelte +++ b/plugins/view-resources/src/components/RelationsEditor.svelte @@ -42,6 +42,11 @@ $: getAssociations(object) + const q = createQuery() + $: q.query(core.class.Association, {}, () => { + getAssociations(object) + }) + let relationsA: Record, Doc[]> = {} let relationsB: Record, Doc[]> = {} diff --git a/server-plugins/card-resources/src/index.ts b/server-plugins/card-resources/src/index.ts index a67b83d49b..264aaf565c 100644 --- a/server-plugins/card-resources/src/index.ts +++ b/server-plugins/card-resources/src/index.ts @@ -13,10 +13,9 @@ // limitations under the License. // -import card, { Card, DOMAIN_CARD, MasterTag, Tag } from '@hcengineering/card' +import card, { Card, MasterTag, Tag } from '@hcengineering/card' import core, { AnyAttribute, - Class, Data, Doc, Ref, @@ -27,8 +26,8 @@ import core, { TxUpdateDoc } from '@hcengineering/core' import { TriggerControl } from '@hcengineering/server-core' -import view from '@hcengineering/view' import setting from '@hcengineering/setting' +import view from '@hcengineering/view' async function OnAttribute (ctx: TxCreateDoc[], control: TriggerControl): Promise { const attr = TxProcessor.createDoc2Doc(ctx[0]) @@ -91,51 +90,83 @@ async function OnAttributeRemove (ctx: TxRemoveDoc[], control: Tri return [] } -async function OnMasterTagRemove (ctx: TxRemoveDoc[], control: TriggerControl): Promise { +async function OnMasterTagRemove (ctx: TxUpdateDoc[], control: TriggerControl): Promise { + const updateTx = ctx[0] + if (updateTx.operations.removed !== true) return [] + const res: Tx[] = [] + const desc = control.hierarchy.getDescendants(updateTx.objectId) + // should remove objects if masterTag + const cards = await control.findAll(control.ctx, updateTx.objectId, {}) + for (const doc of cards) { + res.push(control.txFactory.createTxRemoveDoc(card.class.Card, doc.space, doc._id)) + } + for (const des of desc) { + res.push(...(await removeTagRelations(control, des))) + } + for (const des of desc) { + if (des === updateTx.objectId) continue + const _class = control.hierarchy.findClass(des) + if (_class === undefined) continue + if (_class._class === card.class.MasterTag) { + res.push( + control.txFactory.createTxUpdateDoc(card.class.MasterTag, core.space.Model, des, { + removed: true + }) + ) + } else { + res.push(control.txFactory.createTxRemoveDoc(card.class.Tag, core.space.Model, des)) + } + } + + return res +} + +async function OnTagRemove (ctx: TxRemoveDoc[], control: TriggerControl): Promise { const removeTx = ctx[0] const removedTag = control.removedMap.get(removeTx.objectId) if (removedTag === undefined) return [] const res: Tx[] = [] - // should remove objects if masterTag - if (removedTag._class === card.class.MasterTag) { - const cards = await control.lowLevel.rawFindAll(DOMAIN_CARD, { _class: removedTag._id as Ref> }) - for (const card of cards) { - res.push(control.txFactory.createTxRemoveDoc(card._class, card.space, card._id)) - } + const desc = control.hierarchy.getDescendants(removeTx.objectId) + + for (const des of desc) { + if (des === removeTx.objectId) continue + res.push(control.txFactory.createTxRemoveDoc(card.class.Tag, core.space.Model, des)) } + + res.push(...(await removeTagRelations(control, removeTx.objectId))) + + return res +} + +async function removeTagRelations (control: TriggerControl, tag: Ref): Promise { + const res: Tx[] = [] const viewlets = await control.findAll(control.ctx, view.class.Viewlet, { - attachTo: removeTx.objectId + attachTo: tag }) for (const viewlet of viewlets) { res.push(control.txFactory.createTxRemoveDoc(viewlet._class, viewlet.space, viewlet._id)) } const attributes = control.modelDb.findAllSync(core.class.Attribute, { - attributeOf: removeTx.objectId + attributeOf: tag }) for (const attribute of attributes) { res.push(control.txFactory.createTxRemoveDoc(attribute._class, attribute.space, attribute._id)) } - const desc = control.hierarchy.getDescendants(removeTx.objectId) - for (const des of desc) { - if (des === removeTx.objectId) continue - res.push(control.txFactory.createTxRemoveDoc(card.class.MasterTag, core.space.Model, des)) - } const removedRelation = new Set() const relationsA = control.modelDb.findAllSync(core.class.Association, { - classA: removeTx.objectId + classA: tag }) for (const rel of relationsA) { removedRelation.add(rel._id) res.push(control.txFactory.createTxRemoveDoc(core.class.Association, core.space.Model, rel._id)) } const relationsB = control.modelDb.findAllSync(core.class.Association, { - classB: removeTx.objectId + classB: tag }) for (const rel of relationsB) { if (removedRelation.has(rel._id)) continue res.push(control.txFactory.createTxRemoveDoc(core.class.Association, core.space.Model, rel._id)) } - return res } @@ -328,6 +359,7 @@ export default async () => ({ OnAttributeRemove, OnMasterTagCreate, OnMasterTagRemove, + OnTagRemove, OnCardRemove, OnCardCreate, OnCardUpdate diff --git a/server-plugins/card/src/index.ts b/server-plugins/card/src/index.ts index 949b89cf66..a16853948a 100644 --- a/server-plugins/card/src/index.ts +++ b/server-plugins/card/src/index.ts @@ -30,6 +30,7 @@ export default plugin(serverCardId, { OnAttribute: '' as Resource, OnAttributeRemove: '' as Resource, OnMasterTagCreate: '' as Resource, + OnTagRemove: '' as Resource, OnMasterTagRemove: '' as Resource, OnCardCreate: '' as Resource, OnCardUpdate: '' as Resource, diff --git a/server/middleware/src/domainTx.ts b/server/middleware/src/domainTx.ts index f19f3acc57..1b01fd9edb 100644 --- a/server/middleware/src/domainTx.ts +++ b/server/middleware/src/domainTx.ts @@ -138,7 +138,8 @@ export class DomainTxMiddleware extends BaseMiddleware implements Middleware { ctx.error('Unsupported transaction', tx) continue } - const domain = this.context.hierarchy.getDomain(txCUD.objectClass) + const domain = this.context.hierarchy.findDomain(txCUD.objectClass) + if (domain === undefined) continue domains.add(domain) const adapterName = this.adapterManager.getAdapterName(domain) diff --git a/server/middleware/src/triggers.ts b/server/middleware/src/triggers.ts index 1fb46c31f8..4d5328b8fd 100644 --- a/server/middleware/src/triggers.ts +++ b/server/middleware/src/triggers.ts @@ -293,12 +293,15 @@ export class TriggersMiddleware extends BaseMiddleware implements Middleware { continue } result.push(...(await this.deleteClassCollections(ctx, object._class, rtx.objectId, findAll))) - const mixins = this.getMixins(object._class, object) - for (const mixin of mixins) { - result.push(...(await this.deleteClassCollections(ctx, mixin, rtx.objectId, findAll, object._class))) - } + const _class = this.context.hierarchy.findClass(object._class) + if (_class !== undefined) { + const mixins = this.getMixins(object._class, object) + for (const mixin of mixins) { + result.push(...(await this.deleteClassCollections(ctx, mixin, rtx.objectId, findAll, object._class))) + } - result.push(...(await this.deleteRelatedDocuments(ctx, object, findAll))) + result.push(...(await this.deleteRelatedDocuments(ctx, object, findAll))) + } } return result } diff --git a/server/postgres/src/storage.ts b/server/postgres/src/storage.ts index 9738994a6c..17a447661e 100644 --- a/server/postgres/src/storage.ts +++ b/server/postgres/src/storage.ts @@ -1458,7 +1458,9 @@ abstract class PostgresAdapterBase implements DbAdapter { } const isReverse = association[1] === -1 const _class = isReverse ? assoc.classA : assoc.classB - const tagetDomain = translateDomain(this.hierarchy.getDomain(_class)) + const domain = this.hierarchy.findDomain(_class) + if (domain === undefined) continue + const tagetDomain = translateDomain(domain) const keyA = isReverse ? 'docB' : 'docA' const keyB = isReverse ? 'docA' : 'docB' const wsId = vars.add(this.workspaceId, '::uuid')