feat(editor): Add functionality to delete and rename folders (no-changelog) (#13785)

This commit is contained in:
Milorad FIlipović
2025-03-12 11:04:53 +01:00
committed by GitHub
parent 09ebc3adc7
commit e73f618851
21 changed files with 1155 additions and 97 deletions

View File

@@ -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');
}

View File

@@ -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
});
});

View File

@@ -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;