diff --git a/packages/frontend/@n8n/i18n/src/locales/en.json b/packages/frontend/@n8n/i18n/src/locales/en.json index 90702e3077f..2804bb0be35 100644 --- a/packages/frontend/@n8n/i18n/src/locales/en.json +++ b/packages/frontend/@n8n/i18n/src/locales/en.json @@ -688,8 +688,7 @@ "credentials.noResults": "No credentials found", "credentials.noResults.withSearch.switchToShared.preamble": "some credentials may be", "credentials.noResults.withSearch.switchToShared.link": "hidden", - "credentials.create.personal.toast.title": "Credential successfully created", - "credentials.create.personal.toast.text": "This credential has been created inside your personal space.", + "credentials.create.personal.toast.title": "Credential successfully created inside your personal space", "credentials.create.project.toast.title": "Credential successfully created in {projectName}", "credentials.create.project.toast.text": "All members from {projectName} will have access to this credential.", "dataDisplay.needHelp": "Need help?", @@ -2736,8 +2735,7 @@ "workflows.concurrentChanges.confirmMessage.message": "Someone saved this workflow while you were editing it. You can view their version (in new tab).

Overwrite their changes with yours?", "workflows.concurrentChanges.confirmMessage.cancelButtonText": "Cancel", "workflows.concurrentChanges.confirmMessage.confirmButtonText": "Overwrite and Save", - "workflows.create.personal.toast.title": "Workflow successfully created", - "workflows.create.personal.toast.text": "This workflow has been created inside your personal space.", + "workflows.create.personal.toast.title": "Workflow successfully created inside your personal space", "workflows.create.project.toast.title": "Workflow successfully created in {projectName}", "workflows.create.folder.toast.title": "Workflow successfully created in \"{projectName}\", within \"{folderName}\"", "workflows.create.project.toast.text": "All members from {projectName} will have access to this workflow.", diff --git a/packages/frontend/editor-ui/src/components/CredentialEdit/CredentialEdit.vue b/packages/frontend/editor-ui/src/components/CredentialEdit/CredentialEdit.vue index 40386311d04..d785515757a 100644 --- a/packages/frontend/editor-ui/src/components/CredentialEdit/CredentialEdit.vue +++ b/packages/frontend/editor-ui/src/components/CredentialEdit/CredentialEdit.vue @@ -28,7 +28,6 @@ import { useMessage } from '@/composables/useMessage'; import { useNodeHelpers } from '@/composables/useNodeHelpers'; import { useToast } from '@/composables/useToast'; import { CREDENTIAL_EDIT_MODAL_KEY, EnterpriseEditionFeature, MODAL_CONFIRM } from '@/constants'; -import { getResourcePermissions } from '@n8n/permissions'; import { useCredentialsStore } from '@/stores/credentials.store'; import { useNDVStore } from '@/stores/ndv.store'; import { useNodeTypesStore } from '@/stores/nodeTypes.store'; @@ -37,11 +36,11 @@ import { useUIStore } from '@/stores/ui.store'; import { useWorkflowsStore } from '@/stores/workflows.store'; import type { Project, ProjectSharingData } from '@/types/projects.types'; import { N8nInlineTextEdit, N8nText, type IMenuItem } from '@n8n/design-system'; +import { getResourcePermissions } from '@n8n/permissions'; import { assert } from '@n8n/utils/assert'; import { createEventBus } from '@n8n/utils/event-bus'; import { useExternalHooks } from '@/composables/useExternalHooks'; -import { useI18n } from '@n8n/i18n'; import { useTelemetry } from '@/composables/useTelemetry'; import { useProjectsStore } from '@/stores/projects.store'; import { isExpression, isTestableExpression } from '@/utils/expressions'; @@ -51,6 +50,7 @@ import { updateNodeAuthType, } from '@/utils/nodeTypesUtils'; import { isCredentialModalState, isValidCredentialResponse } from '@/utils/typeGuards'; +import { useI18n } from '@n8n/i18n'; import { useElementSize } from '@vueuse/core'; type Props = { @@ -771,17 +771,10 @@ async function saveCredential(): Promise { return credential; } -const createToastMessagingForNewCredentials = ( - credentialDetails: ICredentialsDecrypted, - project?: Project | null, -) => { +const createToastMessagingForNewCredentials = (project?: Project | null) => { let toastTitle = i18n.baseText('credentials.create.personal.toast.title'); let toastText = ''; - if (!credentialDetails.sharedWithProjects) { - toastText = i18n.baseText('credentials.create.personal.toast.text'); - } - if ( projectsStore.currentProject && projectsStore.currentProject.id !== projectsStore.personalProject?.id @@ -811,7 +804,7 @@ async function createCredential( credential = await credentialsStore.createNewCredential(credentialDetails, project?.id); hasUnsavedChanges.value = false; - const { title, message } = createToastMessagingForNewCredentials(credentialDetails, project); + const { title, message } = createToastMessagingForNewCredentials(project); toast.showMessage({ title, diff --git a/packages/frontend/editor-ui/src/components/MainHeader/WorkflowDetails.test.ts b/packages/frontend/editor-ui/src/components/MainHeader/WorkflowDetails.test.ts index 907a0061f98..4ce3836385f 100644 --- a/packages/frontend/editor-ui/src/components/MainHeader/WorkflowDetails.test.ts +++ b/packages/frontend/editor-ui/src/components/MainHeader/WorkflowDetails.test.ts @@ -18,14 +18,25 @@ import { useRoute, useRouter } from 'vue-router'; import { useMessage } from '@/composables/useMessage'; import { useToast } from '@/composables/useToast'; import { useWorkflowsStore } from '@/stores/workflows.store'; +import { useProjectsStore } from '@/stores/projects.store'; +import type { Project } from '@/types/projects.types'; vi.mock('vue-router', async (importOriginal) => ({ // eslint-disable-next-line @typescript-eslint/consistent-type-imports ...(await importOriginal()), - useRoute: vi.fn().mockReturnValue({}), + useRoute: vi.fn().mockReturnValue({ + params: { name: 'test' }, + query: { parentFolderId: '1' }, + }), useRouter: vi.fn().mockReturnValue({ replace: vi.fn(), push: vi.fn().mockResolvedValue(undefined), + currentRoute: { + value: { + params: { name: 'test' }, + query: { parentFolderId: '1' }, + }, + }, }), })); @@ -55,6 +66,12 @@ vi.mock('@/composables/useMessage', () => { }; }); +vi.mock('@/composables/useWorkflowSaving', () => ({ + useWorkflowSaving: () => ({ + saveCurrentWorkflow: vi.fn().mockResolvedValue(true), + }), +})); + const initialState = { [STORES.SETTINGS]: { settings: { @@ -98,6 +115,7 @@ const renderComponent = createComponentRenderer(WorkflowDetails, { let uiStore: ReturnType; let workflowsStore: MockedStore; +let projectsStore: MockedStore; let message: ReturnType; let toast: ReturnType; let router: ReturnType; @@ -114,6 +132,12 @@ describe('WorkflowDetails', () => { beforeEach(() => { uiStore = useUIStore(); workflowsStore = mockedStore(useWorkflowsStore); + projectsStore = mockedStore(useProjectsStore); + + // Set up default mocks + workflowsStore.saveCurrentWorkflow = vi.fn().mockResolvedValue(true); + projectsStore.currentProject = null; + projectsStore.personalProject = { id: 'personal', name: 'Personal' } as Project; message = useMessage(); toast = useToast(); @@ -423,6 +447,106 @@ describe('WorkflowDetails', () => { }); }); + describe('Toast notifications', () => { + it('should show personal toast when creating workflow without project context', async () => { + projectsStore.currentProject = null; + + const { getByTestId } = renderComponent({ + props: { + ...workflow, + id: 'new', + readOnly: false, + }, + }); + + await userEvent.click(getByTestId('workflow-save-button')); + + expect(toast.showMessage).toHaveBeenCalledWith( + expect.objectContaining({ + type: 'success', + title: 'Workflow successfully created inside your personal space', + }), + ); + }); + + it('should show project toast when creating workflow in non-personal project', async () => { + projectsStore.currentProject = { + id: 'project-1', + name: 'Test Project', + type: 'team', + icon: null, + createdAt: '2023-01-01', + updatedAt: '2023-01-01', + relations: [], + scopes: [], + }; + + const { getByTestId } = renderComponent({ + props: { + ...workflow, + id: 'new', + readOnly: false, + }, + }); + + await userEvent.click(getByTestId('workflow-save-button')); + + expect(toast.showMessage).toHaveBeenCalledWith( + expect.objectContaining({ + type: 'success', + title: 'Workflow successfully created in Test Project', + message: 'All members from Test Project will have access to this workflow.', + }), + ); + }); + + it('should show folder toast when creating workflow in folder context', async () => { + projectsStore.currentProject = { + id: 'project-1', + name: 'Test Project', + type: 'team', + icon: null, + createdAt: '2023-01-01', + updatedAt: '2023-01-01', + relations: [], + scopes: [], + }; + + const { getByTestId } = renderComponent({ + props: { + ...workflow, + id: 'new', + readOnly: false, + currentFolder: { id: 'folder-1', name: 'Test Folder' }, + }, + }); + + await userEvent.click(getByTestId('workflow-save-button')); + + expect(toast.showMessage).toHaveBeenCalledWith( + expect.objectContaining({ + type: 'success', + title: 'Workflow successfully created in "Test Project", within "Test Folder"', + message: 'All members from Test Project will have access to this workflow.', + }), + ); + }); + + it('should not show toast when saving existing workflow', async () => { + const { getByTestId } = renderComponent({ + props: { + ...workflow, + id: '123', + readOnly: false, + }, + }); + + await userEvent.click(getByTestId('workflow-save-button')); + + expect(toast.showMessage).not.toHaveBeenCalled(); + }); + }); + describe('Archived badge', () => { it('should show badge on archived workflow', async () => { const { getByTestId } = renderComponent({ diff --git a/packages/frontend/editor-ui/src/components/MainHeader/WorkflowDetails.vue b/packages/frontend/editor-ui/src/components/MainHeader/WorkflowDetails.vue index 557ee86d169..85baf2623e8 100644 --- a/packages/frontend/editor-ui/src/components/MainHeader/WorkflowDetails.vue +++ b/packages/frontend/editor-ui/src/components/MainHeader/WorkflowDetails.vue @@ -1,4 +1,13 @@