From 7d6b336fc9168cc8a0f1b3d870b6495e08b387b9 Mon Sep 17 00:00:00 2001 From: "marcel.kocisek" Date: Wed, 18 Dec 2024 09:52:59 +0100 Subject: [PATCH 01/14] Added method for error handling which could be reused in other applications --- .../lib/src/modules/project/projectApi.ts | 6 ++++-- .../packages/lib/src/modules/project/store.ts | 17 ++++++++++------- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/web-app/packages/lib/src/modules/project/projectApi.ts b/web-app/packages/lib/src/modules/project/projectApi.ts index fbd77fdd..67ff0e4e 100644 --- a/web-app/packages/lib/src/modules/project/projectApi.ts +++ b/web-app/packages/lib/src/modules/project/projectApi.ts @@ -184,7 +184,8 @@ export const ProjectApi = { `/v2/projects/${id}/collaborators/${userId}`, data, { - ...(withRetry ? getDefaultRetryOptions() : {}) + ...(withRetry ? getDefaultRetryOptions() : {}), + validateStatus } ) }, @@ -207,7 +208,8 @@ export const ProjectApi = { return ProjectModule.httpService.delete( `/v2/projects/${id}/collaborators/${userId}`, { - ...(withRetry ? getDefaultRetryOptions() : {}) + ...(withRetry ? getDefaultRetryOptions() : {}), + validateStatus } ) }, diff --git a/web-app/packages/lib/src/modules/project/store.ts b/web-app/packages/lib/src/modules/project/store.ts index 363d2ec7..d52baeae 100644 --- a/web-app/packages/lib/src/modules/project/store.ts +++ b/web-app/packages/lib/src/modules/project/store.ts @@ -41,7 +41,6 @@ import { SaveProjectSettings, ErrorCodes, ProjectAccessDetail, - UpdateProjectAccessParams, ProjectVersionFileChange, ProjectVersionListItem, UpdateProjectPayload, @@ -757,8 +756,8 @@ export const useProjectStore = defineStore('projectModule', { async removeProjectAccess( item: Pick ) { - const notificationStore = useNotificationStore() this.accessLoading = true + const notificationStore = useNotificationStore() try { const response = await ProjectApi.removeProjectAccess( this.project.id, @@ -786,7 +785,6 @@ export const useProjectStore = defineStore('projectModule', { userId: number data: UpdateProjectPayload }) { - const notificationStore = useNotificationStore() this.accessLoading = true try { const response = await ProjectApi.updateProjectAccess( @@ -801,15 +799,20 @@ export const useProjectStore = defineStore('projectModule', { return access }) this.project.access = response.data - } catch { - notificationStore.error({ - text: `Failed to update project access` - }) + } catch (err) { + this.handleProjectAccessError(err, 'Failed to update project access') } finally { this.accessLoading = false } }, + handleProjectAccessError(err: unknown, defaultMessage: string) { + const notificationStore = useNotificationStore() + notificationStore.error({ + text: getErrorMessage(err, defaultMessage) + }) + }, + async updatePublicFlag(payload: { projectId: string data: UpdatePublicFlagParams From 14b95ab019d625583c7d4c446a51baecc44089e9 Mon Sep 17 00:00:00 2001 From: Herman Snevajs Date: Wed, 18 Dec 2024 12:14:22 +0100 Subject: [PATCH 02/14] Use v2 api to get project collaborators and their project permission --- .../lib/src/common/permission_utils.ts | 29 ++++++++++++++++++- .../components/ProjectMembersTable.vue | 13 ++++++--- .../lib/src/modules/project/projectApi.ts | 6 ++++ .../packages/lib/src/modules/project/store.ts | 16 ++++++++++ .../packages/lib/src/modules/project/types.ts | 7 ++--- 5 files changed, 62 insertions(+), 9 deletions(-) diff --git a/web-app/packages/lib/src/common/permission_utils.ts b/web-app/packages/lib/src/common/permission_utils.ts index e810b2db..4877e2b7 100644 --- a/web-app/packages/lib/src/common/permission_utils.ts +++ b/web-app/packages/lib/src/common/permission_utils.ts @@ -4,7 +4,7 @@ import { DropdownOption } from './components/types' -import { ProjectAccess } from '@/modules' +import {ProjectAccess, ProjectAccessDetail} from '@/modules' export enum WorkspaceRole { guest, @@ -41,6 +41,14 @@ export type ProjectRoleName = | Extract | 'none' +const ROLE_HIERARCHY: ProjectRoleName[] = [ + 'none', + 'reader', + 'editor', + 'writer', + 'owner' +] + export type ProjectPermissionName = 'owner' | 'write' | 'edit' | 'read' export const USER_ROLE_NAME_BY_ROLE: Record = @@ -209,3 +217,22 @@ export function getProjectPermissionByRoleName( } return mapper[roleName] } + +export function calculateProjectPermission( + project_role: ProjectRoleName, + workspace_role: WorkspaceRoleName +): ProjectRoleName { + const mappedWorkspaceRole: ProjectRoleName = + workspace_role === 'admin' ? 'owner' : (workspace_role as ProjectRoleName) + + if (project_role === 'none' && workspace_role === 'guest') { + return 'none' + } + + const projectRoleIndex = ROLE_HIERARCHY.indexOf(project_role) + const workspaceRoleIndex = ROLE_HIERARCHY.indexOf(mappedWorkspaceRole) + + return projectRoleIndex > workspaceRoleIndex + ? project_role + : mappedWorkspaceRole +} diff --git a/web-app/packages/lib/src/modules/project/components/ProjectMembersTable.vue b/web-app/packages/lib/src/modules/project/components/ProjectMembersTable.vue index f96f9e6d..0ea7d66f 100644 --- a/web-app/packages/lib/src/modules/project/components/ProjectMembersTable.vue +++ b/web-app/packages/lib/src/modules/project/components/ProjectMembersTable.vue @@ -47,7 +47,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-MerginMaps-Commercial ]" > { projectStore.access = [] }) -projectStore.getProjectAccess(projectStore.project?.id) +projectStore.getProjectCollaborators(projectStore.project?.id) diff --git a/web-app/packages/lib/src/modules/project/projectApi.ts b/web-app/packages/lib/src/modules/project/projectApi.ts index fbd77fdd..c1e3aba0 100644 --- a/web-app/packages/lib/src/modules/project/projectApi.ts +++ b/web-app/packages/lib/src/modules/project/projectApi.ts @@ -317,5 +317,11 @@ export const ProjectApi = { return ProjectModule.httpService.get( `/v1/resource/changesets/${workspace}/${projectName}/${versionId}?path=${path}` ) + }, + + async getProjectCollaborators( + projectId: string + ): Promise> { + return ProjectModule.httpService.get(`/v2/projects/${projectId}/collaborators`) } } diff --git a/web-app/packages/lib/src/modules/project/store.ts b/web-app/packages/lib/src/modules/project/store.ts index 363d2ec7..3193b155 100644 --- a/web-app/packages/lib/src/modules/project/store.ts +++ b/web-app/packages/lib/src/modules/project/store.ts @@ -749,6 +749,22 @@ export const useProjectStore = defineStore('projectModule', { } }, + async getProjectCollaborators(projectId: string) { + const notificationStore = useNotificationStore() + + try { + this.accessLoading = true + const response = await ProjectApi.getProjectCollaborators(projectId) + this.access = response.data + } catch { + notificationStore.error({ + text: 'Failed to get project access' + }) + } finally { + this.accessLoading = false + } + }, + /** * Removes the given user's access to the current project. * diff --git a/web-app/packages/lib/src/modules/project/types.ts b/web-app/packages/lib/src/modules/project/types.ts index e8c39aaa..1763fd3a 100644 --- a/web-app/packages/lib/src/modules/project/types.ts +++ b/web-app/packages/lib/src/modules/project/types.ts @@ -5,7 +5,7 @@ /* eslint-disable camelcase */ import { ProjectRoleName, - ProjectPermissionName + ProjectPermissionName, WorkspaceRoleName } from '@/common/permission_utils' import { PaginatedRequestParamsApi, @@ -343,9 +343,8 @@ export type ErrorCodes = 'UpdateProjectAccessError' export interface ProjectAccessDetail { id: number - type: 'member' email: string username: string - project_permission: ProjectRoleName - name: string + workspace_role: WorkspaceRoleName + project_role: ProjectRoleName } From ec4284059c62b677d91c75dabce96bdca7abf2ab Mon Sep 17 00:00:00 2001 From: Herman Snevajs Date: Wed, 18 Dec 2024 12:21:29 +0100 Subject: [PATCH 03/14] Keep original interfaces --- web-app/packages/lib/src/common/permission_utils.ts | 2 +- .../project/components/ProjectMembersTable.vue | 10 +++++----- .../packages/lib/src/modules/project/projectApi.ts | 5 +++-- web-app/packages/lib/src/modules/project/types.ts | 12 +++++++++++- 4 files changed, 20 insertions(+), 9 deletions(-) diff --git a/web-app/packages/lib/src/common/permission_utils.ts b/web-app/packages/lib/src/common/permission_utils.ts index 4877e2b7..a86c4123 100644 --- a/web-app/packages/lib/src/common/permission_utils.ts +++ b/web-app/packages/lib/src/common/permission_utils.ts @@ -4,7 +4,7 @@ import { DropdownOption } from './components/types' -import {ProjectAccess, ProjectAccessDetail} from '@/modules' +import { ProjectAccess } from '@/modules' export enum WorkspaceRole { guest, diff --git a/web-app/packages/lib/src/modules/project/components/ProjectMembersTable.vue b/web-app/packages/lib/src/modules/project/components/ProjectMembersTable.vue index 0ea7d66f..734a6cee 100644 --- a/web-app/packages/lib/src/modules/project/components/ProjectMembersTable.vue +++ b/web-app/packages/lib/src/modules/project/components/ProjectMembersTable.vue @@ -86,7 +86,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-MerginMaps-Commercial import { computed, ref, onUnmounted } from 'vue' import { useProjectStore } from '../store' -import { ProjectAccessDetail } from '../types' +import { ProjectCollaborators } from '../types' import AppContainer from '@/common/components/AppContainer.vue' import AppDropdown from '@/common/components/AppDropdown.vue' @@ -153,15 +153,15 @@ const searchedItems = computed(() => }) ) -function canRemoveMember(item: ProjectAccessDetail) { +function canRemoveMember(item: ProjectCollaborators) { return props.allowRemove && item.id !== loggedUser.value?.id } -function removeMember(item: ProjectAccessDetail) { +function removeMember(item: ProjectCollaborators) { projectStore.removeProjectAccess(item) } -function roleUpdate(item: ProjectAccessDetail, value: ProjectRoleName) { +function roleUpdate(item: ProjectCollaborators, value: ProjectRoleName) { projectStore.updateProjectAccess({ projectId: projectStore.project.id, userId: item.id, @@ -169,7 +169,7 @@ function roleUpdate(item: ProjectAccessDetail, value: ProjectRoleName) { }) } -function ProjectPermission(item: ProjectAccessDetail) { +function ProjectPermission(item: ProjectCollaborators) { return calculateProjectPermission(item.project_role, item.workspace_role) } diff --git a/web-app/packages/lib/src/modules/project/projectApi.ts b/web-app/packages/lib/src/modules/project/projectApi.ts index c1e3aba0..9537d61c 100644 --- a/web-app/packages/lib/src/modules/project/projectApi.ts +++ b/web-app/packages/lib/src/modules/project/projectApi.ts @@ -26,7 +26,8 @@ import { ProjectAccess, ProjectVersionFileChange, UpdateProjectPayload, - UpdatePublicFlagParams + UpdatePublicFlagParams, + ProjectCollaborators } from '@/modules/project/types' export const ProjectApi = { @@ -321,7 +322,7 @@ export const ProjectApi = { async getProjectCollaborators( projectId: string - ): Promise> { + ): Promise> { return ProjectModule.httpService.get(`/v2/projects/${projectId}/collaborators`) } } diff --git a/web-app/packages/lib/src/modules/project/types.ts b/web-app/packages/lib/src/modules/project/types.ts index 1763fd3a..8b1e4bb7 100644 --- a/web-app/packages/lib/src/modules/project/types.ts +++ b/web-app/packages/lib/src/modules/project/types.ts @@ -5,7 +5,8 @@ /* eslint-disable camelcase */ import { ProjectRoleName, - ProjectPermissionName, WorkspaceRoleName + ProjectPermissionName, + WorkspaceRoleName } from '@/common/permission_utils' import { PaginatedRequestParamsApi, @@ -342,6 +343,15 @@ export interface ProjectVersionFileChange { export type ErrorCodes = 'UpdateProjectAccessError' export interface ProjectAccessDetail { + id: number + type: 'member' + email: string + username: string + project_permission: ProjectRoleName + name: string +} + +export interface ProjectCollaborators { id: number email: string username: string From dfb6ad72bf757f9c5f31e44236c9d7442395b62e Mon Sep 17 00:00:00 2001 From: "marcel.kocisek" Date: Wed, 18 Dec 2024 16:54:13 +0100 Subject: [PATCH 04/14] Added project_role to access --- server/mergin/sync/models.py | 1 + server/mergin/sync/private_api.yaml | 8 ++++++++ server/mergin/sync/schemas.py | 1 + 3 files changed, 10 insertions(+) diff --git a/server/mergin/sync/models.py b/server/mergin/sync/models.py index de81676a..ff1aa137 100644 --- a/server/mergin/sync/models.py +++ b/server/mergin/sync/models.py @@ -360,6 +360,7 @@ class ProjectAccessDetail: username: str name: Optional[str] project_permission: str + project_role: Optional[ProjectRole] type: str diff --git a/server/mergin/sync/private_api.yaml b/server/mergin/sync/private_api.yaml index 4160ed07..40af2e9b 100644 --- a/server/mergin/sync/private_api.yaml +++ b/server/mergin/sync/private_api.yaml @@ -594,6 +594,14 @@ components: - writer - editor - reader + project_role: + type: string + nullable: true + enum: + - owner + - writer + - editor + - reader invitation: description: Present only for type `invitation` type: object diff --git a/server/mergin/sync/schemas.py b/server/mergin/sync/schemas.py index c782c5ca..2faaae13 100644 --- a/server/mergin/sync/schemas.py +++ b/server/mergin/sync/schemas.py @@ -362,6 +362,7 @@ class ProjectAccessDetailSchema(Schema): username = fields.String() name = fields.String() project_permission = fields.String() + project_role = fields.String() type = fields.String() invitation = fields.Nested(ProjectInvitationAccessSchema()) From 4f3c23d6a847c2074b2d8bc4e1dc6ac0887c1168 Mon Sep 17 00:00:00 2001 From: "marcel.kocisek" Date: Wed, 18 Dec 2024 16:54:37 +0100 Subject: [PATCH 05/14] Cleanup of none ProjecRole --- .../lib/src/common/permission_utils.ts | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/web-app/packages/lib/src/common/permission_utils.ts b/web-app/packages/lib/src/common/permission_utils.ts index e810b2db..97179ffd 100644 --- a/web-app/packages/lib/src/common/permission_utils.ts +++ b/web-app/packages/lib/src/common/permission_utils.ts @@ -16,7 +16,6 @@ export enum WorkspaceRole { } export enum ProjectRole { - none, reader, editor, writer, @@ -37,9 +36,10 @@ export type WorkspaceRoleName = | 'admin' | 'owner' -export type ProjectRoleName = - | Extract - | 'none' +export type ProjectRoleName = Extract< + WorkspaceRoleName, + 'reader' | 'editor' | 'writer' | 'owner' +> export type ProjectPermissionName = 'owner' | 'write' | 'edit' | 'read' @@ -63,7 +63,6 @@ export const USER_ROLE_BY_NAME: Record = { } export const PROJECT_ROLE_NAME_BY_ROLE: Record = { - [ProjectRole.none]: 'none', [ProjectRole.reader]: 'reader', [ProjectRole.editor]: 'editor', [ProjectRole.writer]: 'writer', @@ -71,7 +70,6 @@ export const PROJECT_ROLE_NAME_BY_ROLE: Record = { } export const PROJECT_ROLE_BY_NAME: Record = { - none: ProjectRole.none, reader: ProjectRole.reader, editor: ProjectRole.editor, writer: ProjectRole.writer, @@ -191,8 +189,7 @@ export function getProjectAccessKeyByRoleName( owner: 'ownersnames', writer: 'writersnames', editor: 'editorsnames', - reader: 'readersnames', - none: undefined + reader: 'readersnames' } return mapper[roleName] } @@ -200,12 +197,11 @@ export function getProjectAccessKeyByRoleName( export function getProjectPermissionByRoleName( roleName: ProjectRoleName ): ProjectPermissionName { - const mapper: Record = { + const mapper: Record = { owner: 'owner', writer: 'write', editor: 'edit', - reader: 'read', - none: undefined + reader: 'read' } return mapper[roleName] } From 67f146ed96fc368241c4f23b232168bbbc6e7076 Mon Sep 17 00:00:00 2001 From: "marcel.kocisek" Date: Wed, 18 Dec 2024 16:55:32 +0100 Subject: [PATCH 06/14] Updated: - remove project collaborator/update project collaboratr methods in store.ts - replace for access methods --- .../modules/admin/views/AccountDetailView.vue | 3 +- .../components/AccessRequestTableTemplate.vue | 4 +- .../components/ProjectAccessRequests.vue | 4 +- .../components/ProjectMembersTable.vue | 2 +- .../components/ProjectsTableDataLoader.vue | 7 ---- .../lib/src/modules/project/projectApi.ts | 35 ++++++++++------ .../packages/lib/src/modules/project/store.ts | 32 +++++++++------ .../packages/lib/src/modules/project/types.ts | 41 +++++++++++++++---- 8 files changed, 82 insertions(+), 46 deletions(-) diff --git a/web-app/packages/admin-lib/src/modules/admin/views/AccountDetailView.vue b/web-app/packages/admin-lib/src/modules/admin/views/AccountDetailView.vue index 86b9cd2b..9032e85b 100644 --- a/web-app/packages/admin-lib/src/modules/admin/views/AccountDetailView.vue +++ b/web-app/packages/admin-lib/src/modules/admin/views/AccountDetailView.vue @@ -215,7 +215,8 @@ const changeStatusDialog = () => { await adminStore.updateUser({ username: user.value.username, data: { - active: !user.value.active + active: !user.value.active, + is_admin: user.value.is_admin } }) } diff --git a/web-app/packages/lib/src/modules/project/components/AccessRequestTableTemplate.vue b/web-app/packages/lib/src/modules/project/components/AccessRequestTableTemplate.vue index 91eb6f22..e0e813c2 100644 --- a/web-app/packages/lib/src/modules/project/components/AccessRequestTableTemplate.vue +++ b/web-app/packages/lib/src/modules/project/components/AccessRequestTableTemplate.vue @@ -187,7 +187,7 @@ export default defineComponent({ await this.acceptProjectAccessRequest({ data, itemId: request.id, - namespace: this.namespace + workspace: this.namespace }) await this.updatePaginationOrFetch() } catch (err) { @@ -200,7 +200,7 @@ export default defineComponent({ async cancelRequest(request) { await this.cancelProjectAccessRequest({ itemId: request.id, - namespace: this.namespace + workspace: this.namespace }) await this.updatePaginationOrFetch() }, diff --git a/web-app/packages/lib/src/modules/project/components/ProjectAccessRequests.vue b/web-app/packages/lib/src/modules/project/components/ProjectAccessRequests.vue index 80d0eb3e..bc7af479 100644 --- a/web-app/packages/lib/src/modules/project/components/ProjectAccessRequests.vue +++ b/web-app/packages/lib/src/modules/project/components/ProjectAccessRequests.vue @@ -158,7 +158,7 @@ export default defineComponent({ await this.acceptProjectAccessRequest({ data, itemId: request.id, - namespace: this.project.namespace + workspace: this.project.namespace }) await this.updatePaginationOrFetch() } catch (err) { @@ -173,7 +173,7 @@ export default defineComponent({ async cancelRequest(request) { await this.cancelProjectAccessRequest({ itemId: request.id, - namespace: this.project.namespace + workspace: this.project.namespace }) await this.updatePaginationOrFetch() }, diff --git a/web-app/packages/lib/src/modules/project/components/ProjectMembersTable.vue b/web-app/packages/lib/src/modules/project/components/ProjectMembersTable.vue index f96f9e6d..95b0edaf 100644 --- a/web-app/packages/lib/src/modules/project/components/ProjectMembersTable.vue +++ b/web-app/packages/lib/src/modules/project/components/ProjectMembersTable.vue @@ -163,7 +163,7 @@ function removeMember(item: ProjectAccessDetail) { function roleUpdate(item: ProjectAccessDetail, value: ProjectRoleName) { projectStore.updateProjectAccess({ projectId: projectStore.project.id, - userId: item.id, + access: item, data: { role: value } }) } diff --git a/web-app/packages/lib/src/modules/project/components/ProjectsTableDataLoader.vue b/web-app/packages/lib/src/modules/project/components/ProjectsTableDataLoader.vue index 04229c27..9e79484b 100644 --- a/web-app/packages/lib/src/modules/project/components/ProjectsTableDataLoader.vue +++ b/web-app/packages/lib/src/modules/project/components/ProjectsTableDataLoader.vue @@ -60,10 +60,6 @@ export default defineComponent({ default: false }, namespace: String, - asAdmin: { - type: Boolean, - default: false - }, public: { type: Boolean, default: true @@ -136,9 +132,6 @@ export default defineComponent({ if (projectGridState.namespace) { params.only_namespace = projectGridState.namespace } - if (this.asAdmin) { - params.as_admin = true - } if (!this.public) { params.public = false } diff --git a/web-app/packages/lib/src/modules/project/projectApi.ts b/web-app/packages/lib/src/modules/project/projectApi.ts index 67ff0e4e..76f4d832 100644 --- a/web-app/packages/lib/src/modules/project/projectApi.ts +++ b/web-app/packages/lib/src/modules/project/projectApi.ts @@ -25,8 +25,10 @@ import { ProjectAccessDetail, ProjectAccess, ProjectVersionFileChange, - UpdateProjectPayload, - UpdatePublicFlagParams + UpdateProjectCollaboratorPayload, + UpdatePublicFlagParams, + ProjectCollaborator, + AddProjectCollaboratorPayload } from '@/modules/project/types' export const ProjectApi = { @@ -174,17 +176,28 @@ export const ProjectApi = { ) }, - async updateProjectAccess( + async addProjectCollaborator( + id: string, + data: AddProjectCollaboratorPayload + ): Promise> { + return ProjectModule.httpService.post( + `/v2/projects/${id}/collaborators`, + data, + { + validateStatus + } + ) + }, + + async updateProjectCollaborator( id: string, userId: number, - data: UpdateProjectPayload, - withRetry?: boolean - ): Promise> { + data: UpdateProjectCollaboratorPayload + ): Promise> { return ProjectModule.httpService.patch( `/v2/projects/${id}/collaborators/${userId}`, data, { - ...(withRetry ? getDefaultRetryOptions() : {}), validateStatus } ) @@ -200,15 +213,13 @@ export const ProjectApi = { }) }, - async removeProjectAccess( + async removeProjectCollaborator( id: string, - userId: number, - withRetry?: boolean - ): Promise> { + userId: number + ): Promise> { return ProjectModule.httpService.delete( `/v2/projects/${id}/collaborators/${userId}`, { - ...(withRetry ? getDefaultRetryOptions() : {}), validateStatus } ) diff --git a/web-app/packages/lib/src/modules/project/store.ts b/web-app/packages/lib/src/modules/project/store.ts index d52baeae..011b33c5 100644 --- a/web-app/packages/lib/src/modules/project/store.ts +++ b/web-app/packages/lib/src/modules/project/store.ts @@ -43,7 +43,7 @@ import { ProjectAccessDetail, ProjectVersionFileChange, ProjectVersionListItem, - UpdateProjectPayload, + UpdateProjectCollaboratorPayload, UpdatePublicFlagParams } from '@/modules/project/types' import { useUserStore } from '@/modules/user/store' @@ -759,12 +759,11 @@ export const useProjectStore = defineStore('projectModule', { this.accessLoading = true const notificationStore = useNotificationStore() try { - const response = await ProjectApi.removeProjectAccess( + await ProjectApi.removeProjectCollaborator( this.project.id, - item.id + Number(item.id) ) this.access = this.access.filter((access) => access.id !== item.id) - this.project.access = response.data } catch { notificationStore.error({ text: `Failed to update project access for user ${item.username}` @@ -782,23 +781,30 @@ export const useProjectStore = defineStore('projectModule', { */ async updateProjectAccess(payload: { projectId: string - userId: number - data: UpdateProjectPayload + access: ProjectAccessDetail + data: UpdateProjectCollaboratorPayload }) { this.accessLoading = true try { - const response = await ProjectApi.updateProjectAccess( - payload.projectId, - payload.userId, - payload.data - ) + if (!payload.access.project_role) { + await ProjectApi.addProjectCollaborator(payload.projectId, { + ...payload.data, + username: payload.access.username + }) + } else { + await ProjectApi.updateProjectCollaborator( + payload.projectId, + Number(payload.access.id), + payload.data + ) + } this.access = this.access.map((access) => { - if (access.id === payload.userId) { + if (access.id === payload.access.id) { access.project_permission = payload.data.role + access.project_role = payload.data.role } return access }) - this.project.access = response.data } catch (err) { this.handleProjectAccessError(err, 'Failed to update project access') } finally { diff --git a/web-app/packages/lib/src/modules/project/types.ts b/web-app/packages/lib/src/modules/project/types.ts index e8c39aaa..025ccdd8 100644 --- a/web-app/packages/lib/src/modules/project/types.ts +++ b/web-app/packages/lib/src/modules/project/types.ts @@ -5,7 +5,8 @@ /* eslint-disable camelcase */ import { ProjectRoleName, - ProjectPermissionName + ProjectPermissionName, + WorkspaceRoleName } from '@/common/permission_utils' import { PaginatedRequestParamsApi, @@ -184,12 +185,12 @@ export interface GetAccessRequestsPayload extends GetUserAccessRequestsPayload { export interface AcceptProjectAccessRequestPayload { itemId: number data: AcceptProjectAccessRequestData - namespace?: string + workspace?: string } export interface CancelProjectAccessRequestPayload { itemId: number - namespace: string + workspace: string } export interface CreateProjectParams { @@ -292,7 +293,12 @@ export type EnhancedProjectDetail = ProjectDetail & { path: string } -export interface UpdateProjectPayload { +export interface AddProjectCollaboratorPayload { + role: ProjectRoleName + username: string +} + +export interface UpdateProjectCollaboratorPayload { role: ProjectRoleName } @@ -341,11 +347,30 @@ export interface ProjectVersionFileChange { export type ErrorCodes = 'UpdateProjectAccessError' +export type ProjectAccessDetailType = 'invitation' | 'member' + export interface ProjectAccessDetail { + id: number | string + type: ProjectAccessDetailType + role: WorkspaceRoleName + name?: string + email: string + username?: string + project_permission?: ProjectRoleName + project_role?: ProjectRoleName | null + invitation?: { + expires_at: string + projects?: { + ids: string[] + permissions: ProjectPermissionName + } + } +} + +export interface ProjectCollaborator { id: number - type: 'member' + usernaname: string email: string - username: string - project_permission: ProjectRoleName - name: string + workspace_role: WorkspaceRoleName + project_role: ProjectRoleName } From 13e68b413d5397b20ebf0691eb3505638f489657 Mon Sep 17 00:00:00 2001 From: "marcel.kocisek" Date: Wed, 18 Dec 2024 17:51:38 +0100 Subject: [PATCH 07/14] Fix tests for project_role --- server/mergin/sync/private_api_controller.py | 1 - server/mergin/sync/workspace.py | 9 ++++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/server/mergin/sync/private_api_controller.py b/server/mergin/sync/private_api_controller.py index 616ac69d..31d3e2af 100644 --- a/server/mergin/sync/private_api_controller.py +++ b/server/mergin/sync/private_api_controller.py @@ -26,7 +26,6 @@ AdminProjectSchema, ProjectAccessSchema, ProjectAccessDetailSchema, - ProjectVersionListSchema, ) from .permissions import ( require_project_by_uuid, diff --git a/server/mergin/sync/workspace.py b/server/mergin/sync/workspace.py index 8cc0a27a..3f0b5eec 100644 --- a/server/mergin/sync/workspace.py +++ b/server/mergin/sync/workspace.py @@ -323,17 +323,19 @@ def project_access(self, project: Project) -> List[ProjectAccessDetail]: direct_members_ids = [u.user_id for u in project.project_users] users = User.query.filter(User.active.is_(True)).order_by(User.email) - direct_members = users.filter(User.id.in_(direct_members_ids)).all() + direct_members: list[User] = users.filter(User.id.in_(direct_members_ids)).all() for dm in direct_members: - project_role = ProjectPermissions.get_user_project_role(project, dm) + project_permission = ProjectPermissions.get_user_project_role(project, dm) + project_role = project.get_role(dm.id) member = ProjectAccessDetail( id=dm.id, username=dm.username, role=ws.get_user_role(dm).value, name=dm.profile.name(), email=dm.email, - project_permission=project_role and project_role.value, + project_permission=project_permission and project_permission.value, + project_role=project_role.value if project_role else None, type="member", ) result.append(member) @@ -347,6 +349,7 @@ def project_access(self, project: Project) -> List[ProjectAccessDetail]: email=gm.email, role=global_role, project_permission=global_role, + project_role=None, type="member", ) result.append(member) From e61f7b5f4cea1c1852e77e3d9194ffb81a23588f Mon Sep 17 00:00:00 2001 From: "marcel.kocisek" Date: Thu, 19 Dec 2024 13:45:02 +0100 Subject: [PATCH 08/14] Address comments @varmar05 - renaming attributes --- server/mergin/sync/models.py | 2 +- server/mergin/sync/private_api.yaml | 48 +++++++++---------- server/mergin/sync/schemas.py | 2 +- server/mergin/sync/workspace.py | 6 +-- .../mergin/tests/test_private_project_api.py | 28 +++++------ .../components/ProjectMembersTable.vue | 2 +- .../packages/lib/src/modules/project/store.ts | 2 +- .../packages/lib/src/modules/project/types.ts | 4 +- 8 files changed, 47 insertions(+), 47 deletions(-) diff --git a/server/mergin/sync/models.py b/server/mergin/sync/models.py index ff1aa137..cccd7b43 100644 --- a/server/mergin/sync/models.py +++ b/server/mergin/sync/models.py @@ -359,7 +359,7 @@ class ProjectAccessDetail: role: str username: str name: Optional[str] - project_permission: str + workspace_role: str project_role: Optional[ProjectRole] type: str diff --git a/server/mergin/sync/private_api.yaml b/server/mergin/sync/private_api.yaml index 40af2e9b..0116cd65 100644 --- a/server/mergin/sync/private_api.yaml +++ b/server/mergin/sync/private_api.yaml @@ -551,8 +551,7 @@ components: - id - type - email - - project_permission - - role + - workspace_role properties: id: description: User/Invitation (uu)id @@ -569,16 +568,9 @@ components: type: string format: email example: john.doe@example.com - role: + workspace_role: description: Workspace role - type: string - enum: - - owner - - admin - - writer - - editor - - reader - - guest + $ref: "#/components/schemas/WorkspaceRole" username: description: Present only for type `member` type: string @@ -587,21 +579,13 @@ components: description: Present only for type `member` type: string example: John Doe - project_permission: - type: string - enum: - - owner - - writer - - editor - - reader + role: + description: Project role defined as combination of project and workspace roles + $ref: "#/components/schemas/ProjectRole" project_role: - type: string nullable: true - enum: - - owner - - writer - - editor - - reader + description: Project role defined in database, not calculated version + $ref: "#/components/schemas/ProjectRole" invitation: description: Present only for type `invitation` type: object @@ -666,3 +650,19 @@ components: items: type: integer example: [1] + WorkspaceRole: + type: string + enum: + - owner + - admin + - writer + - editor + - reader + - guest + ProjectRole: + type: string + enum: + - owner + - writer + - editor + - reader diff --git a/server/mergin/sync/schemas.py b/server/mergin/sync/schemas.py index 2faaae13..c282b56c 100644 --- a/server/mergin/sync/schemas.py +++ b/server/mergin/sync/schemas.py @@ -361,7 +361,7 @@ class ProjectAccessDetailSchema(Schema): role = fields.String() username = fields.String() name = fields.String() - project_permission = fields.String() + workspace_role = fields.String() project_role = fields.String() type = fields.String() invitation = fields.Nested(ProjectInvitationAccessSchema()) diff --git a/server/mergin/sync/workspace.py b/server/mergin/sync/workspace.py index 3f0b5eec..25b8e6fe 100644 --- a/server/mergin/sync/workspace.py +++ b/server/mergin/sync/workspace.py @@ -331,10 +331,10 @@ def project_access(self, project: Project) -> List[ProjectAccessDetail]: member = ProjectAccessDetail( id=dm.id, username=dm.username, - role=ws.get_user_role(dm).value, + workspace_role=ws.get_user_role(dm).value, name=dm.profile.name(), email=dm.email, - project_permission=project_permission and project_permission.value, + role=project_permission and project_permission.value, project_role=project_role.value if project_role else None, type="member", ) @@ -347,8 +347,8 @@ def project_access(self, project: Project) -> List[ProjectAccessDetail]: username=gm.username, name=gm.profile.name(), email=gm.email, + workspace_role=global_role, role=global_role, - project_permission=global_role, project_role=None, type="member", ) diff --git a/server/mergin/tests/test_private_project_api.py b/server/mergin/tests/test_private_project_api.py index c8aabd9e..f475255f 100644 --- a/server/mergin/tests/test_private_project_api.py +++ b/server/mergin/tests/test_private_project_api.py @@ -490,7 +490,7 @@ def test_get_project_access(client): resp = client.get(url) assert resp.status_code == 200 assert len(resp.json) == 1 - assert resp.json[0]["project_permission"] == "owner" + assert resp.json[0]["role"] == "owner" project.set_role(users[0].id, ProjectRole.OWNER) project.set_role(users[1].id, ProjectRole.WRITER) project.set_role(users[2].id, ProjectRole.READER) @@ -498,9 +498,9 @@ def test_get_project_access(client): resp = client.get(url) assert resp.status_code == 200 assert len(resp.json) == 4 - assert sum(map(lambda x: int(x["project_permission"] == "owner"), resp.json)) == 2 - assert sum(map(lambda x: int(x["project_permission"] == "writer"), resp.json)) == 1 - assert sum(map(lambda x: int(x["project_permission"] == "reader"), resp.json)) == 1 + assert sum(map(lambda x: int(x["role"] == "owner"), resp.json)) == 2 + assert sum(map(lambda x: int(x["role"] == "writer"), resp.json)) == 1 + assert sum(map(lambda x: int(x["role"] == "reader"), resp.json)) == 1 # user3 does not have access to the project assert not any(users[3].email == access["email"] for access in resp.json) assert any(users[2].email == access["email"] for access in resp.json) @@ -508,27 +508,27 @@ def test_get_project_access(client): resp = client.get(url) assert resp.status_code == 200 assert len(resp.json) == 6 - assert sum(map(lambda x: int(x["project_permission"] == "owner"), resp.json)) == 2 - assert sum(map(lambda x: int(x["project_permission"] == "writer"), resp.json)) == 1 - assert sum(map(lambda x: int(x["project_permission"] == "reader"), resp.json)) == 3 + assert sum(map(lambda x: int(x["role"] == "owner"), resp.json)) == 2 + assert sum(map(lambda x: int(x["role"] == "writer"), resp.json)) == 1 + assert sum(map(lambda x: int(x["role"] == "reader"), resp.json)) == 3 Configuration.GLOBAL_WRITE = True resp = client.get(url) assert resp.status_code == 200 assert len(resp.json) == 6 - assert sum(map(lambda x: int(x["project_permission"] == "owner"), resp.json)) == 2 - assert sum(map(lambda x: int(x["project_permission"] == "writer"), resp.json)) == 4 - assert sum(map(lambda x: int(x["project_permission"] == "reader"), resp.json)) == 0 + assert sum(map(lambda x: int(x["role"] == "owner"), resp.json)) == 2 + assert sum(map(lambda x: int(x["role"] == "writer"), resp.json)) == 4 + assert sum(map(lambda x: int(x["role"] == "reader"), resp.json)) == 0 Configuration.GLOBAL_ADMIN = True resp = client.get(url) assert resp.status_code == 200 assert len(resp.json) == 6 - assert sum(map(lambda x: int(x["project_permission"] == "owner"), resp.json)) == 6 - assert sum(map(lambda x: int(x["project_permission"] == "writer"), resp.json)) == 0 - assert sum(map(lambda x: int(x["project_permission"] == "reader"), resp.json)) == 0 + assert sum(map(lambda x: int(x["role"] == "owner"), resp.json)) == 6 + assert sum(map(lambda x: int(x["role"] == "writer"), resp.json)) == 0 + assert sum(map(lambda x: int(x["role"] == "reader"), resp.json)) == 0 # pretend a user was deleted to test that api can handle it users[3].inactivate() users[3].anonymize() resp = client.get(url) assert resp.status_code == 200 assert len(resp.json) == 5 - assert sum(map(lambda x: int(x["project_permission"] == "owner"), resp.json)) == 5 + assert sum(map(lambda x: int(x["role"] == "owner"), resp.json)) == 5 diff --git a/web-app/packages/lib/src/modules/project/components/ProjectMembersTable.vue b/web-app/packages/lib/src/modules/project/components/ProjectMembersTable.vue index 95b0edaf..6f066f2b 100644 --- a/web-app/packages/lib/src/modules/project/components/ProjectMembersTable.vue +++ b/web-app/packages/lib/src/modules/project/components/ProjectMembersTable.vue @@ -71,7 +71,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-MerginMaps-Commercial