mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-16 17:46:45 +00:00
fix(editor): Update misleading toaster success message when creating a personal workflow / credential (#18541)
This commit is contained in:
committed by
GitHub
parent
a8e4387f4d
commit
b6681bb92c
@@ -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.",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
Reference in New Issue
Block a user