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