From 5b9dfb8ba001c93c7e8d3009a6083e479b733cb1 Mon Sep 17 00:00:00 2001 From: Martastain Date: Wed, 16 Oct 2024 10:04:46 +0200 Subject: [PATCH 01/35] fix: show project level access groups --- src/pages/SettingsPage/AccessGroups/AccessGroupDetail.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/SettingsPage/AccessGroups/AccessGroupDetail.jsx b/src/pages/SettingsPage/AccessGroups/AccessGroupDetail.jsx index 3d2b39949..eed5230d8 100644 --- a/src/pages/SettingsPage/AccessGroups/AccessGroupDetail.jsx +++ b/src/pages/SettingsPage/AccessGroups/AccessGroupDetail.jsx @@ -113,7 +113,7 @@ const AccessGroupDetail = ({ projectName, accessGroupName }) => { originalData={originalData} formData={formData} onChange={setFormData} - level={isProjectLevel ? 'project' : 'studio'} + level={projectName ? 'project' : 'studio'} context={{ headerProjectName: projectName, }} From facace6d8279a83f184b5d3cef62cfca700c7163 Mon Sep 17 00:00:00 2001 From: Florin Tudor Date: Wed, 16 Oct 2024 10:49:26 +0200 Subject: [PATCH 02/35] generating user permissions endpoints --- src/api/rest/permissions.ts | 89 +++++++++++++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 src/api/rest/permissions.ts diff --git a/src/api/rest/permissions.ts b/src/api/rest/permissions.ts new file mode 100644 index 000000000..bee3406f7 --- /dev/null +++ b/src/api/rest/permissions.ts @@ -0,0 +1,89 @@ +import { RestAPI as api } from '../../services/ayon' +const injectedRtkApi = api.injectEndpoints({ + endpoints: (build) => ({ + getCurrentUserPermissions: build.query< + GetCurrentUserPermissionsApiResponse, + GetCurrentUserPermissionsApiArg + >({ + query: () => ({ url: `/api/users/me/perimissions` }), + }), + getCurrentUserProjectPermissions: build.query< + GetCurrentUserProjectPermissionsApiResponse, + GetCurrentUserProjectPermissionsApiArg + >({ + query: (queryArg) => ({ url: `/api/users/me/permissions/${queryArg.projectName}` }), + }), + }), + overrideExisting: false, +}) +export { injectedRtkApi as api } +export type GetCurrentUserPermissionsApiResponse = /** status 200 Successful Response */ any +export type GetCurrentUserPermissionsApiArg = void +export type GetCurrentUserProjectPermissionsApiResponse = + /** status 200 Successful Response */ Permissions +export type GetCurrentUserProjectPermissionsApiArg = { + projectName: 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 StudioSettingsAccessModel = { + enabled?: boolean + /** List of addons a user can access */ + addons?: string[] +} +export type ProjectSettingsAccessModel = { + enabled?: boolean + /** List of addons a user can access */ + addons?: string[] + /** Allow users to update the project anatomy */ + anatomy_update?: boolean +} +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 Permissions = { + /** Restrict access to studio settings */ + studio_settings?: StudioSettingsAccessModel + /** Restrict write access to project settings */ + project_settings?: ProjectSettingsAccessModel + /** 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 +} From 0b0a55b51b16353d5a51ee5b8c95c9af50532c10 Mon Sep 17 00:00:00 2001 From: Martastain Date: Wed, 16 Oct 2024 13:56:34 +0200 Subject: [PATCH 03/35] feat: project permissions p.o.c. --- .../SettingsEditor/SettingsEditor.sass | 2 +- .../ProjectManagerPage/ProjectManagerPage.jsx | 47 ++++++++++++------- src/services/permissions/getPermissions.ts | 20 ++++++++ 3 files changed, 52 insertions(+), 17 deletions(-) create mode 100644 src/services/permissions/getPermissions.ts diff --git a/src/containers/SettingsEditor/SettingsEditor.sass b/src/containers/SettingsEditor/SettingsEditor.sass index 3c9e4e15c..e83fd09e1 100644 --- a/src/containers/SettingsEditor/SettingsEditor.sass +++ b/src/containers/SettingsEditor/SettingsEditor.sass @@ -116,7 +116,7 @@ $field-gap: 8px padding: 0 3px .form-inline-field-label - flex-basis: 200px + flex-basis: 250px padding-left: 4px cursor: pointer user-select: none diff --git a/src/pages/ProjectManagerPage/ProjectManagerPage.jsx b/src/pages/ProjectManagerPage/ProjectManagerPage.jsx index d8d4fd989..388a2cc5a 100644 --- a/src/pages/ProjectManagerPage/ProjectManagerPage.jsx +++ b/src/pages/ProjectManagerPage/ProjectManagerPage.jsx @@ -11,6 +11,7 @@ import NewProjectDialog from './NewProjectDialog' import { selectProject } from '@state/context' import { useDeleteProjectMutation, useUpdateProjectMutation } from '@queries/project/updateProject' +import { useGetCurrentUserProjectPermissionsQuery } from '@queries/permissions/getPermissions' import TeamsPage from '../TeamsPage' import ProjectManagerPageContainer from './ProjectManagerPageContainer' import ProjectManagerPageLayout from './ProjectManagerPageLayout' @@ -48,6 +49,8 @@ const ProjectManagerPage = () => { withDefault(StringParam, projectName), ) + const { data: permissions} = useGetCurrentUserProjectPermissionsQuery({ projectName: selectedProject }) + // UPDATE DATA const [updateProject] = useUpdateProjectMutation() @@ -81,21 +84,33 @@ const ProjectManagerPage = () => { await updateProject({ projectName: sel, update: { active } }).unwrap() } - let links = [ - { - name: 'Anatomy', - path: '/manageProjects/anatomy', - module: 'anatomy', - accessLevels: ['manager'], - shortcut: 'A+A', - }, - { - name: 'Project settings', - path: '/manageProjects/projectSettings', - module: 'projectSettings', - accessLevels: ['manager'], - shortcut: 'P+P', - }, + const links = [] + const projectPermissions = permissions?.project_settings + + if (projectPermissions){ + if (!projectPermissions.enabled || projectPermissions.anatomy_update) { + links.push({ + name: 'Anatomy', + path: '/manageProjects/anatomy', + module: 'anatomy', + accessLevels: [], + shortcut: 'A+A', + }) + } + + if (!projectPermissions.enabled || projectPermissions.addon_settings_update) { + links.push({ + name: 'Project settings', + path: '/manageProjects/projectSettings', + module: 'projectSettings', + accessLevels: [], + shortcut: 'P+P', + }) + } + } + + + links.push( { name: 'Site settings', path: '/manageProjects/siteSettings', @@ -114,7 +129,7 @@ const ProjectManagerPage = () => { module: 'teams', accessLevels: ['manager'], }, - ] + ) const linksWithProject = useMemo( () => diff --git a/src/services/permissions/getPermissions.ts b/src/services/permissions/getPermissions.ts new file mode 100644 index 000000000..eee3e4678 --- /dev/null +++ b/src/services/permissions/getPermissions.ts @@ -0,0 +1,20 @@ +import { api } from '@api/rest/permissions' + +const permissionsApi = api.enhanceEndpoints({ + endpoints: { + getCurrentUserPermissions: { + providesTags: (result) =>[ + { type: 'userPermissions' }, + ] + }, + getCurrentUserProjectPermissions: { + providesTags: (_result, _err, args ) => [ + { type: 'userProjectPermissions' }, + ], + }, + }, +}) + +export const { useGetCurrentUserPermissionsQuery, useGetCurrentUserProjectPermissionsQuery } = permissionsApi +export default permissionsApi + From bfffcf72f883334dfac8f506d5b21adb535daf9e Mon Sep 17 00:00:00 2001 From: Martastain Date: Thu, 17 Oct 2024 13:18:12 +0200 Subject: [PATCH 04/35] feat(settings): add permissions widget --- .../SettingsEditor/Widgets/TextWidget.tsx | 65 +++++++++++++++---- 1 file changed, 54 insertions(+), 11 deletions(-) diff --git a/src/containers/SettingsEditor/Widgets/TextWidget.tsx b/src/containers/SettingsEditor/Widgets/TextWidget.tsx index 5ed3be4a9..0ecd546ae 100644 --- a/src/containers/SettingsEditor/Widgets/TextWidget.tsx +++ b/src/containers/SettingsEditor/Widgets/TextWidget.tsx @@ -1,7 +1,9 @@ +import React from 'react'; import { useEffect, useState } from 'react' import { equiv, getDefaultValue, parseContext, updateChangedKeys } from '../helpers' import { $Any } from '@types' import { + Button, IconSelect, InputColor, InputNumber, @@ -9,6 +11,35 @@ import { InputTextarea, } from '@ynput/ayon-react-components' + + +type PermissionWidgetProps = { + value: number; + setValue: (value: number) => void; +}; + +const PermissionWidget: React.FC = ({ value, setValue }) => { + return ( +
+
+ ) +} + export const TextWidget = (props: $Any) => { const { originalValue, path } = parseContext(props) const [value, setValue] = useState(null) @@ -91,17 +122,29 @@ export const TextWidget = (props: $Any) => { // if (['integer', 'number'].includes(props.schema.type)) { - Input = InputNumber - opts.value = value === undefined || value === null ? '' : value - opts.onBlur = () => onChangeCommit(props.schema.type) - opts.onChange = (e: $Any) => { - // ensure that the value is a number. decimal points are allowed - // but no other characters - // use regex to check if the value is a number - - if (!/^-?\d*\.?\d*$/.test(e.target.value)) return - - onChange(e.target.value) + if (props.schema.widget === 'permission') { + Input = PermissionWidget + opts.value = value || 0 + opts.setValue = onChange + opts.setValue = (e: $Any) => { + // internal state is handled by the component, + // so we shouldn't need to debounce this + updateChangedKeys(props, e !== originalValue, path); + props.onChange(e); + } + } else { + Input = InputNumber + opts.value = value === undefined || value === null ? '' : value + opts.showButtons = true + opts.useGrouping = false + opts.onBlur = () => onChangeCommit(props.schema.type) + opts.onChange = (e: $Any) => { + // ensure that the value is a number. decimal points are allowed + // but no other characters + // use regex to check if the value is a number + if (!/^-?\d*\.?\d*$/.test(e.target.value)) return + onChange(e.target.value) + } } } else if (props.schema.widget === 'color') { // From 229fa0466fdbab4b0f52a8d5a20b3dba9acba426 Mon Sep 17 00:00:00 2001 From: Martastain Date: Thu, 17 Oct 2024 13:19:11 +0200 Subject: [PATCH 05/35] feat(settings): allow hide disabled groups using hideDisabledGroups context flag --- .../SettingsEditor/FormTemplates/ObjectFieldTemplate.tsx | 9 ++++++++- src/containers/SettingsEditor/SettingsPanel.jsx | 3 ++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/containers/SettingsEditor/FormTemplates/ObjectFieldTemplate.tsx b/src/containers/SettingsEditor/FormTemplates/ObjectFieldTemplate.tsx index 5de11a7b1..484c6dd91 100644 --- a/src/containers/SettingsEditor/FormTemplates/ObjectFieldTemplate.tsx +++ b/src/containers/SettingsEditor/FormTemplates/ObjectFieldTemplate.tsx @@ -250,7 +250,7 @@ function ObjectFieldTemplate(props: { id: string } & ObjectFieldTemplateProps) { ) - } + } // Root object - show badges and title titleComponent = props.idSchema.$id === 'root' ? rootTitle : stringTitle @@ -263,6 +263,12 @@ function ObjectFieldTemplate(props: { id: string } & ObjectFieldTemplateProps) { contextMenu(e, contextMenuModel) } + + const hasEnabled = (props?.schema?.properties || {}).hasOwnProperty('enabled') + const isEnabled = props?.formData?.enabled + + const disabled = props.formContext.hideDisabledGroups && hasEnabled && !isEnabled + return ( {fields} diff --git a/src/containers/SettingsEditor/SettingsPanel.jsx b/src/containers/SettingsEditor/SettingsPanel.jsx index 326e549d0..7d6e251d3 100644 --- a/src/containers/SettingsEditor/SettingsPanel.jsx +++ b/src/containers/SettingsEditor/SettingsPanel.jsx @@ -121,6 +121,7 @@ const SettingsPanel = ({ onClick, onContextMenu, currentId, + disabled, }) => { const [expandedObjects, setExpandedObjects] = useLocalStorage('expanded-settings-keys', []) @@ -155,7 +156,7 @@ const SettingsPanel = ({ Date: Thu, 17 Oct 2024 13:19:39 +0200 Subject: [PATCH 06/35] chore: hide disabled groups in accessgroup editor --- src/pages/SettingsPage/AccessGroups/AccessGroupDetail.jsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pages/SettingsPage/AccessGroups/AccessGroupDetail.jsx b/src/pages/SettingsPage/AccessGroups/AccessGroupDetail.jsx index eed5230d8..885eb450c 100644 --- a/src/pages/SettingsPage/AccessGroups/AccessGroupDetail.jsx +++ b/src/pages/SettingsPage/AccessGroups/AccessGroupDetail.jsx @@ -116,6 +116,7 @@ const AccessGroupDetail = ({ projectName, accessGroupName }) => { level={projectName ? 'project' : 'studio'} context={{ headerProjectName: projectName, + hideDisabledGroups: true, }} /> From ed4b0ca2286e19ece58153c9c1710ba7f11d1df0 Mon Sep 17 00:00:00 2001 From: Martastain Date: Thu, 17 Oct 2024 15:23:52 +0200 Subject: [PATCH 07/35] feat: limit project management pages p.o.c. --- .../ProjectManagerPage/ProjectAnatomy.jsx | 16 ++++++++++-- .../ProjectManagerPage/ProjectManagerPage.jsx | 26 +++++++++++-------- 2 files changed, 29 insertions(+), 13 deletions(-) diff --git a/src/pages/ProjectManagerPage/ProjectAnatomy.jsx b/src/pages/ProjectManagerPage/ProjectAnatomy.jsx index 60cd02400..ec902a90d 100644 --- a/src/pages/ProjectManagerPage/ProjectAnatomy.jsx +++ b/src/pages/ProjectManagerPage/ProjectAnatomy.jsx @@ -2,6 +2,7 @@ import { toast } from 'react-toastify' import { useState } from 'react' import { ScrollPanel, SaveButton, Spacer, Button } from '@ynput/ayon-react-components' import { useUpdateProjectAnatomyMutation } from '@queries/project/updateProject' +import { useGetCurrentUserProjectPermissionsQuery } from '@queries/permissions/getPermissions' import ProjectManagerPageLayout from './ProjectManagerPageLayout' import AnatomyEditor from '@containers/AnatomyEditor' @@ -15,6 +16,15 @@ const ProjectAnatomy = ({ projectName, projectList }) => { const [updateProjectAnatomy, { isLoading: isUpdating }] = useUpdateProjectAnatomyMutation() const { requestPaste } = usePaste() + + const { data: permissions } = useGetCurrentUserProjectPermissionsQuery({ + projectName: projectName, + }) + + const accessLevel = permissions?.project?.enabled ? permissions.project.anatomy : 2 + //const accessLevel = 2 + + const saveAnatomy = () => { updateProjectAnatomy({ projectName, anatomy: formData }) .unwrap() @@ -62,23 +72,25 @@ const ProjectAnatomy = ({ projectName, projectList }) => { }} /> + + {!isUnassigned && ( + + )} + + ) +} + +export default UserRow diff --git a/src/pages/ProjectManagerPage/Users/mappers.ts b/src/pages/ProjectManagerPage/Users/mappers.ts index 0443d17ae..289d68607 100644 --- a/src/pages/ProjectManagerPage/Users/mappers.ts +++ b/src/pages/ProjectManagerPage/Users/mappers.ts @@ -1,12 +1,6 @@ -type ProjectUsersResponse = { - [key: string]: string[] -} - -type AccessGroupUsers = { - [key: string]: string[] -} +import { AccessGroupUsers, ProjectUsersResponse } from './types' -const getAllProjectUsers = (groupedUsers: AccessGroupUsers): string[] => { +const getAllProjectUsers = (groupedUsers: AccessGroupUsers): string[] => { let allUsers: string[] = [] for (const [_, users] of Object.entries(groupedUsers)) { allUsers.push(...users) @@ -22,7 +16,6 @@ const mapUsersByAccessGroups = (response: ProjectUsersResponse | undefined): Acc const groupedUsers: { [key: string]: string[] } = {} for (const [user, acessGroupsList] of Object.entries(response)) { - console.log(user) for (const accessGroup of acessGroupsList) { if (groupedUsers[accessGroup] === undefined) { groupedUsers[accessGroup] = [] diff --git a/src/pages/ProjectManagerPage/Users/types.ts b/src/pages/ProjectManagerPage/Users/types.ts new file mode 100644 index 000000000..6f821b06a --- /dev/null +++ b/src/pages/ProjectManagerPage/Users/types.ts @@ -0,0 +1,12 @@ +export type ProjectUsersResponse = { + [key: string]: string[] +} + +export type AccessGroupUsers = { + [key: string]: string[] +} + +export type SelectedAccessGroupUsers = { + accessGroup: string + users: string[] +} From 1e2bc5798efdaf38c841d41f1fcfc98de9aaa9c6 Mon Sep 17 00:00:00 2001 From: Florin Tudor Date: Thu, 7 Nov 2024 09:09:59 +0100 Subject: [PATCH 16/35] feature(Users): Extracting logic to hook, added remove handler --- .../Users/ProjectUserList.tsx | 10 ++ .../ProjectManagerPage/Users/ProjectUsers.tsx | 148 +++++++++--------- .../ProjectManagerPage/Users/UserRow.tsx | 17 +- src/pages/ProjectManagerPage/Users/hooks.ts | 58 +++++++ 4 files changed, 153 insertions(+), 80 deletions(-) create mode 100644 src/pages/ProjectManagerPage/Users/hooks.ts diff --git a/src/pages/ProjectManagerPage/Users/ProjectUserList.tsx b/src/pages/ProjectManagerPage/Users/ProjectUserList.tsx index 266e47fd5..5f0d9c61d 100644 --- a/src/pages/ProjectManagerPage/Users/ProjectUserList.tsx +++ b/src/pages/ProjectManagerPage/Users/ProjectUserList.tsx @@ -20,6 +20,8 @@ type Props = { isUnassigned?: boolean onContextMenu?: $Any onSelectUsers?: (selectedUsers: string[]) => void + onAdd: () => void + onRemove?: (user: string) => void } const ProjectUserList = ({ @@ -30,6 +32,8 @@ const ProjectUserList = ({ header, sortable = false, isUnassigned = false, + onAdd, + onRemove, onContextMenu, onSelectUsers, }: Props) => { @@ -74,6 +78,12 @@ const ProjectUserList = ({ { + onAdd() + }} + onRemove={() => { + onRemove && onRemove(rowData.name) + }} showButtonsOnHover={selectedUnassignedUsers.length == 0} selected={selectedUnassignedUserNames.includes(rowData.name)} /> diff --git a/src/pages/ProjectManagerPage/Users/ProjectUsers.tsx b/src/pages/ProjectManagerPage/Users/ProjectUsers.tsx index 1459f7783..f1187cdcc 100644 --- a/src/pages/ProjectManagerPage/Users/ProjectUsers.tsx +++ b/src/pages/ProjectManagerPage/Users/ProjectUsers.tsx @@ -1,106 +1,96 @@ import { UserNode } from '@api/graphql' -import { useGetUsersQuery } from '@queries/user/getUsers' import { $Any } from '@types' import { Button, SaveButton, Spacer, Toolbar } from '@ynput/ayon-react-components' import { Splitter, SplitterPanel } from 'primereact/splitter' import { useState } from 'react' -import { useSelector } from 'react-redux' import ProjectUserList from './ProjectUserList' import SearchFilter from '@components/SearchFilter/SearchFilter' -import { useListProjectsQuery } from '@queries/project/getProject' import { useGetAccessGroupsQuery } from '@queries/accessGroups/getAccessGroups' import AssignAccessGroupsDialog from './AssignAccessGroupsDialog' -import {api }from '@api/rest/project' -import { useUpdateProjectUsersMutation } from '@queries/project/updateProject' -import { toast } from 'react-toastify' import ProjectList from '@containers/projectList' -import { getAllProjectUsers, mapUsersByAccessGroups } from './mappers' import { SelectedAccessGroupUsers } from './types' +import { useProjectAccessGroupData } from './hooks' +import { toast } from 'react-toastify' +import { useGetUsersQuery } from '@queries/user/getUsers' +import { getAllProjectUsers, mapUsersByAccessGroups } from './mappers' +import { useSelector } from 'react-redux' type Props = {} const ProjectUsers = ({}: Props) => { - const selfName = useSelector((state: $Any) => state.user.name) - let { data: userList = [], isLoading } = useGetUsersQuery({ selfName }) - - const { data: projectsList = [] } = useListProjectsQuery({}) - // Load user list const { data: accessGroupList = [] } = useGetAccessGroupsQuery({ projectName: '_', }) - const [updateUser] = useUpdateProjectUsersMutation() - // const accessGroups = { artist: ['project_a', 'project_b', 'project_c'], freelancer: [], supervisor: [] } // const { allProjects, activeProjects } = getProjectsListForSelection([], accessGroups) - // const [localActiveProjects, setLocalActiveProjects] = useState<$Any[]>(activeProjects) // const [selectedAccessGroups, setSelectedAccessGroups] = useState<$Any>([]) - const [selectedProjects, setSelectedProjects] = useState([]) - const [selectedUsers, setSelectedUsers] = useState([]) - const [showDialog, setShowDialog] = useState(false) - const [selectedAccessGroupUsers, setSelectedAccessGroupUsers] = useState() - - - - const onSelectProjects = (selection: $Any) => { - setSelectedProjects([selection]) - } - - const actionEnabled = selectedProjects.length > 0 && selectedUsers.length > 0 + // const onSelectProjects = (selection: $Any) => { setSelectedProjects([selection]) } + // const onFiltersChange = (changes: $Any) => { console.log('on change? ', changes) } + // const onFiltersFinish = (changes: $Any) => { console.log('on filters finish: ', changes) } + + const { + users: projectUsers, + accessGroupUsers, + selectedProjects, + setSelectedProjects, + removeUserAccessGroup, + updateUserAccessGroups, + } = useProjectAccessGroupData() + const selfName = useSelector((state: $Any) => state.user.name) + let { data: userList = [], isLoading } = useGetUsersQuery({ selfName }) const activeNonManagerUsers = userList.filter( (user: UserNode) => !user.isAdmin && !user.isManager && user.active, ) - const result = api.useGetProjectUsersQuery({ projectName: selectedProjects[0] || '_' }) - const mappedUsers = mapUsersByAccessGroups(result.data) + const mappedUsers = mapUsersByAccessGroups(projectUsers) const allProjectUsers = getAllProjectUsers(mappedUsers) - const unasignedUsers = activeNonManagerUsers.filter((user: UserNode) => !allProjectUsers.includes(user.name)) + const unassignedUsers = activeNonManagerUsers.filter((user: UserNode) => !allProjectUsers.includes(user.name)) + + + const [selectedUsers, setSelectedUsers] = useState([]) + const [showDialog, setShowDialog] = useState(false) + const [selectedAccessGroupUsers, setSelectedAccessGroupUsers] = useState< + SelectedAccessGroupUsers | undefined + >() + + + const actionEnabled = selectedProjects.length > 0 && selectedUsers.length > 0 const getAccessGroupUsers = (accessGroup?: string): string[] => { if (!selectedAccessGroupUsers || !accessGroup) { return [] } - return selectedAccessGroupUsers.accessGroup === accessGroup ? selectedAccessGroupUsers.users : [] + return selectedAccessGroupUsers.accessGroup === accessGroup + ? selectedAccessGroupUsers.users + : [] } - const onFiltersChange = (changes: $Any) => { - console.log('on change? ', changes) - } - - const onFiltersFinish = (changes: $Any) => { - console.log('on filters finish: ', changes) - } const updateSelectedAccessGroupUsers = (accessGroup: string, selectedUsers: string[]) => { setSelectedAccessGroupUsers({ accessGroup, users: selectedUsers }) } const onSave = async (changes: $Any) => { - for (const user of selectedUsers) { - const accessGroups = changes.filter((ag: $Any) => ag.selected).map((ag: $Any) => ag.name) - - try { - await updateUser({ - projectName: selectedProjects, - userName: user, - update: accessGroups - }).unwrap() - } catch (error: $Any) { - console.log(error) - toast.error(error.details) - } + const errorMessage = await updateUserAccessGroups(selectedUsers, changes) + if (errorMessage) { + toast.error(errorMessage) } } + const onRemove = (accessGroup: string) => (user: string) => { + removeUserAccessGroup(user, accessGroup) + } return (
+ {/* @ts-ignore */} onFiltersChange(v)} - onFinish={(v) => onFiltersFinish(v)} // when changes are applied + // onChange={(v) => onFiltersChange(v)} + // onFinish={(v) => onFiltersFinish(v)} // when changes are applied options={[]} /> @@ -134,9 +124,10 @@ const ProjectUsers = ({}: Props) => { { }} onSelectUsers={(selection: string[]) => setSelectedUsers(selection)} sortable isUnassigned @@ -145,29 +136,30 @@ const ProjectUsers = ({}: Props) => { - {Object.keys(mappedUsers) - .map((accessGroup) => { - return ( - - - mappedUsers[accessGroup].includes(user.name), - )} - onSelectUsers={(selection: string[]) => - updateSelectedAccessGroupUsers(accessGroup, selection) - } - isLoading={isLoading} - /> - - ) - })} + {Object.keys(mappedUsers).sort().map((accessGroup) => { + return ( + + + mappedUsers[accessGroup].includes(user.name), + )} + onSelectUsers={(selection: string[]) => + updateSelectedAccessGroupUsers(accessGroup, selection) + } + onAdd={() => {}} + onRemove={onRemove(accessGroup)} + isLoading={isLoading} + /> + + ) + })} diff --git a/src/pages/ProjectManagerPage/Users/UserRow.tsx b/src/pages/ProjectManagerPage/Users/UserRow.tsx index ebd5e8983..8f08a6550 100644 --- a/src/pages/ProjectManagerPage/Users/UserRow.tsx +++ b/src/pages/ProjectManagerPage/Users/UserRow.tsx @@ -2,8 +2,8 @@ import { Button } from '@ynput/ayon-react-components' import UserImage from '@components/UserImage' import styled from 'styled-components' -import { $Any } from '@types' import clsx from 'clsx' +import { $Any } from '@types' const StyledProfileRow = styled.div` display: flex; @@ -28,12 +28,23 @@ const StyledProfileRow = styled.div` } } ` +type Props = { + rowData: $Any + selected: boolean + isUnassigned: boolean + showButtonsOnHover: boolean + onAdd: () => void + onRemove?: () => void +} + export const UserRow = ({ rowData, selected = false, isUnassigned = false, showButtonsOnHover = false, -}: $Any) => { + onAdd, + onRemove, +}: Props) => { const { name, self, isMissing } = rowData return ( @@ -64,6 +75,7 @@ export const UserRow = ({ icon={'add'} onClick={(e) => { e.stopPropagation() + onAdd() }} > {isUnassigned ? ( @@ -83,6 +95,7 @@ export const UserRow = ({ variant="filled" onClick={(e) => { e.stopPropagation() + onRemove!() }} > {isUnassigned ? ( diff --git a/src/pages/ProjectManagerPage/Users/hooks.ts b/src/pages/ProjectManagerPage/Users/hooks.ts new file mode 100644 index 000000000..68ce58baf --- /dev/null +++ b/src/pages/ProjectManagerPage/Users/hooks.ts @@ -0,0 +1,58 @@ +import { $Any } from "@types" +import { api } from '@api/rest/project' +import { useState } from "react" +import { useUpdateProjectUsersMutation } from "@queries/project/updateProject" +import { useDispatch } from "react-redux" + +const useProjectAccessGroupData = () => { + const dispatch = useDispatch() + const [updateUser] = useUpdateProjectUsersMutation() + + const [selectedProjects, setSelectedProjects] = useState([]) + const [projectUsers, setProjectUsers] = useState<$Any>({}) + + const result = api.useGetProjectUsersQuery({ projectName: selectedProjects[0] || '_' }) + const users = result.data + + + const accessGroupUsers: $Any = {} + const removeUserAccessGroup = (user: string, accessGroup: string) => { + setProjectUsers({}) + const updatedAccessGroups = users![user].filter((item: string) => item !== accessGroup) + updateUser({ + projectName: selectedProjects, + userName: user, + update: updatedAccessGroups, + }) + dispatch( + // @ts-ignore + api.util.updateQueryData( + 'getProjectUsers', + { projectName: selectedProjects[0] }, + (draft: $Any) => { + draft[user] = updatedAccessGroups + }, + ), + ) + } + const updateUserAccessGroups = async (users: $Any, changes: $Any ): Promise => { + for (const user of users) { + const accessGroups = changes.filter((ag: $Any) => ag.selected).map((ag: $Any) => ag.name) + + try { + await updateUser({ + projectName: selectedProjects, + userName: user, + update: accessGroups, + }).unwrap() + } catch (error: $Any) { + console.log(error) + return(error.details) + } + } + } + + return { users, projectUsers, accessGroupUsers, selectedProjects, setSelectedProjects, removeUserAccessGroup, updateUserAccessGroups } +} + +export { useProjectAccessGroupData } From dbe12e6ad7e88a70a8e9991fbdbd94d7a68157b6 Mon Sep 17 00:00:00 2001 From: Florin Tudor Date: Thu, 7 Nov 2024 10:44:31 +0100 Subject: [PATCH 17/35] feature(Users): Using add handler for users already asssigned groups also --- .../Users/AssignAccessGroupsDialog.tsx | 10 ++- .../ProjectManagerPage/Users/ProjectUsers.tsx | 79 +++++++++---------- src/pages/ProjectManagerPage/Users/hooks.ts | 28 ++++--- 3 files changed, 60 insertions(+), 57 deletions(-) diff --git a/src/pages/ProjectManagerPage/Users/AssignAccessGroupsDialog.tsx b/src/pages/ProjectManagerPage/Users/AssignAccessGroupsDialog.tsx index bab2fd64d..a23c654a6 100644 --- a/src/pages/ProjectManagerPage/Users/AssignAccessGroupsDialog.tsx +++ b/src/pages/ProjectManagerPage/Users/AssignAccessGroupsDialog.tsx @@ -6,15 +6,17 @@ import * as Styled from './AssignAccessGroupsDialog.styled' type Props = { accessGroups: $Any[] - onSave: (items: AccessGroupItem[]) => void + users: string[] + onSave: (items: AccessGroupItem[], users: string[]) => void onClose: () => void } + type AccessGroupItem = { name: string selected: boolean } -const AssignAccessGroupsDialog = ({ accessGroups, onSave, onClose }: Props) => { +const AssignAccessGroupsDialog = ({ accessGroups, users, onSave, onClose }: Props) => { const [accessGroupItems, setAccessGroupItems] = useState(accessGroups) const toggleAccessGroup = (accessGroup: AccessGroupItem) => { @@ -28,14 +30,14 @@ const AssignAccessGroupsDialog = ({ accessGroups, onSave, onClose }: Props) => { } const handleSave = () => { - onSave(accessGroupItems) + onSave(accessGroupItems, users) onClose() } return ( handleSave()} />} isOpen={true} onClose={handleClose} diff --git a/src/pages/ProjectManagerPage/Users/ProjectUsers.tsx b/src/pages/ProjectManagerPage/Users/ProjectUsers.tsx index f1187cdcc..a61d97e12 100644 --- a/src/pages/ProjectManagerPage/Users/ProjectUsers.tsx +++ b/src/pages/ProjectManagerPage/Users/ProjectUsers.tsx @@ -51,6 +51,7 @@ const ProjectUsers = ({}: Props) => { const [selectedUsers, setSelectedUsers] = useState([]) + const [actionedUsers, setActionedUsers] = useState([]) const [showDialog, setShowDialog] = useState(false) const [selectedAccessGroupUsers, setSelectedAccessGroupUsers] = useState< SelectedAccessGroupUsers | undefined @@ -73,8 +74,8 @@ const ProjectUsers = ({}: Props) => { setSelectedAccessGroupUsers({ accessGroup, users: selectedUsers }) } - const onSave = async (changes: $Any) => { - const errorMessage = await updateUserAccessGroups(selectedUsers, changes) + const onSave = async (changes: $Any, users: string[]) => { + const errorMessage = await updateUserAccessGroups(users, changes) if (errorMessage) { toast.error(errorMessage) } @@ -93,21 +94,6 @@ const ProjectUsers = ({}: Props) => { // onFinish={(v) => onFiltersFinish(v)} // when changes are applied options={[]} /> - - )} diff --git a/src/pages/ProjectManagerPage/Users/hooks.ts b/src/pages/ProjectManagerPage/Users/hooks.ts index e394697da..b9955fd19 100644 --- a/src/pages/ProjectManagerPage/Users/hooks.ts +++ b/src/pages/ProjectManagerPage/Users/hooks.ts @@ -3,6 +3,7 @@ import { api } from '@api/rest/project' import { useState } from "react" import { useUpdateProjectUsersMutation } from "@queries/project/updateProject" import { useDispatch } from "react-redux" +import { SelectionStatus } from "./types" const useProjectAccessGroupData = () => { @@ -23,7 +24,6 @@ const useProjectAccessGroupData = () => { const [updateUser] = useUpdateProjectUsersMutation() const [selectedProjects, setSelectedProjects] = useState([]) - const [projectUsers, setProjectUsers] = useState<$Any>({}) const result = api.useGetProjectUsersQuery({ projectName: selectedProjects[0] || '_' }) const users = result.data @@ -31,34 +31,59 @@ const useProjectAccessGroupData = () => { const accessGroupUsers: $Any = {} const removeUserAccessGroup = (user: string, accessGroup: string) => { - setProjectUsers({}) const updatedAccessGroups = users![user].filter((item: string) => item !== accessGroup) - updateUser({ - projectName: selectedProjects, - userName: user, - update: updatedAccessGroups, - }) - udpateApiCache(selectedProjects[0], user, updatedAccessGroups) - } - const updateUserAccessGroups = async (users: $Any, changes: $Any ): Promise => { - for (const user of users) { - const accessGroups = changes.filter((ag: $Any) => ag.selected).map((ag: $Any) => ag.name) - + for (const project of selectedProjects) { try { - await updateUser({ - projectName: selectedProjects[0], + updateUser({ + projectName: project, userName: user, - update: accessGroups, - }).unwrap() - udpateApiCache(selectedProjects[0], user, accessGroups) + update: updatedAccessGroups, + }) + udpateApiCache(project, user, updatedAccessGroups) } catch (error: $Any) { console.log(error) - return(error.details) + return error.details + } + } + } + + const updateUserAccessGroups = async (selectedUsers: $Any, changes: {name: string, status: SelectionStatus}[] ): Promise => { + const updatedAccessGroups = ( + existing: string[], + changes: { name: string; status: SelectionStatus }[], + ): string[] => { + const existingSet = new Set(existing) + for (const change of changes) { + if (change.status == SelectionStatus.All) { + existingSet.add(change.name) + } else { + existingSet.delete(change.name) + } } + + return [...existingSet] + } + + for (const user of selectedUsers) { + const accessGroups = updatedAccessGroups(users?.[user] || [], changes) + + for (const project of selectedProjects) { + try { + await updateUser({ + projectName: project, + userName: user, + update: accessGroups, + }).unwrap() + udpateApiCache(project, user, accessGroups) + } catch (error: $Any) { + console.log(error) + return error.details + } + } } } - return { users, projectUsers, accessGroupUsers, selectedProjects, setSelectedProjects, removeUserAccessGroup, updateUserAccessGroups } + return { users, accessGroupUsers, selectedProjects, setSelectedProjects, removeUserAccessGroup, updateUserAccessGroups } } export { useProjectAccessGroupData } diff --git a/src/pages/ProjectManagerPage/Users/types.ts b/src/pages/ProjectManagerPage/Users/types.ts index 6f821b06a..7539eb02e 100644 --- a/src/pages/ProjectManagerPage/Users/types.ts +++ b/src/pages/ProjectManagerPage/Users/types.ts @@ -7,6 +7,13 @@ export type AccessGroupUsers = { } export type SelectedAccessGroupUsers = { - accessGroup: string + accessGroup?: string users: string[] } + + +export enum SelectionStatus { + None = 'none', + Mixed = 'mixed', + All = 'all', +} \ No newline at end of file From 8cf585a79ef6fa7213aab8580d9ffcbbfa49aefb Mon Sep 17 00:00:00 2001 From: Florin Tudor Date: Fri, 8 Nov 2024 08:50:23 +0100 Subject: [PATCH 19/35] feature(Users): Re-enabled multiple users remove action --- src/pages/ProjectManagerPage/Users/ProjectUsers.tsx | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/pages/ProjectManagerPage/Users/ProjectUsers.tsx b/src/pages/ProjectManagerPage/Users/ProjectUsers.tsx index 77d855d43..fb7b4b686 100644 --- a/src/pages/ProjectManagerPage/Users/ProjectUsers.tsx +++ b/src/pages/ProjectManagerPage/Users/ProjectUsers.tsx @@ -29,11 +29,6 @@ const ProjectUsers = () => { projectName: '_', }) - // const accessGroups = { artist: ['project_a', 'project_b', 'project_c'], freelancer: [], supervisor: [] } - // const { allProjects, activeProjects } = getProjectsListForSelection([], accessGroups) - // const [localActiveProjects, setLocalActiveProjects] = useState<$Any[]>(activeProjects) - // const [selectedAccessGroups, setSelectedAccessGroups] = useState<$Any>([]) - // const onSelectProjects = (selection: $Any) => { setSelectedProjects([selection]) } // const onFiltersChange = (changes: $Any) => { console.log('on change? ', changes) } // const onFiltersFinish = (changes: $Any) => { console.log('on filters finish: ', changes) } @@ -144,6 +139,8 @@ const ProjectUsers = () => { disabled={!actionEnabled} onClick={(e) => { e.stopPropagation() + setActionedUsers(getSelectedUsers()) + onRemove(selectedAccessGroupUsers!.accessGroup!)() }} > Remove R{' '} From 1b47bbd675706eeab28e3a9ab5e9f277a8f6f0b6 Mon Sep 17 00:00:00 2001 From: Florin Tudor Date: Fri, 8 Nov 2024 18:05:58 +0100 Subject: [PATCH 20/35] feature(Users): Added filtering for projects & users --- .../ProjectAccessSearchFilterWrapper.tsx | 57 ++++++++++++ .../ProjectManagerPage/Users/ProjectList.tsx | 88 +++++++++++-------- .../Users/ProjectUserList.tsx | 38 +++++++- .../ProjectManagerPage/Users/ProjectUsers.tsx | 24 ++--- src/pages/ProjectManagerPage/Users/hooks.ts | 25 +++++- 5 files changed, 177 insertions(+), 55 deletions(-) create mode 100644 src/pages/ProjectManagerPage/Users/ProjectAccessSearchFilterWrapper.tsx diff --git a/src/pages/ProjectManagerPage/Users/ProjectAccessSearchFilterWrapper.tsx b/src/pages/ProjectManagerPage/Users/ProjectAccessSearchFilterWrapper.tsx new file mode 100644 index 000000000..d39c8a750 --- /dev/null +++ b/src/pages/ProjectManagerPage/Users/ProjectAccessSearchFilterWrapper.tsx @@ -0,0 +1,57 @@ +import SearchFilter from '@components/SearchFilter/SearchFilter' +import { Filter } from '@components/SearchFilter/types' +import { useEffect, useState } from 'react' +import { useProjectAccessSearchFilterBuiler } from './hooks' +import { $Any } from '@types' +import { useListProjectsQuery } from '@queries/project/getProject' +import { useGetAccessGroupsQuery } from '@queries/accessGroups/getAccessGroups' +import { useSelector } from 'react-redux' +import { useGetUsersQuery } from '@queries/user/getUsers' + +type Props = { + filters: $Any, + onChange: $Any +} + +const ProjectAccessSearchFilterWrapper = ({ filters: _filters, onChange }: Props) => { + const selfName = useSelector((state: $Any) => state.user.name) + const { isLoading: isProjectsLoading, data: projects = [] } = useListProjectsQuery({}) + const { isLoading: isUsersLoading, data: users = [] } = useGetUsersQuery({ selfName }) + const { isLoading: isAccessGroupsLoading, data: accessGroups = [] } = useGetAccessGroupsQuery({ + projectName: '_', + }) + + const options = useProjectAccessSearchFilterBuiler({ + projects: isProjectsLoading + ? [] + // @ts-ignore + : projects.map((project: $Any) => ({ id: project.name, label: project.name })), + users: isUsersLoading ? [] : users.map((user: $Any) => ({ id: user.name, label: user.name, img: `/api/users/${user.name}/avatar` })), + accessGroups: isAccessGroupsLoading + ? [] + : accessGroups!.map((accessGroup: $Any) => ({ + id: accessGroup.name, + label: accessGroup.name, + })), + }) + + // keeps track of the filters whilst adding/removing filters + const [filters, setFilters] = useState(_filters) + + // update filters when it changes + useEffect(() => { + setFilters(_filters) + }, [_filters, setFilters]) + + return ( + onChange(v)} + /> + ) +} + +export default ProjectAccessSearchFilterWrapper diff --git a/src/pages/ProjectManagerPage/Users/ProjectList.tsx b/src/pages/ProjectManagerPage/Users/ProjectList.tsx index c18bbea8e..9246a3cbd 100644 --- a/src/pages/ProjectManagerPage/Users/ProjectList.tsx +++ b/src/pages/ProjectManagerPage/Users/ProjectList.tsx @@ -7,6 +7,8 @@ import useTableLoadingData from '@hooks/useTableLoadingData' import { $Any } from '@types' import { useRef } from 'react' import { TablePanel } from '@ynput/ayon-react-components' +import { ProjectNode } from '@api/graphql' +import { Filter } from '@components/SearchFilter/types' const formatName = (rowData: $Any, field: string) => { return rowData[field] @@ -27,11 +29,6 @@ const StyledProjectName = styled.div` opacity: 0; } - &:not(.isActive) { - font-style: italic; - color: var(--md-ref-palette-secondary50); - } - &:not(.isOpen) { span:first-child { opacity: 0; @@ -45,50 +42,63 @@ const StyledProjectName = styled.div` type Props = { className: string selection: string[] - onSelectionChange: (selection: $Any) => {} + filters: Filter[] + setSelection: $Any + onSelectionChange: (selection: $Any) => void } -const ProjectList = ({ selection, onSelectionChange }: Props) => { - const tableRef = useRef(null) +const ProjectList = ({ selection, onSelectionChange, filters }: Props) => { + console.log('filters', filters) const { data: projects = [], isLoading, isError, error } = useListProjectsQuery({}) if (isError) { console.error(error) } - const projectList = projects + const getFilteredProjects = (projects: ProjectNode[], filters: Filter) => { + if (!filters) { + return projects + } + + const filterProjects = filters && filters.values!.map((match: Filter) => match.id) + if (filters!.inverted) { + return projects.filter((project: ProjectNode) => !filterProjects.includes(project.name)) + } + return projects.filter((project: ProjectNode) => filterProjects.includes(project.name)) + } + + // @ts-ignore + const projectList = getFilteredProjects(projects, filters) const tableData = useTableLoadingData(projectList, isLoading, 10, 'name') - console.log('td: ', tableData) - console.log('sel: ', selection) + const selected = tableData.filter((project: ProjectNode) => selection.includes(project.name)) return ( - - - ({ loading: isLoading })} - onSelectionChange={(selection) => { - console.log(selection) - return onSelectionChange(selection.value) - }} - > - ( - - {formatName(rowData, 'name')} - - )} - style={{ minWidth: 150 }} - /> - - + + ({ loading: isLoading })} + onSelectionChange={(selection) => { + console.log(selection) + onSelectionChange(selection.value.map((project: ProjectNode) => project.name)) + }} + > + ( + + {formatName(rowData, 'name')} + + )} + style={{ minWidth: 150 }} + /> + + ) } diff --git a/src/pages/ProjectManagerPage/Users/ProjectUserList.tsx b/src/pages/ProjectManagerPage/Users/ProjectUserList.tsx index 16d5a5816..3d11b665f 100644 --- a/src/pages/ProjectManagerPage/Users/ProjectUserList.tsx +++ b/src/pages/ProjectManagerPage/Users/ProjectUserList.tsx @@ -4,15 +4,16 @@ import { Column } from 'primereact/column' import { TablePanel, Section } from '@ynput/ayon-react-components' import clsx from 'clsx' -import useTableLoadingData from '@hooks/useTableLoadingData' import { $Any } from '@types' import { UserNode } from '@api/graphql' import UserRow from './UserRow' +import { Filter } from '@components/SearchFilter/types' type Props = { selectedProjects: string[] selectedUsers: string[] tableList: $Any + filters?: Filter isLoading: boolean header?: string emptyMessage: string @@ -28,6 +29,7 @@ const ProjectUserList = ({ selectedProjects, selectedUsers, tableList, + filters, isLoading, header, emptyMessage, @@ -56,8 +58,36 @@ const ProjectUserList = ({ } } - const tableData = useTableLoadingData(tableList, isLoading, 40, 'name') - const selectedUnassignedUsers = tableData.filter((user: $Any) => selectedUsers.includes(user.name)) + const getFilteredUsers = (users: UserNode[], filters?: Filter) => { + const exactFilter = (users: UserNode[], filters: Filter) => { + const filterUsers = filters && filters.values!.map((match: Filter) => match.id) + if (filters!.inverted) { + return users.filter((user: UserNode) => !filterUsers.includes(user.name)) + } + return users.filter((user: UserNode) => filterUsers.includes(user.name)) + } + + const fuzzyFilter = (users: UserNode[], filters: Filter) => { + const filterString = filters.values![0].id + if (filters!.inverted) { + return users.filter((user: UserNode) => user.name.indexOf(filterString) == -1) + } + return users.filter((user: UserNode) => user.name.indexOf(filterString) != -1) + } + + if (!filters || !filters.values || filters.values.length == 0) { + return users + } + + if (filters.values!.length == 1 && filters.values[0]!.isCustom) { + return fuzzyFilter(users, filters) + } + + return exactFilter(users, filters) + } + + const filteredUsers = getFilteredUsers(tableList, filters) + const selectedUnassignedUsers = filteredUsers.filter((user: $Any) => selectedUsers.includes(user.name)) const selectedUnassignedUserNames = selectedUnassignedUsers.map((user: $Any) => user.name) // Render return ( @@ -65,7 +95,7 @@ const ProjectUserList = ({ { projectName: '_', }) - // const onFiltersChange = (changes: $Any) => { console.log('on change? ', changes) } - // const onFiltersFinish = (changes: $Any) => { console.log('on filters finish: ', changes) } - const { users: projectUsers, selectedProjects, @@ -54,6 +52,7 @@ const ProjectUsers = () => { const [actionedUsers, setActionedUsers] = useState([]) const [showDialog, setShowDialog] = useState(false) + const [filters, setFilters] = useState<$Any>([]) const [selectedAccessGroupUsers, setSelectedAccessGroupUsers] = useState< SelectedAccessGroupUsers | undefined >() @@ -113,11 +112,9 @@ const ProjectUsers = () => {
{/* @ts-ignore */} - onFiltersChange(v)} - // onFinish={(v) => onFiltersFinish(v)} // when changes are applied - options={[]} + setFilters(results)} /> { minSize={10} > {/* @ts-ignore */} - + el.label === 'Project')} + selection={selectedProjects} + onSelectionChange={(selection: $Any) => setSelectedProjects(selection)} + /> @@ -165,6 +166,7 @@ const ProjectUsers = () => { selectedProjects={selectedProjects} selectedUsers={getSelectedUsers()} tableList={unassignedUsers} + filters={filters?.find((el: Filter) => el.label === 'User')} isLoading={isLoading} onAdd={handleAdd} onSelectUsers={(selection) => setSelectedAccessGroupUsers({ users: selection })} diff --git a/src/pages/ProjectManagerPage/Users/hooks.ts b/src/pages/ProjectManagerPage/Users/hooks.ts index b9955fd19..8bcddfb39 100644 --- a/src/pages/ProjectManagerPage/Users/hooks.ts +++ b/src/pages/ProjectManagerPage/Users/hooks.ts @@ -4,6 +4,12 @@ import { useState } from "react" import { useUpdateProjectUsersMutation } from "@queries/project/updateProject" import { useDispatch } from "react-redux" import { SelectionStatus } from "./types" +import { Option } from "@components/SearchFilter/types" + +type FilterValues = { + id: string, + label: string +} const useProjectAccessGroupData = () => { @@ -86,4 +92,21 @@ const useProjectAccessGroupData = () => { return { users, accessGroupUsers, selectedProjects, setSelectedProjects, removeUserAccessGroup, updateUserAccessGroups } } -export { useProjectAccessGroupData } + +const useProjectAccessSearchFilterBuiler = ({ + projects, + users, + accessGroups, +}: { + [key: string]: FilterValues[] +}) => { + const options: Option[] = [ + { id: 'project', label: 'Project', icon: 'deployed_code', values: projects, allowsCustomValues: true }, + { id: 'user', label: 'User', icon: 'person', values: users, allowsCustomValues: true }, + { id: 'accessGroup', label: 'Access Group', icon: 'key', values: accessGroups, allowsCustomValues: true }, + ] + + return options +} + +export { useProjectAccessGroupData, useProjectAccessSearchFilterBuiler } From db466744ef5c091dbc073825e24e1320ce335629 Mon Sep 17 00:00:00 2001 From: Florin Tudor Date: Mon, 11 Nov 2024 08:41:42 +0100 Subject: [PATCH 21/35] feature(Users): Filtering access groups also, respecting filtering when selections exist, renamed components for consistency --- src/hooks/useUserProjectPermissions.ts | 19 ++- .../ProjectManagerPage/ProjectManagerPage.jsx | 9 +- ...ProjectUsers.tsx => ProjectUserAccess.tsx} | 157 +++++++++--------- ...> ProjectUserAccessAssignDialog.styled.js} | 0 ....tsx => ProjectUserAccessAssignDialog.tsx} | 6 +- ...t.tsx => ProjectUserAccessProjectList.tsx} | 27 +-- ... ProjectUserAccessSearchFilterWrapper.tsx} | 4 +- ...List.tsx => ProjectUserAccessUserList.tsx} | 37 +---- src/pages/ProjectManagerPage/Users/hooks.ts | 10 +- src/pages/ProjectManagerPage/Users/mappers.ts | 106 +++++++++++- 10 files changed, 226 insertions(+), 149 deletions(-) rename src/pages/ProjectManagerPage/Users/{ProjectUsers.tsx => ProjectUserAccess.tsx} (57%) rename src/pages/ProjectManagerPage/Users/{AssignAccessGroupsDialog.styled.js => ProjectUserAccessAssignDialog.styled.js} (100%) rename src/pages/ProjectManagerPage/Users/{AssignAccessGroupsDialog.tsx => ProjectUserAccessAssignDialog.tsx} (95%) rename src/pages/ProjectManagerPage/Users/{ProjectList.tsx => ProjectUserAccessProjectList.tsx} (69%) rename src/pages/ProjectManagerPage/Users/{ProjectAccessSearchFilterWrapper.tsx => ProjectUserAccessSearchFilterWrapper.tsx} (92%) rename src/pages/ProjectManagerPage/Users/{ProjectUserList.tsx => ProjectUserAccessUserList.tsx} (69%) diff --git a/src/hooks/useUserProjectPermissions.ts b/src/hooks/useUserProjectPermissions.ts index 4febaa3eb..7679366ee 100644 --- a/src/hooks/useUserProjectPermissions.ts +++ b/src/hooks/useUserProjectPermissions.ts @@ -15,21 +15,29 @@ enum UserPermissionsEntity { class UserPermissions { permissions: GetCurrentUserPermissionsApiResponse + isUser: boolean - constructor(permissions: GetCurrentUserPermissionsApiResponse) { + constructor(permissions: GetCurrentUserPermissionsApiResponse, isUser: boolean = false) { this.permissions = permissions + this.isUser = isUser } canCreateProject(): boolean { + if (!this.isUser) { + return true + } return this.projectSettingsAreEnabled() && this.permissions?.project?.create || false } getPermissionLevel(type: UserPermissionsEntity): UserPermissionsLevel { + if (!this.isUser) { + return UserPermissionsLevel.readWrite + } return this.permissions?.project?.[type]|| UserPermissionsLevel.readWrite } canEdit(type: UserPermissionsEntity): boolean { - if (!this.projectSettingsAreEnabled()) { + if (!this.isUser || !this.projectSettingsAreEnabled()) { return true } @@ -37,14 +45,13 @@ class UserPermissions { } canView(type: UserPermissionsEntity): boolean { - if (!this.projectSettingsAreEnabled()) { + if (!this.isUser || !this.projectSettingsAreEnabled()) { return true } return ( this.canEdit(type) || this.permissions?.project?.[type]=== UserPermissionsLevel.readOnly ) - } getSettingsPermissionLevel(): UserPermissionsLevel { @@ -88,12 +95,12 @@ class UserPermissions { } } -const useUserProjectPermissions = (projectName?: string): UserPermissions | undefined => { +const useUserProjectPermissions = (projectName: string, isUser?: boolean): UserPermissions | undefined => { const { data: permissions } = projectName ? useGetCurrentUserProjectPermissionsQuery({ projectName }) : useGetCurrentUserPermissionsQuery() - return new UserPermissions(permissions) + return new UserPermissions(permissions, isUser) } export { UserPermissionsLevel } diff --git a/src/pages/ProjectManagerPage/ProjectManagerPage.jsx b/src/pages/ProjectManagerPage/ProjectManagerPage.jsx index b06c5f45a..0221ede7b 100644 --- a/src/pages/ProjectManagerPage/ProjectManagerPage.jsx +++ b/src/pages/ProjectManagerPage/ProjectManagerPage.jsx @@ -17,7 +17,7 @@ import ProjectManagerPageLayout from './ProjectManagerPageLayout' import AppNavLinks from '@containers/header/AppNavLinks' import confirmDelete from '@helpers/confirmDelete' import useUserProjectPermissions from '@hooks/useUserProjectPermissions' -import ProjectUsers from './Users/ProjectUsers' +import ProjectUserAccess from './Users/ProjectUserAccess' const ProjectSettings = ({ projectList, projectManager, projectName }) => { return ( @@ -50,7 +50,8 @@ const ProjectManagerPage = () => { withDefault(StringParam, projectName), ) - const userPermissions = useUserProjectPermissions(selectedProject) + const userPermissions = useUserProjectPermissions(selectedProject, isUser) + console.log('up: ', userPermissions) // UPDATE DATA const [updateProject] = useUpdateProjectMutation() @@ -86,7 +87,7 @@ const ProjectManagerPage = () => { } const links = [] - if (userPermissions.projectSettingsAreEnabled()) { + if (!isUser || userPermissions.projectSettingsAreEnabled()) { if (userPermissions.canViewAnatomy() || module === 'anatomy') { links.push({ name: 'Anatomy', @@ -163,7 +164,7 @@ const ProjectManagerPage = () => { {module === 'anatomy' && } {module === 'projectSettings' && } {module === 'siteSettings' && } - {module === 'userSettings' && } + {module === 'userSettings' && } {module === 'roots' && } {module === 'teams' && } diff --git a/src/pages/ProjectManagerPage/Users/ProjectUsers.tsx b/src/pages/ProjectManagerPage/Users/ProjectUserAccess.tsx similarity index 57% rename from src/pages/ProjectManagerPage/Users/ProjectUsers.tsx rename to src/pages/ProjectManagerPage/Users/ProjectUserAccess.tsx index 4cb0814fa..66ad2ed46 100644 --- a/src/pages/ProjectManagerPage/Users/ProjectUsers.tsx +++ b/src/pages/ProjectManagerPage/Users/ProjectUserAccess.tsx @@ -3,20 +3,29 @@ import { $Any } from '@types' import { Button, Toolbar } from '@ynput/ayon-react-components' import { Splitter, SplitterPanel } from 'primereact/splitter' import { useState } from 'react' -import ProjectUserList from './ProjectUserList' +import ProjectUserAccessUserList from './ProjectUserAccessUserList' import { useGetAccessGroupsQuery } from '@queries/accessGroups/getAccessGroups' -import AssignAccessGroupsDialog from './AssignAccessGroupsDialog' +import ProjectUserAccessAssignDialog from './ProjectUserAccessAssignDialog' import { SelectedAccessGroupUsers } from './types' import { useProjectAccessGroupData } from './hooks' import { toast } from 'react-toastify' import { useGetUsersQuery } from '@queries/user/getUsers' -import { getAllProjectUsers, mapUsersByAccessGroups } from './mappers' +import { + getAccessGroupUsers, + getAllProjectUsers, + getFilteredAccessGroups, + getFilteredProjects, + getFilteredUsers, + getSelectedUsers, + mapUsersByAccessGroups, +} from './mappers' import { useSelector } from 'react-redux' import EmptyPlaceholder from '@components/EmptyPlaceholder/EmptyPlaceholder' import styled from 'styled-components' -import ProjectAccessSearchFilterWrapper from './ProjectAccessSearchFilterWrapper' -import ProjectList from './ProjectList' +import ProjectUserAccessSearchFilterWrapper from './ProjectUserAccessSearchFilterWrapper' +import ProjectUserAccessProjectList from './ProjectUserAccessProjectList' import { Filter } from '@components/SearchFilter/types' +import { AccessGroupObject } from '@api/rest/accessGroups' const StyledButton = styled(Button)` .shortcut { @@ -25,7 +34,7 @@ const StyledButton = styled(Button)` border-radius: var(--border-radius-m); } ` -const ProjectUsers = () => { +const ProjectUserAccess = () => { const { data: accessGroupList = [] } = useGetAccessGroupsQuery({ projectName: '_', }) @@ -57,26 +66,25 @@ const ProjectUsers = () => { SelectedAccessGroupUsers | undefined >() - const getSelectedUsers = (): string[] => { - if (!selectedAccessGroupUsers) { - return [] - } + const filteredSelectedProjects = getFilteredProjects( + selectedProjects, + filters.find((filter: Filter) => filter.label === 'Project'), + ) - return selectedAccessGroupUsers!.users - } + const filteredUnassignedUsers = getFilteredUsers( + unassignedUsers, + filters?.find((el: Filter) => el.label === 'User'), + ) + const selectedUsers = getSelectedUsers(selectedAccessGroupUsers, filteredUnassignedUsers) - const actionEnabled = selectedProjects.length > 0 && getSelectedUsers().length > 0 + const addActionEnabled = filteredSelectedProjects.length > 0 && selectedUsers.length > 0 + const removeActionEnabled = + filteredSelectedProjects.length > 0 && + getSelectedUsers(selectedAccessGroupUsers, [], true).length > 0 && + selectedAccessGroupUsers?.accessGroup != undefined - const getAccessGroupUsers = (accessGroup?: string): string[] => { - if (!selectedAccessGroupUsers || !accessGroup) { - return [] - } - return selectedAccessGroupUsers.accessGroup === accessGroup - ? selectedAccessGroupUsers.users - : [] - } const handleAdd = (user?: string) => { - setActionedUsers(user ? [user] : getSelectedUsers()) + setActionedUsers(user ? [user] : selectedUsers) setShowDialog(true) } @@ -112,18 +120,18 @@ const ProjectUsers = () => {
{/* @ts-ignore */} - setFilters(results)} /> { e.stopPropagation() - setActionedUsers(getSelectedUsers()) + setActionedUsers(selectedUsers) setShowDialog(true) }} > @@ -133,10 +141,10 @@ const ProjectUsers = () => { { e.stopPropagation() - setActionedUsers(getSelectedUsers()) + setActionedUsers(selectedUsers) onRemove(selectedAccessGroupUsers!.accessGroup!)() }} > @@ -151,22 +159,20 @@ const ProjectUsers = () => { minSize={10} > {/* @ts-ignore */} - el.label === 'Project')} - selection={selectedProjects} - onSelectionChange={(selection: $Any) => setSelectedProjects(selection)} + - {selectedProjects.length > 0 ? ( - 0 ? ( + el.label === 'User')} + selectedProjects={filteredSelectedProjects} + selectedUsers={selectedUsers} + tableList={filteredUnassignedUsers} isLoading={isLoading} onAdd={handleAdd} onSelectUsers={(selection) => setSelectedAccessGroupUsers({ users: selection })} @@ -181,47 +187,50 @@ const ProjectUsers = () => { )} - - - {accessGroupList - .map((item) => item.name) - .map((accessGroup) => { - return ( - - - mappedUsers[accessGroup] && mappedUsers[accessGroup].includes(user.name), - )} - onSelectUsers={(selection: string[]) => - updateSelectedAccessGroupUsers(accessGroup, selection) - } - onAdd={() => { - setActionedUsers(getAccessGroupUsers(accessGroup)) - setShowDialog(true) - }} - onRemove={onRemove(accessGroup)} - isLoading={isLoading} - /> - - ) - })} - + {filteredSelectedProjects.length > 0 && ( + + {getFilteredAccessGroups(accessGroupList, filters) + .map((item: AccessGroupObject) => item.name) + .map((accessGroup) => { + const selectedUsers = getAccessGroupUsers(selectedAccessGroupUsers!, accessGroup) + return ( + + + mappedUsers[accessGroup] && + mappedUsers[accessGroup].includes(user.name), + )} + onSelectUsers={(selection: string[]) => + updateSelectedAccessGroupUsers(accessGroup, selection) + } + onAdd={() => { + setActionedUsers(selectedUsers) + setShowDialog(true) + }} + onRemove={onRemove(accessGroup)} + isLoading={isLoading} + /> + + ) + })} + + )} {showDialog && ( - ({ ...item, selected: false }))} @@ -234,4 +243,4 @@ const ProjectUsers = () => {
) } -export default ProjectUsers +export default ProjectUserAccess diff --git a/src/pages/ProjectManagerPage/Users/AssignAccessGroupsDialog.styled.js b/src/pages/ProjectManagerPage/Users/ProjectUserAccessAssignDialog.styled.js similarity index 100% rename from src/pages/ProjectManagerPage/Users/AssignAccessGroupsDialog.styled.js rename to src/pages/ProjectManagerPage/Users/ProjectUserAccessAssignDialog.styled.js diff --git a/src/pages/ProjectManagerPage/Users/AssignAccessGroupsDialog.tsx b/src/pages/ProjectManagerPage/Users/ProjectUserAccessAssignDialog.tsx similarity index 95% rename from src/pages/ProjectManagerPage/Users/AssignAccessGroupsDialog.tsx rename to src/pages/ProjectManagerPage/Users/ProjectUserAccessAssignDialog.tsx index 5b4132e7c..7ae19280d 100644 --- a/src/pages/ProjectManagerPage/Users/AssignAccessGroupsDialog.tsx +++ b/src/pages/ProjectManagerPage/Users/ProjectUserAccessAssignDialog.tsx @@ -2,7 +2,7 @@ import { useState } from 'react' import { FormLayout, Dialog, Button, Icon } from '@ynput/ayon-react-components' import { $Any } from '@types' import clsx from 'clsx' -import * as Styled from './AssignAccessGroupsDialog.styled' +import * as Styled from './ProjectUserAccessAssignDialog.styled' import { AccessGroupUsers, SelectionStatus } from './types' const icons: {[key in SelectionStatus] : string | undefined} = { @@ -24,7 +24,7 @@ type Props = { onClose: () => void } -const AssignAccessGroupsDialog = ({ +const ProjectUserAccessAssignDialog = ({ accessGroups, users, userAccessGroups, @@ -113,4 +113,4 @@ const AssignAccessGroupsDialog = ({ ) } -export default AssignAccessGroupsDialog +export default ProjectUserAccessAssignDialog diff --git a/src/pages/ProjectManagerPage/Users/ProjectList.tsx b/src/pages/ProjectManagerPage/Users/ProjectUserAccessProjectList.tsx similarity index 69% rename from src/pages/ProjectManagerPage/Users/ProjectList.tsx rename to src/pages/ProjectManagerPage/Users/ProjectUserAccessProjectList.tsx index 9246a3cbd..35b35635e 100644 --- a/src/pages/ProjectManagerPage/Users/ProjectList.tsx +++ b/src/pages/ProjectManagerPage/Users/ProjectUserAccessProjectList.tsx @@ -5,10 +5,8 @@ import styled from 'styled-components' import clsx from 'clsx' import useTableLoadingData from '@hooks/useTableLoadingData' import { $Any } from '@types' -import { useRef } from 'react' import { TablePanel } from '@ynput/ayon-react-components' import { ProjectNode } from '@api/graphql' -import { Filter } from '@components/SearchFilter/types' const formatName = (rowData: $Any, field: string) => { return rowData[field] @@ -42,37 +40,21 @@ const StyledProjectName = styled.div` type Props = { className: string selection: string[] - filters: Filter[] - setSelection: $Any onSelectionChange: (selection: $Any) => void } -const ProjectList = ({ selection, onSelectionChange, filters }: Props) => { - console.log('filters', filters) +const ProjectUserAccessProjectList = ({ selection, onSelectionChange }: Props) => { const { data: projects = [], isLoading, isError, error } = useListProjectsQuery({}) if (isError) { console.error(error) } - const getFilteredProjects = (projects: ProjectNode[], filters: Filter) => { - if (!filters) { - return projects - } - - const filterProjects = filters && filters.values!.map((match: Filter) => match.id) - if (filters!.inverted) { - return projects.filter((project: ProjectNode) => !filterProjects.includes(project.name)) - } - return projects.filter((project: ProjectNode) => filterProjects.includes(project.name)) - } - // @ts-ignore - const projectList = getFilteredProjects(projects, filters) - const tableData = useTableLoadingData(projectList, isLoading, 10, 'name') + const tableData = useTableLoadingData(projects, isLoading, 10, 'name') const selected = tableData.filter((project: ProjectNode) => selection.includes(project.name)) return ( - + { className={clsx({ loading: isLoading })} rowClassName={() => ({ loading: isLoading })} onSelectionChange={(selection) => { - console.log(selection) onSelectionChange(selection.value.map((project: ProjectNode) => project.name)) }} > @@ -102,4 +83,4 @@ const ProjectList = ({ selection, onSelectionChange, filters }: Props) => { ) } -export default ProjectList +export default ProjectUserAccessProjectList diff --git a/src/pages/ProjectManagerPage/Users/ProjectAccessSearchFilterWrapper.tsx b/src/pages/ProjectManagerPage/Users/ProjectUserAccessSearchFilterWrapper.tsx similarity index 92% rename from src/pages/ProjectManagerPage/Users/ProjectAccessSearchFilterWrapper.tsx rename to src/pages/ProjectManagerPage/Users/ProjectUserAccessSearchFilterWrapper.tsx index d39c8a750..48ad35f3f 100644 --- a/src/pages/ProjectManagerPage/Users/ProjectAccessSearchFilterWrapper.tsx +++ b/src/pages/ProjectManagerPage/Users/ProjectUserAccessSearchFilterWrapper.tsx @@ -13,7 +13,7 @@ type Props = { onChange: $Any } -const ProjectAccessSearchFilterWrapper = ({ filters: _filters, onChange }: Props) => { +const ProjectUserAccessSearchFilterWrapper = ({ filters: _filters, onChange }: Props) => { const selfName = useSelector((state: $Any) => state.user.name) const { isLoading: isProjectsLoading, data: projects = [] } = useListProjectsQuery({}) const { isLoading: isUsersLoading, data: users = [] } = useGetUsersQuery({ selfName }) @@ -54,4 +54,4 @@ const ProjectAccessSearchFilterWrapper = ({ filters: _filters, onChange }: Props ) } -export default ProjectAccessSearchFilterWrapper +export default ProjectUserAccessSearchFilterWrapper diff --git a/src/pages/ProjectManagerPage/Users/ProjectUserList.tsx b/src/pages/ProjectManagerPage/Users/ProjectUserAccessUserList.tsx similarity index 69% rename from src/pages/ProjectManagerPage/Users/ProjectUserList.tsx rename to src/pages/ProjectManagerPage/Users/ProjectUserAccessUserList.tsx index 3d11b665f..d2adf9f10 100644 --- a/src/pages/ProjectManagerPage/Users/ProjectUserList.tsx +++ b/src/pages/ProjectManagerPage/Users/ProjectUserAccessUserList.tsx @@ -25,7 +25,7 @@ type Props = { onRemove?: (user?: string) => void } -const ProjectUserList = ({ +const ProjectUserAccessUserList = ({ selectedProjects, selectedUsers, tableList, @@ -58,36 +58,7 @@ const ProjectUserList = ({ } } - const getFilteredUsers = (users: UserNode[], filters?: Filter) => { - const exactFilter = (users: UserNode[], filters: Filter) => { - const filterUsers = filters && filters.values!.map((match: Filter) => match.id) - if (filters!.inverted) { - return users.filter((user: UserNode) => !filterUsers.includes(user.name)) - } - return users.filter((user: UserNode) => filterUsers.includes(user.name)) - } - - const fuzzyFilter = (users: UserNode[], filters: Filter) => { - const filterString = filters.values![0].id - if (filters!.inverted) { - return users.filter((user: UserNode) => user.name.indexOf(filterString) == -1) - } - return users.filter((user: UserNode) => user.name.indexOf(filterString) != -1) - } - - if (!filters || !filters.values || filters.values.length == 0) { - return users - } - - if (filters.values!.length == 1 && filters.values[0]!.isCustom) { - return fuzzyFilter(users, filters) - } - - return exactFilter(users, filters) - } - - const filteredUsers = getFilteredUsers(tableList, filters) - const selectedUnassignedUsers = filteredUsers.filter((user: $Any) => selectedUsers.includes(user.name)) + const selectedUnassignedUsers = tableList.filter((user: $Any) => selectedUsers.includes(user.name)) const selectedUnassignedUserNames = selectedUnassignedUsers.map((user: $Any) => user.name) // Render return ( @@ -95,7 +66,7 @@ const ProjectUserList = ({ { const result = api.useGetProjectUsersQuery({ projectName: selectedProjects[0] || '_' }) const users = result.data - const accessGroupUsers: $Any = {} const removeUserAccessGroup = (user: string, accessGroup: string) => { const updatedAccessGroups = users![user].filter((item: string) => item !== accessGroup) @@ -89,7 +88,14 @@ const useProjectAccessGroupData = () => { } } - return { users, accessGroupUsers, selectedProjects, setSelectedProjects, removeUserAccessGroup, updateUserAccessGroups } + return { + users, + accessGroupUsers, + selectedProjects, + setSelectedProjects, + removeUserAccessGroup, + updateUserAccessGroups, + } } diff --git a/src/pages/ProjectManagerPage/Users/mappers.ts b/src/pages/ProjectManagerPage/Users/mappers.ts index 289d68607..745f8212f 100644 --- a/src/pages/ProjectManagerPage/Users/mappers.ts +++ b/src/pages/ProjectManagerPage/Users/mappers.ts @@ -1,4 +1,7 @@ -import { AccessGroupUsers, ProjectUsersResponse } from './types' +import { AccessGroupObject } from '@api/rest/accessGroups' +import { AccessGroupUsers, ProjectUsersResponse, SelectedAccessGroupUsers } from './types' +import { Filter } from '@components/SearchFilter/types' +import { ProjectNode, UserNode } from '@api/graphql' const getAllProjectUsers = (groupedUsers: AccessGroupUsers): string[] => { let allUsers: string[] = [] @@ -30,4 +33,103 @@ const mapUsersByAccessGroups = (response: ProjectUsersResponse | undefined): Acc return groupedUsers } -export { mapUsersByAccessGroups, getAllProjectUsers } +const getFilteredAccessGroups = (accessGroupList: AccessGroupObject[], filters: Filter[]) => { + if (!filters) { + return accessGroupList + } + const accessGroupFilters = filters?.find((el: Filter) => el.label === 'Access Group') + if (!accessGroupFilters) { + return accessGroupList + } + + const filterProjects = filters && accessGroupFilters.values!.map((match: Filter) => match.id) + if (accessGroupFilters!.inverted) { + return accessGroupList.filter( + (accessGroup: AccessGroupObject) => !filterProjects.includes(accessGroup.name), + ) + } + return accessGroupList.filter((accessGroup: AccessGroupObject) => + filterProjects.includes(accessGroup.name), + ) +} + + const getSelectedUsers = ( + selectedAccessGroupUsers: SelectedAccessGroupUsers | undefined, + filteredUsers: UserNode[], + skipFiltering = false, + ): string[] => { + if (!selectedAccessGroupUsers) { + return [] + } + + if (skipFiltering) { + return selectedAccessGroupUsers.users + } + + const filteredUserNames = filteredUsers.map((user: UserNode) => user.name) + return selectedAccessGroupUsers!.users.filter((user: string) => + filteredUserNames.includes(user), + ) + } + + const getAccessGroupUsers = ( + selectedAccessGroupUsers: SelectedAccessGroupUsers, + accessGroup?: string, + ): string[] => { + if (!selectedAccessGroupUsers || !accessGroup) { + return [] + } + return selectedAccessGroupUsers.accessGroup === accessGroup + ? selectedAccessGroupUsers.users + : [] + } + + const getFilteredProjects = (projects: string[], filters: Filter) => { + if (!filters) { + return projects + } + + const filterProjects = filters && filters.values!.map((match: Filter) => match.id) + if (filters!.inverted) { + return projects.filter((project) => !filterProjects.includes(project)) + } + return projects.filter((project) => filterProjects.includes(project)) + } + + const getFilteredUsers = (users: UserNode[], filters?: Filter) => { + const exactFilter = (users: UserNode[], filters: Filter) => { + const filterUsers = filters && filters.values!.map((match: Filter) => match.id) + if (filters!.inverted) { + return users.filter((user: UserNode) => !filterUsers.includes(user.name)) + } + return users.filter((user: UserNode) => filterUsers.includes(user.name)) + } + + const fuzzyFilter = (users: UserNode[], filters: Filter) => { + const filterString = filters.values![0].id + if (filters!.inverted) { + return users.filter((user: UserNode) => user.name.indexOf(filterString) == -1) + } + return users.filter((user: UserNode) => user.name.indexOf(filterString) != -1) + } + + if (!filters || !filters.values || filters.values.length == 0) { + return users + } + + if (filters.values!.length == 1 && filters.values[0]!.isCustom) { + return fuzzyFilter(users, filters) + } + + return exactFilter(users, filters) + } + + export { + mapUsersByAccessGroups, + getAllProjectUsers, + getFilteredAccessGroups, + getSelectedUsers, + getAccessGroupUsers, + getFilteredProjects, + getFilteredUsers, + } From cface9e94915602fb0c461a16a3383e7d4a2e5c5 Mon Sep 17 00:00:00 2001 From: Florin Tudor Date: Mon, 11 Nov 2024 09:02:25 +0100 Subject: [PATCH 22/35] feature(Users): User Permissions class refactoring, removed gap in projects list component when user lacks create project permissions --- src/containers/AddonSettings/AddonSettings.jsx | 3 ++- src/containers/projectList.jsx | 9 ++------- src/hooks/useUserProjectPermissions.ts | 18 +++++++++--------- .../ProjectManagerPage/ProjectAnatomy.jsx | 11 +++++++---- 4 files changed, 20 insertions(+), 21 deletions(-) diff --git a/src/containers/AddonSettings/AddonSettings.jsx b/src/containers/AddonSettings/AddonSettings.jsx index 9547558ce..0f074f705 100644 --- a/src/containers/AddonSettings/AddonSettings.jsx +++ b/src/containers/AddonSettings/AddonSettings.jsx @@ -54,6 +54,7 @@ const isChildPath = (childPath, parentPath) => { } const AddonSettings = ({ projectName, showSites = false }) => { + const isUser = useSelector((state) => state.user.data.isUser) //const navigate = useNavigate() const [showHelp, setShowHelp] = useState(false) const [selectedAddons, setSelectedAddons] = useState([]) @@ -75,7 +76,7 @@ const AddonSettings = ({ projectName, showSites = false }) => { const [promoteBundle] = usePromoteBundleMutation() const { requestPaste } = usePaste() - const userPermissions = useUserProjectPermissions(projectName) + const userPermissions = useUserProjectPermissions(null, isUser) const projectKey = projectName || '_' diff --git a/src/containers/projectList.jsx b/src/containers/projectList.jsx index a6aae0608..16270a59a 100644 --- a/src/containers/projectList.jsx +++ b/src/containers/projectList.jsx @@ -55,11 +55,6 @@ const StyledProjectName = styled.div` } ` -const ButtonPlaceholder = styled.div` - height: 26px; - width: 100%; -` - const StyledAddButton = styled(Button)` overflow: hidden; position: relative; @@ -227,7 +222,7 @@ const ProjectList = ({ const [updateUserPreferences] = useSetFrontendPreferencesMutation() - const userPermissions = useUserProjectPermissions() + const userPermissions = useUserProjectPermissions(null, user?.data?.isUser || true) const handlePinProjects = async (sel, isPinning) => { try { @@ -417,7 +412,7 @@ const ProjectList = ({
{/*
*/} - ): } + ): null} {isCollapsible && ( diff --git a/src/hooks/useUserProjectPermissions.ts b/src/hooks/useUserProjectPermissions.ts index 7679366ee..87f15accb 100644 --- a/src/hooks/useUserProjectPermissions.ts +++ b/src/hooks/useUserProjectPermissions.ts @@ -15,29 +15,29 @@ enum UserPermissionsEntity { class UserPermissions { permissions: GetCurrentUserPermissionsApiResponse - isUser: boolean + hasLimitedPermissions: boolean - constructor(permissions: GetCurrentUserPermissionsApiResponse, isUser: boolean = false) { + constructor(permissions: GetCurrentUserPermissionsApiResponse, hasLimitedPermissions: boolean = false) { this.permissions = permissions - this.isUser = isUser + this.hasLimitedPermissions = hasLimitedPermissions } canCreateProject(): boolean { - if (!this.isUser) { + if (!this.hasLimitedPermissions) { return true } return this.projectSettingsAreEnabled() && this.permissions?.project?.create || false } getPermissionLevel(type: UserPermissionsEntity): UserPermissionsLevel { - if (!this.isUser) { + if (!this.hasLimitedPermissions) { return UserPermissionsLevel.readWrite } return this.permissions?.project?.[type]|| UserPermissionsLevel.readWrite } canEdit(type: UserPermissionsEntity): boolean { - if (!this.isUser || !this.projectSettingsAreEnabled()) { + if (!this.hasLimitedPermissions || !this.projectSettingsAreEnabled()) { return true } @@ -45,7 +45,7 @@ class UserPermissions { } canView(type: UserPermissionsEntity): boolean { - if (!this.isUser || !this.projectSettingsAreEnabled()) { + if (!this.hasLimitedPermissions || !this.projectSettingsAreEnabled()) { return true } @@ -95,12 +95,12 @@ class UserPermissions { } } -const useUserProjectPermissions = (projectName: string, isUser?: boolean): UserPermissions | undefined => { +const useUserProjectPermissions = (projectName: string, hasLimitedPermissions?: boolean): UserPermissions | undefined => { const { data: permissions } = projectName ? useGetCurrentUserProjectPermissionsQuery({ projectName }) : useGetCurrentUserPermissionsQuery() - return new UserPermissions(permissions, isUser) + return new UserPermissions(permissions, hasLimitedPermissions) } export { UserPermissionsLevel } diff --git a/src/pages/ProjectManagerPage/ProjectAnatomy.jsx b/src/pages/ProjectManagerPage/ProjectAnatomy.jsx index dece8f444..f2b57ad6d 100644 --- a/src/pages/ProjectManagerPage/ProjectAnatomy.jsx +++ b/src/pages/ProjectManagerPage/ProjectAnatomy.jsx @@ -9,16 +9,19 @@ import copyToClipboard from '@helpers/copyToClipboard' import { usePaste } from '@context/pasteContext' import useUserProjectPermissions, { UserPermissionsLevel } from '@hooks/useUserProjectPermissions' import EmptyPlaceholder from '@components/EmptyPlaceholder/EmptyPlaceholder' +import { useSelector } from 'react-redux' const ProjectAnatomy = ({ projectName, projectList }) => { - const [formData, setFormData] = useState(null) - const [isChanged, setIsChanged] = useState(false) - + const isUser = useSelector((state) => state.user.data.isUser) const [updateProjectAnatomy, { isLoading: isUpdating }] = useUpdateProjectAnatomyMutation() const { requestPaste } = usePaste() - const userPermissions = useUserProjectPermissions(projectName) + const userPermissions = useUserProjectPermissions(projectName, isUser) const accessLevel = userPermissions.getAnatomyPermissionLevel() + const [formData, setFormData] = useState(null) + const [isChanged, setIsChanged] = useState(false) + + const saveAnatomy = () => { updateProjectAnatomy({ projectName, anatomy: formData }) From bfef11302d065f1dab4a355df45ca021a056e5bc Mon Sep 17 00:00:00 2001 From: Florin Tudor Date: Mon, 11 Nov 2024 10:54:41 +0100 Subject: [PATCH 23/35] feature(Users): Fetching user access data for multiple projects in custom hook --- src/pages/ProjectManagerPage/Users/hooks.ts | 4 +-- src/pages/ProjectManagerPage/Users/mappers.ts | 25 ++++++++------ src/pages/ProjectManagerPage/Users/types.ts | 4 --- src/services/project/getProject.ts | 34 ++++++++++++++++++- 4 files changed, 49 insertions(+), 18 deletions(-) diff --git a/src/pages/ProjectManagerPage/Users/hooks.ts b/src/pages/ProjectManagerPage/Users/hooks.ts index 193cb35dd..cc0db919c 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 } from '@api/rest/project' +import { useGetProjectsUsersQuery } from '@queries/project/getProject' import { useState } from "react" import { useUpdateProjectUsersMutation } from "@queries/project/updateProject" import { useDispatch } from "react-redux" @@ -31,7 +31,7 @@ const useProjectAccessGroupData = () => { const [selectedProjects, setSelectedProjects] = useState([]) - const result = api.useGetProjectUsersQuery({ projectName: selectedProjects[0] || '_' }) + const result = useGetProjectsUsersQuery({ projects: selectedProjects }) const users = result.data const accessGroupUsers: $Any = {} diff --git a/src/pages/ProjectManagerPage/Users/mappers.ts b/src/pages/ProjectManagerPage/Users/mappers.ts index 745f8212f..ed511403f 100644 --- a/src/pages/ProjectManagerPage/Users/mappers.ts +++ b/src/pages/ProjectManagerPage/Users/mappers.ts @@ -1,7 +1,8 @@ import { AccessGroupObject } from '@api/rest/accessGroups' -import { AccessGroupUsers, ProjectUsersResponse, SelectedAccessGroupUsers } from './types' +import { AccessGroupUsers, SelectedAccessGroupUsers } from './types' import { Filter } from '@components/SearchFilter/types' -import { ProjectNode, UserNode } from '@api/graphql' +import { UserNode } from '@api/graphql' +import { GetProjectUsersApiResponse } from '@api/rest/project' const getAllProjectUsers = (groupedUsers: AccessGroupUsers): string[] => { let allUsers: string[] = [] @@ -12,21 +13,23 @@ const getAllProjectUsers = (groupedUsers: AccessGroupUsers): string[] => { return [...new Set(allUsers)] } -const mapUsersByAccessGroups = (response: ProjectUsersResponse | undefined): AccessGroupUsers => { +const mapUsersByAccessGroups = (response: GetProjectUsersApiResponse | undefined): AccessGroupUsers => { if (!response) { return {} } const groupedUsers: { [key: string]: string[] } = {} - for (const [user, acessGroupsList] of Object.entries(response)) { - for (const accessGroup of acessGroupsList) { - if (groupedUsers[accessGroup] === undefined) { - groupedUsers[accessGroup] = [] + for (const [_, projectData] of Object.entries(response)) { + for (const [user, acessGroupsList] of Object.entries(projectData)) { + for (const accessGroup of acessGroupsList) { + if (groupedUsers[accessGroup] === undefined) { + groupedUsers[accessGroup] = [] + } + if (groupedUsers[accessGroup].includes(user)) { + continue + } + groupedUsers[accessGroup].push(user) } - if (groupedUsers[accessGroup].includes(user)) { - continue - } - groupedUsers[accessGroup].push(user) } } diff --git a/src/pages/ProjectManagerPage/Users/types.ts b/src/pages/ProjectManagerPage/Users/types.ts index 7539eb02e..e6fb52806 100644 --- a/src/pages/ProjectManagerPage/Users/types.ts +++ b/src/pages/ProjectManagerPage/Users/types.ts @@ -1,7 +1,3 @@ -export type ProjectUsersResponse = { - [key: string]: string[] -} - export type AccessGroupUsers = { [key: string]: string[] } diff --git a/src/services/project/getProject.ts b/src/services/project/getProject.ts index 11e964d7b..95c22021f 100644 --- a/src/services/project/getProject.ts +++ b/src/services/project/getProject.ts @@ -1,4 +1,4 @@ -import { api } from '@api/rest/project' +import { api, GetProjectUsersApiResponse } from '@api/rest/project' // @ts-ignore import { selectProject, setProjectData } from '@state/project' @@ -34,6 +34,10 @@ const createProjectQuery = (attribs: $Any, fields: $Any) => { ` } +type GetProjectsUsersParams = { + projects: string[] +} + const getProjectInjected = api.injectEndpoints({ endpoints: (build) => ({ getProjectAttribs: build.query({ @@ -48,6 +52,33 @@ const getProjectInjected = api.injectEndpoints({ transformResponse: (res: any) => res.data?.project, providesTags: (_res, _error, { projectName }) => [{ type: 'project', id: projectName }], }), + getProjectsUsers: build.query({ + async queryFn({ projects = [] }, { dispatch, forced }) { + try { + const projectUsersData: $Any = {} + for (const project of projects) { + const response = await dispatch( + api.endpoints.getProjectUsers.initiate( + { projectName: project }, + { forceRefetch: forced }, + ), + ) + + if (response.status === 'rejected') { + throw 'No projects found' + } + projectUsersData[project] = response.data + } + + 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: 'project', id: projectName })), + }), }), overrideExisting: true, }) @@ -127,6 +158,7 @@ const getProjectApi = getProjectInjected.enhanceEndpoints({ export const { useGetProjectQuery, + useGetProjectsUsersQuery, useListProjectsQuery, useGetProjectAnatomyQuery, useGetProjectAttribsQuery, From 28b545d370ac621b592196097caa794c48d2642a Mon Sep 17 00:00:00 2001 From: Florin Tudor Date: Mon, 11 Nov 2024 13:00:23 +0100 Subject: [PATCH 24/35] feature(Users): Fixing invalidation/cache update issues --- .../Users/ProjectUserAccess.tsx | 22 ++++-- .../Users/ProjectUserAccessProjectList.tsx | 12 +-- src/pages/ProjectManagerPage/Users/hooks.ts | 13 ++-- src/pages/ProjectManagerPage/Users/mappers.ts | 74 +++++++++++-------- src/services/project/getProject.ts | 10 ++- 5 files changed, 80 insertions(+), 51 deletions(-) diff --git a/src/pages/ProjectManagerPage/Users/ProjectUserAccess.tsx b/src/pages/ProjectManagerPage/Users/ProjectUserAccess.tsx index 66ad2ed46..0fa41585f 100644 --- a/src/pages/ProjectManagerPage/Users/ProjectUserAccess.tsx +++ b/src/pages/ProjectManagerPage/Users/ProjectUserAccess.tsx @@ -15,6 +15,7 @@ import { getAllProjectUsers, getFilteredAccessGroups, getFilteredProjects, + getFilteredSelectedProjects, getFilteredUsers, getSelectedUsers, mapUsersByAccessGroups, @@ -26,6 +27,7 @@ import ProjectUserAccessSearchFilterWrapper from './ProjectUserAccessSearchFilte import ProjectUserAccessProjectList from './ProjectUserAccessProjectList' import { Filter } from '@components/SearchFilter/types' import { AccessGroupObject } from '@api/rest/accessGroups' +import { useListProjectsQuery } from '@queries/project/getProject' const StyledButton = styled(Button)` .shortcut { @@ -66,10 +68,15 @@ const ProjectUserAccess = () => { SelectedAccessGroupUsers | undefined >() - const filteredSelectedProjects = getFilteredProjects( - selectedProjects, - filters.find((filter: Filter) => filter.label === 'Project'), - ) + const { data: projects, isLoading: projectsIsLoading, isError, error } = useListProjectsQuery({}) + if (isError) { + console.error(error) + } + + const projectFilters = filters.find((filter: Filter) => filter.label === 'Project') + // @ts-ignore Weird one, the response type seems to be mismatched? + const filteredProjects = getFilteredProjects(projects, projectFilters) + const filteredSelectedProjects = getFilteredSelectedProjects(selectedProjects, projectFilters) const filteredUnassignedUsers = getFilteredUsers( unassignedUsers, @@ -158,11 +165,12 @@ const ProjectUserAccess = () => { size={25} minSize={10} > - {/* @ts-ignore */} + // @ts-ignore + projects={filteredProjects} + isLoading={projectsIsLoading} + onSelectionChange={setSelectedProjects} /> diff --git a/src/pages/ProjectManagerPage/Users/ProjectUserAccessProjectList.tsx b/src/pages/ProjectManagerPage/Users/ProjectUserAccessProjectList.tsx index 35b35635e..77bd089fa 100644 --- a/src/pages/ProjectManagerPage/Users/ProjectUserAccessProjectList.tsx +++ b/src/pages/ProjectManagerPage/Users/ProjectUserAccessProjectList.tsx @@ -1,6 +1,5 @@ import { DataTable } from 'primereact/datatable' import { Column } from 'primereact/column' -import { useListProjectsQuery } from '@queries/project/getProject' import styled from 'styled-components' import clsx from 'clsx' import useTableLoadingData from '@hooks/useTableLoadingData' @@ -38,18 +37,13 @@ const StyledProjectName = styled.div` ` type Props = { - className: string + projects: ProjectNode[] selection: string[] + isLoading: boolean onSelectionChange: (selection: $Any) => void } -const ProjectUserAccessProjectList = ({ selection, onSelectionChange }: Props) => { - const { data: projects = [], isLoading, isError, error } = useListProjectsQuery({}) - if (isError) { - console.error(error) - } - - // @ts-ignore +const ProjectUserAccessProjectList = ({ projects, isLoading, selection, onSelectionChange }: Props) => { const tableData = useTableLoadingData(projects, isLoading, 10, 'name') const selected = tableData.filter((project: ProjectNode) => selection.includes(project.name)) diff --git a/src/pages/ProjectManagerPage/Users/hooks.ts b/src/pages/ProjectManagerPage/Users/hooks.ts index cc0db919c..a2e697506 100644 --- a/src/pages/ProjectManagerPage/Users/hooks.ts +++ b/src/pages/ProjectManagerPage/Users/hooks.ts @@ -1,5 +1,5 @@ import { $Any } from "@types" -import { useGetProjectsUsersQuery } from '@queries/project/getProject' +import api, { useGetProjectsUsersQuery } from '@queries/project/getProject' import { useState } from "react" import { useUpdateProjectUsersMutation } from "@queries/project/updateProject" import { useDispatch } from "react-redux" @@ -14,13 +14,14 @@ type FilterValues = { const useProjectAccessGroupData = () => { const udpateApiCache = (project: string, user: string, accessGroups: string[]) => { + dispatch(api.util.invalidateTags([{ type: 'project', id: project }])) dispatch( // @ts-ignore api.util.updateQueryData( - 'getProjectUsers', - { projectName: project}, + 'getProjectsUsers', + { projects: [project]}, (draft: $Any) => { - draft[user] = accessGroups + draft[project][user] = accessGroups }, ), ) @@ -36,8 +37,9 @@ const useProjectAccessGroupData = () => { const accessGroupUsers: $Any = {} const removeUserAccessGroup = (user: string, accessGroup: string) => { - const updatedAccessGroups = users![user].filter((item: string) => item !== accessGroup) for (const project of selectedProjects) { + // @ts-ignore + const updatedAccessGroups = users![project][user].filter((item: string) => item !== accessGroup) try { updateUser({ projectName: project, @@ -70,6 +72,7 @@ const useProjectAccessGroupData = () => { } for (const user of selectedUsers) { + // @ts-ignore const accessGroups = updatedAccessGroups(users?.[user] || [], changes) for (const project of selectedProjects) { diff --git a/src/pages/ProjectManagerPage/Users/mappers.ts b/src/pages/ProjectManagerPage/Users/mappers.ts index ed511403f..158d0e2a3 100644 --- a/src/pages/ProjectManagerPage/Users/mappers.ts +++ b/src/pages/ProjectManagerPage/Users/mappers.ts @@ -2,7 +2,8 @@ import { AccessGroupObject } from '@api/rest/accessGroups' import { AccessGroupUsers, SelectedAccessGroupUsers } from './types' import { Filter } from '@components/SearchFilter/types' import { UserNode } from '@api/graphql' -import { GetProjectUsersApiResponse } from '@api/rest/project' +import { ListProjectsItemModel } from '@api/rest/project' +import { GetProjectsUsersApiResponse } from '@queries/project/getProject' const getAllProjectUsers = (groupedUsers: AccessGroupUsers): string[] => { let allUsers: string[] = [] @@ -13,7 +14,7 @@ const getAllProjectUsers = (groupedUsers: AccessGroupUsers): string[] => { return [...new Set(allUsers)] } -const mapUsersByAccessGroups = (response: GetProjectUsersApiResponse | undefined): AccessGroupUsers => { +const mapUsersByAccessGroups = (response: GetProjectsUsersApiResponse | undefined): AccessGroupUsers => { if (!response) { return {} } @@ -21,6 +22,7 @@ const mapUsersByAccessGroups = (response: GetProjectUsersApiResponse | undefined const groupedUsers: { [key: string]: string[] } = {} for (const [_, projectData] of Object.entries(response)) { for (const [user, acessGroupsList] of Object.entries(projectData)) { + // @ts-ignore for (const accessGroup of acessGroupsList) { if (groupedUsers[accessGroup] === undefined) { groupedUsers[accessGroup] = [] @@ -87,7 +89,7 @@ const getFilteredAccessGroups = (accessGroupList: AccessGroupObject[], filters: : [] } - const getFilteredProjects = (projects: string[], filters: Filter) => { + const getFilteredSelectedProjects = (projects: string[], filters: Filter) => { if (!filters) { return projects } @@ -99,40 +101,54 @@ const getFilteredAccessGroups = (accessGroupList: AccessGroupObject[], filters: return projects.filter((project) => filterProjects.includes(project)) } - const getFilteredUsers = (users: UserNode[], filters?: Filter) => { - const exactFilter = (users: UserNode[], filters: Filter) => { - const filterUsers = filters && filters.values!.map((match: Filter) => match.id) - if (filters!.inverted) { - return users.filter((user: UserNode) => !filterUsers.includes(user.name)) - } - return users.filter((user: UserNode) => filterUsers.includes(user.name)) + const getFilteredProjects = (projects: ListProjectsItemModel[], filter: Filter) => { + if (!filter) { + return projects } - const fuzzyFilter = (users: UserNode[], filters: Filter) => { - const filterString = filters.values![0].id - if (filters!.inverted) { - return users.filter((user: UserNode) => user.name.indexOf(filterString) == -1) - } - return users.filter((user: UserNode) => user.name.indexOf(filterString) != -1) + const filterProjects = filter.values!.map((match: Filter) => match.id) + if (filter!.inverted) { + return projects.filter((project: ListProjectsItemModel) => !filterProjects.includes(project.name)) } - if (!filters || !filters.values || filters.values.length == 0) { - return users + return projects.filter((project: ListProjectsItemModel) => filterProjects.includes(project.name)) + } + +const getFilteredUsers = (users: UserNode[], filters?: Filter) => { + const exactFilter = (users: UserNode[], filters: Filter) => { + const filterUsers = filters && filters.values!.map((match: Filter) => match.id) + if (filters!.inverted) { + return users.filter((user: UserNode) => !filterUsers.includes(user.name)) } + return users.filter((user: UserNode) => filterUsers.includes(user.name)) + } - if (filters.values!.length == 1 && filters.values[0]!.isCustom) { - return fuzzyFilter(users, filters) + const fuzzyFilter = (users: UserNode[], filters: Filter) => { + const filterString = filters.values![0].id + if (filters!.inverted) { + return users.filter((user: UserNode) => user.name.indexOf(filterString) == -1) } + return users.filter((user: UserNode) => user.name.indexOf(filterString) != -1) + } - return exactFilter(users, filters) + if (!filters || !filters.values || filters.values.length == 0) { + return users } - export { - mapUsersByAccessGroups, - getAllProjectUsers, - getFilteredAccessGroups, - getSelectedUsers, - getAccessGroupUsers, - getFilteredProjects, - getFilteredUsers, + if (filters.values!.length == 1 && filters.values[0]!.isCustom) { + return fuzzyFilter(users, filters) } + + return exactFilter(users, filters) +} + +export { + mapUsersByAccessGroups, + getAllProjectUsers, + getFilteredAccessGroups, + getSelectedUsers, + getAccessGroupUsers, + getFilteredSelectedProjects, + getFilteredProjects, + getFilteredUsers, +} diff --git a/src/services/project/getProject.ts b/src/services/project/getProject.ts index 95c22021f..b04bf2894 100644 --- a/src/services/project/getProject.ts +++ b/src/services/project/getProject.ts @@ -38,6 +38,14 @@ type GetProjectsUsersParams = { projects: string[] } +export type GetProjectsUsersApiResponse = { + data: { + [project: string]: { + [user: string]: string[] + } + } +} + const getProjectInjected = api.injectEndpoints({ endpoints: (build) => ({ getProjectAttribs: build.query({ @@ -52,7 +60,7 @@ const getProjectInjected = api.injectEndpoints({ transformResponse: (res: any) => res.data?.project, providesTags: (_res, _error, { projectName }) => [{ type: 'project', id: projectName }], }), - getProjectsUsers: build.query({ + getProjectsUsers: build.query({ async queryFn({ projects = [] }, { dispatch, forced }) { try { const projectUsersData: $Any = {} From 768c349ae662529776024a5b7403c084e96abb10 Mon Sep 17 00:00:00 2001 From: Florin Tudor Date: Tue, 12 Nov 2024 09:19:50 +0100 Subject: [PATCH 25/35] feature(Users): Refactoring permissions module, considering all project permissions on project tab menu display --- .../AddonSettings/AddonSettings.jsx | 8 +- src/containers/projectList.jsx | 2 +- src/hooks/useUserProjectPermissions.ts | 145 ++++++++++++------ .../ProjectManagerPage/ProjectAnatomy.jsx | 55 ++++--- .../ProjectManagerPage/ProjectManagerPage.jsx | 11 +- 5 files changed, 134 insertions(+), 87 deletions(-) diff --git a/src/containers/AddonSettings/AddonSettings.jsx b/src/containers/AddonSettings/AddonSettings.jsx index 0f074f705..01b5e79f3 100644 --- a/src/containers/AddonSettings/AddonSettings.jsx +++ b/src/containers/AddonSettings/AddonSettings.jsx @@ -76,7 +76,7 @@ const AddonSettings = ({ projectName, showSites = false }) => { const [promoteBundle] = usePromoteBundleMutation() const { requestPaste } = usePaste() - const userPermissions = useUserProjectPermissions(null, isUser) + const userPermissions = useUserProjectPermissions(!isUser) const projectKey = projectName || '_' @@ -631,9 +631,9 @@ const AddonSettings = ({ projectName, showSites = false }) => { /> { setCurrentSelection(null) } - if (!userPermissions.canViewSettings()) { + if (!userPermissions.canViewSettings(projectName)) { return { try { diff --git a/src/hooks/useUserProjectPermissions.ts b/src/hooks/useUserProjectPermissions.ts index 87f15accb..af4cd0016 100644 --- a/src/hooks/useUserProjectPermissions.ts +++ b/src/hooks/useUserProjectPermissions.ts @@ -1,107 +1,156 @@ -import { GetCurrentUserPermissionsApiResponse } from '@api/rest/permissions' -import { useGetCurrentUserPermissionsQuery, useGetCurrentUserProjectPermissionsQuery } from '@queries/permissions/getPermissions' +import { useGetCurrentUserPermissionsQuery } from '@queries/permissions/getPermissions' + +type AllProjectsPremissions = { + projects: { + [projectName: string]: { + project: { + anatomy: PermissionLevel + create: boolean + enabled: boolean + settings: PermissionLevel + users: PermissionLevel + } + } + } + studio: { + create_project: boolean + } + user_level: 'user' | 'admin' +} -enum UserPermissionsLevel { +enum PermissionLevel { none = 0, readOnly = 1, readWrite = 2, } -enum UserPermissionsEntity { +export enum UserPermissionsEntity { users = 'users', anatomy = 'anatomy', settings = 'settings', } class UserPermissions { - permissions: GetCurrentUserPermissionsApiResponse - hasLimitedPermissions: boolean + permissions: AllProjectsPremissions + hasElevatedPrivileges: boolean - constructor(permissions: GetCurrentUserPermissionsApiResponse, hasLimitedPermissions: boolean = false) { + constructor(permissions: AllProjectsPremissions, hasLimitedPermissions: boolean = false) { this.permissions = permissions - this.hasLimitedPermissions = hasLimitedPermissions + this.hasElevatedPrivileges = hasLimitedPermissions } canCreateProject(): boolean { - if (!this.hasLimitedPermissions) { + if (this.hasElevatedPrivileges) { return true } - return this.projectSettingsAreEnabled() && this.permissions?.project?.create || false + if (!this.permissions) { + return false + } + + return (this.projectSettingsAreEnabled() && this.permissions.studio.create_project) || false } - getPermissionLevel(type: UserPermissionsEntity): UserPermissionsLevel { - if (!this.hasLimitedPermissions) { - return UserPermissionsLevel.readWrite + getPermissionLevel(type: UserPermissionsEntity, projectName: string): PermissionLevel { + if (this.hasElevatedPrivileges) { + return PermissionLevel.readWrite + } + if (!this.permissions) { + return PermissionLevel.none } - return this.permissions?.project?.[type]|| UserPermissionsLevel.readWrite + + return this.permissions.projects[projectName]?.project[type] || PermissionLevel.none } - canEdit(type: UserPermissionsEntity): boolean { - if (!this.hasLimitedPermissions || !this.projectSettingsAreEnabled()) { + canEdit(type: UserPermissionsEntity, projectName: string): boolean { + if (this.hasElevatedPrivileges || !this.projectSettingsAreEnabled()) { return true } + if (!this.permissions || !projectName) { + return false + } - return this.permissions?.project?.[type] === UserPermissionsLevel.readWrite + return this.permissions.projects[projectName]?.project[type] === PermissionLevel.readWrite } - canView(type: UserPermissionsEntity): boolean { - if (!this.hasLimitedPermissions || !this.projectSettingsAreEnabled()) { + canView(type: UserPermissionsEntity, projectName: string): boolean { + if (!this.permissions) { + return false + } + + if (this.hasElevatedPrivileges || !this.projectSettingsAreEnabled()) { return true } - return ( - this.canEdit(type) || this.permissions?.project?.[type]=== UserPermissionsLevel.readOnly - ) - } + if (this.canEdit(type, projectName)) { + return true + } - getSettingsPermissionLevel(): UserPermissionsLevel { - return this.getPermissionLevel(UserPermissionsEntity.settings) - } + if (this.permissions.projects[projectName]?.project[type] === PermissionLevel.readOnly) { + return true + } - getAnatomyPermissionLevel(): UserPermissionsLevel { - return this.getPermissionLevel(UserPermissionsEntity.anatomy) + return false } - getUsersPermissionLevel(): UserPermissionsLevel { - return this.getPermissionLevel(UserPermissionsEntity.users) + getAnatomyPermissionLevel(projectName: string): PermissionLevel { + return this.getPermissionLevel(UserPermissionsEntity.anatomy, projectName) } - canEditSettings(): boolean { - return this.canEdit(UserPermissionsEntity.settings) + canEditSettings(projectName: string): boolean { + return this.canEdit(UserPermissionsEntity.settings, projectName) } - canEditAnatomy(): boolean { - return this.canEdit(UserPermissionsEntity.anatomy) + canEditAnatomy(projectName: string): boolean { + return this.canEdit(UserPermissionsEntity.anatomy, projectName) } - canEditUsers(): boolean { - return this.canEdit(UserPermissionsEntity.users) + canViewSettings(projectName?: string ): boolean { + if (projectName) { + return this.canView(UserPermissionsEntity.settings, projectName) + } + + return this.canViewAny(UserPermissionsEntity.settings) } - canViewSettings(): boolean { - return this.canView(UserPermissionsEntity.settings) + canViewAnatomy(projectName?: string): boolean { + if (projectName) { + return this.canView(UserPermissionsEntity.anatomy, projectName) + } + + return this.canViewAny(UserPermissionsEntity.anatomy) } + canViewAny(type: UserPermissionsEntity): boolean { + if (!this.permissions) { + return false + } + + for (const projectName of Object.keys(this.permissions.projects)) { + if (this.canView(type, projectName)) { + return true + } + } - canViewAnatomy(): boolean { - return this.canView(UserPermissionsEntity.anatomy) + return false } - canViewUsers(): boolean { - return this.canView(UserPermissionsEntity.users) + canViewUsers(projectName?: string): boolean { + if (projectName) { + return this.canView(UserPermissionsEntity.users, projectName) + } + + return this.canViewAny(UserPermissionsEntity.users) } projectSettingsAreEnabled(): boolean { - return this.permissions?.project?.enabled + return this.permissions?.user_level === 'user' } } -const useUserProjectPermissions = (projectName: string, hasLimitedPermissions?: boolean): UserPermissions | undefined => { - const { data: permissions } = projectName - ? useGetCurrentUserProjectPermissionsQuery({ projectName }) - : useGetCurrentUserPermissionsQuery() +const useUserProjectPermissions = (hasLimitedPermissions?: boolean): UserPermissions | undefined => { + const { data: permissions } = useGetCurrentUserPermissionsQuery() return new UserPermissions(permissions, hasLimitedPermissions) } -export { UserPermissionsLevel } +export { PermissionLevel} export default useUserProjectPermissions diff --git a/src/pages/ProjectManagerPage/ProjectAnatomy.jsx b/src/pages/ProjectManagerPage/ProjectAnatomy.jsx index f2b57ad6d..9bdd894ef 100644 --- a/src/pages/ProjectManagerPage/ProjectAnatomy.jsx +++ b/src/pages/ProjectManagerPage/ProjectAnatomy.jsx @@ -7,7 +7,7 @@ import AnatomyEditor from '@containers/AnatomyEditor' import copyToClipboard from '@helpers/copyToClipboard' import { usePaste } from '@context/pasteContext' -import useUserProjectPermissions, { UserPermissionsLevel } from '@hooks/useUserProjectPermissions' +import useUserProjectPermissions, { PermissionLevel } from '@hooks/useUserProjectPermissions' import EmptyPlaceholder from '@components/EmptyPlaceholder/EmptyPlaceholder' import { useSelector } from 'react-redux' @@ -16,8 +16,8 @@ const ProjectAnatomy = ({ projectName, projectList }) => { const [updateProjectAnatomy, { isLoading: isUpdating }] = useUpdateProjectAnatomyMutation() const { requestPaste } = usePaste() - const userPermissions = useUserProjectPermissions(projectName, isUser) - const accessLevel = userPermissions.getAnatomyPermissionLevel() + const userPermissions = useUserProjectPermissions(!isUser) + const accessLevel = userPermissions.getAnatomyPermissionLevel(projectName) const [formData, setFormData] = useState(null) const [isChanged, setIsChanged] = useState(false) @@ -61,34 +61,33 @@ const ProjectAnatomy = ({ projectName, projectList }) => { - + {!isUnassigned && ( - + Remove R + )} ) From a73b3b0f818faf2f214ea3e0cda1e5cda1b8e27c Mon Sep 17 00:00:00 2001 From: Florin Tudor Date: Wed, 13 Nov 2024 07:35:41 +0100 Subject: [PATCH 27/35] feature(Users): Enabled fuzzy filtering for projects and access groups also, added select/deselect all button to access groups modal --- src/hooks/useUserProjectPermissions.ts | 17 +- .../Users/ProjectUserAccess.tsx | 28 ++-- .../Users/ProjectUserAccessAssignDialog.tsx | 34 +++- src/pages/ProjectManagerPage/Users/hooks.ts | 87 +++++----- src/pages/ProjectManagerPage/Users/mappers.ts | 150 ++++++++---------- 5 files changed, 176 insertions(+), 140 deletions(-) diff --git a/src/hooks/useUserProjectPermissions.ts b/src/hooks/useUserProjectPermissions.ts index af4cd0016..d2ff83c55 100644 --- a/src/hooks/useUserProjectPermissions.ts +++ b/src/hooks/useUserProjectPermissions.ts @@ -104,7 +104,7 @@ class UserPermissions { return this.canEdit(UserPermissionsEntity.anatomy, projectName) } - canViewSettings(projectName?: string ): boolean { + canViewSettings(projectName?: string): boolean { if (projectName) { return this.canView(UserPermissionsEntity.settings, projectName) } @@ -114,17 +114,22 @@ class UserPermissions { canViewAnatomy(projectName?: string): boolean { if (projectName) { - return this.canView(UserPermissionsEntity.anatomy, projectName) + return this.canView(UserPermissionsEntity.anatomy, projectName) } return this.canViewAny(UserPermissionsEntity.anatomy) } + canViewAny(type: UserPermissionsEntity): boolean { + if (this.hasElevatedPrivileges) { + return true + } + if (!this.permissions) { return false } - for (const projectName of Object.keys(this.permissions.projects)) { + for (const projectName of Object.keys(this.permissions?.projects || {})) { if (this.canView(type, projectName)) { return true } @@ -146,11 +151,13 @@ class UserPermissions { } } -const useUserProjectPermissions = (hasLimitedPermissions?: boolean): UserPermissions | undefined => { +const useUserProjectPermissions = ( + hasLimitedPermissions?: boolean, +): UserPermissions | undefined => { const { data: permissions } = useGetCurrentUserPermissionsQuery() return new UserPermissions(permissions, hasLimitedPermissions) } -export { PermissionLevel} +export { PermissionLevel } export default useUserProjectPermissions diff --git a/src/pages/ProjectManagerPage/Users/ProjectUserAccess.tsx b/src/pages/ProjectManagerPage/Users/ProjectUserAccess.tsx index e3a928f75..3724a57a7 100644 --- a/src/pages/ProjectManagerPage/Users/ProjectUserAccess.tsx +++ b/src/pages/ProjectManagerPage/Users/ProjectUserAccess.tsx @@ -88,14 +88,9 @@ const ProjectUserAccess = () => { const filteredProjects = getFilteredProjects(projects, projectFilters) const filteredSelectedProjects = getFilteredSelectedProjects(selectedProjects, projectFilters) - const filteredUnassignedUsers = getFilteredUsers( - unassignedUsers, - filters?.find((el: Filter) => el.label === 'User'), - ) - const filteredNonManagerUsers = getFilteredUsers( - activeNonManagerUsers, - filters?.find((el: Filter) => el.label === 'User'), - ) + const userFilter = filters?.find((el: Filter) => el.label === 'User') + const filteredUnassignedUsers = getFilteredUsers(unassignedUsers, userFilter) + const filteredNonManagerUsers = getFilteredUsers(activeNonManagerUsers, userFilter) const selectedUsers = getSelectedUsers(selectedAccessGroupUsers, filteredUnassignedUsers) const addActionEnabled = filteredSelectedProjects.length > 0 && selectedUsers.length > 0 @@ -115,7 +110,7 @@ const ProjectUserAccess = () => { const resetSelectedUsers = () => setSelectedAccessGroupUsers({ users: [] }) - const onSave = async (changes: $Any, users: string[]) => { + const onSave = async (users: string[], changes: $Any) => { const errorMessage = await updateUserAccessGroups(users, changes) if (errorMessage) { toast.error(errorMessage) @@ -233,8 +228,11 @@ const ProjectUserAccess = () => { Access groups {filteredSelectedProjects.length > 0 ? ( - - {getFilteredAccessGroups(accessGroupList, filters) + + {getFilteredAccessGroups( + accessGroupList, + filters.find((filter: Filter) => filter.label === 'Access Group'), + ) .map((item: AccessGroupObject) => item.name) .map((accessGroup) => { const selectedUsers = getAccessGroupUsers(selectedAccessGroupUsers!, accessGroup) @@ -284,7 +282,13 @@ const ProjectUserAccess = () => { ({ ...item, selected: false }))} + accessGroups={getFilteredAccessGroups( + accessGroupList, + filters.find((filter: Filter) => filter.label === 'Access Group'), + ).map((item) => ({ + ...item, + selected: false, + }))} onSave={onSave} onClose={function (): void { setShowDialog(false) diff --git a/src/pages/ProjectManagerPage/Users/ProjectUserAccessAssignDialog.tsx b/src/pages/ProjectManagerPage/Users/ProjectUserAccessAssignDialog.tsx index 7ae19280d..a182f8372 100644 --- a/src/pages/ProjectManagerPage/Users/ProjectUserAccessAssignDialog.tsx +++ b/src/pages/ProjectManagerPage/Users/ProjectUserAccessAssignDialog.tsx @@ -1,5 +1,5 @@ import { useState } from 'react' -import { FormLayout, Dialog, Button, Icon } from '@ynput/ayon-react-components' +import { FormLayout, Dialog, Button, Icon, Spacer } from '@ynput/ayon-react-components' import { $Any } from '@types' import clsx from 'clsx' import * as Styled from './ProjectUserAccessAssignDialog.styled' @@ -20,7 +20,7 @@ type Props = { accessGroups: $Any[] users: string[] userAccessGroups: AccessGroupUsers - onSave: (items: AccessGroupItem[], users: string[]) => void + onSave: (users: string[], items: AccessGroupItem[]) => void onClose: () => void } @@ -62,22 +62,33 @@ const ProjectUserAccessAssignDialog = ({ const initialStatesList = Object.keys(initialStates).map(agName => ({name: agName, status: initialStates[agName]})) const [accessGroupItems, setAccessGroupItems] = useState(initialStatesList) + const allSelected = accessGroupItems.find(item => item.status !== SelectionStatus.All) === undefined const toggleAccessGroup = (accessGroup: AccessGroupItem) => { - const newStatus = [SelectionStatus.Mixed, SelectionStatus.All].includes(accessGroup.status) ? SelectionStatus.None : SelectionStatus.All + const newStatus = [SelectionStatus.Mixed, SelectionStatus.All].includes(accessGroup.status) + ? SelectionStatus.None + : SelectionStatus.All setAccessGroupItems((prev: AccessGroupItem[]) => { const idx = prev.findIndex((item) => item.name === accessGroup.name) return [...prev.slice(0, idx), {...accessGroup, status: newStatus}, ...prev.slice(idx + 1)] }) } + + const handleToggleAll = (value: boolean) => { + setAccessGroupItems((prev: AccessGroupItem[]) => { + return prev.map(item => ({...item, status: value ? SelectionStatus.All : SelectionStatus.None})) + }) + + } + const handleClose = () => { onClose() } const handleSave = () => { const changes = accessGroupItems.filter(item => initialStates[item.name] !== item.status) - onSave(changes, users) + onSave(users, changes) onClose() } @@ -85,7 +96,18 @@ const ProjectUserAccessAssignDialog = ({ handleSave()} />} + footer={ + <> + + <> + {/* @ts-ignore */} + + + handleToggleAll(!allSelected)} + /> + + + ) } diff --git a/src/pages/ProjectManagerPage/Users/ProjectUserAccessSearchFilterWrapper.tsx b/src/pages/ProjectManagerPage/Users/ProjectUserAccessSearchFilterWrapper.tsx index 48ad35f3f..c42de4cd5 100644 --- a/src/pages/ProjectManagerPage/Users/ProjectUserAccessSearchFilterWrapper.tsx +++ b/src/pages/ProjectManagerPage/Users/ProjectUserAccessSearchFilterWrapper.tsx @@ -1,32 +1,37 @@ +import { useEffect, useState } from 'react' + import SearchFilter from '@components/SearchFilter/SearchFilter' import { Filter } from '@components/SearchFilter/types' -import { useEffect, useState } from 'react' -import { useProjectAccessSearchFilterBuiler } from './hooks' -import { $Any } from '@types' -import { useListProjectsQuery } from '@queries/project/getProject' import { useGetAccessGroupsQuery } from '@queries/accessGroups/getAccessGroups' -import { useSelector } from 'react-redux' -import { useGetUsersQuery } from '@queries/user/getUsers' +import { $Any } from '@types' + +import { useProjectAccessSearchFilterBuiler } from './hooks' + type Props = { filters: $Any, + projects: $Any, + users: $Any, onChange: $Any } -const ProjectUserAccessSearchFilterWrapper = ({ filters: _filters, onChange }: Props) => { - const selfName = useSelector((state: $Any) => state.user.name) - const { isLoading: isProjectsLoading, data: projects = [] } = useListProjectsQuery({}) - const { isLoading: isUsersLoading, data: users = [] } = useGetUsersQuery({ selfName }) +const ProjectUserAccessSearchFilterWrapper = ({ + filters: _filters, + projects, + users, + onChange, +}: Props) => { const { isLoading: isAccessGroupsLoading, data: accessGroups = [] } = useGetAccessGroupsQuery({ projectName: '_', }) const options = useProjectAccessSearchFilterBuiler({ - projects: isProjectsLoading - ? [] - // @ts-ignore - : projects.map((project: $Any) => ({ id: project.name, label: project.name })), - users: isUsersLoading ? [] : users.map((user: $Any) => ({ id: user.name, label: user.name, img: `/api/users/${user.name}/avatar` })), + projects: projects.map((project: $Any) => ({ id: project.name, label: project.name })), + users: users.map((user: $Any) => ({ + id: user.name, + label: user.name, + img: `/api/users/${user.name}/avatar`, + })), accessGroups: isAccessGroupsLoading ? [] : accessGroups!.map((accessGroup: $Any) => ({ diff --git a/src/pages/ProjectManagerPage/Users/UserRow.tsx b/src/pages/ProjectManagerPage/Users/UserRow.tsx index 31a87a141..e02a0ce0c 100644 --- a/src/pages/ProjectManagerPage/Users/UserRow.tsx +++ b/src/pages/ProjectManagerPage/Users/UserRow.tsx @@ -65,7 +65,7 @@ export const UserRow = ({ return ( {/* @ts-ignore */} - + { e.stopPropagation() - onAdd(rowData.name) + // Handle click outside selection on hovering, make sure selection changes accordingly (one user selection only) + onAdd() }} > {isUnassigned ? ( diff --git a/src/pages/ProjectManagerPage/Users/hooks.ts b/src/pages/ProjectManagerPage/Users/hooks.ts index 9ab087a12..d6500a1fb 100644 --- a/src/pages/ProjectManagerPage/Users/hooks.ts +++ b/src/pages/ProjectManagerPage/Users/hooks.ts @@ -4,7 +4,9 @@ import { useState } from 'react' import { useUpdateProjectUsersMutation } from '@queries/project/updateProject' import { useDispatch } from 'react-redux' import { SelectionStatus } from './types' -import { Option } from '@components/SearchFilter/types' +import { Filter, Option } from '@components/SearchFilter/types' +import { useAppSelector } from '@state/store' +import { useSetFrontendPreferencesMutation } from '@queries/user/updateUser' type FilterValues = { id: string @@ -31,16 +33,16 @@ const useProjectAccessGroupData = () => { const users = result.data const accessGroupUsers: $Any = {} - const removeUserAccessGroup = (user: string, accessGroup: string) => { + const removeUserAccessGroup = (user: string, accessGroup?: string) => { for (const project of selectedProjects) { // @ts-ignore if (!users![project][user]) { continue } // @ts-ignore - const updatedAccessGroups = users![project][user]?.filter( + const updatedAccessGroups = accessGroup ? users![project][user]?.filter( (item: string) => item !== accessGroup, - ) + ) : [] try { updateUser({ projectName: project, @@ -133,4 +135,24 @@ const useProjectAccessSearchFilterBuiler = ({ return options } -export { useProjectAccessGroupData, useProjectAccessSearchFilterBuiler } +const userPageFilters = (): [filters: Filter[], setFilters: (value: Filter[]) => void] => { + const pageId = 'project.settings.user.access_groups' + const [updateUserPreferences] = useSetFrontendPreferencesMutation() + const userName = useAppSelector((state) => state.user.name) + const frontendPreferences = useAppSelector((state) => state.user.data.frontendPreferences) + const frontendPreferencesFilters: { + [pageId: string]: [] + } = frontendPreferences?.filters + + const filters = frontendPreferencesFilters?.[pageId] || [] + + const setFilters = (value: Filter[]) => { + const updatedUserFilters = { ...frontendPreferencesFilters, [pageId]: value} + const updatedFrontendPreferences = { ...frontendPreferences, filters: updatedUserFilters } + updateUserPreferences({ userName, patchData: updatedFrontendPreferences }) + } + + return [filters, setFilters] +} + +export { useProjectAccessGroupData, useProjectAccessSearchFilterBuiler, userPageFilters } diff --git a/src/pages/ProjectManagerPage/Users/mappers.ts b/src/pages/ProjectManagerPage/Users/mappers.ts index 5558cb4e0..106e3c0be 100644 --- a/src/pages/ProjectManagerPage/Users/mappers.ts +++ b/src/pages/ProjectManagerPage/Users/mappers.ts @@ -1,9 +1,10 @@ import { AccessGroupObject } from '@api/rest/accessGroups' -import { AccessGroupUsers, SelectedAccessGroupUsers } from './types' +import { AccessGroupUsers, SelectedAccessGroupUsers, SelectionStatus } from './types' import { Filter } from '@components/SearchFilter/types' import { ProjectNode, UserNode } from '@api/graphql' import { GetProjectsUsersApiResponse } from '@queries/project/getProject' import { UserPermissions, UserPermissionsEntity } from '@hooks/useUserProjectPermissions' +import { $Any } from '@types' const getAllProjectUsers = (groupedUsers: AccessGroupUsers): string[] => { let allUsers: string[] = [] @@ -68,16 +69,13 @@ const getAccessGroupUsers = ( return selectedAccessGroupUsers.accessGroup === accessGroup ? selectedAccessGroupUsers.users : [] } -const getFilteredSelectedProjects = (projects: string[], filters: Filter) => { - if (!filters) { +const getFilteredSelectedProjects = (projects: string[], filteredProjects: ProjectNode[] ) => { + if (!filteredProjects) { return projects } - const filterProjects = filters && filters.values!.map((match: Filter) => match.id) - if (filters!.inverted) { - return projects.filter((project) => !filterProjects.includes(project)) - } - return projects.filter((project) => filterProjects.includes(project)) + const filteredProjectNames = filteredProjects.map(project => project.name) + return projects.filter((project) => filteredProjectNames.includes(project)) } const exactFilter = (entities: T[], filters: Filter): T[] => { @@ -88,15 +86,15 @@ const exactFilter = (entities: T[], filters: Filter): return entities.filter((entity: T) => filterUsers.includes(entity.name)) } -const fuzzyFilter = (users: T[], filters: Filter): T[] => { +const fuzzyFilter = (entities: T[], filters: Filter): T[] => { const filterString = filters.values![0].id if (filters!.inverted) { - return users.filter((user: T) => user.name.indexOf(filterString) == -1) + return entities.filter((entity: T) => entity.name.indexOf(filterString) == -1) } - return users.filter((user: T) => user.name.indexOf(filterString) != -1) + return entities.filter((entity: T) => entity.name.indexOf(filterString) != -1) } -const getFilteredProjects = (projects: ProjectNode[], filter: Filter): ProjectNode[] => { +const getFilteredProjects = (projects: ProjectNode[], filter?: Filter): ProjectNode[] => { if (!filter || !filter.values || filter.values.length == 0) { return projects } @@ -122,7 +120,7 @@ const getFilteredUsers = (users: UserNode[], filter?: Filter): UserNode[] => { const getFilteredAccessGroups = ( accessGroupList: AccessGroupObject[], - filter: Filter, + filter?: Filter, ): AccessGroupObject[] => { if (!filter || !filter.values || filter.values.length == 0) { return accessGroupList @@ -144,6 +142,37 @@ const canAllEditUsers = (projects: string[], userPermissions?: UserPermissions) return true } + const mapInitialAccessGroupStates = ( + accessGroups: $Any[], + users: string[], + userAccessGroups: AccessGroupUsers, + ) => { + const getStatus = (users: string[], accessGroupUsers: string[]) => { + const usersSet = new Set(users) + const accessGroupUsersSet = new Set(accessGroupUsers) + const intersection = usersSet.intersection(accessGroupUsersSet) + + // No users in ag users + if (intersection.size == 0) { + return SelectionStatus.None + } + + //All users / some users in ag users + return intersection.size == usersSet.size ? SelectionStatus.All : SelectionStatus.Mixed + } + + const data: $Any = {} + accessGroups.map((ag) => { + if (userAccessGroups[ag.name] === undefined) { + data[ag.name] = SelectionStatus.None + } else { + data[ag.name] = getStatus(users, userAccessGroups[ag.name]) + } + }) + + return data + } + export { canAllEditUsers, mapUsersByAccessGroups, @@ -154,4 +183,5 @@ export { getFilteredSelectedProjects, getFilteredProjects, getFilteredUsers, + mapInitialAccessGroupStates, } diff --git a/src/pages/SettingsPage/AccessGroups/index.jsx b/src/pages/SettingsPage/AccessGroups/index.jsx index e24ebbc18..f257090d3 100644 --- a/src/pages/SettingsPage/AccessGroups/index.jsx +++ b/src/pages/SettingsPage/AccessGroups/index.jsx @@ -15,6 +15,7 @@ const AccessGroups = () => { diff --git a/src/pages/SettingsPage/UsersSettings/UsersSettings.jsx b/src/pages/SettingsPage/UsersSettings/UsersSettings.jsx index db7838171..273c14640 100644 --- a/src/pages/SettingsPage/UsersSettings/UsersSettings.jsx +++ b/src/pages/SettingsPage/UsersSettings/UsersSettings.jsx @@ -332,11 +332,12 @@ const UsersSettings = () => { />
diff --git a/src/pages/UserDashboardPage/UserDashboardPage.jsx b/src/pages/UserDashboardPage/UserDashboardPage.jsx index d1f1c34bf..0d066fefe 100644 --- a/src/pages/UserDashboardPage/UserDashboardPage.jsx +++ b/src/pages/UserDashboardPage/UserDashboardPage.jsx @@ -94,6 +94,7 @@ const UserDashboardPage = () => { collapsedId="dashboard" styleSection={{ position: 'relative', height: '100%', minWidth: 200, maxWidth: 200 }} hideCode + hideAddProjectButton={module !== 'overview'} multiselect={isProjectsMultiSelect} selection={isProjectsMultiSelect ? selectedProjects : selectedProjects[0]} onSelect={(p) => setSelectedProjects(isProjectsMultiSelect ? p : [p])} From fd5898c1cee52c58e72a1fa7821f29bcd9675aa0 Mon Sep 17 00:00:00 2001 From: Florin Tudor Date: Fri, 15 Nov 2024 09:42:43 +0100 Subject: [PATCH 35/35] fix(Editor): Consistent search filtering between filter component and user lists --- .../Users/ProjectUserAccess.tsx | 34 ++++++++++++++----- src/pages/ProjectManagerPage/Users/mappers.ts | 8 +++-- src/pages/ProjectManagerPage/Users/types.ts | 2 +- 3 files changed, 33 insertions(+), 11 deletions(-) diff --git a/src/pages/ProjectManagerPage/Users/ProjectUserAccess.tsx b/src/pages/ProjectManagerPage/Users/ProjectUserAccess.tsx index 974139b76..30854cd88 100644 --- a/src/pages/ProjectManagerPage/Users/ProjectUserAccess.tsx +++ b/src/pages/ProjectManagerPage/Users/ProjectUserAccess.tsx @@ -241,24 +241,38 @@ const ProjectUserAccess = () => { { key: 'a', action: () => { - if(!selectedAccessGroupUsers?.users || hoveredUser?.user) { + if(!selectedAccessGroupUsers?.users && !hoveredUser?.user) { return } - handleAdd(selectedAccessGroupUsers?.users) + let actionedUsers = selectedAccessGroupUsers?.users || [] + if (hoveredUser?.user && !actionedUsers.includes(hoveredUser.user)) { + actionedUsers = [hoveredUser.user] + setSelectedAccessGroupUsers({ accessGroup: hoveredUser.accessGroup, users: [hoveredUser.user]}) + } + + handleAdd(actionedUsers) }, }, { key: 'r', action: () => { - if (!selectedAccessGroupUsers?.users || !hoveredUser?.accessGroup) { + if (!selectedAccessGroupUsers?.accessGroup && !hoveredUser?.accessGroup) { return } - onRemove(hoveredUser!.accessGroup!)([hoveredUser.user]) + let actionedUsers = selectedAccessGroupUsers?.users || [] + let actionedAccessGroup = selectedAccessGroupUsers?.accessGroup + if (hoveredUser?.user && !actionedUsers.includes(hoveredUser.user)) { + actionedUsers = [hoveredUser.user] + actionedAccessGroup = hoveredUser.accessGroup + setSelectedAccessGroupUsers({ accessGroup: hoveredUser.accessGroup, users: [hoveredUser.user]}) + } + + onRemove(actionedAccessGroup)(actionedUsers) }, }, ], - [hoveredUser], + [selectedAccessGroupUsers, hoveredUser], ) const handleProjectSelectionChange = (selection: string[]) => { @@ -284,7 +298,7 @@ const ProjectUserAccess = () => { return ( // @ts-ignore - + {/* @ts-ignore */} @@ -351,7 +365,9 @@ const ProjectUserAccess = () => { readOnly={!hasEditRightsOnProject} onContextMenu={handleAddContextMenu} onAdd={handleAdd} - onHoverRow={(userName: string) => setHoveredUser({ user: userName })} + onHoverRow={(userName: string) => { + userName ? setHoveredUser({ user: userName }) : setHoveredUser({}) + }} onSelectUsers={(selection) => setSelectedAccessGroupUsers({ users: selection })} sortable isUnassigned @@ -403,7 +419,9 @@ const ProjectUserAccess = () => { mappedUsers[accessGroup].includes(user.name), )} onHoverRow={(userName: string) => - setHoveredUser({ accessGroup, user: userName }) + userName + ? setHoveredUser({ accessGroup, user: userName }) + : setHoveredUser({}) } onSelectUsers={(selection: string[]) => updateSelectedAccessGroupUsers(accessGroup, selection) diff --git a/src/pages/ProjectManagerPage/Users/mappers.ts b/src/pages/ProjectManagerPage/Users/mappers.ts index 106e3c0be..18a675648 100644 --- a/src/pages/ProjectManagerPage/Users/mappers.ts +++ b/src/pages/ProjectManagerPage/Users/mappers.ts @@ -5,6 +5,7 @@ import { ProjectNode, UserNode } from '@api/graphql' import { GetProjectsUsersApiResponse } from '@queries/project/getProject' import { UserPermissions, UserPermissionsEntity } from '@hooks/useUserProjectPermissions' import { $Any } from '@types' +import { matchSorter } from 'match-sorter' const getAllProjectUsers = (groupedUsers: AccessGroupUsers): string[] => { let allUsers: string[] = [] @@ -88,10 +89,13 @@ const exactFilter = (entities: T[], filters: Filter): const fuzzyFilter = (entities: T[], filters: Filter): T[] => { const filterString = filters.values![0].id + + const matches = matchSorter(entities, filterString, {keys: ['name']}) if (filters!.inverted) { - return entities.filter((entity: T) => entity.name.indexOf(filterString) == -1) + return entities.filter((entity: T) => !matches.includes(entity)) } - return entities.filter((entity: T) => entity.name.indexOf(filterString) != -1) + + return matches } const getFilteredProjects = (projects: ProjectNode[], filter?: Filter): ProjectNode[] => { diff --git a/src/pages/ProjectManagerPage/Users/types.ts b/src/pages/ProjectManagerPage/Users/types.ts index fcc3edfd4..734943cbc 100644 --- a/src/pages/ProjectManagerPage/Users/types.ts +++ b/src/pages/ProjectManagerPage/Users/types.ts @@ -4,7 +4,7 @@ export type AccessGroupUsers = { export type HoveredUser = { accessGroup?: string - user: string + user?: string } export type SelectedAccessGroupUsers = {