refactor(editor): Migrate workflow timestamps to document store (no-changelog) (#26292)

Co-authored-by: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
Alex Grozav 2026-02-27 12:29:51 +02:00 committed by GitHub
parent b17960d2f9
commit 520ff6c1c9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 209 additions and 9 deletions

View File

@ -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 };
}

View File

@ -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);

View File

@ -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) {

View File

@ -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,
};
})();
}

View File

@ -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();
});
});
});

View File

@ -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<CreatedAtPayload>;
export type UpdatedAtChangeEvent = ChangeEvent<UpdatedAtPayload>;
export function useWorkflowDocumentTimestamps() {
const createdAt = ref<TimestampValue>(-1);
const updatedAt = ref<TimestampValue>(-1);
const onCreatedAtChange = createEventHook<CreatedAtChangeEvent>();
const onUpdatedAtChange = createEventHook<UpdatedAtChangeEvent>();
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,
};
}

View File

@ -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 } } : {}),

View File

@ -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' });

View File

@ -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(