fix(editor): Enable credential sharing between all types of projects (#10233)

This commit is contained in:
Csaba Tuncsik 2024-08-09 16:31:02 +02:00 committed by GitHub
parent 484737735a
commit 1cf48cc301
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 173 additions and 96 deletions

View file

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

View file

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

View file

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

View file

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

View file

@ -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() {

View file

@ -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}>`,

View file

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

View file

@ -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'),

View file

@ -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 wont 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",

View file

@ -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) {

View file

@ -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: {

View file

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

View file

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