feat: add viewlet integration and update attribute handling in various components (#10957)

Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com>
This commit is contained in:
Denis Bykhov
2026-07-05 22:04:21 +05:00
committed by GitHub
parent 7f13ca3a0e
commit 5c48abda35
9 changed files with 240 additions and 15 deletions
+3
View File
@@ -10446,6 +10446,9 @@ importers:
'@hcengineering/server-notification':
specifier: workspace:^0.7.0
version: link:../../server-plugins/notification
'@hcengineering/view':
specifier: workspace:^0.7.0
version: link:../../plugins/view
devDependencies:
'@hcengineering/platform-rig':
specifier: workspace:^0.7.21
+1
View File
@@ -37,6 +37,7 @@
"@hcengineering/core": "workspace:^0.7.26",
"@hcengineering/model": "workspace:^0.7.17",
"@hcengineering/platform": "workspace:^0.7.20",
"@hcengineering/view": "workspace:^0.7.0",
"@hcengineering/card": "workspace:^0.7.0",
"@hcengineering/communication": "workspace:^0.7.0",
"@hcengineering/server-notification": "workspace:^0.7.0",
+11
View File
@@ -21,6 +21,7 @@ import serverCard from '@hcengineering/server-card'
import card from '@hcengineering/card'
import communication from '@hcengineering/communication'
import serverNotification from '@hcengineering/server-notification'
import view from '@hcengineering/view'
export { serverCardId } from '@hcengineering/server-card'
@@ -43,6 +44,16 @@ export function createModel (builder: Builder): void {
}
})
builder.createDoc(serverCore.class.Trigger, core.space.Model, {
trigger: serverCard.trigger.OnViewletUpdate,
isAsync: true,
txMatch: {
_class: core.class.TxUpdateDoc,
objectClass: view.class.Viewlet,
'operations.config': { $exists: true }
}
})
builder.createDoc(serverCore.class.Trigger, core.space.Model, {
trigger: serverCard.trigger.OnTagRemove,
txMatch: {
@@ -44,7 +44,7 @@
type
}
if (key !== undefined) {
const attr = client.getHierarchy().findAttribute(process.masterTag, key)
const attr = client.getModel().findAllSync(core.class.Attribute, { name: key })[0]
if (attr?.label !== undefined) {
name = await translate(attr.label, {})
result.name = name
+1 -2
View File
@@ -243,10 +243,9 @@ export class ProcessMiddleware extends BasePresentationMiddleware implements Pre
results = await Promise.all(
results.map(async (r) => {
if (r.key !== undefined) {
const h = this.client.getHierarchy()
const _process = this.client.getModel().findObject(execution.process)
if (_process !== undefined) {
const attr = h.findAttribute(_process.masterTag, r.key)
const attr = this.client.getModel().findAllSync(core.class.Attribute, { name: r.key })[0]
if (attr?.label !== undefined) {
const name = await translate(attr.label, {})
return { ...r, name }
@@ -252,6 +252,67 @@
return typeof value === 'string' ? value : value?.key
}
function getAttributeKey (key: string): string {
if (key.startsWith('$lookup.')) {
return key.slice('$lookup.'.length)
}
const dotIndex = key.lastIndexOf('.')
return dotIndex === -1 ? key : key.slice(dotIndex + 1)
}
function isSourceAttribute (sourceClass: Ref<Class<Doc>>, key: string): boolean {
return hierarchy.getAllAttributes(sourceClass).has(getAttributeKey(key))
}
function syncConfigOrder (
sourceClass: Ref<Class<Doc>>,
previousSourceConfig: Array<BuildModelKey | string>,
sourceConfig: Array<BuildModelKey | string>,
targetConfig: Array<BuildModelKey | string>
): Array<BuildModelKey | string> {
const sourceKeys = new Set(sourceConfig.map(getKey).filter((it): it is string => it !== undefined))
const previousSourceKeys = new Set(previousSourceConfig.map(getKey).filter((it): it is string => it !== undefined))
const targetByKey = new Map<string, Array<{ item: BuildModelKey | string, index: number }>>()
for (const [index, item] of targetConfig.entries()) {
const key = getKey(item)
if (key === undefined) continue
const items = targetByKey.get(key) ?? []
items.push({ item, index })
targetByKey.set(key, items)
}
const sourceItems: Array<BuildModelKey | string> = []
const usedIndexes = new Set<number>()
for (const sourceItem of sourceConfig) {
const key = getKey(sourceItem)
if (key === undefined) continue
const targetItem = targetByKey.get(key)?.shift()
sourceItems.push(targetItem?.item ?? sourceItem)
if (targetItem !== undefined) {
usedIndexes.add(targetItem.index)
}
}
const synced = [...sourceItems]
for (const [index, targetItem] of targetConfig.entries()) {
if (usedIndexes.has(index)) continue
const key = getKey(targetItem)
if (
key !== undefined &&
!sourceKeys.has(key) &&
(previousSourceKeys.has(key) || isSourceAttribute(sourceClass, key))
) {
continue
}
synced.splice(Math.min(index, synced.length), 0, targetItem)
}
return synced
}
function isExist (result: Config[], newValue: Config): boolean {
if (!isAttribute(newValue)) return false
const newValueKey = getKey(newValue.value)
@@ -391,6 +452,45 @@
return preference === undefined ? result : setStatus(result, preference)
}
async function upsertViewletPreference (
viewletId: Ref<Viewlet>,
config: Array<BuildModelKey | string>
): Promise<void> {
const preference = preferences.find((p) => p.attachedTo === viewletId)
if (preference !== undefined) {
if (!deepEqual(preference.config, config)) {
await client.update(preference, {
config
})
}
} else {
await client.createDoc(view.class.ViewletPreference, core.space.Workspace, {
attachedTo: viewletId,
config
})
}
}
async function syncChildViewletPreferences (
sourceViewlet: Viewlet,
previousSourceConfig: Array<BuildModelKey | string>,
sourceConfig: Array<BuildModelKey | string>
): Promise<void> {
const descendants = new Set(
hierarchy.getDescendants(sourceViewlet.attachTo).filter((it) => it !== sourceViewlet.attachTo)
)
for (const childViewlet of viewlets) {
if (!descendants.has(childViewlet.attachTo)) continue
const preference = preferences.find((p) => p.attachedTo === childViewlet._id)
const targetConfig = preference?.config ?? childViewlet.config
const config = syncConfigOrder(sourceViewlet.attachTo, previousSourceConfig, sourceConfig, targetConfig)
if (deepEqual(targetConfig, config)) continue
await upsertViewletPreference(childViewlet._id, config)
}
}
async function addAssociations (
result: Config[],
_class: Ref<Class<Doc>>,
@@ -437,16 +537,14 @@
}
return value
})
const preference = preferences.find((p) => p.attachedTo === viewletId)
if (preference !== undefined) {
await client.update(preference, {
config
})
} else {
await client.createDoc(view.class.ViewletPreference, core.space.Workspace, {
attachedTo: viewletId,
config
})
const selectedViewlet = viewlets.find((it) => it._id === viewletId)
const previousSourceConfig =
preferences.find((p) => p.attachedTo === viewletId)?.config ?? selectedViewlet?.config ?? []
await upsertViewletPreference(viewletId, config)
if (selectedViewlet !== undefined) {
await syncChildViewletPreferences(selectedViewlet, previousSourceConfig, config)
}
}
+113 -1
View File
@@ -61,9 +61,79 @@ import { getMetadata, translate } from '@hcengineering/platform'
import { getEmployee, getPersonSpaces } from '@hcengineering/server-contact'
import serverCore, { TriggerControl } from '@hcengineering/server-core'
import setting from '@hcengineering/setting'
import view from '@hcengineering/view'
import view, { type BuildModelKey, type Viewlet } from '@hcengineering/view'
import { workbenchId } from '@hcengineering/workbench'
type ViewletConfigItem = BuildModelKey | string
interface IndexedViewletConfigItem {
item: ViewletConfigItem
index: number
}
function getViewletConfigKey (item: ViewletConfigItem): string {
return typeof item === 'string' ? item : item.key
}
function getAttributeKey (key: string): string {
if (key.startsWith('$lookup.')) {
return key.slice('$lookup.'.length)
}
const dotIndex = key.lastIndexOf('.')
return dotIndex === -1 ? key : key.slice(dotIndex + 1)
}
function isSourceAttribute (control: TriggerControl, sourceClass: Ref<Class<Doc>>, key: string): boolean {
return control.hierarchy.getAllAttributes(sourceClass).has(getAttributeKey(key))
}
function syncViewletConfigOrder (
control: TriggerControl,
sourceClass: Ref<Class<Doc>>,
previousSourceConfig: ViewletConfigItem[],
sourceConfig: ViewletConfigItem[],
targetConfig: ViewletConfigItem[]
): ViewletConfigItem[] {
const sourceKeys = new Set(sourceConfig.map(getViewletConfigKey))
const previousSourceKeys = new Set(previousSourceConfig.map(getViewletConfigKey))
const targetByKey = new Map<string, IndexedViewletConfigItem[]>()
for (const [index, item] of targetConfig.entries()) {
const key = getViewletConfigKey(item)
const items = targetByKey.get(key) ?? []
items.push({ item, index })
targetByKey.set(key, items)
}
const sourceItems: ViewletConfigItem[] = []
const usedIndexes = new Set<number>()
for (const sourceItem of sourceConfig) {
const key = getViewletConfigKey(sourceItem)
const targetItem = targetByKey.get(key)?.shift()
const item = targetItem?.item ?? sourceItem
sourceItems.push(item)
if (targetItem !== undefined) {
usedIndexes.add(targetItem.index)
}
}
const synced = [...sourceItems]
for (const [index, targetItem] of targetConfig.entries()) {
if (usedIndexes.has(index)) continue
const key = getViewletConfigKey(targetItem)
if (!sourceKeys.has(key) && (previousSourceKeys.has(key) || isSourceAttribute(control, sourceClass, key))) {
continue
}
synced.splice(Math.min(index, synced.length), 0, targetItem)
}
return synced
}
function isConfigOrderChanged (current: ViewletConfigItem[], next: ViewletConfigItem[]): boolean {
return current.length !== next.length || current.some((item, index) => item !== next[index])
}
async function OnAttribute (ctx: TxCreateDoc<AnyAttribute>[], control: TriggerControl): Promise<Tx[]> {
const attr = TxProcessor.createDoc2Doc(ctx[0])
if (control.hierarchy.isDerived(attr.attributeOf, card.class.Card)) {
@@ -159,6 +229,47 @@ async function OnAttributeRemove (ctx: TxRemoveDoc<AnyAttribute>[], control: Tri
return []
}
async function OnViewletUpdate (ctx: TxUpdateDoc<Viewlet>[], control: TriggerControl): Promise<Tx[]> {
const updateTx = ctx[0]
if (updateTx.space === core.space.DerivedTx) return []
if (!Array.isArray(updateTx.operations.config)) return []
const sourceViewlet = (await control.findAll<Viewlet>(control.ctx, view.class.Viewlet, { _id: updateTx.objectId }))[0]
if (sourceViewlet === undefined) return []
if (!control.hierarchy.isDerived(sourceViewlet.attachTo, card.class.Card)) return []
const descendants = control.hierarchy
.getDescendants(sourceViewlet.attachTo)
.filter((it) => it !== sourceViewlet.attachTo)
if (descendants.length === 0) return []
const childViewlets = await control.findAll<Viewlet>(control.ctx, view.class.Viewlet, {
attachTo: { $in: descendants },
descriptor: sourceViewlet.descriptor,
variant: sourceViewlet.variant ?? { $exists: false }
})
const res: Tx[] = []
for (const childViewlet of childViewlets) {
const config = syncViewletConfigOrder(
control,
sourceViewlet.attachTo,
sourceViewlet.config,
updateTx.operations.config,
childViewlet.config
)
if (!isConfigOrderChanged(childViewlet.config, config)) continue
res.push(
control.txFactory.createTxUpdateDoc(childViewlet._class, childViewlet.space, childViewlet._id, {
config
})
)
}
return res
}
async function OnMasterTagRemove (ctx: TxUpdateDoc<MasterTag>[], control: TriggerControl): Promise<Tx[]> {
const updateTx = ctx[0]
if (updateTx.space === core.space.DerivedTx) return []
@@ -937,6 +1048,7 @@ export default async () => ({
trigger: {
OnAttribute,
OnAttributeRemove,
OnViewletUpdate,
OnMasterTagCreate,
OnMasterTagRemove,
OnTagRemove,
+1
View File
@@ -37,6 +37,7 @@ export default plugin(serverCardId, {
trigger: {
OnAttribute: '' as Resource<TriggerFunc>,
OnAttributeRemove: '' as Resource<TriggerFunc>,
OnViewletUpdate: '' as Resource<TriggerFunc>,
OnMasterTagCreate: '' as Resource<TriggerFunc>,
OnTagRemove: '' as Resource<TriggerFunc>,
OnMasterTagRemove: '' as Resource<TriggerFunc>,
@@ -804,7 +804,7 @@ export async function CreateToDo (
todoResults.push({
_id: generateId() as any as ContextId,
name: attr.name,
name: attr.label,
key: attr.name,
type: attr.type
})