mirror of
https://github.com/n8n-io/n8n.git
synced 2026-05-29 15:57:00 +02:00
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:
parent
b17960d2f9
commit
520ff6c1c9
|
|
@ -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 };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
})();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
@ -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 } } : {}),
|
||||
|
|
|
|||
|
|
@ -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' });
|
||||
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user