fix(editor): Update misleading toaster success message when creating a personal workflow / credential (#18541)

This commit is contained in:
Raúl Gómez Morales
2025-08-22 16:51:00 +02:00
committed by GitHub
parent a8e4387f4d
commit b6681bb92c
4 changed files with 208 additions and 68 deletions

View File

@@ -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 <a href=\"{url}\" target=\"_blank\">view their version</a> (in new tab).<br/><br/>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.",

View File

@@ -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<ICredentialsResponse | null> {
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,

View File

@@ -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<typeof import('vue-router')>()),
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<typeof useUIStore>;
let workflowsStore: MockedStore<typeof useWorkflowsStore>;
let projectsStore: MockedStore<typeof useProjectsStore>;
let message: ReturnType<typeof useMessage>;
let toast: ReturnType<typeof useToast>;
let router: ReturnType<typeof useRouter>;
@@ -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({

View File

@@ -1,4 +1,13 @@
<script lang="ts" setup>
import BreakpointsObserver from '@/components/BreakpointsObserver.vue';
import CollaborationPane from '@/components/MainHeader/CollaborationPane.vue';
import WorkflowHistoryButton from '@/components/MainHeader/WorkflowHistoryButton.vue';
import PushConnectionTracker from '@/components/PushConnectionTracker.vue';
import SaveButton from '@/components/SaveButton.vue';
import WorkflowActivator from '@/components/WorkflowActivator.vue';
import WorkflowProductionChecklist from '@/components/WorkflowProductionChecklist.vue';
import WorkflowTagsContainer from '@/components/WorkflowTagsContainer.vue';
import WorkflowTagsDropdown from '@/components/WorkflowTagsDropdown.vue';
import {
DUPLICATE_MODAL_KEY,
EnterpriseEditionFeature,
@@ -12,15 +21,6 @@ import {
WORKFLOW_SETTINGS_MODAL_KEY,
WORKFLOW_SHARE_MODAL_KEY,
} from '@/constants';
import WorkflowTagsContainer from '@/components/WorkflowTagsContainer.vue';
import PushConnectionTracker from '@/components/PushConnectionTracker.vue';
import WorkflowActivator from '@/components/WorkflowActivator.vue';
import SaveButton from '@/components/SaveButton.vue';
import WorkflowTagsDropdown from '@/components/WorkflowTagsDropdown.vue';
import BreakpointsObserver from '@/components/BreakpointsObserver.vue';
import WorkflowHistoryButton from '@/components/MainHeader/WorkflowHistoryButton.vue';
import CollaborationPane from '@/components/MainHeader/CollaborationPane.vue';
import WorkflowProductionChecklist from '@/components/WorkflowProductionChecklist.vue';
import { ResourceType } from '@/utils/projects.utils';
import { useProjectsStore } from '@/stores/projects.store';
@@ -32,35 +32,35 @@ import { useUsersStore } from '@/stores/users.store';
import { useWorkflowsStore } from '@/stores/workflows.store';
import { useRootStore } from '@n8n/stores/useRootStore';
import { saveAs } from 'file-saver';
import { useDocumentTitle } from '@/composables/useDocumentTitle';
import { useMessage } from '@/composables/useMessage';
import { usePageRedirectionHelper } from '@/composables/usePageRedirectionHelper';
import { useTelemetry } from '@/composables/useTelemetry';
import { useToast } from '@/composables/useToast';
import { getResourcePermissions } from '@n8n/permissions';
import { createEventBus } from '@n8n/utils/event-bus';
import { nodeViewEventBus } from '@/event-bus';
import { hasPermission } from '@/utils/rbac/permissions';
import { useRoute, useRouter } from 'vue-router';
import { useWorkflowHelpers } from '@/composables/useWorkflowHelpers';
import { computed, ref, useCssModule, useTemplateRef, watch } from 'vue';
import { useWorkflowSaving } from '@/composables/useWorkflowSaving';
import { nodeViewEventBus } from '@/event-bus';
import type {
ActionDropdownItem,
FolderShortInfo,
IWorkflowDb,
IWorkflowToShare,
} from '@/Interface';
import type { WorkflowDataUpdate } from '@n8n/rest-api-client/api/workflows';
import { usePageRedirectionHelper } from '@/composables/usePageRedirectionHelper';
import { useTelemetry } from '@/composables/useTelemetry';
import type { PathItem } from '@n8n/design-system/components/N8nBreadcrumbs/Breadcrumbs.vue';
import { N8nInlineTextEdit } from '@n8n/design-system';
import { useFoldersStore } from '@/stores/folders.store';
import { useNpsSurveyStore } from '@/stores/npsSurvey.store';
import { type BaseTextKey, useI18n } from '@n8n/i18n';
import { ProjectTypes } from '@/types/projects.types';
import { useWorkflowSaving } from '@/composables/useWorkflowSaving';
import { sanitizeFilename } from '@/utils/fileUtils';
import { hasPermission } from '@/utils/rbac/permissions';
import { N8nInlineTextEdit } from '@n8n/design-system';
import type { PathItem } from '@n8n/design-system/components/N8nBreadcrumbs/Breadcrumbs.vue';
import { type BaseTextKey, useI18n } from '@n8n/i18n';
import { getResourcePermissions } from '@n8n/permissions';
import type { WorkflowDataUpdate } from '@n8n/rest-api-client/api/workflows';
import { createEventBus } from '@n8n/utils/event-bus';
import { saveAs } from 'file-saver';
import { computed, ref, useCssModule, useTemplateRef, watch } from 'vue';
import { I18nT } from 'vue-i18n';
import { useRoute, useRouter } from 'vue-router';
const WORKFLOW_NAME_MAX_WIDTH_SMALL_SCREENS = 150;
const WORKFLOW_NAME_MAX_WIDTH_WIDE_SCREENS = 200;
@@ -641,36 +641,61 @@ function goToWorkflowHistoryUpgrade() {
void pageRedirectionHelper.goToUpgrade('workflow-history', 'upgrade-workflow-history');
}
function showCreateWorkflowSuccessToast(id?: string) {
if (!id || ['new', PLACEHOLDER_EMPTY_WORKFLOW_ID].includes(id)) {
let toastTitle = locale.baseText('workflows.create.personal.toast.title');
let toastText = locale.baseText('workflows.create.personal.toast.text');
if (projectsStore.currentProject) {
if (props.currentFolder) {
toastTitle = locale.baseText('workflows.create.folder.toast.title', {
interpolate: {
projectName: currentProjectName.value ?? '',
folderName: props.currentFolder.name ?? '',
},
});
} else if (projectsStore.currentProject.id !== projectsStore.personalProject?.id) {
toastTitle = locale.baseText('workflows.create.project.toast.title', {
interpolate: { projectName: currentProjectName.value ?? '' },
});
}
toastText = locale.baseText('workflows.create.project.toast.text', {
interpolate: { projectName: currentProjectName.value ?? '' },
});
}
toast.showMessage({
title: toastTitle,
message: toastText,
type: 'success',
});
function getPersonalProjectToastContent() {
const title = locale.baseText('workflows.create.personal.toast.title');
if (!props.currentFolder) {
return { title };
}
const toastMessage = locale.baseText('workflows.create.folder.toast.title', {
interpolate: {
projectName: 'Personal',
folderName: props.currentFolder.name,
},
});
return { title, toastMessage };
}
function getToastContent() {
const currentProject = projectsStore.currentProject;
const isPersonalProject =
!projectsStore.currentProject || currentProject?.id === projectsStore.personalProject?.id;
const projectName = currentProjectName.value ?? '';
if (isPersonalProject) {
return getPersonalProjectToastContent();
}
const titleKey = props.currentFolder
? 'workflows.create.folder.toast.title'
: 'workflows.create.project.toast.title';
const interpolateData: Record<string, string> = props.currentFolder
? { projectName, folderName: props.currentFolder.name ?? '' }
: { projectName };
const title = locale.baseText(titleKey, { interpolate: interpolateData });
const toastMessage = locale.baseText('workflows.create.project.toast.text', {
interpolate: { projectName },
});
return { title, toastMessage };
}
function showCreateWorkflowSuccessToast(id?: string) {
const shouldShowToast = !id || ['new', PLACEHOLDER_EMPTY_WORKFLOW_ID].includes(id);
if (!shouldShowToast) return;
const { title, toastMessage } = getToastContent();
toast.showMessage({
title,
message: toastMessage,
type: 'success',
});
}
const onBreadcrumbsItemSelected = (item: PathItem) => {