diff --git a/packages/frontend/@n8n/i18n/src/locales/en.json b/packages/frontend/@n8n/i18n/src/locales/en.json index de54deee6b..39eeae0ca0 100644 --- a/packages/frontend/@n8n/i18n/src/locales/en.json +++ b/packages/frontend/@n8n/i18n/src/locales/en.json @@ -980,10 +980,12 @@ "folders.move.modal.message.usedCredentials.warning": "Workflow may not execute correctly if you choose not to share the credentials.", "folders.move.success.title": "Successfully moved folder", "folders.move.success.message": "{folderName} has been moved to {newFolderName}, along with all its workflows and subfolders.

View {newFolderName}", + "folders.move.success.messageNoAccess": "{folderName} has been moved to {newFolderName}, along with all its workflows and subfolders.", "folders.move.error.title": "Problem moving folder", "folders.move.workflow.error.title": "Problem moving workflow", "folders.move.workflow.success.title": "Successfully moved workflow", "folders.move.workflow.success.message": "{workflowName} has been moved to {newFolderName}.

View {newFolderName}", + "folders.move.workflow.success.messageNoAccess": "{workflowName} has been moved to {newFolderName}.", "folders.move.project.root.name": "No folder (project root)", "folders.open.error.title": "Problem opening folder", "folders.create.error.title": "Problem creating folder", diff --git a/packages/frontend/editor-ui/src/components/Folders/MoveToFolderModal.test.ts b/packages/frontend/editor-ui/src/components/Folders/MoveToFolderModal.test.ts index b7fa94924a..556fae82be 100644 --- a/packages/frontend/editor-ui/src/components/Folders/MoveToFolderModal.test.ts +++ b/packages/frontend/editor-ui/src/components/Folders/MoveToFolderModal.test.ts @@ -487,14 +487,21 @@ describe('MoveToFolderModal', () => { await userEvent.click(submitButton); expect(mockEventBus.emit).toHaveBeenCalledWith('folder-transferred', { - newParent: { - id: folder.id, - name: folder.name, - type: folder.resource, + source: { + projectId: personalProject.id, + folder: { + id: TEST_FOLDER_RESOURCE.id, + name: TEST_FOLDER_RESOURCE.name, + }, + }, + destination: { + projectId: teamProjects[0].id, + parentFolder: { + id: folder.id, + name: folder.name, + }, + canAccess: true, }, - folder: { id: TEST_FOLDER_RESOURCE.id, name: TEST_FOLDER_RESOURCE.name }, - projectId: personalProject.id, - destinationProjectId: teamProjects[0].id, shareCredentials: undefined, }); }); @@ -536,14 +543,21 @@ describe('MoveToFolderModal', () => { await userEvent.click(submitButton); expect(mockEventBus.emit).toHaveBeenCalledWith('folder-transferred', { - newParent: { - id: folder.id, - name: folder.name, - type: folder.resource, + source: { + projectId: personalProject.id, + folder: { + id: TEST_FOLDER_RESOURCE.id, + name: TEST_FOLDER_RESOURCE.name, + }, + }, + destination: { + projectId: teamProjects[0].id, + parentFolder: { + id: folder.id, + name: folder.name, + }, + canAccess: true, }, - folder: { id: TEST_FOLDER_RESOURCE.id, name: TEST_FOLDER_RESOURCE.name }, - projectId: personalProject.id, - destinationProjectId: teamProjects[0].id, shareCredentials: [shareableUsedCredential.id], }); }); @@ -584,14 +598,21 @@ describe('MoveToFolderModal', () => { await userEvent.click(submitButton); expect(mockEventBus.emit).toHaveBeenCalledWith('folder-transferred', { - newParent: { - id: folder.id, - name: folder.name, - type: folder.resource, + source: { + projectId: personalProject.id, + folder: { + id: TEST_FOLDER_RESOURCE.id, + name: TEST_FOLDER_RESOURCE.name, + }, + }, + destination: { + projectId: teamProjects[0].id, + parentFolder: { + id: folder.id, + name: folder.name, + }, + canAccess: true, }, - folder: { id: TEST_FOLDER_RESOURCE.id, name: TEST_FOLDER_RESOURCE.name }, - projectId: personalProject.id, - destinationProjectId: teamProjects[0].id, shareCredentials: undefined, }); }); @@ -628,14 +649,21 @@ describe('MoveToFolderModal', () => { await userEvent.click(submitButton); expect(mockEventBus.emit).toHaveBeenCalledWith('folder-transferred', { - newParent: { - id: anotherUser.id, - name: anotherUser.name, - type: 'project', + source: { + projectId: personalProject.id, + folder: { + id: TEST_FOLDER_RESOURCE.id, + name: TEST_FOLDER_RESOURCE.name, + }, + }, + destination: { + projectId: anotherUser.id, + parentFolder: { + id: undefined, + name: anotherUser.name, + }, + canAccess: false, }, - folder: { id: TEST_FOLDER_RESOURCE.id, name: TEST_FOLDER_RESOURCE.name }, - projectId: personalProject.id, - destinationProjectId: anotherUser.id, shareCredentials: undefined, }); }); @@ -731,17 +759,21 @@ describe('MoveToFolderModal', () => { await userEvent.click(submitButton); expect(mockEventBus.emit).toHaveBeenCalledWith('workflow-transferred', { - newParent: { - id: folder.id, - name: folder.name, - type: folder.resource, + source: { + projectId: personalProject.id, + workflow: { + id: TEST_WORKFLOW_RESOURCE.id, + name: TEST_WORKFLOW_RESOURCE.name, + }, }, - workflow: { - id: TEST_WORKFLOW_RESOURCE.id, - name: TEST_WORKFLOW_RESOURCE.name, - oldParentId: TEST_WORKFLOW_RESOURCE.parentFolderId, + destination: { + projectId: teamProjects[0].id, + parentFolder: { + id: folder.id, + name: folder.name, + }, + canAccess: true, }, - projectId: teamProjects[0].id, shareCredentials: undefined, }); }); @@ -778,17 +810,21 @@ describe('MoveToFolderModal', () => { await userEvent.click(submitButton); expect(mockEventBus.emit).toHaveBeenCalledWith('workflow-transferred', { - newParent: { - id: anotherUser.id, - name: anotherUser.name, - type: 'project', + source: { + projectId: personalProject.id, + workflow: { + id: TEST_WORKFLOW_RESOURCE.id, + name: TEST_WORKFLOW_RESOURCE.name, + }, }, - workflow: { - id: TEST_WORKFLOW_RESOURCE.id, - name: TEST_WORKFLOW_RESOURCE.name, - oldParentId: TEST_WORKFLOW_RESOURCE.parentFolderId, + destination: { + projectId: anotherUser.id, + parentFolder: { + id: undefined, + name: anotherUser.name, + }, + canAccess: false, }, - projectId: anotherUser.id, shareCredentials: undefined, }); }); diff --git a/packages/frontend/editor-ui/src/components/Folders/MoveToFolderModal.vue b/packages/frontend/editor-ui/src/components/Folders/MoveToFolderModal.vue index bfa827a178..63919c6200 100644 --- a/packages/frontend/editor-ui/src/components/Folders/MoveToFolderModal.vue +++ b/packages/frontend/editor-ui/src/components/Folders/MoveToFolderModal.vue @@ -60,14 +60,14 @@ const credentialsStore = useCredentialsStore(); const workflowsStore = useWorkflowsStore(); const selectedFolder = ref(null); -const selectedProject = ref(projectsStore.currentProject ?? null); +const selectedProject = ref(projectsStore.currentProject); const isPersonalProject = computed(() => { return selectedProject.value?.type === ProjectTypes.Personal; }); const isOwnPersonalProject = computed(() => { return ( selectedProject.value?.type === ProjectTypes.Personal && - selectedProject.value.id === projectsStore.personalProject?.id + selectedProject.value?.id === projectsStore.personalProject?.id ); }); const isTransferringOwnership = computed(() => { @@ -196,12 +196,29 @@ const onSubmit = () => { }; if (props.data.resourceType === 'folder') { - if (selectedProject.value?.id !== projectsStore.currentProject?.id) { + if (selectedProject.value.id !== projectsStore.currentProject?.id) { props.data.workflowListEventBus.emit('folder-transferred', { - newParent, - folder: { id: props.data.resource.id, name: props.data.resource.name }, - projectId: projectsStore.currentProject?.id, - destinationProjectId: selectedProject.value.id, + source: { + projectId: projectsStore.currentProject?.id, + folder: { + id: props.data.resource.id, + name: props.data.resource.name, + }, + }, + destination: { + projectId: selectedProject.value.id, + parentFolder: { + id: + selectedFolder.value && selectedFolder.value.id !== selectedProject.value.id + ? selectedFolder.value.id + : undefined, + name: + selectedFolder.value && selectedFolder.value.id !== selectedProject.value.id + ? selectedFolder.value.name + : targetProjectName.value, + }, + canAccess: isFolderSelectable.value, + }, shareCredentials: shareUsedCredentials.value ? shareableCredentials.value.map((c) => c.id) : undefined, @@ -215,12 +232,26 @@ const onSubmit = () => { } else { if (isTransferringOwnership.value) { props.data.workflowListEventBus.emit('workflow-transferred', { - newParent, - projectId: selectedProject.value.id, - workflow: { - id: props.data.resource.id, - name: props.data.resource.name, - oldParentId: props.data.resource.parentFolderId, + source: { + projectId: projectsStore.currentProject?.id, + workflow: { + id: props.data.resource.id, + name: props.data.resource.name, + }, + }, + destination: { + projectId: selectedProject.value.id, + parentFolder: { + id: + selectedFolder.value && selectedFolder.value.id !== selectedProject.value.id + ? selectedFolder.value.id + : undefined, + name: + selectedFolder.value && selectedFolder.value.id !== selectedProject.value.id + ? selectedFolder.value.name + : targetProjectName.value, + }, + canAccess: isFolderSelectable.value, }, shareCredentials: shareUsedCredentials.value ? shareableCredentials.value.map((c) => c.id) @@ -237,6 +268,7 @@ const onSubmit = () => { }); } } + uiStore.closeModal(MOVE_FOLDER_MODAL_KEY); }; diff --git a/packages/frontend/editor-ui/src/views/WorkflowsView.vue b/packages/frontend/editor-ui/src/views/WorkflowsView.vue index d2f9c65294..a414e8422d 100644 --- a/packages/frontend/editor-ui/src/views/WorkflowsView.vue +++ b/packages/frontend/editor-ui/src/views/WorkflowsView.vue @@ -1181,6 +1181,7 @@ const moveFolder = async (payload: { }; }) => { if (!route.params.projectId) return; + try { await foldersStore.moveFolder( route.params.projectId as string, @@ -1227,55 +1228,85 @@ const moveFolder = async (payload: { }; const onFolderTransferred = async (payload: { - folder: { id: string; name: string }; - projectId: string; - destinationProjectId: string; - newParent: { id: string; name: string; type: 'folder' | 'project' }; + source: { + projectId: string; + folder: { id: string; name: string }; + }; + destination: { + projectId: string; + parentFolder: { id: string | undefined; name: string }; + canAccess: boolean; + }; shareCredentials?: string[]; }) => { - const destinationParentFolderId = - payload.newParent.type === 'folder' ? payload.newParent.id : undefined; + try { + await foldersStore.moveFolderToProject( + payload.source.projectId, + payload.source.folder.id, + payload.destination.projectId, + payload.destination.parentFolder.id, + payload.shareCredentials, + ); - await foldersStore.moveFolderToProject( - payload.projectId, - payload.folder.id, - payload.destinationProjectId, - destinationParentFolderId, - payload.shareCredentials, - ); - - const isCurrentFolder = currentFolderId.value === payload.folder.id; - const newFolderURL = router.resolve({ - name: VIEWS.PROJECTS_FOLDERS, - params: { - projectId: payload.destinationProjectId, - folderId: destinationParentFolderId, - }, - }).href; - - if (isCurrentFolder) { - // If we just moved the current folder, automatically navigate to the new folder - void router.push(newFolderURL); - } else { - // Else show success message and update the list - toast.showToast({ - title: i18n.baseText('folders.move.success.title'), - message: i18n.baseText('folders.move.success.message', { - interpolate: { - folderName: payload.folder.name, - newFolderName: payload.newParent.name, - }, - }), - onClick: (event: MouseEvent | undefined) => { - if (event?.target instanceof HTMLAnchorElement) { - event.preventDefault(); - void router.push(newFolderURL); - } + const isCurrentFolder = currentFolderId.value === payload.source.folder.id; + const newFolderURL = router.resolve({ + name: VIEWS.PROJECTS_FOLDERS, + params: { + projectId: payload.destination.canAccess + ? payload.destination.projectId + : payload.source.projectId, + folderId: payload.destination.canAccess ? payload.source.folder.id : undefined, }, - type: 'success', - }); + }).href; - await fetchWorkflows(); + if (isCurrentFolder) { + if (payload.destination.canAccess) { + // If we just moved the current folder and can access the destination navigate there + void router.push(newFolderURL); + } else { + // Otherwise navigate to the workflows page of the source project + void router.push({ + name: VIEWS.PROJECTS_WORKFLOWS, + params: { + projectId: payload.source.projectId, + }, + }); + } + } else { + await refreshWorkflows(); + + if (payload.destination.canAccess) { + toast.showToast({ + title: i18n.baseText('folders.move.success.title'), + message: i18n.baseText('folders.move.success.message', { + interpolate: { + folderName: payload.source.folder.name, + newFolderName: payload.destination.parentFolder.name, + }, + }), + onClick: (event: MouseEvent | undefined) => { + if (event?.target instanceof HTMLAnchorElement) { + event.preventDefault(); + void router.push(newFolderURL); + } + }, + type: 'success', + }); + } else { + toast.showToast({ + title: i18n.baseText('folders.move.success.title'), + message: i18n.baseText('folders.move.success.messageNoAccess', { + interpolate: { + folderName: payload.source.folder.name, + newFolderName: payload.destination.parentFolder.name, + }, + }), + type: 'success', + }); + } + } + } catch (error) { + toast.showError(error, i18n.baseText('folders.move.error.title')); } }; @@ -1305,46 +1336,63 @@ const moveWorkflowToFolder = async (payload: { }; const onWorkflowTransferred = async (payload: { - projectId: string; - workflow: { id: string; name: string; oldParentId: string }; - newParent: { id: string; name: string; type: 'folder' | 'project' }; + source: { + projectId: string; + workflow: { id: string; name: string }; + }; + destination: { + projectId: string; + parentFolder: { id: string | undefined; name: string }; + canAccess: boolean; + }; shareCredentials?: string[]; }) => { - const parentFolderId = payload.newParent.type === 'folder' ? payload.newParent.id : undefined; - - await projectsStore.moveResourceToProject( - 'workflow', - payload.workflow.id, - payload.projectId, - parentFolderId, - payload.shareCredentials, - ); - - await fetchWorkflows(); - try { - toast.showToast({ - title: i18n.baseText('folders.move.workflow.success.title'), - message: i18n.baseText('folders.move.workflow.success.message', { - interpolate: { - workflowName: payload.workflow.name, - newFolderName: payload.newParent.name, + await projectsStore.moveResourceToProject( + 'workflow', + payload.source.workflow.id, + payload.destination.projectId, + payload.destination.parentFolder.id, + payload.shareCredentials, + ); + + await refreshWorkflows(); + + if (payload.destination.canAccess) { + toast.showToast({ + title: i18n.baseText('folders.move.workflow.success.title'), + message: i18n.baseText('folders.move.workflow.success.message', { + interpolate: { + workflowName: payload.source.workflow.name, + newFolderName: payload.destination.parentFolder.name, + }, + }), + onClick: (event: MouseEvent | undefined) => { + if (event?.target instanceof HTMLAnchorElement) { + event.preventDefault(); + void router.push({ + name: VIEWS.PROJECTS_FOLDERS, + params: { + projectId: payload.destination.projectId, + folderId: payload.destination.parentFolder.id, + }, + }); + } }, - }), - onClick: (event: MouseEvent | undefined) => { - if (event?.target instanceof HTMLAnchorElement) { - event.preventDefault(); - void router.push({ - name: VIEWS.PROJECTS_FOLDERS, - params: { - projectId: payload.projectId, - folderId: payload.newParent.type === 'folder' ? payload.newParent.id : undefined, - }, - }); - } - }, - type: 'success', - }); + type: 'success', + }); + } else { + toast.showToast({ + title: i18n.baseText('folders.move.workflow.success.title'), + message: i18n.baseText('folders.move.workflow.success.messageNoAccess', { + interpolate: { + workflowName: payload.source.workflow.name, + newFolderName: payload.destination.parentFolder.name, + }, + }), + type: 'success', + }); + } } catch (error) { toast.showError(error, i18n.baseText('folders.move.workflow.error.title')); }