diff --git a/common/config/rush/pnpm-lock.yaml b/common/config/rush/pnpm-lock.yaml index 928464ae94..0a14a2cd86 100644 --- a/common/config/rush/pnpm-lock.yaml +++ b/common/config/rush/pnpm-lock.yaml @@ -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 diff --git a/models/server-card/package.json b/models/server-card/package.json index e1888aed2a..afbe444681 100644 --- a/models/server-card/package.json +++ b/models/server-card/package.json @@ -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", diff --git a/models/server-card/src/index.ts b/models/server-card/src/index.ts index 32d1be5798..828acd381a 100644 --- a/models/server-card/src/index.ts +++ b/models/server-card/src/index.ts @@ -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: { diff --git a/plugins/process-resources/src/components/settings/ResultEditor.svelte b/plugins/process-resources/src/components/settings/ResultEditor.svelte index ef6e4666d8..35ea436716 100644 --- a/plugins/process-resources/src/components/settings/ResultEditor.svelte +++ b/plugins/process-resources/src/components/settings/ResultEditor.svelte @@ -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 diff --git a/plugins/process-resources/src/middleware.ts b/plugins/process-resources/src/middleware.ts index 6fcf26ee64..dcc1497bd3 100644 --- a/plugins/process-resources/src/middleware.ts +++ b/plugins/process-resources/src/middleware.ts @@ -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 } diff --git a/plugins/view-resources/src/components/ViewletSetting.svelte b/plugins/view-resources/src/components/ViewletSetting.svelte index 468f2fb23a..4ee033794b 100644 --- a/plugins/view-resources/src/components/ViewletSetting.svelte +++ b/plugins/view-resources/src/components/ViewletSetting.svelte @@ -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>, key: string): boolean { + return hierarchy.getAllAttributes(sourceClass).has(getAttributeKey(key)) + } + + function syncConfigOrder ( + sourceClass: Ref>, + previousSourceConfig: Array, + sourceConfig: Array, + targetConfig: Array + ): Array { + 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>() + 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 = [] + const usedIndexes = new Set() + 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, + config: Array + ): Promise { + 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, + sourceConfig: Array + ): Promise { + 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>, @@ -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) } } diff --git a/server-plugins/card-resources/src/index.ts b/server-plugins/card-resources/src/index.ts index 9a8658714d..e420b1e055 100644 --- a/server-plugins/card-resources/src/index.ts +++ b/server-plugins/card-resources/src/index.ts @@ -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>, key: string): boolean { + return control.hierarchy.getAllAttributes(sourceClass).has(getAttributeKey(key)) +} + +function syncViewletConfigOrder ( + control: TriggerControl, + sourceClass: Ref>, + 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() + 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() + 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[], control: TriggerControl): Promise { const attr = TxProcessor.createDoc2Doc(ctx[0]) if (control.hierarchy.isDerived(attr.attributeOf, card.class.Card)) { @@ -159,6 +229,47 @@ async function OnAttributeRemove (ctx: TxRemoveDoc[], control: Tri return [] } +async function OnViewletUpdate (ctx: TxUpdateDoc[], control: TriggerControl): Promise { + const updateTx = ctx[0] + if (updateTx.space === core.space.DerivedTx) return [] + if (!Array.isArray(updateTx.operations.config)) return [] + + const sourceViewlet = (await control.findAll(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(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[], control: TriggerControl): Promise { 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, diff --git a/server-plugins/card/src/index.ts b/server-plugins/card/src/index.ts index 0f31d72cf2..1e0bdca707 100644 --- a/server-plugins/card/src/index.ts +++ b/server-plugins/card/src/index.ts @@ -37,6 +37,7 @@ export default plugin(serverCardId, { trigger: { OnAttribute: '' as Resource, OnAttributeRemove: '' as Resource, + OnViewletUpdate: '' as Resource, OnMasterTagCreate: '' as Resource, OnTagRemove: '' as Resource, OnMasterTagRemove: '' as Resource, diff --git a/server-plugins/process-resources/src/functions.ts b/server-plugins/process-resources/src/functions.ts index e14b37d168..32868b62e6 100644 --- a/server-plugins/process-resources/src/functions.ts +++ b/server-plugins/process-resources/src/functions.ts @@ -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 })