mirror of
https://github.com/n8n-io/n8n.git
synced 2026-05-12 16:10:30 +02:00
refactor(editor): Migrate workflow name to workflowDocument store (#27343)
Some checks are pending
CI: Master (Build, Test, Lint) / Build for Github Cache (push) Waiting to run
CI: Master (Build, Test, Lint) / Unit tests (22.x) (push) Waiting to run
CI: Master (Build, Test, Lint) / Unit tests (24.14.1) (push) Waiting to run
CI: Master (Build, Test, Lint) / Unit tests (25.x) (push) Waiting to run
CI: Master (Build, Test, Lint) / Lint (push) Waiting to run
CI: Master (Build, Test, Lint) / Performance (push) Waiting to run
CI: Master (Build, Test, Lint) / Notify Slack on failure (push) Blocked by required conditions
Some checks are pending
CI: Master (Build, Test, Lint) / Build for Github Cache (push) Waiting to run
CI: Master (Build, Test, Lint) / Unit tests (22.x) (push) Waiting to run
CI: Master (Build, Test, Lint) / Unit tests (24.14.1) (push) Waiting to run
CI: Master (Build, Test, Lint) / Unit tests (25.x) (push) Waiting to run
CI: Master (Build, Test, Lint) / Lint (push) Waiting to run
CI: Master (Build, Test, Lint) / Performance (push) Waiting to run
CI: Master (Build, Test, Lint) / Notify Slack on failure (push) Blocked by required conditions
This commit is contained in:
parent
9120283009
commit
4aba06d78e
|
|
@ -6,8 +6,12 @@ import { useWorkflowsStore } from '@/app/stores/workflows.store';
|
|||
import { useSourceControlStore } from '@/features/integrations/sourceControl.ee/sourceControl.store';
|
||||
import { useCollaborationStore } from '@/features/collaboration/collaboration/collaboration.store';
|
||||
import { STORES } from '@n8n/stores';
|
||||
import { WorkflowIdKey } from '@/app/constants/injectionKeys';
|
||||
import { computed } from 'vue';
|
||||
import { WorkflowIdKey, WorkflowDocumentStoreKey } from '@/app/constants/injectionKeys';
|
||||
import { computed, shallowRef } from 'vue';
|
||||
import {
|
||||
useWorkflowDocumentStore,
|
||||
createWorkflowDocumentId,
|
||||
} from '@/app/stores/workflowDocument.store';
|
||||
|
||||
vi.mock('@n8n/permissions', () => ({
|
||||
getResourcePermissions: vi.fn(() => ({
|
||||
|
|
@ -68,29 +72,23 @@ const initialState = {
|
|||
},
|
||||
};
|
||||
|
||||
const pinia = createTestingPinia({ initialState, stubActions: false });
|
||||
const workflowDocumentStore = useWorkflowDocumentStore(createWorkflowDocumentId('1'));
|
||||
|
||||
const renderComponent = createComponentRenderer(MainHeader, {
|
||||
pinia: createTestingPinia({ initialState }),
|
||||
pinia,
|
||||
global: {
|
||||
stubs: {
|
||||
WorkflowDetails: {
|
||||
props: [
|
||||
'id',
|
||||
'tags',
|
||||
'name',
|
||||
'meta',
|
||||
'scopes',
|
||||
'active',
|
||||
'currentFolder',
|
||||
'isArchived',
|
||||
'description',
|
||||
],
|
||||
props: ['id', 'tags', 'name', 'currentFolder', 'isArchived', 'description'],
|
||||
template: '<div data-test-id="workflow-details-stub"></div>',
|
||||
},
|
||||
GithubButton: { template: '<div></div>' },
|
||||
TabBar: { template: '<div></div>' },
|
||||
},
|
||||
provide: {
|
||||
[WorkflowIdKey]: computed(() => 'test-workflow-id'),
|
||||
[WorkflowIdKey as symbol]: computed(() => 'test-workflow-id'),
|
||||
[WorkflowDocumentStoreKey as symbol]: shallowRef(workflowDocumentStore),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
@ -122,6 +120,8 @@ describe('MainHeader', () => {
|
|||
meta: {},
|
||||
};
|
||||
|
||||
workflowDocumentStore.setName('Test Workflow');
|
||||
|
||||
sourceControlStore.preferences.branchReadOnly = false;
|
||||
vi.spyOn(collaborationStore, 'shouldBeReadOnly', 'get').mockReturnValue(false);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -74,6 +74,7 @@ const hideMenuBar = computed(() =>
|
|||
const workflow = computed(() => workflowsStore.workflow);
|
||||
const workflowId = useInjectWorkflowId();
|
||||
const workflowDocumentStore = inject(WorkflowDocumentStoreKey, null);
|
||||
const workflowName = computed(() => workflowDocumentStore?.value?.name ?? '');
|
||||
const workflowTags = computed(() => workflowDocumentStore?.value?.tags ?? []);
|
||||
const workflowIsArchived = computed(() => workflowDocumentStore?.value?.isArchived ?? false);
|
||||
const onWorkflowPage = computed(() => !!(route.meta.nodeView || route.meta.keepWorkflowAlive));
|
||||
|
|
@ -288,10 +289,10 @@ async function onWorkflowDeactivated() {
|
|||
>
|
||||
<div v-show="!hideMenuBar && !settingsStore.isCanvasOnly" :class="$style['top-menu']">
|
||||
<WorkflowDetails
|
||||
v-if="workflow?.name"
|
||||
v-if="workflowName"
|
||||
:id="workflow.id"
|
||||
:tags="workflowTags"
|
||||
:name="workflow.name"
|
||||
:name="workflowName"
|
||||
:current-folder="parentFolderForBreadcrumbs"
|
||||
:is-archived="workflowIsArchived"
|
||||
:description="workflow.description"
|
||||
|
|
|
|||
|
|
@ -15,7 +15,6 @@ import { useInjectWorkflowId } from '@/app/composables/useInjectWorkflowId';
|
|||
import { useMessage } from '@/app/composables/useMessage';
|
||||
import { useTelemetry } from '@/app/composables/useTelemetry';
|
||||
import { useToast } from '@/app/composables/useToast';
|
||||
import { injectWorkflowState } from '@/app/composables/useWorkflowState';
|
||||
import { nodeViewEventBus } from '@/app/event-bus';
|
||||
import type { IWorkflowDb } from '@/Interface';
|
||||
import type { FolderShortInfo } from '@/features/core/folders/folders.types';
|
||||
|
|
@ -83,7 +82,6 @@ const message = useMessage();
|
|||
const toast = useToast();
|
||||
const documentTitle = useDocumentTitle();
|
||||
const workflowId = useInjectWorkflowId();
|
||||
const workflowState = injectWorkflowState();
|
||||
const workflowDocumentStore = inject(WorkflowDocumentStoreKey, null);
|
||||
|
||||
const isTagsEditEnabled = ref(false);
|
||||
|
|
@ -215,7 +213,8 @@ function onNameSubmit(name: string) {
|
|||
}
|
||||
|
||||
// Update workflow name in store and mark state as dirty
|
||||
workflowState.setWorkflowName({ newName, setStateDirty: true });
|
||||
workflowDocumentStore?.value?.setName(newName);
|
||||
uiStore.markStateDirty('metadata');
|
||||
|
||||
documentTitle.setDocumentTitle(newName, 'IDLE');
|
||||
renameInput.value?.forceCancel();
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import { nextTick, reactive } from 'vue';
|
||||
import { reactive } from 'vue';
|
||||
import { flushPromises } from '@vue/test-utils';
|
||||
import { createTestingPinia } from '@pinia/testing';
|
||||
import type { MockInstance } from 'vitest';
|
||||
import { waitFor, within } from '@testing-library/vue';
|
||||
import { fireEvent, waitFor, within } from '@testing-library/vue';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import type { FrontendSettings } from '@n8n/api-types';
|
||||
import { createComponentRenderer } from '@/__tests__/render';
|
||||
|
|
@ -71,6 +71,14 @@ const createComponent = createComponentRenderer(WorkflowSettingsVue, {
|
|||
template:
|
||||
'<div role="dialog"><slot name="header" /><slot name="content" /><slot name="footer" /></div>',
|
||||
},
|
||||
// Stub ElSwitch to prevent spurious update:model-value emissions in jsdom.
|
||||
// userEvent.click simulates pointer movement that can trigger the switch
|
||||
// during mouse path traversal, toggling executionTimeout and breaking save.
|
||||
ElSwitch: {
|
||||
props: ['modelValue', 'disabled'],
|
||||
template:
|
||||
'<span :data-test-id="$attrs[\'data-test-id\']" :aria-checked="!!modelValue" role="switch" />',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
@ -100,8 +108,8 @@ describe('WorkflowSettingsVue', () => {
|
|||
releaseChannel: 'stable',
|
||||
});
|
||||
vi.spyOn(settingsStore, 'isModuleActive').mockReturnValue(true);
|
||||
workflowsStore.workflowName = 'Test Workflow';
|
||||
workflowsStore.workflowId = '1';
|
||||
workflowDocumentStore.setName('Test Workflow');
|
||||
// Populate workflowsById to mark workflow as existing (not new)
|
||||
const testWorkflow = createTestWorkflow({
|
||||
id: '1',
|
||||
|
|
@ -121,7 +129,7 @@ describe('WorkflowSettingsVue', () => {
|
|||
it('should render correctly', async () => {
|
||||
settingsStore.settings.enterprise[EnterpriseEditionFeature.Sharing] = false;
|
||||
const { getByTestId } = createComponent({ pinia });
|
||||
await nextTick();
|
||||
await flushPromises();
|
||||
expect(getByTestId('workflow-settings-dialog')).toBeVisible();
|
||||
});
|
||||
|
||||
|
|
@ -129,7 +137,7 @@ describe('WorkflowSettingsVue', () => {
|
|||
settingsStore.settings.enterprise[EnterpriseEditionFeature.Sharing] = false;
|
||||
const { getByTestId } = createComponent({ pinia });
|
||||
|
||||
await nextTick();
|
||||
await flushPromises();
|
||||
|
||||
expect(
|
||||
within(getByTestId('workflow-settings-dialog')).queryByTestId('workflow-caller-policy'),
|
||||
|
|
@ -140,7 +148,7 @@ describe('WorkflowSettingsVue', () => {
|
|||
settingsStore.settings.enterprise[EnterpriseEditionFeature.Sharing] = true;
|
||||
const { getByTestId } = createComponent({ pinia });
|
||||
|
||||
await nextTick();
|
||||
await flushPromises();
|
||||
|
||||
expect(getByTestId('workflow-caller-policy')).toBeVisible();
|
||||
});
|
||||
|
|
@ -149,7 +157,7 @@ describe('WorkflowSettingsVue', () => {
|
|||
settingsStore.settings.enterprise[EnterpriseEditionFeature.Sharing] = true;
|
||||
const { getByTestId } = createComponent({ pinia });
|
||||
|
||||
await nextTick();
|
||||
await flushPromises();
|
||||
const dropdownItems = await getDropdownItems(getByTestId('workflow-caller-policy'));
|
||||
await userEvent.click(dropdownItems[2]);
|
||||
|
||||
|
|
@ -161,7 +169,7 @@ describe('WorkflowSettingsVue', () => {
|
|||
settingsStore.settings.enterprise[EnterpriseEditionFeature.Sharing] = true;
|
||||
const { getByTestId } = createComponent({ pinia });
|
||||
|
||||
await nextTick();
|
||||
await flushPromises();
|
||||
const dropdownItems = await getDropdownItems(getByTestId('error-workflow'));
|
||||
|
||||
// first is `- No Workflow -`, second is the workflow returned by
|
||||
|
|
@ -181,7 +189,7 @@ describe('WorkflowSettingsVue', () => {
|
|||
});
|
||||
|
||||
const { getByTestId, getByRole } = createComponent({ pinia });
|
||||
await nextTick();
|
||||
await flushPromises();
|
||||
|
||||
const dropdownItems = await getDropdownItems(getByTestId('error-workflow'));
|
||||
expect(dropdownItems[0]).toHaveTextContent('No Workflow');
|
||||
|
|
@ -203,12 +211,21 @@ describe('WorkflowSettingsVue', () => {
|
|||
});
|
||||
|
||||
const { getByTestId, getByRole } = createComponent({ pinia });
|
||||
await nextTick();
|
||||
await flushPromises();
|
||||
|
||||
const dropdownItems = await getDropdownItems(getByTestId('error-workflow'));
|
||||
// Open the error workflow dropdown
|
||||
const errorWorkflowRow = getByTestId('error-workflow');
|
||||
const combobox = within(errorWorkflowRow).getByRole('combobox');
|
||||
await userEvent.click(combobox);
|
||||
|
||||
// Select "No Workflow" (first option)
|
||||
await userEvent.click(dropdownItems[0]);
|
||||
// Wait for dropdown to appear and select "No Workflow"
|
||||
await waitFor(async () => {
|
||||
const option = within(document.body as HTMLElement).getAllByRole('option');
|
||||
const noWorkflow = option.find((o) => o.textContent?.includes('No Workflow'));
|
||||
expect(noWorkflow).toBeTruthy();
|
||||
await userEvent.click(noWorkflow!);
|
||||
});
|
||||
await flushPromises();
|
||||
|
||||
await userEvent.click(getByRole('button', { name: 'Save' }));
|
||||
|
||||
|
|
@ -226,12 +243,20 @@ describe('WorkflowSettingsVue', () => {
|
|||
});
|
||||
|
||||
const { getByTestId, getByRole } = createComponent({ pinia });
|
||||
await nextTick();
|
||||
await flushPromises();
|
||||
|
||||
const dropdownItems = await getDropdownItems(getByTestId('error-workflow'));
|
||||
// Open the error workflow dropdown
|
||||
const errorWorkflowRow = getByTestId('error-workflow');
|
||||
await userEvent.click(within(errorWorkflowRow).getByRole('combobox'));
|
||||
|
||||
// Select the test workflow (second option)
|
||||
await userEvent.click(dropdownItems[1]);
|
||||
// Wait for dropdown and select the test workflow
|
||||
await waitFor(async () => {
|
||||
const options = within(document.body as HTMLElement).getAllByRole('option');
|
||||
const testWorkflow = options.find((o) => o.textContent?.includes('Test Workflow'));
|
||||
expect(testWorkflow).toBeTruthy();
|
||||
await userEvent.click(testWorkflow!);
|
||||
});
|
||||
await flushPromises();
|
||||
|
||||
await userEvent.click(getByRole('button', { name: 'Save' }));
|
||||
|
||||
|
|
@ -251,7 +276,7 @@ describe('WorkflowSettingsVue', () => {
|
|||
settingsStore.settings.enterprise[EnterpriseEditionFeature.Sharing] = true;
|
||||
const { getByTestId } = createComponent({ pinia });
|
||||
|
||||
await nextTick();
|
||||
await flushPromises();
|
||||
|
||||
const dropdownItems = await getDropdownItems(getByTestId('workflow-caller-policy'));
|
||||
await userEvent.click(dropdownItems[2]);
|
||||
|
|
@ -268,7 +293,7 @@ describe('WorkflowSettingsVue', () => {
|
|||
settingsStore.settings.enterprise[EnterpriseEditionFeature.Sharing] = true;
|
||||
const { getByTestId } = createComponent({ pinia });
|
||||
|
||||
await nextTick();
|
||||
await flushPromises();
|
||||
|
||||
const dropdownItems = await getDropdownItems(getByTestId('workflow-caller-policy'));
|
||||
await userEvent.click(dropdownItems[2]);
|
||||
|
|
@ -316,7 +341,7 @@ describe('WorkflowSettingsVue', () => {
|
|||
async (testId, optionText, storeSetter) => {
|
||||
storeSetter();
|
||||
const { getByTestId } = createComponent({ pinia });
|
||||
await nextTick();
|
||||
await flushPromises();
|
||||
|
||||
const dropdownItems = await getDropdownItems(getByTestId(testId));
|
||||
|
||||
|
|
@ -327,7 +352,7 @@ describe('WorkflowSettingsVue', () => {
|
|||
it('should save time saved per execution correctly', async () => {
|
||||
workflowDocumentStore.setSettings({ timeSavedMode: 'fixed' });
|
||||
const { getByTestId, getByRole } = createComponent({ pinia });
|
||||
await nextTick();
|
||||
await flushPromises();
|
||||
await waitFor(() => {
|
||||
expect(getByTestId('workflow-settings-time-saved-per-execution')).toBeVisible();
|
||||
});
|
||||
|
|
@ -350,7 +375,7 @@ describe('WorkflowSettingsVue', () => {
|
|||
workflowDocumentStore.setSettings({ timeSavedMode: 'fixed', timeSavedPerExecution: 10 });
|
||||
|
||||
const { getByTestId, getByRole } = createComponent({ pinia });
|
||||
await nextTick();
|
||||
await flushPromises();
|
||||
await waitFor(() => {
|
||||
expect(getByTestId('workflow-settings-time-saved-per-execution')).toBeVisible();
|
||||
});
|
||||
|
|
@ -377,7 +402,7 @@ describe('WorkflowSettingsVue', () => {
|
|||
sourceControlStore.preferences.branchReadOnly = true;
|
||||
|
||||
const { getByTestId } = createComponent({ pinia });
|
||||
await nextTick();
|
||||
await flushPromises();
|
||||
await waitFor(() => {
|
||||
expect(getByTestId('workflow-settings-time-saved-per-execution')).toBeVisible();
|
||||
});
|
||||
|
|
@ -402,7 +427,7 @@ describe('WorkflowSettingsVue', () => {
|
|||
workflowsListStore.getWorkflowById.mockImplementation(() => readOnlyWorkflow);
|
||||
|
||||
const { getByTestId } = createComponent({ pinia });
|
||||
await nextTick();
|
||||
await flushPromises();
|
||||
await waitFor(() => {
|
||||
expect(getByTestId('workflow-settings-time-saved-per-execution')).toBeVisible();
|
||||
});
|
||||
|
|
@ -417,7 +442,7 @@ describe('WorkflowSettingsVue', () => {
|
|||
describe('Execution Order & Binary Mode', () => {
|
||||
it('should render execution order dropdown with correct options', async () => {
|
||||
const { getByTestId } = createComponent({ pinia });
|
||||
await nextTick();
|
||||
await flushPromises();
|
||||
|
||||
const dropdownItems = await getDropdownItems(
|
||||
getByTestId('workflow-settings-execution-order'),
|
||||
|
|
@ -436,7 +461,7 @@ describe('WorkflowSettingsVue', () => {
|
|||
workflowDocumentStore.setSettings({ executionOrder: 'v1', binaryMode: BINARY_MODE_COMBINED });
|
||||
|
||||
const { getByTestId, getByRole } = createComponent({ pinia });
|
||||
await nextTick();
|
||||
await flushPromises();
|
||||
|
||||
const dropdownItems = await getDropdownItems(
|
||||
getByTestId('workflow-settings-execution-order'),
|
||||
|
|
@ -460,7 +485,7 @@ describe('WorkflowSettingsVue', () => {
|
|||
workflowDocumentStore.setSettings({ executionOrder: 'v0', binaryMode: BINARY_MODE_COMBINED });
|
||||
|
||||
const { getByTestId, getByRole } = createComponent({ pinia });
|
||||
await nextTick();
|
||||
await flushPromises();
|
||||
|
||||
const dropdownItems = await getDropdownItems(
|
||||
getByTestId('workflow-settings-execution-order'),
|
||||
|
|
@ -484,7 +509,7 @@ describe('WorkflowSettingsVue', () => {
|
|||
workflowDocumentStore.setSettings({ executionOrder: 'v1', binaryMode: BINARY_MODE_COMBINED });
|
||||
|
||||
const { getByTestId, getByRole } = createComponent({ pinia });
|
||||
await nextTick();
|
||||
await flushPromises();
|
||||
|
||||
const dropdownItems = await getDropdownItems(
|
||||
getByTestId('workflow-settings-execution-order'),
|
||||
|
|
@ -508,7 +533,7 @@ describe('WorkflowSettingsVue', () => {
|
|||
workflowDocumentStore.setSettings({ executionOrder: 'v0', binaryMode: 'separate' });
|
||||
|
||||
const { getByTestId } = createComponent({ pinia });
|
||||
await nextTick();
|
||||
await flushPromises();
|
||||
|
||||
toast.showMessage.mockClear();
|
||||
|
||||
|
|
@ -528,7 +553,7 @@ describe('WorkflowSettingsVue', () => {
|
|||
});
|
||||
|
||||
const { getByTestId } = createComponent({ pinia });
|
||||
await nextTick();
|
||||
await flushPromises();
|
||||
|
||||
const dropdownItems = await getDropdownItems(
|
||||
getByTestId('workflow-settings-execution-order'),
|
||||
|
|
@ -541,7 +566,7 @@ describe('WorkflowSettingsVue', () => {
|
|||
sourceControlStore.preferences.branchReadOnly = true;
|
||||
|
||||
const { getByTestId } = createComponent({ pinia });
|
||||
await nextTick();
|
||||
await flushPromises();
|
||||
|
||||
const executionOrderDropdown = within(
|
||||
getByTestId('workflow-settings-execution-order'),
|
||||
|
|
@ -566,7 +591,7 @@ describe('WorkflowSettingsVue', () => {
|
|||
}));
|
||||
|
||||
const { getByTestId } = createComponent({ pinia });
|
||||
await nextTick();
|
||||
await flushPromises();
|
||||
|
||||
const executionOrderDropdown = within(
|
||||
getByTestId('workflow-settings-execution-order'),
|
||||
|
|
@ -628,14 +653,14 @@ describe('WorkflowSettingsVue', () => {
|
|||
|
||||
it('should render credential resolver dropdown', async () => {
|
||||
const { getByTestId } = createComponent({ pinia });
|
||||
await nextTick();
|
||||
await flushPromises();
|
||||
|
||||
expect(getByTestId('workflow-settings-credential-resolver')).toBeVisible();
|
||||
});
|
||||
|
||||
it('should load credential resolvers on mount', async () => {
|
||||
const { getByTestId } = createComponent({ pinia });
|
||||
await nextTick();
|
||||
await flushPromises();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(restApiClient.getCredentialResolvers).toHaveBeenCalled();
|
||||
|
|
@ -654,7 +679,7 @@ describe('WorkflowSettingsVue', () => {
|
|||
|
||||
it('should show "New" button for creating a new resolver', async () => {
|
||||
const { getByTestId } = createComponent({ pinia });
|
||||
await nextTick();
|
||||
await flushPromises();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(getByTestId('workflow-settings-credential-resolver-create-new')).toBeInTheDocument();
|
||||
|
|
@ -663,7 +688,7 @@ describe('WorkflowSettingsVue', () => {
|
|||
|
||||
it('should not show "Edit" button when no resolver is selected', async () => {
|
||||
const { queryByTestId } = createComponent({ pinia });
|
||||
await nextTick();
|
||||
await flushPromises();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(queryByTestId('workflow-settings-credential-resolver-edit')).not.toBeInTheDocument();
|
||||
|
|
@ -674,7 +699,7 @@ describe('WorkflowSettingsVue', () => {
|
|||
workflowDocumentStore.setSettings({ credentialResolverId: 'resolver-1' });
|
||||
|
||||
const { getByTestId } = createComponent({ pinia });
|
||||
await nextTick();
|
||||
await flushPromises();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(getByTestId('workflow-settings-credential-resolver-edit')).toBeInTheDocument();
|
||||
|
|
@ -685,7 +710,7 @@ describe('WorkflowSettingsVue', () => {
|
|||
workflowDocumentStore.setSettings({ credentialResolverId: 'resolver-n8n' });
|
||||
|
||||
const { queryByTestId } = createComponent({ pinia });
|
||||
await nextTick();
|
||||
await flushPromises();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(restApiClient.getCredentialResolverTypes).toHaveBeenCalled();
|
||||
|
|
@ -698,7 +723,7 @@ describe('WorkflowSettingsVue', () => {
|
|||
|
||||
it('should select a resolver from dropdown', async () => {
|
||||
const { getByTestId, getByRole } = createComponent({ pinia });
|
||||
await nextTick();
|
||||
await flushPromises();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(restApiClient.getCredentialResolvers).toHaveBeenCalled();
|
||||
|
|
@ -708,30 +733,44 @@ describe('WorkflowSettingsVue', () => {
|
|||
getByTestId('workflow-settings-credential-resolver'),
|
||||
);
|
||||
|
||||
// Select "Test Resolver 1"
|
||||
expect(dropdownItems).toHaveLength(3);
|
||||
expect(dropdownItems[0]).toHaveTextContent('Test Resolver 1');
|
||||
expect(dropdownItems[1]).toHaveTextContent('Test Resolver 2');
|
||||
expect(dropdownItems[2]).toHaveTextContent('N8n Resolver');
|
||||
|
||||
await userEvent.click(dropdownItems[0]);
|
||||
await flushPromises();
|
||||
|
||||
await userEvent.click(getByRole('button', { name: 'Save' }));
|
||||
|
||||
const callArgs = workflowsStore.updateWorkflow.mock.calls[0];
|
||||
expect(callArgs[0]).toBe('1');
|
||||
expect(callArgs[1].settings?.credentialResolverId).toBe('resolver-1');
|
||||
expect(workflowsStore.updateWorkflow).toHaveBeenCalledWith(
|
||||
'1',
|
||||
expect.objectContaining({
|
||||
settings: expect.objectContaining({ credentialResolverId: 'resolver-1' }),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should save workflow with selected resolver', async () => {
|
||||
const { getByTestId, getByRole } = createComponent({ pinia });
|
||||
await nextTick();
|
||||
await flushPromises();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(restApiClient.getCredentialResolvers).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
const dropdownItems = await getDropdownItems(
|
||||
getByTestId('workflow-settings-credential-resolver'),
|
||||
);
|
||||
// Open the credential resolver dropdown
|
||||
const resolverContainer = getByTestId('workflow-settings-credential-resolver');
|
||||
await userEvent.click(within(resolverContainer).getByRole('combobox'));
|
||||
|
||||
// Select "Test Resolver 2"
|
||||
await userEvent.click(dropdownItems[1]);
|
||||
// Wait for dropdown and select "Test Resolver 2"
|
||||
await waitFor(async () => {
|
||||
const options = within(document.body as HTMLElement).getAllByRole('option');
|
||||
const resolver = options.find((o) => o.textContent?.includes('Test Resolver 2'));
|
||||
expect(resolver).toBeTruthy();
|
||||
await userEvent.click(resolver!);
|
||||
});
|
||||
await flushPromises();
|
||||
|
||||
await userEvent.click(getByRole('button', { name: 'Save' }));
|
||||
|
||||
|
|
@ -741,29 +780,55 @@ describe('WorkflowSettingsVue', () => {
|
|||
});
|
||||
|
||||
it('should save with empty credentialResolverId when resolver is cleared', async () => {
|
||||
// Element Plus clearable sets the model value to '' when the clear icon is clicked.
|
||||
// The clear icon requires CSS hover state which jsdom cannot simulate,
|
||||
// so we verify the save behavior when the value is already empty.
|
||||
workflowDocumentStore.setSettings({ credentialResolverId: '' });
|
||||
workflowDocumentStore.setSettings({ credentialResolverId: 'resolver-1' });
|
||||
|
||||
const { getByRole } = createComponent({ pinia });
|
||||
// flushPromises drains the full microtask queue, ensuring onMounted's
|
||||
// Promise.all (loadCredentialResolvers, loadWorkflows, etc.) fully resolves
|
||||
// and workflowSettings.value is initialized before we click Save.
|
||||
const { getByTestId, getByRole } = createComponent({ pinia });
|
||||
await flushPromises();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(restApiClient.getCredentialResolvers).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
const dropdown = getByTestId('workflow-settings-credential-resolver');
|
||||
const input = dropdown.querySelector('input') as HTMLInputElement;
|
||||
|
||||
// Wait for the select to display the selected resolver
|
||||
await waitFor(() => {
|
||||
expect(input?.value).toBe('Test Resolver 1');
|
||||
});
|
||||
|
||||
// Hover over the select trigger to reveal the clear icon.
|
||||
// Element Plus toggles the clear icon via a JS mouseenter handler on
|
||||
// .select-trigger (not CSS :hover), and Vue re-renders asynchronously.
|
||||
const selectTrigger = dropdown.querySelector('.select-trigger') as HTMLElement;
|
||||
const arrowIcon = dropdown.querySelector('.el-icon');
|
||||
selectTrigger.dispatchEvent(new MouseEvent('mouseenter', { bubbles: false }));
|
||||
|
||||
// Wait for Vue to swap the arrow icon for the clear icon (different VNode keys)
|
||||
await waitFor(() => {
|
||||
expect(dropdown.querySelector('.el-icon')).not.toBe(arrowIcon);
|
||||
});
|
||||
|
||||
// Click the clear icon
|
||||
const clearIcon = dropdown.querySelector('.el-icon') as HTMLElement;
|
||||
await fireEvent.click(clearIcon);
|
||||
await flushPromises();
|
||||
|
||||
await userEvent.click(getByRole('button', { name: 'Save' }));
|
||||
|
||||
const callArgs = workflowsStore.updateWorkflow.mock.calls[0];
|
||||
expect(callArgs[0]).toBe('1');
|
||||
expect(callArgs[1].settings?.credentialResolverId).toBe('');
|
||||
expect(workflowsStore.updateWorkflow).toHaveBeenCalledWith(
|
||||
'1',
|
||||
expect.objectContaining({
|
||||
settings: expect.objectContaining({ credentialResolverId: '' }),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should disable credential resolver dropdown when environment is read-only', async () => {
|
||||
sourceControlStore.preferences.branchReadOnly = true;
|
||||
|
||||
const { getByTestId } = createComponent({ pinia });
|
||||
await nextTick();
|
||||
await flushPromises();
|
||||
|
||||
const dropdownContainer = getByTestId('workflow-settings-credential-resolver');
|
||||
const input = dropdownContainer.querySelector('input');
|
||||
|
|
@ -786,7 +851,7 @@ describe('WorkflowSettingsVue', () => {
|
|||
}));
|
||||
|
||||
const { getByTestId } = createComponent({ pinia });
|
||||
await nextTick();
|
||||
await flushPromises();
|
||||
|
||||
const dropdownContainer = getByTestId('workflow-settings-credential-resolver');
|
||||
const input = dropdownContainer.querySelector('input');
|
||||
|
|
@ -799,7 +864,7 @@ describe('WorkflowSettingsVue', () => {
|
|||
});
|
||||
|
||||
const { getByTestId } = createComponent({ pinia });
|
||||
await nextTick();
|
||||
await flushPromises();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(restApiClient.getCredentialResolvers).toHaveBeenCalled();
|
||||
|
|
@ -903,6 +968,13 @@ describe('WorkflowSettingsVue', () => {
|
|||
});
|
||||
|
||||
describe('Redaction Policy', () => {
|
||||
it('should not render redaction policy when env feature flag is missing', async () => {
|
||||
const { queryByTestId } = createComponent({ pinia });
|
||||
await flushPromises();
|
||||
|
||||
expect(queryByTestId('workflow-settings-redaction-policy')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should not render redaction policy when redaction module is inactive', async () => {
|
||||
vi.spyOn(settingsStore, 'isModuleActive').mockImplementation(
|
||||
(name: string) => name !== 'redaction',
|
||||
|
|
@ -918,7 +990,7 @@ describe('WorkflowSettingsVue', () => {
|
|||
workflowsListStore.getWorkflowById.mockImplementation(() => workflowWithRedactionScope);
|
||||
|
||||
const { queryByTestId } = createComponent({ pinia });
|
||||
await nextTick();
|
||||
await flushPromises();
|
||||
|
||||
expect(queryByTestId('workflow-settings-redaction-policy')).not.toBeInTheDocument();
|
||||
});
|
||||
|
|
@ -927,7 +999,7 @@ describe('WorkflowSettingsVue', () => {
|
|||
vi.spyOn(settingsStore, 'isModuleActive').mockReturnValue(true);
|
||||
|
||||
const { queryByTestId } = createComponent({ pinia });
|
||||
await nextTick();
|
||||
await flushPromises();
|
||||
|
||||
expect(queryByTestId('workflow-settings-redaction-policy')).not.toBeInTheDocument();
|
||||
});
|
||||
|
|
@ -944,7 +1016,7 @@ describe('WorkflowSettingsVue', () => {
|
|||
workflowsListStore.getWorkflowById.mockImplementation(() => workflowWithRedactionScope);
|
||||
|
||||
const { getByTestId } = createComponent({ pinia });
|
||||
await nextTick();
|
||||
await flushPromises();
|
||||
|
||||
expect(getByTestId('workflow-settings-redaction-policy')).toBeVisible();
|
||||
});
|
||||
|
|
@ -962,7 +1034,7 @@ describe('WorkflowSettingsVue', () => {
|
|||
workflowsListStore.getWorkflowById.mockImplementation(() => workflowWithRedactionScope);
|
||||
|
||||
const { getByTestId } = createComponent({ pinia });
|
||||
await nextTick();
|
||||
await flushPromises();
|
||||
|
||||
const productionItems = await getDropdownItems(
|
||||
getByTestId('workflow-settings-redact-production-select'),
|
||||
|
|
@ -981,6 +1053,7 @@ describe('WorkflowSettingsVue', () => {
|
|||
|
||||
it('should save redaction policy as non-manual when only production is set to redact', async () => {
|
||||
vi.spyOn(settingsStore, 'isModuleActive').mockReturnValue(true);
|
||||
settingsStore.settings.envFeatureFlags.N8N_ENV_FEAT_REDACTION_POLICY = true;
|
||||
|
||||
const workflowWithRedactionScope = createTestWorkflow({
|
||||
id: '1',
|
||||
|
|
@ -992,14 +1065,24 @@ describe('WorkflowSettingsVue', () => {
|
|||
workflowsListStore.getWorkflowById.mockImplementation(() => workflowWithRedactionScope);
|
||||
|
||||
const { getByTestId, getByRole } = createComponent({ pinia });
|
||||
await nextTick();
|
||||
await flushPromises();
|
||||
|
||||
const productionItems = await getDropdownItems(
|
||||
getByTestId('workflow-settings-redact-production-select'),
|
||||
);
|
||||
await userEvent.click(productionItems[1]);
|
||||
// Open the production redaction dropdown
|
||||
const productionSelect = getByTestId('workflow-settings-redact-production-select');
|
||||
await userEvent.click(within(productionSelect).getByRole('combobox'));
|
||||
|
||||
// Select "Redact"
|
||||
await waitFor(async () => {
|
||||
const options = within(document.body as HTMLElement).getAllByRole('option');
|
||||
const redactOption = options.find((o) => o.textContent?.trim() === 'Redact');
|
||||
expect(redactOption).toBeTruthy();
|
||||
await userEvent.click(redactOption!);
|
||||
});
|
||||
await flushPromises();
|
||||
|
||||
toast.showError.mockClear();
|
||||
await userEvent.click(getByRole('button', { name: 'Save' }));
|
||||
expect(toast.showError).not.toHaveBeenCalled();
|
||||
|
||||
expect(workflowsStore.updateWorkflow).toHaveBeenCalledWith(
|
||||
expect.any(String),
|
||||
|
|
@ -1022,17 +1105,31 @@ describe('WorkflowSettingsVue', () => {
|
|||
workflowsListStore.getWorkflowById.mockImplementation(() => workflowWithRedactionScope);
|
||||
|
||||
const { getByTestId, getByRole } = createComponent({ pinia });
|
||||
await nextTick();
|
||||
await flushPromises();
|
||||
|
||||
const productionItems = await getDropdownItems(
|
||||
getByTestId('workflow-settings-redact-production-select'),
|
||||
);
|
||||
await userEvent.click(productionItems[1]);
|
||||
// Open the production redaction dropdown and select "Redact"
|
||||
const productionSelect = getByTestId('workflow-settings-redact-production-select');
|
||||
await userEvent.click(within(productionSelect).getByRole('combobox'));
|
||||
await waitFor(async () => {
|
||||
const options = within(document.body as HTMLElement).getAllByRole('option');
|
||||
const redactOption = options.find((o) => o.textContent?.trim() === 'Redact');
|
||||
expect(redactOption).toBeTruthy();
|
||||
await userEvent.click(redactOption!);
|
||||
});
|
||||
await flushPromises();
|
||||
|
||||
const manualItems = await getDropdownItems(
|
||||
getByTestId('workflow-settings-redact-manual-select'),
|
||||
);
|
||||
await userEvent.click(manualItems[1]);
|
||||
// Open the manual redaction dropdown and select "Redact"
|
||||
const manualSelect = getByTestId('workflow-settings-redact-manual-select');
|
||||
await userEvent.click(within(manualSelect).getByRole('combobox'));
|
||||
await waitFor(async () => {
|
||||
const options = within(document.body as HTMLElement).getAllByRole('option');
|
||||
const redactOption = options.find(
|
||||
(o) => o.textContent?.trim() === 'Redact' && !o.classList.contains('selected'),
|
||||
);
|
||||
expect(redactOption).toBeTruthy();
|
||||
await userEvent.click(redactOption!);
|
||||
});
|
||||
await flushPromises();
|
||||
|
||||
await userEvent.click(getByRole('button', { name: 'Save' }));
|
||||
|
||||
|
|
@ -1046,6 +1143,22 @@ describe('WorkflowSettingsVue', () => {
|
|||
|
||||
it('should disable production redaction select and force "Redact" when dynamic credentials are configured', async () => {
|
||||
vi.spyOn(settingsStore, 'isModuleActive').mockReturnValue(true);
|
||||
vi.mocked(restApiClient.getCredentialResolvers).mockResolvedValue([
|
||||
{
|
||||
id: 'resolver-1',
|
||||
name: 'Test Resolver 1',
|
||||
type: 'editable-type',
|
||||
config: '{}',
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
]);
|
||||
vi.mocked(restApiClient.getCredentialResolverTypes).mockResolvedValue([
|
||||
{ name: 'editable-type', displayName: 'Editable', options: [] },
|
||||
]);
|
||||
const rbacStore = useRBACStore();
|
||||
rbacStore.addGlobalScope('credentialResolver:list');
|
||||
|
||||
const workflowWithRedactionScope = createTestWorkflow({
|
||||
id: '1',
|
||||
name: 'Test Workflow',
|
||||
|
|
@ -1055,17 +1168,21 @@ describe('WorkflowSettingsVue', () => {
|
|||
workflowsListStore.workflowsById = { '1': workflowWithRedactionScope };
|
||||
workflowsListStore.getWorkflowById.mockImplementation(() => workflowWithRedactionScope);
|
||||
|
||||
workflowDocumentStore.setSettings({ credentialResolverId: 'some-resolver-id' });
|
||||
workflowDocumentStore.setSettings({ credentialResolverId: 'resolver-1' });
|
||||
|
||||
const { getByTestId } = createComponent({ pinia });
|
||||
await flushPromises();
|
||||
|
||||
await nextTick();
|
||||
// Verify the credential resolver dropdown shows the selected resolver
|
||||
await waitFor(() => {
|
||||
const resolverDropdown = getByTestId('workflow-settings-credential-resolver');
|
||||
const resolverInput = resolverDropdown.querySelector('input') as HTMLInputElement;
|
||||
expect(resolverInput.value).toBe('Test Resolver 1');
|
||||
});
|
||||
|
||||
// Verify the dropdown cannot be opened (disabled by workflowHasDynamicCredentials)
|
||||
const productionSelect = getByTestId('workflow-settings-redact-production-select');
|
||||
const dropdownItems = await getDropdownItems(productionSelect).catch(() => null);
|
||||
expect(dropdownItems).toBeNull();
|
||||
const input = productionSelect.querySelector('input');
|
||||
expect(input).toBeDisabled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -192,7 +192,7 @@ const isMCPEnabled = computed(
|
|||
const readOnlyEnv = computed(
|
||||
() => sourceControlStore.preferences.branchReadOnly || collaborationStore.shouldBeReadOnly,
|
||||
);
|
||||
const workflowName = computed(() => workflowsStore.workflowName);
|
||||
const workflowName = computed(() => workflowDocumentStore.value?.name ?? '');
|
||||
const workflowId = computed(() => workflowsStore.workflowId);
|
||||
const workflow = computed(() => workflowsListStore.getWorkflowById(workflowId.value));
|
||||
const isSharingEnabled = computed(
|
||||
|
|
|
|||
|
|
@ -57,16 +57,34 @@ const router = useRouter();
|
|||
const route = useRoute();
|
||||
const workflowSaving = useWorkflowSaving({ router });
|
||||
|
||||
const workflow = ref(
|
||||
data.id && workflowsListStore.workflowsById[data.id]
|
||||
? workflowsListStore.workflowsById[data.id]
|
||||
: workflowsStore.workflow,
|
||||
const workflowDocumentStore = computed(() =>
|
||||
data.id ? useWorkflowDocumentStore(createWorkflowDocumentId(data.id)) : undefined,
|
||||
);
|
||||
const workflowListEntry = computed(() => workflowsListStore.workflowsById[data.id]);
|
||||
const workflowId = computed(() => data.id);
|
||||
const workflowName = computed(
|
||||
() => workflowListEntry.value?.name ?? workflowDocumentStore.value?.name ?? '',
|
||||
);
|
||||
const workflowHomeProject = computed(
|
||||
() =>
|
||||
workflowListEntry.value?.homeProject ??
|
||||
workflowDocumentStore.value?.homeProject ??
|
||||
workflowsStore.workflow.homeProject,
|
||||
);
|
||||
const workflowScopes = computed(
|
||||
() =>
|
||||
workflowListEntry.value?.scopes ??
|
||||
workflowDocumentStore.value?.scopes ??
|
||||
workflowsStore.workflow.scopes,
|
||||
);
|
||||
const workflowSharedWithProjects = computed(
|
||||
() => workflowListEntry.value?.sharedWithProjects ?? workflowsStore.workflow.sharedWithProjects,
|
||||
);
|
||||
const loading = ref(true);
|
||||
const isDirty = ref(false);
|
||||
const modalBus = createEventBus();
|
||||
const sharedWithProjects = ref([
|
||||
...(workflow.value.sharedWithProjects ?? []),
|
||||
...(workflowSharedWithProjects.value ?? []),
|
||||
] as ProjectSharingData[]);
|
||||
const teamProject = ref(null as Project | null);
|
||||
|
||||
|
|
@ -74,12 +92,12 @@ const isSharingEnabled = computed(
|
|||
() => settingsStore.isEnterpriseFeatureEnabled[EnterpriseEditionFeature.Sharing],
|
||||
);
|
||||
|
||||
const isHomeTeamProject = computed(() => workflow.value.homeProject?.type === ProjectTypes.Team);
|
||||
const isHomeTeamProject = computed(() => workflowHomeProject.value?.type === ProjectTypes.Team);
|
||||
|
||||
const modalTitle = computed(() => {
|
||||
if (isHomeTeamProject.value) {
|
||||
return i18n.baseText('workflows.shareModal.title.static', {
|
||||
interpolate: { projectName: workflow.value.homeProject?.name ?? '' },
|
||||
interpolate: { projectName: workflowHomeProject.value?.name ?? '' },
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -88,34 +106,31 @@ const modalTitle = computed(() => {
|
|||
? (uiStore.contextBasedTranslationKeys.workflows.sharing.title as BaseTextKey)
|
||||
: (uiStore.contextBasedTranslationKeys.workflows.sharing.unavailable.title as BaseTextKey),
|
||||
{
|
||||
interpolate: { name: workflow.value.name },
|
||||
interpolate: {
|
||||
name: workflowName.value,
|
||||
},
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
const workflowPermissions = computed(() => {
|
||||
// For existing workflows, scopes come from the API response on the workflow object.
|
||||
// For new unsaved workflows, scopes are only in the workflowDocument store.
|
||||
const scopes =
|
||||
workflow.value?.scopes ??
|
||||
useWorkflowDocumentStore(createWorkflowDocumentId(workflow.value.id)).scopes;
|
||||
return getResourcePermissions(scopes).workflow;
|
||||
return getResourcePermissions(workflowScopes.value).workflow;
|
||||
});
|
||||
|
||||
const isPersonalSpaceRestricted = computed(
|
||||
() =>
|
||||
workflow.value.homeProject?.type === ProjectTypes.Personal &&
|
||||
workflow.value.homeProject?.id === projectsStore.personalProject?.id &&
|
||||
workflowHomeProject.value?.type === ProjectTypes.Personal &&
|
||||
workflowHomeProject.value?.id === projectsStore.personalProject?.id &&
|
||||
!workflowPermissions.value.share,
|
||||
);
|
||||
|
||||
const workflowOwnerName = computed(() =>
|
||||
workflowsEEStore.getWorkflowOwnerName(`${workflow.value.id}`),
|
||||
workflowsEEStore.getWorkflowOwnerName(`${workflowId.value}`),
|
||||
);
|
||||
|
||||
const searchFn = useRemoteProjectSearch();
|
||||
const filterFn = (project: ProjectListItem) =>
|
||||
project.type === 'personal' && project.id !== workflow.value.homeProject?.id;
|
||||
project.type === 'personal' && project.id !== workflowHomeProject.value?.id;
|
||||
|
||||
const numberOfMembersInHomeTeamProject = computed(() => teamProject.value?.relations.length ?? 0);
|
||||
|
||||
|
|
@ -143,21 +158,21 @@ const workflowRoles = computed(() =>
|
|||
|
||||
const trackTelemetry = (eventName: string, data: ITelemetryTrackProperties) => {
|
||||
telemetry.track(eventName, {
|
||||
workflow_id: workflow.value.id,
|
||||
workflow_id: workflowId.value,
|
||||
...data,
|
||||
});
|
||||
};
|
||||
|
||||
const onProjectAdded = (project: ProjectSharingData) => {
|
||||
trackTelemetry('User selected sharee to add', {
|
||||
project_id_sharer: workflow.value.homeProject?.id,
|
||||
project_id_sharer: workflowHomeProject.value?.id,
|
||||
project_id_sharee: project.id,
|
||||
});
|
||||
};
|
||||
|
||||
const onProjectRemoved = (project: ProjectSharingData) => {
|
||||
trackTelemetry('User selected sharee to remove', {
|
||||
project_id_sharer: workflow.value.homeProject?.id,
|
||||
project_id_sharer: workflowHomeProject.value?.id,
|
||||
project_id_sharee: project.id,
|
||||
});
|
||||
};
|
||||
|
|
@ -170,7 +185,7 @@ const onSave = async () => {
|
|||
loading.value = true;
|
||||
|
||||
const saveWorkflowPromise = async () => {
|
||||
if (!workflowsStore.isWorkflowSaved[workflow.value.id]) {
|
||||
if (!workflowsStore.isWorkflowSaved[workflowId.value]) {
|
||||
const parentFolderId = route.query.folderId as string | undefined;
|
||||
const workflowId = await workflowSaving.saveAsNewWorkflow({ parentFolderId });
|
||||
if (!workflowId) {
|
||||
|
|
@ -178,7 +193,7 @@ const onSave = async () => {
|
|||
}
|
||||
return workflowId;
|
||||
} else {
|
||||
return workflow.value.id;
|
||||
return workflowId.value;
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -228,12 +243,12 @@ const goToUpgrade = () => {
|
|||
const initialize = async () => {
|
||||
if (isSharingEnabled.value) {
|
||||
// Fetch workflow if it exists and is not new
|
||||
if (workflowsStore.isWorkflowSaved[workflow.value.id]) {
|
||||
await workflowsListStore.fetchWorkflow(workflow.value.id);
|
||||
if (workflowsStore.isWorkflowSaved[workflowId.value]) {
|
||||
await workflowsListStore.fetchWorkflow(workflowId.value);
|
||||
}
|
||||
|
||||
if (isHomeTeamProject.value && workflow.value.homeProject) {
|
||||
teamProject.value = await projectsStore.fetchProject(workflow.value.homeProject.id);
|
||||
if (isHomeTeamProject.value && workflowHomeProject.value) {
|
||||
teamProject.value = await projectsStore.fetchProject(workflowHomeProject.value.id);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -289,7 +304,7 @@ watch(
|
|||
<div>
|
||||
<ProjectSharing
|
||||
v-model="sharedWithProjects"
|
||||
:home-project="workflow.homeProject"
|
||||
:home-project="workflowHomeProject"
|
||||
:search-fn="searchFn"
|
||||
:filter-fn="filterFn"
|
||||
:roles="workflowRoles"
|
||||
|
|
@ -308,7 +323,7 @@ watch(
|
|||
<N8nInfoTip v-if="isHomeTeamProject" :bold="false" class="mt-s">
|
||||
<I18nT keypath="workflows.shareModal.info.members" tag="span" scope="global">
|
||||
<template #projectName>
|
||||
{{ workflow.homeProject?.name }}
|
||||
{{ workflowHomeProject?.name }}
|
||||
</template>
|
||||
<template #members>
|
||||
<strong>
|
||||
|
|
|
|||
|
|
@ -4596,7 +4596,7 @@ describe('useCanvasOperations', () => {
|
|||
renameNode: vi.fn(),
|
||||
});
|
||||
|
||||
const setWorkflowName = vi.spyOn(workflowState, 'setWorkflowName');
|
||||
const setNameSpy = vi.spyOn(workflowDocumentStoreInstance, 'setName');
|
||||
|
||||
const canvasOperations = useCanvasOperations();
|
||||
const workflowDataWithName = {
|
||||
|
|
@ -4607,10 +4607,7 @@ describe('useCanvasOperations', () => {
|
|||
|
||||
await canvasOperations.importWorkflowData(workflowDataWithName, 'file');
|
||||
|
||||
expect(setWorkflowName).toHaveBeenCalledWith({
|
||||
newName: 'Test Workflow Name',
|
||||
setStateDirty: true,
|
||||
});
|
||||
expect(setNameSpy).toHaveBeenCalledWith('Test Workflow Name');
|
||||
});
|
||||
|
||||
it('should not crash when importing nodes that exceed maxNodes limit', async () => {
|
||||
|
|
@ -6324,9 +6321,9 @@ describe('useCanvasOperations', () => {
|
|||
uiStore.lastInteractedWithNodeHandle = `outputs/${NodeConnectionTypes.AiTool}/0`;
|
||||
|
||||
const { addNode } = useCanvasOperations();
|
||||
const docStore = useWorkflowDocumentStore(createWorkflowDocumentId(workflowId));
|
||||
const addConnectionSpy = vi.spyOn(docStore, 'addConnection');
|
||||
const removeConnectionSpy = vi.spyOn(docStore, 'removeConnection');
|
||||
const workflowDocumentStore = useWorkflowDocumentStore(createWorkflowDocumentId(workflowId));
|
||||
const addConnectionSpy = vi.spyOn(workflowDocumentStore, 'addConnection');
|
||||
const removeConnectionSpy = vi.spyOn(workflowDocumentStore, 'removeConnection');
|
||||
|
||||
// Act: Add HITL node
|
||||
await nextTick();
|
||||
|
|
|
|||
|
|
@ -2726,7 +2726,10 @@ export function useCanvasOperations() {
|
|||
}
|
||||
|
||||
if (workflowData.name) {
|
||||
workflowState.setWorkflowName({ newName: workflowData.name, setStateDirty });
|
||||
workflowDocumentStore.value?.setName(workflowData.name);
|
||||
if (setStateDirty) {
|
||||
uiStore.markStateDirty('metadata');
|
||||
}
|
||||
}
|
||||
|
||||
return workflowData;
|
||||
|
|
@ -3005,7 +3008,11 @@ export function useCanvasOperations() {
|
|||
workflowDocumentStore.value?.setConnections(workflow.connections);
|
||||
}
|
||||
await addNodes(convertedNodes ?? [], { keepPristine: true });
|
||||
await workflowState.getNewWorkflowData(name, projectsStore.currentProjectId);
|
||||
const workflowData = await workflowState.getNewWorkflowData(
|
||||
name,
|
||||
projectsStore.currentProjectId,
|
||||
);
|
||||
workflowDocumentStore.value?.setName(workflowData.name);
|
||||
}
|
||||
|
||||
function tryToOpenSubworkflowInNewTab(nodeId: string): boolean {
|
||||
|
|
|
|||
|
|
@ -10,14 +10,14 @@ export async function workflowActivated({ data }: WorkflowActivated) {
|
|||
const { initializeWorkspace } = useCanvasOperations();
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
const workflowsListStore = useWorkflowsListStore();
|
||||
const documentStore = injectWorkflowDocumentStore();
|
||||
const workflowDocumentStore = injectWorkflowDocumentStore();
|
||||
const bannersStore = useBannersStore();
|
||||
const uiStore = useUIStore();
|
||||
|
||||
const { workflowId, activeVersionId } = data;
|
||||
|
||||
const workflowIsBeingViewed = workflowsStore.workflowId === workflowId;
|
||||
const activeVersionChanged = documentStore?.value?.activeVersionId !== activeVersionId;
|
||||
const activeVersionChanged = workflowDocumentStore?.value?.activeVersionId !== activeVersionId;
|
||||
if (workflowIsBeingViewed && activeVersionChanged) {
|
||||
// Only update workflow if there are no unsaved changes
|
||||
if (!uiStore.stateIsDirty) {
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import { useCanvasOperations } from '@/app/composables/useCanvasOperations';
|
|||
export async function workflowAutoDeactivated({ data }: WorkflowAutoDeactivated) {
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
const workflowsListStore = useWorkflowsListStore();
|
||||
const documentStore = injectWorkflowDocumentStore();
|
||||
const workflowDocumentStore = injectWorkflowDocumentStore();
|
||||
const { initializeWorkspace } = useCanvasOperations();
|
||||
const bannersStore = useBannersStore();
|
||||
const uiStore = useUIStore();
|
||||
|
|
@ -26,7 +26,7 @@ export async function workflowAutoDeactivated({ data }: WorkflowAutoDeactivated)
|
|||
// initializeWorkspace calls initState which sets the document store
|
||||
await initializeWorkspace(updatedWorkflow);
|
||||
} else {
|
||||
documentStore?.value?.setActiveState({ activeVersionId: null, activeVersion: null });
|
||||
workflowDocumentStore?.value?.setActiveState({ activeVersionId: null, activeVersion: null });
|
||||
}
|
||||
|
||||
bannersStore.pushBannerToStack('WORKFLOW_AUTO_DEACTIVATED');
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ export async function workflowDeactivated({ data }: WorkflowDeactivated) {
|
|||
const { initializeWorkspace } = useCanvasOperations();
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
const workflowsListStore = useWorkflowsListStore();
|
||||
const documentStore = injectWorkflowDocumentStore();
|
||||
const workflowDocumentStore = injectWorkflowDocumentStore();
|
||||
const uiStore = useUIStore();
|
||||
|
||||
if (workflowsStore.workflowId === data.workflowId) {
|
||||
|
|
@ -22,7 +22,7 @@ export async function workflowDeactivated({ data }: WorkflowDeactivated) {
|
|||
// initializeWorkspace calls initState which sets the document store
|
||||
await initializeWorkspace(updatedWorkflow);
|
||||
} else {
|
||||
documentStore?.value?.setActiveState({ activeVersionId: null, activeVersion: null });
|
||||
workflowDocumentStore?.value?.setActiveState({ activeVersionId: null, activeVersion: null });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,14 +10,14 @@ export async function workflowFailedToActivate(
|
|||
_options: { workflowState: WorkflowState },
|
||||
) {
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
const documentStore = injectWorkflowDocumentStore();
|
||||
const workflowDocumentStore = injectWorkflowDocumentStore();
|
||||
|
||||
if (workflowsStore.workflowId !== data.workflowId) {
|
||||
return;
|
||||
}
|
||||
|
||||
workflowsStore.setWorkflowInactive(data.workflowId);
|
||||
documentStore?.value?.setActiveState({ activeVersionId: null, activeVersion: null });
|
||||
workflowDocumentStore?.value?.setActiveState({ activeVersionId: null, activeVersion: null });
|
||||
|
||||
const toast = useToast();
|
||||
const i18n = useI18n();
|
||||
|
|
|
|||
|
|
@ -6,6 +6,10 @@ import { renderComponent } from '@/__tests__/render';
|
|||
import { setActivePinia } from 'pinia';
|
||||
import { createTestingPinia } from '@pinia/testing';
|
||||
import { injectWorkflowState, useWorkflowState, type WorkflowState } from './useWorkflowState';
|
||||
import {
|
||||
useWorkflowDocumentStore,
|
||||
createWorkflowDocumentId,
|
||||
} from '@/app/stores/workflowDocument.store';
|
||||
|
||||
vi.mock('@/app/composables/useWorkflowState', async () => {
|
||||
const actual = await vi.importActual('@/app/composables/useWorkflowState');
|
||||
|
|
@ -118,10 +122,14 @@ describe('useResolvedExpression', () => {
|
|||
|
||||
it('should re-resolve when workflow name changes', async () => {
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
workflowsStore.workflow.id = 'test-workflow';
|
||||
const workflowDocumentStore = useWorkflowDocumentStore(
|
||||
createWorkflowDocumentId('test-workflow'),
|
||||
);
|
||||
const resolveExpressionSpy = mockResolveExpression();
|
||||
resolveExpressionSpy.mockImplementation(async () => workflowsStore.workflow.name);
|
||||
resolveExpressionSpy.mockImplementation(async () => workflowDocumentStore.name);
|
||||
|
||||
workflowState.setWorkflowName({ newName: 'Old Name', setStateDirty: false });
|
||||
workflowDocumentStore.setName('Old Name');
|
||||
|
||||
const { resolvedExpressionString } = await renderTestComponent({
|
||||
expression: '={{ $workflow.name }}',
|
||||
|
|
@ -135,7 +143,7 @@ describe('useResolvedExpression', () => {
|
|||
expect(toValue(resolvedExpressionString)).toBe('Old Name');
|
||||
|
||||
// Update name and expect re-resolution
|
||||
workflowState.setWorkflowName({ newName: 'New Name', setStateDirty: false });
|
||||
workflowDocumentStore.setName('New Name');
|
||||
await nextTick();
|
||||
vi.advanceTimersByTime(200);
|
||||
await nextTick();
|
||||
|
|
|
|||
|
|
@ -18,6 +18,10 @@ import {
|
|||
watch,
|
||||
} from 'vue';
|
||||
import { useWorkflowHelpers, type ResolveParameterOptions } from './useWorkflowHelpers';
|
||||
import {
|
||||
useWorkflowDocumentStore,
|
||||
createWorkflowDocumentId,
|
||||
} from '@/app/stores/workflowDocument.store';
|
||||
import { ExpressionLocalResolveContextSymbol } from '@/app/constants';
|
||||
import type { ExpressionLocalResolveContext } from '@/app/types/expressions';
|
||||
|
||||
|
|
@ -36,6 +40,11 @@ export function useResolvedExpression({
|
|||
}) {
|
||||
const ndvStore = useNDVStore();
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
const workflowDocumentStore = computed(() =>
|
||||
workflowsStore.workflowId
|
||||
? useWorkflowDocumentStore(createWorkflowDocumentId(workflowsStore.workflowId))
|
||||
: undefined,
|
||||
);
|
||||
|
||||
const { resolveExpression } = useWorkflowHelpers();
|
||||
|
||||
|
|
@ -119,7 +128,7 @@ export function useResolvedExpression({
|
|||
toRef(additionalData),
|
||||
() => workflowsStore.getWorkflowExecution,
|
||||
() => workflowsStore.getWorkflowRunData,
|
||||
() => workflowsStore.workflow.name,
|
||||
() => workflowDocumentStore.value?.name,
|
||||
targetItem,
|
||||
],
|
||||
debouncedUpdateExpression,
|
||||
|
|
|
|||
|
|
@ -247,7 +247,6 @@ describe('useWorkflowHelpers', () => {
|
|||
});
|
||||
const addWorkflowSpy = vi.spyOn(workflowsListStore, 'addWorkflow');
|
||||
const setWorkflowIdSpy = vi.spyOn(workflowState, 'setWorkflowId');
|
||||
const setWorkflowNameSpy = vi.spyOn(workflowState, 'setWorkflowName');
|
||||
const setWorkflowVersionDataSpy = vi.spyOn(workflowsStore, 'setWorkflowVersionData');
|
||||
const setWorkflowSharedWithSpy = vi.spyOn(workflowsEEStore, 'setWorkflowSharedWith');
|
||||
const upsertTagsSpy = vi.spyOn(tagsStore, 'upsertTags');
|
||||
|
|
@ -256,10 +255,6 @@ describe('useWorkflowHelpers', () => {
|
|||
|
||||
expect(addWorkflowSpy).toHaveBeenCalledWith(workflowData);
|
||||
expect(setWorkflowIdSpy).toHaveBeenCalledWith('1');
|
||||
expect(setWorkflowNameSpy).toHaveBeenCalledWith({
|
||||
newName: 'Test Workflow',
|
||||
setStateDirty: false,
|
||||
});
|
||||
expect(setWorkflowVersionDataSpy).toHaveBeenCalledWith({
|
||||
versionId: 'v1',
|
||||
name: null,
|
||||
|
|
@ -322,12 +317,12 @@ describe('useWorkflowHelpers', () => {
|
|||
};
|
||||
|
||||
workflowsStore.workflowId = workflowId;
|
||||
workflowsStore.workflowName = 'Test Workflow';
|
||||
workflowsStore.allNodes = [];
|
||||
workflowsStore.workflow.versionId = 'v1';
|
||||
|
||||
const documentId = createWorkflowDocumentId(workflowId);
|
||||
const workflowDocumentStore = useWorkflowDocumentStore(documentId);
|
||||
Object.defineProperty(workflowDocumentStore, 'name', { value: 'Test Workflow' });
|
||||
Object.defineProperty(workflowDocumentStore, 'connectionsBySourceNode', {
|
||||
value: initialConnections,
|
||||
configurable: true,
|
||||
|
|
@ -354,7 +349,6 @@ describe('useWorkflowHelpers', () => {
|
|||
const tagIds = ['tag1', 'tag2'];
|
||||
|
||||
workflowsStore.workflowId = workflowId;
|
||||
workflowsStore.workflowName = 'Test Workflow';
|
||||
workflowsStore.allNodes = [];
|
||||
workflowsStore.isWorkflowActive = false;
|
||||
workflowsStore.workflow.settings = { executionOrder: 'v1' };
|
||||
|
|
@ -368,7 +362,8 @@ describe('useWorkflowHelpers', () => {
|
|||
configurable: true,
|
||||
});
|
||||
|
||||
// Note: createTestingPinia() stubs actions by default, so setTags()/setSettings() won't work
|
||||
// Note: createTestingPinia() stubs actions by default, so setTags()/setSettings()/setName() won't work
|
||||
Object.defineProperty(workflowDocumentStore, 'name', { value: 'Test Workflow' });
|
||||
Object.defineProperty(workflowDocumentStore, 'tags', {
|
||||
value: tagIds,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -610,7 +610,7 @@ export function useWorkflowHelpers() {
|
|||
const connections = deepCopy(workflowConnections);
|
||||
|
||||
const data: WorkflowData = {
|
||||
name: workflowsStore.workflowName,
|
||||
name: workflowDocumentStore.name,
|
||||
nodes,
|
||||
pinData: workflowDocumentStore.getPinDataSnapshot(),
|
||||
connections,
|
||||
|
|
@ -889,17 +889,17 @@ export function useWorkflowHelpers() {
|
|||
uiStore.markStateClean();
|
||||
}
|
||||
|
||||
const docStore = useWorkflowDocumentStore(createWorkflowDocumentId(workflowId));
|
||||
const workflowDocumentStore = useWorkflowDocumentStore(createWorkflowDocumentId(workflowId));
|
||||
|
||||
if (workflow.activeVersion) {
|
||||
workflowsStore.setWorkflowActive(workflowId, workflow.activeVersion, isCurrentWorkflow);
|
||||
docStore.setActiveState({
|
||||
workflowDocumentStore.setActiveState({
|
||||
activeVersionId: workflow.activeVersion.versionId,
|
||||
activeVersion: workflow.activeVersion,
|
||||
});
|
||||
} else {
|
||||
workflowsStore.setWorkflowInactive(workflowId);
|
||||
docStore.setActiveState({
|
||||
workflowDocumentStore.setActiveState({
|
||||
activeVersionId: null,
|
||||
activeVersion: null,
|
||||
});
|
||||
|
|
@ -986,10 +986,6 @@ export function useWorkflowHelpers() {
|
|||
workflowsListStore.addWorkflow(workflowData);
|
||||
workflowsStore.setDescription(workflowData.description);
|
||||
ws.setWorkflowId(workflowData.id);
|
||||
ws.setWorkflowName({
|
||||
newName: workflowData.name,
|
||||
setStateDirty: uiStore.stateIsDirty,
|
||||
});
|
||||
workflowsStore.setWorkflowVersionData({
|
||||
versionId: workflowData.versionId,
|
||||
name: null,
|
||||
|
|
@ -1036,7 +1032,12 @@ export function useWorkflowHelpers() {
|
|||
initializedWorkflowDocumentStore.onSettingsChange(({ payload }) => {
|
||||
workflowsStore.workflowObject.setSettings(payload.settings);
|
||||
});
|
||||
initializedWorkflowDocumentStore.onNameChange(({ payload }) => {
|
||||
workflowsStore.workflowObject.name = payload.name;
|
||||
workflowsListStore.updateWorkflowInCache(workflowData.id, { name: payload.name });
|
||||
});
|
||||
|
||||
initializedWorkflowDocumentStore.setName(workflowData.name);
|
||||
initializedWorkflowDocumentStore.setTags(tagIds);
|
||||
initializedWorkflowDocumentStore.setActiveState({
|
||||
activeVersionId: workflowData.activeVersionId,
|
||||
|
|
|
|||
|
|
@ -179,7 +179,7 @@ export function useWorkflowInitialization(workflowState: WorkflowState) {
|
|||
async function handleDebugModeRoute() {
|
||||
if (!isDebugRoute.value) return;
|
||||
|
||||
documentTitle.setDocumentTitle(workflowsStore.workflowName, 'DEBUG');
|
||||
documentTitle.setDocumentTitle(currentWorkflowDocumentStore.value?.name ?? '', 'DEBUG');
|
||||
|
||||
if (!workflowsStore.isInDebugMode) {
|
||||
const executionId = route.params.executionId;
|
||||
|
|
@ -257,17 +257,23 @@ export function useWorkflowInitialization(workflowState: WorkflowState) {
|
|||
|
||||
const parentFolderId = route.query.parentFolderId as string | undefined;
|
||||
|
||||
await workflowState.getNewWorkflowData(
|
||||
workflowState.setWorkflowId(workflowId.value);
|
||||
|
||||
const workflowDocumentId = createWorkflowDocumentId(workflowId.value);
|
||||
currentWorkflowDocumentStore.value = useWorkflowDocumentStore(workflowDocumentId);
|
||||
|
||||
// Sync document store name → workflowObject + list cache (mirrors initializeWorkflowDocument)
|
||||
currentWorkflowDocumentStore.value.onNameChange(({ payload }) => {
|
||||
workflowsStore.workflowObject.name = payload.name;
|
||||
workflowsListStore.updateWorkflowInCache(workflowId.value, { name: payload.name });
|
||||
});
|
||||
|
||||
const workflowData = await workflowState.getNewWorkflowData(
|
||||
undefined,
|
||||
projectsStore.currentProjectId,
|
||||
parentFolderId,
|
||||
);
|
||||
|
||||
workflowState.setWorkflowId(workflowId.value);
|
||||
|
||||
// Create document store for new workflow
|
||||
const workflowDocumentId = createWorkflowDocumentId(workflowId.value);
|
||||
currentWorkflowDocumentStore.value = useWorkflowDocumentStore(workflowDocumentId);
|
||||
currentWorkflowDocumentStore.value.setName(workflowData.name);
|
||||
const homeProject = projectsStore.currentProject ?? projectsStore.personalProject ?? null;
|
||||
currentWorkflowDocumentStore.value.setHomeProject(homeProject);
|
||||
|
||||
|
|
|
|||
|
|
@ -53,7 +53,6 @@ vi.mock('@n8n/permissions', () => ({
|
|||
|
||||
const mockWorkflowState = {
|
||||
setWorkflowProperty: vi.fn(),
|
||||
setWorkflowName: vi.fn(),
|
||||
setActive: vi.fn(),
|
||||
setWorkflowId: vi.fn(),
|
||||
setNodeValue: vi.fn(),
|
||||
|
|
@ -172,7 +171,6 @@ describe('useWorkflowSaving', () => {
|
|||
modalConfirmSpy.mockResolvedValue(MODAL_CONFIRM);
|
||||
|
||||
const mockWorkflowState: Partial<WorkflowState> = {
|
||||
setWorkflowName: vi.fn(),
|
||||
setWorkflowProperty: vi.fn(),
|
||||
};
|
||||
|
||||
|
|
@ -589,7 +587,6 @@ describe('useWorkflowSaving', () => {
|
|||
workflowDocumentStore.setTags(tagIds);
|
||||
|
||||
const testWorkflowState: Partial<WorkflowState> = {
|
||||
setWorkflowName: vi.fn(),
|
||||
setWorkflowProperty: vi.fn(),
|
||||
};
|
||||
|
||||
|
|
@ -665,7 +662,6 @@ describe('useWorkflowSaving', () => {
|
|||
const initialDirtyCount = uiStore.dirtyStateSetCount;
|
||||
|
||||
const mockWorkflowState: Partial<WorkflowState> = {
|
||||
setWorkflowName: vi.fn(),
|
||||
setWorkflowProperty: vi.fn(),
|
||||
};
|
||||
|
||||
|
|
@ -711,7 +707,6 @@ describe('useWorkflowSaving', () => {
|
|||
uiStore.markStateDirty();
|
||||
|
||||
const mockWorkflowState: Partial<WorkflowState> = {
|
||||
setWorkflowName: vi.fn(),
|
||||
setWorkflowProperty: vi.fn(),
|
||||
};
|
||||
|
||||
|
|
@ -747,7 +742,6 @@ describe('useWorkflowSaving', () => {
|
|||
const saveStore = useWorkflowSaveStore();
|
||||
|
||||
const mockWorkflowState: Partial<WorkflowState> = {
|
||||
setWorkflowName: vi.fn(),
|
||||
setWorkflowProperty: vi.fn(),
|
||||
};
|
||||
|
||||
|
|
@ -812,7 +806,6 @@ describe('useWorkflowSaving', () => {
|
|||
workflowsStore.workflowId = workflow.id;
|
||||
|
||||
const testWorkflowState: Partial<WorkflowState> = {
|
||||
setWorkflowName: vi.fn(),
|
||||
setWorkflowProperty: vi.fn(),
|
||||
};
|
||||
|
||||
|
|
@ -872,7 +865,6 @@ describe('useWorkflowSaving', () => {
|
|||
const saveStore = useWorkflowSaveStore();
|
||||
|
||||
const testWorkflowState: Partial<WorkflowState> = {
|
||||
setWorkflowName: vi.fn(),
|
||||
setWorkflowProperty: vi.fn(),
|
||||
};
|
||||
|
||||
|
|
@ -935,7 +927,6 @@ describe('useWorkflowSaving', () => {
|
|||
const saveStore = useWorkflowSaveStore();
|
||||
|
||||
const testWorkflowState: Partial<WorkflowState> = {
|
||||
setWorkflowName: vi.fn(),
|
||||
setWorkflowProperty: vi.fn(),
|
||||
};
|
||||
|
||||
|
|
@ -1014,7 +1005,6 @@ describe('useWorkflowSaving', () => {
|
|||
const saveStore = useWorkflowSaveStore();
|
||||
|
||||
const testWorkflowState: Partial<WorkflowState> = {
|
||||
setWorkflowName: vi.fn(),
|
||||
setWorkflowProperty: vi.fn(),
|
||||
};
|
||||
|
||||
|
|
@ -1079,7 +1069,6 @@ describe('useWorkflowSaving', () => {
|
|||
const saveStore = useWorkflowSaveStore();
|
||||
|
||||
const testWorkflowState: Partial<WorkflowState> = {
|
||||
setWorkflowName: vi.fn(),
|
||||
setWorkflowProperty: vi.fn(),
|
||||
};
|
||||
|
||||
|
|
@ -1116,7 +1105,6 @@ describe('useWorkflowSaving', () => {
|
|||
const initialRetryCount = saveStore.retryCount;
|
||||
|
||||
const testWorkflowState: Partial<WorkflowState> = {
|
||||
setWorkflowName: vi.fn(),
|
||||
setWorkflowProperty: vi.fn(),
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -19,21 +19,6 @@ describe('useWorkflowState', () => {
|
|||
workflowState = useWorkflowState();
|
||||
});
|
||||
|
||||
describe('setWorkflowName()', () => {
|
||||
it('should set the workflow name correctly', () => {
|
||||
workflowState.setWorkflowName({
|
||||
newName: 'New Workflow Name',
|
||||
setStateDirty: false,
|
||||
});
|
||||
expect(workflowsStore.workflow.name).toBe('New Workflow Name');
|
||||
});
|
||||
|
||||
it('should propagate name to workflowObject for pre-exec expressions', () => {
|
||||
workflowState.setWorkflowName({ newName: 'WF Title', setStateDirty: false });
|
||||
expect(workflowsStore.workflowObject.name).toBe('WF Title');
|
||||
});
|
||||
});
|
||||
|
||||
describe('markExecutionAsStopped', () => {
|
||||
beforeEach(() => {
|
||||
workflowsStore.workflowExecutionData = createTestWorkflowExecutionResponse({
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ import type {
|
|||
} from '@/features/execution/executions/executions.types';
|
||||
import { useUIStore } from '@/app/stores/ui.store';
|
||||
import { useWorkflowsStore } from '@/app/stores/workflows.store';
|
||||
import { useWorkflowsListStore } from '@/app/stores/workflowsList.store';
|
||||
import { useBuilderStore } from '@/features/ai/assistant/builder.store';
|
||||
import { getPairedItemsMapping } from '@/app/utils/pairedItemUtils';
|
||||
import {
|
||||
|
|
@ -50,7 +49,6 @@ export const workflowStateEventBus = createEventBus<WorkflowStateBusEvents>();
|
|||
|
||||
export function useWorkflowState() {
|
||||
const ws = useWorkflowsStore();
|
||||
const workflowsListStore = useWorkflowsListStore();
|
||||
const workflowStateStore = useWorkflowStateStore();
|
||||
const uiStore = useUIStore();
|
||||
const rootStore = useRootStore();
|
||||
|
|
@ -60,18 +58,6 @@ export function useWorkflowState() {
|
|||
// Workflow editing state
|
||||
////
|
||||
|
||||
function setWorkflowName(data: { newName: string; setStateDirty: boolean }) {
|
||||
if (data.setStateDirty) {
|
||||
uiStore.markStateDirty('metadata');
|
||||
}
|
||||
ws.workflow.name = data.newName;
|
||||
ws.workflowObject.name = data.newName;
|
||||
|
||||
if (ws.workflow.id && workflowsListStore.workflowsById[ws.workflow.id]) {
|
||||
workflowsListStore.workflowsById[ws.workflow.id].name = data.newName;
|
||||
}
|
||||
}
|
||||
|
||||
/** @deprecated Use `workflowDocumentStore.removeAllConnections()` instead. */
|
||||
function removeAllConnections(data: { setStateDirty: boolean }): void {
|
||||
if (data?.setStateDirty) {
|
||||
|
|
@ -161,8 +147,6 @@ export function useWorkflowState() {
|
|||
workflowData.name = name || DEFAULT_NEW_WORKFLOW_NAME;
|
||||
}
|
||||
|
||||
setWorkflowName({ newName: workflowData.name, setStateDirty: false });
|
||||
|
||||
return workflowData;
|
||||
}
|
||||
|
||||
|
|
@ -176,7 +160,10 @@ export function useWorkflowState() {
|
|||
setActiveExecutionId(undefined);
|
||||
workflowStateStore.executingNode.clearNodeExecutionQueue();
|
||||
ws.executionWaitingForWebhook = false;
|
||||
documentTitle.setDocumentTitle(ws.workflowName, 'IDLE');
|
||||
const workflowDocumentStore = ws.workflow.id
|
||||
? useWorkflowDocumentStore(createWorkflowDocumentId(ws.workflow.id))
|
||||
: undefined;
|
||||
documentTitle.setDocumentTitle(workflowDocumentStore?.name ?? '', 'IDLE');
|
||||
ws.workflowExecutionStartedData = undefined;
|
||||
|
||||
// TODO(ckolb): confirm this works across files?
|
||||
|
|
@ -208,8 +195,15 @@ export function useWorkflowState() {
|
|||
setWorkflowExecutionData(null);
|
||||
resetAllNodesIssues();
|
||||
|
||||
// Reset name via document store (triggers onNameChange → updates workflowObject.name)
|
||||
if (ws.workflow.id) {
|
||||
const workflowDocumentStore = useWorkflowDocumentStore(
|
||||
createWorkflowDocumentId(ws.workflow.id),
|
||||
);
|
||||
workflowDocumentStore.setName('');
|
||||
}
|
||||
|
||||
setWorkflowId('');
|
||||
setWorkflowName({ newName: '', setStateDirty: false });
|
||||
// Settings are managed by workflowDocumentStore; reset the runtime Workflow instance directly
|
||||
ws.workflowObject.setSettings({ ...DEFAULT_SETTINGS });
|
||||
// Note: Tags are now managed by workflowDocumentStore, which is disposed during reset
|
||||
|
|
@ -424,7 +418,6 @@ export function useWorkflowState() {
|
|||
setWorkflowExecutionData,
|
||||
resetAllNodesIssues,
|
||||
setWorkflowId,
|
||||
setWorkflowName,
|
||||
setWorkflowProperty,
|
||||
setActiveExecutionId,
|
||||
getNewWorkflowData,
|
||||
|
|
|
|||
|
|
@ -3,7 +3,10 @@ import { setActivePinia } from 'pinia';
|
|||
import { createTestingPinia } from '@pinia/testing';
|
||||
import { useWorkflowUpdate } from './useWorkflowUpdate';
|
||||
import { useWorkflowsStore } from '@/app/stores/workflows.store';
|
||||
import type { useWorkflowDocumentStore } from '@/app/stores/workflowDocument.store';
|
||||
import {
|
||||
useWorkflowDocumentStore,
|
||||
createWorkflowDocumentId,
|
||||
} 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';
|
||||
|
|
@ -29,6 +32,8 @@ vi.mock('@/features/workflows/canvas/canvas.utils', () => ({
|
|||
// Mock workflowDocumentStore - using hoisted for proper initialization
|
||||
const mockDocumentStore = vi.hoisted(() => ({
|
||||
allNodes: [] as INodeUi[],
|
||||
name: '',
|
||||
setName: vi.fn(),
|
||||
setNodes: vi.fn(),
|
||||
setConnections: vi.fn(),
|
||||
resetParametersLastUpdatedAt: vi.fn(),
|
||||
|
|
@ -46,7 +51,7 @@ vi.mock('@/app/stores/workflowDocument.store', () => ({
|
|||
|
||||
// Mock useWorkflowState - using hoisted for proper initialization
|
||||
const mockWorkflowState = vi.hoisted(() => ({
|
||||
setWorkflowName: vi.fn(),
|
||||
resetParametersLastUpdatedAt: vi.fn(),
|
||||
}));
|
||||
vi.mock('@/app/composables/useWorkflowState', () => ({
|
||||
injectWorkflowState: vi.fn(() => mockWorkflowState),
|
||||
|
|
@ -91,6 +96,8 @@ describe('useWorkflowUpdate', () => {
|
|||
|
||||
// Setup default mocks
|
||||
(mockDocumentStore as { allNodes: INodeUi[] }).allNodes = [];
|
||||
(mockDocumentStore as { name: string }).name = '';
|
||||
vi.mocked(mockDocumentStore.setName).mockClear();
|
||||
vi.mocked(mockDocumentStore.setNodes).mockClear();
|
||||
vi.mocked(mockDocumentStore.setConnections).mockClear();
|
||||
vi.mocked(mockDocumentStore.resetParametersLastUpdatedAt).mockClear();
|
||||
|
|
@ -639,7 +646,14 @@ describe('useWorkflowUpdate', () => {
|
|||
|
||||
describe('workflow name update', () => {
|
||||
it('should update workflow name on initial generation when name starts with default', async () => {
|
||||
workflowsStore.workflow.name = DEFAULT_NEW_WORKFLOW_NAME;
|
||||
const workflowDocumentStore = useWorkflowDocumentStore(
|
||||
createWorkflowDocumentId('test-workflow'),
|
||||
);
|
||||
Object.defineProperty(workflowDocumentStore, 'name', {
|
||||
value: DEFAULT_NEW_WORKFLOW_NAME,
|
||||
configurable: true,
|
||||
});
|
||||
const setNameSpy = vi.spyOn(workflowDocumentStore, 'setName');
|
||||
|
||||
const { updateWorkflow } = useWorkflowUpdate();
|
||||
|
||||
|
|
@ -652,14 +666,18 @@ describe('useWorkflowUpdate', () => {
|
|||
{ isInitialGeneration: true },
|
||||
);
|
||||
|
||||
expect(mockWorkflowState.setWorkflowName).toHaveBeenCalledWith({
|
||||
newName: 'My Generated Workflow',
|
||||
setStateDirty: false,
|
||||
});
|
||||
expect(setNameSpy).toHaveBeenCalledWith('My Generated Workflow');
|
||||
});
|
||||
|
||||
it('should not update workflow name when not initial generation', async () => {
|
||||
workflowsStore.workflow.name = DEFAULT_NEW_WORKFLOW_NAME;
|
||||
const workflowDocumentStore = useWorkflowDocumentStore(
|
||||
createWorkflowDocumentId('test-workflow'),
|
||||
);
|
||||
Object.defineProperty(workflowDocumentStore, 'name', {
|
||||
value: DEFAULT_NEW_WORKFLOW_NAME,
|
||||
configurable: true,
|
||||
});
|
||||
const setNameSpy = vi.spyOn(workflowDocumentStore, 'setName');
|
||||
|
||||
const { updateWorkflow } = useWorkflowUpdate();
|
||||
|
||||
|
|
@ -672,11 +690,18 @@ describe('useWorkflowUpdate', () => {
|
|||
{ isInitialGeneration: false },
|
||||
);
|
||||
|
||||
expect(mockWorkflowState.setWorkflowName).not.toHaveBeenCalled();
|
||||
expect(setNameSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not update workflow name when current name does not start with default', async () => {
|
||||
workflowsStore.workflow.name = 'Custom Workflow Name';
|
||||
const workflowDocumentStore = useWorkflowDocumentStore(
|
||||
createWorkflowDocumentId('test-workflow'),
|
||||
);
|
||||
Object.defineProperty(workflowDocumentStore, 'name', {
|
||||
value: 'Custom Workflow Name',
|
||||
configurable: true,
|
||||
});
|
||||
const setNameSpy = vi.spyOn(workflowDocumentStore, 'setName');
|
||||
|
||||
const { updateWorkflow } = useWorkflowUpdate();
|
||||
|
||||
|
|
@ -689,7 +714,7 @@ describe('useWorkflowUpdate', () => {
|
|||
{ isInitialGeneration: true },
|
||||
);
|
||||
|
||||
expect(mockWorkflowState.setWorkflowName).not.toHaveBeenCalled();
|
||||
expect(setNameSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@ 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 { injectWorkflowState } from '@/app/composables/useWorkflowState';
|
||||
import { computed } from 'vue';
|
||||
import { useCanvasOperations } from '@/app/composables/useCanvasOperations';
|
||||
import { useNodeHelpers } from '@/app/composables/useNodeHelpers';
|
||||
|
|
@ -44,7 +43,6 @@ export type UpdateWorkflowResult =
|
|||
|
||||
export function useWorkflowUpdate() {
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
const workflowState = injectWorkflowState();
|
||||
const credentialsStore = useCredentialsStore();
|
||||
const nodeTypesStore = useNodeTypesStore();
|
||||
const builderStore = useBuilderStore();
|
||||
|
|
@ -323,12 +321,13 @@ export function useWorkflowUpdate() {
|
|||
* Update workflow name if initial generation and name starts with default
|
||||
*/
|
||||
function updateWorkflowNameIfNeeded(name?: string, isInitialGeneration?: boolean): void {
|
||||
if (
|
||||
name &&
|
||||
isInitialGeneration &&
|
||||
workflowsStore.workflow.name.startsWith(DEFAULT_NEW_WORKFLOW_NAME)
|
||||
) {
|
||||
workflowState.setWorkflowName({ newName: name, setStateDirty: false });
|
||||
if (!name || !isInitialGeneration || !workflowsStore.workflowId) return;
|
||||
|
||||
const workflowDocumentStore = useWorkflowDocumentStore(
|
||||
createWorkflowDocumentId(workflowsStore.workflowId),
|
||||
);
|
||||
if (workflowDocumentStore.name.startsWith(DEFAULT_NEW_WORKFLOW_NAME)) {
|
||||
workflowDocumentStore.setName(name);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import { useWorkflowDocumentUsedCredentials } from './workflowDocument/useWorkfl
|
|||
import { useWorkflowDocumentNodes } from './workflowDocument/useWorkflowDocumentNodes';
|
||||
import { useWorkflowDocumentViewport } from './workflowDocument/useWorkflowDocumentViewport';
|
||||
import { useWorkflowDocumentConnections } from './workflowDocument/useWorkflowDocumentConnections';
|
||||
import { useWorkflowDocumentName } from './workflowDocument/useWorkflowDocumentName';
|
||||
import { useUIStore } from '@/app/stores/ui.store';
|
||||
import { useNodeTypesStore } from '@/app/stores/nodeTypes.store';
|
||||
|
||||
|
|
@ -60,6 +61,7 @@ export function useWorkflowDocumentStore(id: WorkflowDocumentId) {
|
|||
return defineStore(getWorkflowDocumentStoreId(id), () => {
|
||||
const [workflowId, workflowVersion] = id.split('@');
|
||||
|
||||
const workflowDocumentName = useWorkflowDocumentName();
|
||||
const workflowDocumentActive = useWorkflowDocumentActive();
|
||||
const workflowDocumentHomeProject = useWorkflowDocumentHomeProject();
|
||||
const workflowDocumentChecksum = useWorkflowDocumentChecksum();
|
||||
|
|
@ -101,6 +103,7 @@ export function useWorkflowDocumentStore(id: WorkflowDocumentId) {
|
|||
return {
|
||||
workflowId,
|
||||
workflowVersion,
|
||||
...workflowDocumentName,
|
||||
...workflowDocumentActive,
|
||||
...workflowDocumentHomeProject,
|
||||
...workflowDocumentChecksum,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,51 @@
|
|||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { useWorkflowDocumentName } from './useWorkflowDocumentName';
|
||||
|
||||
function createName() {
|
||||
return useWorkflowDocumentName();
|
||||
}
|
||||
|
||||
describe('useWorkflowDocumentName', () => {
|
||||
describe('initial state', () => {
|
||||
it('should start with empty string', () => {
|
||||
const { name } = createName();
|
||||
expect(name.value).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('setName', () => {
|
||||
it('should set name and fire event hook', () => {
|
||||
const { name, setName, onNameChange } = createName();
|
||||
const hookSpy = vi.fn();
|
||||
onNameChange(hookSpy);
|
||||
|
||||
setName('My Workflow');
|
||||
|
||||
expect(name.value).toBe('My Workflow');
|
||||
expect(hookSpy).toHaveBeenCalledWith({
|
||||
action: 'update',
|
||||
payload: { name: 'My Workflow' },
|
||||
});
|
||||
});
|
||||
|
||||
it('should replace existing value', () => {
|
||||
const { name, setName } = createName();
|
||||
setName('First Name');
|
||||
|
||||
setName('Second Name');
|
||||
|
||||
expect(name.value).toBe('Second Name');
|
||||
});
|
||||
|
||||
it('should fire event hook on every call', () => {
|
||||
const { setName, onNameChange } = createName();
|
||||
const hookSpy = vi.fn();
|
||||
onNameChange(hookSpy);
|
||||
|
||||
setName('First');
|
||||
setName('Second');
|
||||
|
||||
expect(hookSpy).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
import { ref, readonly } from 'vue';
|
||||
import { createEventHook } from '@vueuse/core';
|
||||
import { CHANGE_ACTION } from './types';
|
||||
import type { ChangeAction, ChangeEvent } from './types';
|
||||
|
||||
export type NamePayload = {
|
||||
name: string;
|
||||
};
|
||||
|
||||
export type NameChangeEvent = ChangeEvent<NamePayload>;
|
||||
|
||||
export function useWorkflowDocumentName() {
|
||||
const name = ref<string>('');
|
||||
|
||||
const onNameChange = createEventHook<NameChangeEvent>();
|
||||
|
||||
function applyName(value: string, action: ChangeAction = CHANGE_ACTION.UPDATE) {
|
||||
name.value = value;
|
||||
void onNameChange.trigger({ action, payload: { name: value } });
|
||||
}
|
||||
|
||||
function setName(value: string) {
|
||||
applyName(value);
|
||||
}
|
||||
|
||||
return {
|
||||
name: readonly(name),
|
||||
setName,
|
||||
onNameChange: onNameChange.on,
|
||||
};
|
||||
}
|
||||
|
|
@ -149,8 +149,6 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
|
|||
const chatPartialExecutionDestinationNode = ref<string | null>(null);
|
||||
const selectedTriggerNodeName = ref<string>();
|
||||
|
||||
const workflowName = computed(() => workflow.value.name);
|
||||
|
||||
const workflowId = computed(() => workflow.value.id);
|
||||
|
||||
const workflowVersionId = computed(() => workflow.value.versionId);
|
||||
|
|
@ -587,7 +585,7 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
|
|||
|
||||
return new Workflow({
|
||||
id,
|
||||
name: workflow.value.name,
|
||||
name: workflowDocumentStore?.name ?? '',
|
||||
nodes: copyData ? deepCopy(nodes) : nodes,
|
||||
connections: copyData ? deepCopy(connections) : connections,
|
||||
active: false,
|
||||
|
|
@ -1740,7 +1738,6 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
|
|||
isInDebugMode,
|
||||
chatMessages,
|
||||
chatPartialExecutionDestinationNode,
|
||||
workflowName,
|
||||
workflowId,
|
||||
workflowVersionId,
|
||||
isNewWorkflow,
|
||||
|
|
|
|||
|
|
@ -121,7 +121,6 @@ let nodeTypesStore: ReturnType<typeof mockedStore<typeof useNodeTypesStore>>;
|
|||
let credentialsStore: ReturnType<typeof mockedStore<typeof useCredentialsStore>>;
|
||||
let pinia: ReturnType<typeof createTestingPinia>;
|
||||
|
||||
let setWorkflowNameSpy: Mock;
|
||||
let getNodeTypeSpy: Mock;
|
||||
let getCredentialsByTypeSpy: Mock;
|
||||
|
||||
|
|
@ -184,11 +183,6 @@ describe('AI Builder store', () => {
|
|||
workflowState = useWorkflowState();
|
||||
vi.mocked(injectWorkflowState).mockReturnValue(workflowState);
|
||||
|
||||
setWorkflowNameSpy = vi.fn().mockImplementation(({ newName }: { newName: string }) => {
|
||||
workflowsStore.workflow.name = newName;
|
||||
});
|
||||
vi.spyOn(workflowState, 'setWorkflowName').mockImplementation(setWorkflowNameSpy);
|
||||
|
||||
getNodeTypeSpy = vi.fn();
|
||||
vi.spyOn(nodeTypesStore, 'getNodeType', 'get').mockReturnValue(getNodeTypeSpy);
|
||||
|
||||
|
|
@ -3208,7 +3202,10 @@ describe('AI Builder store', () => {
|
|||
describe('Page title status', () => {
|
||||
it('should set title to AI_BUILDING when streaming starts', async () => {
|
||||
const builderStore = useBuilderStore();
|
||||
workflowsStore.workflowName = 'Test Workflow';
|
||||
const workflowDocumentStore = useWorkflowDocumentStore(
|
||||
createWorkflowDocumentId(workflowsStore.workflowId),
|
||||
);
|
||||
workflowDocumentStore.setName('Test Workflow');
|
||||
|
||||
// Mock the API to prevent actual calls
|
||||
apiSpy.mockImplementation(() => {});
|
||||
|
|
@ -3222,7 +3219,10 @@ describe('AI Builder store', () => {
|
|||
Object.defineProperty(document, 'hidden', { value: true, configurable: true });
|
||||
|
||||
const builderStore = useBuilderStore();
|
||||
workflowsStore.workflowName = 'Test Workflow';
|
||||
const workflowDocumentStore = useWorkflowDocumentStore(
|
||||
createWorkflowDocumentId(workflowsStore.workflowId),
|
||||
);
|
||||
workflowDocumentStore.setName('Test Workflow');
|
||||
|
||||
// Start streaming first
|
||||
builderStore.streaming = true;
|
||||
|
|
@ -3237,7 +3237,10 @@ describe('AI Builder store', () => {
|
|||
Object.defineProperty(document, 'hidden', { value: false, configurable: true });
|
||||
|
||||
const builderStore = useBuilderStore();
|
||||
workflowsStore.workflowName = 'Test Workflow';
|
||||
const workflowDocumentStore = useWorkflowDocumentStore(
|
||||
createWorkflowDocumentId(workflowsStore.workflowId),
|
||||
);
|
||||
workflowDocumentStore.setName('Test Workflow');
|
||||
|
||||
// Start streaming first
|
||||
builderStore.streaming = true;
|
||||
|
|
@ -3252,7 +3255,10 @@ describe('AI Builder store', () => {
|
|||
Object.defineProperty(document, 'hidden', { value: true, configurable: true });
|
||||
|
||||
const builderStore = useBuilderStore();
|
||||
workflowsStore.workflowName = 'Test Workflow';
|
||||
const workflowDocumentStore = useWorkflowDocumentStore(
|
||||
createWorkflowDocumentId(workflowsStore.workflowId),
|
||||
);
|
||||
workflowDocumentStore.setName('Test Workflow');
|
||||
|
||||
builderStore.streaming = true;
|
||||
builderStore.abortStreaming();
|
||||
|
|
@ -3268,7 +3274,10 @@ describe('AI Builder store', () => {
|
|||
Object.defineProperty(document, 'hidden', { value: false, configurable: true });
|
||||
|
||||
const builderStore = useBuilderStore();
|
||||
workflowsStore.workflowName = 'Test Workflow';
|
||||
const workflowDocumentStore = useWorkflowDocumentStore(
|
||||
createWorkflowDocumentId(workflowsStore.workflowId),
|
||||
);
|
||||
workflowDocumentStore.setName('Test Workflow');
|
||||
|
||||
setDocumentTitleMock.mockClear();
|
||||
|
||||
|
|
@ -3607,8 +3616,11 @@ describe('AI Builder store', () => {
|
|||
|
||||
const triggerSuccessfulStreamingComplete = async () => {
|
||||
const builderStore = useBuilderStore();
|
||||
workflowsStore.workflowName = 'Test Workflow';
|
||||
workflowsStore.workflowId = 'test-workflow-123';
|
||||
const workflowDocumentStore = useWorkflowDocumentStore(
|
||||
createWorkflowDocumentId('test-workflow-123'),
|
||||
);
|
||||
workflowDocumentStore.setName('Test Workflow');
|
||||
workflowsStore.isNewWorkflow = false;
|
||||
workflowsStore.workflowVersionId = 'version-1';
|
||||
|
||||
|
|
@ -3670,7 +3682,10 @@ describe('AI Builder store', () => {
|
|||
mockIsNotificationsEnabled.value = true;
|
||||
|
||||
const builderStore = useBuilderStore();
|
||||
workflowsStore.workflowName = 'Test Workflow';
|
||||
const workflowDocumentStore = useWorkflowDocumentStore(
|
||||
createWorkflowDocumentId(workflowsStore.workflowId),
|
||||
);
|
||||
workflowDocumentStore.setName('Test Workflow');
|
||||
|
||||
builderStore.streaming = true;
|
||||
builderStore.abortStreaming();
|
||||
|
|
@ -4030,10 +4045,10 @@ describe('AI Builder store', () => {
|
|||
builderStore.storeGeneratedPinData(pinData);
|
||||
|
||||
expect(builderStore.hasDeferredPinData).toBe(true);
|
||||
const wfDocStore = useWorkflowDocumentStore(
|
||||
const workflowDocumentStore = useWorkflowDocumentStore(
|
||||
createWorkflowDocumentId(workflowsStore.workflowId),
|
||||
);
|
||||
expect(wfDocStore.pinData).toEqual({});
|
||||
expect(workflowDocumentStore.pinData).toEqual({});
|
||||
});
|
||||
|
||||
it('storeGeneratedPinData merges multiple calls', () => {
|
||||
|
|
@ -4058,10 +4073,10 @@ describe('AI Builder store', () => {
|
|||
builderStore.storeGeneratedPinData(pinData);
|
||||
builderStore.applyGeneratedPinData();
|
||||
|
||||
const wfDocStore = useWorkflowDocumentStore(
|
||||
const workflowDocumentStore = useWorkflowDocumentStore(
|
||||
createWorkflowDocumentId(workflowsStore.workflowId),
|
||||
);
|
||||
expect(wfDocStore.pinData).toEqual(pinData);
|
||||
expect(workflowDocumentStore.pinData).toEqual(pinData);
|
||||
expect(uiStore.stateIsDirty).toBe(true);
|
||||
expect(builderStore.hasDeferredPinData).toBe(false);
|
||||
expect(builderStore.pinDataApplied).toBe(true);
|
||||
|
|
|
|||
|
|
@ -548,7 +548,7 @@ export const useBuilderStore = defineStore(STORES.BUILDER, () => {
|
|||
return;
|
||||
}
|
||||
|
||||
const workflowName = workflowsStore.workflowName;
|
||||
const workflowName = workflowDocumentStore.value?.name ?? '';
|
||||
|
||||
const titleKey =
|
||||
completionType === 'workflow-ready'
|
||||
|
|
@ -641,7 +641,7 @@ export const useBuilderStore = defineStore(STORES.BUILDER, () => {
|
|||
// Update page title on completion. We show Done when the user is not on the page
|
||||
// Browser notifications are only shown when the tab is hidden
|
||||
if (document.hidden) {
|
||||
documentTitle.setDocumentTitle(workflowsStore.workflowName, 'AI_DONE');
|
||||
documentTitle.setDocumentTitle(workflowDocumentStore.value?.name ?? '', 'AI_DONE');
|
||||
if (!wasAborted && userMessageId) {
|
||||
const completionType = hasWorkflowUpdateInCurrentBatch(userMessageId)
|
||||
? 'workflow-ready'
|
||||
|
|
@ -649,7 +649,7 @@ export const useBuilderStore = defineStore(STORES.BUILDER, () => {
|
|||
notifyOnCompletion(completionType);
|
||||
}
|
||||
} else {
|
||||
documentTitle.setDocumentTitle(workflowsStore.workflowName, 'IDLE');
|
||||
documentTitle.setDocumentTitle(workflowDocumentStore.value?.name ?? '', 'IDLE');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -726,7 +726,7 @@ export const useBuilderStore = defineStore(STORES.BUILDER, () => {
|
|||
|
||||
// Updates page title to show AI is building (skip for help questions)
|
||||
if (!isHelpStreaming.value) {
|
||||
documentTitle.setDocumentTitle(workflowsStore.workflowName, 'AI_BUILDING');
|
||||
documentTitle.setDocumentTitle(workflowDocumentStore.value?.name ?? '', 'AI_BUILDING');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1347,10 +1347,10 @@ export const useBuilderStore = defineStore(STORES.BUILDER, () => {
|
|||
posthogStore.getVariant(CODE_WORKFLOW_BUILDER_EXPERIMENT.name) ===
|
||||
CODE_WORKFLOW_BUILDER_EXPERIMENT.codePinData;
|
||||
if (isPinDataEnabled) {
|
||||
const wfDocStore = workflowsStore.workflowId
|
||||
const workflowDocumentStore = workflowsStore.workflowId
|
||||
? useWorkflowDocumentStore(createWorkflowDocumentId(workflowsStore.workflowId))
|
||||
: undefined;
|
||||
const pinData = wfDocStore?.getPinDataSnapshot();
|
||||
const pinData = workflowDocumentStore?.getPinDataSnapshot();
|
||||
if (pinData && Object.keys(pinData).length > 0) {
|
||||
generatedPinData.value = pinData;
|
||||
pinDataApplied.value = true;
|
||||
|
|
@ -1658,7 +1658,7 @@ export const useBuilderStore = defineStore(STORES.BUILDER, () => {
|
|||
*/
|
||||
function clearDoneIndicatorTitle() {
|
||||
if (documentTitle.getDocumentState() === 'AI_DONE') {
|
||||
documentTitle.setDocumentTitle(workflowsStore.workflowName, 'IDLE');
|
||||
documentTitle.setDocumentTitle(workflowDocumentStore.value?.name ?? '', 'IDLE');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -72,7 +72,6 @@ vi.mock('@/app/composables/useWorkflowState', async () => {
|
|||
return {
|
||||
...actual,
|
||||
injectWorkflowState: vi.fn(() => ({
|
||||
setWorkflowName: vi.fn(),
|
||||
isWorkflowRunning: ref(false),
|
||||
})),
|
||||
};
|
||||
|
|
|
|||
|
|
@ -68,7 +68,7 @@ const chatTriggerNode = computed(() =>
|
|||
const agentDisplayName = computed(() => {
|
||||
const triggerName = chatTriggerNode.value?.parameters?.agentName;
|
||||
if (typeof triggerName === 'string' && triggerName.trim()) return triggerName.trim();
|
||||
return workflowsStore.workflowName || 'Workflow';
|
||||
return workflowDocumentStore?.value?.name || 'Workflow';
|
||||
});
|
||||
|
||||
const workflowAgent = computed<ChatModelDto | null>(() => {
|
||||
|
|
|
|||
|
|
@ -3,12 +3,21 @@ 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 { usePopOutWindow } from '@/features/execution/logs/composables/usePopOutWindow';
|
||||
import CanvasChatFloatingWindow from './CanvasChatFloatingWindow.vue';
|
||||
|
||||
const route = useRoute();
|
||||
const chatHubPanelStore = useChatHubPanelStore();
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
const workflowDocumentStore = computed(() =>
|
||||
workflowsStore.workflowId
|
||||
? useWorkflowDocumentStore(createWorkflowDocumentId(workflowsStore.workflowId))
|
||||
: undefined,
|
||||
);
|
||||
|
||||
const canvasChatFloatingWindowRef = ref<InstanceType<typeof CanvasChatFloatingWindow>>();
|
||||
|
||||
|
|
@ -36,7 +45,9 @@ watch(
|
|||
{ immediate: true },
|
||||
);
|
||||
|
||||
const popOutWindowTitle = computed(() => `Chat - ${workflowsStore.workflowName || 'Workflow'}`);
|
||||
const popOutWindowTitle = computed(
|
||||
() => `Chat - ${workflowDocumentStore.value?.name || 'Workflow'}`,
|
||||
);
|
||||
const shouldPopOut = computed(() => isPoppedOut.value && chatHubPanelStore.isOpen);
|
||||
|
||||
usePopOutWindow({
|
||||
|
|
|
|||
|
|
@ -16,6 +16,10 @@ import { useLogsPanelLayout } from '@/features/execution/logs/composables/useLog
|
|||
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 { N8nResizeWrapper } from '@n8n/design-system';
|
||||
const props = withDefaults(defineProps<{ isReadOnly?: boolean }>(), { isReadOnly: false });
|
||||
|
|
@ -28,7 +32,12 @@ const popOutContent = useTemplateRef('popOutContent');
|
|||
const logsStore = useLogsStore();
|
||||
const ndvStore = useNDVStore();
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
const workflowName = computed(() => workflowsStore.workflow.name);
|
||||
const workflowDocumentStore = computed(() =>
|
||||
workflowsStore.workflowId
|
||||
? useWorkflowDocumentStore(createWorkflowDocumentId(workflowsStore.workflowId))
|
||||
: undefined,
|
||||
);
|
||||
const workflowName = computed(() => workflowDocumentStore.value?.name ?? '');
|
||||
|
||||
const {
|
||||
height,
|
||||
|
|
|
|||
|
|
@ -26,7 +26,6 @@ describe('TriggerPanel.vue', () => {
|
|||
beforeEach(async () => {
|
||||
setActivePinia(createTestingPinia({ stubActions: false }));
|
||||
workflowsStore = mockedStore(useWorkflowsStore);
|
||||
workflowsStore.workflowName = 'Test Workflow';
|
||||
workflowsStore.workflowId = '1';
|
||||
const node = createTestNode({ id: '0', name: 'Webhook', type: 'n8n-nodes-base.webhook' });
|
||||
workflowsStore.workflow.nodes = [node];
|
||||
|
|
|
|||
|
|
@ -37,8 +37,8 @@ vi.mock('@/features/collaboration/projects/projects.store', () => ({
|
|||
currentProjectId: 'p1',
|
||||
}),
|
||||
}));
|
||||
vi.mock('@/app/stores/workflows.store', () => ({
|
||||
useWorkflowsStore: () => ({ workflow: { name: 'WF' } }),
|
||||
vi.mock('@/app/stores/workflowDocument.store', () => ({
|
||||
injectWorkflowDocumentStore: () => ref({ name: 'WF' }),
|
||||
}));
|
||||
|
||||
// Command groups
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ import { useInstanceAiCommands } from './useInstanceAiCommands';
|
|||
import type { CommandGroup } from '../types';
|
||||
import { useI18n } from '@n8n/i18n';
|
||||
import { PROJECT_DATA_TABLES, DATA_TABLE_VIEW } from '@/features/core/dataTable/constants';
|
||||
import { useWorkflowsStore } from '@/app/stores/workflows.store';
|
||||
import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store';
|
||||
import { useTelemetry } from '@/app/composables/useTelemetry';
|
||||
import {
|
||||
CHAT_CONVERSATION_VIEW,
|
||||
|
|
@ -31,7 +31,7 @@ import {
|
|||
export function useCommandBar() {
|
||||
const nodeTypesStore = useNodeTypesStore();
|
||||
const projectsStore = useProjectsStore();
|
||||
const workflowStore = useWorkflowsStore();
|
||||
const workflowDocumentStore = injectWorkflowDocumentStore();
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const i18n = useI18n();
|
||||
|
|
@ -220,21 +220,22 @@ export function useCommandBar() {
|
|||
});
|
||||
|
||||
const context = computed(() => {
|
||||
const workflowName = workflowDocumentStore?.value?.name ?? '';
|
||||
switch (router.currentRoute.value.name) {
|
||||
case VIEWS.WORKFLOW:
|
||||
case VIEWS.NEW_WORKFLOW:
|
||||
return workflowStore.workflow.name
|
||||
? i18n.baseText('commandBar.sections.workflow') + ' ⋅ ' + workflowStore.workflow.name
|
||||
return workflowName
|
||||
? i18n.baseText('commandBar.sections.workflow') + ' ⋅ ' + workflowName
|
||||
: '';
|
||||
case VIEWS.EXECUTION_PREVIEW:
|
||||
case VIEWS.EXECUTION_DEBUG:
|
||||
return workflowStore.workflow.name
|
||||
? i18n.baseText('commandBar.sections.execution') + ' ⋅ ' + workflowStore.workflow.name
|
||||
return workflowName
|
||||
? i18n.baseText('commandBar.sections.execution') + ' ⋅ ' + workflowName
|
||||
: '';
|
||||
case VIEWS.EVALUATION:
|
||||
case VIEWS.EVALUATION_EDIT:
|
||||
case VIEWS.EVALUATION_RUNS_DETAIL:
|
||||
return workflowStore.workflow.name ? ' ⋅ ' + workflowStore.workflow.name : '';
|
||||
return workflowName ? ' ⋅ ' + workflowName : '';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user