mirror of
https://github.com/n8n-io/n8n.git
synced 2025-03-05 20:50:17 -08:00
fix(editor): Enable credential sharing between all types of projects (#10233)
This commit is contained in:
parent
484737735a
commit
1cf48cc301
|
@ -5,7 +5,8 @@ const credentialsModal = new CredentialsModal();
|
||||||
|
|
||||||
export const getHomeButton = () => cy.getByTestId('project-home-menu-item');
|
export const getHomeButton = () => cy.getByTestId('project-home-menu-item');
|
||||||
export const getMenuItems = () => cy.getByTestId('project-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 getProjectTabs = () => cy.getByTestId('project-tabs').find('a');
|
||||||
export const getProjectTabWorkflows = () => getProjectTabs().filter('a[href$="/workflows"]');
|
export const getProjectTabWorkflows = () => getProjectTabs().filter('a[href$="/workflows"]');
|
||||||
export const getProjectTabCredentials = () => getProjectTabs().filter('a[href$="/credentials"]');
|
export const getProjectTabCredentials = () => getProjectTabs().filter('a[href$="/credentials"]');
|
||||||
|
@ -28,7 +29,7 @@ export const getResourceMoveConfirmModal = () =>
|
||||||
export const getProjectMoveSelect = () => cy.getByTestId('project-move-resource-modal-select');
|
export const getProjectMoveSelect = () => cy.getByTestId('project-move-resource-modal-select');
|
||||||
|
|
||||||
export function createProject(name: string) {
|
export function createProject(name: string) {
|
||||||
getAddProjectButton().should('be.visible').click();
|
getAddProjectButton().click();
|
||||||
|
|
||||||
getProjectNameInput()
|
getProjectNameInput()
|
||||||
.should('be.visible')
|
.should('be.visible')
|
||||||
|
@ -46,7 +47,7 @@ export function createWorkflow(fixtureKey: string, name: string) {
|
||||||
workflowPage.actions.zoomToFit();
|
workflowPage.actions.zoomToFit();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createCredential(name: string) {
|
export function createCredential(name: string, closeModal = true) {
|
||||||
credentialsModal.getters.newCredentialModal().should('be.visible');
|
credentialsModal.getters.newCredentialModal().should('be.visible');
|
||||||
credentialsModal.getters.newCredentialTypeSelect().should('be.visible');
|
credentialsModal.getters.newCredentialTypeSelect().should('be.visible');
|
||||||
credentialsModal.getters.newCredentialTypeOption('Notion API').click();
|
credentialsModal.getters.newCredentialTypeOption('Notion API').click();
|
||||||
|
@ -54,13 +55,8 @@ export function createCredential(name: string) {
|
||||||
credentialsModal.getters.connectionParameter('Internal Integration Secret').type('1234567890');
|
credentialsModal.getters.connectionParameter('Internal Integration Secret').type('1234567890');
|
||||||
credentialsModal.actions.setName(name);
|
credentialsModal.actions.setName(name);
|
||||||
credentialsModal.actions.save();
|
credentialsModal.actions.save();
|
||||||
credentialsModal.actions.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
export const actions = {
|
if (closeModal) {
|
||||||
createProject: (name: string) => {
|
credentialsModal.actions.close();
|
||||||
getAddProjectButton().click();
|
}
|
||||||
getProjectSettingsNameInput().type(name);
|
}
|
||||||
getProjectSettingsSaveButton().click();
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
|
@ -7,7 +7,7 @@ import {
|
||||||
WorkflowSharingModal,
|
WorkflowSharingModal,
|
||||||
WorkflowsPage,
|
WorkflowsPage,
|
||||||
} from '../pages';
|
} from '../pages';
|
||||||
import { getVisibleSelect } from '../utils';
|
import { getVisibleDropdown, getVisibleSelect } from '../utils';
|
||||||
import * as projects from '../composables/projects';
|
import * as projects from '../composables/projects';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -192,6 +192,73 @@ describe('Sharing', { disableAutoLogin: true }, () => {
|
||||||
credentialsModal.actions.saveSharing();
|
credentialsModal.actions.saveSharing();
|
||||||
credentialsModal.actions.close();
|
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', () => {
|
describe('Credential Usage in Cross Shared Workflows', () => {
|
||||||
|
@ -217,13 +284,13 @@ describe('Credential Usage in Cross Shared Workflows', () => {
|
||||||
credentialsModal.actions.createNewCredential('Notion API');
|
credentialsModal.actions.createNewCredential('Notion API');
|
||||||
|
|
||||||
// Create a notion credential in one project
|
// Create a notion credential in one project
|
||||||
projects.actions.createProject('Development');
|
projects.createProject('Development');
|
||||||
projects.getProjectTabCredentials().click();
|
projects.getProjectTabCredentials().click();
|
||||||
credentialsPage.getters.emptyListCreateCredentialButton().click();
|
credentialsPage.getters.emptyListCreateCredentialButton().click();
|
||||||
credentialsModal.actions.createNewCredential('Notion API');
|
credentialsModal.actions.createNewCredential('Notion API');
|
||||||
|
|
||||||
// Create a notion credential in another project
|
// Create a notion credential in another project
|
||||||
projects.actions.createProject('Test');
|
projects.createProject('Test');
|
||||||
projects.getProjectTabCredentials().click();
|
projects.getProjectTabCredentials().click();
|
||||||
credentialsPage.getters.emptyListCreateCredentialButton().click();
|
credentialsPage.getters.emptyListCreateCredentialButton().click();
|
||||||
credentialsModal.actions.createNewCredential('Notion API');
|
credentialsModal.actions.createNewCredential('Notion API');
|
||||||
|
|
|
@ -237,7 +237,7 @@ describe('Projects', { disableAutoLogin: true }, () => {
|
||||||
cy.signinAsMember(1);
|
cy.signinAsMember(1);
|
||||||
cy.visit(workflowsPage.url);
|
cy.visit(workflowsPage.url);
|
||||||
|
|
||||||
projects.getAddProjectButton().should('not.exist');
|
cy.getByTestId('add-project-menu-item').should('not.exist');
|
||||||
projects.getMenuItems().should('not.exist');
|
projects.getMenuItems().should('not.exist');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -446,6 +446,11 @@ const showSaveButton = computed(() => {
|
||||||
|
|
||||||
const showSharingContent = computed(() => activeTab.value === 'sharing' && !!credentialType.value);
|
const showSharingContent = computed(() => activeTab.value === 'sharing' && !!credentialType.value);
|
||||||
|
|
||||||
|
const homeProject = computed(() => {
|
||||||
|
const { currentProject, personalProject } = projectsStore;
|
||||||
|
return currentProject ?? personalProject;
|
||||||
|
});
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
requiredCredentials.value =
|
requiredCredentials.value =
|
||||||
isCredentialModalState(uiStore.modalsById[CREDENTIAL_EDIT_MODAL_KEY]) &&
|
isCredentialModalState(uiStore.modalsById[CREDENTIAL_EDIT_MODAL_KEY]) &&
|
||||||
|
@ -456,14 +461,12 @@ onMounted(async () => {
|
||||||
credentialTypeName: defaultCredentialTypeName.value,
|
credentialTypeName: defaultCredentialTypeName.value,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { currentProject, personalProject } = projectsStore;
|
const scopes = homeProject.value?.scopes ?? [];
|
||||||
const scopes = currentProject?.scopes ?? personalProject?.scopes ?? [];
|
|
||||||
const homeProject = currentProject ?? personalProject ?? {};
|
|
||||||
|
|
||||||
credentialData.value = {
|
credentialData.value = {
|
||||||
...credentialData.value,
|
...credentialData.value,
|
||||||
scopes,
|
scopes,
|
||||||
homeProject,
|
...(homeProject.value ? { homeProject: homeProject.value } : {}),
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
await loadCurrentCredential();
|
await loadCurrentCredential();
|
||||||
|
@ -793,6 +796,10 @@ async function saveCredential(): Promise<ICredentialsResponse | null> {
|
||||||
.sharedWithProjects as ProjectSharingData[];
|
.sharedWithProjects as ProjectSharingData[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (credentialData.value.homeProject) {
|
||||||
|
credentialDetails.homeProject = credentialData.value.homeProject as ProjectSharingData;
|
||||||
|
}
|
||||||
|
|
||||||
let credential: ICredentialsResponse | null = null;
|
let credential: ICredentialsResponse | null = null;
|
||||||
|
|
||||||
const isNewCredential = props.mode === 'new' && !credentialId.value;
|
const isNewCredential = props.mode === 'new' && !credentialId.value;
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<div :class="$style.container">
|
<div :class="$style.container">
|
||||||
<div v-if="!isSharingEnabled">
|
<div v-if="!isSharingEnabled">
|
||||||
<n8n-action-box
|
<N8nActionBox
|
||||||
:heading="
|
:heading="
|
||||||
$locale.baseText(
|
$locale.baseText(
|
||||||
uiStore.contextBasedTranslationKeys.credentials.sharing.unavailable.title,
|
uiStore.contextBasedTranslationKeys.credentials.sharing.unavailable.title,
|
||||||
|
@ -21,52 +21,28 @@
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<n8n-info-tip
|
<N8nInfoTip v-if="credentialPermissions.share" :bold="false" class="mb-s">
|
||||||
v-if="!credentialPermissions.share && !isHomeTeamProject"
|
{{ $locale.baseText('credentialEdit.credentialSharing.info.owner') }}
|
||||||
:bold="false"
|
</N8nInfoTip>
|
||||||
class="mb-s"
|
<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 },
|
interpolate: { credentialOwnerName },
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
</n8n-info-tip>
|
</N8nInfoTip>
|
||||||
<n8n-info-tip
|
|
||||||
v-if="credentialPermissions.share && !isHomeTeamProject"
|
|
||||||
:bold="false"
|
|
||||||
class="mb-s"
|
|
||||||
>
|
|
||||||
{{ $locale.baseText('credentialEdit.credentialSharing.info.owner') }}
|
|
||||||
</n8n-info-tip>
|
|
||||||
<ProjectSharing
|
<ProjectSharing
|
||||||
v-model="sharedWithProjects"
|
v-model="sharedWithProjects"
|
||||||
:projects="projects"
|
:projects="projects"
|
||||||
:roles="credentialRoles"
|
:roles="credentialRoles"
|
||||||
:home-project="homeProject"
|
:home-project="homeProject"
|
||||||
:readonly="!credentialPermissions.share"
|
:readonly="!credentialPermissions.share"
|
||||||
:static="isHomeTeamProject || !credentialPermissions.share"
|
:static="!credentialPermissions.share"
|
||||||
:placeholder="$locale.baseText('workflows.shareModal.select.placeholder')"
|
: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>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -89,7 +65,7 @@ import { useUsageStore } from '@/stores/usage.store';
|
||||||
import { EnterpriseEditionFeature } from '@/constants';
|
import { EnterpriseEditionFeature } from '@/constants';
|
||||||
import ProjectSharing from '@/components/Projects/ProjectSharing.vue';
|
import ProjectSharing from '@/components/Projects/ProjectSharing.vue';
|
||||||
import { useProjectsStore } from '@/stores/projects.store';
|
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 { ProjectTypes } from '@/types/projects.types';
|
||||||
import type { ICredentialDataDecryptedObject } from 'n8n-workflow';
|
import type { ICredentialDataDecryptedObject } from 'n8n-workflow';
|
||||||
import type { PermissionsMap } from '@/permissions';
|
import type { PermissionsMap } from '@/permissions';
|
||||||
|
@ -97,6 +73,7 @@ import type { CredentialScope } from '@n8n/permissions';
|
||||||
import type { EventBus } from 'n8n-design-system/utils';
|
import type { EventBus } from 'n8n-design-system/utils';
|
||||||
import { useRolesStore } from '@/stores/roles.store';
|
import { useRolesStore } from '@/stores/roles.store';
|
||||||
import type { RoleMap } from '@/types/roles.types';
|
import type { RoleMap } from '@/types/roles.types';
|
||||||
|
import { splitName } from '@/utils/projects.utils';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'CredentialSharing',
|
name: 'CredentialSharing',
|
||||||
|
@ -134,7 +111,6 @@ export default defineComponent({
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
sharedWithProjects: [...(this.credential?.sharedWithProjects ?? [])] as ProjectSharingData[],
|
sharedWithProjects: [...(this.credential?.sharedWithProjects ?? [])] as ProjectSharingData[],
|
||||||
teamProject: null as Project | null,
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
@ -159,7 +135,8 @@ export default defineComponent({
|
||||||
return this.settingsStore.isEnterpriseFeatureEnabled[EnterpriseEditionFeature.Sharing];
|
return this.settingsStore.isEnterpriseFeatureEnabled[EnterpriseEditionFeature.Sharing];
|
||||||
},
|
},
|
||||||
credentialOwnerName(): string {
|
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 {
|
credentialDataHomeProject(): ProjectSharingData | undefined {
|
||||||
const credentialContainsProjectSharingData = (
|
const credentialContainsProjectSharingData = (
|
||||||
|
@ -182,7 +159,7 @@ export default defineComponent({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
projects(): ProjectListItem[] {
|
projects(): ProjectListItem[] {
|
||||||
return this.projectsStore.personalProjects.filter(
|
return this.projectsStore.projects.filter(
|
||||||
(project) =>
|
(project) =>
|
||||||
project.id !== this.credential?.homeProject?.id &&
|
project.id !== this.credential?.homeProject?.id &&
|
||||||
project.id !== this.credentialDataHomeProject?.id,
|
project.id !== this.credentialDataHomeProject?.id,
|
||||||
|
@ -194,9 +171,6 @@ export default defineComponent({
|
||||||
isHomeTeamProject(): boolean {
|
isHomeTeamProject(): boolean {
|
||||||
return this.homeProject?.type === ProjectTypes.Team;
|
return this.homeProject?.type === ProjectTypes.Team;
|
||||||
},
|
},
|
||||||
numberOfMembersInHomeTeamProject(): number {
|
|
||||||
return this.teamProject?.relations.length ?? 0;
|
|
||||||
},
|
|
||||||
credentialRoleTranslations(): Record<string, string> {
|
credentialRoleTranslations(): Record<string, string> {
|
||||||
return {
|
return {
|
||||||
'credential:user': this.$locale.baseText('credentialEdit.credentialSharing.role.user'),
|
'credential:user': this.$locale.baseText('credentialEdit.credentialSharing.role.user'),
|
||||||
|
@ -210,6 +184,11 @@ export default defineComponent({
|
||||||
licensed,
|
licensed,
|
||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
|
sharingSelectPlaceholder() {
|
||||||
|
return this.projectsStore.teamProjects.length
|
||||||
|
? this.$locale.baseText('projects.sharing.select.placeholder.project')
|
||||||
|
: this.$locale.baseText('projects.sharing.select.placeholder.user');
|
||||||
|
},
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
sharedWithProjects: {
|
sharedWithProjects: {
|
||||||
|
@ -221,10 +200,6 @@ export default defineComponent({
|
||||||
},
|
},
|
||||||
async mounted() {
|
async mounted() {
|
||||||
await Promise.all([this.usersStore.fetchUsers(), this.projectsStore.getAllProjects()]);
|
await Promise.all([this.usersStore.fetchUsers(), this.projectsStore.getAllProjects()]);
|
||||||
|
|
||||||
if (this.homeProject && this.isHomeTeamProject) {
|
|
||||||
this.teamProject = await this.projectsStore.fetchProject(this.homeProject.id);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
goToUpgrade() {
|
goToUpgrade() {
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
:name="modalName"
|
:name="modalName"
|
||||||
:title="title"
|
:title="title"
|
||||||
:center="true"
|
:center="true"
|
||||||
width="460px"
|
width="520"
|
||||||
:event-bus="modalBus"
|
:event-bus="modalBus"
|
||||||
@enter="onSubmit"
|
@enter="onSubmit"
|
||||||
>
|
>
|
||||||
|
@ -147,7 +147,7 @@ export default defineComponent({
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
projects(): ProjectListItem[] {
|
projects(): ProjectListItem[] {
|
||||||
return this.projectsStore.personalProjects.filter(
|
return this.projectsStore.projects.filter(
|
||||||
(project) =>
|
(project) =>
|
||||||
project.name !==
|
project.name !==
|
||||||
`${this.userToDelete?.firstName} ${this.userToDelete?.lastName} <${this.userToDelete?.email}>`,
|
`${this.userToDelete?.firstName} ${this.userToDelete?.lastName} <${this.userToDelete?.email}>`,
|
||||||
|
|
|
@ -20,6 +20,7 @@ const enum ProjectState {
|
||||||
Owned = 'owned',
|
Owned = 'owned',
|
||||||
Personal = 'personal',
|
Personal = 'personal',
|
||||||
Team = 'team',
|
Team = 'team',
|
||||||
|
SharedTeam = 'shared-team',
|
||||||
Unknown = 'unknown',
|
Unknown = 'unknown',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -44,6 +45,9 @@ const projectState = computed(() => {
|
||||||
}
|
}
|
||||||
return ProjectState.Personal;
|
return ProjectState.Personal;
|
||||||
} else if (props.resource.homeProject?.type === ProjectTypes.Team) {
|
} else if (props.resource.homeProject?.type === ProjectTypes.Team) {
|
||||||
|
if (props.resource.sharedWithProjects?.length) {
|
||||||
|
return ProjectState.SharedTeam;
|
||||||
|
}
|
||||||
return ProjectState.Team;
|
return ProjectState.Team;
|
||||||
}
|
}
|
||||||
return ProjectState.Unknown;
|
return ProjectState.Unknown;
|
||||||
|
@ -65,6 +69,7 @@ const badgeIcon = computed(() => {
|
||||||
case ProjectState.SharedOwned:
|
case ProjectState.SharedOwned:
|
||||||
return 'user-friends';
|
return 'user-friends';
|
||||||
case ProjectState.Team:
|
case ProjectState.Team:
|
||||||
|
case ProjectState.SharedTeam:
|
||||||
return 'archive';
|
return 'archive';
|
||||||
default:
|
default:
|
||||||
return '';
|
return '';
|
||||||
|
@ -99,6 +104,13 @@ const badgeTooltip = computed(() => {
|
||||||
name: badgeText.value,
|
name: badgeText.value,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
case ProjectState.SharedTeam:
|
||||||
|
return i18n.baseText('projects.badge.tooltip.sharedTeam', {
|
||||||
|
interpolate: {
|
||||||
|
resourceTypeLabel: props.resourceTypeLabel,
|
||||||
|
name: badgeText.value,
|
||||||
|
},
|
||||||
|
});
|
||||||
default:
|
default:
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,11 +29,7 @@ const emit = defineEmits<{
|
||||||
const selectedProject = ref(Array.isArray(model.value) ? '' : model.value?.id ?? '');
|
const selectedProject = ref(Array.isArray(model.value) ? '' : model.value?.id ?? '');
|
||||||
const filter = ref('');
|
const filter = ref('');
|
||||||
const selectPlaceholder = computed(
|
const selectPlaceholder = computed(
|
||||||
() =>
|
() => props.placeholder ?? locale.baseText('projects.sharing.select.placeholder'),
|
||||||
props.placeholder ??
|
|
||||||
(Array.isArray(model.value)
|
|
||||||
? locale.baseText('projects.sharing.placeholder')
|
|
||||||
: locale.baseText('projects.sharing.placeholder.single')),
|
|
||||||
);
|
);
|
||||||
const noDataText = computed(
|
const noDataText = computed(
|
||||||
() => props.emptyOptionsText ?? locale.baseText('projects.sharing.noMatchingUsers'),
|
() => props.emptyOptionsText ?? locale.baseText('projects.sharing.noMatchingUsers'),
|
||||||
|
|
|
@ -532,7 +532,8 @@
|
||||||
"credentialEdit.oAuthButton.connectMyAccount": "Connect my account",
|
"credentialEdit.oAuthButton.connectMyAccount": "Connect my account",
|
||||||
"credentialEdit.oAuthButton.signInWithGoogle": "Sign in with Google",
|
"credentialEdit.oAuthButton.signInWithGoogle": "Sign in with Google",
|
||||||
"credentialEdit.credentialSharing.info.owner": "Sharing a credential allows people to use it in their workflows. They cannot access credential details.",
|
"credentialEdit.credentialSharing.info.owner": "Sharing a credential allows people to use it in their workflows. They cannot access credential details.",
|
||||||
"credentialEdit.credentialSharing.info.sharee": "Only {credentialOwnerName} can change who this credential is shared with",
|
"credentialEdit.credentialSharing.info.sharee.team": "Only users with credential sharing permission can change who this credential is shared with",
|
||||||
|
"credentialEdit.credentialSharing.info.sharee.personal": "Only {credentialOwnerName} or users with credential sharing permission can change who this credential is shared with",
|
||||||
"credentialEdit.credentialSharing.info.sharee.fallback": "the owner",
|
"credentialEdit.credentialSharing.info.sharee.fallback": "the owner",
|
||||||
"credentialEdit.credentialSharing.list.delete": "Remove",
|
"credentialEdit.credentialSharing.list.delete": "Remove",
|
||||||
"credentialEdit.credentialSharing.list.delete.confirm.title": "Remove access?",
|
"credentialEdit.credentialSharing.list.delete.confirm.title": "Remove access?",
|
||||||
|
@ -593,8 +594,6 @@
|
||||||
"credentials.create.personal.toast.text": "This credential is currently private to you.",
|
"credentials.create.personal.toast.text": "This credential is currently private to you.",
|
||||||
"credentials.create.project.toast.title": "Credential successfully created in {projectName}",
|
"credentials.create.project.toast.title": "Credential successfully created in {projectName}",
|
||||||
"credentials.create.project.toast.text": "All members from {projectName} will have access to this credential.",
|
"credentials.create.project.toast.text": "All members from {projectName} will have access to this credential.",
|
||||||
"credentials.shareModal.info.members": "This credential is owned by the {projectName} project which currently has {members} with access to this credential.",
|
|
||||||
"credentials.shareModal.info.members.number": "{number} member | {number} members",
|
|
||||||
"dataDisplay.needHelp": "Need help?",
|
"dataDisplay.needHelp": "Need help?",
|
||||||
"dataDisplay.nodeDocumentation": "Node Documentation",
|
"dataDisplay.nodeDocumentation": "Node Documentation",
|
||||||
"dataDisplay.openDocumentationFor": "Open {nodeTypeDisplayName} documentation",
|
"dataDisplay.openDocumentationFor": "Open {nodeTypeDisplayName} documentation",
|
||||||
|
@ -1653,9 +1652,9 @@
|
||||||
"settings.users.setupToInviteUsers": "To invite users, set up your own account",
|
"settings.users.setupToInviteUsers": "To invite users, set up your own account",
|
||||||
"settings.users.setupToInviteUsersInfo": "Invited users won’t be able to see workflows and credentials of other users unless you upgrade. <a href=\"https://docs.n8n.io/user-management/\" target=\"_blank\">More info</a> <br /> <br />",
|
"settings.users.setupToInviteUsersInfo": "Invited users won’t be able to see workflows and credentials of other users unless you upgrade. <a href=\"https://docs.n8n.io/user-management/\" target=\"_blank\">More info</a> <br /> <br />",
|
||||||
"settings.users.smtpToAddUsersWarning": "Set up SMTP before adding users (so that n8n can send them invitation emails). <a target=\"_blank\" href=\"https://docs.n8n.io/hosting/authentication/user-management-self-hosted/\">Instructions</a>",
|
"settings.users.smtpToAddUsersWarning": "Set up SMTP before adding users (so that n8n can send them invitation emails). <a target=\"_blank\" href=\"https://docs.n8n.io/hosting/authentication/user-management-self-hosted/\">Instructions</a>",
|
||||||
"settings.users.transferWorkflowsAndCredentials": "Transfer their workflows and credentials to another user",
|
"settings.users.transferWorkflowsAndCredentials": "Transfer their workflows and credentials to another user or project",
|
||||||
"settings.users.transferWorkflowsAndCredentials.user": "User to transfer to",
|
"settings.users.transferWorkflowsAndCredentials.user": "User or project to transfer to",
|
||||||
"settings.users.transferWorkflowsAndCredentials.placeholder": "Select user",
|
"settings.users.transferWorkflowsAndCredentials.placeholder": "Select project or user",
|
||||||
"settings.users.transferredToUser": "Data transferred to {projectName}",
|
"settings.users.transferredToUser": "Data transferred to {projectName}",
|
||||||
"settings.users.userDeleted": "User deleted",
|
"settings.users.userDeleted": "User deleted",
|
||||||
"settings.users.userDeletedError": "Problem while deleting user",
|
"settings.users.userDeletedError": "Problem while deleting user",
|
||||||
|
@ -2459,8 +2458,8 @@
|
||||||
"projects.settings.role.editor": "Editor",
|
"projects.settings.role.editor": "Editor",
|
||||||
"projects.settings.delete.title": "Delete {projectName}",
|
"projects.settings.delete.title": "Delete {projectName}",
|
||||||
"projects.settings.delete.message": "What should we do with the project data?",
|
"projects.settings.delete.message": "What should we do with the project data?",
|
||||||
"projects.settings.delete.question.transfer.label": "Transfer its workflows and credentials to another project",
|
"projects.settings.delete.question.transfer.label": "Transfer its workflows and credentials to another project or user",
|
||||||
"projects.settings.delete.question.transfer.title": "Project to transfer to",
|
"projects.settings.delete.question.transfer.title": "Project or user to transfer to",
|
||||||
"projects.settings.delete.question.wipe.label": "Delete its workflows and credentials",
|
"projects.settings.delete.question.wipe.label": "Delete its workflows and credentials",
|
||||||
"projects.settings.delete.question.wipe.title": "Type \"delete all data\" to confirm",
|
"projects.settings.delete.question.wipe.title": "Type \"delete all data\" to confirm",
|
||||||
"projects.settings.delete.question.wipe.placeholder": "delete all data",
|
"projects.settings.delete.question.wipe.placeholder": "delete all data",
|
||||||
|
@ -2473,9 +2472,10 @@
|
||||||
"projects.settings.role.upgrade.title": "Upgrade to unlock additional roles",
|
"projects.settings.role.upgrade.title": "Upgrade to unlock additional roles",
|
||||||
"projects.settings.role.upgrade.message": "You're currently limited to {limit} on the {planName} plan and can only assign the admin role to users within this project. To create more projects and unlock additional roles, upgrade your plan.",
|
"projects.settings.role.upgrade.message": "You're currently limited to {limit} on the {planName} plan and can only assign the admin role to users within this project. To create more projects and unlock additional roles, upgrade your plan.",
|
||||||
"projects.sharing.noMatchingProjects": "There are no available projects",
|
"projects.sharing.noMatchingProjects": "There are no available projects",
|
||||||
"projects.sharing.noMatchingUsers": "No matching users",
|
"projects.sharing.noMatchingUsers": "No matching users or projects",
|
||||||
"projects.sharing.placeholder": "Add projects...",
|
"projects.sharing.select.placeholder": "Select project or user",
|
||||||
"projects.sharing.placeholder.single": "Select project",
|
"projects.sharing.select.placeholder.user": "Share with user(s)",
|
||||||
|
"projects.sharing.select.placeholder.project": "Share with projects or users",
|
||||||
"projects.error.title": "Project error",
|
"projects.error.title": "Project error",
|
||||||
"projects.create.limit": "{num} project | {num} projects",
|
"projects.create.limit": "{num} project | {num} projects",
|
||||||
"projects.create.limitReached": "You have reached the {planName} plan limit of {limit}. Upgrade your plan to unlock more projects. {link}",
|
"projects.create.limitReached": "You have reached the {planName} plan limit of {limit}. Upgrade your plan to unlock more projects. {link}",
|
||||||
|
@ -2492,10 +2492,11 @@
|
||||||
"projects.move.resource.success.title": "Successfully moved {resourceTypeLabel}",
|
"projects.move.resource.success.title": "Successfully moved {resourceTypeLabel}",
|
||||||
"projects.move.resource.success.message": "{resourceName} {resourceTypeLabel} was moved to {targetProjectName} {link}",
|
"projects.move.resource.success.message": "{resourceName} {resourceTypeLabel} was moved to {targetProjectName} {link}",
|
||||||
"projects.move.resource.success.link": "View {targetProjectName}",
|
"projects.move.resource.success.link": "View {targetProjectName}",
|
||||||
"projects.badge.tooltip.sharedOwned": "This {resourceTypeLabel} is owned by you and shared with one or more other users",
|
"projects.badge.tooltip.sharedOwned": "This {resourceTypeLabel} is owned by you and shared with one or more projects or users",
|
||||||
"projects.badge.tooltip.sharedPersonal": "This {resourceTypeLabel} is owned by {name} and shared with one or more other users",
|
"projects.badge.tooltip.sharedPersonal": "This {resourceTypeLabel} is owned by {name} and shared with one or more projects or users",
|
||||||
"projects.badge.tooltip.personal": "This {resourceTypeLabel} is owned by {name}",
|
"projects.badge.tooltip.personal": "This {resourceTypeLabel} is owned by {name}",
|
||||||
"projects.badge.tooltip.team": "This {resourceTypeLabel} is owned by the {name} project. All users in this project have access to this.",
|
"projects.badge.tooltip.team": "This {resourceTypeLabel} is owned and accessible by the {name} project.",
|
||||||
|
"projects.badge.tooltip.sharedTeam": "This {resourceTypeLabel} is owned and accessible by the {name} project and shared with one or more projects or users",
|
||||||
"mfa.setup.invalidAuthenticatorCode": "{code} is not a valid number",
|
"mfa.setup.invalidAuthenticatorCode": "{code} is not a valid number",
|
||||||
"mfa.setup.invalidCode": "Two-factor code failed. Please try again.",
|
"mfa.setup.invalidCode": "Two-factor code failed. Please try again.",
|
||||||
"mfa.code.modal.title": "Two-factor authentication",
|
"mfa.code.modal.title": "Two-factor authentication",
|
||||||
|
|
|
@ -185,9 +185,15 @@ export const useCredentialsStore = defineStore(STORES.CREDENTIALS, () => {
|
||||||
return (credential: ICredentialsResponse | IUsedCredential | undefined): string => {
|
return (credential: ICredentialsResponse | IUsedCredential | undefined): string => {
|
||||||
const { firstName, lastName, email } = splitName(credential?.homeProject?.name ?? '');
|
const { firstName, lastName, email } = splitName(credential?.homeProject?.name ?? '');
|
||||||
|
|
||||||
return credential?.homeProject?.name
|
if (credential?.homeProject?.name) {
|
||||||
? `${firstName} ${lastName} (${email})`
|
if (lastName && email) {
|
||||||
: i18n.baseText('credentialEdit.credentialSharing.info.sharee.fallback');
|
return `${firstName} ${lastName} (${email})`;
|
||||||
|
} else {
|
||||||
|
return firstName;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return i18n.baseText('credentialEdit.credentialSharing.info.sharee.fallback');
|
||||||
|
}
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -314,6 +320,10 @@ export const useCredentialsStore = defineStore(STORES.CREDENTIALS, () => {
|
||||||
projectId,
|
projectId,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (data?.homeProject && !credential.homeProject) {
|
||||||
|
credential.homeProject = data.homeProject as ProjectSharingData;
|
||||||
|
}
|
||||||
|
|
||||||
if (settingsStore.isEnterpriseFeatureEnabled[EnterpriseEditionFeature.Sharing]) {
|
if (settingsStore.isEnterpriseFeatureEnabled[EnterpriseEditionFeature.Sharing]) {
|
||||||
upsertCredential(credential);
|
upsertCredential(credential);
|
||||||
if (data.sharedWithProjects) {
|
if (data.sharedWithProjects) {
|
||||||
|
|
|
@ -11,6 +11,7 @@ import { useUsersStore } from '@/stores/users.store';
|
||||||
import { createProjectListItem } from '@/__tests__/data/projects';
|
import { createProjectListItem } from '@/__tests__/data/projects';
|
||||||
import { useSettingsStore } from '@/stores/settings.store';
|
import { useSettingsStore } from '@/stores/settings.store';
|
||||||
import type { IN8nUISettings } from 'n8n-workflow';
|
import type { IN8nUISettings } from 'n8n-workflow';
|
||||||
|
import { ProjectTypes } from '@/types/projects.types';
|
||||||
|
|
||||||
vi.mock('vue-router', () => {
|
vi.mock('vue-router', () => {
|
||||||
const params = {};
|
const params = {};
|
||||||
|
@ -28,7 +29,12 @@ vi.mock('vue-router', () => {
|
||||||
|
|
||||||
const renderComponent = createComponentRenderer(ProjectSettings);
|
const renderComponent = createComponentRenderer(ProjectSettings);
|
||||||
|
|
||||||
const teamProjects = Array.from({ length: 3 }, () => createProjectListItem('team'));
|
const projects = [
|
||||||
|
ProjectTypes.Personal,
|
||||||
|
ProjectTypes.Personal,
|
||||||
|
ProjectTypes.Team,
|
||||||
|
ProjectTypes.Team,
|
||||||
|
].map(createProjectListItem);
|
||||||
|
|
||||||
let router: ReturnType<typeof useRouter>;
|
let router: ReturnType<typeof useRouter>;
|
||||||
let projectsStore: ReturnType<typeof useProjectsStore>;
|
let projectsStore: ReturnType<typeof useProjectsStore>;
|
||||||
|
@ -48,7 +54,7 @@ describe('ProjectSettings', () => {
|
||||||
vi.spyOn(projectsStore, 'getAllProjects').mockImplementation(
|
vi.spyOn(projectsStore, 'getAllProjects').mockImplementation(
|
||||||
async () => await Promise.resolve(),
|
async () => await Promise.resolve(),
|
||||||
);
|
);
|
||||||
vi.spyOn(projectsStore, 'teamProjects', 'get').mockReturnValue(teamProjects);
|
vi.spyOn(projectsStore, 'projects', 'get').mockReturnValue(projects);
|
||||||
vi.spyOn(settingsStore, 'settings', 'get').mockReturnValue({
|
vi.spyOn(settingsStore, 'settings', 'get').mockReturnValue({
|
||||||
enterprise: {
|
enterprise: {
|
||||||
projects: {
|
projects: {
|
||||||
|
|
|
@ -59,7 +59,7 @@ const usersList = computed(() =>
|
||||||
);
|
);
|
||||||
|
|
||||||
const projects = computed(() =>
|
const projects = computed(() =>
|
||||||
projectsStore.teamProjects.filter((project) => project.id !== projectsStore.currentProjectId),
|
projectsStore.projects.filter((project) => project.id !== projectsStore.currentProjectId),
|
||||||
);
|
);
|
||||||
const projectRoles = computed(() =>
|
const projectRoles = computed(() =>
|
||||||
rolesStore.processedProjectRoles.map((role) => ({
|
rolesStore.processedProjectRoles.map((role) => ({
|
||||||
|
@ -302,6 +302,7 @@ onMounted(() => {
|
||||||
class="mr-2xs"
|
class="mr-2xs"
|
||||||
:model-value="user?.role || projectRoles[0].role"
|
:model-value="user?.role || projectRoles[0].role"
|
||||||
size="small"
|
size="small"
|
||||||
|
data-test-id="projects-settings-user-role-select"
|
||||||
@update:model-value="onRoleAction(user, $event)"
|
@update:model-value="onRoleAction(user, $event)"
|
||||||
>
|
>
|
||||||
<N8nOption
|
<N8nOption
|
||||||
|
|
|
@ -15,6 +15,7 @@ import { DELETE_USER_MODAL_KEY, EnterpriseEditionFeature } from '@/constants';
|
||||||
import * as usersApi from '@/api/users';
|
import * as usersApi from '@/api/users';
|
||||||
import { useSettingsStore } from '@/stores/settings.store';
|
import { useSettingsStore } from '@/stores/settings.store';
|
||||||
import { defaultSettings } from '@/__tests__/defaults';
|
import { defaultSettings } from '@/__tests__/defaults';
|
||||||
|
import { ProjectTypes } from '@/types/projects.types';
|
||||||
|
|
||||||
const wrapperComponentWithModal = {
|
const wrapperComponentWithModal = {
|
||||||
components: { SettingsUsersView, ModalRoot, DeleteUserModal },
|
components: { SettingsUsersView, ModalRoot, DeleteUserModal },
|
||||||
|
@ -34,7 +35,12 @@ const renderComponent = createComponentRenderer(wrapperComponentWithModal);
|
||||||
|
|
||||||
const loggedInUser = createUser();
|
const loggedInUser = createUser();
|
||||||
const users = Array.from({ length: 3 }, createUser);
|
const users = Array.from({ length: 3 }, createUser);
|
||||||
const personalProjects = Array.from({ length: 3 }, createProjectListItem);
|
const projects = [
|
||||||
|
ProjectTypes.Personal,
|
||||||
|
ProjectTypes.Personal,
|
||||||
|
ProjectTypes.Team,
|
||||||
|
ProjectTypes.Team,
|
||||||
|
].map(createProjectListItem);
|
||||||
|
|
||||||
let pinia: ReturnType<typeof createPinia>;
|
let pinia: ReturnType<typeof createPinia>;
|
||||||
let projectsStore: ReturnType<typeof useProjectsStore>;
|
let projectsStore: ReturnType<typeof useProjectsStore>;
|
||||||
|
@ -60,7 +66,7 @@ describe('SettingsUsersView', () => {
|
||||||
vi.spyOn(projectsStore, 'getAllProjects').mockImplementation(
|
vi.spyOn(projectsStore, 'getAllProjects').mockImplementation(
|
||||||
async () => await Promise.resolve(),
|
async () => await Promise.resolve(),
|
||||||
);
|
);
|
||||||
vi.spyOn(projectsStore, 'personalProjects', 'get').mockReturnValue(personalProjects);
|
vi.spyOn(projectsStore, 'projects', 'get').mockReturnValue(projects);
|
||||||
|
|
||||||
usersStore.currentUserId = loggedInUser.id;
|
usersStore.currentUserId = loggedInUser.id;
|
||||||
});
|
});
|
Loading…
Reference in a new issue