Skip to content

Commit

Permalink
fix(editor): Enable credential sharing between all types of projects (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
cstuncsik authored Aug 9, 2024
1 parent 4847377 commit 1cf48cc
Show file tree
Hide file tree
Showing 13 changed files with 173 additions and 96 deletions.
20 changes: 8 additions & 12 deletions cypress/composables/projects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ const credentialsModal = new CredentialsModal();

export const getHomeButton = () => cy.getByTestId('project-home-menu-item');
export const getMenuItems = () => cy.getByTestId('project-menu-item');
export const getAddProjectButton = () => cy.getByTestId('add-project-menu-item');
export const getAddProjectButton = () =>
cy.getByTestId('add-project-menu-item').should('contain', 'Add project').should('be.visible');
export const getProjectTabs = () => cy.getByTestId('project-tabs').find('a');
export const getProjectTabWorkflows = () => getProjectTabs().filter('a[href$="/workflows"]');
export const getProjectTabCredentials = () => getProjectTabs().filter('a[href$="/credentials"]');
Expand All @@ -28,7 +29,7 @@ export const getResourceMoveConfirmModal = () =>
export const getProjectMoveSelect = () => cy.getByTestId('project-move-resource-modal-select');

export function createProject(name: string) {
getAddProjectButton().should('be.visible').click();
getAddProjectButton().click();

getProjectNameInput()
.should('be.visible')
Expand All @@ -46,21 +47,16 @@ export function createWorkflow(fixtureKey: string, name: string) {
workflowPage.actions.zoomToFit();
}

export function createCredential(name: string) {
export function createCredential(name: string, closeModal = true) {
credentialsModal.getters.newCredentialModal().should('be.visible');
credentialsModal.getters.newCredentialTypeSelect().should('be.visible');
credentialsModal.getters.newCredentialTypeOption('Notion API').click();
credentialsModal.getters.newCredentialTypeButton().click();
credentialsModal.getters.connectionParameter('Internal Integration Secret').type('1234567890');
credentialsModal.actions.setName(name);
credentialsModal.actions.save();
credentialsModal.actions.close();
}

export const actions = {
createProject: (name: string) => {
getAddProjectButton().click();
getProjectSettingsNameInput().type(name);
getProjectSettingsSaveButton().click();
},
};
if (closeModal) {
credentialsModal.actions.close();
}
}
73 changes: 70 additions & 3 deletions cypress/e2e/17-sharing.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
WorkflowSharingModal,
WorkflowsPage,
} from '../pages';
import { getVisibleSelect } from '../utils';
import { getVisibleDropdown, getVisibleSelect } from '../utils';
import * as projects from '../composables/projects';

/**
Expand Down Expand Up @@ -192,6 +192,73 @@ describe('Sharing', { disableAutoLogin: true }, () => {
credentialsModal.actions.saveSharing();
credentialsModal.actions.close();
});

it('credentials should work between team and personal projects', () => {
cy.resetDatabase();
cy.enableFeature('sharing');
cy.enableFeature('advancedPermissions');
cy.enableFeature('projectRole:admin');
cy.enableFeature('projectRole:editor');
cy.changeQuota('maxTeamProjects', -1);

cy.signinAsOwner();
cy.visit('/');

projects.createProject('Development');

projects.getHomeButton().click();
workflowsPage.getters.newWorkflowButtonCard().click();
projects.createWorkflow('Test_workflow_1.json', 'Test workflow');

projects.getHomeButton().click();
projects.getProjectTabCredentials().click();
credentialsPage.getters.emptyListCreateCredentialButton().click();
projects.createCredential('Notion API');

credentialsPage.getters.credentialCard('Notion API').click();
credentialsModal.actions.changeTab('Sharing');
credentialsModal.getters.usersSelect().click();
getVisibleSelect()
.find('li')
.should('have.length', 4)
.filter(':contains("Development")')
.should('have.length', 1)
.click();
credentialsModal.getters.saveButton().click();
credentialsModal.actions.close();

projects.getProjectTabWorkflows().click();
workflowsPage.getters.workflowCardActions('Test workflow').click();
getVisibleDropdown().find('li').contains('Share').click();

workflowSharingModal.getters.usersSelect().filter(':visible').click();
getVisibleSelect().find('li').should('have.length', 3).first().click();
workflowSharingModal.getters.saveButton().click();

projects.getMenuItems().first().click();
workflowsPage.getters.newWorkflowButtonCard().click();
projects.createWorkflow('Test_workflow_1.json', 'Test workflow 2');
workflowPage.actions.openShareModal();
workflowSharingModal.getters.usersSelect().should('not.exist');

cy.get('body').type('{esc}');

projects.getMenuItems().first().click();
projects.getProjectTabCredentials().click();
credentialsPage.getters.createCredentialButton().click();
projects.createCredential('Notion API 2', false);
credentialsModal.actions.changeTab('Sharing');
credentialsModal.getters.usersSelect().click();
getVisibleSelect().find('li').should('have.length', 4).first().click();
credentialsModal.getters.saveButton().click();
credentialsModal.actions.close();

credentialsPage.getters
.credentialCards()
.should('have.length', 2)
.filter(':contains("Owned by me")')
.should('have.length', 1);
});
});

describe('Credential Usage in Cross Shared Workflows', () => {
Expand All @@ -217,13 +284,13 @@ describe('Credential Usage in Cross Shared Workflows', () => {
credentialsModal.actions.createNewCredential('Notion API');

// Create a notion credential in one project
projects.actions.createProject('Development');
projects.createProject('Development');
projects.getProjectTabCredentials().click();
credentialsPage.getters.emptyListCreateCredentialButton().click();
credentialsModal.actions.createNewCredential('Notion API');

// Create a notion credential in another project
projects.actions.createProject('Test');
projects.createProject('Test');
projects.getProjectTabCredentials().click();
credentialsPage.getters.emptyListCreateCredentialButton().click();
credentialsModal.actions.createNewCredential('Notion API');
Expand Down
2 changes: 1 addition & 1 deletion cypress/e2e/39-projects.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -237,7 +237,7 @@ describe('Projects', { disableAutoLogin: true }, () => {
cy.signinAsMember(1);
cy.visit(workflowsPage.url);

projects.getAddProjectButton().should('not.exist');
cy.getByTestId('add-project-menu-item').should('not.exist');
projects.getMenuItems().should('not.exist');
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -446,6 +446,11 @@ const showSaveButton = computed(() => {
const showSharingContent = computed(() => activeTab.value === 'sharing' && !!credentialType.value);
const homeProject = computed(() => {
const { currentProject, personalProject } = projectsStore;
return currentProject ?? personalProject;
});
onMounted(async () => {
requiredCredentials.value =
isCredentialModalState(uiStore.modalsById[CREDENTIAL_EDIT_MODAL_KEY]) &&
Expand All @@ -456,14 +461,12 @@ onMounted(async () => {
credentialTypeName: defaultCredentialTypeName.value,
});
const { currentProject, personalProject } = projectsStore;
const scopes = currentProject?.scopes ?? personalProject?.scopes ?? [];
const homeProject = currentProject ?? personalProject ?? {};
const scopes = homeProject.value?.scopes ?? [];
credentialData.value = {
...credentialData.value,
scopes,
homeProject,
...(homeProject.value ? { homeProject: homeProject.value } : {}),
};
} else {
await loadCurrentCredential();
Expand Down Expand Up @@ -793,6 +796,10 @@ async function saveCredential(): Promise<ICredentialsResponse | null> {
.sharedWithProjects as ProjectSharingData[];
}
if (credentialData.value.homeProject) {
credentialDetails.homeProject = credentialData.value.homeProject as ProjectSharingData;
}
let credential: ICredentialsResponse | null = null;
const isNewCredential = props.mode === 'new' && !credentialId.value;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<template>
<div :class="$style.container">
<div v-if="!isSharingEnabled">
<n8n-action-box
<N8nActionBox
:heading="
$locale.baseText(
uiStore.contextBasedTranslationKeys.credentials.sharing.unavailable.title,
Expand All @@ -21,52 +21,28 @@
/>
</div>
<div v-else>
<n8n-info-tip
v-if="!credentialPermissions.share && !isHomeTeamProject"
:bold="false"
class="mb-s"
>
<N8nInfoTip v-if="credentialPermissions.share" :bold="false" class="mb-s">
{{ $locale.baseText('credentialEdit.credentialSharing.info.owner') }}
</N8nInfoTip>
<N8nInfoTip v-else-if="isHomeTeamProject" :bold="false" class="mb-s">
{{ $locale.baseText('credentialEdit.credentialSharing.info.sharee.team') }}
</N8nInfoTip>
<N8nInfoTip v-else :bold="false" class="mb-s">
{{
$locale.baseText('credentialEdit.credentialSharing.info.sharee', {
$locale.baseText('credentialEdit.credentialSharing.info.sharee.personal', {
interpolate: { credentialOwnerName },
})
}}
</n8n-info-tip>
<n8n-info-tip
v-if="credentialPermissions.share && !isHomeTeamProject"
:bold="false"
class="mb-s"
>
{{ $locale.baseText('credentialEdit.credentialSharing.info.owner') }}
</n8n-info-tip>
</N8nInfoTip>
<ProjectSharing
v-model="sharedWithProjects"
:projects="projects"
:roles="credentialRoles"
:home-project="homeProject"
:readonly="!credentialPermissions.share"
:static="isHomeTeamProject || !credentialPermissions.share"
:placeholder="$locale.baseText('workflows.shareModal.select.placeholder')"
:static="!credentialPermissions.share"
:placeholder="sharingSelectPlaceholder"
/>
<n8n-info-tip v-if="isHomeTeamProject" :bold="false" class="mt-s">
<i18n-t keypath="credentials.shareModal.info.members" tag="span">
<template #projectName>
{{ homeProject?.name }}
</template>
<template #members>
<strong>
{{
$locale.baseText('credentials.shareModal.info.members.number', {
interpolate: {
number: String(numberOfMembersInHomeTeamProject),
},
adjustToNumber: numberOfMembersInHomeTeamProject,
})
}}
</strong>
</template>
</i18n-t>
</n8n-info-tip>
</div>
</div>
</template>
Expand All @@ -89,14 +65,15 @@ import { useUsageStore } from '@/stores/usage.store';
import { EnterpriseEditionFeature } from '@/constants';
import ProjectSharing from '@/components/Projects/ProjectSharing.vue';
import { useProjectsStore } from '@/stores/projects.store';
import type { ProjectListItem, ProjectSharingData, Project } from '@/types/projects.types';
import type { ProjectListItem, ProjectSharingData } from '@/types/projects.types';
import { ProjectTypes } from '@/types/projects.types';
import type { ICredentialDataDecryptedObject } from 'n8n-workflow';
import type { PermissionsMap } from '@/permissions';
import type { CredentialScope } from '@n8n/permissions';
import type { EventBus } from 'n8n-design-system/utils';
import { useRolesStore } from '@/stores/roles.store';
import type { RoleMap } from '@/types/roles.types';
import { splitName } from '@/utils/projects.utils';
export default defineComponent({
name: 'CredentialSharing',
Expand Down Expand Up @@ -134,7 +111,6 @@ export default defineComponent({
data() {
return {
sharedWithProjects: [...(this.credential?.sharedWithProjects ?? [])] as ProjectSharingData[],
teamProject: null as Project | null,
};
},
computed: {
Expand All @@ -159,7 +135,8 @@ export default defineComponent({
return this.settingsStore.isEnterpriseFeatureEnabled[EnterpriseEditionFeature.Sharing];
},
credentialOwnerName(): string {
return this.credentialsStore.getCredentialOwnerNameById(`${this.credentialId}`);
const { firstName, lastName, email } = splitName(this.credential?.homeProject?.name ?? '');
return firstName || lastName ? `${firstName}${lastName ? ' ' + lastName : ''}` : email ?? '';
},
credentialDataHomeProject(): ProjectSharingData | undefined {
const credentialContainsProjectSharingData = (
Expand All @@ -182,7 +159,7 @@ export default defineComponent({
});
},
projects(): ProjectListItem[] {
return this.projectsStore.personalProjects.filter(
return this.projectsStore.projects.filter(
(project) =>
project.id !== this.credential?.homeProject?.id &&
project.id !== this.credentialDataHomeProject?.id,
Expand All @@ -194,9 +171,6 @@ export default defineComponent({
isHomeTeamProject(): boolean {
return this.homeProject?.type === ProjectTypes.Team;
},
numberOfMembersInHomeTeamProject(): number {
return this.teamProject?.relations.length ?? 0;
},
credentialRoleTranslations(): Record<string, string> {
return {
'credential:user': this.$locale.baseText('credentialEdit.credentialSharing.role.user'),
Expand All @@ -210,6 +184,11 @@ export default defineComponent({
licensed,
}));
},
sharingSelectPlaceholder() {
return this.projectsStore.teamProjects.length
? this.$locale.baseText('projects.sharing.select.placeholder.project')
: this.$locale.baseText('projects.sharing.select.placeholder.user');
},
},
watch: {
sharedWithProjects: {
Expand All @@ -221,10 +200,6 @@ export default defineComponent({
},
async mounted() {
await Promise.all([this.usersStore.fetchUsers(), this.projectsStore.getAllProjects()]);

if (this.homeProject && this.isHomeTeamProject) {
this.teamProject = await this.projectsStore.fetchProject(this.homeProject.id);
}
},
methods: {
goToUpgrade() {
Expand Down
4 changes: 2 additions & 2 deletions packages/editor-ui/src/components/DeleteUserModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
:name="modalName"
:title="title"
:center="true"
width="460px"
width="520"
:event-bus="modalBus"
@enter="onSubmit"
>
Expand Down Expand Up @@ -147,7 +147,7 @@ export default defineComponent({
return false;
},
projects(): ProjectListItem[] {
return this.projectsStore.personalProjects.filter(
return this.projectsStore.projects.filter(
(project) =>
project.name !==
`${this.userToDelete?.firstName} ${this.userToDelete?.lastName} <${this.userToDelete?.email}>`,
Expand Down
Loading

0 comments on commit 1cf48cc

Please sign in to comment.