Cards fixes (#8186)

This commit is contained in:
Denis Bykhov 2025-03-11 09:00:29 +05:00 committed by GitHub
parent 92273f548b
commit a89f75ae9f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 153 additions and 76 deletions

View File

@ -16,13 +16,12 @@ import {
CardEvents, CardEvents,
cardId, cardId,
DOMAIN_CARD, DOMAIN_CARD,
type ParentInfo,
type Card, type Card,
type MasterTag, type MasterTag,
type ParentInfo,
type Tag type Tag
} from '@hcengineering/card' } from '@hcengineering/card'
import chunter from '@hcengineering/chunter' import chunter from '@hcengineering/chunter'
import contact from '@hcengineering/contact'
import core, { import core, {
AccountRole, AccountRole,
DOMAIN_MODEL, DOMAIN_MODEL,
@ -368,8 +367,7 @@ export function createModel (builder: Builder): void {
name: 'tagrelation', name: 'tagrelation',
label: card.string.TagRelations, label: card.string.TagRelations,
icon: setting.icon.Relations, icon: setting.icon.Relations,
props: { _classes: [card.class.Card, contact.class.Contact], exclude: [] }, component: card.component.RelationSetting,
component: setting.component.RelationSetting,
group: 'settings-editor', group: 'settings-editor',
role: AccountRole.Maintainer, role: AccountRole.Maintainer,
order: 4501 order: 4501
@ -381,7 +379,7 @@ export function createModel (builder: Builder): void {
setting.class.SettingsCategory, setting.class.SettingsCategory,
core.space.Model, core.space.Model,
{ {
name: 'masterTags', name: 'types',
label: card.string.MasterTags, label: card.string.MasterTags,
icon: card.icon.Card, icon: card.icon.Card,
component: card.component.ManageMasterTagsContent, component: card.component.ManageMasterTagsContent,

View File

@ -77,12 +77,11 @@ export function createModel (builder: Builder): void {
}) })
builder.createDoc(serverCore.class.Trigger, core.space.Model, { builder.createDoc(serverCore.class.Trigger, core.space.Model, {
trigger: serverCard.trigger.OnCardParentChange, trigger: serverCard.trigger.OnCardUpdate,
isAsync: true, isAsync: true,
txMatch: { txMatch: {
_class: core.class.TxUpdateDoc, _class: core.class.TxUpdateDoc,
objectClass: card.class.Card, objectClass: card.class.Card
operations: { parent: { $exists: true } }
} }
}) })

View File

@ -5,10 +5,10 @@
"Cards": "Karten", "Cards": "Karten",
"Content": "Inhalt", "Content": "Inhalt",
"CreateCard": "Karte erstellen", "CreateCard": "Karte erstellen",
"CreateMasterTag": "Master-Tag erstellen", "CreateMasterTag": "Typ erstellen",
"CreateTag": "Tag erstellen", "CreateTag": "Tag erstellen",
"MasterTag": "Master-Tag", "MasterTag": "Typ",
"MasterTags": "Master-Tags", "MasterTags": "Typen",
"Parent": "Eltern", "Parent": "Eltern",
"Tags": "Tags", "Tags": "Tags",
"Tag": "Tag", "Tag": "Tag",
@ -18,8 +18,8 @@
"TagRelations": "Tag-Beziehungen", "TagRelations": "Tag-Beziehungen",
"DeleteTag": "Tag löschen", "DeleteTag": "Tag löschen",
"DeleteTagConfirm": "Möchten Sie diesen Tag wirklich löschen? Alle zugehörigen Eigenschaften werden gelöscht", "DeleteTagConfirm": "Möchten Sie diesen Tag wirklich löschen? Alle zugehörigen Eigenschaften werden gelöscht",
"DeleteMasterTag": "Master-Tag löschen", "DeleteMasterTag": "Typ löschen",
"DeleteMasterTagConfirm": "Möchten Sie diesen Master-Tag wirklich löschen? Alle mit diesem Master-Tag verbundenen Objekte werden gelöscht", "DeleteMasterTagConfirm": "Möchten Sie diesen Typ wirklich löschen? Alle mit diesem Typ verbundenen Objekte werden gelöscht",
"UnsetParent": "Elternteil entfernen", "UnsetParent": "Elternteil entfernen",
"SetParent": "Elternteil setzen", "SetParent": "Elternteil setzen",
"CreateChild": "Kind erstellen", "CreateChild": "Kind erstellen",

View File

@ -40,6 +40,9 @@
{:else if value} {:else if value}
{#if type === 'link'} {#if type === 'link'}
<div class="flex-row-center"> <div class="flex-row-center">
{#if showParent}
<ParentNamesPresenter {value} />
{/if}
<DocNavLink <DocNavLink
object={value} object={value}
{onClick} {onClick}
@ -58,9 +61,6 @@
{/if} {/if}
<span class="overflow-label" class:select-text={!noSelect} title={value?.title}> <span class="overflow-label" class:select-text={!noSelect} title={value?.title}>
{value.title} {value.title}
{#if showParent}
<ParentNamesPresenter {value} />
{/if}
<slot name="details" /> <slot name="details" />
</span> </span>
</span> </span>

View File

@ -74,7 +74,7 @@
ev.stopPropagation() ev.stopPropagation()
const loc = getCurrentResolvedLocation() const loc = getCurrentResolvedLocation()
loc.path[2] = settingId loc.path[2] = settingId
loc.path[3] = 'masterTags' loc.path[3] = 'types'
loc.path[4] = value._class loc.path[4] = value._class
loc.path.length = 5 loc.path.length = 5
loc.fragment = undefined loc.fragment = undefined

View File

@ -72,7 +72,7 @@
&:active { &:active {
color: var(--theme-content-color); color: var(--theme-content-color);
} }
&::before { &::after {
content: ''; content: '';
padding: 0 0.25rem; padding: 0 0.25rem;
} }

View File

@ -14,7 +14,7 @@
--> -->
<script lang="ts"> <script lang="ts">
import { Card } from '@hcengineering/card' import { Card } from '@hcengineering/card'
import { FindOptions, SortingOrder } from '@hcengineering/core' import { Doc, FindOptions, SortingOrder } from '@hcengineering/core'
import { ObjectPopup, getClient } from '@hcengineering/presentation' import { ObjectPopup, getClient } from '@hcengineering/presentation'
import { createEventDispatcher } from 'svelte' import { createEventDispatcher } from 'svelte'
import card from '../plugin' import card from '../plugin'
@ -48,6 +48,13 @@
$: selected = !Array.isArray(value) ? value.parent ?? undefined : undefined $: selected = !Array.isArray(value) ? value.parent ?? undefined : undefined
$: ignoreObjects = !Array.isArray(value) ? [value._id] : undefined $: ignoreObjects = !Array.isArray(value) ? [value._id] : undefined
$: cards = new Set(Array.isArray(value) ? value.map((p) => p._id) : [value._id])
const filter = (it: Doc): boolean => {
const card = it as Card
return !card.parentInfo.some((p) => cards.has(p._id))
}
</script> </script>
<ObjectPopup <ObjectPopup
@ -59,6 +66,7 @@
allowDeselect={true} allowDeselect={true}
placeholder={card.string.SetParent} placeholder={card.string.SetParent}
create={undefined} create={undefined}
{filter}
{ignoreObjects} {ignoreObjects}
shadows={true} shadows={true}
{width} {width}

View File

@ -81,7 +81,7 @@
ev.stopPropagation() ev.stopPropagation()
const loc = getCurrentResolvedLocation() const loc = getCurrentResolvedLocation()
loc.path[2] = settingId loc.path[2] = settingId
loc.path[3] = 'masterTags' loc.path[3] = 'types'
loc.path[4] = tag._id loc.path[4] = tag._id
loc.path.length = 5 loc.path.length = 5
loc.fragment = undefined loc.fragment = undefined

View File

@ -17,7 +17,7 @@
import { Class, Doc, Ref } from '@hcengineering/core' import { Class, Doc, Ref } from '@hcengineering/core'
import { getClient } from '@hcengineering/presentation' import { getClient } from '@hcengineering/presentation'
import { NavItem } from '@hcengineering/ui' import { NavItem } from '@hcengineering/ui'
import { NavLink, showMenu } from '@hcengineering/view-resources' import { NavLink } from '@hcengineering/view-resources'
import { createEventDispatcher } from 'svelte' import { createEventDispatcher } from 'svelte'
import card from '../plugin' import card from '../plugin'
@ -67,9 +67,6 @@
on:click={() => { on:click={() => {
dispatch('select', clazz._id) dispatch('select', clazz._id)
}} }}
on:contextmenu={(evt) => {
showMenu(evt, { object: clazz })
}}
/> />
</NavLink> </NavLink>
{#if (descendants.get(clazz._id)?.length ?? 0) > 0} {#if (descendants.get(clazz._id)?.length ?? 0) > 0}

View File

@ -0,0 +1,17 @@
<script lang="ts">
import contact from '@hcengineering/contact'
import { Class, Doc, Ref } from '@hcengineering/core'
import { getClient } from '@hcengineering/presentation'
import { CreateRelation } from '@hcengineering/setting-resources'
import card from '../../plugin'
export let aClass: Ref<Class<Doc>> | undefined = undefined
const client = getClient()
const hierarchy = client.getHierarchy()
const _classes = [...hierarchy.getDescendants(card.class.Card), contact.class.Contact].filter(
(c) => c !== card.class.Card
)
</script>
<CreateRelation {aClass} {_classes} exclude={[]} on:close />

View File

@ -35,7 +35,7 @@
onDestroy(resolvedLocationStore.subscribe(handleLocationChanged)) onDestroy(resolvedLocationStore.subscribe(handleLocationChanged))
function handleLocationChanged ({ path }: Location): void { function handleLocationChanged ({ path }: Location): void {
if (path[3] !== 'masterTags' || path[4] === undefined) { if (path[3] !== 'types' || path[4] === undefined) {
selectedTagId = undefined selectedTagId = undefined
} else { } else {
selectedTagId = path[4] as Ref<MasterTag> selectedTagId = path[4] as Ref<MasterTag>

View File

@ -0,0 +1,14 @@
<script lang="ts">
import { getClient } from '@hcengineering/presentation'
import { RelationSetting } from '@hcengineering/setting-resources'
import contact from '@hcengineering/contact'
import card from '../../plugin'
const client = getClient()
const hierarchy = client.getHierarchy()
const _classes = [...hierarchy.getDescendants(card.class.Card), contact.class.Contact].filter(
(c) => c !== card.class.Card
)
</script>
<RelationSetting {_classes} exclude={[]} />

View File

@ -13,8 +13,7 @@
// limitations under the License. // limitations under the License.
--> -->
<script lang="ts"> <script lang="ts">
import card, { MasterTag } from '@hcengineering/card' import { MasterTag } from '@hcengineering/card'
import contact from '@hcengineering/contact'
import core, { Association, Class, Doc, Ref } from '@hcengineering/core' import core, { Association, Class, Doc, Ref } from '@hcengineering/core'
import { IntlString } from '@hcengineering/platform' import { IntlString } from '@hcengineering/platform'
import { createQuery, getClient } from '@hcengineering/presentation' import { createQuery, getClient } from '@hcengineering/presentation'
@ -22,6 +21,7 @@
import { clearSettingsStore, settingsStore } from '@hcengineering/setting-resources' import { clearSettingsStore, settingsStore } from '@hcengineering/setting-resources'
import { ButtonIcon, Icon, IconAdd, Label, showPopup } from '@hcengineering/ui' import { ButtonIcon, Icon, IconAdd, Label, showPopup } from '@hcengineering/ui'
import { onDestroy } from 'svelte' import { onDestroy } from 'svelte'
import CreateRelation from './CreateRelation.svelte'
export let masterTag: MasterTag export let masterTag: MasterTag
@ -49,10 +49,8 @@
} }
function addRelation (): void { function addRelation (): void {
showPopup(setting.component.CreateRelation, { showPopup(CreateRelation, {
aClass: masterTag._id, aClass: masterTag._id
exclude: [],
_classes: [card.class.Card, contact.class.Contact]
}) })
} }

View File

@ -35,6 +35,7 @@ import TagsSection from './components/settings/TagsSection.svelte'
import RelationsSection from './components/settings/RelationsSection.svelte' import RelationsSection from './components/settings/RelationsSection.svelte'
import ChildsSection from './components/settings/ChildsSection.svelte' import ChildsSection from './components/settings/ChildsSection.svelte'
import SetParentActionPopup from './components/SetParentActionPopup.svelte' import SetParentActionPopup from './components/SetParentActionPopup.svelte'
import RelationSetting from './components/settings/RelationSetting.svelte'
export default async (): Promise<Resources> => ({ export default async (): Promise<Resources> => ({
component: { component: {
@ -51,7 +52,8 @@ export default async (): Promise<Resources> => ({
TagsSection, TagsSection,
RelationsSection, RelationsSection,
ChildsSection, ChildsSection,
SetParentActionPopup SetParentActionPopup,
RelationSetting
}, },
completion: { completion: {
CardQuery: queryCard CardQuery: queryCard

View File

@ -35,7 +35,8 @@ export default mergeIds(cardId, card, {
TagsSection: '' as AnyComponent, TagsSection: '' as AnyComponent,
ChildsSection: '' as AnyComponent, ChildsSection: '' as AnyComponent,
RelationsSection: '' as AnyComponent, RelationsSection: '' as AnyComponent,
SetParentActionPopup: '' as AnyComponent SetParentActionPopup: '' as AnyComponent,
RelationSetting: '' as AnyComponent
}, },
completion: { completion: {
CardQuery: '' as Resource<ObjectSearchFactory>, CardQuery: '' as Resource<ObjectSearchFactory>,

View File

@ -82,7 +82,9 @@
const clazz = hierarchy.getClass(key) const clazz = hierarchy.getClass(key)
result.push([ result.push([
{ id: key, label: clazz.label, icon: clazz.icon }, { id: key, label: clazz.label, icon: clazz.icon },
value.map((it) => ({ id: it._id, label: it.label, icon: it.icon })) value
.map((it) => ({ id: it._id, label: it.label, icon: it.icon }))
.sort((a, b) => a.label.localeCompare(b.label))
]) ])
} catch {} } catch {}
} }

View File

@ -75,7 +75,9 @@ export {
ClassSetting, ClassSetting,
filterDescendants, filterDescendants,
SpaceTypeGeneralSectionEditor, SpaceTypeGeneralSectionEditor,
ClassHierarchy ClassHierarchy,
RelationSetting,
CreateRelation
} }
async function DeleteMixin (object: Mixin<Class<Doc>>): Promise<void> { async function DeleteMixin (object: Mixin<Class<Doc>>): Promise<void> {

View File

@ -48,7 +48,7 @@
$: query.query( $: query.query(
view.class.Viewlet, view.class.Viewlet,
{ {
attachTo: _class attachTo: client.getHierarchy().getBaseClass(_class)
}, },
(res) => { (res) => {
viewlet = res[0] viewlet = res[0]

View File

@ -200,29 +200,18 @@ async function OnCardRemove (ctx: TxRemoveDoc<Card>[], control: TriggerControl):
return res return res
} }
async function OnCardParentChange (ctx: TxUpdateDoc<Card>[], control: TriggerControl): Promise<Tx[]> { async function OnCardUpdate (ctx: TxUpdateDoc<Card>[], control: TriggerControl): Promise<Tx[]> {
const updateTx = ctx[0] const updateTx = ctx[0]
if (updateTx.operations.parent === undefined) return []
const newParent = updateTx.operations.parent
const doc = (await control.findAll(control.ctx, card.class.Card, { _id: updateTx.objectId }))[0] const doc = (await control.findAll(control.ctx, card.class.Card, { _id: updateTx.objectId }))[0]
if (doc === undefined) return [] if (doc === undefined) return []
const oldParent = doc.parentInfo[doc.parentInfo.length - 1]?._id
const res: Tx[] = [] const res: Tx[] = []
if (oldParent != null) { if (updateTx.operations.parent !== undefined) {
const parent = (await control.findAll(control.ctx, card.class.Card, { _id: oldParent }))[0] const newParent = updateTx.operations.parent
if (parent !== undefined) { const oldParent = doc.parentInfo[doc.parentInfo.length - 1]?._id
res.push(
control.txFactory.createTxUpdateDoc(parent._class, parent.space, parent._id, {
$inc: {
children: -1
}
})
)
}
}
if (newParent != null) { if (newParent != null) {
const parent = (await control.findAll(control.ctx, card.class.Card, { _id: newParent }))[0] const parent = (await control.findAll(control.ctx, card.class.Card, { _id: newParent }))[0]
if (parent !== undefined) { if (parent !== undefined) {
if (parent.parentInfo.findIndex((p) => p._id === doc._id) === -1) {
res.push( res.push(
control.txFactory.createTxUpdateDoc(parent._class, parent.space, parent._id, { control.txFactory.createTxUpdateDoc(parent._class, parent.space, parent._id, {
$inc: { $inc: {
@ -242,12 +231,62 @@ async function OnCardParentChange (ctx: TxUpdateDoc<Card>[], control: TriggerCon
] ]
}) })
) )
} else {
// rollback
return [
control.txFactory.createTxUpdateDoc(doc._class, doc.space, doc._id, {
parent: oldParent ?? null
})
]
} }
} }
}
if (oldParent != null) {
const parent = (await control.findAll(control.ctx, card.class.Card, { _id: oldParent }))[0]
if (parent !== undefined) {
res.push(
control.txFactory.createTxUpdateDoc(parent._class, parent.space, parent._id, {
$inc: {
children: -1
}
})
)
}
}
}
if (updateTx.operations.title !== undefined) {
res.push(...(await updateParentInfoName(control, doc._id, updateTx.operations.title, doc._id)))
}
return res return res
} }
async function updateParentInfoName (
control: TriggerControl,
parent: Ref<Card>,
title: string,
originParent: Ref<Card>
): Promise<Tx[]> {
const res: Tx[] = []
const childs = await control.findAll(control.ctx, card.class.Card, { parent })
for (const child of childs) {
if (child._id === originParent) continue
const parentInfo = child.parentInfo
const index = parentInfo.findIndex((p) => p._id === parent)
if (index === -1) {
continue
}
parentInfo[index].title = title
res.push(
control.txFactory.createTxUpdateDoc(child._class, child.space, child._id, {
parentInfo
})
)
res.push(...(await updateParentInfoName(control, child._id, title, originParent)))
}
return res
}
async function OnCardCreate (ctx: TxCreateDoc<Card>[], control: TriggerControl): Promise<Tx[]> { async function OnCardCreate (ctx: TxCreateDoc<Card>[], control: TriggerControl): Promise<Tx[]> {
const createTx = ctx[0] const createTx = ctx[0]
const doc = TxProcessor.createDoc2Doc(createTx) const doc = TxProcessor.createDoc2Doc(createTx)
@ -277,6 +316,6 @@ export default async () => ({
OnMasterTagRemove, OnMasterTagRemove,
OnCardRemove, OnCardRemove,
OnCardCreate, OnCardCreate,
OnCardParentChange OnCardUpdate
} }
}) })

View File

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