From e73f618851d5e1d6dd2a498a77700cfa27e94040 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Milorad=20FIlipovi=C4=87?= Date: Wed, 12 Mar 2025 11:04:53 +0100 Subject: [PATCH] feat(editor): Add functionality to delete and rename folders (no-changelog) (#13785) --- .github/workflows/e2e-reusable.yml | 1 + cypress/composables/folders.ts | 242 ++++++++++++++++ cypress/e2e/49-folders.cy.ts | 256 +++++++++++++++++ cypress/scripts/run-e2e.js | 4 + .../N8nBreadcrumbs/BreadCrumbs.test.ts | 6 +- .../components/N8nBreadcrumbs/Breadcrumbs.vue | 14 +- .../__snapshots__/BreadCrumbs.test.ts.snap | 42 +-- packages/frontend/editor-ui/src/Interface.ts | 1 + .../frontend/editor-ui/src/api/workflows.ts | 30 ++ .../components/Folders/DeleteFolderModal.vue | 265 ++++++++++++++++++ .../components/Folders/FolderBreadcrumbs.vue | 4 +- .../src/components/Folders/FolderCard.test.ts | 21 +- .../src/components/Folders/FolderCard.vue | 20 +- .../editor-ui/src/components/Modals.vue | 7 + .../src/components/Projects/ProjectHeader.vue | 3 +- packages/frontend/editor-ui/src/constants.ts | 1 + .../src/plugins/i18n/locales/en.json | 29 +- .../editor-ui/src/stores/folders.store.ts | 22 ++ .../frontend/editor-ui/src/stores/ui.store.ts | 22 ++ .../editor-ui/src/views/WorkflowsView.test.ts | 1 + .../editor-ui/src/views/WorkflowsView.vue | 261 +++++++++++++---- 21 files changed, 1155 insertions(+), 97 deletions(-) create mode 100644 cypress/composables/folders.ts create mode 100644 cypress/e2e/49-folders.cy.ts create mode 100644 packages/frontend/editor-ui/src/components/Folders/DeleteFolderModal.vue diff --git a/.github/workflows/e2e-reusable.yml b/.github/workflows/e2e-reusable.yml index 4f394325a8..8826f980c5 100644 --- a/.github/workflows/e2e-reusable.yml +++ b/.github/workflows/e2e-reusable.yml @@ -161,6 +161,7 @@ jobs: env: NODE_OPTIONS: --dns-result-order=ipv4first CYPRESS_NODE_VIEW_VERSION: 2 + N8N_FOLDERS_ENABLED: true CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} E2E_TESTS: true diff --git a/cypress/composables/folders.ts b/cypress/composables/folders.ts new file mode 100644 index 0000000000..7d604e71bc --- /dev/null +++ b/cypress/composables/folders.ts @@ -0,0 +1,242 @@ +import { successToast } from '../pages/notifications'; + +/** + * Getters + */ +export function getPersonalProjectMenuItem() { + return cy.getByTestId('project-personal-menu-item'); +} + +export function getOverviewMenuItem() { + return cy.getByTestId('menu-item').contains('Overview'); +} + +export function getAddResourceDropdown() { + return cy.getByTestId('add-resource'); +} + +export function getFolderCards() { + return cy.getByTestId('folder-card'); +} + +export function getFolderCard(name: string) { + return cy.getByTestId('folder-card-name').contains(name).closest('[data-test-id="folder-card"]'); +} +export function getAddFolderButton() { + return cy.getByTestId('add-folder-button'); +} + +export function getListBreadcrumbs() { + return cy.getByTestId('main-breadcrumbs'); +} + +export function getHomeProjectBreadcrumb() { + return getListBreadcrumbs().findChildByTestId('home-project'); +} + +export function getVisibleListBreadcrumbs() { + return getListBreadcrumbs().findChildByTestId('breadcrumbs-item'); +} + +export function getCurrentBreadcrumb() { + return getListBreadcrumbs().findChildByTestId('breadcrumbs-item-current'); +} + +export function getMainBreadcrumbsEllipsis() { + return getListBreadcrumbs().findChildByTestId('hidden-items-menu'); +} + +export function getMainBreadcrumbsEllipsisMenuItems() { + return cy + .getByTestId('hidden-items-menu') + .find('span[aria-controls]') + .invoke('attr', 'aria-controls') + .then((popperId) => { + return cy.get(`#${popperId}`).find('li'); + }); +} + +export function getFolderCardBreadCrumbs(folderName: string) { + return getFolderCard(folderName).find('[data-test-id="folder-card-breadcrumbs"]'); +} + +export function getFolderCardBreadCrumbsEllipsis(folderName: string) { + return getFolderCardBreadCrumbs(folderName).find('[data-test-id="ellipsis"]'); +} + +export function getFolderCardHomeProjectBreadcrumb(folderName: string) { + return getFolderCardBreadCrumbs(folderName).find('[data-test-id="folder-card-home-project"]'); +} + +export function getFolderCardCurrentBreadcrumb(folderName: string) { + return getFolderCardBreadCrumbs(folderName).find('[data-test-id="breadcrumbs-item-current"]'); +} + +export function getOpenHiddenItemsTooltip() { + return cy.getByTestId('hidden-items-tooltip').filter(':visible'); +} + +export function getListActionsToggle() { + return cy.getByTestId('folder-breadcrumbs-actions'); +} + +export function getListActionItem(name: string) { + return cy + .getByTestId('folder-breadcrumbs-actions') + .find('span[aria-controls]') + .invoke('attr', 'aria-controls') + .then((popperId) => { + return cy.get(`#${popperId}`).find(`[data-test-id="action-${name}"]`); + }); +} + +export function getFolderCardActionToggle(folderName: string) { + return getFolderCard(folderName).find('[data-test-id="folder-card-actions"]'); +} + +export function getFolderCardActionItem(name: string) { + return cy + .getByTestId('folder-card-actions') + .find('span[aria-controls]') + .invoke('attr', 'aria-controls') + .then((popperId) => { + return cy.get(`#${popperId}`).find(`[data-test-id="action-${name}"]`); + }); +} + +export function getFolderDeleteModal() { + return cy.getByTestId('deleteFolder-modal'); +} + +export function getDeleteRadioButton() { + return cy.getByTestId('delete-content-radio'); +} + +export function getConfirmDeleteInput() { + return getFolderDeleteModal().findChildByTestId('delete-data-input').find('input'); +} + +export function getDeleteFolderModalConfirmButton() { + return getFolderDeleteModal().findChildByTestId('confirm-delete-folder-button'); +} +/** + * Actions + */ +export function goToPersonalProject() { + getPersonalProjectMenuItem().click(); +} + +export function createFolderInsideFolder(childName: string, parentName: string) { + getFolderCard(parentName).click(); + createFolderFromListHeaderButton(childName); +} + +export function createFolderFromListHeaderButton(folderName: string) { + getAddFolderButton().click(); + createNewFolder(folderName); +} + +export function createFolderFromProjectHeader(folderName: string) { + getPersonalProjectMenuItem().click(); + getAddResourceDropdown().click(); + cy.getByTestId('action-folder').click(); + createNewFolder(folderName); +} + +export function createFolderFromListDropdown(folderName: string) { + getListActionsToggle().click(); + getListActionItem('create').click(); + createNewFolder(folderName); +} + +export function createFolderFromCardActions(parentName: string, folderName: string) { + getFolderCardActionToggle(parentName).click(); + getFolderCardActionItem('create').click(); + createNewFolder(folderName); +} + +export function renameFolderFromListActions(folderName: string, newName: string) { + getFolderCard(folderName).click(); + getListActionsToggle().click(); + getListActionItem('rename').click(); + renameFolder(newName); +} + +export function renameFolderFromCardActions(folderName: string, newName: string) { + getFolderCardActionToggle(folderName).click(); + getFolderCardActionItem('rename').click(); + renameFolder(newName); +} + +export function deleteEmptyFolderFromCardDropdown(folderName: string) { + cy.intercept('DELETE', '/rest/projects/**').as('deleteFolder'); + getFolderCard(folderName).click(); + getListActionsToggle().click(); + getListActionItem('delete').click(); + cy.wait('@deleteFolder'); + successToast().should('contain.text', 'Folder deleted'); +} + +export function deleteEmptyFolderFromListDropdown(folderName: string) { + cy.intercept('DELETE', '/rest/projects/**').as('deleteFolder'); + getFolderCard(folderName).click(); + getListActionsToggle().click(); + getListActionItem('delete').click(); + cy.wait('@deleteFolder'); + successToast().should('contain.text', 'Folder deleted'); +} + +export function deleteFolderWithContentsFromListDropdown(folderName: string) { + getListActionsToggle().click(); + getListActionItem('delete').click(); + confirmFolderDelete(folderName); +} + +export function deleteFolderWithContentsFromCardDropdown(folderName: string) { + getFolderCardActionToggle(folderName).click(); + getFolderCardActionItem('delete').click(); + confirmFolderDelete(folderName); +} +/** + * Utils + */ + +/** + * Types folder name in the prompt and waits for the folder to be created + * @param name + */ +function createNewFolder(name: string) { + cy.intercept('POST', '/rest/projects/**').as('createFolder'); + cy.get('[role=dialog]') + .filter(':visible') + .within(() => { + cy.get('input.el-input__inner').type(name, { delay: 50 }); + cy.get('button.btn--confirm').click(); + }); + cy.wait('@createFolder'); + successToast().should('exist'); +} + +function renameFolder(newName: string) { + cy.intercept('PATCH', '/rest/projects/**').as('renameFolder'); + cy.get('[role=dialog]') + .filter(':visible') + .within(() => { + cy.get('input.el-input__inner').type('{selectall}'); + cy.get('input.el-input__inner').type(newName, { delay: 50 }); + cy.get('button.btn--confirm').click(); + }); + cy.wait('@renameFolder'); + successToast().should('exist'); +} + +function confirmFolderDelete(folderName: string) { + cy.intercept('DELETE', '/rest/projects/**').as('deleteFolder'); + getFolderDeleteModal().should('be.visible'); + getDeleteRadioButton().click(); + getConfirmDeleteInput().should('be.visible'); + getConfirmDeleteInput().type(`delete ${folderName}`, { delay: 50 }); + getDeleteFolderModalConfirmButton().should('be.enabled').click(); + cy.wait('@deleteFolder'); + successToast().contains('Folder deleted').should('exist'); +} diff --git a/cypress/e2e/49-folders.cy.ts b/cypress/e2e/49-folders.cy.ts new file mode 100644 index 0000000000..69d5a6a4ea --- /dev/null +++ b/cypress/e2e/49-folders.cy.ts @@ -0,0 +1,256 @@ +import { + createFolderFromCardActions, + createFolderFromListDropdown, + createFolderFromListHeaderButton, + createFolderFromProjectHeader, + createFolderInsideFolder, + deleteEmptyFolderFromCardDropdown, + deleteEmptyFolderFromListDropdown, + deleteFolderWithContentsFromCardDropdown, + deleteFolderWithContentsFromListDropdown, + getAddResourceDropdown, + getCurrentBreadcrumb, + getFolderCard, + getFolderCardActionItem, + getFolderCardActionToggle, + getFolderCardBreadCrumbsEllipsis, + getFolderCardCurrentBreadcrumb, + getFolderCardHomeProjectBreadcrumb, + getFolderCards, + getHomeProjectBreadcrumb, + getListBreadcrumbs, + getMainBreadcrumbsEllipsis, + getMainBreadcrumbsEllipsisMenuItems, + getOpenHiddenItemsTooltip, + getOverviewMenuItem, + getPersonalProjectMenuItem, + getVisibleListBreadcrumbs, + goToPersonalProject, + renameFolderFromCardActions, + renameFolderFromListActions, +} from '../composables/folders'; +import { visitWorkflowsPage } from '../composables/workflowsPage'; +import { successToast } from '../pages/notifications'; + +describe('Folders', () => { + before(() => { + cy.resetDatabase(); + cy.enableFeature('sharing'); + cy.enableFeature('advancedPermissions'); + cy.enableFeature('projectRole:admin'); + cy.enableFeature('projectRole:editor'); + cy.changeQuota('maxTeamProjects', -1); + }); + + beforeEach(() => { + visitWorkflowsPage(); + }); + + describe('Create and navigate folders', () => { + it('should create folder from the project header', () => { + createFolderFromProjectHeader('My Folder'); + getFolderCards().should('have.length.greaterThan', 0); + // Clicking on the success toast should navigate to the folder + successToast().find('a').click(); + getCurrentBreadcrumb().should('contain.text', 'My Folder'); + }); + + it('should create folder from the list header button', () => { + goToPersonalProject(); + // First create a folder so list appears + createFolderFromProjectHeader('Test 2'); + createFolderFromListHeaderButton('My Folder 2'); + getFolderCards().should('have.length.greaterThan', 0); + // Clicking on the success toast should navigate to the folder + successToast().find('a').contains('My Folder 2').click(); + getCurrentBreadcrumb().should('contain.text', 'My Folder 2'); + }); + + it('should create folder from the list header dropdown', () => { + goToPersonalProject(); + createFolderFromProjectHeader('Created from list dropdown'); + getFolderCard('Created from list dropdown').should('exist'); + getFolderCard('Created from list dropdown').click(); + createFolderFromListDropdown('Child Folder'); + successToast().should('exist'); + getFolderCard('Child Folder').should('exist'); + }); + + it('should create folder from the card dropdown', () => { + goToPersonalProject(); + createFolderFromProjectHeader('Created from card dropdown'); + getFolderCard('Created from card dropdown').should('exist'); + createFolderFromCardActions('Created from card dropdown', 'Child Folder'); + successToast().should('exist'); + // Open parent folder to see the new child folder + getFolderCard('Created from card dropdown').click(); + getFolderCard('Child Folder').should('exist'); + }); + + it('should navigate folders using breadcrumbs and dropdown menu', () => { + goToPersonalProject(); + createFolderFromProjectHeader('Navigate Test'); + // Open folder using menu item + getFolderCardActionToggle('Navigate Test').click(); + getFolderCardActionItem('open').click(); + getCurrentBreadcrumb().should('contain.text', 'Navigate Test'); + // Create new child folder and navigate to it + createFolderFromListHeaderButton('Child Folder'); + getFolderCard('Child Folder').should('exist'); + getFolderCard('Child Folder').click(); + getCurrentBreadcrumb().should('contain.text', 'Child Folder'); + // Navigate back to parent folder using breadcrumbs + getVisibleListBreadcrumbs().contains('Navigate Test').click(); + getCurrentBreadcrumb().should('contain.text', 'Navigate Test'); + // Go back to home project using breadcrumbs + getHomeProjectBreadcrumb().click(); + getListBreadcrumbs().should('not.exist'); + }); + + // Creates folders inside folders and also checks breadcrumbs + it('should create multiple levels of folders', () => { + goToPersonalProject(); + createFolderFromProjectHeader('Multi-level Test'); + createFolderInsideFolder('Child Folder', 'Multi-level Test'); + // One level deep: + // - Both main breadcrumbs & card breadcrumbs should only show home project and current folder + getHomeProjectBreadcrumb().should('exist'); + getCurrentBreadcrumb().should('contain.text', 'Multi-level Test'); + getFolderCard('Child Folder').should('exist'); + getFolderCardHomeProjectBreadcrumb('Child Folder').should('exist'); + getFolderCardCurrentBreadcrumb('Child Folder').should('contain.text', 'Multi-level Test'); + // No hidden items at this level + getFolderCardBreadCrumbsEllipsis('Child Folder').should('not.exist'); + + createFolderInsideFolder('Child Folder 2', 'Child Folder'); + // Two levels deep: + // - Main breadcrumbs should also show parent folder, without hidden ellipsis + // - Card breadcrumbs should show home project, parent folder, with hidden ellipsis + getHomeProjectBreadcrumb().should('exist'); + getCurrentBreadcrumb().should('contain.text', 'Child Folder'); + getVisibleListBreadcrumbs().should('have.length', 1); + getMainBreadcrumbsEllipsis().should('not.exist'); + getFolderCardCurrentBreadcrumb('Child Folder 2').should('contain.text', 'Child Folder'); + getFolderCardBreadCrumbsEllipsis('Child Folder 2').should('exist'); + + // Three levels deep: + // - Main breadcrumbs should show parents up to the grandparent folder, with one hidden element + // - Card breadcrumbs should now show two hidden elements + createFolderInsideFolder('Child Folder 3', 'Child Folder 2'); + getVisibleListBreadcrumbs().should('have.length', 1); + getMainBreadcrumbsEllipsis().should('exist'); + // Clicking on the ellipsis should show hidden element in main breadcrumbs + getMainBreadcrumbsEllipsis().click(); + getMainBreadcrumbsEllipsisMenuItems().first().should('contain.text', 'Multi-level Test'); + getMainBreadcrumbsEllipsis().click(); + // Card breadcrumbs should show two hidden elements + getFolderCardBreadCrumbsEllipsis('Child Folder 3').should('exist'); + // Clicking on the ellipsis should show hidden element in card breadcrumbs + getFolderCardBreadCrumbsEllipsis('Child Folder 3').click(); + getOpenHiddenItemsTooltip().should('be.visible'); + getOpenHiddenItemsTooltip().should('contain.text', 'Multi-level Test / Child Folder'); + }); + + // Make sure breadcrumbs and folder card show correct info when landing straight on a folder page + it('should correctly render all elements when landing on a folder page', () => { + // Create a few levels of folders + goToPersonalProject(); + createFolderFromProjectHeader('Landing Test'); + createFolderInsideFolder('Child Folder', 'Landing Test'); + createFolderInsideFolder('Child Folder 2', 'Child Folder'); + createFolderInsideFolder('Child Folder 3', 'Child Folder 2'); + // Reload page to simulate landing on a folder page + cy.reload(); + // Main list breadcrumbs should show home project, parent, grandparent, with one hidden element + getHomeProjectBreadcrumb().should('exist'); + getCurrentBreadcrumb().should('contain.text', 'Child Folder 2'); + getVisibleListBreadcrumbs().should('have.length', 1); + getVisibleListBreadcrumbs().first().should('contain.text', 'Child Folder'); + getMainBreadcrumbsEllipsis().should('exist'); + getMainBreadcrumbsEllipsis().click(); + getMainBreadcrumbsEllipsisMenuItems().first().should('contain.text', 'Landing Test'); + // Should load child folder card + getFolderCard('Child Folder 3').should('exist'); + // Card breadcrumbs should show home project and parent, with two hidden elements + getFolderCardHomeProjectBreadcrumb('Child Folder 3').should('exist'); + getFolderCardCurrentBreadcrumb('Child Folder 3').should('contain.text', 'Child Folder 2'); + getFolderCardBreadCrumbsEllipsis('Child Folder 3').should('exist'); + getFolderCardBreadCrumbsEllipsis('Child Folder 3').click(); + getOpenHiddenItemsTooltip().should('be.visible'); + getOpenHiddenItemsTooltip().should('contain.text', 'Landing Test / Child Folder'); + }); + + it('should show folders only in projects', () => { + // No folder cards should be shown in the overview page + getOverviewMenuItem().click(); + getFolderCards().should('not.exist'); + // Option to create folders should not be available in the dropdown + getAddResourceDropdown().click(); + cy.getByTestId('action-folder').should('not.exist'); + + // In personal, we should see previously created folders + getPersonalProjectMenuItem().click(); + cy.getByTestId('action-folder').should('exist'); + createFolderFromProjectHeader('Personal Folder'); + getFolderCards().should('exist'); + }); + }); + + describe('Rename and delete folders', () => { + it('should rename folder from main dropdown', () => { + goToPersonalProject(); + createFolderFromProjectHeader('Rename Me'); + getFolderCard('Rename Me').should('exist'); + renameFolderFromListActions('Rename Me', 'Renamed'); + getCurrentBreadcrumb().should('contain.text', 'Renamed'); + }); + + it('should rename folder from card dropdown', () => { + goToPersonalProject(); + createFolderFromProjectHeader('Rename Me 2'); + renameFolderFromCardActions('Rename Me 2', 'Renamed 2'); + getFolderCard('Renamed 2').should('exist'); + }); + + it('should delete empty folder from card dropdown', () => { + goToPersonalProject(); + createFolderFromProjectHeader('Delete Me'); + getFolderCard('Delete Me').should('exist'); + deleteEmptyFolderFromCardDropdown('Delete Me'); + }); + + it('should delete empty folder from main dropdown', () => { + goToPersonalProject(); + createFolderFromProjectHeader('Delete Me 2'); + getFolderCard('Delete Me 2').should('exist'); + deleteEmptyFolderFromListDropdown('Delete Me 2'); + // Since we deleted the current folder, we should be back in the home project + getListBreadcrumbs().should('not.exist'); + getPersonalProjectMenuItem().find('li').should('have.class', 'is-active'); + }); + + it('should warn before deleting non-empty folder from list dropdown', () => { + goToPersonalProject(); + createFolderFromProjectHeader('I have children'); + createFolderInsideFolder('Child 1', 'I have children'); + deleteFolderWithContentsFromListDropdown('I have children'); + // Since we deleted the current folder, we should be back in the home project + getListBreadcrumbs().should('not.exist'); + getPersonalProjectMenuItem().find('li').should('have.class', 'is-active'); + }); + + // TODO: Once we have a backend endpoint that returns sub-folder count, enable this + // eslint-disable-next-line n8n-local-rules/no-skipped-tests + it.skip('should warn before deleting non-empty folder from card dropdown', () => { + goToPersonalProject(); + createFolderFromProjectHeader('I also have family'); + createFolderInsideFolder('Child 1', 'I also have family'); + // Back to home + getHomeProjectBreadcrumb().click(); + getFolderCard('I also have family').should('exist'); + deleteFolderWithContentsFromCardDropdown('I also have family'); + }); + + // TODO: Once we have backend endpoint that lists project folders, test transfer when deleting + }); +}); diff --git a/cypress/scripts/run-e2e.js b/cypress/scripts/run-e2e.js index c7f9ccf749..545b88bcf9 100755 --- a/cypress/scripts/run-e2e.js +++ b/cypress/scripts/run-e2e.js @@ -47,6 +47,7 @@ switch (scenario) { testCommand: 'cypress open', customEnv: { CYPRESS_NODE_VIEW_VERSION: 2, + N8N_FOLDERS_ENABLED: true, }, }); break; @@ -58,6 +59,7 @@ switch (scenario) { customEnv: { CYPRESS_NODE_VIEW_VERSION: 1, CYPRESS_BASE_URL: 'http://localhost:8080', + N8N_FOLDERS_ENABLED: true, }, }); break; @@ -69,6 +71,7 @@ switch (scenario) { customEnv: { CYPRESS_NODE_VIEW_VERSION: 2, CYPRESS_BASE_URL: 'http://localhost:8080', + N8N_FOLDERS_ENABLED: true, }, }); break; @@ -82,6 +85,7 @@ switch (scenario) { testCommand: `cypress run --headless ${specParam}`, customEnv: { CYPRESS_NODE_VIEW_VERSION: 2, + N8N_FOLDERS_ENABLED: true, }, }); break; diff --git a/packages/frontend/@n8n/design-system/src/components/N8nBreadcrumbs/BreadCrumbs.test.ts b/packages/frontend/@n8n/design-system/src/components/N8nBreadcrumbs/BreadCrumbs.test.ts index 1c3cf8b8e2..5efa713356 100644 --- a/packages/frontend/@n8n/design-system/src/components/N8nBreadcrumbs/BreadCrumbs.test.ts +++ b/packages/frontend/@n8n/design-system/src/components/N8nBreadcrumbs/BreadCrumbs.test.ts @@ -155,9 +155,11 @@ describe('Breadcrumbs', async () => { }, }); expect(getByTestId('ellipsis')).toBeTruthy(); - expect(getByTestId('action-toggle')).toBeTruthy(); + expect(getByTestId('hidden-items-menu')).toBeTruthy(); expect(getByTestId('ellipsis')).toHaveClass('disabled'); - expect(getByTestId('action-toggle').querySelector('.el-dropdown')).toHaveClass('is-disabled'); + expect(getByTestId('hidden-items-menu').querySelector('.el-dropdown')).toHaveClass( + 'is-disabled', + ); }); it('does not highlight last item for "highlightLastItem = false" ', () => { diff --git a/packages/frontend/@n8n/design-system/src/components/N8nBreadcrumbs/Breadcrumbs.vue b/packages/frontend/@n8n/design-system/src/components/N8nBreadcrumbs/Breadcrumbs.vue index a7ecd21628..626a6ef4e4 100644 --- a/packages/frontend/@n8n/design-system/src/components/N8nBreadcrumbs/Breadcrumbs.vue +++ b/packages/frontend/@n8n/design-system/src/components/N8nBreadcrumbs/Breadcrumbs.vue @@ -137,7 +137,7 @@ const handleTooltipClose = () => { > " @@ -25,12 +25,12 @@ exports[`Breadcrumbs > renders custom separator correctly 1`] = `
  • Folder 1
  • - +
  • Folder 2
  • - +
  • Folder 3
  • - -
  • Current
  • +
  • +
  • Current
  • " @@ -43,12 +43,12 @@ exports[`Breadcrumbs > renders default version correctly 1`] = `
  • Folder 1
  • - +
  • /
  • Folder 2
  • - +
  • /
  • Folder 3
  • - -
  • Current
  • +
  • /
  • +
  • Current
  • " @@ -58,16 +58,16 @@ exports[`Breadcrumbs > renders slots correctly 1`] = ` "
    [PRE] Custom content
    [POST] Custom content
    @@ -81,12 +81,12 @@ exports[`Breadcrumbs > renders small version correctly 1`] = `
  • Folder 1
  • - +
  • /
  • Folder 2
  • - +
  • /
  • Folder 3
  • - -
  • Current
  • +
  • /
  • +
  • Current
  • " diff --git a/packages/frontend/editor-ui/src/Interface.ts b/packages/frontend/editor-ui/src/Interface.ts index ceb7b1579e..73fca7fc74 100644 --- a/packages/frontend/editor-ui/src/Interface.ts +++ b/packages/frontend/editor-ui/src/Interface.ts @@ -342,6 +342,7 @@ export type BaseFolderItem = BaseResource & { createdAt: string; updatedAt: string; workflowCount: number; + subFolderCount: number; parentFolder?: FolderShortInfo; homeProject?: ProjectSharingData; sharedWithProjects?: ProjectSharingData[]; diff --git a/packages/frontend/editor-ui/src/api/workflows.ts b/packages/frontend/editor-ui/src/api/workflows.ts index 7ac2e74d34..5e97417ccd 100644 --- a/packages/frontend/editor-ui/src/api/workflows.ts +++ b/packages/frontend/editor-ui/src/api/workflows.ts @@ -1,5 +1,6 @@ import type { FolderCreateResponse, + FolderListItem, FolderTreeResponseItem, IExecutionResponse, IExecutionsCurrentSummaryExtended, @@ -110,3 +111,32 @@ export async function getFolderPath( `/projects/${projectId}/folders/${folderId}/tree`, ); } + +export async function deleteFolder( + context: IRestApiContext, + projectId: string, + folderId: string, + transferToFolderId?: string, +): Promise { + return await makeRestApiRequest(context, 'DELETE', `/projects/${projectId}/folders/${folderId}`, { + transferToFolderId, + }); +} + +export async function renameFolder( + context: IRestApiContext, + projectId: string, + folderId: string, + name: string, +): Promise { + return await makeRestApiRequest(context, 'PATCH', `/projects/${projectId}/folders/${folderId}`, { + name, + }); +} + +export async function getProjectFolders( + context: IRestApiContext, + projectId: string, +): Promise { + return await makeRestApiRequest(context, 'GET', `/projects/${projectId}/folders`); +} diff --git a/packages/frontend/editor-ui/src/components/Folders/DeleteFolderModal.vue b/packages/frontend/editor-ui/src/components/Folders/DeleteFolderModal.vue new file mode 100644 index 0000000000..72fca8e7fb --- /dev/null +++ b/packages/frontend/editor-ui/src/components/Folders/DeleteFolderModal.vue @@ -0,0 +1,265 @@ + + + + + diff --git a/packages/frontend/editor-ui/src/components/Folders/FolderBreadcrumbs.vue b/packages/frontend/editor-ui/src/components/Folders/FolderBreadcrumbs.vue index 1c8e0fbe0a..57ee9a8a14 100644 --- a/packages/frontend/editor-ui/src/components/Folders/FolderBreadcrumbs.vue +++ b/packages/frontend/editor-ui/src/components/Folders/FolderBreadcrumbs.vue @@ -51,11 +51,11 @@ const onAction = (action: string) => { :highlight-last-item="false" :path-truncated="breadcrumbs.visibleItems[0].parentFolder" :hidden-items="breadcrumbs.hiddenItems" - data-test-id="folder-card-breadcrumbs" + data-test-id="folder-list-breadcrumbs" @item-selected="onItemSelect" >