mirror of
https://github.com/n8n-io/n8n.git
synced 2024-12-24 20:24:05 -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 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"]');
|
||||
|
@ -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')
|
||||
|
@ -46,7 +47,7 @@ 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();
|
||||
|
@ -54,13 +55,8 @@ export function createCredential(name: string) {
|
|||
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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@ import {
|
|||
WorkflowSharingModal,
|
||||
WorkflowsPage,
|
||||
} from '../pages';
|
||||
import { getVisibleSelect } from '../utils';
|
||||
import { getVisibleDropdown, getVisibleSelect } from '../utils';
|
||||
import * as projects from '../composables/projects';
|
||||
|
||||
/**
|
||||
|
@ -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', () => {
|
||||
|
@ -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');
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
|
||||
|
|
|
@ -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]) &&
|
||||
|
@ -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();
|
||||
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
|
@ -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>
|
||||
|
@ -89,7 +65,7 @@ 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';
|
||||
|
@ -97,6 +73,7 @@ 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',
|
||||
|
@ -134,7 +111,6 @@ export default defineComponent({
|
|||
data() {
|
||||
return {
|
||||
sharedWithProjects: [...(this.credential?.sharedWithProjects ?? [])] as ProjectSharingData[],
|
||||
teamProject: null as Project | null,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
|
@ -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 = (
|
||||
|
@ -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,
|
||||
|
@ -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'),
|
||||
|
@ -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: {
|
||||
|
@ -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() {
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
:name="modalName"
|
||||
:title="title"
|
||||
:center="true"
|
||||
width="460px"
|
||||
width="520"
|
||||
:event-bus="modalBus"
|
||||
@enter="onSubmit"
|
||||
>
|
||||
|
@ -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}>`,
|
||||
|
|
|
@ -20,6 +20,7 @@ const enum ProjectState {
|
|||
Owned = 'owned',
|
||||
Personal = 'personal',
|
||||
Team = 'team',
|
||||
SharedTeam = 'shared-team',
|
||||
Unknown = 'unknown',
|
||||
}
|
||||
|
||||
|
@ -44,6 +45,9 @@ const projectState = computed(() => {
|
|||
}
|
||||
return ProjectState.Personal;
|
||||
} else if (props.resource.homeProject?.type === ProjectTypes.Team) {
|
||||
if (props.resource.sharedWithProjects?.length) {
|
||||
return ProjectState.SharedTeam;
|
||||
}
|
||||
return ProjectState.Team;
|
||||
}
|
||||
return ProjectState.Unknown;
|
||||
|
@ -65,6 +69,7 @@ const badgeIcon = computed(() => {
|
|||
case ProjectState.SharedOwned:
|
||||
return 'user-friends';
|
||||
case ProjectState.Team:
|
||||
case ProjectState.SharedTeam:
|
||||
return 'archive';
|
||||
default:
|
||||
return '';
|
||||
|
@ -99,6 +104,13 @@ const badgeTooltip = computed(() => {
|
|||
name: badgeText.value,
|
||||
},
|
||||
});
|
||||
case ProjectState.SharedTeam:
|
||||
return i18n.baseText('projects.badge.tooltip.sharedTeam', {
|
||||
interpolate: {
|
||||
resourceTypeLabel: props.resourceTypeLabel,
|
||||
name: badgeText.value,
|
||||
},
|
||||
});
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
|
|
|
@ -29,11 +29,7 @@ const emit = defineEmits<{
|
|||
const selectedProject = ref(Array.isArray(model.value) ? '' : model.value?.id ?? '');
|
||||
const filter = ref('');
|
||||
const selectPlaceholder = computed(
|
||||
() =>
|
||||
props.placeholder ??
|
||||
(Array.isArray(model.value)
|
||||
? locale.baseText('projects.sharing.placeholder')
|
||||
: locale.baseText('projects.sharing.placeholder.single')),
|
||||
() => props.placeholder ?? locale.baseText('projects.sharing.select.placeholder'),
|
||||
);
|
||||
const noDataText = computed(
|
||||
() => props.emptyOptionsText ?? locale.baseText('projects.sharing.noMatchingUsers'),
|
||||
|
|
|
@ -532,7 +532,8 @@
|
|||
"credentialEdit.oAuthButton.connectMyAccount": "Connect my account",
|
||||
"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.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.list.delete": "Remove",
|
||||
"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.project.toast.title": "Credential successfully created in {projectName}",
|
||||
"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.nodeDocumentation": "Node Documentation",
|
||||
"dataDisplay.openDocumentationFor": "Open {nodeTypeDisplayName} documentation",
|
||||
|
@ -1653,9 +1652,9 @@
|
|||
"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.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.user": "User to transfer to",
|
||||
"settings.users.transferWorkflowsAndCredentials.placeholder": "Select user",
|
||||
"settings.users.transferWorkflowsAndCredentials": "Transfer their workflows and credentials to another user or project",
|
||||
"settings.users.transferWorkflowsAndCredentials.user": "User or project to transfer to",
|
||||
"settings.users.transferWorkflowsAndCredentials.placeholder": "Select project or user",
|
||||
"settings.users.transferredToUser": "Data transferred to {projectName}",
|
||||
"settings.users.userDeleted": "User deleted",
|
||||
"settings.users.userDeletedError": "Problem while deleting user",
|
||||
|
@ -2459,8 +2458,8 @@
|
|||
"projects.settings.role.editor": "Editor",
|
||||
"projects.settings.delete.title": "Delete {projectName}",
|
||||
"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.title": "Project to transfer to",
|
||||
"projects.settings.delete.question.transfer.label": "Transfer its workflows and credentials to another project or user",
|
||||
"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.title": "Type \"delete all data\" to confirm",
|
||||
"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.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.noMatchingUsers": "No matching users",
|
||||
"projects.sharing.placeholder": "Add projects...",
|
||||
"projects.sharing.placeholder.single": "Select project",
|
||||
"projects.sharing.noMatchingUsers": "No matching users or projects",
|
||||
"projects.sharing.select.placeholder": "Select project or user",
|
||||
"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.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}",
|
||||
|
@ -2492,10 +2492,11 @@
|
|||
"projects.move.resource.success.title": "Successfully moved {resourceTypeLabel}",
|
||||
"projects.move.resource.success.message": "{resourceName} {resourceTypeLabel} was moved to {targetProjectName} {link}",
|
||||
"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.sharedPersonal": "This {resourceTypeLabel} is owned by {name} 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 projects or users",
|
||||
"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.invalidCode": "Two-factor code failed. Please try again.",
|
||||
"mfa.code.modal.title": "Two-factor authentication",
|
||||
|
|
|
@ -185,9 +185,15 @@ export const useCredentialsStore = defineStore(STORES.CREDENTIALS, () => {
|
|||
return (credential: ICredentialsResponse | IUsedCredential | undefined): string => {
|
||||
const { firstName, lastName, email } = splitName(credential?.homeProject?.name ?? '');
|
||||
|
||||
return credential?.homeProject?.name
|
||||
? `${firstName} ${lastName} (${email})`
|
||||
: i18n.baseText('credentialEdit.credentialSharing.info.sharee.fallback');
|
||||
if (credential?.homeProject?.name) {
|
||||
if (lastName && email) {
|
||||
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,
|
||||
);
|
||||
|
||||
if (data?.homeProject && !credential.homeProject) {
|
||||
credential.homeProject = data.homeProject as ProjectSharingData;
|
||||
}
|
||||
|
||||
if (settingsStore.isEnterpriseFeatureEnabled[EnterpriseEditionFeature.Sharing]) {
|
||||
upsertCredential(credential);
|
||||
if (data.sharedWithProjects) {
|
||||
|
|
|
@ -11,6 +11,7 @@ import { useUsersStore } from '@/stores/users.store';
|
|||
import { createProjectListItem } from '@/__tests__/data/projects';
|
||||
import { useSettingsStore } from '@/stores/settings.store';
|
||||
import type { IN8nUISettings } from 'n8n-workflow';
|
||||
import { ProjectTypes } from '@/types/projects.types';
|
||||
|
||||
vi.mock('vue-router', () => {
|
||||
const params = {};
|
||||
|
@ -28,7 +29,12 @@ vi.mock('vue-router', () => {
|
|||
|
||||
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 projectsStore: ReturnType<typeof useProjectsStore>;
|
||||
|
@ -48,7 +54,7 @@ describe('ProjectSettings', () => {
|
|||
vi.spyOn(projectsStore, 'getAllProjects').mockImplementation(
|
||||
async () => await Promise.resolve(),
|
||||
);
|
||||
vi.spyOn(projectsStore, 'teamProjects', 'get').mockReturnValue(teamProjects);
|
||||
vi.spyOn(projectsStore, 'projects', 'get').mockReturnValue(projects);
|
||||
vi.spyOn(settingsStore, 'settings', 'get').mockReturnValue({
|
||||
enterprise: {
|
||||
projects: {
|
||||
|
|
|
@ -59,7 +59,7 @@ const usersList = computed(() =>
|
|||
);
|
||||
|
||||
const projects = computed(() =>
|
||||
projectsStore.teamProjects.filter((project) => project.id !== projectsStore.currentProjectId),
|
||||
projectsStore.projects.filter((project) => project.id !== projectsStore.currentProjectId),
|
||||
);
|
||||
const projectRoles = computed(() =>
|
||||
rolesStore.processedProjectRoles.map((role) => ({
|
||||
|
@ -302,6 +302,7 @@ onMounted(() => {
|
|||
class="mr-2xs"
|
||||
:model-value="user?.role || projectRoles[0].role"
|
||||
size="small"
|
||||
data-test-id="projects-settings-user-role-select"
|
||||
@update:model-value="onRoleAction(user, $event)"
|
||||
>
|
||||
<N8nOption
|
||||
|
|
|
@ -15,6 +15,7 @@ import { DELETE_USER_MODAL_KEY, EnterpriseEditionFeature } from '@/constants';
|
|||
import * as usersApi from '@/api/users';
|
||||
import { useSettingsStore } from '@/stores/settings.store';
|
||||
import { defaultSettings } from '@/__tests__/defaults';
|
||||
import { ProjectTypes } from '@/types/projects.types';
|
||||
|
||||
const wrapperComponentWithModal = {
|
||||
components: { SettingsUsersView, ModalRoot, DeleteUserModal },
|
||||
|
@ -34,7 +35,12 @@ const renderComponent = createComponentRenderer(wrapperComponentWithModal);
|
|||
|
||||
const loggedInUser = 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 projectsStore: ReturnType<typeof useProjectsStore>;
|
||||
|
@ -60,7 +66,7 @@ describe('SettingsUsersView', () => {
|
|||
vi.spyOn(projectsStore, 'getAllProjects').mockImplementation(
|
||||
async () => await Promise.resolve(),
|
||||
);
|
||||
vi.spyOn(projectsStore, 'personalProjects', 'get').mockReturnValue(personalProjects);
|
||||
vi.spyOn(projectsStore, 'projects', 'get').mockReturnValue(projects);
|
||||
|
||||
usersStore.currentUserId = loggedInUser.id;
|
||||
});
|
Loading…
Reference in a new issue