From 33d8d500f346cfc2e93f582cb561d58ec42abedd Mon Sep 17 00:00:00 2001 From: Denis Bykhov Date: Sun, 1 Mar 2026 00:41:19 +0500 Subject: [PATCH] =?UTF-8?q?feat:=20Implement=20`FieldChangedRollback`=20to?= =?UTF-8?q?=20revert=20document=20updates=20in=20=E2=80=A6=20(#10572)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Denis Bykhov --- models/process/src/triggers.ts | 2 +- models/server-process/src/index.ts | 2 +- .../src/components/EnumArrayEditor.svelte | 2 + server-plugins/process-resources/src/index.ts | 5 +- .../process-resources/src/rollback.ts | 70 ++++++++++++++++++- server-plugins/process/src/index.ts | 3 +- 6 files changed, 78 insertions(+), 6 deletions(-) diff --git a/models/process/src/triggers.ts b/models/process/src/triggers.ts index a96bc53aab..5d783bfbdb 100644 --- a/models/process/src/triggers.ts +++ b/models/process/src/triggers.ts @@ -119,7 +119,7 @@ export function defineTriggers (builder: Builder): void { requiredParams: [], checkFunction: process.triggerCheck.FieldChangedCheck, init: false, - auto: true + auto: false }, process.trigger.WhenFieldChanges ) diff --git a/models/server-process/src/index.ts b/models/server-process/src/index.ts index 5311e13197..e893385ebd 100644 --- a/models/server-process/src/index.ts +++ b/models/server-process/src/index.ts @@ -66,7 +66,7 @@ export function createModel (builder: Builder): void { }) builder.mixin(process.trigger.WhenFieldChanges, process.class.Trigger, serverProcess.mixin.TriggerImpl, { - preventRollback: true, + rollbackFunc: serverProcess.rollbacks.FieldChangedRollback, // set to null for now serverCheckFunc: serverProcess.func.FieldChangedCheck }) diff --git a/plugins/view-resources/src/components/EnumArrayEditor.svelte b/plugins/view-resources/src/components/EnumArrayEditor.svelte index 58fa7b3d82..ae640c3e44 100644 --- a/plugins/view-resources/src/components/EnumArrayEditor.svelte +++ b/plugins/view-resources/src/components/EnumArrayEditor.svelte @@ -22,6 +22,7 @@ export let value: string[] = [] export let type: ArrOf export let onChange: (value: string[]) => void + export let readonly: boolean = false export let kind: ButtonKind = 'link' export let size: ButtonSize = 'large' @@ -55,6 +56,7 @@ {size} width={'100%'} multiselect + disabled={readonly} autoSelect={false} on:selected={(e) => { onChange(e.detail) diff --git a/server-plugins/process-resources/src/index.ts b/server-plugins/process-resources/src/index.ts index 3d4ad2fef6..33e4e27967 100644 --- a/server-plugins/process-resources/src/index.ts +++ b/server-plugins/process-resources/src/index.ts @@ -114,7 +114,7 @@ import { UnlockCard, UnlockSection } from './functions' -import { ToDoCancellRollback, ToDoCloseRollback } from './rollback' +import { FieldChangedRollback, ToDoCancellRollback, ToDoCloseRollback } from './rollback' async function putEventToQueue (value: Omit, control: TriggerControl): Promise { if (control.queue === undefined) return @@ -643,7 +643,8 @@ export default async () => ({ }, rollbacks: { ToDoCloseRollback, - ToDoCancellRollback + ToDoCancellRollback, + FieldChangedRollback }, trigger: { OnProcessRemove, diff --git a/server-plugins/process-resources/src/rollback.ts b/server-plugins/process-resources/src/rollback.ts index 925e200035..80e18019f8 100644 --- a/server-plugins/process-resources/src/rollback.ts +++ b/server-plugins/process-resources/src/rollback.ts @@ -13,7 +13,8 @@ // limitations under the License. // -import { Tx } from '@hcengineering/core' +import { Card } from '@hcengineering/card' +import { DocumentUpdate, Tx } from '@hcengineering/core' import { ProcessToDo } from '@hcengineering/process' import { ProcessControl } from '@hcengineering/server-process' @@ -37,3 +38,70 @@ export function ToDoCancellRollback (context: Record, control: Proc todo.modifiedBy ) } + +export function FieldChangedRollback (context: Record, control: ProcessControl): Tx | undefined { + const card = context.card as Card + const ops = context.operations as DocumentUpdate + if (card === undefined || ops === undefined) return + const antiOps: DocumentUpdate = invertUpdate(ops) + return control.client.txFactory.createTxUpdateDoc(card._class, card.space, card._id, antiOps) +} + +function invertUpdate (ops: Record): Record { + const antiOps: Record = {} + for (const [key, value] of Object.entries(ops)) { + if (key.startsWith('$')) { + const inverted = invertOperator(key, value) + if (inverted !== undefined) { + for (const [antiKey, antiValue] of Object.entries(inverted)) { + antiOps[antiKey] = { ...(antiOps[antiKey] ?? {}), ...antiValue } + } + } + } else { + antiOps.$unset = { ...(antiOps.$unset ?? {}), [key]: true } + } + } + return antiOps +} + +function invertOperator (name: string, op: any): Record | undefined { + switch (name) { + case '$inc': { + const result: Record = {} + for (const [key, val] of Object.entries(op)) { + result[key] = -(val as number) + } + return { $inc: result } + } + case '$push': { + const result: Record = {} + for (const [key, val] of Object.entries(op)) { + if (typeof val === 'object' && val !== null && '$each' in val) { + result[key] = { $in: (val as any).$each } + } else { + result[key] = val + } + } + return { $pull: result } + } + case '$pull': { + const result: Record = {} + for (const [key, val] of Object.entries(op)) { + if (typeof val === 'object' && val !== null && '$in' in val) { + result[key] = { $each: (val as any).$in } + } else { + result[key] = { $each: [val] } + } + } + return { $push: result } + } + case '$rename': { + const result: Record = {} + for (const [key, val] of Object.entries(op)) { + result[val as string] = key + } + return { $rename: result } + } + } + return undefined +} diff --git a/server-plugins/process/src/index.ts b/server-plugins/process/src/index.ts index 48ea82bb99..bf29fd9f2e 100644 --- a/server-plugins/process/src/index.ts +++ b/server-plugins/process/src/index.ts @@ -44,7 +44,8 @@ export default plugin(serverProcessId, { }, rollbacks: { ToDoCloseRollback: '' as Resource, - ToDoCancellRollback: '' as Resource + ToDoCancellRollback: '' as Resource, + FieldChangedRollback: '' as Resource }, func: { RunSubProcess: '' as Resource,