From f8dd8cecc77b69a4fddb823b3cdf289fa34327ee Mon Sep 17 00:00:00 2001 From: Florin Tudor Date: Mon, 2 Dec 2024 11:57:59 +0100 Subject: [PATCH 01/19] fix(UserAccess): Consistent interface when no data/permissions exist for project roots page --- src/hooks/useUserProjectPermissions.ts | 16 +++++++ .../ProjectManagerPage/ProjectManagerPage.jsx | 2 +- src/pages/ProjectManagerPage/ProjectRoots.jsx | 43 +++++++++++++------ 3 files changed, 48 insertions(+), 13 deletions(-) diff --git a/src/hooks/useUserProjectPermissions.ts b/src/hooks/useUserProjectPermissions.ts index e56268884..233863127 100644 --- a/src/hooks/useUserProjectPermissions.ts +++ b/src/hooks/useUserProjectPermissions.ts @@ -104,6 +104,22 @@ class UserPermissions { return this.canEdit(UserPermissionsEntity.anatomy, projectName) } + assignedToProject(projectName: string): boolean { + if (!this.permissions) { + return false + } + + if (this.hasElevatedPrivileges || !this.projectSettingsAreEnabled()) { + return true + } + + if (this.permissions.projects[projectName] !== undefined) { + return true + } + + return false + } + canViewSettings(projectName?: string): boolean { if (projectName) { return this.canView(UserPermissionsEntity.settings, projectName) diff --git a/src/pages/ProjectManagerPage/ProjectManagerPage.jsx b/src/pages/ProjectManagerPage/ProjectManagerPage.jsx index 94f645e83..0f97e9ce0 100644 --- a/src/pages/ProjectManagerPage/ProjectManagerPage.jsx +++ b/src/pages/ProjectManagerPage/ProjectManagerPage.jsx @@ -196,7 +196,7 @@ const ProjectManagerPage = () => { {module === 'projectSettings' && } {module === 'siteSettings' && } {module === 'userSettings' && } - {module === 'roots' && } + {module === 'roots' && } {module === 'teams' && } {module === 'permissions' && } diff --git a/src/pages/ProjectManagerPage/ProjectRoots.jsx b/src/pages/ProjectManagerPage/ProjectRoots.jsx index 2f9f287aa..5945bcd98 100644 --- a/src/pages/ProjectManagerPage/ProjectRoots.jsx +++ b/src/pages/ProjectManagerPage/ProjectRoots.jsx @@ -12,6 +12,8 @@ import { } from '@ynput/ayon-react-components' import ProjectManagerPageLayout from './ProjectManagerPageLayout' import { toast } from 'react-toastify' +import EmptyPlaceholder from '@components/EmptyPlaceholder/EmptyPlaceholder' +import LoadingPage from '@pages/LoadingPage' const ProjectRootForm = ({ projectName, siteName, siteId, roots }) => { const [setCustomRoots, { isLoading }] = useSetCustomRootsMutation() @@ -69,13 +71,22 @@ const ProjectRootForm = ({ projectName, siteName, siteId, roots }) => { ) } -const ProjectRoots = ({ projectName, projectList }) => { +const ProjectRoots = ({ projectName, projectList, userPermissions }) => { + if (userPermissions && !userPermissions.assignedToProject(projectName)) { + return + + + } + const { data: project, - isLoading: projectLoading, + isLoading: isLoadingProject, isError, } = useGetProjectQuery({ projectName }, { skip: !projectName }) - const { data: rootOverrides, isLoading: overridesLoading } = useGetCustomRootsQuery({ + const { data: rootOverrides, isLoading: isLoadingOverrides } = useGetCustomRootsQuery({ projectName, }) @@ -102,18 +113,26 @@ const ProjectRoots = ({ projectName, projectList }) => { return forms }, [project, rootOverrides]) - if (projectLoading || overridesLoading) return <>loading - - if (isError) return <>error + if (isLoadingProject || isLoadingOverrides) { + return + } - // if (forms.length === 0) return

No sites configured

return ( -
- {forms.map((form) => ( - - ))} -
+ {(!isLoadingProject && isError) || !userPermissions.assignedToProject(projectName) ? ( + + ) : forms.length === 0 ? ( + + ) : ( +
+ {forms.map((form) => ( + + ))} +
+ )}
) } From 6d7be78ee63fe5f5fee94a20836ae555ad24a368 Mon Sep 17 00:00:00 2001 From: Florin Tudor Date: Mon, 2 Dec 2024 14:36:48 +0100 Subject: [PATCH 02/19] fix(UserAccess): Enabling "add new project" button for users that have create_project permissions --- src/api/rest/accessGroups.ts | 16 +++ src/api/rest/users.ts | 104 ++++++++++++++++-- src/hooks/useUserProjectPermissions.ts | 7 +- src/pages/ProjectManagerPage/ProjectRoots.jsx | 2 +- 4 files changed, 112 insertions(+), 17 deletions(-) diff --git a/src/api/rest/accessGroups.ts b/src/api/rest/accessGroups.ts index 320b78c9a..90744ff6a 100644 --- a/src/api/rest/accessGroups.ts +++ b/src/api/rest/accessGroups.ts @@ -65,6 +65,20 @@ export type ValidationError = { export type HttpValidationError = { detail?: ValidationError[] } +export type StudioManagementPermissions = { + /** Allow users to create new projects */ + create_projects?: boolean + /** Allow users to list all users in the studio */ + list_all_users?: boolean +} +export type ProjectManagementPermissions = { + /** Allow users to view or edit the project anatomy */ + anatomy?: number + /** Allow users to view or assign users to project access groups */ + access?: number + /** Allow users to view or edit the project addon settings */ + settings?: number +} export type FolderAccess = { access_type?: string /** The path of the folder to allow access to. Required for access_type 'hierarchy and 'children' */ @@ -83,6 +97,8 @@ export type EndpointsAccessList = { endpoints?: string[] } export type Permissions = { + studio?: StudioManagementPermissions + project?: ProjectManagementPermissions /** Whitelist folders a user can create */ create?: FolderAccessList /** Whitelist folders a user can read */ diff --git a/src/api/rest/users.ts b/src/api/rest/users.ts index f67fc01fd..abfd2b9e6 100644 --- a/src/api/rest/users.ts +++ b/src/api/rest/users.ts @@ -1,6 +1,20 @@ import { RestAPI as api } from '../../services/ayon' const injectedRtkApi = api.injectEndpoints({ endpoints: (build) => ({ + getUserStudioPermissions: build.query< + GetUserStudioPermissionsApiResponse, + GetUserStudioPermissionsApiArg + >({ + query: (queryArg) => ({ url: `/api/users/${queryArg.userName}/permissions` }), + }), + getUserProjectPermissions: build.query< + GetUserProjectPermissionsApiResponse, + GetUserProjectPermissionsApiArg + >({ + query: (queryArg) => ({ + url: `/api/users/${queryArg.userName}/permissions/${queryArg.projectName}`, + }), + }), getUser: build.query({ query: (queryArg) => ({ url: `/api/users/${queryArg.userName}` }), }), @@ -21,6 +35,17 @@ const injectedRtkApi = api.injectEndpoints({ overrideExisting: false, }) export { injectedRtkApi as api } +export type GetUserStudioPermissionsApiResponse = + /** status 200 Successful Response */ StudioPermissions +export type GetUserStudioPermissionsApiArg = { + userName: string +} +export type GetUserProjectPermissionsApiResponse = + /** status 200 Successful Response */ ProjectPermissions +export type GetUserProjectPermissionsApiArg = { + projectName: string + userName: string +} export type GetUserApiResponse = /** status 200 Successful Response */ | UserModel | { @@ -39,11 +64,78 @@ export type SetFrontendPreferencesApiArg = { userName: string patchData: object } +export type StudioManagementPermissions = { + /** Allow users to create new projects */ + create_projects?: boolean + /** Allow users to list all users in the studio */ + list_all_users?: boolean +} +export type StudioPermissions = { + studio?: StudioManagementPermissions +} +export type ErrorResponse = { + code: number + detail: string +} +export type ValidationError = { + loc: (string | number)[] + msg: string + type: string +} +export type HttpValidationError = { + detail?: ValidationError[] +} +export type ProjectManagementPermissions = { + /** Allow users to view or edit the project anatomy */ + anatomy?: number + /** Allow users to view or assign users to project access groups */ + access?: number + /** Allow users to view or edit the project addon settings */ + settings?: number +} +export type FolderAccess = { + access_type?: string + /** The path of the folder to allow access to. Required for access_type 'hierarchy and 'children' */ + path?: string +} +export type FolderAccessList = { + enabled?: boolean + access_list?: FolderAccess[] +} +export type AttributeAccessList = { + enabled?: boolean + attributes?: string[] +} +export type EndpointsAccessList = { + enabled?: boolean + endpoints?: string[] +} +export type ProjectPermissions = { + project?: ProjectManagementPermissions + /** Whitelist folders a user can create */ + create?: FolderAccessList + /** Whitelist folders a user can read */ + read?: FolderAccessList + /** Whitelist folders a user can update */ + update?: FolderAccessList + /** Whitelist folders a user can publish to */ + publish?: FolderAccessList + /** Whitelist folders a user can delete */ + delete?: FolderAccessList + /** Whitelist attributes a user can read */ + attrib_read?: AttributeAccessList + /** Whitelist attributes a user can write */ + attrib_write?: AttributeAccessList + /** Whitelist REST endpoints a user can access */ + endpoints?: EndpointsAccessList +} export type UserAttribModel = { fullName?: string email?: string avatarUrl?: string developerMode?: boolean + /** Do they live in Olympus */ + god?: boolean } export type UserModel = { /** Name is an unique id of the {entity_name} */ @@ -58,18 +150,6 @@ export type UserModel = { /** Time of last update */ updatedAt?: string } -export type ErrorResponse = { - code: number - detail: string -} -export type ValidationError = { - loc: (string | number)[] - msg: string - type: string -} -export type HttpValidationError = { - detail?: ValidationError[] -} export type LocationInfo = { country?: string subdivision?: string diff --git a/src/hooks/useUserProjectPermissions.ts b/src/hooks/useUserProjectPermissions.ts index 233863127..1cdae6ce0 100644 --- a/src/hooks/useUserProjectPermissions.ts +++ b/src/hooks/useUserProjectPermissions.ts @@ -1,3 +1,4 @@ +import { StudioManagementPermissions } from '@api/rest/users' import { useGetCurrentUserPermissionsQuery } from '@queries/permissions/getPermissions' type AllProjectsPremissions = { @@ -12,9 +13,7 @@ type AllProjectsPremissions = { } } } - studio: { - create_project: boolean - } + studio: StudioManagementPermissions user_level: 'user' | 'admin' } @@ -47,7 +46,7 @@ class UserPermissions { return false } - return (this.projectSettingsAreEnabled() && this.permissions.studio.create_project) || false + return (this.projectSettingsAreEnabled() && this.permissions.studio.create_projects) || false } getPermissionLevel(type: UserPermissionsEntity, projectName: string): PermissionLevel { diff --git a/src/pages/ProjectManagerPage/ProjectRoots.jsx b/src/pages/ProjectManagerPage/ProjectRoots.jsx index 5945bcd98..534a60918 100644 --- a/src/pages/ProjectManagerPage/ProjectRoots.jsx +++ b/src/pages/ProjectManagerPage/ProjectRoots.jsx @@ -125,7 +125,7 @@ const ProjectRoots = ({ projectName, projectList, userPermissions }) => { message="You went wrong while fetching project roots data" /> ) : forms.length === 0 ? ( - + ) : (
{forms.map((form) => ( From 08db31a34458f4eeeb25da4b91f17924761a651e Mon Sep 17 00:00:00 2001 From: Florin Tudor Date: Mon, 2 Dec 2024 17:11:32 +0100 Subject: [PATCH 03/19] fix(UserAccess): Marking no access project as disabled in project roots page --- src/pages/ProjectManagerPage/ProjectManagerPage.jsx | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/pages/ProjectManagerPage/ProjectManagerPage.jsx b/src/pages/ProjectManagerPage/ProjectManagerPage.jsx index 0f97e9ce0..272394277 100644 --- a/src/pages/ProjectManagerPage/ProjectManagerPage.jsx +++ b/src/pages/ProjectManagerPage/ProjectManagerPage.jsx @@ -180,6 +180,11 @@ const ProjectManagerPage = () => { const bPerm = userPermissions.canView(UserPermissionsEntity.settings, b) ? 1 : -1 return bPerm - aPerm } + if (module === 'roots') { + const aPerm = userPermissions.assignedToProject(a) ? 1 : -1 + const bPerm = userPermissions.assignedToProject(b) ? 1 : -1 + return bPerm - aPerm + } return 0 }} isActiveCallable={(projectName) => { @@ -189,6 +194,9 @@ const ProjectManagerPage = () => { if (module === 'projectSettings') { return userPermissions.canView(UserPermissionsEntity.settings, projectName) } + if (module === 'roots') { + return userPermissions.assignedToProject(projectName) + } return true }} > From 37de43fa36d4929b77d4814a7d16e6a4ee8ca264 Mon Sep 17 00:00:00 2001 From: Florin Tudor Date: Mon, 2 Dec 2024 23:42:44 +0100 Subject: [PATCH 04/19] fix(UserAccess): Redirecting to first accessible module when navigating to project settings --- .../AddonSettings/AddonSettings.jsx | 3 - src/hooks/useUserProjectPermissions.ts | 23 ++++ .../ProjectManagerPage/ProjectManagerPage.jsx | 108 ++++++++---------- src/pages/ProjectManagerPage/mappers.ts | 87 ++++++++++++++ 4 files changed, 158 insertions(+), 63 deletions(-) create mode 100644 src/pages/ProjectManagerPage/mappers.ts diff --git a/src/containers/AddonSettings/AddonSettings.jsx b/src/containers/AddonSettings/AddonSettings.jsx index db14afd55..9b3750491 100644 --- a/src/containers/AddonSettings/AddonSettings.jsx +++ b/src/containers/AddonSettings/AddonSettings.jsx @@ -657,7 +657,6 @@ const AddonSettings = ({ projectName, showSites = false, bypassPermissions = fal ) const onSelectAddon = (newSelection) => { - console.log('on select addon...', newSelection) setSelectedAddons(newSelection) setCurrentSelection(null) } @@ -679,8 +678,6 @@ const AddonSettings = ({ projectName, showSites = false, bypassPermissions = fal }) } - // console.log('selected addons: ', selectedAddons) - if (isLoading) { return } diff --git a/src/hooks/useUserProjectPermissions.ts b/src/hooks/useUserProjectPermissions.ts index 1cdae6ce0..51e8ed3b7 100644 --- a/src/hooks/useUserProjectPermissions.ts +++ b/src/hooks/useUserProjectPermissions.ts @@ -1,4 +1,5 @@ import { StudioManagementPermissions } from '@api/rest/users' +import { Module } from '@pages/ProjectManagerPage/mappers' import { useGetCurrentUserPermissionsQuery } from '@queries/permissions/getPermissions' type AllProjectsPremissions = { @@ -71,6 +72,28 @@ class UserPermissions { return this.permissions.projects[projectName]?.project[type] === PermissionLevel.readWrite } + canAccessModule(module: string, projectName: string): boolean { + if (module === Module.siteSettings) { + return true + } + + if (module === Module.projectSettings) { + return this.canView(UserPermissionsEntity.settings, projectName) + } + + if (module === Module.anatomy) { + return this.canView(UserPermissionsEntity.anatomy, projectName) + } + if (module === Module.userSettings) { + return this.canView(UserPermissionsEntity.users, projectName) + } + if (module === Module.roots) { + return this.assignedToProject(projectName) + } + + return true + } + canView(type: UserPermissionsEntity, projectName: string): boolean { if (!this.permissions) { return false diff --git a/src/pages/ProjectManagerPage/ProjectManagerPage.jsx b/src/pages/ProjectManagerPage/ProjectManagerPage.jsx index 272394277..0422fcd16 100644 --- a/src/pages/ProjectManagerPage/ProjectManagerPage.jsx +++ b/src/pages/ProjectManagerPage/ProjectManagerPage.jsx @@ -1,5 +1,5 @@ import { useState, useEffect, useMemo } from 'react' -import { useParams, useSearchParams } from 'react-router-dom' +import { useNavigate, useParams, useSearchParams } from 'react-router-dom' import { useSelector, useDispatch } from 'react-redux' import { StringParam, useQueryParam, withDefault } from 'use-query-params' @@ -19,6 +19,7 @@ import confirmDelete from '@helpers/confirmDelete' import useUserProjectPermissions, { UserPermissionsEntity } from '@hooks/useUserProjectPermissions' import ProjectUserAccess from './Users/ProjectUserAccess' import ProjectPermissions from './ProjectPermissions' +import { isActiveDecider, projectSorter, Module, ModuleList, ModulePath } from './mappers' const ProjectSettings = ({ projectList, projectManager, projectName }) => { return ( @@ -37,6 +38,7 @@ const SiteSettings = ({ projectList, projectManager, projectName }) => { const ProjectManagerPage = () => { // get is user from context + const navigate = useNavigate() const isUser = useSelector((state) => state.user.data.isUser) const projectName = useSelector((state) => state.project.name) const dispatch = useDispatch() @@ -51,7 +53,7 @@ const ProjectManagerPage = () => { withDefault(StringParam, projectName), ) - const { isLoading: userPermissionsLoading, permissions: userPermissions } = useUserProjectPermissions(isUser) + const { isLoading: isLoadingUserPermissions, permissions: userPermissions } = useUserProjectPermissions(isUser) // UPDATE DATA const [updateProject] = useUpdateProjectMutation() @@ -87,31 +89,31 @@ const ProjectManagerPage = () => { } const links = [] - if (!isUser || !userPermissionsLoading && userPermissions.projectSettingsAreEnabled()) { - if (userPermissions.canViewAny(UserPermissionsEntity.anatomy) || module === 'anatomy') { + if (!isUser || !isLoadingUserPermissions && userPermissions.projectSettingsAreEnabled()) { + if (userPermissions.canViewAny(UserPermissionsEntity.anatomy) || module === Module.anatomy) { links.push({ name: 'Anatomy', - path: '/manageProjects/anatomy', - module: 'anatomy', + path: ModulePath[Module.anatomy], + module: Module.anatomy, accessLevels: [], shortcut: 'A+A', }) } - if (userPermissions.canViewAny(UserPermissionsEntity.settings) || module === 'projectSettings') { + if (userPermissions.canViewAny(UserPermissionsEntity.settings) || module === Module.projectSettings) { links.push({ name: 'Project settings', - path: '/manageProjects/projectSettings', - module: 'projectSettings', + path: ModulePath[Module.projectSettings], + module: Module.projectSettings, accessLevels: [], shortcut: 'P+P', }) } - if (userPermissions.canViewAny(UserPermissionsEntity.users) || module === 'userSettings') { + if (userPermissions.canViewAny(UserPermissionsEntity.users) || module === Module.userSettings) { links.push({ name: 'Project access', - path: '/manageProjects/userSettings', - module: 'userSettings', + path: ModulePath[Module.userSettings], + module: Module.userSettings, accessLevels: [], shortcut: 'P+A', }) @@ -121,26 +123,26 @@ const ProjectManagerPage = () => { links.push( { name: 'Site settings', - path: '/manageProjects/siteSettings', - module: 'siteSettings', + path: ModulePath[Module.siteSettings], + module: Module.siteSettings, accessLevels: [], }, { name: 'Roots', - path: '/manageProjects/roots', - module: 'roots', + path: ModulePath[Module.roots], + module: Module.roots, accessLevels: [], }, { name: 'Teams', - path: '/manageProjects/teams', - module: 'teams', + path: ModulePath[Module.teams], + module: Module.teams, accessLevels: ['manager'], }, { name: 'Permissions', - path: '/manageProjects/permissions', - module: 'permissions', + path: ModulePath[Module.permisssions], + module: Module.permisssions, accessLevels: ['manager'], }, ) @@ -154,6 +156,23 @@ const ProjectManagerPage = () => { [links, selectedProject], ) + useEffect(() => { + if (isLoadingUserPermissions || selectedProject === null) { + return + } + + if (userPermissions.canAccessModule(module)) { + return + } + + for (const item of ModuleList) { + if (userPermissions.canAccessModule(item)) { + navigate(ModulePath[item]) + return + } + } + }, [isLoadingUserPermissions, selectedProject, module]) + return ( <> @@ -166,47 +185,16 @@ const ProjectManagerPage = () => { onNewProject={() => setShowNewProject(true)} onDeleteProject={handleDeleteProject} onActivateProject={handleActivateProject} - customSort={(a, b) => { - if (userPermissionsLoading) { - return 1 - } - if (module === 'anatomy') { - const aPerm = userPermissions.canView(UserPermissionsEntity.anatomy, a) ? 1 : -1 - const bPerm = userPermissions.canView(UserPermissionsEntity.anatomy, b) ? 1 : -1 - return bPerm - aPerm - } - if (module === 'projectSettings') { - const aPerm = userPermissions.canView(UserPermissionsEntity.settings, a) ? 1 : -1 - const bPerm = userPermissions.canView(UserPermissionsEntity.settings, b) ? 1 : -1 - return bPerm - aPerm - } - if (module === 'roots') { - const aPerm = userPermissions.assignedToProject(a) ? 1 : -1 - const bPerm = userPermissions.assignedToProject(b) ? 1 : -1 - return bPerm - aPerm - } - return 0 - }} - isActiveCallable={(projectName) => { - if (module === 'anatomy') { - return userPermissions.canView(UserPermissionsEntity.anatomy, projectName) - } - if (module === 'projectSettings') { - return userPermissions.canView(UserPermissionsEntity.settings, projectName) - } - if (module === 'roots') { - return userPermissions.assignedToProject(projectName) - } - return true - }} + customSort={projectSorter({ isLoadingUserPermissions, userPermissions, module })} + isActiveCallable={isActiveDecider({ userPermissions, projectName, module })} > - {module === 'anatomy' && } - {module === 'projectSettings' && } - {module === 'siteSettings' && } - {module === 'userSettings' && } - {module === 'roots' && } - {module === 'teams' && } - {module === 'permissions' && } + {module === Module.anatomy && } + {module === Module.projectSettings && } + {module === Module.siteSettings && } + {module === Module.userSettings && } + {module === Module.roots && } + {module === Module.teams && } + {module === Module.permisssions && } {showNewProject && ( diff --git a/src/pages/ProjectManagerPage/mappers.ts b/src/pages/ProjectManagerPage/mappers.ts new file mode 100644 index 000000000..e9687a05a --- /dev/null +++ b/src/pages/ProjectManagerPage/mappers.ts @@ -0,0 +1,87 @@ +import { UserPermissions, UserPermissionsEntity } from '@hooks/useUserProjectPermissions' + +export enum Module { + anatomy = 'anatomy', + projectSettings = 'projectSettings', + siteSettings = 'siteSettings', + userSettings = 'userSettings', + roots = 'roots', + teams = 'teams', + permisssions = 'permisssions', +} + +const ModuleList = [ + Module.anatomy, + Module.projectSettings, + Module.siteSettings, + Module.userSettings, + Module.roots, + Module.teams, + Module.permisssions, +] + +const ModulePath = { + [Module.anatomy]: '/manageProjects/anatomy', + [Module.projectSettings]: '/manageProjects/projectSettings', + [Module.siteSettings]: '/manageProjects/siteSettings', + [Module.userSettings]: '/manageProjects/userSettings', + [Module.roots]: '/manageProjects/roots', + [Module.teams]: '/manageProjects/teams', + [Module.permisssions]: '/manageProjects/permissions', +} + +const projectSorter = + ({ + userPermissions, + isLoadingUserPermissions, + module, + }: { + userPermissions: UserPermissions + isLoadingUserPermissions: boolean + module: string + }) => + (a: string, b: string) => { + if (isLoadingUserPermissions) { + return 1 + } + if (module === Module.anatomy) { + const aPerm = userPermissions.canView(UserPermissionsEntity.anatomy, a) ? 1 : -1 + const bPerm = userPermissions.canView(UserPermissionsEntity.anatomy, b) ? 1 : -1 + return bPerm - aPerm + } + if (module === Module.projectSettings) { + const aPerm = userPermissions.canView(UserPermissionsEntity.settings, a) ? 1 : -1 + const bPerm = userPermissions.canView(UserPermissionsEntity.settings, b) ? 1 : -1 + return bPerm - aPerm + } + if (module === Module.roots) { + const aPerm = userPermissions.assignedToProject(a) ? 1 : -1 + const bPerm = userPermissions.assignedToProject(b) ? 1 : -1 + return bPerm - aPerm + } + return 0 + } + +const isActiveDecider = + ({ + userPermissions, + module, + }: { + projectName: string + userPermissions: UserPermissions + module: string + }) => + (projectName: string) => { + if (module === Module.anatomy) { + return userPermissions.canView(UserPermissionsEntity.anatomy, projectName) + } + if (module === Module.projectSettings) { + return userPermissions.canView(UserPermissionsEntity.settings, projectName) + } + if (module === Module.roots) { + return userPermissions.assignedToProject(projectName) + } + return true + } + +export { projectSorter, isActiveDecider, ModuleList, ModulePath } From 34da586b9ca6ec85d67c24ba4233301575d4d9ac Mon Sep 17 00:00:00 2001 From: Florin Tudor Date: Tue, 3 Dec 2024 00:28:09 +0100 Subject: [PATCH 05/19] fix(UserAccess): Adding access groups counter to project list --- .../Users/AccessGroupsCell.tsx | 3 +- .../Users/ProjectUserAccess.tsx | 43 +------------------ .../Users/ProjectUserAccessProjectList.tsx | 21 +++++++++ .../ProjectManagerPage/Users/UserCell.tsx | 4 +- 4 files changed, 25 insertions(+), 46 deletions(-) diff --git a/src/pages/ProjectManagerPage/Users/AccessGroupsCell.tsx b/src/pages/ProjectManagerPage/Users/AccessGroupsCell.tsx index afc7adb46..b314aba27 100644 --- a/src/pages/ProjectManagerPage/Users/AccessGroupsCell.tsx +++ b/src/pages/ProjectManagerPage/Users/AccessGroupsCell.tsx @@ -3,7 +3,6 @@ import { $Any } from '@types' import * as Styled from './ProjectUserAccess.styled' import { Spacer } from '@ynput/ayon-react-components' -import { capitalizeFirstLetter } from '@helpers/string' type Props = { data: $Any @@ -29,7 +28,7 @@ export const AccessGroupsCell = ({ {data.assignedAccessGroups.map( (ag: { accessGroup: string; complete: boolean }, idx: number) => ( - {capitalizeFirstLetter(ag.accessGroup)} + {ag.accessGroup} {idx !== data.assignedAccessGroups.length - 1 ? ',' : ''} ), diff --git a/src/pages/ProjectManagerPage/Users/ProjectUserAccess.tsx b/src/pages/ProjectManagerPage/Users/ProjectUserAccess.tsx index 37193d2f6..ab8bbc72d 100644 --- a/src/pages/ProjectManagerPage/Users/ProjectUserAccess.tsx +++ b/src/pages/ProjectManagerPage/Users/ProjectUserAccess.tsx @@ -44,7 +44,6 @@ import LoadingPage from '@pages/LoadingPage' import { useQueryParam } from 'use-query-params' import { uuid } from 'short-uuid' import ProjectUserAccesAccessGroupPanel from './ProjectUserAccessAccessGroupPanel' -import { capitalizeFirstLetter } from '@helpers/string' const StyledButton = styled(Button)` .shortcut { @@ -263,22 +262,10 @@ const ProjectUserAccess = () => { accessGroup?: string actionedUsers: string[] } => { - console.log({ accessGroup, users, interactionType}) - - //button click - //key || global button click - //hover & click - - //no users -> user existing selection - let actionedUsers = selectedAccessGroupUsers?.users || [] let actionedAccessGroup = selectedAccessGroupUsers?.accessGroup - console.log('orig au: ', actionedUsers) - console.log('orig auag: ', actionedAccessGroup) if (interactionType == InteractionType.bulkButton) { - console.log('testing if hovered user exists and matching the selection...') - console.log('hu: ', hoveredUser) if (hoveredUser?.user && !actionedUsers.includes(hoveredUser.user)) { actionedUsers = [hoveredUser.user] actionedAccessGroup = hoveredUser.accessGroup @@ -287,11 +274,8 @@ const ProjectUserAccess = () => { users: [hoveredUser.user], }) } - // Nothing to do here ... for now! } else if (interactionType == InteractionType.button) { - // We know it's only 1 user if interaction is button click if (!actionedUsers.includes(users![0]) || accessGroup !== actionedAccessGroup) { - console.log('resetting selection, user/access group does not match') actionedUsers = users! actionedAccessGroup = accessGroup setSelectedAccessGroupUsers({ accessGroup: actionedAccessGroup, users: users! }) @@ -299,28 +283,7 @@ const ProjectUserAccess = () => { } setActionedUsers(actionedUsers) - console.log('returning: ', { accessGroup: actionedAccessGroup, actionedUsers }) return { accessGroup: actionedAccessGroup, actionedUsers } - /* - if (hoveredUser?.user && !actionedUsers.includes(hoveredUser.user)) { - actionedUsers = [hoveredUser.user] - setSelectedAccessGroupUsers({ - accessGroup: hoveredUser.accessGroup, - users: [hoveredUser.user], - }) - } - - if (!selectedAccessGroupUsers?.users && !hoveredUser?.user) { - return - } - if (hoveredUser?.user && !actionedUsers.includes(hoveredUser.user)) { - actionedUsers = [hoveredUser.user] - setSelectedAccessGroupUsers({ - accessGroup: hoveredUser.accessGroup, - users: [hoveredUser.user], - }) - } - */ } const onRemove = (accessGroup?: string) => async (users?: string[]) => { @@ -342,11 +305,7 @@ const ProjectUserAccess = () => { { key: 'a', action: () => { - console.log('handling a keypress...') - console.log('sagu: ', selectedAccessGroupUsers) - console.log('hu: ', hoveredUser) if (!selectedAccessGroupUsers?.users && !hoveredUser?.user) { - console.log('nothing to do ?!?!!?') return } @@ -425,7 +384,7 @@ const ProjectUserAccess = () => { return ( + { + const isActive = rowData.active + const hasPermissions = + userPermissions.canEdit(UserPermissionsEntity.users, rowData.name) || + userPermissions.canView(UserPermissionsEntity.users, rowData.name) + return ( + + 1 + + ) + }} + style={{ minWidth: 150 }} + /> ) diff --git a/src/pages/ProjectManagerPage/Users/UserCell.tsx b/src/pages/ProjectManagerPage/Users/UserCell.tsx index 321004c89..2dad76802 100644 --- a/src/pages/ProjectManagerPage/Users/UserCell.tsx +++ b/src/pages/ProjectManagerPage/Users/UserCell.tsx @@ -28,7 +28,7 @@ export const UserCell = ({ onAdd, onRemove, }: Props) => { - const { name, self, isMissing } = rowData + const { attrib, name, self, isMissing } = rowData return ( {/* @ts-ignore */} @@ -39,7 +39,7 @@ export const UserCell = ({ color: isMissing ? 'var(--color-hl-error)' : 'inherit', }} > - {name} + {attrib?.fullName || name} {!readOnly && showButtonsOnHover && (showAddButton || showAddMoreButton) && ( Date: Tue, 3 Dec 2024 08:34:22 +0100 Subject: [PATCH 06/19] fix(UserAccess): Reverting counter to project list - technical limitation --- .../Users/ProjectUserAccessProjectList.tsx | 21 ------------------- 1 file changed, 21 deletions(-) diff --git a/src/pages/ProjectManagerPage/Users/ProjectUserAccessProjectList.tsx b/src/pages/ProjectManagerPage/Users/ProjectUserAccessProjectList.tsx index 61d241df3..8be91377b 100644 --- a/src/pages/ProjectManagerPage/Users/ProjectUserAccessProjectList.tsx +++ b/src/pages/ProjectManagerPage/Users/ProjectUserAccessProjectList.tsx @@ -100,27 +100,6 @@ const ProjectUserAccessProjectList = ({ projects, isLoading, selection, userPerm }} style={{ minWidth: 150 }} /> - { - const isActive = rowData.active - const hasPermissions = - userPermissions.canEdit(UserPermissionsEntity.users, rowData.name) || - userPermissions.canView(UserPermissionsEntity.users, rowData.name) - return ( - - 1 - - ) - }} - style={{ minWidth: 150 }} - /> ) From 22d203d1bcbd1b529cfe92a2cb8faffa5414de39 Mon Sep 17 00:00:00 2001 From: Florin Tudor Date: Tue, 3 Dec 2024 15:37:43 +0100 Subject: [PATCH 07/19] fix(UserAccess): Fixed redirection loop --- src/hooks/useUserProjectPermissions.ts | 2 +- .../ProjectManagerPage/ProjectManagerPage.jsx | 28 ++++++++++++------- 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/src/hooks/useUserProjectPermissions.ts b/src/hooks/useUserProjectPermissions.ts index 51e8ed3b7..e836396f7 100644 --- a/src/hooks/useUserProjectPermissions.ts +++ b/src/hooks/useUserProjectPermissions.ts @@ -72,7 +72,7 @@ class UserPermissions { return this.permissions.projects[projectName]?.project[type] === PermissionLevel.readWrite } - canAccessModule(module: string, projectName: string): boolean { + canAccessModule({ module, projectName }: { module: string; projectName: string }): boolean { if (module === Module.siteSettings) { return true } diff --git a/src/pages/ProjectManagerPage/ProjectManagerPage.jsx b/src/pages/ProjectManagerPage/ProjectManagerPage.jsx index 0422fcd16..67bfa812c 100644 --- a/src/pages/ProjectManagerPage/ProjectManagerPage.jsx +++ b/src/pages/ProjectManagerPage/ProjectManagerPage.jsx @@ -20,6 +20,7 @@ import useUserProjectPermissions, { UserPermissionsEntity } from '@hooks/useUser import ProjectUserAccess from './Users/ProjectUserAccess' import ProjectPermissions from './ProjectPermissions' import { isActiveDecider, projectSorter, Module, ModuleList, ModulePath } from './mappers' +import { replaceQueryParams } from '@helpers/url' const ProjectSettings = ({ projectList, projectManager, projectName }) => { return ( @@ -31,7 +32,7 @@ const ProjectSettings = ({ projectList, projectManager, projectName }) => { const SiteSettings = ({ projectList, projectManager, projectName }) => { return ( - + ) } @@ -53,10 +54,13 @@ const ProjectManagerPage = () => { withDefault(StringParam, projectName), ) - const { isLoading: isLoadingUserPermissions, permissions: userPermissions } = useUserProjectPermissions(isUser) + const { isLoading: isLoadingUserPermissions, permissions: userPermissions } = + useUserProjectPermissions(isUser) // UPDATE DATA const [updateProject] = useUpdateProjectMutation() + // We only need the to redirect on the initial navigation, the following are considered user explicit + const [initialRedirectDone, setInitialRedirectDone] = useState(false) useEffect(() => { // Update project name in header @@ -89,7 +93,7 @@ const ProjectManagerPage = () => { } const links = [] - if (!isUser || !isLoadingUserPermissions && userPermissions.projectSettingsAreEnabled()) { + if (!isUser || (!isLoadingUserPermissions && userPermissions.projectSettingsAreEnabled())) { if (userPermissions.canViewAny(UserPermissionsEntity.anatomy) || module === Module.anatomy) { links.push({ name: 'Anatomy', @@ -100,7 +104,10 @@ const ProjectManagerPage = () => { }) } - if (userPermissions.canViewAny(UserPermissionsEntity.settings) || module === Module.projectSettings) { + if ( + userPermissions.canViewAny(UserPermissionsEntity.settings) || + module === Module.projectSettings + ) { links.push({ name: 'Project settings', path: ModulePath[Module.projectSettings], @@ -151,27 +158,28 @@ const ProjectManagerPage = () => { () => links.map((link) => ({ ...link, - path: link.path + (selectedProject ? `?project=${selectedProject}` : ''), + path: replaceQueryParams(link.path, selectedProject ? { project: selectedProject } : {}), })), [links, selectedProject], ) useEffect(() => { - if (isLoadingUserPermissions || selectedProject === null) { + if (isLoadingUserPermissions || selectedProject === null || !module || initialRedirectDone) { return } - if (userPermissions.canAccessModule(module)) { + if (userPermissions.canAccessModule({ module, projectName: selectedProject })) { return } for (const item of ModuleList) { - if (userPermissions.canAccessModule(item)) { - navigate(ModulePath[item]) + if (userPermissions.canAccessModule({ module: item, projectName: selectedProject })) { + setInitialRedirectDone(true) + navigate(replaceQueryParams(ModulePath[item], { project: selectedProject })) return } } - }, [isLoadingUserPermissions, selectedProject, module]) + }, [isLoadingUserPermissions, selectedProject, module, initialRedirectDone]) return ( <> From 35abf11b5709b8e76a6e6ce3204119bab5926dcb Mon Sep 17 00:00:00 2001 From: Florin Tudor Date: Tue, 3 Dec 2024 17:10:41 +0100 Subject: [PATCH 08/19] fix(UserAccess): Typo fixes, renamed project access paths,updated redirection logic to avoid refresh sideffects, disabled context menu for non active projects --- src/app.jsx | 9 ++++---- src/components/Menu/Menus/AppMenu.jsx | 2 +- .../AddonSettings/AddonSettings.jsx | 10 ++++----- src/containers/projectList.jsx | 4 ++++ src/context/shortcutsContext.jsx | 2 +- src/hooks/useUserProjectPermissions.ts | 2 +- .../ProjectManagerPage/ProjectAnatomy.jsx | 2 +- .../ProjectManagerPage/ProjectManagerPage.jsx | 22 ++++++------------- src/pages/ProjectManagerPage/ProjectRoots.jsx | 6 +---- src/pages/ProjectManagerPage/Users/mappers.ts | 2 +- src/pages/ProjectManagerPage/mappers.ts | 6 ++--- tests/fixtures/projectUserAccessGroupsPage.ts | 2 +- 12 files changed, 29 insertions(+), 40 deletions(-) diff --git a/src/app.jsx b/src/app.jsx index 8f0e12ac4..c67d9674b 100644 --- a/src/app.jsx +++ b/src/app.jsx @@ -186,11 +186,6 @@ const App = () => { exact element={} /> - } - /> { element={} /> + } + /> } diff --git a/src/components/Menu/Menus/AppMenu.jsx b/src/components/Menu/Menus/AppMenu.jsx index 48c4325c3..3a805fcdc 100644 --- a/src/components/Menu/Menus/AppMenu.jsx +++ b/src/components/Menu/Menus/AppMenu.jsx @@ -43,7 +43,7 @@ export const AppMenu = ({ user, ...props }) => { const items = [ { id: 'projectsManager', - link: '/manageProjects/projectSettings', + link: '/manageProjects', label: 'Projects Settings', icon: 'settings_applications', shortcut: 'P+P', diff --git a/src/containers/AddonSettings/AddonSettings.jsx b/src/containers/AddonSettings/AddonSettings.jsx index 9b3750491..b6de730ef 100644 --- a/src/containers/AddonSettings/AddonSettings.jsx +++ b/src/containers/AddonSettings/AddonSettings.jsx @@ -683,12 +683,10 @@ const AddonSettings = ({ projectName, showSites = false, bypassPermissions = fal } if (!bypassPermissions && !userPermissions.canViewSettings(projectName)) { - return ( - - ) + return } return ( diff --git a/src/containers/projectList.jsx b/src/containers/projectList.jsx index e6317a5c6..04b7558a4 100644 --- a/src/containers/projectList.jsx +++ b/src/containers/projectList.jsx @@ -366,6 +366,10 @@ const ProjectList = ({ // When right clicking on the already selected node, we don't want to change the selection const onContextMenu = (event) => { + const isActiveCallableValue = isActiveCallable ? isActiveCallable(event.data.name) : true + if (!isActiveCallableValue) { + return + } let newSelection = selection if (multiselect) { diff --git a/src/context/shortcutsContext.jsx b/src/context/shortcutsContext.jsx index a8b984268..8dc15da4b 100644 --- a/src/context/shortcutsContext.jsx +++ b/src/context/shortcutsContext.jsx @@ -36,7 +36,7 @@ function ShortcutsProvider(props) { // project settings { key: 'p+p', - action: () => navigate('/manageProjects/projectSettings?' + searchParams.toString()), + action: () => navigate('/manageProjects'), }, { key: 'p+a', diff --git a/src/hooks/useUserProjectPermissions.ts b/src/hooks/useUserProjectPermissions.ts index e836396f7..d45093b10 100644 --- a/src/hooks/useUserProjectPermissions.ts +++ b/src/hooks/useUserProjectPermissions.ts @@ -84,7 +84,7 @@ class UserPermissions { if (module === Module.anatomy) { return this.canView(UserPermissionsEntity.anatomy, projectName) } - if (module === Module.userSettings) { + if (module === Module.projectAccess) { return this.canView(UserPermissionsEntity.users, projectName) } if (module === Module.roots) { diff --git a/src/pages/ProjectManagerPage/ProjectAnatomy.jsx b/src/pages/ProjectManagerPage/ProjectAnatomy.jsx index 248555373..258f859ed 100644 --- a/src/pages/ProjectManagerPage/ProjectAnatomy.jsx +++ b/src/pages/ProjectManagerPage/ProjectAnatomy.jsx @@ -98,7 +98,7 @@ const ProjectAnatomy = ({ projectName, projectList }) => { ) : ( )} diff --git a/src/pages/ProjectManagerPage/ProjectManagerPage.jsx b/src/pages/ProjectManagerPage/ProjectManagerPage.jsx index 67bfa812c..5ed2a5885 100644 --- a/src/pages/ProjectManagerPage/ProjectManagerPage.jsx +++ b/src/pages/ProjectManagerPage/ProjectManagerPage.jsx @@ -38,10 +38,9 @@ const SiteSettings = ({ projectList, projectManager, projectName }) => { } const ProjectManagerPage = () => { - // get is user from context - const navigate = useNavigate() const isUser = useSelector((state) => state.user.data.isUser) const projectName = useSelector((state) => state.project.name) + const navigate = useNavigate() const dispatch = useDispatch() let { module } = useParams() @@ -59,8 +58,6 @@ const ProjectManagerPage = () => { // UPDATE DATA const [updateProject] = useUpdateProjectMutation() - // We only need the to redirect on the initial navigation, the following are considered user explicit - const [initialRedirectDone, setInitialRedirectDone] = useState(false) useEffect(() => { // Update project name in header @@ -116,11 +113,11 @@ const ProjectManagerPage = () => { shortcut: 'P+P', }) } - if (userPermissions.canViewAny(UserPermissionsEntity.users) || module === Module.userSettings) { + if (userPermissions.canViewAny(UserPermissionsEntity.users) || module === Module.projectAccess) { links.push({ name: 'Project access', - path: ModulePath[Module.userSettings], - module: Module.userSettings, + path: ModulePath[Module.projectAccess], + module: Module.projectAccess, accessLevels: [], shortcut: 'P+A', }) @@ -164,22 +161,17 @@ const ProjectManagerPage = () => { ) useEffect(() => { - if (isLoadingUserPermissions || selectedProject === null || !module || initialRedirectDone) { - return - } - - if (userPermissions.canAccessModule({ module, projectName: selectedProject })) { + if (isLoadingUserPermissions || module !== undefined) { return } for (const item of ModuleList) { if (userPermissions.canAccessModule({ module: item, projectName: selectedProject })) { - setInitialRedirectDone(true) navigate(replaceQueryParams(ModulePath[item], { project: selectedProject })) return } } - }, [isLoadingUserPermissions, selectedProject, module, initialRedirectDone]) + }, [isLoadingUserPermissions, selectedProject, module]) return ( <> @@ -199,7 +191,7 @@ const ProjectManagerPage = () => { {module === Module.anatomy && } {module === Module.projectSettings && } {module === Module.siteSettings && } - {module === Module.userSettings && } + {module === Module.projectAccess && } {module === Module.roots && } {module === Module.teams && } {module === Module.permisssions && } diff --git a/src/pages/ProjectManagerPage/ProjectRoots.jsx b/src/pages/ProjectManagerPage/ProjectRoots.jsx index 534a60918..c91ddccc9 100644 --- a/src/pages/ProjectManagerPage/ProjectRoots.jsx +++ b/src/pages/ProjectManagerPage/ProjectRoots.jsx @@ -76,7 +76,7 @@ const ProjectRoots = ({ projectName, projectList, userPermissions }) => { return } @@ -113,10 +113,6 @@ const ProjectRoots = ({ projectName, projectList, userPermissions }) => { return forms }, [project, rootOverrides]) - if (isLoadingProject || isLoadingOverrides) { - return - } - return ( {(!isLoadingProject && isError) || !userPermissions.assignedToProject(projectName) ? ( diff --git a/src/pages/ProjectManagerPage/Users/mappers.ts b/src/pages/ProjectManagerPage/Users/mappers.ts index 8242b224b..9ac0c1181 100644 --- a/src/pages/ProjectManagerPage/Users/mappers.ts +++ b/src/pages/ProjectManagerPage/Users/mappers.ts @@ -244,7 +244,7 @@ const getErrorInfo = ( return { icon: 'person', message: 'Missing permissions', - details: "You don't have permissions to manage this project's users", + details: "You don't have permission to manage this project's users", } } } diff --git a/src/pages/ProjectManagerPage/mappers.ts b/src/pages/ProjectManagerPage/mappers.ts index e9687a05a..5e8251f89 100644 --- a/src/pages/ProjectManagerPage/mappers.ts +++ b/src/pages/ProjectManagerPage/mappers.ts @@ -4,7 +4,7 @@ export enum Module { anatomy = 'anatomy', projectSettings = 'projectSettings', siteSettings = 'siteSettings', - userSettings = 'userSettings', + projectAccess = 'projectAccess', roots = 'roots', teams = 'teams', permisssions = 'permisssions', @@ -14,7 +14,7 @@ const ModuleList = [ Module.anatomy, Module.projectSettings, Module.siteSettings, - Module.userSettings, + Module.projectAccess, Module.roots, Module.teams, Module.permisssions, @@ -24,7 +24,7 @@ const ModulePath = { [Module.anatomy]: '/manageProjects/anatomy', [Module.projectSettings]: '/manageProjects/projectSettings', [Module.siteSettings]: '/manageProjects/siteSettings', - [Module.userSettings]: '/manageProjects/userSettings', + [Module.projectAccess]: '/manageProjects/projectAccess', [Module.roots]: '/manageProjects/roots', [Module.teams]: '/manageProjects/teams', [Module.permisssions]: '/manageProjects/permissions', diff --git a/tests/fixtures/projectUserAccessGroupsPage.ts b/tests/fixtures/projectUserAccessGroupsPage.ts index 74c37ce23..d2169d2e3 100644 --- a/tests/fixtures/projectUserAccessGroupsPage.ts +++ b/tests/fixtures/projectUserAccessGroupsPage.ts @@ -5,7 +5,7 @@ class ProjectUserAccessGroupsPage { constructor(public readonly page: Page, public readonly browserName: String) {} async goto(userName?: string) { - await this.page.goto(`/manageProjects/userSettings`) + await this.page.goto(`/manageProjects/projectAccess`) } async selectProject(project: string, navigationNeeded = false) { From eb8c15e9080f6d5ef8498e0cc1461553094b33e6 Mon Sep 17 00:00:00 2001 From: Florin Tudor Date: Wed, 4 Dec 2024 08:24:04 +0100 Subject: [PATCH 09/19] fix(UserAccess): Using same logic for project sorting & active decider in both roots and site settings --- src/pages/ProjectManagerPage/mappers.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/ProjectManagerPage/mappers.ts b/src/pages/ProjectManagerPage/mappers.ts index 5e8251f89..c3d30cf6f 100644 --- a/src/pages/ProjectManagerPage/mappers.ts +++ b/src/pages/ProjectManagerPage/mappers.ts @@ -54,7 +54,7 @@ const projectSorter = const bPerm = userPermissions.canView(UserPermissionsEntity.settings, b) ? 1 : -1 return bPerm - aPerm } - if (module === Module.roots) { + if ([Module.roots, Module.siteSettings].includes(module as Module)) { const aPerm = userPermissions.assignedToProject(a) ? 1 : -1 const bPerm = userPermissions.assignedToProject(b) ? 1 : -1 return bPerm - aPerm @@ -78,7 +78,7 @@ const isActiveDecider = if (module === Module.projectSettings) { return userPermissions.canView(UserPermissionsEntity.settings, projectName) } - if (module === Module.roots) { + if ([Module.roots, Module.siteSettings].includes(module as Module)) { return userPermissions.assignedToProject(projectName) } return true From e9f6be06c9d40c01b06a1444443768700430862f Mon Sep 17 00:00:00 2001 From: Florin Tudor Date: Wed, 4 Dec 2024 09:43:10 +0100 Subject: [PATCH 10/19] fix(UserAccess): Renaming permissions users -> as per api schema --- src/hooks/useUserProjectPermissions.ts | 19 +++++++------------ .../ProjectManagerPage/ProjectManagerPage.jsx | 2 +- .../Users/ProjectUserAccessProjectList.tsx | 12 ++++++------ src/pages/ProjectManagerPage/Users/mappers.ts | 6 +++--- 4 files changed, 17 insertions(+), 22 deletions(-) diff --git a/src/hooks/useUserProjectPermissions.ts b/src/hooks/useUserProjectPermissions.ts index d45093b10..acbc55734 100644 --- a/src/hooks/useUserProjectPermissions.ts +++ b/src/hooks/useUserProjectPermissions.ts @@ -1,17 +1,11 @@ -import { StudioManagementPermissions } from '@api/rest/users' +import { StudioManagementPermissions, ProjectManagementPermissions } from '@api/rest/users' import { Module } from '@pages/ProjectManagerPage/mappers' import { useGetCurrentUserPermissionsQuery } from '@queries/permissions/getPermissions' type AllProjectsPremissions = { projects: { [projectName: string]: { - project: { - anatomy: PermissionLevel - create: boolean - enabled: boolean - settings: PermissionLevel - users: PermissionLevel - } + project: ProjectManagementPermissions } } studio: StudioManagementPermissions @@ -25,7 +19,8 @@ enum PermissionLevel { } export enum UserPermissionsEntity { - users = 'users', + // TODO This needs to be in sync with ProjectManagementPermissions + access = 'access', anatomy = 'anatomy', settings = 'settings', } @@ -85,7 +80,7 @@ class UserPermissions { return this.canView(UserPermissionsEntity.anatomy, projectName) } if (module === Module.projectAccess) { - return this.canView(UserPermissionsEntity.users, projectName) + return this.canView(UserPermissionsEntity.access, projectName) } if (module === Module.roots) { return this.assignedToProject(projectName) @@ -178,10 +173,10 @@ class UserPermissions { canViewUsers(projectName?: string): boolean { if (projectName) { - return this.canView(UserPermissionsEntity.users, projectName) + return this.canView(UserPermissionsEntity.access, projectName) } - return this.canViewAny(UserPermissionsEntity.users) + return this.canViewAny(UserPermissionsEntity.access) } projectSettingsAreEnabled(): boolean { diff --git a/src/pages/ProjectManagerPage/ProjectManagerPage.jsx b/src/pages/ProjectManagerPage/ProjectManagerPage.jsx index 5ed2a5885..873d87084 100644 --- a/src/pages/ProjectManagerPage/ProjectManagerPage.jsx +++ b/src/pages/ProjectManagerPage/ProjectManagerPage.jsx @@ -113,7 +113,7 @@ const ProjectManagerPage = () => { shortcut: 'P+P', }) } - if (userPermissions.canViewAny(UserPermissionsEntity.users) || module === Module.projectAccess) { + if (userPermissions.canViewAny(UserPermissionsEntity.access) || module === Module.projectAccess) { links.push({ name: 'Project access', path: ModulePath[Module.projectAccess], diff --git a/src/pages/ProjectManagerPage/Users/ProjectUserAccessProjectList.tsx b/src/pages/ProjectManagerPage/Users/ProjectUserAccessProjectList.tsx index 8be91377b..28e3364a3 100644 --- a/src/pages/ProjectManagerPage/Users/ProjectUserAccessProjectList.tsx +++ b/src/pages/ProjectManagerPage/Users/ProjectUserAccessProjectList.tsx @@ -41,8 +41,8 @@ const StyledProjectName = styled.div` const formatName = (rowData: ProjectNode, userPermissions: UserPermissions) => { const readOnly = - !userPermissions.canEdit(UserPermissionsEntity.users, rowData.name) && - userPermissions.canView(UserPermissionsEntity.users, rowData.name) + !userPermissions.canEdit(UserPermissionsEntity.access, rowData.name) && + userPermissions.canView(UserPermissionsEntity.access, rowData.name) return rowData.name + (readOnly ? ' (read only)' : '') } @@ -64,8 +64,8 @@ const ProjectUserAccessProjectList = ({ projects, isLoading, selection, userPerm value={tableData.sort((a: ProjectNode, b: ProjectNode) => { const aActive = a.active ? 10 : -10 const bActive = b.active ? 10 : -10 - const aPerm = userPermissions.canEdit(UserPermissionsEntity.users, a.name) ? 1 : -1 - const bPerm = userPermissions.canEdit(UserPermissionsEntity.users, b.name) ? 1 : -1 + const aPerm = userPermissions.canEdit(UserPermissionsEntity.access, a.name) ? 1 : -1 + const bPerm = userPermissions.canEdit(UserPermissionsEntity.access, b.name) ? 1 : -1 const mainComparison = bActive - aActive + bPerm - aPerm if (mainComparison !== 0) { return mainComparison @@ -90,8 +90,8 @@ const ProjectUserAccessProjectList = ({ projects, isLoading, selection, userPerm body={(rowData) => { const isActive = rowData.active const hasPermissions = - userPermissions.canEdit(UserPermissionsEntity.users, rowData.name) || - userPermissions.canView(UserPermissionsEntity.users, rowData.name) + userPermissions.canEdit(UserPermissionsEntity.access, rowData.name) || + userPermissions.canView(UserPermissionsEntity.access, rowData.name) return ( {formatName(rowData, userPermissions)} diff --git a/src/pages/ProjectManagerPage/Users/mappers.ts b/src/pages/ProjectManagerPage/Users/mappers.ts index 9ac0c1181..e267720bf 100644 --- a/src/pages/ProjectManagerPage/Users/mappers.ts +++ b/src/pages/ProjectManagerPage/Users/mappers.ts @@ -150,7 +150,7 @@ const getFilteredEntities = ( const canAllEditUsers = (projects: string[], userPermissions?: UserPermissions) => { for (const project of projects) { - if (!userPermissions?.canEdit(UserPermissionsEntity.users, project)) { + if (!userPermissions?.canEdit(UserPermissionsEntity.access, project)) { return false } } @@ -238,8 +238,8 @@ const getErrorInfo = ( } if ( - !userPermissions?.canView(UserPermissionsEntity.users, project) && - !userPermissions?.canView(UserPermissionsEntity.users, project) + !userPermissions?.canView(UserPermissionsEntity.access, project) && + !userPermissions?.canView(UserPermissionsEntity.access, project) ) { return { icon: 'person', From b3d6ca10c02ef1a5b47ef4531729d78cd7b81ada Mon Sep 17 00:00:00 2001 From: Florin Tudor Date: Wed, 4 Dec 2024 17:52:21 +0100 Subject: [PATCH 11/19] fix(UserAccess): Moved api logic to dedicated enhanced endpoint --- .../Users/ProjectUserAccessAssignDialog.tsx | 6 --- src/pages/ProjectManagerPage/Users/hooks.ts | 33 +++--------- .../accessGroups/updateAccessGroups.ts | 52 ++++++++++++++++++- src/services/project/getProject.ts | 8 +-- 4 files changed, 62 insertions(+), 37 deletions(-) diff --git a/src/pages/ProjectManagerPage/Users/ProjectUserAccessAssignDialog.tsx b/src/pages/ProjectManagerPage/Users/ProjectUserAccessAssignDialog.tsx index b8ce8392e..f08417585 100644 --- a/src/pages/ProjectManagerPage/Users/ProjectUserAccessAssignDialog.tsx +++ b/src/pages/ProjectManagerPage/Users/ProjectUserAccessAssignDialog.tsx @@ -38,14 +38,8 @@ const ProjectUserAccessAssignDialog = ({ onSave, onClose, }: Props) => { - console.log('u: ', users) - console.log('pu: ', projectUsers) - console.log('ag: ', accessGroups) - console.log('uag: ', userAccessGroups) const initialStates = mapInitialAccessGroupStates(accessGroups, users, projectUsers, userAccessGroups) const initialStatesList = Object.keys(initialStates).map(agName => ({name: agName, status: initialStates[agName]})) - console.log('is: ', initialStates) - console.log('isl: ', initialStatesList) const [accessGroupItems, setAccessGroupItems] = useState(initialStatesList) const allSelected = accessGroupItems.find(item => item.status !== SelectionStatus.All) === undefined diff --git a/src/pages/ProjectManagerPage/Users/hooks.ts b/src/pages/ProjectManagerPage/Users/hooks.ts index a413e99a6..36c6f6b45 100644 --- a/src/pages/ProjectManagerPage/Users/hooks.ts +++ b/src/pages/ProjectManagerPage/Users/hooks.ts @@ -1,5 +1,5 @@ import { $Any } from '@types' -import api, { useGetProjectsUsersQuery } from '@queries/project/getProject' +import api, { useGetProjectsAccessQuery } from '@queries/project/getProject' import { api as accessApi } from '@api/rest/access' import { useState } from 'react' import { useDispatch } from 'react-redux' @@ -7,32 +7,22 @@ import { SelectionStatus } from './types' import { Filter } from '@components/SearchFilter/types' import { useAppSelector } from '@state/store' import { useSetFrontendPreferencesMutation } from '@queries/user/updateUser' +import { useUpdateAccessGroupsMutation } from '@queries/accessGroups/updateAccessGroups' const useProjectAccessGroupData = (selectedProject: string) => { - const udpateApiCache = (project: string, user: string, accessGroups: string[]) => { - dispatch(api.util.invalidateTags([{ type: 'project', id: project }])) - dispatch( - // @ts-ignore - api.util.updateQueryData('getProjectsUsers', { projects: [project] }, (draft: $Any) => { - draft[project][user] = accessGroups - }), - ) - } - - const dispatch = useDispatch() - const [updateProjectAccess] = accessApi.useSetProjectsAccessMutation() const [selectedProjects, setSelectedProjects] = useState( selectedProject ? [selectedProject] : [], ) - const result = useGetProjectsUsersQuery({ projects: selectedProjects }) + const [updateAcessGroups] = useUpdateAccessGroupsMutation() + const result = useGetProjectsAccessQuery({ projects: selectedProjects }) const users = result.data const accessGroupUsers: $Any = {} const removeUserAccessGroup = async (userList: string[], accessGroup?: string) => { let multiUpdateData: { [key: string]: { [key: string]: string[] } } = {} - let cacheInvalidations = [] + for (const user of userList) { for (const project of selectedProjects) { // @ts-ignore @@ -50,14 +40,10 @@ const useProjectAccessGroupData = (selectedProject: string) => { [project]: updatedAccessGroups, }, } - cacheInvalidations.push(() => udpateApiCache(project, user, updatedAccessGroups)) } } try { - await updateProjectAccess({ payload: multiUpdateData }).unwrap() - for (const callable of cacheInvalidations) { - callable() - } + updateAcessGroups({ payload: multiUpdateData }) } catch (error: $Any) { console.log(error) return error.details @@ -85,7 +71,6 @@ const useProjectAccessGroupData = (selectedProject: string) => { } let multiUpdateData: { [key: string]: { [key: string]: string[] } } = {} - let invalidations = [] for (const user of selectedUsers) { for (const project of selectedProjects) { // @ts-ignore @@ -97,15 +82,11 @@ const useProjectAccessGroupData = (selectedProject: string) => { [project]: accessGroups, }, } - invalidations.push(() => udpateApiCache(project, user, accessGroups)) } } try { - updateProjectAccess({ payload: multiUpdateData }) - for (const callable of invalidations) { - callable() - } + updateAcessGroups({ payload: multiUpdateData }) } catch (error: $Any) { console.log(error) return error.details diff --git a/src/services/accessGroups/updateAccessGroups.ts b/src/services/accessGroups/updateAccessGroups.ts index 5c55df992..f1777d60e 100644 --- a/src/services/accessGroups/updateAccessGroups.ts +++ b/src/services/accessGroups/updateAccessGroups.ts @@ -1,3 +1,4 @@ +import { $Any } from '@types' import api from './getAccessGroups' const setAccessGroups = api.injectEndpoints({ @@ -10,11 +11,60 @@ const setAccessGroups = api.injectEndpoints({ }), invalidatesTags: [{ type: 'accessGroup', id: 'LIST' }], }), + updateAccessGroups: build.mutation({ + query: (queryArg) => ({ url: `/api/access`, method: 'POST', body: queryArg.payload }), + async onQueryStarted({ payload }, { dispatch, queryFulfilled }) { + let projects = [] + for (const user of Object.keys(payload)) { + projects.push(...Object.keys(payload[user])) + } + + const patchResult = dispatch( + api.util.updateQueryData( + // @ts-ignore + 'getProjectsAccess', + { projects: [...new Set(projects)] }, + (draft: $Any) => { + for (const user of Object.keys(payload)) { + for (const project of Object.keys(payload[user])) { + draft = { + ...draft, + [project]: { + ...draft[project], + [user]: payload[user][project], + }, + } + } + } + }, + ), + ) + try { + await queryFulfilled + } catch { + patchResult.undo() + } + }, + // @ts-ignore + invalidatesTags: (_result, _error, { payload }) => { + let projects = [] + for (const user of Object.keys(payload)) { + projects.push(...Object.keys(payload[user])) + } + + let invalidations = [] + for (const project of [...new Set(projects)]) { + invalidations.push({ type: 'projectAccess', id: project }) + } + + return invalidations + }, + }), }), overrideExisting: true, }) -export const { useCreateAccessGroupMutation } = setAccessGroups +export const { useCreateAccessGroupMutation, useUpdateAccessGroupsMutation } = setAccessGroups const updateAccessGroupsApi = api.enhanceEndpoints({ endpoints: { diff --git a/src/services/project/getProject.ts b/src/services/project/getProject.ts index a1711e5f7..c5f74017e 100644 --- a/src/services/project/getProject.ts +++ b/src/services/project/getProject.ts @@ -1,4 +1,4 @@ -import { api, GetProjectUsersApiResponse } from '@api/rest/project' +import { api } from '@api/rest/project' // @ts-ignore import { selectProject, setProjectData } from '@state/project' @@ -62,7 +62,7 @@ const getProjectInjected = api.injectEndpoints({ transformResponse: (res: any) => res.data?.project, providesTags: (_res, _error, { projectName }) => [{ type: 'project', id: projectName }], }), - getProjectsUsers: build.query({ + getProjectsAccess: build.query({ async queryFn({ projects = [] }, { dispatch, forced }) { try { const projectUsersData: $Any = {} @@ -87,7 +87,7 @@ const getProjectInjected = api.injectEndpoints({ } }, providesTags: (_res, _error, { projects }) => - projects.map((projectName) => ({ type: 'project', id: projectName })), + projects.map((projectName) => ({ type: 'projectAccess', id: projectName })), }), }), overrideExisting: true, @@ -168,7 +168,7 @@ const getProjectApi = getProjectInjected.enhanceEndpoints({ export const { useGetProjectQuery, - useGetProjectsUsersQuery, + useGetProjectsAccessQuery, useListProjectsQuery, useGetProjectAnatomyQuery, useGetProjectAttribsQuery, From 0f3c39fb4bb45ebaeb2e2476163364d9e5d096b4 Mon Sep 17 00:00:00 2001 From: Innders <49156310+Innders@users.noreply.github.com> Date: Wed, 4 Dec 2024 17:19:37 +0000 Subject: [PATCH 12/19] chore: fix typo --- src/pages/ProjectManagerPage/Users/hooks.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/pages/ProjectManagerPage/Users/hooks.ts b/src/pages/ProjectManagerPage/Users/hooks.ts index 36c6f6b45..25b00ef35 100644 --- a/src/pages/ProjectManagerPage/Users/hooks.ts +++ b/src/pages/ProjectManagerPage/Users/hooks.ts @@ -10,12 +10,11 @@ import { useSetFrontendPreferencesMutation } from '@queries/user/updateUser' import { useUpdateAccessGroupsMutation } from '@queries/accessGroups/updateAccessGroups' const useProjectAccessGroupData = (selectedProject: string) => { - const [selectedProjects, setSelectedProjects] = useState( selectedProject ? [selectedProject] : [], ) - const [updateAcessGroups] = useUpdateAccessGroupsMutation() + const [updateAccessGroups] = useUpdateAccessGroupsMutation() const result = useGetProjectsAccessQuery({ projects: selectedProjects }) const users = result.data @@ -43,7 +42,7 @@ const useProjectAccessGroupData = (selectedProject: string) => { } } try { - updateAcessGroups({ payload: multiUpdateData }) + updateAccessGroups({ payload: multiUpdateData }) } catch (error: $Any) { console.log(error) return error.details From 43378b63014e34ad4124c54f616980167af728d9 Mon Sep 17 00:00:00 2001 From: Innders <49156310+Innders@users.noreply.github.com> Date: Wed, 4 Dec 2024 17:20:26 +0000 Subject: [PATCH 13/19] chore: more typos --- src/pages/ProjectManagerPage/Users/hooks.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/ProjectManagerPage/Users/hooks.ts b/src/pages/ProjectManagerPage/Users/hooks.ts index 25b00ef35..aca212f89 100644 --- a/src/pages/ProjectManagerPage/Users/hooks.ts +++ b/src/pages/ProjectManagerPage/Users/hooks.ts @@ -85,7 +85,7 @@ const useProjectAccessGroupData = (selectedProject: string) => { } try { - updateAcessGroups({ payload: multiUpdateData }) + updateAccessGroups({ payload: multiUpdateData }) } catch (error: $Any) { console.log(error) return error.details From 9b69bd140bf0076700e61434127f461b3a665d89 Mon Sep 17 00:00:00 2001 From: Florin Tudor Date: Thu, 5 Dec 2024 00:19:37 +0100 Subject: [PATCH 14/19] fix(UserAccess): Fetching user data in parallel, small refactoring --- src/pages/ProjectManagerPage/Users/hooks.ts | 4 +-- .../accessGroups/updateAccessGroups.ts | 12 ++++---- src/services/project/getProject.ts | 29 ++++++++++++------- 3 files changed, 26 insertions(+), 19 deletions(-) diff --git a/src/pages/ProjectManagerPage/Users/hooks.ts b/src/pages/ProjectManagerPage/Users/hooks.ts index aca212f89..12aba694f 100644 --- a/src/pages/ProjectManagerPage/Users/hooks.ts +++ b/src/pages/ProjectManagerPage/Users/hooks.ts @@ -1,8 +1,6 @@ import { $Any } from '@types' -import api, { useGetProjectsAccessQuery } from '@queries/project/getProject' -import { api as accessApi } from '@api/rest/access' +import { useGetProjectsAccessQuery } from '@queries/project/getProject' import { useState } from 'react' -import { useDispatch } from 'react-redux' import { SelectionStatus } from './types' import { Filter } from '@components/SearchFilter/types' import { useAppSelector } from '@state/store' diff --git a/src/services/accessGroups/updateAccessGroups.ts b/src/services/accessGroups/updateAccessGroups.ts index f1777d60e..e86de9705 100644 --- a/src/services/accessGroups/updateAccessGroups.ts +++ b/src/services/accessGroups/updateAccessGroups.ts @@ -25,17 +25,19 @@ const setAccessGroups = api.injectEndpoints({ 'getProjectsAccess', { projects: [...new Set(projects)] }, (draft: $Any) => { + let updatedData: $Any = {} for (const user of Object.keys(payload)) { for (const project of Object.keys(payload[user])) { - draft = { - ...draft, + updatedData = { + ...updatedData, [project]: { - ...draft[project], - [user]: payload[user][project], - }, + ...updatedData[project] || {}, + [user]: payload[user][project] + } } } } + draft = { ...draft, ...updatedData } }, ), ) diff --git a/src/services/project/getProject.ts b/src/services/project/getProject.ts index c5f74017e..7c4de9771 100644 --- a/src/services/project/getProject.ts +++ b/src/services/project/getProject.ts @@ -65,21 +65,28 @@ const getProjectInjected = api.injectEndpoints({ getProjectsAccess: build.query({ async queryFn({ projects = [] }, { dispatch, forced }) { try { - const projectUsersData: $Any = {} + let promises = [] + let projectUsersData: $Any = {} for (const project of projects) { - const response = await dispatch( - api.endpoints.getProjectUsers.initiate( - { projectName: project }, - { forceRefetch: forced }, - ), + promises.push( + dispatch( + api.endpoints.getProjectUsers.initiate( + { projectName: project }, + { forceRefetch: forced }, + ), + ).then((response) => { + if (response.status === 'rejected') { + return + } + projectUsersData = { + ...projectUsersData, + [project]: response.data, + } + }), ) - - if (response.status === 'rejected') { - throw 'No projects found' - } - projectUsersData[project] = response.data } + await Promise.all(promises) return { data: projectUsersData, meta: undefined, error: undefined } } catch (error: $Any) { console.error(error) From a06f180587db077e6df501edb9d5dbb916c3ae9a Mon Sep 17 00:00:00 2001 From: Florin Tudor Date: Thu, 5 Dec 2024 00:42:56 +0100 Subject: [PATCH 15/19] fix(UserAccess): Moving query endpoint to access groups service --- .../Users/ProjectUserAccessAssignDialog.tsx | 2 +- src/pages/ProjectManagerPage/Users/hooks.ts | 2 +- src/pages/ProjectManagerPage/Users/mappers.ts | 3 +- src/services/accessGroups/getAccessGroups.ts | 66 ++++++++++++++++++- src/services/project/getProject.ts | 49 -------------- 5 files changed, 66 insertions(+), 56 deletions(-) diff --git a/src/pages/ProjectManagerPage/Users/ProjectUserAccessAssignDialog.tsx b/src/pages/ProjectManagerPage/Users/ProjectUserAccessAssignDialog.tsx index f08417585..f653dfe6a 100644 --- a/src/pages/ProjectManagerPage/Users/ProjectUserAccessAssignDialog.tsx +++ b/src/pages/ProjectManagerPage/Users/ProjectUserAccessAssignDialog.tsx @@ -8,7 +8,7 @@ import { getPlatformShortcutKey, KeyMode } from '@helpers/platform' import { mapInitialAccessGroupStates } from './mappers' import { AccessGroupUsers, SelectionStatus } from './types' import * as Styled from './ProjectUserAccessAssignDialog.styled' -import { ProjectUserData } from '@queries/project/getProject' +import { ProjectUserData } from '@queries/accessGroups/getAccessGroups' const icons: {[key in SelectionStatus] : string | undefined} = { [SelectionStatus.None]: 'add', diff --git a/src/pages/ProjectManagerPage/Users/hooks.ts b/src/pages/ProjectManagerPage/Users/hooks.ts index 12aba694f..006fc386a 100644 --- a/src/pages/ProjectManagerPage/Users/hooks.ts +++ b/src/pages/ProjectManagerPage/Users/hooks.ts @@ -1,11 +1,11 @@ import { $Any } from '@types' -import { useGetProjectsAccessQuery } from '@queries/project/getProject' import { useState } from 'react' import { SelectionStatus } from './types' import { Filter } from '@components/SearchFilter/types' import { useAppSelector } from '@state/store' import { useSetFrontendPreferencesMutation } from '@queries/user/updateUser' import { useUpdateAccessGroupsMutation } from '@queries/accessGroups/updateAccessGroups' +import { useGetProjectsAccessQuery } from '@queries/accessGroups/getAccessGroups' const useProjectAccessGroupData = (selectedProject: string) => { const [selectedProjects, setSelectedProjects] = useState( diff --git a/src/pages/ProjectManagerPage/Users/mappers.ts b/src/pages/ProjectManagerPage/Users/mappers.ts index e267720bf..850da9a67 100644 --- a/src/pages/ProjectManagerPage/Users/mappers.ts +++ b/src/pages/ProjectManagerPage/Users/mappers.ts @@ -1,11 +1,10 @@ import { AccessGroupUsers, ListingError, SelectedAccessGroupUsers, SelectionStatus } from './types' import { Filter, FilterValue, Option } from '@components/SearchFilter/types' import { ProjectNode, UserNode } from '@api/graphql' -import { GetProjectsUsersApiResponse, ProjectUserData } from '@queries/project/getProject' import { UserPermissions, UserPermissionsEntity } from '@hooks/useUserProjectPermissions' import { $Any } from '@types' import { matchSorter } from 'match-sorter' -import { difference } from 'lodash' +import { GetProjectsUsersApiResponse, ProjectUserData } from '@queries/accessGroups/getAccessGroups' const getAllProjectUsers = (groupedUsers: AccessGroupUsers): string[] => { let allUsers: string[] = [] diff --git a/src/services/accessGroups/getAccessGroups.ts b/src/services/accessGroups/getAccessGroups.ts index bf88b6d9d..9cf239c81 100644 --- a/src/services/accessGroups/getAccessGroups.ts +++ b/src/services/accessGroups/getAccessGroups.ts @@ -1,6 +1,62 @@ import { api } from '@api/rest/accessGroups' +import { api as projectApi } from '@api/rest/project' +import { $Any } from '@types' -const accessGroupsApi = api.enhanceEndpoints({ +export type ProjectUserData = { + [project: string]: { + [user: string]: string[] + } +} + +export type GetProjectsUsersApiResponse = { + data: ProjectUserData +} + +type GetProjectsUsersParams = { + projects: string[] +} + +const accessGroupsApi = api.injectEndpoints({ + endpoints: (build) => ({ + getProjectsAccess: build.query({ + async queryFn({ projects = [] }, { dispatch, forced }) { + try { + let promises = [] + let projectUsersData: $Any = {} + for (const project of projects) { + promises.push( + dispatch( + projectApi.endpoints.getProjectUsers.initiate( + { projectName: project }, + { forceRefetch: forced }, + ), + ).then((response: $Any) => { + if (response.status === 'rejected') { + return + } + projectUsersData = { + ...projectUsersData, + [project]: response.data, + } + }), + ) + } + + await Promise.all(promises) + return { data: projectUsersData, meta: undefined, error: undefined } + } catch (error: $Any) { + console.error(error) + return { error, meta: undefined, data: undefined } + } + }, + providesTags: (_res, _error, { projects }) => + projects.map((projectName) => ({ type: 'projectAccess', id: projectName })), + }), + }), + overrideExisting: true, +}) + +accessGroupsApi.enhanceEndpoints({ endpoints: { getAccessGroups: { providesTags: (result) => @@ -21,7 +77,11 @@ const accessGroupsApi = api.enhanceEndpoints({ }, }) -export const { useGetAccessGroupsQuery, useGetAccessGroupQuery, useGetAccessGroupSchemaQuery } = - accessGroupsApi +export const { + useGetAccessGroupsQuery, + useGetAccessGroupQuery, + useGetAccessGroupSchemaQuery, + useGetProjectsAccessQuery, +} = accessGroupsApi export default accessGroupsApi diff --git a/src/services/project/getProject.ts b/src/services/project/getProject.ts index 7c4de9771..11e964d7b 100644 --- a/src/services/project/getProject.ts +++ b/src/services/project/getProject.ts @@ -34,20 +34,6 @@ const createProjectQuery = (attribs: $Any, fields: $Any) => { ` } -type GetProjectsUsersParams = { - projects: string[] -} - -export type ProjectUserData = { - [project: string]: { - [user: string]: string[] - } -} - -export type GetProjectsUsersApiResponse = { - data: ProjectUserData -} - const getProjectInjected = api.injectEndpoints({ endpoints: (build) => ({ getProjectAttribs: build.query({ @@ -62,40 +48,6 @@ const getProjectInjected = api.injectEndpoints({ transformResponse: (res: any) => res.data?.project, providesTags: (_res, _error, { projectName }) => [{ type: 'project', id: projectName }], }), - getProjectsAccess: build.query({ - async queryFn({ projects = [] }, { dispatch, forced }) { - try { - let promises = [] - let projectUsersData: $Any = {} - for (const project of projects) { - promises.push( - dispatch( - api.endpoints.getProjectUsers.initiate( - { projectName: project }, - { forceRefetch: forced }, - ), - ).then((response) => { - if (response.status === 'rejected') { - return - } - projectUsersData = { - ...projectUsersData, - [project]: response.data, - } - }), - ) - } - - await Promise.all(promises) - return { data: projectUsersData, meta: undefined, error: undefined } - } catch (error: $Any) { - console.error(error) - return { error, meta: undefined, data: undefined } - } - }, - providesTags: (_res, _error, { projects }) => - projects.map((projectName) => ({ type: 'projectAccess', id: projectName })), - }), }), overrideExisting: true, }) @@ -175,7 +127,6 @@ const getProjectApi = getProjectInjected.enhanceEndpoints({ export const { useGetProjectQuery, - useGetProjectsAccessQuery, useListProjectsQuery, useGetProjectAnatomyQuery, useGetProjectAttribsQuery, From 9b9022a469120eb3d67944dca50edaed45115841 Mon Sep 17 00:00:00 2001 From: Florin Tudor Date: Thu, 5 Dec 2024 00:45:27 +0100 Subject: [PATCH 16/19] fix(UserAccess): Better naming --- src/pages/ProjectManagerPage/Users/hooks.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/ProjectManagerPage/Users/hooks.ts b/src/pages/ProjectManagerPage/Users/hooks.ts index 006fc386a..489f9326d 100644 --- a/src/pages/ProjectManagerPage/Users/hooks.ts +++ b/src/pages/ProjectManagerPage/Users/hooks.ts @@ -51,7 +51,7 @@ const useProjectAccessGroupData = (selectedProject: string) => { selectedUsers: $Any, changes: { name: string; status: SelectionStatus }[], ): Promise => { - const updatedAccessGroups = ( + const getUpdatedAccessGroups = ( existing: string[], changes: { name: string; status: SelectionStatus }[], ): string[] => { @@ -71,7 +71,7 @@ const useProjectAccessGroupData = (selectedProject: string) => { for (const user of selectedUsers) { for (const project of selectedProjects) { // @ts-ignore - const accessGroups = updatedAccessGroups(users?.[project][user] || [], changes) + const accessGroups = getUpdatedAccessGroups(users?.[project][user] || [], changes) multiUpdateData = { ...multiUpdateData, [user]: { From ec8c7ec4209e5628b3a3dcbf25922f732262db03 Mon Sep 17 00:00:00 2001 From: Florin Tudor Date: Thu, 5 Dec 2024 09:43:48 +0100 Subject: [PATCH 17/19] fix(UserAccess): Adding shimmer on loading to main users panel --- .../ProjectManagerPage/Users/ProjectUserAccess.tsx | 14 ++++++++------ .../Users/ProjectUserAccessUserList.tsx | 10 +++++++++- src/pages/ProjectManagerPage/Users/hooks.ts | 12 ++++++------ 3 files changed, 23 insertions(+), 13 deletions(-) diff --git a/src/pages/ProjectManagerPage/Users/ProjectUserAccess.tsx b/src/pages/ProjectManagerPage/Users/ProjectUserAccess.tsx index ab8bbc72d..52732c8e0 100644 --- a/src/pages/ProjectManagerPage/Users/ProjectUserAccess.tsx +++ b/src/pages/ProjectManagerPage/Users/ProjectUserAccess.tsx @@ -61,6 +61,7 @@ const ProjectUserAccess = () => { const [selectedProject] = useQueryParam('project') const { + isLoading: isLoadingAccessGroupsData, users: projectUsers, selectedProjects, setSelectedProjects, @@ -77,7 +78,7 @@ const ProjectUserAccess = () => { const selfName = useSelector((state: $Any) => state.user.name) let { data: userList = [], - isLoading: usersLoading, + isLoading: isLoadingUsers, isError: usersFetchError, } = useGetUsersQuery({ selfName }) @@ -91,7 +92,7 @@ const ProjectUserAccess = () => { >() const [hoveredUser, setHoveredUser] = useState() - const { data: projects, isLoading: projectsLoading, isError, error } = useListProjectsQuery({}) + const { data: projects, isLoading: isLoadingProjects, isError, error } = useListProjectsQuery({}) if (isError) { console.error(error) } @@ -339,7 +340,7 @@ const ProjectUserAccess = () => { { selectedProjects={filteredSelectedProjects} selectedUsers={selectedAccessGroupUsers?.accessGroup ? [] : selectedUsers} tableList={filteredUsersWithAccessGroups} - isLoading={usersLoading} + isLoading={isLoadingUsers || isLoadingAccessGroupsData} readOnly={!hasEditRightsOnProject} hoveredUser={hoveredUser} onContextMenu={(e: $Any) => handleContextMenu()(e)} @@ -369,6 +370,7 @@ const ProjectUserAccess = () => { sortable showAddButton showAccessGroups + shimmerEnabled /> @@ -415,7 +417,7 @@ const ProjectUserAccess = () => { } onAdd={handleRowAddButton} onRemove={onRemove(accessGroup)} - isLoading={usersLoading} + isLoading={isLoadingUsers} /> ) @@ -424,7 +426,7 @@ const ProjectUserAccess = () => { ) - if (permissionsLoading || usersLoading || projectsLoading) { + if (permissionsLoading || isLoadingUsers || isLoadingProjects) { return } diff --git a/src/pages/ProjectManagerPage/Users/ProjectUserAccessUserList.tsx b/src/pages/ProjectManagerPage/Users/ProjectUserAccessUserList.tsx index 61b13da6a..dc49b6566 100644 --- a/src/pages/ProjectManagerPage/Users/ProjectUserAccessUserList.tsx +++ b/src/pages/ProjectManagerPage/Users/ProjectUserAccessUserList.tsx @@ -24,6 +24,7 @@ type Props = { showAddButton?: boolean showAddMoreButton?: boolean showAccessGroups?: boolean + shimmerEnabled?: boolean onContextMenu?: $Any onHoverRow: $Any onSelectUsers?: (selectedUsers: string[]) => void @@ -45,6 +46,7 @@ const ProjectUserAccessUserList = ({ showAddButton = false, showAddMoreButton = false, showAccessGroups = false, + shimmerEnabled = false, onAdd, onRemove, onContextMenu, @@ -87,7 +89,13 @@ const ProjectUserAccessUserList = ({ loading: isLoading, fullBorderRadius: accessGroup === undefined, })} - rowClassName={(rowData: $Any) => clsx({ inactive: !rowData.active, loading: isLoading })} + rowClassName={(rowData: $Any) => + clsx({ + inactive: !rowData.active, + loading: isLoading, + 'shimmer-light': shimmerEnabled && isLoading, + }) + } onContextMenu={!readOnly && onContextMenu} onRowMouseEnter={(e) => onHoverRow(e.data.name)} onRowMouseLeave={() => onHoverRow()} diff --git a/src/pages/ProjectManagerPage/Users/hooks.ts b/src/pages/ProjectManagerPage/Users/hooks.ts index 489f9326d..c25f23053 100644 --- a/src/pages/ProjectManagerPage/Users/hooks.ts +++ b/src/pages/ProjectManagerPage/Users/hooks.ts @@ -13,8 +13,7 @@ const useProjectAccessGroupData = (selectedProject: string) => { ) const [updateAccessGroups] = useUpdateAccessGroupsMutation() - const result = useGetProjectsAccessQuery({ projects: selectedProjects }) - const users = result.data + const { isLoading, data: usersData } = useGetProjectsAccessQuery({ projects: selectedProjects }) const accessGroupUsers: $Any = {} const removeUserAccessGroup = async (userList: string[], accessGroup?: string) => { @@ -23,12 +22,12 @@ const useProjectAccessGroupData = (selectedProject: string) => { for (const user of userList) { for (const project of selectedProjects) { // @ts-ignore - if (!users![project][user]) { + if (!usersData![project][user]) { continue } const updatedAccessGroups = accessGroup ? // @ts-ignore - users![project][user]?.filter((item: string) => item !== accessGroup) + usersData![project][user]?.filter((item: string) => item !== accessGroup) : [] multiUpdateData = { ...multiUpdateData, @@ -71,7 +70,7 @@ const useProjectAccessGroupData = (selectedProject: string) => { for (const user of selectedUsers) { for (const project of selectedProjects) { // @ts-ignore - const accessGroups = getUpdatedAccessGroups(users?.[project][user] || [], changes) + const accessGroups = getUpdatedAccessGroups(usersData?.[project][user] || [], changes) multiUpdateData = { ...multiUpdateData, [user]: { @@ -91,7 +90,8 @@ const useProjectAccessGroupData = (selectedProject: string) => { } return { - users, + isLoading, + users: usersData, accessGroupUsers, selectedProjects, setSelectedProjects, From 0309d45a57ff1f866c5165798b61d1d5b699670c Mon Sep 17 00:00:00 2001 From: Florin Tudor Date: Thu, 5 Dec 2024 09:51:17 +0100 Subject: [PATCH 18/19] fix(UserAccess): Fixed small bug where access groups dialog pops on key press while no users are selected --- src/pages/ProjectManagerPage/Users/ProjectUserAccess.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/pages/ProjectManagerPage/Users/ProjectUserAccess.tsx b/src/pages/ProjectManagerPage/Users/ProjectUserAccess.tsx index 52732c8e0..881d46783 100644 --- a/src/pages/ProjectManagerPage/Users/ProjectUserAccess.tsx +++ b/src/pages/ProjectManagerPage/Users/ProjectUserAccess.tsx @@ -306,7 +306,10 @@ const ProjectUserAccess = () => { { key: 'a', action: () => { - if (!selectedAccessGroupUsers?.users && !hoveredUser?.user) { + if ( + (!selectedAccessGroupUsers?.users || selectedAccessGroupUsers!.users.length == 0) && + !hoveredUser?.user + ) { return } From 515799d0c1234c60da9ed145d4086c887901579f Mon Sep 17 00:00:00 2001 From: Innders <49156310+Innders@users.noreply.github.com> Date: Thu, 5 Dec 2024 10:01:04 +0000 Subject: [PATCH 19/19] fix(UserAccess): optimistic updates --- src/pages/ProjectManagerPage/Users/hooks.ts | 4 ++-- .../accessGroups/updateAccessGroups.ts | 19 ++++++++----------- 2 files changed, 10 insertions(+), 13 deletions(-) diff --git a/src/pages/ProjectManagerPage/Users/hooks.ts b/src/pages/ProjectManagerPage/Users/hooks.ts index c25f23053..406065370 100644 --- a/src/pages/ProjectManagerPage/Users/hooks.ts +++ b/src/pages/ProjectManagerPage/Users/hooks.ts @@ -39,7 +39,7 @@ const useProjectAccessGroupData = (selectedProject: string) => { } } try { - updateAccessGroups({ payload: multiUpdateData }) + updateAccessGroups({ payload: multiUpdateData, selectedProjects }) } catch (error: $Any) { console.log(error) return error.details @@ -82,7 +82,7 @@ const useProjectAccessGroupData = (selectedProject: string) => { } try { - updateAccessGroups({ payload: multiUpdateData }) + updateAccessGroups({ payload: multiUpdateData, selectedProjects }) } catch (error: $Any) { console.log(error) return error.details diff --git a/src/services/accessGroups/updateAccessGroups.ts b/src/services/accessGroups/updateAccessGroups.ts index e86de9705..2ab69be08 100644 --- a/src/services/accessGroups/updateAccessGroups.ts +++ b/src/services/accessGroups/updateAccessGroups.ts @@ -13,17 +13,12 @@ const setAccessGroups = api.injectEndpoints({ }), updateAccessGroups: build.mutation({ query: (queryArg) => ({ url: `/api/access`, method: 'POST', body: queryArg.payload }), - async onQueryStarted({ payload }, { dispatch, queryFulfilled }) { - let projects = [] - for (const user of Object.keys(payload)) { - projects.push(...Object.keys(payload[user])) - } - + async onQueryStarted({ payload, selectedProjects }, { dispatch, queryFulfilled }) { const patchResult = dispatch( api.util.updateQueryData( // @ts-ignore 'getProjectsAccess', - { projects: [...new Set(projects)] }, + { projects: selectedProjects }, (draft: $Any) => { let updatedData: $Any = {} for (const user of Object.keys(payload)) { @@ -31,13 +26,15 @@ const setAccessGroups = api.injectEndpoints({ updatedData = { ...updatedData, [project]: { - ...updatedData[project] || {}, - [user]: payload[user][project] - } + ...(draft[project] || {}), + ...(updatedData[project] || {}), + [user]: payload[user][project], + }, } } } - draft = { ...draft, ...updatedData } + + return { ...draft, ...updatedData } }, ), )