Fix nested tags (#8474)
Some checks are pending
CI / build (push) Waiting to run
CI / svelte-check (push) Blocked by required conditions
CI / formatting (push) Blocked by required conditions
CI / test (push) Blocked by required conditions
CI / uitest (push) Waiting to run
CI / uitest-pg (push) Waiting to run
CI / uitest-qms (push) Waiting to run
CI / uitest-workspaces (push) Waiting to run
CI / docker-build (push) Blocked by required conditions
CI / dist-build (push) Blocked by required conditions

Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com>
This commit is contained in:
Denis Bykhov 2025-04-04 21:09:00 +05:00 committed by GitHub
parent 3c58f2b7f9
commit 099fb90c59
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 127 additions and 62 deletions

View File

@ -94,6 +94,15 @@ export function createModel (builder: Builder): void {
}
})
builder.createDoc(serverCore.class.Trigger, core.space.Model, {
trigger: serverCard.trigger.OnCardTag,
isAsync: true,
txMatch: {
_class: core.class.TxMixin,
objectClass: card.class.Card
}
})
builder.mixin(card.class.Card, core.class.Class, serverCore.mixin.SearchPresenter, {
searchIcon: card.icon.Card,
title: [['title']]

View File

@ -252,7 +252,7 @@ export class TxOperations implements Omit<Client, 'notify' | 'getConnection'> {
if (hierarchy.isMixin(mixClass)) {
const baseClass = hierarchy.getBaseClass(doc._class)
const byClass = this.splitMixinUpdate(update, mixClass, baseClass)
const byClass = splitMixinUpdate(hierarchy, update, mixClass, baseClass)
const ops = this.apply(doc._id)
for (const it of byClass) {
if (hierarchy.isMixin(it[0])) {
@ -323,17 +323,7 @@ export class TxOperations implements Omit<Client, 'notify' | 'getConnection'> {
date?: Timestamp,
account?: PersonId
): Promise<T> {
// We need to update fields if they are different.
const documentUpdate: DocumentUpdate<T> = {}
for (const [k, v] of Object.entries(update)) {
if (['_class', '_id', 'modifiedBy', 'modifiedOn', 'space', 'attachedTo', 'attachedToClass'].includes(k)) {
continue
}
const dv = (doc as any)[k]
if (!deepEqual(dv, v) && v !== undefined) {
;(documentUpdate as any)[k] = v
}
}
const documentUpdate = getDiffUpdate(doc, update)
if (Object.keys(documentUpdate).length > 0) {
await this.update(doc, documentUpdate, false, date ?? Date.now(), account)
TxProcessor.applyUpdate(doc, documentUpdate)
@ -372,56 +362,71 @@ export class TxOperations implements Omit<Client, 'notify' | 'getConnection'> {
}
return doc
}
}
private splitMixinUpdate<T extends Doc>(
update: DocumentUpdate<T>,
mixClass: Ref<Class<T>>,
baseClass: Ref<Class<T>>
): Map<Ref<Class<Doc>>, DocumentUpdate<T>> {
const hierarchy = this.getHierarchy()
const attributes = hierarchy.getAllAttributes(mixClass)
const updateAttrs = Object.fromEntries(
Object.entries(update).filter((it) => !it[0].startsWith('$'))
) as DocumentUpdate<T>
const updateOps = Object.fromEntries(
Object.entries(update).filter((it) => it[0].startsWith('$'))
) as DocumentUpdate<T>
const result: Map<Ref<Class<Doc>>, DocumentUpdate<T>> = this.splitObjectAttributes(
updateAttrs,
baseClass,
attributes
)
for (const [key, value] of Object.entries(updateOps)) {
const updates = this.splitObjectAttributes(value as object, baseClass, attributes)
for (const [opsClass, opsUpdate] of updates) {
const upd: DocumentUpdate<T> = result.get(opsClass) ?? {}
result.set(opsClass, { ...upd, [key]: opsUpdate })
}
export function getDiffUpdate<T extends Doc> (doc: T, update: T | Data<T> | DocumentUpdate<T>): DocumentUpdate<T> {
// We need to update fields if they are different.
const documentUpdate: DocumentUpdate<T> = {}
for (const [k, v] of Object.entries(update)) {
if (['_class', '_id', 'modifiedBy', 'modifiedOn', 'space', 'attachedTo', 'attachedToClass'].includes(k)) {
continue
}
const dv = (doc as any)[k]
if (!deepEqual(dv, v) && v !== undefined) {
;(documentUpdate as any)[k] = v
}
}
return documentUpdate
}
return result
export function splitMixinUpdate<T extends Doc> (
hierarchy: Hierarchy,
update: DocumentUpdate<T>,
mixClass: Ref<Class<T>>,
baseClass: Ref<Class<T>>
): Map<Ref<Class<Doc>>, DocumentUpdate<T>> {
const attributes = hierarchy.getAllAttributes(mixClass)
const updateAttrs = Object.fromEntries(
Object.entries(update).filter((it) => !it[0].startsWith('$'))
) as DocumentUpdate<T>
const updateOps = Object.fromEntries(
Object.entries(update).filter((it) => it[0].startsWith('$'))
) as DocumentUpdate<T>
const result: Map<Ref<Class<Doc>>, DocumentUpdate<T>> = splitObjectAttributes(
hierarchy,
updateAttrs,
baseClass,
attributes
)
for (const [key, value] of Object.entries(updateOps)) {
const updates = splitObjectAttributes(hierarchy, value as object, baseClass, attributes)
for (const [opsClass, opsUpdate] of updates) {
const upd: DocumentUpdate<T> = result.get(opsClass) ?? {}
result.set(opsClass, { ...upd, [key]: opsUpdate })
}
}
private splitObjectAttributes<T extends object>(
obj: T,
objClass: Ref<Class<Doc>>,
attributes: Map<string, AnyAttribute>
): Map<Ref<Class<Doc>>, object> {
const hierarchy = this.getHierarchy()
return result
}
const result = new Map<Ref<Class<Doc>>, any>()
for (const [key, value] of Object.entries(obj)) {
const attributeOf = attributes.get(key)?.attributeOf
const clazz = attributeOf !== undefined && hierarchy.isMixin(attributeOf) ? attributeOf : objClass
result.set(clazz, { ...(result.get(clazz) ?? {}), [key]: value })
}
return result
function splitObjectAttributes<T extends object> (
hierarchy: Hierarchy,
obj: T,
objClass: Ref<Class<Doc>>,
attributes: Map<string, AnyAttribute>
): Map<Ref<Class<Doc>>, object> {
const result = new Map<Ref<Class<Doc>>, any>()
for (const [key, value] of Object.entries(obj)) {
const attributeOf = attributes.get(key)?.attributeOf
const clazz = attributeOf !== undefined && hierarchy.isMixin(attributeOf) ? attributeOf : objClass
result.set(clazz, { ...(result.get(clazz) ?? {}), [key]: value })
}
return result
}
export interface CommitResult {

View File

@ -16,7 +16,7 @@
-->
<script lang="ts">
import card, { Card, Tag } from '@hcengineering/card'
import { Class, Doc, fillDefaults, Ref } from '@hcengineering/core'
import { Class, Doc, Mixin, Ref } from '@hcengineering/core'
import { createQuery, getClient } from '@hcengineering/presentation'
import {
ButtonIcon,
@ -77,14 +77,17 @@
async (res) => {
if (res !== undefined) {
await client.createMixin(doc._id, doc._class, doc.space, res, {})
const updated = fillDefaults(hierarchy, hierarchy.clone(doc), res)
await client.diffUpdate(doc, updated)
}
}
)
}
let divScroll: HTMLElement
function isRemoveable (mixinId: Ref<Mixin<Doc>>, activeTags: Tag[]): boolean {
const desc = hierarchy.getDescendants(mixinId)
return !desc.some((p) => hierarchy.hasMixin(doc, p) && p !== mixinId)
}
</script>
<div class="container py-4 gap-2">
@ -94,9 +97,12 @@
<ScrollerBar gap={'none'} bind:scroller={divScroll}>
<div class="tags gap-2">
{#each activeTags as mixin}
<div class="tag no-word-wrap">
{@const removable = isRemoveable(mixin._id, activeTags)}
<div class="tag no-word-wrap" class:removable>
<Label label={mixin.label} />
<ButtonIcon icon={IconClose} size="extra-small" kind="tertiary" on:click={() => removeTag(mixin._id)} />
{#if removable}
<ButtonIcon icon={IconClose} size="extra-small" kind="tertiary" on:click={() => removeTag(mixin._id)} />
{/if}
</div>
{/each}
{#if dropdownItems.length > 0}
@ -114,7 +120,7 @@
align-items: center;
.tag {
padding: 0.25rem 0.25rem 0.25rem 0.5rem;
padding: 0.25rem 0.5rem;
height: 1.5rem;
border: 1px solid var(--theme-content-color);
@ -126,6 +132,10 @@
align-items: center;
justify-content: center;
gap: 0.25rem;
&.removable {
padding-right: 0.25rem;
}
}
}

View File

@ -18,9 +18,14 @@ import core, {
AnyAttribute,
Data,
Doc,
fillDefaults,
getDiffUpdate,
Mixin,
Ref,
splitMixinUpdate,
Tx,
TxCreateDoc,
TxMixin,
TxProcessor,
TxRemoveDoc,
TxUpdateDoc
@ -391,6 +396,40 @@ async function OnCardCreate (ctx: TxCreateDoc<Card>[], control: TriggerControl):
return res
}
export async function OnCardTag (ctx: TxMixin<Card, Card>[], control: TriggerControl): Promise<Tx[]> {
const res: Tx[] = []
for (const tx of ctx) {
if (tx.space === core.space.DerivedTx) continue
if (tx._class !== core.class.TxMixin) continue
const target = tx.mixin
const to = control.hierarchy.getBaseClass(target)
const ancestors = control.hierarchy.getAncestors(target).filter((p) => control.hierarchy.isDerived(p, to))
const mixinAncestors: Ref<Mixin<Doc>>[] = []
const doc = (await control.findAll(control.ctx, tx.objectClass, { _id: tx.objectId }))[0]
if (doc === undefined) continue
for (const anc of ancestors) {
if (anc === target) continue
if (control.hierarchy.hasMixin(doc, anc)) break
if (anc === to) break
mixinAncestors.unshift(anc)
}
for (const anc of mixinAncestors) {
res.push(control.txFactory.createTxMixin(doc._id, doc._class, doc.space, anc, {}))
}
const updated = fillDefaults(control.hierarchy, control.hierarchy.as(control.hierarchy.clone(doc), target), target)
const diff = getDiffUpdate(doc, updated)
const splitted = splitMixinUpdate(control.hierarchy, diff, target, doc._class)
for (const it of splitted) {
if (control.hierarchy.isMixin(it[0])) {
res.push(control.txFactory.createTxMixin(doc._id, doc._class, doc.space, it[0], it[1]))
} else {
res.push(control.txFactory.createTxUpdateDoc(it[0], doc.space, doc._id, it[1]))
}
}
}
return res
}
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
export default async () => ({
trigger: {
@ -401,6 +440,7 @@ export default async () => ({
OnTagRemove,
OnCardRemove,
OnCardCreate,
OnCardUpdate
OnCardUpdate,
OnCardTag
}
})

View File

@ -34,6 +34,7 @@ export default plugin(serverCardId, {
OnMasterTagRemove: '' as Resource<TriggerFunc>,
OnCardCreate: '' as Resource<TriggerFunc>,
OnCardUpdate: '' as Resource<TriggerFunc>,
OnCardTag: '' as Resource<TriggerFunc>,
OnCardRemove: '' as Resource<TriggerFunc>
}
})