From 520ff6c1c9834fd3939650ca4a70de8e73d1ede7 Mon Sep 17 00:00:00 2001 From: Alex Grozav Date: Fri, 27 Feb 2026 12:29:51 +0200 Subject: [PATCH] refactor(editor): Migrate workflow timestamps to document store (no-changelog) (#26292) Co-authored-by: Claude Haiku 4.5 --- .../app/composables/useCanvasOperations.ts | 2 - .../src/app/composables/useWorkflowHelpers.ts | 2 + .../src/app/composables/useWorkflowSaving.ts | 4 +- .../src/app/stores/workflowDocument.store.ts | 3 + .../useWorkflowDocumentTimestamps.test.ts | 136 ++++++++++++++++++ .../useWorkflowDocumentTimestamps.ts | 52 +++++++ .../src/app/stores/workflows.store.ts | 2 - .../ai/assistant/builder.store.test.ts | 9 +- .../features/ai/assistant/builder.store.ts | 8 +- 9 files changed, 209 insertions(+), 9 deletions(-) create mode 100644 packages/frontend/editor-ui/src/app/stores/workflowDocument/useWorkflowDocumentTimestamps.test.ts create mode 100644 packages/frontend/editor-ui/src/app/stores/workflowDocument/useWorkflowDocumentTimestamps.ts diff --git a/packages/frontend/editor-ui/src/app/composables/useCanvasOperations.ts b/packages/frontend/editor-ui/src/app/composables/useCanvasOperations.ts index c91da69f11e..7a3056dfe5d 100644 --- a/packages/frontend/editor-ui/src/app/composables/useCanvasOperations.ts +++ b/packages/frontend/editor-ui/src/app/composables/useCanvasOperations.ts @@ -2315,8 +2315,6 @@ export function useCanvasOperations() { workflowsStore.setNodes(data.nodes); workflowsStore.setConnections(data.connections); - workflowState.setWorkflowProperty('createdAt', data.createdAt); - workflowState.setWorkflowProperty('updatedAt', data.updatedAt); return { workflowDocumentStore }; } diff --git a/packages/frontend/editor-ui/src/app/composables/useWorkflowHelpers.ts b/packages/frontend/editor-ui/src/app/composables/useWorkflowHelpers.ts index 542a38b7026..f58c74bf7bf 100644 --- a/packages/frontend/editor-ui/src/app/composables/useWorkflowHelpers.ts +++ b/packages/frontend/editor-ui/src/app/composables/useWorkflowHelpers.ts @@ -1033,6 +1033,8 @@ export function useWorkflowHelpers() { activeVersion: workflowData.activeVersion ?? null, }); workflowDocumentStore.setPinData(workflowData.pinData ?? {}); + workflowDocumentStore.setCreatedAt(workflowData.createdAt); + workflowDocumentStore.setUpdatedAt(workflowData.updatedAt); workflowDocumentStore.setHomeProject(workflowData.homeProject ?? null); if (workflowData.checksum) { workflowDocumentStore.setChecksum(workflowData.checksum); diff --git a/packages/frontend/editor-ui/src/app/composables/useWorkflowSaving.ts b/packages/frontend/editor-ui/src/app/composables/useWorkflowSaving.ts index 54f993c35a0..56c1546b4d4 100644 --- a/packages/frontend/editor-ui/src/app/composables/useWorkflowSaving.ts +++ b/packages/frontend/editor-ui/src/app/composables/useWorkflowSaving.ts @@ -230,7 +230,7 @@ export function useWorkflowSaving({ name: null, description: null, }); - workflowState.setWorkflowProperty('updatedAt', workflowData.updatedAt); + workflowDocumentStore.setUpdatedAt(workflowData.updatedAt); // Only mark state clean if no new changes were made during the save if (uiStore.dirtyStateSetCount === dirtyCountBeforeSave) { @@ -483,7 +483,7 @@ export function useWorkflowSaving({ name: null, description: null, }); - workflowState.setWorkflowProperty('updatedAt', workflowData.updatedAt); + workflowDocumentStore.setUpdatedAt(workflowData.updatedAt); // Only update webhook IDs if we explicitly reset them if (resetWebhookUrls) { diff --git a/packages/frontend/editor-ui/src/app/stores/workflowDocument.store.ts b/packages/frontend/editor-ui/src/app/stores/workflowDocument.store.ts index fa8a244ebae..c2b41ec8da7 100644 --- a/packages/frontend/editor-ui/src/app/stores/workflowDocument.store.ts +++ b/packages/frontend/editor-ui/src/app/stores/workflowDocument.store.ts @@ -7,6 +7,7 @@ import { useWorkflowDocumentHomeProject } from './workflowDocument/useWorkflowDo import { useWorkflowDocumentChecksum } from './workflowDocument/useWorkflowDocumentChecksum'; import { useWorkflowDocumentPinData } from './workflowDocument/useWorkflowDocumentPinData'; import { useWorkflowDocumentTags } from './workflowDocument/useWorkflowDocumentTags'; +import { useWorkflowDocumentTimestamps } from './workflowDocument/useWorkflowDocumentTimestamps'; export { getPinDataSize, @@ -53,6 +54,7 @@ export function useWorkflowDocumentStore(id: WorkflowDocumentId) { const workflowDocumentChecksum = useWorkflowDocumentChecksum(); const workflowDocumentTags = useWorkflowDocumentTags(); const workflowDocumentPinData = useWorkflowDocumentPinData(); + const workflowDocumentTimestamps = useWorkflowDocumentTimestamps(); return { workflowId, @@ -62,6 +64,7 @@ export function useWorkflowDocumentStore(id: WorkflowDocumentId) { ...workflowDocumentChecksum, ...workflowDocumentTags, ...workflowDocumentPinData, + ...workflowDocumentTimestamps, }; })(); } diff --git a/packages/frontend/editor-ui/src/app/stores/workflowDocument/useWorkflowDocumentTimestamps.test.ts b/packages/frontend/editor-ui/src/app/stores/workflowDocument/useWorkflowDocumentTimestamps.test.ts new file mode 100644 index 00000000000..99942f48607 --- /dev/null +++ b/packages/frontend/editor-ui/src/app/stores/workflowDocument/useWorkflowDocumentTimestamps.test.ts @@ -0,0 +1,136 @@ +import { describe, it, expect, vi } from 'vitest'; +import { useWorkflowDocumentTimestamps } from './useWorkflowDocumentTimestamps'; + +function createTimestamps() { + return useWorkflowDocumentTimestamps(); +} + +describe('useWorkflowDocumentTimestamps', () => { + describe('initial state', () => { + it('should start with -1', () => { + const { createdAt, updatedAt } = createTimestamps(); + expect(createdAt.value).toBe(-1); + expect(updatedAt.value).toBe(-1); + }); + }); + + describe('setCreatedAt', () => { + it('should set createdAt and fire event hook', () => { + const { createdAt, setCreatedAt, onCreatedAtChange } = createTimestamps(); + const hookSpy = vi.fn(); + onCreatedAtChange(hookSpy); + + setCreatedAt('2024-01-01T00:00:00Z'); + + expect(createdAt.value).toBe('2024-01-01T00:00:00Z'); + expect(hookSpy).toHaveBeenCalledWith({ + action: 'update', + payload: { createdAt: '2024-01-01T00:00:00Z' }, + }); + }); + + it('should replace existing createdAt', () => { + const { createdAt, setCreatedAt } = createTimestamps(); + setCreatedAt('2024-01-01T00:00:00Z'); + + setCreatedAt('2024-06-15T12:00:00Z'); + + expect(createdAt.value).toBe('2024-06-15T12:00:00Z'); + }); + + it('should fire event hook on every call', () => { + const { setCreatedAt, onCreatedAtChange } = createTimestamps(); + const hookSpy = vi.fn(); + onCreatedAtChange(hookSpy); + + setCreatedAt('2024-01-01T00:00:00Z'); + setCreatedAt('2024-06-15T12:00:00Z'); + + expect(hookSpy).toHaveBeenCalledTimes(2); + }); + + it('should accept numeric timestamps', () => { + const { createdAt, setCreatedAt, onCreatedAtChange } = createTimestamps(); + const hookSpy = vi.fn(); + onCreatedAtChange(hookSpy); + + setCreatedAt(1704067200000); + + expect(createdAt.value).toBe(1704067200000); + expect(hookSpy).toHaveBeenCalledWith({ + action: 'update', + payload: { createdAt: 1704067200000 }, + }); + }); + + it('should not fire updatedAt event hook', () => { + const { setCreatedAt, onUpdatedAtChange } = createTimestamps(); + const hookSpy = vi.fn(); + onUpdatedAtChange(hookSpy); + + setCreatedAt('2024-01-01T00:00:00Z'); + + expect(hookSpy).not.toHaveBeenCalled(); + }); + }); + + describe('setUpdatedAt', () => { + it('should set updatedAt and fire event hook', () => { + const { updatedAt, setUpdatedAt, onUpdatedAtChange } = createTimestamps(); + const hookSpy = vi.fn(); + onUpdatedAtChange(hookSpy); + + setUpdatedAt('2024-01-01T00:00:00Z'); + + expect(updatedAt.value).toBe('2024-01-01T00:00:00Z'); + expect(hookSpy).toHaveBeenCalledWith({ + action: 'update', + payload: { updatedAt: '2024-01-01T00:00:00Z' }, + }); + }); + + it('should replace existing updatedAt', () => { + const { updatedAt, setUpdatedAt } = createTimestamps(); + setUpdatedAt('2024-01-01T00:00:00Z'); + + setUpdatedAt('2024-06-15T12:00:00Z'); + + expect(updatedAt.value).toBe('2024-06-15T12:00:00Z'); + }); + + it('should fire event hook on every call', () => { + const { setUpdatedAt, onUpdatedAtChange } = createTimestamps(); + const hookSpy = vi.fn(); + onUpdatedAtChange(hookSpy); + + setUpdatedAt('2024-01-01T00:00:00Z'); + setUpdatedAt('2024-06-15T12:00:00Z'); + + expect(hookSpy).toHaveBeenCalledTimes(2); + }); + + it('should accept numeric timestamps', () => { + const { updatedAt, setUpdatedAt, onUpdatedAtChange } = createTimestamps(); + const hookSpy = vi.fn(); + onUpdatedAtChange(hookSpy); + + setUpdatedAt(1704067200000); + + expect(updatedAt.value).toBe(1704067200000); + expect(hookSpy).toHaveBeenCalledWith({ + action: 'update', + payload: { updatedAt: 1704067200000 }, + }); + }); + + it('should not fire createdAt event hook', () => { + const { setUpdatedAt, onCreatedAtChange } = createTimestamps(); + const hookSpy = vi.fn(); + onCreatedAtChange(hookSpy); + + setUpdatedAt('2024-01-01T00:00:00Z'); + + expect(hookSpy).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/packages/frontend/editor-ui/src/app/stores/workflowDocument/useWorkflowDocumentTimestamps.ts b/packages/frontend/editor-ui/src/app/stores/workflowDocument/useWorkflowDocumentTimestamps.ts new file mode 100644 index 00000000000..15c00ccf1ba --- /dev/null +++ b/packages/frontend/editor-ui/src/app/stores/workflowDocument/useWorkflowDocumentTimestamps.ts @@ -0,0 +1,52 @@ +import { ref, readonly } from 'vue'; +import { createEventHook } from '@vueuse/core'; +import { CHANGE_ACTION } from './types'; +import type { ChangeAction, ChangeEvent } from './types'; + +export type TimestampValue = number | string; + +export type CreatedAtPayload = { + createdAt: TimestampValue; +}; + +export type UpdatedAtPayload = { + updatedAt: TimestampValue; +}; + +export type CreatedAtChangeEvent = ChangeEvent; +export type UpdatedAtChangeEvent = ChangeEvent; + +export function useWorkflowDocumentTimestamps() { + const createdAt = ref(-1); + const updatedAt = ref(-1); + + const onCreatedAtChange = createEventHook(); + const onUpdatedAtChange = createEventHook(); + + function applyCreatedAt(value: TimestampValue, action: ChangeAction = CHANGE_ACTION.UPDATE) { + createdAt.value = value; + void onCreatedAtChange.trigger({ action, payload: { createdAt: value } }); + } + + function applyUpdatedAt(value: TimestampValue, action: ChangeAction = CHANGE_ACTION.UPDATE) { + updatedAt.value = value; + void onUpdatedAtChange.trigger({ action, payload: { updatedAt: value } }); + } + + function setCreatedAt(value: TimestampValue) { + applyCreatedAt(value); + } + + function setUpdatedAt(value: TimestampValue) { + applyUpdatedAt(value); + } + + return { + createdAt: readonly(createdAt), + updatedAt: readonly(updatedAt), + setCreatedAt, + setUpdatedAt, + onCreatedAtChange: onCreatedAtChange.on, + onUpdatedAtChange: onUpdatedAtChange.on, + }; +} diff --git a/packages/frontend/editor-ui/src/app/stores/workflows.store.ts b/packages/frontend/editor-ui/src/app/stores/workflows.store.ts index 1dbe3e83ec7..6af60de6b1f 100644 --- a/packages/frontend/editor-ui/src/app/stores/workflows.store.ts +++ b/packages/frontend/editor-ui/src/app/stores/workflows.store.ts @@ -833,8 +833,6 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => { ...value, ...(!value.hasOwnProperty('active') ? { active: false } : {}), ...(!value.hasOwnProperty('connections') ? { connections: {} } : {}), - ...(!value.hasOwnProperty('createdAt') ? { createdAt: -1 } : {}), - ...(!value.hasOwnProperty('updatedAt') ? { updatedAt: -1 } : {}), ...(!value.hasOwnProperty('id') ? { id: '' } : {}), ...(!value.hasOwnProperty('nodes') ? { nodes: [] } : {}), ...(!value.hasOwnProperty('settings') ? { settings: { ...defaults.settings } } : {}), diff --git a/packages/frontend/editor-ui/src/features/ai/assistant/builder.store.test.ts b/packages/frontend/editor-ui/src/features/ai/assistant/builder.store.test.ts index 05c4bcd9561..30fb004d02d 100644 --- a/packages/frontend/editor-ui/src/features/ai/assistant/builder.store.test.ts +++ b/packages/frontend/editor-ui/src/features/ai/assistant/builder.store.test.ts @@ -36,6 +36,10 @@ import { useWorkflowsStore } from '@/app/stores/workflows.store'; import { useNodeTypesStore } from '@/app/stores/nodeTypes.store'; import { useCredentialsStore } from '@/features/credentials/credentials.store'; import { useUIStore } from '@/app/stores/ui.store'; +import { + useWorkflowDocumentStore, + createWorkflowDocumentId, +} from '@/app/stores/workflowDocument.store'; import { AI_BUILDER_PLAN_MODE_EXPERIMENT } from '@/app/constants/experiments'; // Mock useI18n to return the keys instead of translations @@ -2595,7 +2599,10 @@ describe('AI Builder store', () => { workflowsStore.workflowId = 'test-workflow-123'; workflowsStore.isNewWorkflow = false; workflowsStore.workflowVersionId = 'version-1'; - workflowsStore.workflow.updatedAt = '2024-01-01T00:00:00Z'; + const workflowDocumentStore = useWorkflowDocumentStore( + createWorkflowDocumentId(workflowsStore.workflowId), + ); + workflowDocumentStore.setUpdatedAt('2024-01-01T00:00:00Z'); await builderStore.sendChatMessage({ text: 'Build a workflow' }); diff --git a/packages/frontend/editor-ui/src/features/ai/assistant/builder.store.ts b/packages/frontend/editor-ui/src/features/ai/assistant/builder.store.ts index 4ab46b95d5f..8f39b8322f8 100644 --- a/packages/frontend/editor-ui/src/features/ai/assistant/builder.store.ts +++ b/packages/frontend/editor-ui/src/features/ai/assistant/builder.store.ts @@ -686,7 +686,10 @@ export const useBuilderStore = defineStore(STORES.BUILDER, () => { // Use workflow updatedAt as version timestamp // might not be the same as "version.createdAt" but close enough - const updatedAt = workflowsStore.workflow.updatedAt; + const workflowDocumentStore = useWorkflowDocumentStore( + createWorkflowDocumentId(workflowsStore.workflowId), + ); + const updatedAt = workflowDocumentStore.updatedAt; return { id: versionId, createdAt: typeof updatedAt === 'number' ? new Date(updatedAt).toISOString() : updatedAt, @@ -1168,7 +1171,8 @@ export const useBuilderStore = defineStore(STORES.BUILDER, () => { // version id is important to update, because otherwise the next time user saves, // "overwrite" prevention modal shows, because the version id on the FE would be out of sync with latest on the backend workflowState.setWorkflowProperty('versionId', updatedWorkflow.versionId); - workflowState.setWorkflowProperty('updatedAt', updatedWorkflow.updatedAt); + const workflowDocumentStore = useWorkflowDocumentStore(createWorkflowDocumentId(workflowId)); + workflowDocumentStore.setUpdatedAt(updatedWorkflow.updatedAt); // 2. Truncate messages in backend session (removes message with messageId and all after) await truncateBuilderMessages(