From 57444d3a16d77aabf3bd4d3835d86eca7aeff8f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Milorad=20FIlipovi=C4=87?= Date: Tue, 15 Apr 2025 16:59:57 +0200 Subject: [PATCH] feat(editor): Add drag n drop support for folders (#14549) --- cypress/composables/folders.ts | 15 ++ cypress/e2e/49-folders.cy.ts | 59 +++++ .../N8nActionToggle/ActionToggle.vue | 19 +- .../components/N8nBreadcrumbs/Breadcrumbs.vue | 63 ++++- .../__snapshots__/BreadCrumbs.test.ts.snap | 40 +-- packages/frontend/editor-ui/src/Interface.ts | 2 +- .../editor-ui/src/components/Draggable.vue | 1 + .../components/Folders/FolderBreadcrumbs.vue | 70 +++++- .../__snapshots__/RunDataJson.test.ts.snap | 1 + .../__snapshots__/VirtualSchema.test.ts.snap | 5 + .../src/composables/useFolders.test.ts | 17 ++ .../editor-ui/src/composables/useFolders.ts | 137 ++++++++++ .../editor-ui/src/stores/folders.store.ts | 8 + .../editor-ui/src/views/WorkflowsView.vue | 238 ++++++++++++++++-- 14 files changed, 619 insertions(+), 56 deletions(-) diff --git a/cypress/composables/folders.ts b/cypress/composables/folders.ts index eec01873ab..de6614487c 100644 --- a/cypress/composables/folders.ts +++ b/cypress/composables/folders.ts @@ -387,6 +387,21 @@ export function moveWorkflowToFolder(workflowName: string, folderName: string) { getMoveToFolderOption(folderName).should('be.visible').click(); getMoveFolderConfirmButton().should('be.enabled').click(); } + +export function dragAndDropToFolder(sourceName: string, destinationName: string) { + const draggable = `[data-test-id=draggable]:has([data-resourcename="${sourceName}"])`; + const droppable = `[data-test-id=draggable]:has([data-resourcename="${destinationName}"])`; + cy.get(draggable).trigger('mousedown'); + cy.draganddrop(draggable, droppable, { position: 'center' }); +} + +export function dragAndDropToProjectRoot(sourceName: string) { + const draggable = `[data-test-id=draggable]:has([data-resourcename="${sourceName}"])`; + const droppable = '[data-test-id="home-project"]'; + cy.get(draggable).trigger('mousedown'); + cy.draganddrop(draggable, droppable, { position: 'center' }); +} + /** * Utils */ diff --git a/cypress/e2e/49-folders.cy.ts b/cypress/e2e/49-folders.cy.ts index 5b927508ec..33a1bd385f 100644 --- a/cypress/e2e/49-folders.cy.ts +++ b/cypress/e2e/49-folders.cy.ts @@ -14,6 +14,8 @@ import { deleteEmptyFolderFromListDropdown, deleteFolderWithContentsFromCardDropdown, deleteFolderWithContentsFromListDropdown, + dragAndDropToFolder, + dragAndDropToProjectRoot, getAddResourceDropdown, getCurrentBreadcrumb, getFolderCard, @@ -535,4 +537,61 @@ describe('Folders', () => { getWorkflowCard('Child - Workflow').findChildByTestId('card-badge').should('exist'); }); }); + + describe('Drag and drop', () => { + it('should drag and drop folders into folders', () => { + const PROJECT_NAME = 'Drag and Drop Test'; + const TARGET_NAME = 'Drag me'; + const DESTINATION_NAME = 'Folder Destination'; + + createNewProject(PROJECT_NAME, { openAfterCreate: true }); + createFolderFromProjectHeader(TARGET_NAME); + createFolderFromProjectHeader(DESTINATION_NAME); + + dragAndDropToFolder(TARGET_NAME, DESTINATION_NAME); + successToast().should('contain.text', `${TARGET_NAME} has been moved to ${DESTINATION_NAME}`); + // Only one folder card should remain + getFolderCards().should('have.length', 1); + // Check folder in the destination + getFolderCard(DESTINATION_NAME).click(); + getFolderCard(TARGET_NAME).should('exist'); + }); + + it('should drag and drop folders into project root breadcrumb', () => { + const PROJECT_NAME = 'Drag to root test'; + const TARGET_NAME = 'To Project root'; + const PARENT_NAME = 'Parent Folder'; + + createNewProject(PROJECT_NAME, { openAfterCreate: true }); + createFolderFromProjectHeader(PARENT_NAME); + createFolderInsideFolder(TARGET_NAME, PARENT_NAME); + + dragAndDropToProjectRoot(TARGET_NAME); + + // No folder cards should be shown in the parent folder + getFolderCards().should('not.exist'); + successToast().should('contain.text', `${TARGET_NAME} has been moved to ${PROJECT_NAME}`); + // Check folder in the project root + getProjectMenuItem(PROJECT_NAME).click(); + getFolderCard(TARGET_NAME).should('exist'); + }); + + it('should drag and drop workflows into folders', () => { + const PROJECT_NAME = 'Drag and Drop WF Test'; + const TARGET_NAME = 'Drag me - WF'; + const DESTINATION_NAME = 'Workflow Destination'; + + createNewProject(PROJECT_NAME, { openAfterCreate: true }); + createFolderFromProjectHeader(DESTINATION_NAME); + createWorkflowFromProjectHeader(undefined, TARGET_NAME); + getProjectMenuItem(PROJECT_NAME).click(); + dragAndDropToFolder(TARGET_NAME, DESTINATION_NAME); + // No workflow cards should be shown in the project root + getWorkflowCards().should('not.exist'); + successToast().should('contain.text', `${TARGET_NAME} has been moved to ${DESTINATION_NAME}`); + // Check workflow in the destination + getFolderCard(DESTINATION_NAME).click(); + getWorkflowCard(TARGET_NAME).should('exist'); + }); + }); }); diff --git a/packages/frontend/@n8n/design-system/src/components/N8nActionToggle/ActionToggle.vue b/packages/frontend/@n8n/design-system/src/components/N8nActionToggle/ActionToggle.vue index bd6f3c47ac..5035266042 100644 --- a/packages/frontend/@n8n/design-system/src/components/N8nActionToggle/ActionToggle.vue +++ b/packages/frontend/@n8n/design-system/src/components/N8nActionToggle/ActionToggle.vue @@ -21,6 +21,7 @@ interface ActionToggleProps { loadingRowCount?: number; disabled?: boolean; popperClass?: string; + trigger?: 'click' | 'hover'; } defineOptions({ name: 'N8nActionToggle' }); @@ -35,13 +36,17 @@ withDefaults(defineProps(), { loadingRowCount: 3, disabled: false, popperClass: '', + trigger: 'click', }); const actionToggleRef = ref | null>(null); + const emit = defineEmits<{ action: [value: string]; 'visible-change': [value: boolean]; + 'item-mouseup': [action: UserAction]; }>(); + const onCommand = (value: string) => emit('action', value); const onVisibleChange = (value: boolean) => emit('visible-change', value); const openActionToggle = (isOpen: boolean) => { @@ -52,20 +57,29 @@ const openActionToggle = (isOpen: boolean) => { } }; +const onActionMouseUp = (action: UserAction) => { + emit('item-mouseup', action); + actionToggleRef.value?.handleClose(); +}; + defineExpose({ openActionToggle, });