refactor(editor): Migrate workflow document store init (#30077)

This commit is contained in:
Suguru Inoue 2026-05-11 11:57:07 +02:00 committed by GitHub
parent 40ffbfa5ab
commit d5d290d706
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
93 changed files with 390 additions and 775 deletions

View File

@ -18,18 +18,13 @@ import {
} from '../constants';
import { N8nButton, N8nCheckbox, N8nText } from '@n8n/design-system';
import {
createWorkflowDocumentId,
useWorkflowDocumentStore,
} from '../stores/workflowDocument.store';
import { injectWorkflowDocumentStore } from '../stores/workflowDocument.store';
const checked = ref(false);
const executionsStore = useExecutionsStore();
const workflowsStore = useWorkflowsStore();
const workflowDocumentStore = computed(() =>
useWorkflowDocumentStore(createWorkflowDocumentId(workflowsStore.workflowId)),
);
const workflowDocumentStore = injectWorkflowDocumentStore();
const nodeTypesStore = useNodeTypesStore();
const uiStore = useUIStore();
const router = useRouter();

View File

@ -5,11 +5,7 @@ import { createEventBus } from '@n8n/utils/event-bus';
import Modal from './Modal.vue';
import { CHAT_EMBED_MODAL_KEY, CHAT_TRIGGER_NODE_TYPE, WEBHOOK_NODE_TYPE } from '../constants';
import { useRootStore } from '@n8n/stores/useRootStore';
import { useWorkflowsStore } from '@/app/stores/workflows.store';
import {
useWorkflowDocumentStore,
createWorkflowDocumentId,
} from '@/app/stores/workflowDocument.store';
import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store';
import HtmlEditor from '@/features/shared/editors/components/HtmlEditor/HtmlEditor.vue';
import JsEditor from '@/features/shared/editors/components/JsEditor/JsEditor.vue';
import { useI18n } from '@n8n/i18n';
@ -27,10 +23,7 @@ const props = withDefaults(
const i18n = useI18n();
const rootStore = useRootStore();
const workflowsStore = useWorkflowsStore();
const workflowDocumentStore = computed(() =>
useWorkflowDocumentStore(createWorkflowDocumentId(workflowsStore.workflowId)),
);
const workflowDocumentStore = injectWorkflowDocumentStore();
type ChatEmbedModalTabValue = 'cdn' | 'vue' | 'react' | 'other';
type ChatEmbedModalTab = {

View File

@ -9,7 +9,7 @@ import { useProjectsStore } from '@/features/collaboration/projects/projects.sto
import { useRouter } from 'vue-router';
import { NodeConnectionTypes } from 'n8n-workflow';
import { useNodeTypesStore } from '@/app/stores/nodeTypes.store';
import { nextTick } from 'vue';
import { nextTick, shallowRef } from 'vue';
import { createTestWorkflow } from '@/__tests__/mocks';
import { type MockedStore, mockedStore } from '@/__tests__/utils';
@ -27,6 +27,7 @@ const { mockWorkflowDocumentStore } = vi.hoisted(() => ({
vi.mock('@/app/stores/workflowDocument.store', () => ({
useWorkflowDocumentStore: vi.fn().mockReturnValue(mockWorkflowDocumentStore),
injectWorkflowDocumentStore: () => shallowRef(mockWorkflowDocumentStore),
createWorkflowDocumentId: vi.fn().mockReturnValue('test-id'),
}));

View File

@ -4,10 +4,7 @@ import { useRunWorkflow } from '@/app/composables/useRunWorkflow';
import { useTelemetry } from '@/app/composables/useTelemetry';
import { FROM_AI_PARAMETERS_MODAL_KEY } from '@/app/constants';
import { useWorkflowsStore } from '@/app/stores/workflows.store';
import {
useWorkflowDocumentStore,
createWorkflowDocumentId,
} from '@/app/stores/workflowDocument.store';
import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store';
import { useNDVStore } from '@/features/ndv/shared/ndv.store';
import type { FormFieldValueUpdate } from '@n8n/design-system';
import { N8nButton, N8nCallout, N8nFormInputs, N8nText } from '@n8n/design-system';
@ -41,9 +38,7 @@ const telemetry = useTelemetry();
const ndvStore = useNDVStore();
const modalBus = createEventBus();
const workflowsStore = useWorkflowsStore();
const workflowDocumentStore = computed(() =>
useWorkflowDocumentStore(createWorkflowDocumentId(workflowsStore.workflowId)),
);
const workflowDocumentStore = injectWorkflowDocumentStore();
const router = useRouter();
const { runWorkflow } = useRunWorkflow({ router });
const agentRequestStore = useAgentRequestStore();

View File

@ -26,19 +26,14 @@ import { useSettingsStore } from '@/app/stores/settings.store';
import type { INodeUi } from '@/Interface';
import type { IUsedCredential } from '@/features/credentials/credentials.types';
import WorkflowActivationErrorMessage from '@/app/components/WorkflowActivationErrorMessage.vue';
import {
useWorkflowDocumentStore,
createWorkflowDocumentId,
} from '@/app/stores/workflowDocument.store';
import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store';
import { generateVersionLabelFromId } from '@/features/workflows/workflowHistory/utils';
const modalBus = createEventBus();
const i18n = useI18n();
const workflowsStore = useWorkflowsStore();
const workflowDocumentStore = computed(() =>
useWorkflowDocumentStore(createWorkflowDocumentId(workflowsStore.workflowId)),
);
const workflowDocumentStore = injectWorkflowDocumentStore();
const credentialsStore = useCredentialsStore();
const settingsStore = useSettingsStore();
const { showMessage } = useToast();

View File

@ -43,10 +43,7 @@ import { getResourcePermissions } from '@n8n/permissions';
import { useI18n } from '@n8n/i18n';
import { useTelemetry } from '@/app/composables/useTelemetry';
import { useDebounce } from '@/app/composables/useDebounce';
import {
useWorkflowDocumentStore,
createWorkflowDocumentId,
} from '@/app/stores/workflowDocument.store';
import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store';
import { useMcp } from '@/features/ai/mcpAccess/composables/useMcp';
import { useGlobalLinkActions } from '@/app/composables/useGlobalLinkActions';
import { usePageRedirectionHelper } from '@/app/composables/usePageRedirectionHelper';
@ -83,11 +80,7 @@ const sourceControlStore = useSourceControlStore();
const collaborationStore = useCollaborationStore();
const workflowsStore = useWorkflowsStore();
const workflowsListStore = useWorkflowsListStore();
const workflowDocumentStore = computed(() => {
const wfId = workflowsStore.workflowId;
if (!wfId) return null;
return useWorkflowDocumentStore(createWorkflowDocumentId(wfId));
});
const workflowDocumentStore = injectWorkflowDocumentStore();
const workflowsEEStore = useWorkflowsEEStore();
const nodeCreatorStore = useNodeCreatorStore();
const posthogStore = usePostHog();
@ -192,7 +185,7 @@ const isMCPEnabled = computed(
const readOnlyEnv = computed(
() => sourceControlStore.preferences.branchReadOnly || collaborationStore.shouldBeReadOnly,
);
const workflowName = computed(() => workflowDocumentStore.value?.name ?? '');
const workflowName = computed(() => workflowDocumentStore.value.name);
const workflowId = computed(() => workflowsStore.workflowId);
const workflow = computed(() => workflowsListStore.getWorkflowById(workflowId.value));
const isSharingEnabled = computed(
@ -594,8 +587,8 @@ const saveSettings = async () => {
delete data.settings.maxExecutionTimeout;
isLoading.value = true;
data.versionId = workflowDocumentStore?.value?.versionId ?? '';
data.expectedChecksum = workflowDocumentStore?.value?.checksum;
data.versionId = workflowDocumentStore.value.versionId;
data.expectedChecksum = workflowDocumentStore.value.checksum;
try {
await workflowsStore.updateWorkflow(String(route.params.workflowId), data);
@ -610,10 +603,9 @@ const saveSettings = async () => {
Object.entries(workflowSettings.value).filter(([, value]) => value !== 'DEFAULT'),
);
const oldSettings = (workflowDocumentStore?.value?.getSettingsSnapshot() ??
{}) as IWorkflowSettings;
const oldSettings = workflowDocumentStore.value.getSettingsSnapshot() as IWorkflowSettings;
workflowDocumentStore?.value?.setSettings(localWorkflowSettings);
workflowDocumentStore.value.setSettings(localWorkflowSettings);
isLoading.value = false;
@ -739,8 +731,8 @@ onMounted(async () => {
});
}
const workflowSettingsData = (workflowDocumentStore?.value?.getSettingsSnapshot() ??
{}) as IWorkflowSettings;
const workflowSettingsData =
workflowDocumentStore.value.getSettingsSnapshot() as IWorkflowSettings;
if (workflowSettingsData.timeSavedMode === undefined) {
workflowSettingsData.timeSavedMode = 'fixed';

View File

@ -1,4 +1,4 @@
import { reactive } from 'vue';
import { reactive, shallowRef } from 'vue';
import { createTestingPinia } from '@pinia/testing';
import userEvent from '@testing-library/user-event';
import { waitFor } from '@testing-library/vue';
@ -30,6 +30,7 @@ const mockWorkflowDocumentState = reactive({
});
vi.mock('@/app/stores/workflowDocument.store', () => ({
useWorkflowDocumentStore: () => mockWorkflowDocumentState,
injectWorkflowDocumentStore: () => shallowRef(mockWorkflowDocumentState),
createWorkflowDocumentId: (id: string) => `${id}@latest`,
}));

View File

@ -1,20 +1,13 @@
import { computed, toValue, type MaybeRefOrGetter } from 'vue';
import { useI18n } from '@n8n/i18n';
import { useWorkflowsStore } from '@/app/stores/workflows.store';
import {
createWorkflowDocumentId,
useWorkflowDocumentStore,
} from '../stores/workflowDocument.store';
import { injectWorkflowDocumentStore } from '../stores/workflowDocument.store';
/**
* Composable for activation error helpers.
* Resolves a node ID to a formatted activation error message reactively.
*/
export function useActivationError(nodeId: MaybeRefOrGetter<string | undefined>) {
const workflowsStore = useWorkflowsStore();
const workflowDocumentStore = computed(() =>
useWorkflowDocumentStore(createWorkflowDocumentId(workflowsStore.workflowId)),
);
const workflowDocumentStore = injectWorkflowDocumentStore();
const i18n = useI18n();
const errorMessage = computed(() => {

View File

@ -138,9 +138,8 @@ import { useSetupPanelStore } from '@/features/setupPanel/setupPanel.store';
import { clearAllNodeResourceLocatorValues } from '@/features/workflows/templates/utils/templateTransforms';
import { useClipboard } from '@vueuse/core';
import {
useWorkflowDocumentStore,
createWorkflowDocumentId,
pinDataToExecutionData,
injectWorkflowDocumentStore,
} from '@/app/stores/workflowDocument.store';
import { serializeNode } from '@/app/utils/nodes/nodeTransforms';
@ -192,9 +191,7 @@ export function useCanvasOperations() {
const templatesStore = useTemplatesStore();
const focusPanelStore = useFocusPanelStore();
const setupPanelStore = useSetupPanelStore();
const workflowDocumentStore = computed(() =>
useWorkflowDocumentStore(createWorkflowDocumentId(workflowsStore.workflowId)),
);
const workflowDocumentStore = injectWorkflowDocumentStore();
const i18n = useI18n();
const toast = useToast();

View File

@ -13,14 +13,21 @@ import {
import { useWorkflowsStore } from '@/app/stores/workflows.store';
import type { JSONSchema7 } from 'json-schema';
import { mock } from 'vitest-mock-extended';
import { computed } from 'vue';
vi.mock('@/app/stores/workflows.store');
vi.mock('@/app/stores/workflowDocument.store', () => ({
createWorkflowDocumentId: vi.fn(() => 'test'),
useWorkflowDocumentStore: vi.fn(() => ({
getSettingsSnapshot: () => ({ binaryMode: undefined }),
})),
}));
vi.mock('@/app/stores/workflowDocument.store', async (importOriginal) => {
const actual = await importOriginal<Record<string, unknown>>();
return {
...actual,
injectWorkflowDocumentStore: vi.fn(() =>
computed(() => ({
getSettingsSnapshot: () => ({ binaryMode: undefined }),
getNodePinData: () => undefined,
})),
),
};
});
describe('useDataSchema', () => {
const getSchema = useDataSchema().getSchema;

View File

@ -8,10 +8,7 @@ import type {
SchemaType,
} from '@/Interface';
import { useWorkflowsStore } from '@/app/stores/workflows.store';
import {
useWorkflowDocumentStore,
createWorkflowDocumentId,
} from '@/app/stores/workflowDocument.store';
import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store';
import { generatePath, getNodeParentExpression } from '@/app/utils/mappingUtils';
import { isObject } from '@/app/utils/objectUtils';
import { isObj } from '@/app/utils/typeGuards';
@ -26,12 +23,14 @@ import {
type ITaskDataConnections,
NodeConnectionTypes,
} from 'n8n-workflow';
import { computed, ref } from 'vue';
import { ref } from 'vue';
import { type IconName } from '@n8n/design-system/components/N8nIcon/icons';
import { DATA_TYPE_ICON_MAP } from '@/app/constants';
import { DEFAULT_SETTINGS } from '../stores/workflowDocument/useWorkflowDocumentSettings';
export function useDataSchema() {
const workflowDocumentStore = injectWorkflowDocumentStore();
function getSchema(
input: Optional<Primitives | object>,
path = '',
@ -211,11 +210,9 @@ export function useDataSchema() {
): INodeExecutionData[] {
if (!node) return [];
const workflowsStore = useWorkflowsStore();
const workflowDocumentStore = useWorkflowDocumentStore(
createWorkflowDocumentId(workflowsStore.workflowId),
);
const pinnedData = workflowDocumentStore.getNodePinData(node.name)?.map((item) => item.json);
const pinnedData = workflowDocumentStore.value
.getNodePinData(node.name)
?.map((item) => item.json);
let inputData = getNodeInputData(node, runIndex, outputIndex);
if (pinnedData) {
@ -396,6 +393,7 @@ const isEmptySchema = (schema: Schema) => {
const prefixTitle = (title: string, prefix?: string) => (prefix ? `${prefix}[${title}]` : title);
export const useFlattenSchema = () => {
const workflowDocumentStore = injectWorkflowDocumentStore();
const closedNodes = ref<Set<string>>(new Set());
const toggleNode = (id: string) => {
if (closedNodes.value.has(id)) {
@ -555,11 +553,6 @@ export const useFlattenSchema = () => {
return acc;
}
const workflowsStore = useWorkflowsStore();
const workflowDocumentStore = computed(() =>
useWorkflowDocumentStore(createWorkflowDocumentId(workflowsStore.workflowId)),
);
acc = acc.concat(
flattenSchema({
isDataEmpty: item.isDataEmpty,

View File

@ -9,10 +9,7 @@ import {
} from '@/app/models/history';
import { useHistoryStore } from '@/app/stores/history.store';
import { useWorkflowsStore } from '@/app/stores/workflows.store';
import {
useWorkflowDocumentStore,
createWorkflowDocumentId,
} from '@/app/stores/workflowDocument.store';
import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store';
import {
CanvasNodeDirtiness,
type CanvasNodeDirtinessType,
@ -125,9 +122,7 @@ export function useNodeDirtiness() {
const historyStore = useHistoryStore();
const workflowsStore = useWorkflowsStore();
const workflowDocumentStore = computed(() =>
useWorkflowDocumentStore(createWorkflowDocumentId(workflowsStore.workflowId)),
);
const workflowDocumentStore = injectWorkflowDocumentStore();
function getIncomingConnections(nodeName: string): INodeConnections {
return workflowDocumentStore.value.incomingConnectionsByNodeName(nodeName);

View File

@ -83,6 +83,7 @@ const {
vi.mock('@/app/stores/workflowDocument.store', () => ({
useWorkflowDocumentStore: vi.fn().mockReturnValue(mockWorkflowDocumentStore),
createWorkflowDocumentId: vi.fn().mockReturnValue('test-id'),
injectWorkflowDocumentStore: () => ({ value: mockWorkflowDocumentStore }),
}));
vi.mock('vue-router', async (importOriginal) => {

View File

@ -24,10 +24,7 @@ import { useTelemetry } from '@/app/composables/useTelemetry';
import { useToast } from '@/app/composables/useToast';
import { useExternalHooks } from '@/app/composables/useExternalHooks';
import { injectWorkflowState } from '@/app/composables/useWorkflowState';
import {
useWorkflowDocumentStore,
createWorkflowDocumentId,
} from '@/app/stores/workflowDocument.store';
import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store';
import { needsAgentInput } from '@/app/utils/nodes/nodeTransforms';
import { generateCodeForAiTransform } from '@/features/ndv/parameters/utils/buttonParameter.utils';
@ -103,9 +100,7 @@ export function useNodeExecution(
const uiStore = useUIStore();
const workflowState = injectWorkflowState();
const workflowDocumentStore = computed(() =>
useWorkflowDocumentStore(createWorkflowDocumentId(workflowsStore.workflowId)),
);
const workflowDocumentStore = injectWorkflowDocumentStore();
const { runWorkflow, stopCurrentExecution } = useRunWorkflow({ router });
const nodeHelpers = useNodeHelpers();

View File

@ -1,3 +1,4 @@
import { shallowRef } from 'vue';
import { setActivePinia } from 'pinia';
import type {
ExecutionStatus,
@ -19,27 +20,29 @@ import { faker } from '@faker-js/faker';
import type { INodeUi } from '@/Interface';
import type { IUsedCredential } from '@/features/credentials/credentials.types';
import { useNodeTypesStore } from '@/app/stores/nodeTypes.store';
import { useWorkflowDocumentStore } from '@/app/stores/workflowDocument.store';
const mockDocumentStoreUsedCredentials: Record<string, IUsedCredential> = {};
const mockDocumentStore = {
name: '',
settings: {},
pinData: {},
usedCredentials: mockDocumentStoreUsedCredentials,
allNodes: [],
workflowTriggerNodes: [],
getNodeByName: vi.fn(),
setNodeIssue: vi.fn(),
updateNodeProperties: vi.fn(),
getExpressionHandler: vi.fn(() => ({})),
getPinDataSnapshot: vi.fn().mockReturnValue({}),
};
vi.mock('@/app/stores/workflowDocument.store', async () => {
const actual = await vi.importActual('@/app/stores/workflowDocument.store');
return {
...actual,
useWorkflowDocumentStore: vi.fn(() => ({
name: '',
settings: {},
pinData: {},
usedCredentials: mockDocumentStoreUsedCredentials,
allNodes: [],
workflowTriggerNodes: [],
getNodeByName: vi.fn(),
setNodeIssue: vi.fn(),
updateNodeProperties: vi.fn(),
getExpressionHandler: vi.fn(() => ({})),
getPinDataSnapshot: vi.fn().mockReturnValue({}),
})),
useWorkflowDocumentStore: vi.fn(() => mockDocumentStore),
injectWorkflowDocumentStore: () => shallowRef(mockDocumentStore),
};
});
@ -77,18 +80,7 @@ describe('useNodeHelpers()', () => {
parameters: {},
};
vi.mocked(useWorkflowDocumentStore).mockReturnValueOnce({
name: '',
settings: {},
usedCredentials: mockDocumentStoreUsedCredentials,
allNodes: [],
workflowTriggerNodes: [],
getNodeByName: vi.fn().mockReturnValue(node),
setNodeIssue: vi.fn(),
updateNodeProperties: vi.fn(),
getExpressionHandler: vi.fn(() => ({})),
getPinDataSnapshot: vi.fn().mockReturnValue({}),
} as unknown as ReturnType<typeof useWorkflowDocumentStore>);
mockDocumentStore.getNodeByName = vi.fn().mockReturnValue(node);
mockedStore(useNodeTypesStore).getNodeType = vi.fn().mockReturnValue({});
mockedStore(useNodeTypesStore).isTriggerNode = vi.fn().mockReturnValue(false);
mockedStore(useNodeTypesStore).isToolNode = vi.fn().mockReturnValue(false);

View File

@ -1,4 +1,4 @@
import { ref, computed } from 'vue';
import { ref } from 'vue';
import { useHistoryStore } from '@/app/stores/history.store';
import {
CUSTOM_API_CALL_KEY,
@ -48,10 +48,7 @@ import { useTelemetry } from './useTelemetry';
import { hasPermission } from '@/app/utils/rbac/permissions';
import { useCanvasStore } from '@/app/stores/canvas.store';
import { useSettingsStore } from '@/app/stores/settings.store';
import {
useWorkflowDocumentStore,
createWorkflowDocumentId,
} from '@/app/stores/workflowDocument.store';
import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store';
declare namespace HttpRequestNode {
namespace V2 {
@ -72,9 +69,7 @@ export function useNodeHelpers() {
const i18n = useI18n();
const canvasStore = useCanvasStore();
const workflowDocumentStore = computed(() =>
useWorkflowDocumentStore(createWorkflowDocumentId(workflowsStore.workflowId)),
);
const workflowDocumentStore = injectWorkflowDocumentStore();
const isInsertingNodes = ref(false);
const credentialsUpdated = ref(false);
@ -154,10 +149,7 @@ export function useNodeHelpers() {
return [];
}
const workflowDocumentStore = useWorkflowDocumentStore(
createWorkflowDocumentId(workflowsStore.workflowId),
);
const usedCredentials = workflowDocumentStore.usedCredentials;
const usedCredentials = workflowDocumentStore.value.usedCredentials;
return Object.values(credentials)
.map(({ id }) => id)
@ -192,10 +184,7 @@ export function useNodeHelpers() {
workflow: WorkflowObjectAccessors,
ignoreIssues?: string[],
): INodeIssues | null {
const workflowDocumentStore = useWorkflowDocumentStore(
createWorkflowDocumentId(workflowsStore.workflowId),
);
const pinDataNodeNames = Object.keys(workflowDocumentStore.pinData);
const pinDataNodeNames = Object.keys(workflowDocumentStore.value.pinData);
let nodeIssues: INodeIssues | null = null;
ignoreIssues = ignoreIssues ?? [];
@ -429,9 +418,6 @@ export function useNodeHelpers() {
nodeType?: INodeTypeDescription,
): INodeIssues | null {
const localNodeType = nodeType ?? nodeTypesStore.getNodeType(node.type, node.typeVersion);
const workflowDocumentStore = useWorkflowDocumentStore(
createWorkflowDocumentId(workflowsStore.workflowId),
);
if (node.disabled) {
// Node is disabled
return null;
@ -470,7 +456,7 @@ export function useNodeHelpers() {
// Prevents HTTP Request node from being unusable if a sharee does not have direct
// access to a credential
const isCredentialUsedInWorkflow =
workflowDocumentStore.usedCredentials?.[
workflowDocumentStore.value.usedCredentials?.[
node.credentials?.[nodeCredentialType]?.id as string
];
@ -558,7 +544,7 @@ export function useNodeHelpers() {
if (nameMatches.length === 0) {
const isCredentialUsedInWorkflow =
workflowDocumentStore.usedCredentials?.[selectedCredentials.id as string];
workflowDocumentStore.value.usedCredentials?.[selectedCredentials.id as string];
if (
!isCredentialUsedInWorkflow &&

View File

@ -1,20 +1,13 @@
import type { INode } from 'n8n-workflow';
import { getNodeIconSource, type IconNodeType, type NodeIconSource } from '../utils/nodeIcon';
import { computed, toValue, type ComputedRef, type MaybeRefOrGetter } from 'vue';
import { useWorkflowsStore } from '../stores/workflows.store';
import {
createWorkflowDocumentId,
useWorkflowDocumentStore,
} from '../stores/workflowDocument.store';
import { injectWorkflowDocumentStore } from '../stores/workflowDocument.store';
export function useNodeIconSource(
nodeType: MaybeRefOrGetter<IconNodeType | string | null | undefined>,
node?: MaybeRefOrGetter<INode | null>,
): ComputedRef<NodeIconSource | undefined> {
const workflowsStore = useWorkflowsStore();
const workflowDocumentStore = computed(() =>
useWorkflowDocumentStore(createWorkflowDocumentId(workflowsStore.workflowId)),
);
const workflowDocumentStore = injectWorkflowDocumentStore();
return computed(() => {
const typeValue = toValue(nodeType);

View File

@ -19,8 +19,6 @@ import { useWorkflowsStore } from '@/app/stores/workflows.store';
import { useUIStore } from '@/app/stores/ui.store';
import {
injectWorkflowDocumentStore,
useWorkflowDocumentStore,
createWorkflowDocumentId,
getPinDataSize,
pinDataToExecutionData,
} from '@/app/stores/workflowDocument.store';
@ -28,7 +26,7 @@ import type { INodeUi, IRunDataDisplayMode } from '@/Interface';
import { useExternalHooks } from '@/app/composables/useExternalHooks';
import { useTelemetry } from '@/app/composables/useTelemetry';
import type { MaybeRef } from 'vue';
import { computed, shallowRef, unref } from 'vue';
import { computed, unref } from 'vue';
import { useRootStore } from '@n8n/stores/useRootStore';
import { useNodeType } from '@/app/composables/useNodeType';
import { useDataSchema } from './useDataSchema';
@ -60,9 +58,7 @@ export function usePinnedData(
const rootStore = useRootStore();
const workflowsStore = useWorkflowsStore();
const uiStore = useUIStore();
const workflowDocumentStore =
injectWorkflowDocumentStore() ??
shallowRef(useWorkflowDocumentStore(createWorkflowDocumentId(workflowsStore.workflowId)));
const workflowDocumentStore = injectWorkflowDocumentStore();
const toast = useToast();
const i18n = useI18n();
const telemetry = useTelemetry();

View File

@ -1,15 +1,29 @@
import type { NodeExecuteAfterData } from '@n8n/api-types/push/execution';
import { useSchemaPreviewStore } from '@/features/ndv/runData/schemaPreview.store';
import { useWorkflowsStore } from '@/app/stores/workflows.store';
import { computed } from 'vue';
import {
createWorkflowDocumentId,
useWorkflowDocumentStore,
} from '@/app/stores/workflowDocument.store';
/**
* Handles the 'nodeExecuteAfterData' event, which is sent after a node has executed and contains the resulting data.
*/
export async function nodeExecuteAfterData({ data: pushData }: NodeExecuteAfterData) {
const workflowsStore = useWorkflowsStore();
const workflowDocumentStore = computed(() =>
useWorkflowDocumentStore(createWorkflowDocumentId(workflowsStore.workflowId)),
);
const schemaPreviewStore = useSchemaPreviewStore();
workflowsStore.updateNodeExecutionRunData(pushData);
void schemaPreviewStore.trackSchemaPreviewExecution(pushData);
const node = workflowDocumentStore.value.getNodeByName(pushData.nodeName);
if (!node) {
return;
}
void schemaPreviewStore.trackSchemaPreviewExecution(workflowsStore.workflowId, node, pushData);
}

View File

@ -25,6 +25,7 @@ const { mockWorkflowDocumentStore } = vi.hoisted(() => ({
vi.mock('@/app/stores/workflowDocument.store', () => ({
useWorkflowDocumentStore: vi.fn(() => mockWorkflowDocumentStore),
createWorkflowDocumentId: (id: string) => id,
injectWorkflowDocumentStore: () => ({ value: mockWorkflowDocumentStore }),
}));
const makeEvent = (

View File

@ -18,10 +18,7 @@ import {
watch,
} from 'vue';
import { useWorkflowHelpers, type ResolveParameterOptions } from './useWorkflowHelpers';
import {
useWorkflowDocumentStore,
createWorkflowDocumentId,
} from '@/app/stores/workflowDocument.store';
import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store';
import { ExpressionLocalResolveContextSymbol } from '@/app/constants';
import type { ExpressionLocalResolveContext } from '@/app/types/expressions';
@ -40,9 +37,7 @@ export function useResolvedExpression({
}) {
const ndvStore = useNDVStore();
const workflowsStore = useWorkflowsStore();
const workflowDocumentStore = computed(() =>
useWorkflowDocumentStore(createWorkflowDocumentId(workflowsStore.workflowId)),
);
const workflowDocumentStore = injectWorkflowDocumentStore();
const { resolveExpression } = useWorkflowHelpers();

View File

@ -1,3 +1,4 @@
import { shallowRef } from 'vue';
import { setActivePinia } from 'pinia';
import { createTestingPinia } from '@pinia/testing';
import { useRouter } from 'vue-router';
@ -87,6 +88,7 @@ const { mockDocumentStore } = vi.hoisted(() => {
vi.mock('@/app/stores/workflowDocument.store', () => ({
useWorkflowDocumentStore: () => mockDocumentStore,
injectWorkflowDocumentStore: () => shallowRef(mockDocumentStore),
createWorkflowDocumentId: (id: string) => `${id}@latest`,
}));

View File

@ -37,10 +37,7 @@ import {
import { useRootStore } from '@n8n/stores/useRootStore';
import { useWorkflowsStore } from '@/app/stores/workflows.store';
import {
useWorkflowDocumentStore,
createWorkflowDocumentId,
} from '@/app/stores/workflowDocument.store';
import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store';
import { displayForm } from '@/features/execution/executions/executions.utils';
import { useExternalHooks } from '@/app/composables/useExternalHooks';
import { useWorkflowHelpers } from '@/app/composables/useWorkflowHelpers';
@ -58,7 +55,6 @@ import { useCanvasOperations } from './useCanvasOperations';
import { chatEventBus } from '@n8n/chat/event-buses';
import { useAgentRequestStore } from '@n8n/stores/useAgentRequestStore';
import { useWorkflowSaving } from './useWorkflowSaving';
import { computed } from 'vue';
import { injectWorkflowState } from '@/app/composables/useWorkflowState';
import { useDocumentTitle } from './useDocumentTitle';
import { useChat } from '@n8n/chat/composables';
@ -81,9 +77,7 @@ export function useRunWorkflow(useRunWorkflowOpts: {
const workflowsStore = useWorkflowsStore();
const workflowState = injectWorkflowState();
const workflowDocumentStore = computed(() =>
useWorkflowDocumentStore(createWorkflowDocumentId(workflowsStore.workflowId)),
);
const workflowDocumentStore = injectWorkflowDocumentStore();
const nodeHelpers = useNodeHelpers();
const workflowSaving = useWorkflowSaving({

View File

@ -1,7 +1,7 @@
import { createTestingPinia } from '@pinia/testing';
import { setActivePinia } from 'pinia';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { ref, nextTick } from 'vue';
import { ref, shallowRef, nextTick } from 'vue';
import { waitFor } from '@testing-library/vue';
import { useToolParameters } from './useToolParameters';
import { useWorkflowsStore } from '../stores/workflows.store';
@ -27,6 +27,7 @@ const { mockWorkflowDocumentStore } = vi.hoisted(() => ({
vi.mock('@/app/stores/workflowDocument.store', () => ({
useWorkflowDocumentStore: vi.fn().mockReturnValue(mockWorkflowDocumentStore),
injectWorkflowDocumentStore: () => shallowRef(mockWorkflowDocumentStore),
createWorkflowDocumentId: vi.fn().mockReturnValue('test-id'),
}));

View File

@ -8,10 +8,7 @@ import {
} from 'n8n-workflow';
import { computed, reactive, ref, watch, type Ref } from 'vue';
import { useWorkflowsStore } from '../stores/workflows.store';
import {
useWorkflowDocumentStore,
createWorkflowDocumentId,
} from '@/app/stores/workflowDocument.store';
import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store';
import { useProjectsStore } from '@/features/collaboration/projects/projects.store';
import { useNodeTypesStore } from '../stores/nodeTypes.store';
import { useAgentRequestStore } from '@n8n/stores/useAgentRequestStore';
@ -35,9 +32,7 @@ export function useToolParameters({ node }: GetToolParametersProps) {
const nodeTypesStore = useNodeTypesStore();
const agentRequestStore = useAgentRequestStore();
const workflowDocumentStore = computed(() =>
useWorkflowDocumentStore(createWorkflowDocumentId(workflowsStore.workflowId)),
);
const workflowDocumentStore = injectWorkflowDocumentStore();
const selectedToolMap = reactive<Record<string, string | undefined>>({});
const error = ref<Error | undefined>(undefined);

View File

@ -1,11 +1,9 @@
import { useWorkflowsStore } from '@/app/stores/workflows.store';
import { useNodeTypesStore } from '@/app/stores/nodeTypes.store';
import {
createWorkflowDocumentId,
useWorkflowDocumentStore,
} from '../stores/workflowDocument.store';
import { injectWorkflowDocumentStore } from '../stores/workflowDocument.store';
export function useUniqueNodeName() {
const workflowDocumentStore = injectWorkflowDocumentStore();
/**
* All in-store node name defaults ending with a number, e.g.
* `AWS S3`, `Magento 2`, `MSG91`, `S3`, `SIGNL4`, `sms77`
@ -27,11 +25,9 @@ export function useUniqueNodeName() {
* all nodes on canvas and any extra names that cannot be used.
*/
function uniqueNodeName(originalName: string, extraNames: string[] = []) {
const { canvasNames } = useWorkflowDocumentStore(
createWorkflowDocumentId(useWorkflowsStore().workflowId),
);
const isUnique = !canvasNames.has(originalName) && !extraNames.includes(originalName);
const isUnique =
!workflowDocumentStore.value.canvasNames.has(originalName) &&
!extraNames.includes(originalName);
if (isUnique) return originalName;
@ -54,7 +50,7 @@ export function useUniqueNodeName() {
unique = originalName;
while (canvasNames.has(unique) || extraNames.includes(unique)) {
while (workflowDocumentStore.value.canvasNames.has(unique) || extraNames.includes(unique)) {
unique = originalName + index++;
}
@ -79,7 +75,7 @@ export function useUniqueNodeName() {
unique = match.groups.base;
while (canvasNames.has(unique) || extraNames.includes(unique)) {
while (workflowDocumentStore.value.canvasNames.has(unique) || extraNames.includes(unique)) {
unique = match.groups.base + '-' + index++;
}
@ -110,7 +106,7 @@ export function useUniqueNodeName() {
unique = base;
while (canvasNames.has(unique) || extraNames.includes(unique)) {
while (workflowDocumentStore.value.canvasNames.has(unique) || extraNames.includes(unique)) {
unique = base + index++;
}

View File

@ -1,8 +1,5 @@
import { useWorkflowsStore } from '@/app/stores/workflows.store';
import {
useWorkflowDocumentStore,
createWorkflowDocumentId,
} from '@/app/stores/workflowDocument.store';
import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store';
import {
buildAdjacencyList,
parseExtractableSubgraphSelection,
@ -41,12 +38,7 @@ const CANVAS_HISTORY_OPTIONS = {
export function useWorkflowExtraction() {
const uiStore = useUIStore();
const workflowsStore = useWorkflowsStore();
const workflowDocumentStore = computed(() => {
if (workflowsStore.workflowId) {
return useWorkflowDocumentStore(createWorkflowDocumentId(workflowsStore.workflowId));
}
return null;
});
const workflowDocumentStore = injectWorkflowDocumentStore();
const nodeTypesStore = useNodeTypesStore();
const toast = useToast();
const router = useRouter();
@ -56,7 +48,7 @@ export function useWorkflowExtraction() {
const telemetry = useTelemetry();
const adjacencyList = computed(() =>
buildAdjacencyList(workflowDocumentStore.value?.connectionsBySourceNode ?? {}),
buildAdjacencyList(workflowDocumentStore.value.connectionsBySourceNode),
);
function showError(message: string) {
@ -263,8 +255,8 @@ export function useWorkflowExtraction() {
...endNodeConnection,
},
settings: { executionOrder: 'v1' },
projectId: workflowDocumentStore.value?.homeProject?.id,
parentFolderId: workflowDocumentStore.value?.parentFolder?.id ?? undefined,
projectId: workflowDocumentStore.value.homeProject?.id,
parentFolderId: workflowDocumentStore.value.parentFolder?.id ?? undefined,
};
}
@ -334,12 +326,11 @@ export function useWorkflowExtraction() {
...x: Parameters<typeof NodeHelpers.getNodeInputs>
) => ReturnType<typeof NodeHelpers.getNodeInputs>,
) => {
const node = workflowDocumentStore?.value?.getNodeByName(nodeName);
const node = workflowDocumentStore.value.getNodeByName(nodeName);
if (!node) return true; // invariant broken -> abort onto error path
const nodeType = useNodeTypesStore().getNodeType(node.type, node.typeVersion);
if (!nodeType) return true; // invariant broken -> abort onto error path
const expression = workflowDocumentStore?.value?.getExpressionHandler();
if (!expression) return true;
const expression = workflowDocumentStore.value.getExpressionHandler();
const ios = getIOs({ expression }, node, nodeType);
return (
@ -407,7 +398,7 @@ export function useWorkflowExtraction() {
);
for (const node of selectionChildNodes) {
const currentNode = workflowDocumentStore?.value?.allNodes.find((x) => x.id === node.id);
const currentNode = workflowDocumentStore.value.allNodes.find((x) => x.id === node.id);
if (isEqual(node, currentNode)) continue;
@ -425,7 +416,7 @@ export function useWorkflowExtraction() {
function tryExtractNodesIntoSubworkflow(nodeIds: string[]): boolean {
const subGraph = nodeIds
.map((id) => workflowDocumentStore?.value?.getNodeById(id))
.map((id) => workflowDocumentStore.value.getNodeById(id))
.filter((x) => x !== undefined);
const triggers = subGraph.filter((x) =>
@ -461,7 +452,7 @@ export function useWorkflowExtraction() {
) {
const { start, end } = selection;
const allNodeNames = workflowDocumentStore?.value?.allNodes.map((x) => x.name) ?? [];
const allNodeNames = workflowDocumentStore.value.allNodes.map((x) => x.name) ?? [];
let startNodeName = 'Start';
const subGraphNames = subGraph.map((x) => x.name);
@ -471,16 +462,16 @@ export function useWorkflowExtraction() {
while (subGraphNames.includes(returnNodeName)) returnNodeName += '_1';
const directAfterEndNodeNames = end
? (workflowDocumentStore?.value
?.getChildNodes(end, 'main', 1)
.map((x) => workflowDocumentStore?.value?.getNodeByName(x)?.name)
? (workflowDocumentStore.value
.getChildNodes(end, 'main', 1)
.map((x) => workflowDocumentStore.value.getNodeByName(x)?.name)
.filter((x) => x !== undefined) ?? [])
: [];
const allAfterEndNodes = end
? (workflowDocumentStore?.value
?.getChildNodes(end, 'ALL')
.map((x) => workflowDocumentStore?.value?.getNodeByName(x) ?? null)
? (workflowDocumentStore.value
.getChildNodes(end, 'ALL')
.map((x) => workflowDocumentStore.value.getNodeByName(x) ?? null)
.filter((x) => x !== null) ?? [])
: [];
@ -507,7 +498,7 @@ export function useWorkflowExtraction() {
newWorkflowName,
selection,
nodes,
workflowDocumentStore.value?.connectionsBySourceNode ?? {},
workflowDocumentStore.value?.connectionsBySourceNode,
variables,
afterVariables,
startNodeName,

View File

@ -54,8 +54,8 @@ import type { ExpressionLocalResolveContext } from '@/app/types/expressions';
import {
useWorkflowDocumentStore,
createWorkflowDocumentId,
injectWorkflowDocumentStore,
} from '@/app/stores/workflowDocument.store';
import { computed } from 'vue';
import type { WorkflowObjectAccessors } from '../types';
export type ResolveParameterOptions = {
@ -503,9 +503,7 @@ export function useWorkflowHelpers() {
const i18n = useI18n();
const workflowDocumentStore = computed(() =>
useWorkflowDocumentStore(createWorkflowDocumentId(workflowsStore.workflowId)),
);
const workflowDocumentStore = injectWorkflowDocumentStore();
function getNodeTypesMaxCount() {
const nodes = workflowDocumentStore.value.allNodes;
@ -858,14 +856,6 @@ export function useWorkflowHelpers() {
return { workflowDocumentStore: initializedWorkflowDocumentStore };
}
/**
* Check if workflow contains any node from specified package
* by performing a quick check based on the node type name.
*/
const containsNodeFromPackage = (workflow: IWorkflowDb, packageName: string) => {
return workflow.nodes.some((node) => node.type.startsWith(packageName));
};
function getMethods(trigger: INode) {
if (trigger.type === WEBHOOK_NODE_TYPE) {
if (trigger.parameters.multipleMethods === true) {
@ -942,7 +932,6 @@ export function useWorkflowHelpers() {
getWorkflowProjectRole,
initState,
getNodeParametersWithResolvedExpressions,
containsNodeFromPackage,
checkConflictingWebhooks,
};
}

View File

@ -55,6 +55,7 @@ const mockDocumentStore = vi.hoisted(() => ({
vi.mock('@/app/stores/workflowDocument.store', () => ({
useWorkflowDocumentStore: vi.fn().mockReturnValue(mockDocumentStore),
createWorkflowDocumentId: vi.fn().mockReturnValue('test-id'),
injectWorkflowDocumentStore: vi.fn().mockReturnValue({ value: mockDocumentStore }),
}));
// Mock useWorkflowState - using hoisted for proper initialization

View File

@ -9,12 +9,12 @@ import { useWorkflowsStore } from '@/app/stores/workflows.store';
import {
useWorkflowDocumentStore,
createWorkflowDocumentId,
injectWorkflowDocumentStore,
} from '@/app/stores/workflowDocument.store';
import { useCredentialsStore } from '@/features/credentials/credentials.store';
import { useNodeTypesStore } from '@/app/stores/nodeTypes.store';
import { useBuilderStore } from '@/features/ai/assistant/builder.store';
import { useUIStore } from '@/app/stores/ui.store';
import { computed } from 'vue';
import { useCanvasOperations } from '@/app/composables/useCanvasOperations';
import { useNodeHelpers } from '@/app/composables/useNodeHelpers';
import { canvasEventBus } from '@/features/workflows/canvas/canvas.eventBus';
@ -50,9 +50,7 @@ export function useWorkflowUpdate() {
const canvasOperations = useCanvasOperations();
const nodeHelpers = useNodeHelpers();
const workflowDocumentStore = computed(() =>
useWorkflowDocumentStore(createWorkflowDocumentId(workflowsStore.workflowId)),
);
const workflowDocumentStore = injectWorkflowDocumentStore();
/**
* Categorize nodes into those to update, add, or remove.

View File

@ -1,30 +1,13 @@
import { computed, ref } from 'vue';
import { defineStore } from 'pinia';
import { useWorkflowsStore } from '@/app/stores/workflows.store';
import {
useWorkflowDocumentStore,
createWorkflowDocumentId,
} from '@/app/stores/workflowDocument.store';
import type { INodeUi, XYPosition } from '@/Interface';
import type { XYPosition } from '@/Interface';
import { useLoadingService } from '@/app/composables/useLoadingService';
export const useCanvasStore = defineStore('canvas', () => {
const workflowStore = useWorkflowsStore();
const workflowDocumentStore = computed(() =>
useWorkflowDocumentStore(createWorkflowDocumentId(workflowStore.workflowId)),
);
const loadingService = useLoadingService();
const newNodeInsertPosition = ref<XYPosition | null>(null);
const nodes = computed<INodeUi[]>(() => workflowDocumentStore.value?.allNodes ?? []);
const aiNodes = computed<INodeUi[]>(() =>
nodes.value.filter(
(node) =>
node.type.includes('langchain') ||
(node.type === 'n8n-nodes-base.evaluation' && node.parameters?.operation === 'setMetrics'),
),
);
const hasRangeSelection = ref(false);
function setHasRangeSelection(value: boolean) {
@ -34,7 +17,6 @@ export const useCanvasStore = defineStore('canvas', () => {
return {
newNodeInsertPosition,
isLoading: loadingService.isLoading,
aiNodes,
hasRangeSelection: computed(() => hasRangeSelection.value),
startLoading: loadingService.startLoading,
setLoadingText: loadingService.setLoadingText,

View File

@ -1,6 +1,6 @@
import { defineStore, getActivePinia } from 'pinia';
import { STORES } from '@n8n/stores';
import { inject } from 'vue';
import { computed, inject, type ShallowRef } from 'vue';
import { WorkflowDocumentStoreKey } from '@/app/constants/injectionKeys';
import { useWorkflowDocumentActive } from './workflowDocument/useWorkflowDocumentActive';
import { useWorkflowDocumentHomeProject } from './workflowDocument/useWorkflowDocumentHomeProject';
@ -40,6 +40,7 @@ import { deepCopy } from 'n8n-workflow';
import type { WorkflowData } from '@n8n/rest-api-client/api/workflows';
import type { Scope } from '@n8n/permissions';
import type { IUsedCredential } from '@/features/credentials/credentials.types';
import { useWorkflowsStore } from './workflows.store';
export {
getPinDataSize,
@ -423,11 +424,23 @@ export function disposeWorkflowDocumentStore(store: ReturnType<typeof useWorkflo
/**
* Injects the workflow document store from the current component tree.
* Returns null if not within a component context that has provided the store.
* Returns fallback document store if not within a component context
*
* Use this in composables/stores that need to interact with the current workflow's
* document store but may be called outside of the NodeView tree.
* document store and avoid calling this outside a component tree.
*/
export function injectWorkflowDocumentStore() {
return inject(WorkflowDocumentStoreKey, null);
export function injectWorkflowDocumentStore(): ShallowRef<
ReturnType<typeof useWorkflowDocumentStore>
> {
const workflowsStore = useWorkflowsStore();
const fallback = computed(() => {
// TODO: once usages outside of a component tree is eliminated,
// this can be replaced with useWorkflowId()
const fallbackWorkflowId = workflowsStore.workflowId;
return useWorkflowDocumentStore(createWorkflowDocumentId(fallbackWorkflowId));
});
const injected = inject(WorkflowDocumentStoreKey, null);
return computed(() => injected?.value ?? fallback.value);
}

View File

@ -193,6 +193,14 @@ export function useWorkflowDocumentNodes(deps: WorkflowDocumentNodesDeps) {
}),
);
const aiNodes = computed<INodeUi[]>(() =>
nodes.value.filter(
(node) =>
node.type.includes('langchain') ||
(node.type === 'n8n-nodes-base.evaluation' && node.parameters?.operation === 'setMetrics'),
),
);
function getNodeById(id: string): INodeUi | undefined {
return nodes.value.find((node) => node.id === id);
}
@ -469,6 +477,7 @@ export function useWorkflowDocumentNodes(deps: WorkflowDocumentNodesDeps) {
nodesByName,
canvasNames,
workflowTriggerNodes,
aiNodes,
getNodeById,
getNodeByName,
findNodeByPartialId,

View File

@ -41,7 +41,6 @@ import { computed, ref } from 'vue';
import { useProjectsStore } from '@/features/collaboration/projects/projects.store';
import type { ExecutionRedactionQueryDto, PushPayload } from '@n8n/api-types';
import { useTelemetry } from '@/app/composables/useTelemetry';
import { useWorkflowHelpers } from '@/app/composables/useWorkflowHelpers';
import { useSettingsStore } from './settings.store';
import { useUsersStore } from '@/features/settings/users/users.store';
import { updateCurrentUserSettings } from '@n8n/rest-api-client/api/users';
@ -55,12 +54,13 @@ import {
createWorkflowDocumentId,
} from '@/app/stores/workflowDocument.store';
import { getPairedItemsMapping } from '@/app/utils/pairedItemUtils';
import { useNodeTypesStore } from './nodeTypes.store';
export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
const uiStore = useUIStore();
const telemetry = useTelemetry();
const workflowHelpers = useWorkflowHelpers();
const settingsStore = useSettingsStore();
const nodeTypesStore = useNodeTypesStore();
const rootStore = useRootStore();
const usersStore = useUsersStore();
const sourceControlStore = useSourceControlStore();
@ -79,6 +79,9 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
const chatPartialExecutionDestinationNode = ref<string | null>(null);
const selectedTriggerNodeName = ref<string>();
/**
* @deprecated use useWorkflowId() in Vue components/composables instead.
*/
const workflowId = ref('');
// A workflow is new if it hasn't been saved to the backend yet.
@ -462,7 +465,7 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
node_graph_string: JSON.stringify(
TelemetryHelpers.generateNodesGraph(
workflowDocumentStore.serialize(),
workflowHelpers.getNodeTypes(),
nodeTypesStore.getAllNodeTypes(),
{
isCloudDeployment: settingsStore.isCloudDeployment,
},
@ -620,6 +623,14 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
return response && unflattenExecutionData(response);
}
/**
* Check if workflow contains any node from specified package
* by performing a quick check based on the node type name.
*/
function containsNodeFromPackage(workflow: IWorkflowDb, packageName: string) {
return workflow.nodes.some((node) => node.type.startsWith(packageName));
}
/**
* Creates a new workflow with the provided data.
* Ensures that the new workflow is not active upon creation.
@ -648,10 +659,7 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
sendData as unknown as IDataObject,
);
const isAIWorkflow = workflowHelpers.containsNodeFromPackage(
newWorkflow,
AI_NODES_PACKAGE_NAME,
);
const isAIWorkflow = containsNodeFromPackage(newWorkflow, AI_NODES_PACKAGE_NAME);
if (isAIWorkflow && !usersStore.isEasyAIWorkflowOnboardingDone) {
await updateCurrentUserSettings(rootStore.restApiContext, {
easyAIWorkflowOnboarded: true,
@ -693,7 +701,7 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
}
if (
workflowHelpers.containsNodeFromPackage(updatedWorkflow, AI_NODES_PACKAGE_NAME) &&
containsNodeFromPackage(updatedWorkflow, AI_NODES_PACKAGE_NAME) &&
!usersStore.isEasyAIWorkflowOnboardingDone
) {
await updateCurrentUserSettings(rootStore.restApiContext, {

View File

@ -18,7 +18,7 @@ import { defaultSettings } from '@/__tests__/defaults';
import merge from 'lodash/merge';
import { DEFAULT_POSTHOG_SETTINGS } from '@/app/stores/posthog.store.test';
import { VIEWS } from '@/app/constants';
import { reactive } from 'vue';
import { reactive, shallowRef } from 'vue';
import * as chatAPI from '@/features/ai/assistant/assistant.api';
import * as telemetryModule from '@/app/composables/useTelemetry';
import type { Telemetry } from '@/app/plugins/telemetry';
@ -39,6 +39,7 @@ const { mockWorkflowDocumentStore } = vi.hoisted(() => ({
vi.mock('@/app/stores/workflowDocument.store', () => ({
useWorkflowDocumentStore: vi.fn().mockReturnValue(mockWorkflowDocumentStore),
injectWorkflowDocumentStore: () => shallowRef(mockWorkflowDocumentStore),
createWorkflowDocumentId: vi.fn().mockReturnValue('test-id'),
}));

View File

@ -8,10 +8,7 @@ import { useLogsStore } from '@/app/stores/logs.store';
import { useRunWorkflow } from '@/app/composables/useRunWorkflow';
import { useToast } from '@/app/composables/useToast';
import { isChatNode } from '@/app/utils/aiUtils';
import {
createWorkflowDocumentId,
useWorkflowDocumentStore,
} from '@/app/stores/workflowDocument.store';
import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store';
const RUNNING_STATES: string[] = ['running', 'waiting'];
@ -25,9 +22,7 @@ export function useBuilderExecution(isReady: ComputedRef<boolean>) {
const router = useRouter();
const i18n = useI18n();
const workflowsStore = useWorkflowsStore();
const workflowDocumentStore = computed(() =>
useWorkflowDocumentStore(createWorkflowDocumentId(workflowsStore.workflowId)),
);
const workflowDocumentStore = injectWorkflowDocumentStore();
const nodeTypesStore = useNodeTypesStore();
const logsStore = useLogsStore();
const toast = useToast();

View File

@ -33,29 +33,30 @@ vi.mock('@/features/ai/assistant/builder.store', () => ({
useBuilderStore: () => mockBuilderStoreState,
}));
const mockUnpinNodeData = vi.fn();
const mockTouchPinnedDataLastRemovedAt = vi.fn();
const mockAllNodes = ref<INodeUi[]>([]);
const { mockDocumentStore } = vi.hoisted(() => ({
mockDocumentStore: {
pinData: {},
unpinNodeData: vi.fn(),
touchPinnedDataLastRemovedAt: vi.fn(),
allNodes: [] as INodeUi[],
name: '',
settings: {},
getPinDataSnapshot: () => ({}),
},
}));
vi.mock('@/app/stores/workflows.store', () => ({
useWorkflowsStore: () => ({
workflowId: 'test-workflow-id',
get allNodes() {
return mockAllNodes.value;
return mockDocumentStore.allNodes;
},
}),
}));
vi.mock('@/app/stores/workflowDocument.store', () => ({
useWorkflowDocumentStore: () => ({
pinData: {},
unpinNodeData: mockUnpinNodeData,
touchPinnedDataLastRemovedAt: mockTouchPinnedDataLastRemovedAt,
allNodes: [],
name: '',
settings: {},
getPinDataSnapshot: () => ({}),
}),
useWorkflowDocumentStore: () => mockDocumentStore,
createWorkflowDocumentId: (id: string) => id,
injectWorkflowDocumentStore: () => ({ value: mockDocumentStore }),
}));
vi.mock('@/app/stores/ui.store', () => ({
@ -119,7 +120,7 @@ describe('useBuilderSetupCards', () => {
currentScope = undefined;
vi.clearAllMocks();
mockSetupCards.value = [];
mockAllNodes.value = [];
mockDocumentStore.allNodes = [];
mockFirstTriggerName.value = null;
mockBuilderStoreState.wizardCurrentStep = 0;
mockBuilderStoreState.wizardHasExecutedWorkflow = false;

View File

@ -4,19 +4,12 @@ import type { SetupCardItem } from '@/features/setupPanel/setupPanel.types';
import { isCardComplete } from '@/features/setupPanel/setupPanel.utils';
import { useWorkflowSetupState } from '@/features/setupPanel/composables/useWorkflowSetupState';
import { useBuilderStore } from '@/features/ai/assistant/builder.store';
import { useWorkflowsStore } from '@/app/stores/workflows.store';
import {
useWorkflowDocumentStore,
createWorkflowDocumentId,
} from '@/app/stores/workflowDocument.store';
import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store';
import { findPlaceholderDetails } from '@/features/ai/assistant/composables/useBuilderTodos';
export function useBuilderSetupCards() {
const builderStore = useBuilderStore();
const workflowsStore = useWorkflowsStore();
const workflowDocumentStore = computed(() =>
useWorkflowDocumentStore(createWorkflowDocumentId(workflowsStore.workflowId)),
);
const workflowDocumentStore = injectWorkflowDocumentStore();
// Sticky map of node name → placeholder parameter names.
// Once a node's placeholders are detected, the entry persists even after the user

View File

@ -1,10 +1,6 @@
import { computed } from 'vue';
import { useI18n } from '@n8n/i18n';
import { useWorkflowsStore } from '@/app/stores/workflows.store';
import {
useWorkflowDocumentStore,
createWorkflowDocumentId,
} from '@/app/stores/workflowDocument.store';
import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store';
import type { WorkflowValidationIssue } from '@/Interface';
import {
extractPlaceholderLabels,
@ -39,12 +35,9 @@ export interface TodosTrackingPayload {
* used by the AI builder.
*/
export function useBuilderTodos() {
const workflowsStore = useWorkflowsStore();
const locale = useI18n();
const workflowDocumentStore = computed(() =>
useWorkflowDocumentStore(createWorkflowDocumentId(workflowsStore.workflowId)),
);
const workflowDocumentStore = injectWorkflowDocumentStore();
/**
* Checks if a node is disabled, either directly or through any ancestor node.

View File

@ -1,11 +1,7 @@
import { computed } from 'vue';
import { useFocusedNodesStore } from '../focusedNodes.store';
import { useNodeTypesStore } from '@/app/stores/nodeTypes.store';
import { useWorkflowsStore } from '@/app/stores/workflows.store';
import {
useWorkflowDocumentStore,
createWorkflowDocumentId,
} from '@/app/stores/workflowDocument.store';
import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store';
import { canvasEventBus } from '@/features/workflows/canvas/canvas.eventBus';
/** Threshold at which individual chips are bundled into a single count chip */
@ -18,10 +14,7 @@ export const CHIP_BUNDLE_THRESHOLD = 3;
export function useFocusedNodesChipUI() {
const focusedNodesStore = useFocusedNodesStore();
const nodeTypesStore = useNodeTypesStore();
const workflowsStore = useWorkflowsStore();
const workflowDocumentStore = computed(() =>
useWorkflowDocumentStore(createWorkflowDocumentId(workflowsStore.workflowId)),
);
const workflowDocumentStore = injectWorkflowDocumentStore();
const confirmedNodes = computed(() => focusedNodesStore.confirmedNodes);
const unconfirmedNodes = computed(() => focusedNodesStore.filteredUnconfirmedNodes);

View File

@ -1,3 +1,4 @@
import { shallowRef } from 'vue';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { setActivePinia } from 'pinia';
import { createTestingPinia } from '@pinia/testing';
@ -33,6 +34,7 @@ const { mockWorkflowDocumentStore } = vi.hoisted(() => ({
vi.mock('@/app/stores/workflowDocument.store', () => ({
useWorkflowDocumentStore: vi.fn().mockReturnValue(mockWorkflowDocumentStore),
injectWorkflowDocumentStore: () => shallowRef(mockWorkflowDocumentStore),
createWorkflowDocumentId: vi.fn().mockReturnValue('test-id'),
}));

View File

@ -1,11 +1,7 @@
import { ref, computed, watch, type Ref } from 'vue';
import type { INodeUi } from '@/Interface';
import { useFocusedNodesStore } from '../focusedNodes.store';
import { useWorkflowsStore } from '@/app/stores/workflows.store';
import {
useWorkflowDocumentStore,
createWorkflowDocumentId,
} from '@/app/stores/workflowDocument.store';
import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store';
export interface UseNodeMentionOptions {
maxResults?: number;
@ -37,10 +33,7 @@ export function useNodeMention(options: UseNodeMentionOptions = {}): UseNodeMent
const { maxResults = 50 } = options;
const focusedNodesStore = useFocusedNodesStore();
const workflowsStore = useWorkflowsStore();
const workflowDocumentStore = computed(() =>
useWorkflowDocumentStore(createWorkflowDocumentId(workflowsStore.workflowId)),
);
const workflowDocumentStore = injectWorkflowDocumentStore();
const showDropdown = ref(false);
const searchQuery = ref('');

View File

@ -1,5 +1,5 @@
import { createPinia, setActivePinia } from 'pinia';
import { defineComponent, nextTick, reactive } from 'vue';
import { defineComponent, nextTick, reactive, shallowRef } from 'vue';
import { mount } from '@vue/test-utils';
import { useReviewChanges } from './useReviewChanges';
import type { INode, IConnections } from 'n8n-workflow';
@ -97,6 +97,7 @@ vi.mock('@/app/stores/workflows.store', () => ({
vi.mock('@/app/stores/workflowDocument.store', () => ({
useWorkflowDocumentStore: () => mockWorkflowDocumentStore,
injectWorkflowDocumentStore: () => shallowRef(mockWorkflowDocumentStore),
createWorkflowDocumentId: () => 'test-id',
}));

View File

@ -10,10 +10,7 @@ import { createEventBus } from '@n8n/utils/event-bus';
import { useI18n } from '@n8n/i18n';
import { useBuilderStore } from '@/features/ai/assistant/builder.store';
import { useWorkflowsStore } from '@/app/stores/workflows.store';
import {
useWorkflowDocumentStore,
createWorkflowDocumentId,
} from '@/app/stores/workflowDocument.store';
import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store';
import { useWorkflowHistoryStore } from '@/features/workflows/workflowHistory/workflowHistory.store';
import { useNodeTypesStore } from '@/app/stores/nodeTypes.store';
import { useUIStore } from '@/app/stores/ui.store';
@ -44,9 +41,7 @@ export function useReviewChanges() {
const posthogStore = usePostHog();
const i18n = useI18n();
const workflowDocumentStore = computed(() =>
useWorkflowDocumentStore(createWorkflowDocumentId(workflowsStore.workflowId)),
);
const workflowDocumentStore = injectWorkflowDocumentStore();
const isLoadingDiff = ref(false);
const cachedVersionNodes = computed<INode[]>(() => {
const version = builderStore.latestRevertVersion;

View File

@ -2,20 +2,13 @@
import { computed, nextTick, ref, useTemplateRef, watch } from 'vue';
import { useRoute } from 'vue-router';
import { useChatHubPanelStore } from '@/features/ai/chatHub/chatHubPanel.store';
import { useWorkflowsStore } from '@/app/stores/workflows.store';
import {
useWorkflowDocumentStore,
createWorkflowDocumentId,
} from '@/app/stores/workflowDocument.store';
import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store';
import { usePopOutWindow } from '@/features/execution/logs/composables/usePopOutWindow';
import CanvasChatFloatingWindow from './CanvasChatFloatingWindow.vue';
const route = useRoute();
const chatHubPanelStore = useChatHubPanelStore();
const workflowsStore = useWorkflowsStore();
const workflowDocumentStore = computed(() =>
useWorkflowDocumentStore(createWorkflowDocumentId(workflowsStore.workflowId)),
);
const workflowDocumentStore = injectWorkflowDocumentStore();
const canvasChatFloatingWindowRef = ref<InstanceType<typeof CanvasChatFloatingWindow>>();

View File

@ -2,10 +2,10 @@
import { getWorkflow as fetchWorkflowApi } from '@/app/api/workflows';
import { ExpressionLocalResolveContextSymbol } from '@/app/constants';
import { useNodeTypesStore } from '@/app/stores/nodeTypes.store';
import { useWorkflowsStore } from '@/app/stores/workflows.store';
import {
useWorkflowDocumentStore,
createWorkflowDocumentId,
injectWorkflowDocumentStore,
} from '@/app/stores/workflowDocument.store';
import { getAppNameFromCredType } from '@/app/utils/nodeTypesUtils';
import { useWizardNavigation } from '@/features/ai/shared/composables/useWizardNavigation';
@ -54,12 +54,9 @@ const props = defineProps<{
const i18n = useI18n();
const store = useInstanceAiStore();
const credentialsStore = useCredentialsStore();
const workflowsStore = useWorkflowsStore();
const nodeTypesStore = useNodeTypesStore();
const rootStore = useRootStore();
const workflowDocumentStore = computed(() =>
useWorkflowDocumentStore(createWorkflowDocumentId(workflowsStore.workflowId)),
);
const workflowDocumentStore = injectWorkflowDocumentStore();
// ---------------------------------------------------------------------------
// Composable wiring order matters for dependencies

View File

@ -1,15 +1,11 @@
import type { ComputedRef } from 'vue';
import { computed, ref } from 'vue';
import { ref } from 'vue';
import { useNodeTypesStore } from '@/app/stores/nodeTypes.store';
import { useUIStore } from '@/app/stores/ui.store';
import { useWorkflowsStore } from '@/app/stores/workflows.store';
import { getMainAuthField } from '@/app/utils/nodeTypesUtils';
import { useCredentialsStore } from '@/features/credentials/credentials.store';
import { credGroupKey, type SetupCard } from '../instanceAiWorkflowSetup.utils';
import {
createWorkflowDocumentId,
useWorkflowDocumentStore,
} from '@/app/stores/workflowDocument.store';
import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store';
export function useCredentialGroupSelection(
cards: ComputedRef<SetupCard[]>,
@ -17,10 +13,7 @@ export function useCredentialGroupSelection(
projectId?: string,
) {
const uiStore = useUIStore();
const workflowsStore = useWorkflowsStore();
const workflowDocumentStore = computed(() =>
useWorkflowDocumentStore(createWorkflowDocumentId(workflowsStore.workflowId)),
);
const workflowDocumentStore = injectWorkflowDocumentStore();
const credentialsStore = useCredentialsStore();
const nodeTypesStore = useNodeTypesStore();

View File

@ -1,17 +1,13 @@
import type { ComputedRef, Ref } from 'vue';
import { ref, watch, onUnmounted, computed } from 'vue';
import { ref, watch, onUnmounted } from 'vue';
import type { InstanceAiToolCallState, InstanceAiWorkflowSetupNode } from '@n8n/api-types';
import { useNodeHelpers } from '@/app/composables/useNodeHelpers';
import { useTelemetry } from '@/app/composables/useTelemetry';
import { useWorkflowsStore } from '@/app/stores/workflows.store';
import type { INodeUi } from '@/Interface';
import { useRootStore } from '@n8n/stores/useRootStore';
import type { useInstanceAiStore } from '../instanceAi.store';
import type { DisplayCard, SetupCard } from '../instanceAiWorkflowSetup.utils';
import {
createWorkflowDocumentId,
useWorkflowDocumentStore,
} from '@/app/stores/workflowDocument.store';
import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store';
export function useSetupActions(deps: {
requestId: Ref<string>;
@ -34,10 +30,7 @@ export function useSetupActions(deps: {
onApplySuccess?: () => void;
}) {
const telemetry = useTelemetry();
const workflowsStore = useWorkflowsStore();
const workflowDocumentStore = computed(() =>
useWorkflowDocumentStore(createWorkflowDocumentId(workflowsStore.workflowId)),
);
const workflowDocumentStore = injectWorkflowDocumentStore();
const nodeHelpers = useNodeHelpers();
const isSubmitted = ref(false);

View File

@ -1,15 +1,11 @@
import type { ComputedRef, Ref } from 'vue';
import { computed, ref } from 'vue';
import { ref } from 'vue';
import { hasPlaceholderDeep } from '@n8n/utils';
import { NodeHelpers, type INodeProperties } from 'n8n-workflow';
import { useNodeTypesStore } from '@/app/stores/nodeTypes.store';
import { useWorkflowsStore } from '@/app/stores/workflows.store';
import type { IUpdateInformation } from '@/Interface';
import { isNestedParam, isParamValueSet, type SetupCard } from '../instanceAiWorkflowSetup.utils';
import {
createWorkflowDocumentId,
useWorkflowDocumentStore,
} from '@/app/stores/workflowDocument.store';
import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store';
/** Check if the original node parameter value was a placeholder sentinel. */
function isOriginalValuePlaceholder(req: SetupCard['nodes'][0], paramName: string): boolean {
@ -21,10 +17,7 @@ export function useSetupCardParameters(
trackedParamNames: Ref<Map<string, Set<string>>>,
cardHasParamWork: (card: SetupCard) => boolean,
) {
const workflowsStore = useWorkflowsStore();
const workflowDocumentStore = computed(() =>
useWorkflowDocumentStore(createWorkflowDocumentId(workflowsStore.workflowId)),
);
const workflowDocumentStore = injectWorkflowDocumentStore();
const nodeTypesStore = useNodeTypesStore();
const paramValues = ref<Record<string, Record<string, unknown>>>({});

View File

@ -4,7 +4,6 @@ import type { InstanceAiWorkflowSetupNode } from '@n8n/api-types';
import { hasPlaceholderDeep } from '@n8n/utils';
import { NodeConnectionTypes, NodeHelpers } from 'n8n-workflow';
import { useNodeTypesStore } from '@/app/stores/nodeTypes.store';
import { useWorkflowsStore } from '@/app/stores/workflows.store';
import { useCredentialsStore } from '@/features/credentials/credentials.store';
import { getNodeParametersIssues } from '@/features/setupPanel/setupPanel.utils';
import {
@ -14,20 +13,14 @@ import {
type SetupCard,
type SetupCardGroup,
} from '../instanceAiWorkflowSetup.utils';
import {
createWorkflowDocumentId,
useWorkflowDocumentStore,
} from '@/app/stores/workflowDocument.store';
import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store';
export function useSetupCards(
setupRequests: Ref<InstanceAiWorkflowSetupNode[]>,
getCardCredentialId: (card: SetupCard) => string | null,
isCredentialTypeTestable: (name: string) => boolean,
) {
const workflowsStore = useWorkflowsStore();
const workflowDocumentStore = computed(() =>
useWorkflowDocumentStore(createWorkflowDocumentId(workflowsStore.workflowId)),
);
const workflowDocumentStore = injectWorkflowDocumentStore();
const nodeTypesStore = useNodeTypesStore();
const credentialsStore = useCredentialsStore();

View File

@ -31,10 +31,7 @@ import { useNDVStore } from '@/features/ndv/shared/ndv.store';
import { useNodeTypesStore } from '@/app/stores/nodeTypes.store';
import { useSettingsStore } from '@/app/stores/settings.store';
import { useUIStore } from '@/app/stores/ui.store';
import {
createWorkflowDocumentId,
useWorkflowDocumentStore,
} from '@/app/stores/workflowDocument.store';
import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store';
import { useWorkflowsStore } from '@/app/stores/workflows.store';
import type { Project, ProjectSharingData } from '@/features/collaboration/projects/projects.types';
import { getResourcePermissions } from '@n8n/permissions';
@ -129,11 +126,7 @@ const useCustomOAuth = ref(false);
const pendingAuthType = ref<string | null>(null);
const credentialDataCache = ref<Record<string, ICredentialDataDecryptedObject>>({});
const workflowDocumentStore = computed(() =>
workflowsStore.workflowId
? useWorkflowDocumentStore(createWorkflowDocumentId(workflowsStore.workflowId))
: undefined,
);
const workflowDocumentStore = injectWorkflowDocumentStore();
const contextNode = computed<INode | null>(() => {
if (ndvStore.activeNode) return ndvStore.activeNode;
@ -893,7 +886,7 @@ async function saveCredential(): Promise<ICredentialsResponse | null> {
}
const appliedAuthType = pendingAuthType.value;
if (appliedAuthType && contextNode.value && workflowDocumentStore.value) {
if (appliedAuthType && contextNode.value) {
updateNodeAuthType(
workflowDocumentStore.value.updateNodeProperties,
contextNode.value,

View File

@ -9,10 +9,7 @@ import WorkflowExecutionsInfoAccordion from './WorkflowExecutionsInfoAccordion.v
import { useI18n } from '@n8n/i18n';
import { N8nButton, N8nHeading, N8nText } from '@n8n/design-system';
import {
createWorkflowDocumentId,
useWorkflowDocumentStore,
} from '@/app/stores/workflowDocument.store';
import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store';
const router = useRouter();
const route = useRoute();
const locale = useI18n();
@ -20,9 +17,7 @@ const locale = useI18n();
const workflowId = useInjectWorkflowId();
const uiStore = useUIStore();
const workflowsStore = useWorkflowsStore();
const workflowDocumentStore = computed(() =>
useWorkflowDocumentStore(createWorkflowDocumentId(workflowsStore.workflowId)),
);
const workflowDocumentStore = injectWorkflowDocumentStore();
const executionCount = computed(() => workflowsStore.currentWorkflowExecutions.length);
const containsTrigger = computed(() => workflowDocumentStore.value.workflowTriggerNodes.length > 0);

View File

@ -48,6 +48,7 @@ const { mockWorkflowDocumentStore } = vi.hoisted(() => ({
vi.mock('@/app/stores/workflowDocument.store', () => ({
useWorkflowDocumentStore: vi.fn().mockReturnValue(mockWorkflowDocumentStore),
createWorkflowDocumentId: vi.fn().mockReturnValue('test-id'),
injectWorkflowDocumentStore: () => ({ value: mockWorkflowDocumentStore }),
}));
let workflowState: WorkflowState;

View File

@ -8,10 +8,7 @@ import { EnterpriseEditionFeature, MODAL_CONFIRM, VIEWS } from '@/app/constants'
import { DEBUG_PAYWALL_MODAL_KEY } from '../executions.constants';
import type { INodeUi } from '@/Interface';
import { useWorkflowsStore } from '@/app/stores/workflows.store';
import {
useWorkflowDocumentStore,
createWorkflowDocumentId,
} from '@/app/stores/workflowDocument.store';
import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store';
import { useSettingsStore } from '@/app/stores/settings.store';
import { useUIStore } from '@/app/stores/ui.store';
import { useTelemetry } from '@/app/composables/useTelemetry';
@ -34,9 +31,7 @@ export const useExecutionDebugging = (providedWorkflowState?: WorkflowState) =>
const message = useMessage();
const toast = useToast();
const workflowsStore = useWorkflowsStore();
const workflowDocumentStore = computed(() =>
useWorkflowDocumentStore(createWorkflowDocumentId(workflowsStore.workflowId)),
);
const workflowDocumentStore = injectWorkflowDocumentStore();
const workflowState = providedWorkflowState ?? injectWorkflowState();
const settingsStore = useSettingsStore();
const uiStore = useUIStore();

View File

@ -5,7 +5,6 @@ import { useExecutionsStore } from '../executions.store';
import { useI18n } from '@n8n/i18n';
import type { ExecutionFilterType } from '../executions.types';
import type { IWorkflowDb } from '@/Interface';
import { useWorkflowsStore } from '@/app/stores/workflows.store';
import { useWorkflowsListStore } from '@/app/stores/workflowsList.store';
import { NO_NETWORK_ERROR_CODE } from '@n8n/rest-api-client';
import { useToast } from '@/app/composables/useToast';
@ -16,16 +15,10 @@ import type { ExecutionSummary } from 'n8n-workflow';
import { useDebounce } from '@/app/composables/useDebounce';
import { useTelemetry } from '@/app/composables/useTelemetry';
import { executionRetryMessage } from '../executions.utils';
import {
createWorkflowDocumentId,
useWorkflowDocumentStore,
} from '@/app/stores/workflowDocument.store';
import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store';
const executionsStore = useExecutionsStore();
const workflowsStore = useWorkflowsStore();
const workflowDocumentStore = computed(() =>
useWorkflowDocumentStore(createWorkflowDocumentId(workflowsStore.workflowId)),
);
const workflowDocumentStore = injectWorkflowDocumentStore();
const workflowsListStore = useWorkflowsListStore();
const i18n = useI18n();
const telemetry = useTelemetry();

View File

@ -15,11 +15,7 @@ import { useLogsStore } from '@/app/stores/logs.store';
import { useLogsPanelLayout } from '@/features/execution/logs/composables/useLogsPanelLayout';
import { type KeyMap } from '@/app/composables/useKeybindings';
import LogsViewKeyboardEventListener from './LogsViewKeyboardEventListener.vue';
import { useWorkflowsStore } from '@/app/stores/workflows.store';
import {
useWorkflowDocumentStore,
createWorkflowDocumentId,
} from '@/app/stores/workflowDocument.store';
import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store';
import { N8nResizeWrapper } from '@n8n/design-system';
const props = withDefaults(defineProps<{ isReadOnly?: boolean }>(), { isReadOnly: false });
@ -31,10 +27,7 @@ const popOutContent = useTemplateRef('popOutContent');
const logsStore = useLogsStore();
const ndvStore = injectNDVStore();
const workflowsStore = useWorkflowsStore();
const workflowDocumentStore = computed(() =>
useWorkflowDocumentStore(createWorkflowDocumentId(workflowsStore.workflowId)),
);
const workflowDocumentStore = injectWorkflowDocumentStore();
const workflowName = computed(() => workflowDocumentStore.value.name);
const {

View File

@ -3,10 +3,7 @@ import { useNodeHelpers } from '@/app/composables/useNodeHelpers';
import { useRunWorkflow } from '@/app/composables/useRunWorkflow';
import { VIEWS } from '@/app/constants';
import { useWorkflowsStore } from '@/app/stores/workflows.store';
import {
useWorkflowDocumentStore,
createWorkflowDocumentId,
} from '@/app/stores/workflowDocument.store';
import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store';
import { useRootStore } from '@n8n/stores/useRootStore';
import MessageWithButtons from '@n8n/chat/components/MessageWithButtons.vue';
import { chatEventBus } from '@n8n/chat/event-buses';
@ -47,9 +44,7 @@ export function useChatState(
): ChatState {
const locale = useI18n();
const workflowsStore = useWorkflowsStore();
const workflowDocumentStore = computed(() =>
useWorkflowDocumentStore(createWorkflowDocumentId(workflowsStore.workflowId)),
);
const workflowDocumentStore = injectWorkflowDocumentStore();
const workflowState = injectWorkflowState();
const rootStore = useRootStore();
const logsStore = useLogsStore();

View File

@ -3,18 +3,13 @@ import { useWorkflowsStore } from '@/app/stores/workflows.store';
import { computed } from 'vue';
import { useRoute } from 'vue-router';
import { useNodeTypesStore } from '@/app/stores/nodeTypes.store';
import {
createWorkflowDocumentId,
useWorkflowDocumentStore,
} from '@/app/stores/workflowDocument.store';
import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store';
export function useClearExecutionButtonVisible() {
const route = useRoute();
const sourceControlStore = useSourceControlStore();
const workflowsStore = useWorkflowsStore();
const workflowDocumentStore = computed(() =>
useWorkflowDocumentStore(createWorkflowDocumentId(workflowsStore.workflowId)),
);
const workflowDocumentStore = injectWorkflowDocumentStore();
const workflowExecutionData = computed(() => workflowsStore.workflowExecutionData);
const isWorkflowRunning = computed(() => workflowsStore.isWorkflowRunning);
const isReadOnlyRoute = computed(() => !!route?.meta?.readOnlyCanvas);

View File

@ -2,10 +2,7 @@ import { watch, computed, ref, type ComputedRef } from 'vue';
import type { IExecutionResponse } from '@/features/execution/executions/executions.types';
import { Workflow, type IRunExecutionData, type ITaskStartedData } from 'n8n-workflow';
import { useWorkflowsStore } from '@/app/stores/workflows.store';
import {
useWorkflowDocumentStore,
createWorkflowDocumentId,
} from '@/app/stores/workflowDocument.store';
import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store';
import { useNodeHelpers } from '@/app/composables/useNodeHelpers';
import {
copyExecutionData,
@ -35,9 +32,7 @@ export function useLogsExecutionData({ isEnabled, filter }: UseLogsExecutionData
const nodeHelpers = useNodeHelpers();
const workflowsStore = useWorkflowsStore();
const nodeTypesStore = useNodeTypesStore();
const workflowDocumentStore = computed(() =>
useWorkflowDocumentStore(createWorkflowDocumentId(workflowsStore.workflowId)),
);
const workflowDocumentStore = injectWorkflowDocumentStore();
const workflowState = injectWorkflowState();
const toast = useToast();

View File

@ -15,11 +15,7 @@ import { useUIStore } from '@/app/stores/ui.store';
import { shallowRef, watch } from 'vue';
import { computed } from 'vue';
import type { Ref, ComputedRef } from 'vue';
import { useWorkflowsStore } from '@/app/stores/workflows.store';
import {
useWorkflowDocumentStore,
createWorkflowDocumentId,
} from '@/app/stores/workflowDocument.store';
import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store';
export function useLogsSelection(
execution: ComputedRef<IExecutionResponse | undefined>,
@ -37,10 +33,7 @@ export function useLogsSelection(
const logsStore = useLogsStore();
const uiStore = useUIStore();
const canvasStore = useCanvasStore();
const workflowsStore = useWorkflowsStore();
const workflowDocumentStore = computed(() =>
useWorkflowDocumentStore(createWorkflowDocumentId(workflowsStore.workflowId)),
);
const workflowDocumentStore = injectWorkflowDocumentStore();
function syncSelectionToCanvasIfEnabled(value: LogEntry) {
if (!logsStore.isLogSelectionSyncedWithCanvas) {

View File

@ -29,11 +29,7 @@ import { createEventBus } from '@n8n/utils/event-bus';
import { useLogStreamingStore } from '../logStreaming.store';
import { useNDVStore } from '@/features/ndv/shared/ndv.store';
import { useWorkflowsStore } from '@/app/stores/workflows.store';
import {
useWorkflowDocumentStore,
createWorkflowDocumentId,
} from '@/app/stores/workflowDocument.store';
import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store';
import ParameterInputList from '@/features/ndv/parameters/components/ParameterInputList.vue';
import type { IMenuItem, IUpdateInformation, ModalKey } from '@/Interface';
import { LOG_STREAM_MODAL_KEY, MODAL_CONFIRM } from '@/app/constants';
@ -88,10 +84,7 @@ const { confirm } = useMessage();
const telemetry = useTelemetry();
const logStreamingStore = useLogStreamingStore();
const ndvStore = useNDVStore();
const workflowsStore = useWorkflowsStore();
const workflowDocumentStore = computed(() =>
useWorkflowDocumentStore(createWorkflowDocumentId(workflowsStore.workflowId)),
);
const workflowDocumentStore = injectWorkflowDocumentStore();
const uiStore = useUIStore();
const unchanged = ref(!isNew);

View File

@ -14,11 +14,7 @@ import { createEventBus } from '@n8n/utils/event-bus';
import { useDocumentTitle } from '@/app/composables/useDocumentTitle';
import { useI18n } from '@n8n/i18n';
import { usePageRedirectionHelper } from '@/app/composables/usePageRedirectionHelper';
import { useWorkflowsStore } from '@/app/stores/workflows.store';
import {
useWorkflowDocumentStore,
createWorkflowDocumentId,
} from '@/app/stores/workflowDocument.store';
import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store';
import { ElCol, ElRow, ElSwitch } from 'element-plus';
import { N8nActionBox, N8nButton, N8nHeading, N8nInfoTip, N8nNotice } from '@n8n/design-system';
@ -26,10 +22,7 @@ const environment = process.env.NODE_ENV;
const settingsStore = useSettingsStore();
const logStreamingStore = useLogStreamingStore();
const workflowsStore = useWorkflowsStore();
const workflowDocumentStore = computed(() =>
useWorkflowDocumentStore(createWorkflowDocumentId(workflowsStore.workflowId)),
);
const workflowDocumentStore = injectWorkflowDocumentStore();
const uiStore = useUIStore();
const credentialsStore = useCredentialsStore();
const documentTitle = useDocumentTitle();

View File

@ -33,10 +33,7 @@ import {
N8nText,
type ResizeData,
} from '@n8n/design-system';
import {
createWorkflowDocumentId,
useWorkflowDocumentStore,
} from '@/app/stores/workflowDocument.store';
import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store';
const DEFAULT_LEFT_SIDEBAR_WIDTH = 360;
type Props = {
@ -62,9 +59,7 @@ const emit = defineEmits<{
const ndvStore = injectNDVStore();
const workflowsStore = useWorkflowsStore();
const workflowDocumentStore = computed(() =>
useWorkflowDocumentStore(createWorkflowDocumentId(workflowsStore.workflowId)),
);
const workflowDocumentStore = injectWorkflowDocumentStore();
const telemetry = useTelemetry();
const i18n = useI18n();

View File

@ -23,21 +23,18 @@ vi.mock('@n8n/stores/useRootStore', () => ({
})),
}));
vi.mock('@/app/stores/workflows.store', () => ({
useWorkflowsStore: vi.fn(() => ({
workflowId: '123',
})),
}));
const { mockGetNodeByName } = vi.hoisted(() => ({
mockGetNodeByName: vi.fn(),
}));
const mockDocumentStore = vi.hoisted(() => ({
getNodeByName: mockGetNodeByName,
}));
vi.mock('@/app/stores/workflowDocument.store', () => ({
useWorkflowDocumentStore: vi.fn(() => ({
getNodeByName: mockGetNodeByName,
})),
useWorkflowDocumentStore: vi.fn(() => mockDocumentStore),
createWorkflowDocumentId: vi.fn().mockReturnValue('test-id'),
injectWorkflowDocumentStore: () => ({ value: mockDocumentStore }),
}));
describe('schemaPreview.store', () => {
@ -116,15 +113,14 @@ describe('schemaPreview.store', () => {
it('should track both the preview schema and the output one', async () => {
const store = useSchemaPreviewStore();
mockGetNodeByName.mockReturnValueOnce(
await store.trackSchemaPreviewExecution(
'123',
mock<INode>({
id: 'test-node-id',
type: options.nodeType,
typeVersion: options.version,
parameters: { resource: options.resource, operation: options.operation },
}),
);
await store.trackSchemaPreviewExecution(
mock<PushPayload<'nodeExecuteAfterData'>>({
nodeName: 'Test',
data: {
@ -147,25 +143,11 @@ describe('schemaPreview.store', () => {
});
});
it('should not track nodes without a schema preview', async () => {
const store = useSchemaPreviewStore();
mockGetNodeByName.mockReturnValueOnce(mock<INode>());
await store.trackSchemaPreviewExecution(
mock<PushPayload<'nodeExecuteAfterData'>>({
nodeName: 'Test',
data: {
executionStatus: 'success',
data: { main: [[{ json: { foo: 'bar', quz: 'qux' } }]] },
},
}),
);
expect(useTelemetry().track).not.toHaveBeenCalled();
});
it('should not track failed executions', async () => {
const store = useSchemaPreviewStore();
await store.trackSchemaPreviewExecution(
'123',
mock<INode>({}),
mock<PushPayload<'nodeExecuteAfterData'>>({
data: {
executionStatus: 'error',

View File

@ -1,16 +1,11 @@
import * as schemaPreviewApi from './schemaPreview.api';
import { createResultError, createResultOk, type Result } from 'n8n-workflow';
import { createResultError, createResultOk, type INode, type Result } from 'n8n-workflow';
import { defineStore } from 'pinia';
import { computed, reactive } from 'vue';
import { reactive } from 'vue';
import { useRootStore } from '@n8n/stores/useRootStore';
import type { JSONSchema7 } from 'json-schema';
import type { PushPayload } from '@n8n/api-types';
import { useTelemetry } from '@/app/composables/useTelemetry';
import { useWorkflowsStore } from '@/app/stores/workflows.store';
import {
useWorkflowDocumentStore,
createWorkflowDocumentId,
} from '@/app/stores/workflowDocument.store';
import { generateJsonSchema } from '@/app/utils/json-schema';
export const useSchemaPreviewStore = defineStore('schemaPreview', () => {
@ -22,10 +17,6 @@ export const useSchemaPreviewStore = defineStore('schemaPreview', () => {
const rootStore = useRootStore();
const telemetry = useTelemetry();
const workflowsStore = useWorkflowsStore();
const workflowDocumentStore = computed(() =>
useWorkflowDocumentStore(createWorkflowDocumentId(workflowsStore.workflowId)),
);
function getSchemaPreviewKey({
nodeType,
@ -55,15 +46,11 @@ export const useSchemaPreviewStore = defineStore('schemaPreview', () => {
}
}
async function trackSchemaPreviewExecution(pushEvent: PushPayload<'nodeExecuteAfterData'>) {
if (schemaPreviews.size === 0 || pushEvent.data.executionStatus !== 'success') {
return;
}
const node = workflowDocumentStore.value.getNodeByName(pushEvent.nodeName) ?? null;
if (!node) return;
async function trackSchemaPreviewExecution(
workflowId: string,
node: INode,
pushEvent: PushPayload<'nodeExecuteAfterData'>,
) {
const {
id,
type,
@ -84,7 +71,7 @@ export const useSchemaPreviewStore = defineStore('schemaPreview', () => {
node_operation: operation,
schema_preview: JSON.stringify(result.result),
output_schema: JSON.stringify(generateJsonSchema(pushEvent.data.data?.main?.[0]?.[0]?.json)),
workflow_id: workflowsStore.workflowId,
workflow_id: workflowId,
});
}

View File

@ -1,6 +1,6 @@
import get from 'lodash/get';
import set from 'lodash/set';
import { computed, type Ref } from 'vue';
import { type Ref } from 'vue';
import {
type INode,
type INodeParameters,
@ -24,17 +24,13 @@ import {
import { useNodeTypesStore } from '@/app/stores/nodeTypes.store';
import { useFocusPanelStore } from '@/app/stores/focusPanel.store';
import { useNDVStore } from '@/features/ndv/shared/ndv.store';
import { useWorkflowsStore } from '@/app/stores/workflows.store';
import { CHAT_TRIGGER_NODE_TYPE, KEEP_AUTH_IN_NDV_FOR_NODES } from '@/app/constants';
import {
getMainAuthField,
getNodeAuthFields,
isAuthRelatedParameter,
} from '@/app/utils/nodeTypesUtils';
import {
useWorkflowDocumentStore,
createWorkflowDocumentId,
} from '@/app/stores/workflowDocument.store';
import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store';
import { useSettingsStore } from '@/app/stores/settings.store';
const hasPublicDisplayCondition = (parameter: INodeProperties, value: boolean) =>
@ -58,10 +54,7 @@ const stripPublicDisplayCondition = (parameter: INodeProperties): INodePropertie
};
export function useNodeSettingsParameters() {
const workflowsStore = useWorkflowsStore();
const workflowDocumentStore = computed(() =>
useWorkflowDocumentStore(createWorkflowDocumentId(workflowsStore.workflowId)),
);
const workflowDocumentStore = injectWorkflowDocumentStore();
const nodeTypesStore = useNodeTypesStore();
const settingsStore = useSettingsStore();
const telemetry = useTelemetry();

View File

@ -2,14 +2,10 @@ import { useCommunityNodesStore } from '../communityNodes.store';
import { useCredentialsStore } from '@/features/credentials/credentials.store';
import { useNodeTypesStore } from '@/app/stores/nodeTypes.store';
import { useUsersStore } from '@/features/settings/users/users.store';
import { computed, nextTick, ref } from 'vue';
import { nextTick, ref } from 'vue';
import { i18n } from '@n8n/i18n';
import { useToast } from '@/app/composables/useToast';
import { useWorkflowsStore } from '@/app/stores/workflows.store';
import {
useWorkflowDocumentStore,
createWorkflowDocumentId,
} from '@/app/stores/workflowDocument.store';
import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store';
import { useCanvasOperations } from '@/app/composables/useCanvasOperations';
import { removePreviewToken } from '@/features/shared/nodeCreator/nodeCreator.utils';
import { useTelemetry } from '@/app/composables/useTelemetry';
@ -43,10 +39,7 @@ export function useInstallNode() {
const communityNodesStore = useCommunityNodesStore();
const nodeTypesStore = useNodeTypesStore();
const credentialsStore = useCredentialsStore();
const workflowsStore = useWorkflowsStore();
const workflowDocumentStore = computed(() =>
useWorkflowDocumentStore(createWorkflowDocumentId(workflowsStore.workflowId)),
);
const workflowDocumentStore = injectWorkflowDocumentStore();
const userStore = useUsersStore();
const loading = ref(false);
const toast = useToast();

View File

@ -16,12 +16,8 @@ import type { INodeUpdatePropertiesInformation, IUpdateInformation } from '@/Int
import { useNodeTypesStore } from '@/app/stores/nodeTypes.store';
import { useNodeHelpers } from '@/app/composables/useNodeHelpers';
import { isHttpRequestNodeType } from '@/features/setupPanel/setupPanel.utils';
import { injectNDVStore } from '@/features/ndv/shared/ndv.store';
import { useWorkflowsStore } from '@/app/stores/workflows.store';
import {
createWorkflowDocumentId,
useWorkflowDocumentStore,
} from '@/app/stores/workflowDocument.store';
import { useNDVStore } from '@/features/ndv/shared/ndv.store';
import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store';
const NESTED_PARAM_TYPES = new Set([
'collection',
@ -55,11 +51,8 @@ const emit = defineEmits<{
const i18n = useI18n();
const nodeTypesStore = useNodeTypesStore();
const nodeHelpers = useNodeHelpers();
const workflowsStore = useWorkflowsStore();
const workflowDocumentStore = computed(() =>
useWorkflowDocumentStore(createWorkflowDocumentId(workflowsStore.workflowId)),
);
const ndvStore = injectNDVStore();
const workflowDocumentStore = injectWorkflowDocumentStore();
const ndvStore = useNDVStore();
const nodeType = computed(() =>
nodeTypesStore.getNodeType(props.state.node.type, props.state.node.typeVersion),

View File

@ -8,10 +8,7 @@ import { useWorkflowsStore } from '@/app/stores/workflows.store';
import { getTriggerNodeServiceName } from '@/app/utils/nodeTypesUtils';
import { CHAT_TRIGGER_NODE_TYPE } from '@/app/constants/nodeTypes';
import { useLogsStore } from '@/app/stores/logs.store';
import {
createWorkflowDocumentId,
useWorkflowDocumentStore,
} from '@/app/stores/workflowDocument.store';
import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store';
/**
* Wraps `useNodeExecution` with listening-hint logic for setup-panel cards.
@ -25,9 +22,7 @@ export function useTriggerExecution(
const i18n = useI18n();
const nodeTypesStore = useNodeTypesStore();
const workflowsStore = useWorkflowsStore();
const workflowDocumentStore = computed(() =>
useWorkflowDocumentStore(createWorkflowDocumentId(workflowsStore.workflowId)),
);
const workflowDocumentStore = injectWorkflowDocumentStore();
const logsStore = useLogsStore();
const {

View File

@ -38,7 +38,8 @@ const mockWorkflowDocumentStore = {
name: '',
settings: {},
getPinDataSnapshot: vi.fn().mockReturnValue({}),
connectionsByDestinationNode: {},
connectionsBySourceNode: {} as Record<string, unknown>,
connectionsByDestinationNode: {} as Record<string, unknown>,
workflowTriggerNodes: [] as INodeUi[],
};
@ -48,6 +49,7 @@ vi.mock('@/app/stores/workflowDocument.store', async () => {
...actual,
useWorkflowDocumentStore: vi.fn(() => mockWorkflowDocumentStore),
createWorkflowDocumentId: vi.fn().mockReturnValue('test-id'),
injectWorkflowDocumentStore: () => ({ value: mockWorkflowDocumentStore }),
};
});
@ -126,6 +128,8 @@ describe('useWorkflowSetupState node grouping', () => {
mockGetNodeTypeDisplayableCredentials.mockReturnValue([]);
mockWorkflowDocumentStore.allNodes = [];
mockWorkflowDocumentStore.connectionsBySourceNode = {};
mockWorkflowDocumentStore.connectionsByDestinationNode = {};
mockWorkflowDocumentStore.getNodeByName = vi.fn();
mockWorkflowDocumentStore.getNodes = vi.fn();
mockUpdateNodeProperties.mockReset();

View File

@ -1,4 +1,4 @@
import { ref, nextTick } from 'vue';
import { ref, shallowRef, nextTick } from 'vue';
import { createTestingPinia } from '@pinia/testing';
import { createTestNode } from '@/__tests__/mocks';
@ -55,6 +55,7 @@ vi.mock('@/app/stores/workflowDocument.store', async () => {
return {
...actual,
useWorkflowDocumentStore: vi.fn(() => mockWorkflowDocumentStore),
injectWorkflowDocumentStore: () => shallowRef(mockWorkflowDocumentStore),
createWorkflowDocumentId: vi.fn().mockReturnValue('test-id'),
};
});

View File

@ -17,10 +17,7 @@ import {
import { useNodeHelpers } from '@/app/composables/useNodeHelpers';
import { useNodeTypesStore } from '@/app/stores/nodeTypes.store';
import { useEnvironmentsStore } from '@/features/settings/environments.ee/environments.store';
import {
useWorkflowDocumentStore,
createWorkflowDocumentId,
} from '@/app/stores/workflowDocument.store';
import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store';
import {
getNodeCredentialTypes,
@ -63,9 +60,7 @@ export const useWorkflowSetupState = (
const nodeHelpers = useNodeHelpers();
const environmentsStore = useEnvironmentsStore();
const templatesStore = useTemplatesStore();
const workflowDocumentStore = computed(() =>
useWorkflowDocumentStore(createWorkflowDocumentId(workflowsStore.workflowId)),
);
const workflowDocumentStore = injectWorkflowDocumentStore();
const sourceNodes = computed(() => nodes?.value ?? workflowDocumentStore.value.allNodes);

View File

@ -10,10 +10,7 @@ import { type CommandBarItem } from '@n8n/design-system/components/N8nCommandBar
import type { CommandGroup } from '../types';
import { useSourceControlStore } from '@/features/integrations/sourceControl.ee/sourceControl.store';
import { useWorkflowsStore } from '@/app/stores/workflows.store';
import {
useWorkflowDocumentStore,
createWorkflowDocumentId,
} from '@/app/stores/workflowDocument.store';
import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store';
import { useCollaborationStore } from '@/features/collaboration/collaboration/collaboration.store';
import { getResourcePermissions } from '@n8n/permissions';
import NodeIcon from '@/app/components/NodeIcon.vue';
@ -41,9 +38,7 @@ export function useNodeCommands(options: {
const collaborationStore = useCollaborationStore();
const { generateMergedNodesAndActions } = useActionsGenerator();
const workflowDocumentStore = computed(() =>
useWorkflowDocumentStore(createWorkflowDocumentId(workflowsStore.workflowId)),
);
const workflowDocumentStore = injectWorkflowDocumentStore();
const isReadOnly = computed(
() => sourceControlStore.preferences.branchReadOnly || collaborationStore.shouldBeReadOnly,

View File

@ -5,16 +5,12 @@ import { useI18n } from '@n8n/i18n';
import { useRouter } from 'vue-router';
import { useLocalStorage } from '@vueuse/core';
import { VIEWS } from '@/app/constants';
import { useWorkflowsStore } from '@/app/stores/workflows.store';
import { useWorkflowsListStore } from '@/app/stores/workflowsList.store';
import { useNodeTypesStore } from '@/app/stores/nodeTypes.store';
import { N8nIcon } from '@n8n/design-system';
import NodeIcon from '@/app/components/NodeIcon.vue';
import { useCanvasOperations } from '@/app/composables/useCanvasOperations';
import {
createWorkflowDocumentId,
useWorkflowDocumentStore,
} from '@/app/stores/workflowDocument.store';
import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store';
const MAX_RECENT_ITEMS = 5;
const MAX_RECENT_WORKFLOWS_TO_DISPLAY = 3;
@ -36,10 +32,7 @@ type RecentNodesMap = Record<string, RecentNode[]>;
export function useRecentResources() {
const i18n = useI18n();
const router = useRouter();
const workflowsStore = useWorkflowsStore();
const workflowDocumentStore = computed(() =>
useWorkflowDocumentStore(createWorkflowDocumentId(workflowsStore.workflowId)),
);
const workflowDocumentStore = injectWorkflowDocumentStore();
const workflowsListStore = useWorkflowsListStore();
const nodeTypesStore = useNodeTypesStore();
const { setNodeActive } = useCanvasOperations();

View File

@ -22,10 +22,7 @@ import { canvasEventBus } from '@/features/workflows/canvas/canvas.eventBus';
import type { IWorkflowToShare } from '@/Interface';
import { saveAs } from 'file-saver';
import { useWorkflowsStore } from '@/app/stores/workflows.store';
import {
useWorkflowDocumentStore,
createWorkflowDocumentId,
} from '@/app/stores/workflowDocument.store';
import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store';
import type { CommandGroup, CommandBarItem } from '../types';
import uniqBy from 'lodash/uniqBy';
import { nodeViewEventBus } from '@/app/event-bus';
@ -60,9 +57,7 @@ export function useWorkflowCommands(): CommandGroup {
const sourceControlStore = useSourceControlStore();
const collaborationStore = useCollaborationStore();
const workflowDocumentStore = computed(() =>
useWorkflowDocumentStore(createWorkflowDocumentId(workflowsStore.workflowId)),
);
const workflowDocumentStore = injectWorkflowDocumentStore();
const router = useRouter();

View File

@ -25,10 +25,7 @@ import {
ASK_AI_LOADING_DURATION_MS,
} from '@/app/constants';
import type { AskAiRequest } from '@/features/ai/assistant/assistant.types';
import {
createWorkflowDocumentId,
useWorkflowDocumentStore,
} from '@/app/stores/workflowDocument.store';
import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store';
const emit = defineEmits<{
submit: [code: string];
replaceCode: [code: string];
@ -49,6 +46,7 @@ const props = withDefaults(
const { getSchemaForExecutionData, getInputDataWithPinned } = useDataSchema();
const i18n = useI18n();
const ndvStore = injectNDVStore();
const workflowDocumentStore = injectWorkflowDocumentStore();
const loadingPhraseIndex = ref(0);
const loaderProgress = ref(0);
@ -97,14 +95,12 @@ function getParentNodes() {
if (!activeNode || !workflowId) return [];
const workflowDocumentStore = useWorkflowDocumentStore(createWorkflowDocumentId(workflowId));
return workflowDocumentStore
return workflowDocumentStore.value
.getParentNodesByDepth(activeNode?.name)
.filter(({ name }, i, nodes) => {
return name !== activeNode.name && nodes.findIndex((node) => node.name === name) === i;
})
.map((n) => workflowDocumentStore.getNodeByName(n.name))
.map((n) => workflowDocumentStore.value.getNodeByName(n.name))
.filter((n) => n !== null);
}

View File

@ -19,19 +19,18 @@ import {
import { CHAT_TRIGGER_NODE_TYPE } from 'n8n-workflow';
import type { INodeUi } from '@/Interface';
let mockAllNodes: INodeUi[] = [];
let mockWorkflowTriggerNodes: INodeUi[] = [];
const mockDocumentStoreState = vi.hoisted(() => ({
allNodes: [] as INodeUi[],
workflowTriggerNodes: [] as INodeUi[],
aiNodes: [] as INodeUi[],
name: '',
settings: {},
getPinDataSnapshot: () => ({}),
}));
vi.mock('@/app/stores/workflowDocument.store', () => ({
useWorkflowDocumentStore: () => ({
allNodes: mockAllNodes,
get workflowTriggerNodes() {
return mockWorkflowTriggerNodes;
},
name: '',
settings: {},
getPinDataSnapshot: () => ({}),
}),
useWorkflowDocumentStore: () => mockDocumentStoreState,
createWorkflowDocumentId: (id: string) => `${id}@latest`,
injectWorkflowDocumentStore: () => ({ value: mockDocumentStoreState }),
}));
describe('useActions', () => {
@ -41,14 +40,14 @@ describe('useActions', () => {
afterEach(() => {
vi.clearAllMocks();
mockAllNodes = [];
mockDocumentStoreState.allNodes = [];
});
describe('getAddedNodesAndConnections', () => {
test('should insert a manual trigger node when there are no triggers', () => {
const nodeCreatorStore = useNodeCreatorStore();
mockWorkflowTriggerNodes = [];
mockDocumentStoreState.workflowTriggerNodes = [];
vi.spyOn(nodeCreatorStore, 'openSource', 'get').mockReturnValue(
NODE_CREATOR_OPEN_SOURCES.ADD_NODE_BUTTON,
);
@ -68,7 +67,7 @@ describe('useActions', () => {
test('should not insert a manual trigger node when there is a trigger in the workflow', () => {
const nodeCreatorStore = useNodeCreatorStore();
mockWorkflowTriggerNodes = [{ type: SCHEDULE_TRIGGER_NODE_TYPE } as never];
mockDocumentStoreState.workflowTriggerNodes = [{ type: SCHEDULE_TRIGGER_NODE_TYPE } as never];
vi.spyOn(nodeCreatorStore, 'openSource', 'get').mockReturnValue(
NODE_CREATOR_OPEN_SOURCES.ADD_NODE_BUTTON,
);
@ -83,8 +82,8 @@ describe('useActions', () => {
});
test('should insert a ChatTrigger node when an AI Agent is added on an empty canvas', () => {
mockWorkflowTriggerNodes = [];
mockAllNodes = [];
mockDocumentStoreState.workflowTriggerNodes = [];
mockDocumentStoreState.allNodes = [];
const { getAddedNodesAndConnections } = useActions();
@ -107,8 +106,8 @@ describe('useActions', () => {
});
test('should insert a ChatTrigger node when an AI Agent is added with only a Manual Trigger present', () => {
mockWorkflowTriggerNodes = [{ type: MANUAL_TRIGGER_NODE_TYPE } as never];
mockAllNodes = [{ type: MANUAL_TRIGGER_NODE_TYPE } as INodeUi];
mockDocumentStoreState.workflowTriggerNodes = [{ type: MANUAL_TRIGGER_NODE_TYPE } as never];
mockDocumentStoreState.allNodes = [{ type: MANUAL_TRIGGER_NODE_TYPE } as INodeUi];
const { getAddedNodesAndConnections } = useActions();
@ -131,8 +130,8 @@ describe('useActions', () => {
});
test('should not insert a ChatTrigger node when an AI Agent is added with a non-trigger node prseent', () => {
mockWorkflowTriggerNodes = [{ type: GITHUB_TRIGGER_NODE_TYPE } as never];
mockAllNodes = [
mockDocumentStoreState.workflowTriggerNodes = [{ type: GITHUB_TRIGGER_NODE_TYPE } as never];
mockDocumentStoreState.allNodes = [
{ type: GITHUB_TRIGGER_NODE_TYPE } as INodeUi,
{ type: HTTP_REQUEST_NODE_TYPE } as INodeUi,
];
@ -146,8 +145,8 @@ describe('useActions', () => {
});
test('should not insert a ChatTrigger node when an AI Agent is added with a Chat Trigger already present', () => {
mockWorkflowTriggerNodes = [{ type: CHAT_TRIGGER_NODE_TYPE } as never];
mockAllNodes = [{ type: CHAT_TRIGGER_NODE_TYPE } as INodeUi];
mockDocumentStoreState.workflowTriggerNodes = [{ type: CHAT_TRIGGER_NODE_TYPE } as never];
mockDocumentStoreState.allNodes = [{ type: CHAT_TRIGGER_NODE_TYPE } as INodeUi];
const { getAddedNodesAndConnections } = useActions();
@ -160,7 +159,7 @@ describe('useActions', () => {
test('should insert a No Op node when a Loop Over Items Node is added', () => {
const nodeCreatorStore = useNodeCreatorStore();
mockWorkflowTriggerNodes = [];
mockDocumentStoreState.workflowTriggerNodes = [];
vi.spyOn(nodeCreatorStore, 'openSource', 'get').mockReturnValue(
NODE_CREATOR_OPEN_SOURCES.ADD_NODE_BUTTON,
);
@ -192,7 +191,7 @@ describe('useActions', () => {
const nodeCreatorStore = useNodeCreatorStore();
const nodeTypesStore = useNodeTypesStore();
mockWorkflowTriggerNodes = [{ type: SCHEDULE_TRIGGER_NODE_TYPE } as never];
mockDocumentStoreState.workflowTriggerNodes = [{ type: SCHEDULE_TRIGGER_NODE_TYPE } as never];
vi.spyOn(nodeCreatorStore, 'openSource', 'get').mockReturnValue(
NODE_CREATOR_OPEN_SOURCES.ADD_NODE_BUTTON,
);
@ -229,7 +228,7 @@ describe('useActions', () => {
const nodeCreatorStore = useNodeCreatorStore();
const nodeTypesStore = useNodeTypesStore();
mockWorkflowTriggerNodes = [{ type: SCHEDULE_TRIGGER_NODE_TYPE } as never];
mockDocumentStoreState.workflowTriggerNodes = [{ type: SCHEDULE_TRIGGER_NODE_TYPE } as never];
vi.spyOn(nodeCreatorStore, 'openSource', 'get').mockReturnValue(
NODE_CREATOR_OPEN_SOURCES.ADD_NODE_BUTTON,
);
@ -335,7 +334,7 @@ describe('useActions', () => {
test('should preserve actionName in nodes array', () => {
const nodeCreatorStore = useNodeCreatorStore();
mockWorkflowTriggerNodes = [];
mockDocumentStoreState.workflowTriggerNodes = [];
vi.spyOn(nodeCreatorStore, 'openSource', 'get').mockReturnValue(
NODE_CREATOR_OPEN_SOURCES.ADD_NODE_BUTTON,
);
@ -359,7 +358,7 @@ describe('useActions', () => {
test('should preserve actionName when no trigger is prepended', () => {
const nodeCreatorStore = useNodeCreatorStore();
mockWorkflowTriggerNodes = [{ type: MANUAL_TRIGGER_NODE_TYPE } as never];
mockDocumentStoreState.workflowTriggerNodes = [{ type: MANUAL_TRIGGER_NODE_TYPE } as never];
vi.spyOn(nodeCreatorStore, 'openSource', 'get').mockReturnValue(
NODE_CREATOR_OPEN_SOURCES.ADD_NODE_BUTTON,
);
@ -380,7 +379,7 @@ describe('useActions', () => {
test('should work with multiple nodes having actionNames', () => {
const nodeCreatorStore = useNodeCreatorStore();
mockWorkflowTriggerNodes = [{ type: MANUAL_TRIGGER_NODE_TYPE } as never];
mockDocumentStoreState.workflowTriggerNodes = [{ type: MANUAL_TRIGGER_NODE_TYPE } as never];
vi.spyOn(nodeCreatorStore, 'openSource', 'get').mockReturnValue(
NODE_CREATOR_OPEN_SOURCES.ADD_NODE_BUTTON,
);

View File

@ -38,11 +38,7 @@ import {
import type { BaseTextKey } from '@n8n/i18n';
import type { Telemetry } from '@/app/plugins/telemetry';
import { useNodeCreatorStore } from '@/features/shared/nodeCreator/nodeCreator.store';
import { useWorkflowsStore } from '@/app/stores/workflows.store';
import {
useWorkflowDocumentStore,
createWorkflowDocumentId,
} from '@/app/stores/workflowDocument.store';
import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store';
import { useNodeTypesStore } from '@/app/stores/nodeTypes.store';
import { useExternalHooks } from '@/app/composables/useExternalHooks';
@ -53,14 +49,10 @@ import {
} from '../nodeCreator.utils';
import { useI18n } from '@n8n/i18n';
import { PUSH_NODES_OFFSET } from '@/app/utils/nodeViewUtils';
import { useCanvasStore } from '@/app/stores/canvas.store';
import { CHANGE_ACTION } from '@/app/stores/workflowDocument/types';
export const useActions = () => {
const workflowsStore = useWorkflowsStore();
const workflowDocumentStore = computed(() =>
useWorkflowDocumentStore(createWorkflowDocumentId(workflowsStore.workflowId)),
);
const workflowDocumentStore = injectWorkflowDocumentStore();
const nodeCreatorStore = useNodeCreatorStore();
const nodeTypesStore = useNodeTypesStore();
const i18n = useI18n();
@ -258,10 +250,8 @@ export const useActions = () => {
function shouldPrependManualTrigger(addedNodes: AddedNode[]): boolean {
const { selectedView, openSource } = useNodeCreatorStore();
const { workflowId } = useWorkflowsStore();
const workflowDocumentStore = useWorkflowDocumentStore(createWorkflowDocumentId(workflowId));
const hasTrigger = addedNodes.some((node) => useNodeTypesStore().isTriggerNode(node.type));
const workflowContainsTrigger = workflowDocumentStore.workflowTriggerNodes.length > 0;
const workflowContainsTrigger = workflowDocumentStore.value.workflowTriggerNodes.length > 0;
const isTriggerPanel = selectedView === TRIGGER_NODE_CREATOR_VIEW;
const onlyStickyNodes = addedNodes.every((node) => node.type === STICKY_NODE_TYPE);
@ -294,7 +284,7 @@ export const useActions = () => {
// AI-226: Prepend LLM Chain node when adding a language model
function shouldPrependLLMChain(addedNodes: AddedNode[]): boolean {
const canvasHasAINodes = useCanvasStore().aiNodes.length > 0;
const canvasHasAINodes = workflowDocumentStore.value.aiNodes.length > 0;
if (canvasHasAINodes) return false;
return addedNodes.some((node) => {

View File

@ -1,3 +1,4 @@
import { shallowRef } from 'vue';
import { createTestingPinia } from '@pinia/testing';
import { setActivePinia } from 'pinia';
import { describe, it, expect, vi, beforeEach } from 'vitest';
@ -25,6 +26,7 @@ vi.mock('@/app/stores/workflowDocument.store', async () => {
return {
...actual,
useWorkflowDocumentStore: vi.fn(() => mockWorkflowDocumentStore),
injectWorkflowDocumentStore: () => shallowRef(mockWorkflowDocumentStore),
createWorkflowDocumentId: vi.fn().mockReturnValue('test-id'),
};
});

View File

@ -4,19 +4,11 @@ import type { NodeConnectionType, INodeInputConfiguration } from 'n8n-workflow';
import { NodeHelpers, NodeConnectionTypes, isHitlToolType } from 'n8n-workflow';
import type { NodeCreatorFilter } from './useViewStacks';
import { useNodeTypesStore } from '@/app/stores/nodeTypes.store';
import { useWorkflowsStore } from '@/app/stores/workflows.store';
import { computed } from 'vue';
import {
createWorkflowDocumentId,
useWorkflowDocumentStore,
} from '@/app/stores/workflowDocument.store';
import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store';
export function useGetNodeCreatorFilter() {
const workflowStore = useWorkflowsStore();
const nodeTypesStore = useNodeTypesStore();
const workflowDocumentStore = computed(() =>
useWorkflowDocumentStore(createWorkflowDocumentId(workflowStore.workflowId)),
);
const workflowDocumentStore = injectWorkflowDocumentStore();
function getNodeCreatorFilter(
nodeName: string,

View File

@ -50,7 +50,6 @@ import { useKeyboardNavigation } from './useKeyboardNavigation';
import { useNodeTypesStore } from '@/app/stores/nodeTypes.store';
import { AI_TRANSFORM_NODE_TYPE, NodeConnectionTypes } from 'n8n-workflow';
import type { NodeConnectionType, INodeFilter } from 'n8n-workflow';
import { useCanvasStore } from '@/app/stores/canvas.store';
import { useSettingsStore } from '@/app/stores/settings.store';
export type CommunityNodeDetails = {
@ -69,6 +68,7 @@ import { type NodeIconSource } from '@/app/utils/nodeIcon';
import { getThemedValue } from '@/app/utils/nodeTypesUtils';
import nodePopularity from 'virtual:node-popularity-data';
import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store';
export type NodeCreatorFilter = INodeFilter & {
conditions?: Array<(item: INodeCreateElement) => boolean>;
@ -110,6 +110,7 @@ const nodePopularityMap = Object.values(nodePopularity).reduce((acc, node) => {
export const useViewStacks = defineStore('nodeCreatorViewStacks', () => {
const nodeCreatorStore = useNodeCreatorStore();
const workflowDocumentStore = injectWorkflowDocumentStore();
const { getActiveItemIndex } = useKeyboardNavigation();
const i18n = useI18n();
const settingsStore = useSettingsStore();
@ -124,7 +125,7 @@ export const useViewStacks = defineStore('nodeCreatorViewStacks', () => {
if (stack.search && searchBaseItems.value) {
let searchBase: INodeCreateElement[] = searchBaseItems.value;
const canvasHasAINodes = useCanvasStore().aiNodes.length > 0;
const canvasHasAINodes = workflowDocumentStore.value.aiNodes.length > 0;
if (searchBaseItems.value.length === 0) {
searchBase = flattenCreateElements(stack.baselineItems ?? []);
}
@ -281,7 +282,7 @@ export const useViewStacks = defineStore('nodeCreatorViewStacks', () => {
sortAlphabetically: boolean,
) {
const aiNodes = items.filter((node): node is NodeCreateElement => isAINode(node));
const canvasHasAINodes = useCanvasStore().aiNodes.length > 0;
const canvasHasAINodes = workflowDocumentStore.value.aiNodes.length > 0;
const isVectorStoresCategory = stack?.title === AI_CATEGORY_VECTOR_STORES;
const isToolsCategory = stack?.title === AI_CATEGORY_TOOLS;
if (

View File

@ -23,10 +23,7 @@ import { useTelemetry } from '@/app/composables/useTelemetry';
import { useNodeTypesStore } from '@/app/stores/nodeTypes.store';
import { useUIStore } from '@/app/stores/ui.store';
import { useWorkflowsStore } from '@/app/stores/workflows.store';
import {
useWorkflowDocumentStore,
createWorkflowDocumentId,
} from '@/app/stores/workflowDocument.store';
import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store';
import type { TelemetryNdvType } from '@/app/types/telemetry';
import { getNodeIconSource } from '@/app/utils/nodeIcon';
import { isVueFlowConnection } from '@/app/utils/typeGuards';
@ -48,9 +45,7 @@ import { prepareCommunityNodeDetailsViewStack, transformNodeType } from './nodeC
export const useNodeCreatorStore = defineStore(STORES.NODE_CREATOR, () => {
const workflowsStore = useWorkflowsStore();
const workflowDocumentStore = computed(() =>
useWorkflowDocumentStore(createWorkflowDocumentId(workflowsStore.workflowId)),
);
const workflowDocumentStore = injectWorkflowDocumentStore();
const ndvStore = useNDVStore();
const uiStore = useUIStore();
const nodeTypesStore = useNodeTypesStore();

View File

@ -5,11 +5,7 @@ import { defineStore } from 'pinia';
import { useRootStore } from '@n8n/stores/useRootStore';
import { computed, ref } from 'vue';
import { hasPermission } from '@/app/utils/rbac/permissions';
import {
useWorkflowDocumentStore,
createWorkflowDocumentId,
} from '@/app/stores/workflowDocument.store';
import { useWorkflowsStore } from '@/app/stores/workflows.store';
import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store';
const apiMapping = {
[STORES.TAGS]: createTagsApi('/tags'),
@ -28,7 +24,7 @@ const createTagsStore = (id: typeof STORES.TAGS | typeof STORES.ANNOTATION_TAGS)
const fetchedUsageCount = ref(false);
const rootStore = useRootStore();
const workflowsStore = useWorkflowsStore();
const workflowDocumentStore = injectWorkflowDocumentStore();
// Computed
@ -127,13 +123,7 @@ const createTagsStore = (id: typeof STORES.TAGS | typeof STORES.ANNOTATION_TAGS)
if (deleted) {
deleteTag(id);
// Update workflowDocumentStore (source of truth) if a workflow is active
if (workflowsStore.workflowId) {
const workflowDocumentId = createWorkflowDocumentId(workflowsStore.workflowId);
const workflowDocumentStore = useWorkflowDocumentStore(workflowDocumentId);
workflowDocumentStore.removeTag(id);
}
workflowDocumentStore.value.removeTag(id);
}
return deleted;

View File

@ -10,11 +10,7 @@ import { computed, ref, useCssModule, useTemplateRef } from 'vue';
import type { CanvasEventBusEvents } from '../canvas.types';
import { useCanvasMapping } from '../composables/useCanvasMapping';
import Canvas from './Canvas.vue';
import { useWorkflowsStore } from '@/app/stores/workflows.store';
import {
createWorkflowDocumentId,
useWorkflowDocumentStore,
} from '@/app/stores/workflowDocument.store';
import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store';
defineOptions({
inheritAttrs: false,
@ -43,10 +39,7 @@ const props = withDefaults(
const canvasRef = useTemplateRef('canvas');
const $style = useCssModule();
const workflowsStore = useWorkflowsStore();
const workflowDocumentStore = computed(() =>
useWorkflowDocumentStore(createWorkflowDocumentId(workflowsStore.workflowId)),
);
const workflowDocumentStore = injectWorkflowDocumentStore();
const { onNodesInitialized, viewport, viewportRef, getNodes, fitBounds } = useVueFlow(props.id);

View File

@ -6,6 +6,7 @@ import { Controls } from '@vue-flow/controls';
import { computed } from 'vue';
import { useExperimentalNdvStore } from '../../../experimental/experimentalNdv.store';
import { N8nButton, N8nIconButton, N8nTooltip } from '@n8n/design-system';
import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store';
const props = withDefaults(
defineProps<{
zoom?: number;
@ -30,6 +31,8 @@ const i18n = useI18n();
const experimentalNdvStore = useExperimentalNdvStore();
const workflowDocumentStore = injectWorkflowDocumentStore();
const isExperimentalNdvActive = computed(() => experimentalNdvStore.isActive(props.zoom));
const isToggleZoomVisible = computed(() => experimentalNdvStore.isZoomedViewEnabled);
@ -55,6 +58,10 @@ function onZoomToFit() {
function onTidyUp() {
emit('tidy-up');
}
function handleClickCollapseAll() {
experimentalNdvStore.collapseAllNodes(workflowDocumentStore.value.allNodes);
}
</script>
<template>
<Controls :show-zoom="false" :show-fit-view="false">
@ -166,7 +173,7 @@ function onTidyUp() {
size="large"
icon="minimize-2"
:aria-label="i18n.baseText('nodeView.collapseAllNodes')"
@click="experimentalNdvStore.collapseAllNodes"
@click="handleClickCollapseAll"
/>
</N8nTooltip>
</Controls>

View File

@ -2,21 +2,14 @@
import { computed } from 'vue';
import { useCanvasNode } from '../../../../../composables/useCanvasNode';
import { useI18n } from '@n8n/i18n';
import { useWorkflowsStore } from '@/app/stores/workflows.store';
import { useCredentialsStore } from '@/features/credentials/credentials.store';
import { useDynamicCredentials } from '@/features/resolvers/composables/useDynamicCredentials';
import { N8nIcon, N8nTooltip } from '@n8n/design-system';
import {
createWorkflowDocumentId,
useWorkflowDocumentStore,
} from '@/app/stores/workflowDocument.store';
import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store';
const { name } = useCanvasNode();
const i18n = useI18n();
const workflowsStore = useWorkflowsStore();
const workflowDocumentStore = computed(() =>
useWorkflowDocumentStore(createWorkflowDocumentId(workflowsStore.workflowId)),
);
const workflowDocumentStore = injectWorkflowDocumentStore();
const credentialsStore = useCredentialsStore();
const { isEnabled: isDynamicCredentialsEnabled } = useDynamicCredentials();

View File

@ -6,10 +6,7 @@ import { useRunWorkflow } from '@/app/composables/useRunWorkflow';
import { CHAT_TRIGGER_NODE_TYPE } from '@/app/constants';
import { useLogsStore } from '@/app/stores/logs.store';
import { useWorkflowsStore } from '@/app/stores/workflows.store';
import {
useWorkflowDocumentStore,
createWorkflowDocumentId,
} from '@/app/stores/workflowDocument.store';
import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store';
import { useChatHubPanelStore } from '@/features/ai/chatHub/chatHubPanel.store';
import { computed, useCssModule } from 'vue';
import { useRouter } from 'vue-router';
@ -45,9 +42,7 @@ const containerClass = computed(() => ({
const router = useRouter();
const i18n = useI18n();
const workflowsStore = useWorkflowsStore();
const workflowDocumentStore = computed(() =>
useWorkflowDocumentStore(createWorkflowDocumentId(workflowsStore.workflowId)),
);
const workflowDocumentStore = injectWorkflowDocumentStore();
const logsStore = useLogsStore();
const chatHubPanelStore = useChatHubPanelStore();
const { runEntireWorkflow } = useRunWorkflow({ router });

View File

@ -6,10 +6,7 @@
import { useI18n } from '@n8n/i18n';
import { useNodeTypesStore } from '@/app/stores/nodeTypes.store';
import { useWorkflowsStore } from '@/app/stores/workflows.store';
import {
useWorkflowDocumentStore,
createWorkflowDocumentId,
} from '@/app/stores/workflowDocument.store';
import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store';
import type { Ref } from 'vue';
import { ref, computed } from 'vue';
import type {
@ -79,9 +76,7 @@ export function useCanvasMapping({
}) {
const i18n = useI18n();
const workflowsStore = useWorkflowsStore();
const workflowDocumentStore = computed(() =>
useWorkflowDocumentStore(createWorkflowDocumentId(workflowsStore.workflowId)),
);
const workflowDocumentStore = injectWorkflowDocumentStore();
const workflowState = injectWorkflowState();
const nodeTypesStore = useNodeTypesStore();
const nodeHelpers = useNodeHelpers();

View File

@ -1,10 +1,7 @@
import type { INodeUi } from '@/Interface';
import useEnvironmentsStore from '@/features/settings/environments.ee/environments.store';
import { useWorkflowsStore } from '@/app/stores/workflows.store';
import {
useWorkflowDocumentStore,
createWorkflowDocumentId,
} from '@/app/stores/workflowDocument.store';
import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store';
import type { ExpressionLocalResolveContext } from '@/app/types/expressions';
import { computed, type ComputedRef } from 'vue';
@ -12,9 +9,7 @@ export function useExpressionResolveCtx(node: ComputedRef<INodeUi | null | undef
const environmentsStore = useEnvironmentsStore();
const workflowsStore = useWorkflowsStore();
const workflowDocumentStore = computed(() =>
useWorkflowDocumentStore(createWorkflowDocumentId(workflowsStore.workflowId)),
);
const workflowDocumentStore = injectWorkflowDocumentStore();
return computed<ExpressionLocalResolveContext | undefined>(() => {
if (!node.value || !workflowDocumentStore.value) {

View File

@ -1,10 +1,5 @@
import { computed, ref, shallowRef } from 'vue';
import { defineStore } from 'pinia';
import { useWorkflowsStore } from '@/app/stores/workflows.store';
import {
useWorkflowDocumentStore,
createWorkflowDocumentId,
} from '@/app/stores/workflowDocument.store';
import {
type Dimensions,
type FitView,
@ -17,12 +12,9 @@ import {
import { CanvasNodeRenderType, type CanvasNodeData } from '../canvas.types';
import { usePostHog } from '@/app/stores/posthog.store';
import { CANVAS_ZOOMED_VIEW_EXPERIMENT, NDV_IN_FOCUS_PANEL_EXPERIMENT } from '@/app/constants';
import type { INodeUi } from '@/Interface';
export const useExperimentalNdvStore = defineStore('experimentalNdv', () => {
const workflowStore = useWorkflowsStore();
const workflowDocumentStore = computed(() =>
useWorkflowDocumentStore(createWorkflowDocumentId(workflowStore.workflowId)),
);
const postHogStore = usePostHog();
const isZoomedViewEnabled = computed(
() =>
@ -48,10 +40,8 @@ export const useExperimentalNdvStore = defineStore('experimentalNdv', () => {
};
}
function collapseAllNodes() {
collapsedNodes.value = (workflowDocumentStore.value?.allNodes ?? []).reduce<
Partial<Record<string, boolean>>
>((acc, node) => {
function collapseAllNodes(allNodes: INodeUi[]) {
collapsedNodes.value = allNodes.reduce<Partial<Record<string, boolean>>>((acc, node) => {
acc[node.id] = true;
return acc;
}, {});

View File

@ -2,27 +2,15 @@ import { computed } from 'vue';
import type { INodeCredentialsDetails } from 'n8n-workflow';
import { useNodeHelpers } from '@/app/composables/useNodeHelpers';
import { useCredentialsStore } from '@/features/credentials/credentials.store';
import { useWorkflowsStore } from '@/app/stores/workflows.store';
import type { TemplateCredentialKey } from '../utils/templateTransforms';
import { useCredentialSetupState } from './useCredentialSetupState';
import {
useWorkflowDocumentStore,
createWorkflowDocumentId,
} from '@/app/stores/workflowDocument.store';
import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store';
export const useSetupWorkflowCredentialsModalState = () => {
const workflowsStore = useWorkflowsStore();
const credentialsStore = useCredentialsStore();
const nodeHelpers = useNodeHelpers();
// This composable is used inside a modal that renders outside the WorkflowLayout
// provider tree, so we can't use injectWorkflowDocumentStore(). Instead, we
// access the Pinia store directly using the current workflow ID.
const workflowDocumentStore = computed(() => {
const workflowId = workflowsStore.workflowId;
if (!workflowId) return undefined;
return useWorkflowDocumentStore(createWorkflowDocumentId(workflowId));
});
const workflowDocumentStore = injectWorkflowDocumentStore();
const workflowNodes = computed(() => {
return workflowDocumentStore.value?.allNodes ?? [];

View File

@ -17,15 +17,17 @@ vi.mock('@/app/stores/workflows.store', () => ({
}),
}));
vi.mock('@/app/stores/workflowDocument.store', () => ({
useWorkflowDocumentStore: () => ({
createWorkflowObject: vi.fn().mockReturnValue({
id: 'test-workflow',
nodes: [],
connections: {},
}),
const mockDocumentStore = vi.hoisted(() => ({
createWorkflowObject: vi.fn().mockReturnValue({
id: 'test-workflow',
nodes: [],
connections: {},
}),
}));
vi.mock('@/app/stores/workflowDocument.store', () => ({
useWorkflowDocumentStore: () => mockDocumentStore,
createWorkflowDocumentId: vi.fn().mockReturnValue('test-id'),
injectWorkflowDocumentStore: () => ({ value: mockDocumentStore }),
}));
vi.mock('@/app/stores/nodeTypes.store', () => ({

View File

@ -1,16 +1,12 @@
import type { CanvasConnection, CanvasNode } from '@/features/workflows/canvas/canvas.types';
import type { INodeUi, IWorkflowDb } from '@/Interface';
import type { MaybeRefOrGetter, Ref, ComputedRef } from 'vue';
import { useWorkflowsStore } from '@/app/stores/workflows.store';
import { toValue, computed, ref, watchEffect, shallowRef } from 'vue';
import { useCanvasMapping } from '@/features/workflows/canvas/composables/useCanvasMapping';
import type { Workflow, IConnections, INodeTypeDescription, NodeDiff } from 'n8n-workflow';
import { compareWorkflowsNodes, NodeDiffStatus } from 'n8n-workflow';
import { useNodeTypesStore } from '@/app/stores/nodeTypes.store';
import {
createWorkflowDocumentId,
useWorkflowDocumentStore,
} from '@/app/stores/workflowDocument.store';
import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store';
export function mapConnections(connections: CanvasConnection[]) {
return connections.reduce(
@ -110,14 +106,17 @@ export const useWorkflowDiff = (
sourceWorkflow: MaybeRefOrGetter<IWorkflowDb | undefined>,
targetWorkflow: MaybeRefOrGetter<IWorkflowDb | undefined>,
) => {
const workflowsStore = useWorkflowsStore();
const workflowDocumentStore = useWorkflowDocumentStore(
createWorkflowDocumentId(workflowsStore.workflowId),
);
const workflowDocumentStore = injectWorkflowDocumentStore();
const nodeTypesStore = useNodeTypesStore();
const sourceRefs = createWorkflowRefs(sourceWorkflow, workflowDocumentStore.createWorkflowObject);
const targetRefs = createWorkflowRefs(targetWorkflow, workflowDocumentStore.createWorkflowObject);
const sourceRefs = createWorkflowRefs(
sourceWorkflow,
workflowDocumentStore.value.createWorkflowObject,
);
const targetRefs = createWorkflowRefs(
targetWorkflow,
workflowDocumentStore.value.createWorkflowObject,
);
const sourceDiff = createWorkflowDiff(
sourceRefs.workflowRef,