From 136d49132567558b7d27069c857c0e0bfee70ce2 Mon Sep 17 00:00:00 2001 From: Csaba Tuncsik Date: Fri, 27 Sep 2024 15:12:31 +0200 Subject: [PATCH] fix(editor): Allow resources to move between personal and team projects (#10683) Co-authored-by: Danny Martini --- cypress/composables/projects.ts | 2 - cypress/e2e/39-projects.cy.ts | 261 ++++++++++++---- .../src/credentials/credentials.service.ee.ts | 8 - .../cli/src/workflows/workflow.service.ee.ts | 8 - .../credentials/credentials.api.ee.test.ts | 277 +++++++---------- .../workflows/workflows.controller.ee.test.ts | 284 +++++++----------- .../src/directives/n8n-truncate.test.ts | 6 +- packages/design-system/src/utils/index.ts | 1 + .../design-system/src/utils/string.test.ts | 4 +- packages/design-system/src/utils/string.ts | 2 +- .../src/components/InputNodeSelect.vue | 7 +- packages/editor-ui/src/components/Modals.vue | 11 - .../editor-ui/src/components/NodeSettings.vue | 27 +- .../Projects/ProjectCardBadge.test.ts | 3 +- .../components/Projects/ProjectCardBadge.vue | 2 +- .../ProjectMoveResourceConfirmModal.test.ts | 68 ----- .../ProjectMoveResourceConfirmModal.vue | 131 -------- .../Projects/ProjectMoveResourceModal.test.ts | 55 +++- .../Projects/ProjectMoveResourceModal.vue | 165 ++++++++-- .../ProjectMoveSuccessToastMessage.test.ts | 87 ++++++ .../ProjectMoveSuccessToastMessage.vue | 55 +++- .../Projects/ProjectSharingInfo.vue | 4 + .../forms/ResourceFiltersDropdown.vue | 9 +- packages/editor-ui/src/constants.ts | 1 - .../src/plugins/i18n/locales/en.json | 21 +- .../editor-ui/src/stores/projects.store.ts | 20 ++ packages/editor-ui/src/stores/ui.store.ts | 2 - .../src/views/ProjectSettings.test.ts | 6 +- .../editor-ui/src/views/ProjectSettings.vue | 6 +- .../editor-ui/src/views/WorkflowsView.test.ts | 2 +- .../editor-ui/src/views/WorkflowsView.vue | 4 +- 31 files changed, 823 insertions(+), 716 deletions(-) delete mode 100644 packages/editor-ui/src/components/Projects/ProjectMoveResourceConfirmModal.test.ts delete mode 100644 packages/editor-ui/src/components/Projects/ProjectMoveResourceConfirmModal.vue create mode 100644 packages/editor-ui/src/components/Projects/ProjectMoveSuccessToastMessage.test.ts diff --git a/cypress/composables/projects.ts b/cypress/composables/projects.ts index 84379088d1a39..da9c6fcc65678 100644 --- a/cypress/composables/projects.ts +++ b/cypress/composables/projects.ts @@ -32,8 +32,6 @@ export const addProjectMember = (email: string, role?: string) => { } }; export const getResourceMoveModal = () => cy.getByTestId('project-move-resource-modal'); -export const getResourceMoveConfirmModal = () => - cy.getByTestId('project-move-resource-confirm-modal'); export const getProjectMoveSelect = () => cy.getByTestId('project-move-resource-modal-select'); export function createProject(name: string) { diff --git a/cypress/e2e/39-projects.cy.ts b/cypress/e2e/39-projects.cy.ts index 59ed6bcb848a9..4e3bb583df970 100644 --- a/cypress/e2e/39-projects.cy.ts +++ b/cypress/e2e/39-projects.cy.ts @@ -1,5 +1,11 @@ import * as projects from '../composables/projects'; -import { INSTANCE_MEMBERS, MANUAL_TRIGGER_NODE_NAME, NOTION_NODE_NAME } from '../constants'; +import { + INSTANCE_ADMIN, + INSTANCE_MEMBERS, + INSTANCE_OWNER, + MANUAL_TRIGGER_NODE_NAME, + NOTION_NODE_NAME, +} from '../constants'; import { WorkflowsPage, WorkflowPage, @@ -481,54 +487,93 @@ describe('Projects', { disableAutoLogin: true }, () => { projects .getResourceMoveModal() .should('be.visible') - .find('button:contains("Next")') + .find('button:contains("Move workflow")') .should('be.disabled'); projects.getProjectMoveSelect().click(); getVisibleSelect() .find('li') - .should('have.length', 2) - .first() - .should('contain.text', 'Project 1') + .should('have.length', 5) + .filter(':contains("Project 1")') .click(); - projects.getResourceMoveModal().find('button:contains("Next")').click(); + projects.getResourceMoveModal().find('button:contains("Move workflow")').click(); + + workflowsPage.getters + .workflowCards() + .should('have.length', 3) + .filter(':contains("Owned by me")') + .should('not.exist'); + + // Move the workflow from Project 1 to Project 2 + projects.getMenuItems().first().click(); + workflowsPage.getters.workflowCards().should('have.length', 2); + workflowsPage.getters.workflowCardActions('Workflow in Home project').click(); + workflowsPage.getters.workflowMoveButton().click(); projects - .getResourceMoveConfirmModal() + .getResourceMoveModal() .should('be.visible') - .find('button:contains("Confirm")') + .find('button:contains("Move workflow")') .should('be.disabled'); - - projects - .getResourceMoveConfirmModal() - .find('input[type="checkbox"]') - .first() - .parents('label') + projects.getProjectMoveSelect().click(); + getVisibleSelect() + .find('li') + .should('have.length', 5) + .filter(':contains("Project 2")') .click(); + projects.getResourceMoveModal().find('button:contains("Move workflow")').click(); + + // Move the workflow from Project 2 to a member user + projects.getMenuItems().last().click(); + workflowsPage.getters.workflowCards().should('have.length', 2); + workflowsPage.getters.workflowCardActions('Workflow in Home project').click(); + workflowsPage.getters.workflowMoveButton().click(); + projects - .getResourceMoveConfirmModal() - .find('button:contains("Confirm")') + .getResourceMoveModal() + .should('be.visible') + .find('button:contains("Move workflow")') .should('be.disabled'); - projects - .getResourceMoveConfirmModal() - .find('input[type="checkbox"]') - .last() - .parents('label') + projects.getProjectMoveSelect().click(); + getVisibleSelect() + .find('li') + .should('have.length', 5) + .filter(`:contains("${INSTANCE_MEMBERS[0].email}")`) .click(); + + projects.getResourceMoveModal().find('button:contains("Move workflow")').click(); + workflowsPage.getters.workflowCards().should('have.length', 1); + + // Move the workflow from member user back to Home + projects.getHomeButton().click(); + workflowsPage.getters + .workflowCards() + .should('have.length', 3) + .filter(':has(.n8n-badge:contains("Project"))') + .should('have.length', 2); + workflowsPage.getters.workflowCardActions('Workflow in Home project').click(); + workflowsPage.getters.workflowMoveButton().click(); + projects - .getResourceMoveConfirmModal() - .find('button:contains("Confirm")') - .should('not.be.disabled') + .getResourceMoveModal() + .should('be.visible') + .find('button:contains("Move workflow")') + .should('be.disabled'); + projects.getProjectMoveSelect().click(); + getVisibleSelect() + .find('li') + .should('have.length', 5) + .filter(`:contains("${INSTANCE_OWNER.email}")`) .click(); + projects.getResourceMoveModal().find('button:contains("Move workflow")').click(); workflowsPage.getters .workflowCards() .should('have.length', 3) .filter(':contains("Owned by me")') - .should('not.exist'); + .should('have.length', 1); // Move the credential from Project 1 to Project 2 projects.getMenuItems().first().click(); - workflowsPage.getters.workflowCards().should('have.length', 2); projects.getProjectTabCredentials().click(); credentialsPage.getters.credentialCards().should('have.length', 1); credentialsPage.getters.credentialCardActions('Credential in Project 1').click(); @@ -537,48 +582,162 @@ describe('Projects', { disableAutoLogin: true }, () => { projects .getResourceMoveModal() .should('be.visible') - .find('button:contains("Next")') + .find('button:contains("Move credential")') .should('be.disabled'); projects.getProjectMoveSelect().click(); getVisibleSelect() .find('li') - .should('have.length', 1) - .first() - .should('contain.text', 'Project 2') + .should('have.length', 5) + .filter(':contains("Project 2")') .click(); - projects.getResourceMoveModal().find('button:contains("Next")').click(); + projects.getResourceMoveModal().find('button:contains("Move credential")').click(); + + credentialsPage.getters.credentialCards().should('not.have.length'); + + // Move the credential from Project 2 to admin user + projects.getMenuItems().last().click(); + projects.getProjectTabCredentials().click(); + credentialsPage.getters.credentialCards().should('have.length', 2); + + credentialsPage.getters.credentialCardActions('Credential in Project 1').click(); + credentialsPage.getters.credentialMoveButton().click(); projects - .getResourceMoveConfirmModal() + .getResourceMoveModal() .should('be.visible') - .find('button:contains("Confirm")') + .find('button:contains("Move credential")') .should('be.disabled'); + projects.getProjectMoveSelect().click(); + getVisibleSelect() + .find('li') + .should('have.length', 5) + .filter(`:contains("${INSTANCE_ADMIN.email}")`) + .click(); + projects.getResourceMoveModal().find('button:contains("Move credential")').click(); + credentialsPage.getters.credentialCards().should('have.length', 1); + + // Move the credential from admin user back to instance owner + projects.getHomeButton().click(); + projects.getProjectTabCredentials().click(); + credentialsPage.getters.credentialCards().should('have.length', 3); + + credentialsPage.getters.credentialCardActions('Credential in Project 1').click(); + credentialsPage.getters.credentialMoveButton().click(); projects - .getResourceMoveConfirmModal() - .find('input[type="checkbox"]') - .first() - .parents('label') + .getResourceMoveModal() + .should('be.visible') + .find('button:contains("Move credential")') + .should('be.disabled'); + projects.getProjectMoveSelect().click(); + getVisibleSelect() + .find('li') + .should('have.length', 5) + .filter(`:contains("${INSTANCE_OWNER.email}")`) .click(); + projects.getResourceMoveModal().find('button:contains("Move credential")').click(); + + credentialsPage.getters + .credentialCards() + .should('have.length', 3) + .filter(':contains("Owned by me")') + .should('have.length', 2); + + // Move the credential from admin user back to its original project (Project 1) + credentialsPage.getters.credentialCardActions('Credential in Project 1').click(); + credentialsPage.getters.credentialMoveButton().click(); + projects - .getResourceMoveConfirmModal() - .find('button:contains("Confirm")') + .getResourceMoveModal() + .should('be.visible') + .find('button:contains("Move credential")') .should('be.disabled'); - projects - .getResourceMoveConfirmModal() - .find('input[type="checkbox"]') - .last() - .parents('label') + projects.getProjectMoveSelect().click(); + getVisibleSelect() + .find('li') + .should('have.length', 5) + .filter(':contains("Project 1")') .click(); + projects.getResourceMoveModal().find('button:contains("Move credential")').click(); + + projects.getMenuItems().first().click(); + projects.getProjectTabCredentials().click(); + credentialsPage.getters + .credentialCards() + .filter(':contains("Credential in Project 1")') + .should('have.length', 1); + }); + + it('should allow to change inaccessible credential when the workflow was moved to a team project', () => { + cy.signinAsOwner(); + cy.visit(workflowsPage.url); + + // Create a credential in the Home project + projects.getProjectTabCredentials().should('be.visible').click(); + credentialsPage.getters.emptyListCreateCredentialButton().click(); + projects.createCredential('Credential in Home project'); + + // Create a workflow in the Home project + projects.getHomeButton().click(); + workflowsPage.getters.workflowCards().should('not.have.length'); + workflowsPage.getters.newWorkflowButtonCard().click(); + workflowsPage.getters.workflowCards().should('not.have.length'); + + workflowsPage.getters.newWorkflowButtonCard().click(); + workflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME); + workflowPage.actions.addNodeToCanvas(NOTION_NODE_NAME, true, true); + ndv.getters.backToCanvas().click(); + workflowPage.actions.saveWorkflowOnButtonClick(); + + // Create a project and add a user to it + projects.createProject('Project 1'); + projects.addProjectMember(INSTANCE_MEMBERS[0].email); + projects.getProjectSettingsSaveButton().click(); + + // Move the workflow from Home to Project 1 + projects.getHomeButton().click(); + workflowsPage.getters + .workflowCards() + .should('have.length', 1) + .filter(':contains("Owned by me")') + .should('exist'); + workflowsPage.getters.workflowCardActions('My workflow').click(); + workflowsPage.getters.workflowMoveButton().click(); + projects - .getResourceMoveConfirmModal() - .find('button:contains("Confirm")') - .should('not.be.disabled') + .getResourceMoveModal() + .should('be.visible') + .find('button:contains("Move workflow")') + .should('be.disabled'); + projects.getProjectMoveSelect().click(); + getVisibleSelect() + .find('li') + .should('have.length', 4) + .filter(':contains("Project 1")') .click(); - credentialsPage.getters.credentialCards().should('not.have.length'); - projects.getMenuItems().last().click(); - projects.getProjectTabCredentials().click(); - credentialsPage.getters.credentialCards().should('have.length', 2); + projects.getResourceMoveModal().find('button:contains("Move workflow")').click(); + + workflowsPage.getters + .workflowCards() + .should('have.length', 1) + .filter(':contains("Owned by me")') + .should('not.exist'); + + //Log out with instance owner and log in with the member user + mainSidebar.actions.openUserMenu(); + cy.getByTestId('user-menu-item-logout').click(); + + cy.get('input[name="email"]').type(INSTANCE_MEMBERS[0].email); + cy.get('input[name="password"]').type(INSTANCE_MEMBERS[0].password); + cy.getByTestId('form-submit-button').click(); + + // Open the moved workflow + workflowsPage.getters.workflowCards().should('have.length', 1); + workflowsPage.getters.workflowCards().first().click(); + + // Check if the credential can be changed + workflowPage.getters.canvasNodeByName(NOTION_NODE_NAME).should('be.visible').dblclick(); + ndv.getters.credentialInput().find('input').should('be.enabled'); }); it('should handle viewer role', () => { diff --git a/packages/cli/src/credentials/credentials.service.ee.ts b/packages/cli/src/credentials/credentials.service.ee.ts index 116137374acf9..aad78fe7b7303 100644 --- a/packages/cli/src/credentials/credentials.service.ee.ts +++ b/packages/cli/src/credentials/credentials.service.ee.ts @@ -157,14 +157,6 @@ export class EnterpriseCredentialsService { "You can't transfer a credential into the project that's already owning it.", ); } - if (sourceProject.type !== 'team' && sourceProject.type !== 'personal') { - throw new TransferCredentialError( - 'You can only transfer credentials out of personal or team projects.', - ); - } - if (destinationProject.type !== 'team') { - throw new TransferCredentialError('You can only transfer credentials into team projects.'); - } await this.sharedCredentialsRepository.manager.transaction(async (trx) => { // 6. transfer the credential diff --git a/packages/cli/src/workflows/workflow.service.ee.ts b/packages/cli/src/workflows/workflow.service.ee.ts index 5456ac62681bd..4e329e7464416 100644 --- a/packages/cli/src/workflows/workflow.service.ee.ts +++ b/packages/cli/src/workflows/workflow.service.ee.ts @@ -285,14 +285,6 @@ export class EnterpriseWorkflowService { "You can't transfer a workflow into the project that's already owning it.", ); } - if (sourceProject.type !== 'team' && sourceProject.type !== 'personal') { - throw new TransferWorkflowError( - 'You can only transfer workflows out of personal or team projects.', - ); - } - if (destinationProject.type !== 'team') { - throw new TransferWorkflowError('You can only transfer workflows into team projects.'); - } // 6. deactivate workflow if necessary const wasActive = workflow.active; diff --git a/packages/cli/test/integration/credentials/credentials.api.ee.test.ts b/packages/cli/test/integration/credentials/credentials.api.ee.test.ts index b1c0bfab75755..5428cafbd4dd2 100644 --- a/packages/cli/test/integration/credentials/credentials.api.ee.test.ts +++ b/packages/cli/test/integration/credentials/credentials.api.ee.test.ts @@ -3,6 +3,7 @@ import { Container } from 'typedi'; import config from '@/config'; import type { Project } from '@/databases/entities/project'; +import type { ProjectRole } from '@/databases/entities/project-relation'; import type { User } from '@/databases/entities/user'; import { ProjectRepository } from '@/databases/repositories/project.repository'; import { SharedCredentialsRepository } from '@/databases/repositories/shared-credentials.repository'; @@ -1118,18 +1119,6 @@ describe('PUT /:credentialId/transfer', () => { .expect(400); }); - test('cannot transfer into a personal project', async () => { - const credential = await saveCredential(randomCredentialPayload(), { - user: member, - }); - - await testServer - .authAgentFor(member) - .put(`/credentials/${credential.id}/transfer`) - .send({ destinationProjectId: memberPersonalProject.id }) - .expect(400); - }); - test('cannot transfer somebody elses credential', async () => { const destinationProject = await createTeamProject('Destination Project', member); @@ -1158,187 +1147,139 @@ describe('PUT /:credentialId/transfer', () => { .expect(404); }); - test('project:editors cannot transfer credentials', async () => { - // - // ARRANGE - // - const sourceProject = await createTeamProject('Source Project'); - await linkUserToProject(member, sourceProject, 'project:editor'); - - const credential = await saveCredential(randomCredentialPayload(), { - project: sourceProject, - }); - - const destinationProject = await createTeamProject('Destination Project', member); - - // - // ACT & ASSERT - // - await testServer - .authAgentFor(member) - .put(`/credentials/${credential.id}/transfer`) - .send({ destinationProjectId: destinationProject.id }) - .expect(403); - }); - - test('transferring from a personal project to a team project severs all sharings', async () => { - // - // ARRANGE - // - const credential = await saveCredential(randomCredentialPayload(), { user: member }); - - // these sharings should be deleted by the transfer - await shareCredentialWithUsers(credential, [anotherMember, owner]); - - const destinationProject = await createTeamProject('Destination Project', member); - - // - // ACT - // - const response = await testServer - .authAgentFor(member) - .put(`/credentials/${credential.id}/transfer`) - .send({ destinationProjectId: destinationProject.id }) - .expect(200); - - // - // ASSERT - // - expect(response.body).toEqual({}); - - const allSharings = await getCredentialSharings(credential); - expect(allSharings).toHaveLength(1); - expect(allSharings[0]).toMatchObject({ - projectId: destinationProject.id, - credentialsId: credential.id, - role: 'credential:owner', - }); - }); - - test('can transfer from team to another team project', async () => { - // - // ARRANGE - // - const sourceProject = await createTeamProject('Team Project 1', member); - const credential = await saveCredential(randomCredentialPayload(), { - project: sourceProject, - }); - - const destinationProject = await createTeamProject('Team Project 2', member); - - // - // ACT - // - const response = await testServer - .authAgentFor(member) - .put(`/credentials/${credential.id}/transfer`) - .send({ destinationProjectId: destinationProject.id }) - .expect(200); - - // - // ASSERT - // - expect(response.body).toEqual({}); - - const allSharings = await getCredentialSharings(credential); - expect(allSharings).toHaveLength(1); - expect(allSharings[0]).toMatchObject({ - projectId: destinationProject.id, - credentialsId: credential.id, - role: 'credential:owner', - }); - }); - - test.each([ - ['owners', () => owner], - ['admins', () => admin], - ])( - '%s can always transfer from any personal or team project into any team project', - async (_name, actor) => { + test.each(['project:editor', 'project:viewer'])( + '%ss cannot transfer credentials', + async (projectRole) => { // // ARRANGE // - const sourceProject = await createTeamProject('Source Project', member); - const teamCredential = await saveCredential(randomCredentialPayload(), { + const sourceProject = await createTeamProject('Source Project'); + await linkUserToProject(member, sourceProject, projectRole); + + const credential = await saveCredential(randomCredentialPayload(), { project: sourceProject, }); - const personalCredential = await saveCredential(randomCredentialPayload(), { user: member }); - const destinationProject = await createTeamProject('Destination Project', member); // - // ACT + // ACT & ASSERT // - const response1 = await testServer - .authAgentFor(actor()) - .put(`/credentials/${teamCredential.id}/transfer`) + await testServer + .authAgentFor(member) + .put(`/credentials/${credential.id}/transfer`) .send({ destinationProjectId: destinationProject.id }) - .expect(200); - const response2 = await testServer - .authAgentFor(actor()) - .put(`/credentials/${personalCredential.id}/transfer`) + .expect(403); + }, + ); + + test.each< + [ + // user role + 'owners' | 'admins', + // source project type + 'team' | 'personal', + // destination project type + 'team' | 'personal', + // actor + () => User, + // source project + () => Promise | Project, + // destination project + () => Promise | Project, + ] + >([ + // owner + [ + 'owners', + 'team', + 'team', + () => owner, + async () => await createTeamProject('Source Project'), + async () => await createTeamProject('Destination Project'), + ], + [ + 'owners', + 'team', + 'personal', + () => owner, + async () => await createTeamProject('Source Project'), + () => memberPersonalProject, + ], + [ + 'owners', + 'personal', + 'team', + () => owner, + () => memberPersonalProject, + async () => await createTeamProject('Destination Project'), + ], + + // admin + [ + 'admins', + 'team', + 'team', + () => admin, + async () => await createTeamProject('Source Project'), + async () => await createTeamProject('Destination Project'), + ], + [ + 'admins', + 'team', + 'personal', + () => admin, + async () => await createTeamProject('Source Project'), + () => memberPersonalProject, + ], + [ + 'admins', + 'personal', + 'team', + () => admin, + () => memberPersonalProject, + async () => await createTeamProject('Destination Project'), + ], + ])( + '%s can always transfer from a %s project to a %s project', + async ( + _roleName, + _sourceProjectName, + _destinationProjectName, + getUser, + getSourceProject, + getDestinationProject, + ) => { + // ARRANGE + const user = getUser(); + const sourceProject = await getSourceProject(); + const destinationProject = await getDestinationProject(); + + const credential = await saveCredential(randomCredentialPayload(), { + project: sourceProject, + }); + + // ACT + const response = await testServer + .authAgentFor(user) + .put(`/credentials/${credential.id}/transfer`) .send({ destinationProjectId: destinationProject.id }) .expect(200); - // // ASSERT - // - expect(response1.body).toEqual({}); - expect(response2.body).toEqual({}); - - { - const allSharings = await getCredentialSharings(teamCredential); - expect(allSharings).toHaveLength(1); - expect(allSharings[0]).toMatchObject({ - projectId: destinationProject.id, - credentialsId: teamCredential.id, - role: 'credential:owner', - }); - } + expect(response.body).toEqual({}); { - const allSharings = await getCredentialSharings(personalCredential); + const allSharings = await getCredentialSharings(credential); expect(allSharings).toHaveLength(1); expect(allSharings[0]).toMatchObject({ projectId: destinationProject.id, - credentialsId: personalCredential.id, + credentialsId: credential.id, role: 'credential:owner', }); } }, ); - - test.each([ - ['owners', () => owner], - ['admins', () => admin], - ])('%s cannot transfer into personal projects', async (_name, actor) => { - // - // ARRANGE - // - const sourceProject = await createTeamProject('Source Project', member); - const teamCredential = await saveCredential(randomCredentialPayload(), { - project: sourceProject, - }); - - const personalCredential = await saveCredential(randomCredentialPayload(), { user: member }); - - const destinationProject = anotherMemberPersonalProject; - - // - // ACT & ASSERT - // - await testServer - .authAgentFor(actor()) - .put(`/credentials/${teamCredential.id}/transfer`) - .send({ destinationProjectId: destinationProject.id }) - .expect(400); - await testServer - .authAgentFor(actor()) - .put(`/credentials/${personalCredential.id}/transfer`) - .send({ destinationProjectId: destinationProject.id }) - .expect(400); - }); }); function validateMainCredentialData(credential: ListQuery.Credentials.WithOwnedByAndSharedWith) { diff --git a/packages/cli/test/integration/workflows/workflows.controller.ee.test.ts b/packages/cli/test/integration/workflows/workflows.controller.ee.test.ts index c8f2db889f7da..2002843bfe829 100644 --- a/packages/cli/test/integration/workflows/workflows.controller.ee.test.ts +++ b/packages/cli/test/integration/workflows/workflows.controller.ee.test.ts @@ -5,6 +5,7 @@ import { v4 as uuid } from 'uuid'; import { ActiveWorkflowManager } from '@/active-workflow-manager'; import config from '@/config'; import type { Project } from '@/databases/entities/project'; +import type { ProjectRole } from '@/databases/entities/project-relation'; import type { User } from '@/databases/entities/user'; import { ProjectRepository } from '@/databases/repositories/project.repository'; import { WorkflowHistoryRepository } from '@/databases/repositories/workflow-history.repository'; @@ -1385,18 +1386,6 @@ describe('PUT /:workflowId/transfer', () => { .expect(400); }); - test('cannot transfer into a personal project', async () => { - const sourceProject = await createTeamProject('Team Project', member); - - const workflow = await createWorkflow({}, sourceProject); - - await testServer - .authAgentFor(member) - .put(`/workflows/${workflow.id}/transfer`) - .send({ destinationProjectId: memberPersonalProject.id }) - .expect(400); - }); - test('cannot transfer somebody elses workflow', async () => { const destinationProject = await createTeamProject('Team Project', member); @@ -1421,181 +1410,134 @@ describe('PUT /:workflowId/transfer', () => { .expect(404); }); - test('project:editors cannot transfer workflows', async () => { - // - // ARRANGE - // - const sourceProject = await createTeamProject(); - await linkUserToProject(member, sourceProject, 'project:editor'); - - const workflow = await createWorkflow({}, sourceProject); - - const destinationProject = await createTeamProject(); - await linkUserToProject(member, destinationProject, 'project:admin'); - - // - // ACT & ASSERT - // - await testServer - .authAgentFor(member) - .put(`/workflows/${workflow.id}/transfer`) - .send({ destinationProjectId: destinationProject.id }) - .expect(403); - }); - - test('transferring from a personal project to a team project severs all sharings', async () => { - // - // ARRANGE - // - const workflow = await createWorkflow({}, member); - - // these sharings should be deleted by the transfer - await shareWorkflowWithUsers(workflow, [anotherMember, owner]); - - const destinationProject = await createTeamProject('Team Project', member); - - // - // ACT - // - const response = await testServer - .authAgentFor(member) - .put(`/workflows/${workflow.id}/transfer`) - .send({ destinationProjectId: destinationProject.id }) - .expect(200); - - // - // ASSERT - // - expect(response.body).toEqual({}); - - const allSharings = await getWorkflowSharing(workflow); - expect(allSharings).toHaveLength(1); - expect(allSharings[0]).toMatchObject({ - projectId: destinationProject.id, - workflowId: workflow.id, - role: 'workflow:owner', - }); - }); - - test('can transfer from team to another team project', async () => { - // - // ARRANGE - // - const sourceProject = await createTeamProject('Team Project 1', member); - const workflow = await createWorkflow({}, sourceProject); - - const destinationProject = await createTeamProject('Team Project 2', member); - - // - // ACT - // - const response = await testServer - .authAgentFor(member) - .put(`/workflows/${workflow.id}/transfer`) - .send({ destinationProjectId: destinationProject.id }) - .expect(200); - - // - // ASSERT - // - expect(response.body).toEqual({}); - - const allSharings = await getWorkflowSharing(workflow); - expect(allSharings).toHaveLength(1); - expect(allSharings[0]).toMatchObject({ - projectId: destinationProject.id, - workflowId: workflow.id, - role: 'workflow:owner', - }); - }); - - test.each([ - ['owners', () => owner], - ['admins', () => admin], - ])( - 'global %s can always transfer from any personal or team project into any team project', - async (_name, actor) => { + test.each(['project:editor', 'project:viewer'])( + '%ss cannot transfer workflows', + async (projectRole) => { // // ARRANGE // - const sourceProject = await createTeamProject('Source Project', member); - const teamWorkflow = await createWorkflow({}, sourceProject); + const sourceProject = await createTeamProject(); + await linkUserToProject(member, sourceProject, projectRole); - const personalWorkflow = await createWorkflow({}, member); + const workflow = await createWorkflow({}, sourceProject); - const destinationProject = await createTeamProject('Destination Project', member); + const destinationProject = await createTeamProject(); + await linkUserToProject(member, destinationProject, 'project:admin'); // - // ACT + // ACT & ASSERT // - const response1 = await testServer - .authAgentFor(actor()) - .put(`/workflows/${teamWorkflow.id}/transfer`) + await testServer + .authAgentFor(member) + .put(`/workflows/${workflow.id}/transfer`) .send({ destinationProjectId: destinationProject.id }) - .expect(200); - const response2 = await testServer - .authAgentFor(actor()) - .put(`/workflows/${personalWorkflow.id}/transfer`) + .expect(403); + }, + ); + + test.each< + [ + // user role + 'owners' | 'admins', + // source project type + 'team' | 'personal', + // destination project type + 'team' | 'personal', + // actor + () => User, + // source project + () => Promise | Project, + // destination project + () => Promise | Project, + ] + >([ + // owner + [ + 'owners', + 'team', + 'team', + () => owner, + async () => await createTeamProject('Source Project'), + async () => await createTeamProject('Destination Project'), + ], + [ + 'owners', + 'team', + 'personal', + () => owner, + async () => await createTeamProject('Source Project'), + () => memberPersonalProject, + ], + [ + 'owners', + 'personal', + 'team', + () => owner, + () => memberPersonalProject, + async () => await createTeamProject('Destination Project'), + ], + + // admin + [ + 'admins', + 'team', + 'team', + () => admin, + async () => await createTeamProject('Source Project'), + async () => await createTeamProject('Destination Project'), + ], + [ + 'admins', + 'team', + 'personal', + () => admin, + async () => await createTeamProject('Source Project'), + () => memberPersonalProject, + ], + [ + 'admins', + 'personal', + 'team', + () => admin, + () => memberPersonalProject, + async () => await createTeamProject('Destination Project'), + ], + ])( + 'global %s can transfer workflows from a %s project to a %s project', + async ( + _roleName, + _sourceProjectName, + _destinationProjectName, + getActor, + getSourceProject, + getDestinationProject, + ) => { + // ARRANGE + const actor = getActor(); + const sourceProject = await getSourceProject(); + const destinationProject = await getDestinationProject(); + const workflow = await createWorkflow({}, sourceProject); + + // ACT + const response = await testServer + .authAgentFor(actor) + .put(`/workflows/${workflow.id}/transfer`) .send({ destinationProjectId: destinationProject.id }) .expect(200); - // // ASSERT - // - expect(response1.body).toEqual({}); - expect(response2.body).toEqual({}); - - { - const allSharings = await getWorkflowSharing(teamWorkflow); - expect(allSharings).toHaveLength(1); - expect(allSharings[0]).toMatchObject({ - projectId: destinationProject.id, - workflowId: teamWorkflow.id, - role: 'workflow:owner', - }); - } - - { - const allSharings = await getWorkflowSharing(personalWorkflow); - expect(allSharings).toHaveLength(1); - expect(allSharings[0]).toMatchObject({ - projectId: destinationProject.id, - workflowId: personalWorkflow.id, - role: 'workflow:owner', - }); - } + expect(response.body).toEqual({}); + + const allSharings = await getWorkflowSharing(workflow); + expect(allSharings).toHaveLength(1); + expect(allSharings[0]).toMatchObject({ + projectId: destinationProject.id, + workflowId: workflow.id, + role: 'workflow:owner', + }); }, ); - test.each([ - ['owners', () => owner], - ['admins', () => admin], - ])('global %s cannot transfer into personal projects', async (_name, actor) => { - // - // ARRANGE - // - const sourceProject = await createTeamProject('Source Project', member); - const teamWorkflow = await createWorkflow({}, sourceProject); - - const personalWorkflow = await createWorkflow({}, member); - - const destinationProject = anotherMemberPersonalProject; - - // - // ACT & ASSERT - // - await testServer - .authAgentFor(actor()) - .put(`/workflows/${teamWorkflow.id}/transfer`) - .send({ destinationProjectId: destinationProject.id }) - .expect(400); - await testServer - .authAgentFor(actor()) - .put(`/workflows/${personalWorkflow.id}/transfer`) - .send({ destinationProjectId: destinationProject.id }) - .expect(400); - }); - test('removes and re-adds the workflow from the active workflow manager during the transfer', async () => { // // ARRANGE diff --git a/packages/design-system/src/directives/n8n-truncate.test.ts b/packages/design-system/src/directives/n8n-truncate.test.ts index 89cd771283517..309cab456374e 100644 --- a/packages/design-system/src/directives/n8n-truncate.test.ts +++ b/packages/design-system/src/directives/n8n-truncate.test.ts @@ -24,7 +24,7 @@ describe('Directive n8n-truncate', () => { }, }, ); - expect(html()).toBe('
This is a very long text that...
'); + expect(html()).toBe('
This is a very long text that ...
'); }); it('should truncate text to 30 chars in case of wrong argument', async () => { @@ -48,7 +48,7 @@ describe('Directive n8n-truncate', () => { }, }, ); - expect(html()).toBe('
This is a very long text that...
'); + expect(html()).toBe('
This is a very long text that ...
'); }); it('should truncate text to given length', async () => { @@ -72,6 +72,6 @@ describe('Directive n8n-truncate', () => { }, }, ); - expect(html()).toBe('
This is a very long text...
'); + expect(html()).toBe('
This is a very long text ...
'); }); }); diff --git a/packages/design-system/src/utils/index.ts b/packages/design-system/src/utils/index.ts index be6ddc63754b0..d029baaa268e6 100644 --- a/packages/design-system/src/utils/index.ts +++ b/packages/design-system/src/utils/index.ts @@ -5,3 +5,4 @@ export * from './typeguards'; export * from './uid'; export * from './valueByPath'; export * from './testUtils'; +export * from './string'; diff --git a/packages/design-system/src/utils/string.test.ts b/packages/design-system/src/utils/string.test.ts index 6f65775f9ac67..0517d260d4e98 100644 --- a/packages/design-system/src/utils/string.test.ts +++ b/packages/design-system/src/utils/string.test.ts @@ -4,13 +4,13 @@ describe('Utils string', () => { describe('truncate', () => { it('should truncate text to 30 chars by default', () => { expect(truncate('This is a very long text that should be truncated')).toBe( - 'This is a very long text that...', + 'This is a very long text that ...', ); }); it('should truncate text to given length', () => { expect(truncate('This is a very long text that should be truncated', 25)).toBe( - 'This is a very long text...', + 'This is a very long text ...', ); }); }); diff --git a/packages/design-system/src/utils/string.ts b/packages/design-system/src/utils/string.ts index 9170b57c00733..1c2b2aecfde09 100644 --- a/packages/design-system/src/utils/string.ts +++ b/packages/design-system/src/utils/string.ts @@ -1,2 +1,2 @@ export const truncate = (text: string, length = 30): string => - text.length > length ? text.slice(0, length).trim() + '...' : text; + text.length > length ? text.slice(0, length) + '...' : text; diff --git a/packages/editor-ui/src/components/InputNodeSelect.vue b/packages/editor-ui/src/components/InputNodeSelect.vue index c7550a76c6a53..d0ec011e0778c 100644 --- a/packages/editor-ui/src/components/InputNodeSelect.vue +++ b/packages/editor-ui/src/components/InputNodeSelect.vue @@ -7,6 +7,7 @@ import { isPresent } from '@/utils/typesUtils'; import type { IConnectedNode, Workflow } from 'n8n-workflow'; import { computed } from 'vue'; import NodeIcon from './NodeIcon.vue'; +import { truncate } from 'n8n-design-system'; type Props = { nodes: IConnectedNode[]; @@ -100,11 +101,7 @@ function getMultipleNodesText(nodeName: string): string { } function title(nodeName: string, length = 30) { - const truncated = nodeName.substring(0, length); - if (truncated.length < nodeName.length) { - return `${truncated}...`; - } - return truncated; + return truncate(nodeName, length); } function subtitle(nodeName: string, depth: number) { diff --git a/packages/editor-ui/src/components/Modals.vue b/packages/editor-ui/src/components/Modals.vue index 396e70ce02bf7..a243ce13a54ef 100644 --- a/packages/editor-ui/src/components/Modals.vue +++ b/packages/editor-ui/src/components/Modals.vue @@ -31,7 +31,6 @@ import { WORKFLOW_HISTORY_VERSION_RESTORE, SETUP_CREDENTIALS_MODAL_KEY, PROJECT_MOVE_RESOURCE_MODAL, - PROJECT_MOVE_RESOURCE_CONFIRM_MODAL, PROMPT_MFA_CODE_MODAL_KEY, } from '@/constants'; @@ -66,7 +65,6 @@ import DebugPaywallModal from '@/components/DebugPaywallModal.vue'; import WorkflowHistoryVersionRestoreModal from '@/components/WorkflowHistory/WorkflowHistoryVersionRestoreModal.vue'; import SetupWorkflowCredentialsModal from '@/components/SetupWorkflowCredentialsModal/SetupWorkflowCredentialsModal.vue'; import ProjectMoveResourceModal from '@/components/Projects/ProjectMoveResourceModal.vue'; -import ProjectMoveResourceConfirmModal from '@/components/Projects/ProjectMoveResourceConfirmModal.vue'; import NewAssistantSessionModal from '@/components/AskAssistant/NewAssistantSessionModal.vue'; import PromptMfaCodeModal from './PromptMfaCodeModal/PromptMfaCodeModal.vue'; @@ -249,15 +247,6 @@ import PromptMfaCodeModal from './PromptMfaCodeModal/PromptMfaCodeModal.vue'; /> - - -