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 (
+
+ setValue(0)}
+ variant={!value ? 'danger' : 'surface'}
+ />
+ setValue(1)}
+ variant={value === 1 ? 'filled' : 'surface'}
+ />
+ setValue(2)}
+ variant={value === 2 ? 'tertiary' : 'surface'}
+ />
+
+ )
+}
+
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 }) => {
}}
/>
+ {accessLevel === 1 && "Read-only"}
1}
saving={isUpdating}
/>
>
}
>
+ {accessLevel ? (
+ />) : "You don't have access to this project's anatomy. We should redirect you somewhere else."}
)
diff --git a/src/pages/ProjectManagerPage/ProjectManagerPage.jsx b/src/pages/ProjectManagerPage/ProjectManagerPage.jsx
index 388a2cc5a..4dfa496a8 100644
--- a/src/pages/ProjectManagerPage/ProjectManagerPage.jsx
+++ b/src/pages/ProjectManagerPage/ProjectManagerPage.jsx
@@ -49,7 +49,9 @@ const ProjectManagerPage = () => {
withDefault(StringParam, projectName),
)
- const { data: permissions} = useGetCurrentUserProjectPermissionsQuery({ projectName: selectedProject })
+ const { data: permissions } = useGetCurrentUserProjectPermissionsQuery({
+ projectName: selectedProject,
+ })
// UPDATE DATA
const [updateProject] = useUpdateProjectMutation()
@@ -85,20 +87,23 @@ const ProjectManagerPage = () => {
}
const links = []
- const projectPermissions = permissions?.project_settings
+ if (permissions?.project) {
+ // How to read this code:
+ // If project management restrictions are NOT enabled
+ // OR if project management restrctions ARE enabled
+ // and access to anatomy is allowed
- if (projectPermissions){
- if (!projectPermissions.enabled || projectPermissions.anatomy_update) {
+ if (!permissions.project.enabled || permissions.project.anatomy) {
links.push({
- name: 'Anatomy',
- path: '/manageProjects/anatomy',
- module: 'anatomy',
- accessLevels: [],
- shortcut: 'A+A',
+ name: 'Anatomy',
+ path: '/manageProjects/anatomy',
+ module: 'anatomy',
+ accessLevels: [],
+ shortcut: 'A+A',
})
}
- if (!projectPermissions.enabled || projectPermissions.addon_settings_update) {
+ if (!permissions.project.enabled || permissions.project.settings) {
links.push({
name: 'Project settings',
path: '/manageProjects/projectSettings',
@@ -109,7 +114,6 @@ const ProjectManagerPage = () => {
}
}
-
links.push(
{
name: 'Site settings',
From ee5c4c8a0483a3cc989372a746c8f8e467bb1981 Mon Sep 17 00:00:00 2001
From: Florin Tudor
Date: Tue, 22 Oct 2024 11:08:34 +0200
Subject: [PATCH 08/35] feature(Users): Limiting user access to project
settings module based on access rules
---
.../AddonSettings/AddonSettings.jsx | 36 ++++++---
src/containers/projectList.jsx | 13 ++-
src/hooks/useUserProjectPermissions.ts | 79 +++++++++++++++++++
.../ProjectManagerPage/ProjectAnatomy.jsx | 19 ++---
.../ProjectManagerPage/ProjectManagerPage.jsx | 21 ++---
.../ProjectManagerPageContainer.jsx | 2 +-
6 files changed, 128 insertions(+), 42 deletions(-)
create mode 100644 src/hooks/useUserProjectPermissions.ts
diff --git a/src/containers/AddonSettings/AddonSettings.jsx b/src/containers/AddonSettings/AddonSettings.jsx
index afd6da800..a979de455 100644
--- a/src/containers/AddonSettings/AddonSettings.jsx
+++ b/src/containers/AddonSettings/AddonSettings.jsx
@@ -1,4 +1,4 @@
-import { useState, useMemo } from 'react'
+import { useState, useMemo } from 'react'
import { useSelector } from 'react-redux'
import { toast } from 'react-toastify'
@@ -37,6 +37,7 @@ import { getValueByPath, setValueByPath, sameKeysStructure, compareObjects } fro
import arrayEquals from '@helpers/arrayEquals'
import { cloneDeep } from 'lodash'
import { usePaste } from '@context/pasteContext'
+import useUserProjectPermissions from '@hooks/useUserProjectPermissions'
/*
* key is {addonName}|{addonVersion}|{variant}|{siteId}|{projectKey}
@@ -73,6 +74,8 @@ const AddonSettings = ({ projectName, showSites = false }) => {
const [promoteBundle] = usePromoteBundleMutation()
const { requestPaste } = usePaste()
+ const userPermissions = useUserProjectPermissions(projectName)
+
const projectKey = projectName || '_'
const onAddonFocus = ({ addonName, addonVersion, siteId, path }) => {
@@ -281,8 +284,7 @@ const AddonSettings = ({ projectName, showSites = false }) => {
}
return { ...unpinnedKeys, [addonKey]: addonChanges }
- }) // setUnpinnedKeys
-
+ }) // setUnpinnedKeys
}
}
}
@@ -294,15 +296,15 @@ const AddonSettings = ({ projectName, showSites = false }) => {
const onRemoveOverride = async (addon, siteId, path) => {
// Remove a single override for this addon (within current project and variant)
// path is an array of strings
-
- // TODO: Use this to staged unpin.
+
+ // TODO: Use this to staged unpin.
// It is not used now because we don't have an information about the original value
//
// const key = `${addon.name}|${addon.version}|${addon.variant}|${siteId || '_'}|${projectKey}`
//
// setChangedKeys((changedKeys) => {
// const keyData = changedKeys[key] || []
- //
+ //
// const index = keyData.findIndex((keyItem) => arrayEquals(keyItem, path))
// if (index === -1) {
// keyData.push(path)
@@ -502,8 +504,8 @@ const AddonSettings = ({ projectName, showSites = false }) => {
Are you sure you want to push {bundleName} to production?
- This will mark the current staging bundle as production and copy all staging
- studio settings and staging projects overrides to production as well.
+ This will mark the current staging bundle as production and copy all staging studio
+ settings and staging projects overrides to production as well.
>
)
@@ -627,6 +629,10 @@ const AddonSettings = ({ projectName, showSites = false }) => {
/>
{
{settingsListHeader}
-
+
{selectedAddons
.filter((addon) => !addon.isBroken)
.reverse()
@@ -761,10 +771,10 @@ const AddonSettings = ({ projectName, showSites = false }) => {
{commitToolbar}
-
{/*}
diff --git a/src/containers/projectList.jsx b/src/containers/projectList.jsx
index bce859aa6..a6aae0608 100644
--- a/src/containers/projectList.jsx
+++ b/src/containers/projectList.jsx
@@ -16,6 +16,7 @@ import { useSetFrontendPreferencesMutation } from '@/services/user/updateUser'
import useTableLoadingData from '@hooks/useTableLoadingData'
import { useProjectSelectDispatcher } from './ProjectMenu/hooks/useProjectSelectDispatcher'
import useAyonNavigate from '@hooks/useAyonNavigate'
+import useUserProjectPermissions from '@hooks/useUserProjectPermissions'
const formatName = (rowData, defaultTitle, field = 'name') => {
if (rowData[field] === '_') return defaultTitle
@@ -54,6 +55,11 @@ const StyledProjectName = styled.div`
}
`
+const ButtonPlaceholder = styled.div`
+ height: 26px;
+ width: 100%;
+`
+
const StyledAddButton = styled(Button)`
overflow: hidden;
position: relative;
@@ -221,6 +227,8 @@ const ProjectList = ({
const [updateUserPreferences] = useSetFrontendPreferencesMutation()
+ const userPermissions = useUserProjectPermissions()
+
const handlePinProjects = async (sel, isPinning) => {
try {
const newPinnedProjects = [...pinnedProjects]
@@ -400,7 +408,7 @@ const ProjectList = ({
disabled={onSelectAllDisabled}
/>
)}
- {isProjectManager && (
+ {(isProjectManager || userPermissions.canCreateProject()) ? (
{/*
*/}
@@ -409,7 +417,8 @@ const ProjectList = ({
{/*
*/}
- )}
+ ): }
+
{isCollapsible && (
{
+ const { data: permissions } = projectName
+ ? useGetCurrentUserProjectPermissionsQuery({ projectName })
+ : useGetCurrentUserPermissionsQuery()
+
+ return new UserPermissions(permissions)
+}
+
+export { UserPermissionsLevel }
+export default useUserProjectPermissions
diff --git a/src/pages/ProjectManagerPage/ProjectAnatomy.jsx b/src/pages/ProjectManagerPage/ProjectAnatomy.jsx
index ec902a90d..7b1116667 100644
--- a/src/pages/ProjectManagerPage/ProjectAnatomy.jsx
+++ b/src/pages/ProjectManagerPage/ProjectAnatomy.jsx
@@ -2,12 +2,12 @@ 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'
import copyToClipboard from '@helpers/copyToClipboard'
import { usePaste } from '@context/pasteContext'
+import useUserProjectPermissions, { UserPermissionsLevel } from '@hooks/useUserProjectPermissions'
const ProjectAnatomy = ({ projectName, projectList }) => {
const [formData, setFormData] = useState(null)
@@ -16,14 +16,8 @@ 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 userPermissions = useUserProjectPermissions(projectName)
+ const accessLevel = userPermissions.getAnatomyPermissionLevel()
const saveAnatomy = () => {
updateProjectAnatomy({ projectName, anatomy: formData })
@@ -72,19 +66,20 @@ const ProjectAnatomy = ({ projectName, projectList }) => {
}}
/>
- {accessLevel === 1 && "Read-only"}
+ {UserPermissionsLevel.readOnly === accessLevel && "Read-only"}
1}
+ active={isChanged && UserPermissionsLevel.readWrite === accessLevel}
saving={isUpdating}
/>
>
}
>
- {accessLevel ? (
+ {UserPermissionsLevel.none !== accessLevel ? (
{
return (
@@ -49,9 +49,7 @@ const ProjectManagerPage = () => {
withDefault(StringParam, projectName),
)
- const { data: permissions } = useGetCurrentUserProjectPermissionsQuery({
- projectName: selectedProject,
- })
+ const userPermissions = useUserProjectPermissions(selectedProject)
// UPDATE DATA
const [updateProject] = useUpdateProjectMutation()
@@ -87,13 +85,8 @@ const ProjectManagerPage = () => {
}
const links = []
- if (permissions?.project) {
- // How to read this code:
- // If project management restrictions are NOT enabled
- // OR if project management restrctions ARE enabled
- // and access to anatomy is allowed
-
- if (!permissions.project.enabled || permissions.project.anatomy) {
+ if (userPermissions.projectSettingsAreEnabled()) {
+ if (userPermissions.canViewAnatomy()) {
links.push({
name: 'Anatomy',
path: '/manageProjects/anatomy',
@@ -103,7 +96,7 @@ const ProjectManagerPage = () => {
})
}
- if (!permissions.project.enabled || permissions.project.settings) {
+ if (userPermissions.canViewSettings()) {
links.push({
name: 'Project settings',
path: '/manageProjects/projectSettings',
@@ -157,8 +150,8 @@ const ProjectManagerPage = () => {
onDeleteProject={handleDeleteProject}
onActivateProject={handleActivateProject}
>
- {module === 'anatomy' && }
- {module === 'projectSettings' && }
+ {module === 'anatomy' && userPermissions.canViewAnatomy() && }
+ {module === 'projectSettings' && userPermissions.canViewSettings() && }
{module === 'siteSettings' && }
{module === 'roots' && }
{module === 'teams' && }
diff --git a/src/pages/ProjectManagerPage/ProjectManagerPageContainer.jsx b/src/pages/ProjectManagerPage/ProjectManagerPageContainer.jsx
index b59ea22ae..a10942af1 100644
--- a/src/pages/ProjectManagerPage/ProjectManagerPageContainer.jsx
+++ b/src/pages/ProjectManagerPage/ProjectManagerPageContainer.jsx
@@ -28,7 +28,7 @@ const ProjectManagerPageContainer = ({
onDeleteProject={onDeleteProject}
onActivateProject={onActivateProject}
onNewProject={onNewProject}
- isProjectManager
+ isProjectManager={!isUser}
{...props}
/>
),
From 00f661e7a43537113538e9ea0a167a45f37f2886 Mon Sep 17 00:00:00 2001
From: Florin Tudor
Date: Thu, 24 Oct 2024 10:10:06 +0200
Subject: [PATCH 09/35] feature(Users): Adding better placeholder messages when
lacking view permissions
---
.../AddonSettings/AddonSettings.jsx | 8 +++++
.../ProjectManagerPage/ProjectAnatomy.jsx | 30 +++++++++++++------
.../ProjectManagerPage/ProjectManagerPage.jsx | 4 +--
3 files changed, 31 insertions(+), 11 deletions(-)
diff --git a/src/containers/AddonSettings/AddonSettings.jsx b/src/containers/AddonSettings/AddonSettings.jsx
index a979de455..9547558ce 100644
--- a/src/containers/AddonSettings/AddonSettings.jsx
+++ b/src/containers/AddonSettings/AddonSettings.jsx
@@ -38,6 +38,7 @@ import arrayEquals from '@helpers/arrayEquals'
import { cloneDeep } from 'lodash'
import { usePaste } from '@context/pasteContext'
import useUserProjectPermissions from '@hooks/useUserProjectPermissions'
+import EmptyPlaceholder from '@components/EmptyPlaceholder/EmptyPlaceholder'
/*
* key is {addonName}|{addonVersion}|{variant}|{siteId}|{projectKey}
@@ -647,6 +648,13 @@ const AddonSettings = ({ projectName, showSites = false }) => {
setCurrentSelection(null)
}
+ if (!userPermissions.canViewSettings()) {
+ return
+ }
+
return (
diff --git a/src/pages/ProjectManagerPage/ProjectAnatomy.jsx b/src/pages/ProjectManagerPage/ProjectAnatomy.jsx
index 7b1116667..dece8f444 100644
--- a/src/pages/ProjectManagerPage/ProjectAnatomy.jsx
+++ b/src/pages/ProjectManagerPage/ProjectAnatomy.jsx
@@ -8,6 +8,7 @@ import AnatomyEditor from '@containers/AnatomyEditor'
import copyToClipboard from '@helpers/copyToClipboard'
import { usePaste } from '@context/pasteContext'
import useUserProjectPermissions, { UserPermissionsLevel } from '@hooks/useUserProjectPermissions'
+import EmptyPlaceholder from '@components/EmptyPlaceholder/EmptyPlaceholder'
const ProjectAnatomy = ({ projectName, projectList }) => {
const [formData, setFormData] = useState(null)
@@ -57,6 +58,7 @@ const ProjectAnatomy = ({ projectName, projectList }) => {
{
}}
/>
- {UserPermissionsLevel.readOnly === accessLevel && "Read-only"}
+ {UserPermissionsLevel.readOnly === accessLevel && 'Read-only'}
{
}
>
- {UserPermissionsLevel.none !== accessLevel ? (
- ) : "You don't have access to this project's anatomy. We should redirect you somewhere else."}
+ {userPermissions.canViewAnatomy() ? (
+
+ ) : (
+
+ )}
)
diff --git a/src/pages/ProjectManagerPage/ProjectManagerPage.jsx b/src/pages/ProjectManagerPage/ProjectManagerPage.jsx
index 5318508c8..352e4a645 100644
--- a/src/pages/ProjectManagerPage/ProjectManagerPage.jsx
+++ b/src/pages/ProjectManagerPage/ProjectManagerPage.jsx
@@ -150,8 +150,8 @@ const ProjectManagerPage = () => {
onDeleteProject={handleDeleteProject}
onActivateProject={handleActivateProject}
>
- {module === 'anatomy' && userPermissions.canViewAnatomy() && }
- {module === 'projectSettings' && userPermissions.canViewSettings() && }
+ {module === 'anatomy' && }
+ {module === 'projectSettings' && }
{module === 'siteSettings' && }
{module === 'roots' && }
{module === 'teams' && }
From dac1a862609db3714a3907bee335fb49ce93b993 Mon Sep 17 00:00:00 2001
From: Florin Tudor
Date: Fri, 25 Oct 2024 00:08:08 +0200
Subject: [PATCH 10/35] wip
---
src/hooks/useUserProjectPermissions.ts | 75 ++++++++++++-------
.../ProjectManagerPage/ProjectManagerPage.jsx | 15 +++-
2 files changed, 61 insertions(+), 29 deletions(-)
diff --git a/src/hooks/useUserProjectPermissions.ts b/src/hooks/useUserProjectPermissions.ts
index 5e8c37c30..4febaa3eb 100644
--- a/src/hooks/useUserProjectPermissions.ts
+++ b/src/hooks/useUserProjectPermissions.ts
@@ -1,10 +1,16 @@
import { GetCurrentUserPermissionsApiResponse } from '@api/rest/permissions'
import { useGetCurrentUserPermissionsQuery, useGetCurrentUserProjectPermissionsQuery } from '@queries/permissions/getPermissions'
-const UserPermissionsLevel = {
- none: 0,
- readOnly: 1,
- readWrite: 2,
+enum UserPermissionsLevel {
+ none = 0,
+ readOnly = 1,
+ readWrite = 2,
+}
+
+enum UserPermissionsEntity {
+ users = 'users',
+ anatomy = 'anatomy',
+ settings = 'settings',
}
class UserPermissions {
@@ -18,48 +24,63 @@ class UserPermissions {
return this.projectSettingsAreEnabled() && this.permissions?.project?.create || false
}
- getSettingsPermissionLevel(): typeof UserPermissionsLevel {
- return this.permissions?.project?.settings || UserPermissionsLevel.readWrite
+ getPermissionLevel(type: UserPermissionsEntity): UserPermissionsLevel {
+ return this.permissions?.project?.[type]|| UserPermissionsLevel.readWrite
}
- getAnatomyPermissionLevel(): typeof UserPermissionsLevel {
- return this.permissions?.project?.anatomy || UserPermissionsLevel.readWrite
- }
-
- canEditSettings(): boolean {
+ canEdit(type: UserPermissionsEntity): boolean {
if (!this.projectSettingsAreEnabled()) {
return true
}
- return this.permissions?.project?.settings === UserPermissionsLevel.readWrite
+ return this.permissions?.project?.[type] === UserPermissionsLevel.readWrite
}
- canEditAnatomy(): boolean {
+ canView(type: UserPermissionsEntity): boolean {
if (!this.projectSettingsAreEnabled()) {
return true
}
- return this.permissions?.project?.anatomy === UserPermissionsLevel.readWrite
+ return (
+ this.canEdit(type) || this.permissions?.project?.[type]=== UserPermissionsLevel.readOnly
+ )
+
}
- canViewSettings(): boolean {
- if (!this.projectSettingsAreEnabled()) {
- return true
- }
+ getSettingsPermissionLevel(): UserPermissionsLevel {
+ return this.getPermissionLevel(UserPermissionsEntity.settings)
+ }
- return (
- this.canEditSettings() || this.permissions?.project?.settings === UserPermissionsLevel.readOnly
- )
+ getAnatomyPermissionLevel(): UserPermissionsLevel {
+ return this.getPermissionLevel(UserPermissionsEntity.anatomy)
+ }
+
+ getUsersPermissionLevel(): UserPermissionsLevel {
+ return this.getPermissionLevel(UserPermissionsEntity.users)
+ }
+
+ canEditSettings(): boolean {
+ return this.canEdit(UserPermissionsEntity.settings)
+ }
+
+ canEditAnatomy(): boolean {
+ return this.canEdit(UserPermissionsEntity.anatomy)
+ }
+
+ canEditUsers(): boolean {
+ return this.canEdit(UserPermissionsEntity.users)
+ }
+
+ canViewSettings(): boolean {
+ return this.canView(UserPermissionsEntity.settings)
}
canViewAnatomy(): boolean {
- if (!this.projectSettingsAreEnabled()) {
- return true
- }
+ return this.canView(UserPermissionsEntity.anatomy)
+ }
- return (
- this.canEditAnatomy() || this.permissions?.project?.anatomy === UserPermissionsLevel.readOnly
- )
+ canViewUsers(): boolean {
+ return this.canView(UserPermissionsEntity.users)
}
projectSettingsAreEnabled(): boolean {
diff --git a/src/pages/ProjectManagerPage/ProjectManagerPage.jsx b/src/pages/ProjectManagerPage/ProjectManagerPage.jsx
index 352e4a645..b06c5f45a 100644
--- a/src/pages/ProjectManagerPage/ProjectManagerPage.jsx
+++ b/src/pages/ProjectManagerPage/ProjectManagerPage.jsx
@@ -17,6 +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'
const ProjectSettings = ({ projectList, projectManager, projectName }) => {
return (
@@ -86,7 +87,7 @@ const ProjectManagerPage = () => {
const links = []
if (userPermissions.projectSettingsAreEnabled()) {
- if (userPermissions.canViewAnatomy()) {
+ if (userPermissions.canViewAnatomy() || module === 'anatomy') {
links.push({
name: 'Anatomy',
path: '/manageProjects/anatomy',
@@ -96,7 +97,7 @@ const ProjectManagerPage = () => {
})
}
- if (userPermissions.canViewSettings()) {
+ if (userPermissions.canViewSettings() || module === 'projectSettings') {
links.push({
name: 'Project settings',
path: '/manageProjects/projectSettings',
@@ -105,6 +106,15 @@ const ProjectManagerPage = () => {
shortcut: 'P+P',
})
}
+ if (userPermissions.canViewSettings() || module === 'userSettings') {
+ links.push({
+ name: 'Project Users',
+ path: '/manageProjects/userSettings',
+ module: 'userSettings',
+ accessLevels: [],
+ shortcut: 'P+U',
+ })
+ }
}
links.push(
@@ -153,6 +163,7 @@ const ProjectManagerPage = () => {
{module === 'anatomy' && }
{module === 'projectSettings' && }
{module === 'siteSettings' && }
+ {module === 'userSettings' && }
{module === 'roots' && }
{module === 'teams' && }
From 251a48ffa969b5eafe60cab6c5261fda068e0511 Mon Sep 17 00:00:00 2001
From: Florin Tudor
Date: Tue, 29 Oct 2024 10:16:41 +0100
Subject: [PATCH 11/35] wip
---
src/api/rest/permissions.ts | 2 +-
.../ProjectManagerPage/Users/ProjectList.tsx | 121 +++++++++++++++
.../ProjectManagerPage/Users/ProjectUsers.tsx | 146 ++++++++++++++++++
.../ProjectManagerPage/Users/UserList.tsx | 115 ++++++++++++++
4 files changed, 383 insertions(+), 1 deletion(-)
create mode 100644 src/pages/ProjectManagerPage/Users/ProjectList.tsx
create mode 100644 src/pages/ProjectManagerPage/Users/ProjectUsers.tsx
create mode 100644 src/pages/ProjectManagerPage/Users/UserList.tsx
diff --git a/src/api/rest/permissions.ts b/src/api/rest/permissions.ts
index bee3406f7..87b9e156e 100644
--- a/src/api/rest/permissions.ts
+++ b/src/api/rest/permissions.ts
@@ -5,7 +5,7 @@ const injectedRtkApi = api.injectEndpoints({
GetCurrentUserPermissionsApiResponse,
GetCurrentUserPermissionsApiArg
>({
- query: () => ({ url: `/api/users/me/perimissions` }),
+ query: () => ({ url: `/api/users/me/permissions` }),
}),
getCurrentUserProjectPermissions: build.query<
GetCurrentUserProjectPermissionsApiResponse,
diff --git a/src/pages/ProjectManagerPage/Users/ProjectList.tsx b/src/pages/ProjectManagerPage/Users/ProjectList.tsx
new file mode 100644
index 000000000..feb6c33fa
--- /dev/null
+++ b/src/pages/ProjectManagerPage/Users/ProjectList.tsx
@@ -0,0 +1,121 @@
+import { CSSProperties, useRef } from 'react'
+import { TablePanel, Section } from '@ynput/ayon-react-components'
+
+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'
+import { $Any } from '@types'
+import { CheckboxWidget } from '@containers/SettingsEditor/Widgets/CheckboxWidget'
+
+const formatName = (rowData: $Any, field: string) => {
+ return rowData[field]
+}
+
+const StyledProjectName = styled.div`
+ /* use grid to stack items on top of each other */
+ display: grid;
+ grid-template-columns: 1fr;
+
+ span {
+ grid-area: 1 / 1 / 2 / 2;
+ transition: opacity 0.15s;
+ }
+
+ /* when open hide the code */
+ span:last-child {
+ opacity: 0;
+ }
+
+ &:not(.isActive) {
+ font-style: italic;
+ color: var(--md-ref-palette-secondary50);
+ }
+
+ &:not(.isOpen) {
+ span:first-child {
+ opacity: 0;
+ }
+ span:last-child {
+ opacity: 1;
+ }
+ }
+`
+
+type Props = {
+ style: CSSProperties
+ className: string
+ selection: string[]
+ onSelectionChange: () => {}
+}
+
+const ProjectList = ({ selection, style, className, onSelectionChange }: Props) => {
+
+ const { data: projects = [], isLoading, isError, error } = useListProjectsQuery({})
+ if (isError) {
+ console.error(error)
+ }
+
+ const projectListWithPinned = projects
+ const projectList = projectListWithPinned
+ const tableData = useTableLoadingData(projectList, isLoading, 10, 'name')
+
+ return (
+
+
+ ({ loading: isLoading })}
+ onSelectionChange={() => {
+ console.log('change??')
+ return onSelectionChange
+ }}
+ >
+ (
+
+ {formatName(rowData, 'name')}
+
+ )}
+ style={{ minWidth: 150, ...style }}
+ />
+ (
+
+ {formatName(rowData, 'code')}
+
+ )}
+ style={{ minWidth: 150, ...style }}
+ />
+ (
+
+ {}}
+ />
+
+ )}
+ style={{ minWidth: 150, ...style }}
+ />
+
+
+
+ )
+}
+
+export default ProjectList
diff --git a/src/pages/ProjectManagerPage/Users/ProjectUsers.tsx b/src/pages/ProjectManagerPage/Users/ProjectUsers.tsx
new file mode 100644
index 000000000..486446bfc
--- /dev/null
+++ b/src/pages/ProjectManagerPage/Users/ProjectUsers.tsx
@@ -0,0 +1,146 @@
+import { UserNode } from '@api/graphql'
+import UserAccessGroups from '@pages/SettingsPage/UsersSettings/UserAccessGroupsForm/UserAccessGroups/UserAccessGroups'
+import { useGetUsersQuery } from '@queries/user/getUsers'
+import { $Any } from '@types'
+import { Button, SaveButton, Section, 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 './UserList'
+import SearchFilter from '@components/SearchFilter/SearchFilter'
+import { Filter } from '@components/SearchFilter/types'
+import UserAccessGroupsProjects from '@pages/SettingsPage/UsersSettings/UserAccessGroupsForm/UserAccessGroupsProjects/UserAccessGroupsProjects'
+import { getProjectsListForSelection } from '@pages/SettingsPage/UsersSettings/UserAccessGroupsForm/UserAccessGroupsHelpers'
+import { useListProjectsQuery } from '@queries/project/getProject'
+import { useGetAccessGroupsQuery } from '@queries/accessGroups/getAccessGroups'
+
+type Props = {}
+
+const ProjectUsers = ({}: Props) => {
+ const selfName = useSelector((state: $Any) => state.user.name)
+ let { data: userList = [], isLoading } = useGetUsersQuery({ selfName })
+
+ const { data: projectsList = [] } = useListProjectsQuery({})
+
+ // console.log({userList})
+ const filteredUsers = userList.filter(
+ (user: UserNode) => !user.isAdmin && !user.isManager && user.active,
+ )
+ // console.log({filteredUsers})
+
+ const accessGroups = {
+ artist: ['project_a', 'project_b', 'project_c'],
+ freelancer: [],
+ supervisor: [],
+ }
+
+ // Load user list
+ const { data: accessGroupList = [] } = useGetAccessGroupsQuery({
+ projectName: '_',
+ })
+
+ const { allProjects, activeProjects } = getProjectsListForSelection(
+ [],
+ accessGroups,
+ )
+
+ const [localActiveProjects, setLocalActiveProjects] = useState<$Any[]>(activeProjects)
+
+ const [selectedUsers, setSelectedUsers] = useState([])
+ const [selectedAccessGroups, setSelectedAccessGroups] = useState<$Any>([])
+
+ // keeps track of the filters whilst adding/removing filters
+ const [filters, setFilters] = useState([
+ { id: 'user_filter', type: 'string', label: 'user' },
+ { id: 'project_filter', type: 'string', label: 'project' },
+ ])
+
+ const onFiltersChange = (changes: $Any) => {
+ console.log('on change? ', changes)
+ }
+
+ const onFiltersFinish = (changes: $Any) => {
+ console.log('on filters finish: ', changes)
+ }
+
+ return (
+
+
+ onFiltersChange(v)}
+ onFinish={(v) => onFiltersFinish(v)} // when changes are applied
+ options={[]}
+ />
+
+
+
+
+
+
+
+ setSelectedUsers(selection)}
+ />
+
+
+
+
+ {
+ console.log('ag selection: ', selection)
+ return setSelectedAccessGroups(selection)
+ }}
+ disableNewGroup={false}
+ />
+
+
+ {/* @ts-ignore */}
+ {/*}
+ {
+ console.log('selection: ', selection)
+ return setSelectedProjects(selection)
+ }}
+ />
+ {*/}
+
+ {
+ console.log('changes...', p, clearAll)
+ setLocalActiveProjects(p)
+ }}
+ isDisabled={!selectedAccessGroups.length}
+ />
+
+
+
+
+ )
+}
+export default ProjectUsers
diff --git a/src/pages/ProjectManagerPage/Users/UserList.tsx b/src/pages/ProjectManagerPage/Users/UserList.tsx
new file mode 100644
index 000000000..a543d36c4
--- /dev/null
+++ b/src/pages/ProjectManagerPage/Users/UserList.tsx
@@ -0,0 +1,115 @@
+
+import { DataTable } from 'primereact/datatable'
+import { Column } from 'primereact/column'
+import { TablePanel, Section } from '@ynput/ayon-react-components'
+import UserImage from '@components/UserImage'
+
+import { useMemo } from 'react'
+import styled from 'styled-components'
+import clsx from 'clsx'
+import useTableLoadingData from '@hooks/useTableLoadingData'
+import { accessGroupsSortFunction } from '@helpers/user'
+import { $Any } from '@types'
+import { UserModel } from '@api/rest/auth'
+
+const StyledProfileRow = styled.div`
+ display: flex;
+ align-items: center;
+ gap: var(--base-gap-large);
+`
+export const ProfileRow = ({ rowData }: $Any) => {
+ const { name, self, isMissing } = rowData
+ return (
+
+ {/* @ts-ignore */}
+
+
+ {name}
+
+
+ )
+}
+
+type Props = {
+ selectedProjects: string[],
+ selectedUsers: string[],
+ userList: UserModel[],
+ tableList: $Any,
+ isLoading: boolean,
+ onSelectUsers: (selectedUsers: string[]) => void,
+}
+
+const ProjectUserList = ({
+ selectedProjects,
+ selectedUsers,
+ userList,
+ tableList,
+ isLoading,
+ onSelectUsers,
+}: Props) => {
+ // Selection
+ const selection = useMemo(() => {
+ return userList.filter((user: UserModel) => selectedUsers.includes(user.name))
+ }, [selectedUsers, selectedProjects, userList])
+
+ const onSelectionChange = (e: $Any) => {
+ if (!onSelectUsers) return
+ let result = []
+ for (const user of e.value) result.push(user.name)
+ onSelectUsers(result)
+ }
+
+ const tableData = useTableLoadingData(tableList, isLoading, 40, 'name')
+
+ // Render
+ return (
+
+
+ clsx({ inactive: !rowData.active, loading: isLoading })}
+ onSelectionChange={onSelectionChange}
+ selection={selection}
+ columnResizeMode="expand"
+ resizableColumns
+ // @ts-ignore
+ responsive="true"
+ stateKey="users-datatable"
+ stateStorage={'local'}
+ reorderableColumns
+ >
+ !isLoading && }
+ sortable
+ resizeable
+ />
+
+
+
+
+ )
+}
+
+export default ProjectUserList
From 035312a5b736d37c5fe757ee1b1a71c583332143 Mon Sep 17 00:00:00 2001
From: Florin Tudor
Date: Tue, 5 Nov 2024 08:29:08 +0100
Subject: [PATCH 12/35] feature(Users): Updated Project users access groups
page
---
.../Users/AssignAccessGroupsDialog.styled.js | 49 +++++
.../Users/AssignAccessGroupsDialog.tsx | 67 +++++++
.../ProjectManagerPage/Users/ProjectList.tsx | 94 ++++------
.../{UserList.tsx => ProjectUserList.tsx} | 29 ++-
.../ProjectManagerPage/Users/ProjectUsers.tsx | 169 ++++++++----------
5 files changed, 238 insertions(+), 170 deletions(-)
create mode 100644 src/pages/ProjectManagerPage/Users/AssignAccessGroupsDialog.styled.js
create mode 100644 src/pages/ProjectManagerPage/Users/AssignAccessGroupsDialog.tsx
rename src/pages/ProjectManagerPage/Users/{UserList.tsx => ProjectUserList.tsx} (82%)
diff --git a/src/pages/ProjectManagerPage/Users/AssignAccessGroupsDialog.styled.js b/src/pages/ProjectManagerPage/Users/AssignAccessGroupsDialog.styled.js
new file mode 100644
index 000000000..53302a80e
--- /dev/null
+++ b/src/pages/ProjectManagerPage/Users/AssignAccessGroupsDialog.styled.js
@@ -0,0 +1,49 @@
+import { Button as BaseButton} from '@ynput/ayon-react-components'
+import styled from 'styled-components'
+
+export const ProjectItem = styled.div`
+ display: flex;
+ justify-content: space-between;
+ padding: 4px 8px;
+ align-items: center;
+ gap: var(--base-gap-large);
+ align-self: stretch;
+ border-radius: var(--border-radius-m);
+ cursor: pointer;
+ min-height: 28px;
+ overflow: hidden;
+ user-select: none;
+
+ .icon {
+ opacity: 0;
+ user-select: none;
+ }
+
+ &:hover {
+ background-color: var(--md-sys-color-surface-container-highest-hover);
+ .icon {
+ opacity: 1;
+ }
+ }
+
+ &.selected {
+ background-color: var(--md-sys-color-primary-container);
+ .icon {
+ opacity: 1;
+ }
+
+ &:hover {
+ background-color: var(--md-sys-color-primary-container-hover);
+ }
+ }
+`
+
+export const List = styled.div`
+ display: flex;
+ flex-direction: column;
+ gap: var(--base-gap-small);
+ overflow: auto;
+`
+
+export const Button = styled(BaseButton)`
+`
\ No newline at end of file
diff --git a/src/pages/ProjectManagerPage/Users/AssignAccessGroupsDialog.tsx b/src/pages/ProjectManagerPage/Users/AssignAccessGroupsDialog.tsx
new file mode 100644
index 000000000..bab2fd64d
--- /dev/null
+++ b/src/pages/ProjectManagerPage/Users/AssignAccessGroupsDialog.tsx
@@ -0,0 +1,67 @@
+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'
+
+type Props = {
+ accessGroups: $Any[]
+ onSave: (items: AccessGroupItem[]) => void
+ onClose: () => void
+}
+type AccessGroupItem = {
+ name: string
+ selected: boolean
+}
+
+const AssignAccessGroupsDialog = ({ accessGroups, onSave, onClose }: Props) => {
+ const [accessGroupItems, setAccessGroupItems] = useState(accessGroups)
+
+ const toggleAccessGroup = (accessGroup: AccessGroupItem) => {
+ setAccessGroupItems((prev: AccessGroupItem[]) => {
+ const idx = prev.findIndex((item) => item.name === accessGroup.name)
+ return [...prev.slice(0, idx), accessGroup, ...prev.slice(idx + 1)]
+ })
+ }
+ const handleClose = () => {
+ onClose()
+ }
+
+ const handleSave = () => {
+ onSave(accessGroupItems)
+ onClose()
+ }
+
+ return (
+ handleSave()} />}
+ isOpen={true}
+ onClose={handleClose}
+ >
+
+
+ {accessGroupItems.map(({ name, selected }) => (
+ {
+ toggleAccessGroup({ name, selected: !selected })
+ }}
+ >
+ {name}
+
+
+ ))}
+
+
+
+ )
+}
+
+export default AssignAccessGroupsDialog
diff --git a/src/pages/ProjectManagerPage/Users/ProjectList.tsx b/src/pages/ProjectManagerPage/Users/ProjectList.tsx
index feb6c33fa..c18bbea8e 100644
--- a/src/pages/ProjectManagerPage/Users/ProjectList.tsx
+++ b/src/pages/ProjectManagerPage/Users/ProjectList.tsx
@@ -1,6 +1,3 @@
-import { CSSProperties, useRef } from 'react'
-import { TablePanel, Section } from '@ynput/ayon-react-components'
-
import { DataTable } from 'primereact/datatable'
import { Column } from 'primereact/column'
import { useListProjectsQuery } from '@queries/project/getProject'
@@ -8,7 +5,8 @@ import styled from 'styled-components'
import clsx from 'clsx'
import useTableLoadingData from '@hooks/useTableLoadingData'
import { $Any } from '@types'
-import { CheckboxWidget } from '@containers/SettingsEditor/Widgets/CheckboxWidget'
+import { useRef } from 'react'
+import { TablePanel } from '@ynput/ayon-react-components'
const formatName = (rowData: $Any, field: string) => {
return rowData[field]
@@ -45,76 +43,52 @@ const StyledProjectName = styled.div`
`
type Props = {
- style: CSSProperties
className: string
selection: string[]
- onSelectionChange: () => {}
+ onSelectionChange: (selection: $Any) => {}
}
-const ProjectList = ({ selection, style, className, onSelectionChange }: Props) => {
-
+const ProjectList = ({ selection, onSelectionChange }: Props) => {
+ const tableRef = useRef(null)
const { data: projects = [], isLoading, isError, error } = useListProjectsQuery({})
if (isError) {
console.error(error)
}
- const projectListWithPinned = projects
- const projectList = projectListWithPinned
+ const projectList = projects
const tableData = useTableLoadingData(projectList, isLoading, 10, 'name')
+ console.log('td: ', tableData)
+ console.log('sel: ', selection)
return (
-
- ({ loading: isLoading })}
- onSelectionChange={() => {
- console.log('change??')
- return onSelectionChange
- }}
- >
- (
-
- {formatName(rowData, 'name')}
-
- )}
- style={{ minWidth: 150, ...style }}
- />
- (
-
- {formatName(rowData, 'code')}
-
- )}
- style={{ minWidth: 150, ...style }}
- />
- (
-
- {}}
- />
-
- )}
- style={{ minWidth: 150, ...style }}
- />
-
+
+ ({ loading: isLoading })}
+ onSelectionChange={(selection) => {
+ console.log(selection)
+ return onSelectionChange(selection.value)
+ }}
+ >
+ (
+
+ {formatName(rowData, 'name')}
+
+ )}
+ style={{ minWidth: 150 }}
+ />
+
-
)
}
diff --git a/src/pages/ProjectManagerPage/Users/UserList.tsx b/src/pages/ProjectManagerPage/Users/ProjectUserList.tsx
similarity index 82%
rename from src/pages/ProjectManagerPage/Users/UserList.tsx
rename to src/pages/ProjectManagerPage/Users/ProjectUserList.tsx
index a543d36c4..dd72dbbc2 100644
--- a/src/pages/ProjectManagerPage/Users/UserList.tsx
+++ b/src/pages/ProjectManagerPage/Users/ProjectUserList.tsx
@@ -46,29 +46,33 @@ export const ProfileRow = ({ rowData }: $Any) => {
}
type Props = {
- selectedProjects: string[],
selectedUsers: string[],
userList: UserModel[],
tableList: $Any,
- isLoading: boolean,
- onSelectUsers: (selectedUsers: string[]) => void,
+ isLoading: boolean
+ header?: string
+ sortable?: boolean
+ onSelectUsers?: (selectedUsers: string[]) => void
}
const ProjectUserList = ({
- selectedProjects,
selectedUsers,
userList,
tableList,
isLoading,
+ header,
+ sortable = false,
onSelectUsers,
}: Props) => {
// Selection
const selection = useMemo(() => {
return userList.filter((user: UserModel) => selectedUsers.includes(user.name))
- }, [selectedUsers, selectedProjects, userList])
+ }, [selectedUsers, userList])
const onSelectionChange = (e: $Any) => {
- if (!onSelectUsers) return
+ if (!onSelectUsers) {
+ return
+ }
let result = []
for (const user of e.value) result.push(user.name)
onSelectUsers(result)
@@ -90,22 +94,15 @@ const ProjectUserList = ({
rowClassName={(rowData: $Any) => clsx({ inactive: !rowData.active, loading: isLoading })}
onSelectionChange={onSelectionChange}
selection={selection}
- columnResizeMode="expand"
- resizableColumns
- // @ts-ignore
- responsive="true"
- stateKey="users-datatable"
stateStorage={'local'}
- reorderableColumns
>
!isLoading && }
- sortable
- resizeable
+ sortable={sortable}
/>
-
diff --git a/src/pages/ProjectManagerPage/Users/ProjectUsers.tsx b/src/pages/ProjectManagerPage/Users/ProjectUsers.tsx
index 486446bfc..f4bd120a8 100644
--- a/src/pages/ProjectManagerPage/Users/ProjectUsers.tsx
+++ b/src/pages/ProjectManagerPage/Users/ProjectUsers.tsx
@@ -1,18 +1,16 @@
import { UserNode } from '@api/graphql'
-import UserAccessGroups from '@pages/SettingsPage/UsersSettings/UserAccessGroupsForm/UserAccessGroups/UserAccessGroups'
import { useGetUsersQuery } from '@queries/user/getUsers'
import { $Any } from '@types'
-import { Button, SaveButton, Section, Spacer, Toolbar } from '@ynput/ayon-react-components'
+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 './UserList'
+import ProjectUserList from './ProjectUserList'
import SearchFilter from '@components/SearchFilter/SearchFilter'
-import { Filter } from '@components/SearchFilter/types'
-import UserAccessGroupsProjects from '@pages/SettingsPage/UsersSettings/UserAccessGroupsForm/UserAccessGroupsProjects/UserAccessGroupsProjects'
-import { getProjectsListForSelection } from '@pages/SettingsPage/UsersSettings/UserAccessGroupsForm/UserAccessGroupsHelpers'
import { useListProjectsQuery } from '@queries/project/getProject'
import { useGetAccessGroupsQuery } from '@queries/accessGroups/getAccessGroups'
+import ProjectList from '@containers/projectList'
+import AssignAccessGroupsDialog from './AssignAccessGroupsDialog'
type Props = {}
@@ -20,126 +18,109 @@ const ProjectUsers = ({}: Props) => {
const selfName = useSelector((state: $Any) => state.user.name)
let { data: userList = [], isLoading } = useGetUsersQuery({ selfName })
- const { data: projectsList = [] } = useListProjectsQuery({})
-
- // console.log({userList})
- const filteredUsers = userList.filter(
- (user: UserNode) => !user.isAdmin && !user.isManager && user.active,
- )
- // console.log({filteredUsers})
-
- const accessGroups = {
- artist: ['project_a', 'project_b', 'project_c'],
- freelancer: [],
- supervisor: [],
- }
-
- // Load user list
const { data: accessGroupList = [] } = useGetAccessGroupsQuery({
projectName: '_',
})
+ console.log(accessGroupList)
- const { allProjects, activeProjects } = getProjectsListForSelection(
- [],
- accessGroups,
- )
+ const [selectedProjects, setSelectedProjects] = useState([])
+ const [selectedUsers, setSelectedUsers] = useState([])
+ const [showDialog, setShowDialog] = useState(false)
- const [localActiveProjects, setLocalActiveProjects] = useState<$Any[]>(activeProjects)
+ const onSelectProjects = (selection: string[]) => {
+ setSelectedProjects(selection)
+ }
- const [selectedUsers, setSelectedUsers] = useState([])
- const [selectedAccessGroups, setSelectedAccessGroups] = useState<$Any>([])
+ const actionEnabled = selectedProjects.length > 0 && selectedUsers.length > 0
- // keeps track of the filters whilst adding/removing filters
- const [filters, setFilters] = useState([
- { id: 'user_filter', type: 'string', label: 'user' },
- { id: 'project_filter', type: 'string', label: 'project' },
- ])
+ const filteredUsers = userList.filter(
+ (user: UserNode) => !user.isAdmin && !user.isManager && user.active,
+ )
const onFiltersChange = (changes: $Any) => {
- console.log('on change? ', changes)
}
const onFiltersFinish = (changes: $Any) => {
- console.log('on filters finish: ', changes)
}
return (
onFiltersChange(v)}
onFinish={(v) => onFiltersFinish(v)} // when changes are applied
options={[]}
/>
setShowDialog(true)}
/>
setShowDialog(true)}
/>
-
-
-
- setSelectedUsers(selection)}
- />
-
+
+
+
+ {/* @ts-ignore */}
+
-
-
- {
- console.log('ag selection: ', selection)
- return setSelectedAccessGroups(selection)
- }}
- disableNewGroup={false}
- />
-
-
- {/* @ts-ignore */}
- {/*}
- {
- console.log('selection: ', selection)
- return setSelectedProjects(selection)
- }}
- />
- {*/}
- {
- console.log('changes...', p, clearAll)
- setLocalActiveProjects(p)
- }}
- isDisabled={!selectedAccessGroups.length}
- />
-
+
+ setSelectedUsers(selection)}
+ sortable
+ />
+
+
+
+
+ {accessGroupList.map((accessGroup) => {
+ return (
+
+
+
+ )
+ })}
+
+
+ {showDialog && (
+ ({ ...item, selected: false }))}
+ onSave={() => {}}
+ onClose={function (): void {
+ setShowDialog(false)
+ }}
+ />
+ )}
)
}
From 03252b1203ef752f55f2a4a58aaad60d6aba0450 Mon Sep 17 00:00:00 2001
From: Florin Tudor
Date: Tue, 5 Nov 2024 16:01:04 +0100
Subject: [PATCH 13/35] wip - mutations added
---
src/api/rest/project.ts | 47 ++++++++++----
.../ProjectManagerPage/Users/ProjectUsers.tsx | 65 +++++++++++++++++--
src/services/project/updateProject.js | 8 +++
3 files changed, 101 insertions(+), 19 deletions(-)
diff --git a/src/api/rest/project.ts b/src/api/rest/project.ts
index c76813671..03541a722 100644
--- a/src/api/rest/project.ts
+++ b/src/api/rest/project.ts
@@ -12,6 +12,14 @@ const injectedRtkApi = api.injectEndpoints({
method: 'HEAD',
}),
}),
+ getProjectFilePayload: build.query<
+ GetProjectFilePayloadApiResponse,
+ GetProjectFilePayloadApiArg
+ >({
+ query: (queryArg) => ({
+ url: `/api/projects/${queryArg.projectName}/files/${queryArg.fileId}/payload`,
+ }),
+ }),
getProjectFileThumbnail: build.query<
GetProjectFileThumbnailApiResponse,
GetProjectFileThumbnailApiArg
@@ -32,12 +40,10 @@ const injectedRtkApi = api.injectEndpoints({
getProjectActivity: build.query({
query: (queryArg) => ({
url: `/api/projects/${queryArg.projectName}/dashboard/activity`,
- params: {
- days: queryArg.days,
- },
+ params: { days: queryArg.days },
}),
}),
- getProjectUsers: build.query({
+ getProjectTeams: build.query({
query: (queryArg) => ({ url: `/api/projects/${queryArg.projectName}/dashboard/users` }),
}),
getProjectAnatomy: build.query({
@@ -72,14 +78,13 @@ const injectedRtkApi = api.injectEndpoints({
getProjectSiteRoots: build.query({
query: (queryArg) => ({
url: `/api/projects/${queryArg.projectName}/siteRoots`,
- headers: {
- 'x-ayon-site-id': queryArg['x-ayon-site-id'],
- },
- params: {
- platform: queryArg.platform,
- },
+ headers: { 'x-ayon-site-id': queryArg['x-ayon-site-id'] },
+ params: { platform: queryArg.platform },
}),
}),
+ getProjectUsers: build.query({
+ query: (queryArg) => ({ url: `/api/projects/${queryArg.projectName}/users` }),
+ }),
getProjectEntityUris: build.mutation<
GetProjectEntityUrisApiResponse,
GetProjectEntityUrisApiArg
@@ -104,6 +109,11 @@ export type GetProjectFileHeadApiArg = {
fileId: string
projectName: string
}
+export type GetProjectFilePayloadApiResponse = /** status 200 Successful Response */ any
+export type GetProjectFilePayloadApiArg = {
+ fileId: string
+ projectName: string
+}
export type GetProjectFileThumbnailApiResponse = /** status 200 Successful Response */ any
export type GetProjectFileThumbnailApiArg = {
fileId: string
@@ -124,8 +134,9 @@ export type GetProjectActivityApiArg = {
/** Number of days to retrieve activity for */
days?: number
}
-export type GetProjectUsersApiResponse = /** status 200 Successful Response */ UsersResponseModel
-export type GetProjectUsersApiArg = {
+export type GetProjectTeamsApiResponse =
+ /** status 200 Successful Response */ ProjectTeamsResponseModel
+export type GetProjectTeamsApiArg = {
projectName: string
}
export type GetProjectAnatomyApiResponse = /** status 200 Successful Response */ ProjectAnatomy
@@ -173,6 +184,12 @@ export type GetProjectSiteRootsApiArg = {
/** Site ID may be specified either as a query parameter (`site_id` or `site`) or in a header. */
'x-ayon-site-id'?: string
}
+export type GetProjectUsersApiResponse = /** status 200 Successful Response */ {
+ [key: string]: string[]
+}
+export type GetProjectUsersApiArg = {
+ projectName: string
+}
export type GetProjectEntityUrisApiResponse = /** status 200 Successful Response */ GetUrisResponse
export type GetProjectEntityUrisApiArg = {
projectName: string
@@ -238,7 +255,7 @@ export type ActivityResponseModel = {
/** Activity per day normalized to 0-100 */
activity: number[]
}
-export type UsersResponseModel = {
+export type ProjectTeamsResponseModel = {
/** Number of active team members */
teamSizeActive?: number
/** Total number of team members */
@@ -299,6 +316,7 @@ export type Templates = {
others?: CustomTemplate[]
}
export type ProjectAttribModel = {
+ priority?: 'urgent' | 'high' | 'normal' | 'low'
/** Frame rate */
fps?: number
/** Horizontal resolution */
@@ -346,6 +364,8 @@ export type Status = {
state?: 'not_started' | 'in_progress' | 'done' | 'blocked'
icon?: string
color?: string
+ /** Limit the status to specific entity types. */
+ scope?: string[]
original_name?: string
}
export type Tag = {
@@ -398,6 +418,7 @@ export type LinkTypeModel = {
data?: object
}
export type ProjectAttribModel2 = {
+ priority?: 'urgent' | 'high' | 'normal' | 'low'
/** Frame rate */
fps?: number
/** Horizontal resolution */
diff --git a/src/pages/ProjectManagerPage/Users/ProjectUsers.tsx b/src/pages/ProjectManagerPage/Users/ProjectUsers.tsx
index f4bd120a8..fa26c80bd 100644
--- a/src/pages/ProjectManagerPage/Users/ProjectUsers.tsx
+++ b/src/pages/ProjectManagerPage/Users/ProjectUsers.tsx
@@ -7,10 +7,15 @@ import { useState } from 'react'
import { useSelector } from 'react-redux'
import ProjectUserList from './ProjectUserList'
import SearchFilter from '@components/SearchFilter/SearchFilter'
+import { getProjectsListForSelection } from '@pages/SettingsPage/UsersSettings/UserAccessGroupsForm/UserAccessGroupsHelpers'
import { useListProjectsQuery } from '@queries/project/getProject'
import { useGetAccessGroupsQuery } from '@queries/accessGroups/getAccessGroups'
-import ProjectList from '@containers/projectList'
import AssignAccessGroupsDialog from './AssignAccessGroupsDialog'
+import ProjectList from '@containers/projectList'
+import {api }from '@api/rest/project'
+import { useUpdateProjectUsersMutation } from '@queries/project/updateProject'
+import { toast } from 'react-toastify'
+import { AccessGroupObject } from '@api/rest/accessGroups'
type Props = {}
@@ -18,29 +23,75 @@ 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: '_',
})
- console.log(accessGroupList)
+ console.log('ag list', accessGroupList)
+
+ 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 onSelectProjects = (selection: string[]) => {
- setSelectedProjects(selection)
+ const results = api.useGetProjectUsersQuery({ projectName: selectedProjects[0] || '_' })
+ console.log('getting users: ', results)
+
+ const onSelectProjects = (selection: string) => {
+ setSelectedProjects([selection])
}
const actionEnabled = selectedProjects.length > 0 && selectedUsers.length > 0
+ // console.log({userList})
const filteredUsers = userList.filter(
(user: UserNode) => !user.isAdmin && !user.isManager && user.active,
)
+ // console.log({filteredUsers})
+
+ // keeps track of the filters whilst adding/removing filters
+ // const [filters, setFilters] = useState([
+ // { id: 'user_filter', type: 'string', label: 'user' },
+ // { id: 'project_filter', type: 'string', label: 'project' },
+ // ])
const onFiltersChange = (changes: $Any) => {
+ console.log('on change? ', changes)
}
const onFiltersFinish = (changes: $Any) => {
+ console.log('on filters finish: ', changes)
+ }
+
+ const onSave = async (changes: $Any) => {
+ console.log('saving????')
+ console.log(changes);
+ for (const user of selectedUsers) {
+ console.log('user: ', user)
+ console.log(selectedProjects)
+ const accessGroups = changes.filter((ag: $Any) => ag.selected).map((ag: $Any) => ag.name)
+ console.log('ags: ', accessGroups)
+
+ try {
+ await updateUser({
+ projectName: selectedProjects,
+ userName: user,
+ update: accessGroups
+ }).unwrap()
+ } catch (error: $Any) {
+ console.log(error)
+ toast.error('Unable to update profile')
+ toast.error(error.details)
+ }
+ }
}
return (
@@ -57,13 +108,15 @@ const ProjectUsers = ({}: Props) => {
icon="remove"
label="Remove access"
disabled={!actionEnabled}
- onClick={() => setShowDialog(true)}
+ // onClick={handleRevert}
/>
setShowDialog(true)}
+ // active={canCommit}
+ // saving={commitUpdating}
/>
@@ -115,7 +168,7 @@ const ProjectUsers = ({}: Props) => {
{showDialog && (
({ ...item, selected: false }))}
- onSave={() => {}}
+ onSave={onSave}
onClose={function (): void {
setShowDialog(false)
}}
diff --git a/src/services/project/updateProject.js b/src/services/project/updateProject.js
index 0acef800a..cf9e67f60 100644
--- a/src/services/project/updateProject.js
+++ b/src/services/project/updateProject.js
@@ -53,6 +53,13 @@ const updateProject = api.injectEndpoints({
{ type: 'projects', id: 'LIST' },
],
}),
+ updateProjectUsers: build.mutation({
+ query: ({ projectName, userName, update }) => ({
+ url: `/api/projects/${projectName}/users/${userName}`,
+ method: 'PATCH',
+ body: update,
+ }),
+ }),
}),
overrideExisting: true,
})
@@ -62,4 +69,5 @@ export const {
useDeleteProjectMutation,
useUpdateProjectAnatomyMutation,
useUpdateProjectMutation,
+ useUpdateProjectUsersMutation
} = updateProject
From 925804f1263e4be51a850dfe9c9962a890de109e Mon Sep 17 00:00:00 2001
From: Florin Tudor
Date: Wed, 6 Nov 2024 12:58:05 +0100
Subject: [PATCH 14/35] feature(Users): Fetching & filtering users based on
project & assigned access groups
---
gen/openapi-config.ts | 2 +-
.../Users/ProjectUserList.tsx | 4 +-
.../ProjectManagerPage/Users/ProjectUsers.tsx | 38 ++++++++++--------
src/pages/ProjectManagerPage/Users/mappers.ts | 40 +++++++++++++++++++
4 files changed, 65 insertions(+), 19 deletions(-)
create mode 100644 src/pages/ProjectManagerPage/Users/mappers.ts
diff --git a/gen/openapi-config.ts b/gen/openapi-config.ts
index 11748ae22..907e263b8 100644
--- a/gen/openapi-config.ts
+++ b/gen/openapi-config.ts
@@ -7,7 +7,7 @@ const outputFiles = {
market: ['marketAddonList', 'marketAddonDetail', 'marketAddonVersionDetail'],
watchers: ['getEntityWatchers', 'setEntityWatchers'],
inbox: ['manageInboxItem'],
- project: ['getProject', 'listProjects', 'getProjectAnatomy'],
+ project: ['getProject', 'listProjects', 'getProjectAnatomy', 'getProjectUsers'],
review: [
'getReviewablesForVersion',
'getReviewablesForProduct',
diff --git a/src/pages/ProjectManagerPage/Users/ProjectUserList.tsx b/src/pages/ProjectManagerPage/Users/ProjectUserList.tsx
index dd72dbbc2..321e3f481 100644
--- a/src/pages/ProjectManagerPage/Users/ProjectUserList.tsx
+++ b/src/pages/ProjectManagerPage/Users/ProjectUserList.tsx
@@ -47,7 +47,7 @@ export const ProfileRow = ({ rowData }: $Any) => {
type Props = {
selectedUsers: string[],
- userList: UserModel[],
+ userList: string[],
tableList: $Any,
isLoading: boolean
header?: string
@@ -66,7 +66,7 @@ const ProjectUserList = ({
}: Props) => {
// Selection
const selection = useMemo(() => {
- return userList.filter((user: UserModel) => selectedUsers.includes(user.name))
+ return userList.filter((user: string) => selectedUsers.includes(user))
}, [selectedUsers, userList])
const onSelectionChange = (e: $Any) => {
diff --git a/src/pages/ProjectManagerPage/Users/ProjectUsers.tsx b/src/pages/ProjectManagerPage/Users/ProjectUsers.tsx
index fa26c80bd..fc10baae9 100644
--- a/src/pages/ProjectManagerPage/Users/ProjectUsers.tsx
+++ b/src/pages/ProjectManagerPage/Users/ProjectUsers.tsx
@@ -7,15 +7,16 @@ import { useState } from 'react'
import { useSelector } from 'react-redux'
import ProjectUserList from './ProjectUserList'
import SearchFilter from '@components/SearchFilter/SearchFilter'
-import { getProjectsListForSelection } from '@pages/SettingsPage/UsersSettings/UserAccessGroupsForm/UserAccessGroupsHelpers'
import { useListProjectsQuery } from '@queries/project/getProject'
import { useGetAccessGroupsQuery } from '@queries/accessGroups/getAccessGroups'
import AssignAccessGroupsDialog from './AssignAccessGroupsDialog'
-import ProjectList from '@containers/projectList'
import {api }from '@api/rest/project'
import { useUpdateProjectUsersMutation } from '@queries/project/updateProject'
import { toast } from 'react-toastify'
-import { AccessGroupObject } from '@api/rest/accessGroups'
+import LocalProjectList from './ProjectList'
+import ProjectList from '@containers/projectList'
+import { getAllProjectUsers, mapUsersByAccessGroups } from './mappers'
+import { access } from 'fs'
type Props = {}
@@ -42,20 +43,23 @@ const ProjectUsers = ({}: Props) => {
const [selectedUsers, setSelectedUsers] = useState([])
const [showDialog, setShowDialog] = useState(false)
- const results = api.useGetProjectUsersQuery({ projectName: selectedProjects[0] || '_' })
- console.log('getting users: ', results)
+ const result = api.useGetProjectUsersQuery({ projectName: selectedProjects[0] || '_' })
+ const mappedUsers = mapUsersByAccessGroups(result.data)
+
- const onSelectProjects = (selection: string) => {
+ const onSelectProjects = (selection: $Any) => {
+ console.log('on select projects...')
setSelectedProjects([selection])
}
const actionEnabled = selectedProjects.length > 0 && selectedUsers.length > 0
// console.log({userList})
- const filteredUsers = userList.filter(
+ const activeNonManagerUsers = userList.filter(
(user: UserNode) => !user.isAdmin && !user.isManager && user.active,
)
- // console.log({filteredUsers})
+ const allProjectUsers = getAllProjectUsers(mappedUsers)
+ const unasignedUsers = activeNonManagerUsers.filter((user: UserNode) => !allProjectUsers.includes(user.name))
// keeps track of the filters whilst adding/removing filters
// const [filters, setFilters] = useState([
@@ -127,15 +131,15 @@ const ProjectUsers = ({}: Props) => {
minSize={10}
>
{/* @ts-ignore */}
-
+
setSelectedUsers(selection)}
sortable
@@ -144,18 +148,20 @@ const ProjectUsers = ({}: Props) => {
- {accessGroupList.map((accessGroup) => {
+ {Object.keys(mappedUsers).map((accessGroup) => {
return (
+ mappedUsers[accessGroup].includes(user.name),
+ )}
isLoading={isLoading}
/>
diff --git a/src/pages/ProjectManagerPage/Users/mappers.ts b/src/pages/ProjectManagerPage/Users/mappers.ts
new file mode 100644
index 000000000..0443d17ae
--- /dev/null
+++ b/src/pages/ProjectManagerPage/Users/mappers.ts
@@ -0,0 +1,40 @@
+type ProjectUsersResponse = {
+ [key: string]: string[]
+}
+
+type AccessGroupUsers = {
+ [key: string]: string[]
+}
+
+const getAllProjectUsers = (groupedUsers: AccessGroupUsers): string[] => {
+ let allUsers: string[] = []
+ for (const [_, users] of Object.entries(groupedUsers)) {
+ allUsers.push(...users)
+ }
+
+ return [...new Set(allUsers)]
+}
+
+const mapUsersByAccessGroups = (response: ProjectUsersResponse | undefined): AccessGroupUsers => {
+ if (!response) {
+ return {}
+ }
+
+ 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] = []
+ }
+ if (groupedUsers[accessGroup].includes(user)) {
+ continue
+ }
+ groupedUsers[accessGroup].push(user)
+ }
+ }
+
+ return groupedUsers
+}
+
+export { mapUsersByAccessGroups, getAllProjectUsers }
From 3d88c4289e25b0d1907b27bcfd36470c053e5816 Mon Sep 17 00:00:00 2001
From: Florin Tudor
Date: Thu, 7 Nov 2024 06:46:48 +0100
Subject: [PATCH 15/35] feature(Users): UI redesign, implemented user panels
hover & select buttons logic
---
.../Users/ProjectUserList.tsx | 84 ++++++---------
.../ProjectManagerPage/Users/ProjectUsers.tsx | 77 ++++++-------
.../ProjectManagerPage/Users/UserRow.tsx | 102 ++++++++++++++++++
src/pages/ProjectManagerPage/Users/mappers.ts | 11 +-
src/pages/ProjectManagerPage/Users/types.ts | 12 +++
5 files changed, 186 insertions(+), 100 deletions(-)
create mode 100644 src/pages/ProjectManagerPage/Users/UserRow.tsx
create mode 100644 src/pages/ProjectManagerPage/Users/types.ts
diff --git a/src/pages/ProjectManagerPage/Users/ProjectUserList.tsx b/src/pages/ProjectManagerPage/Users/ProjectUserList.tsx
index 321e3f481..266e47fd5 100644
--- a/src/pages/ProjectManagerPage/Users/ProjectUserList.tsx
+++ b/src/pages/ProjectManagerPage/Users/ProjectUserList.tsx
@@ -2,56 +2,23 @@
import { DataTable } from 'primereact/datatable'
import { Column } from 'primereact/column'
import { TablePanel, Section } from '@ynput/ayon-react-components'
-import UserImage from '@components/UserImage'
import { useMemo } from 'react'
-import styled from 'styled-components'
import clsx from 'clsx'
import useTableLoadingData from '@hooks/useTableLoadingData'
-import { accessGroupsSortFunction } from '@helpers/user'
import { $Any } from '@types'
-import { UserModel } from '@api/rest/auth'
-
-const StyledProfileRow = styled.div`
- display: flex;
- align-items: center;
- gap: var(--base-gap-large);
-`
-export const ProfileRow = ({ rowData }: $Any) => {
- const { name, self, isMissing } = rowData
- return (
-
- {/* @ts-ignore */}
-
-
- {name}
-
-
- )
-}
+import { UserNode } from '@api/graphql'
+import UserRow from './UserRow'
type Props = {
- selectedUsers: string[],
- userList: string[],
- tableList: $Any,
+ selectedUsers: string[]
+ userList: string[]
+ tableList: $Any
isLoading: boolean
header?: string
sortable?: boolean
+ isUnassigned?: boolean
+ onContextMenu?: $Any
onSelectUsers?: (selectedUsers: string[]) => void
}
@@ -62,6 +29,8 @@ const ProjectUserList = ({
isLoading,
header,
sortable = false,
+ isUnassigned = false,
+ onContextMenu,
onSelectUsers,
}: Props) => {
// Selection
@@ -70,37 +39,46 @@ const ProjectUserList = ({
}, [selectedUsers, userList])
const onSelectionChange = (e: $Any) => {
- if (!onSelectUsers) {
- return
- }
- let result = []
- for (const user of e.value) result.push(user.name)
- onSelectUsers(result)
+ const result = e.value.map((user: UserNode) => user.name)
+
+ onSelectUsers!(result)
}
const tableData = useTableLoadingData(tableList, isLoading, 40, 'name')
-
+ const selectedUnassignedUsers = tableData.filter((user: $Any) => selectedUsers.includes(user.name))
+ const selectedUnassignedUserNames = selectedUnassignedUsers.map((user: $Any) => user.name)
// Render
return (
clsx({ inactive: !rowData.active, loading: isLoading })}
- onSelectionChange={onSelectionChange}
- selection={selection}
- stateStorage={'local'}
+ onContextMenu={onContextMenu}
+ onSelectionChange={(selection) => {
+ return onSelectUsers && onSelectionChange(selection)
+ }}
>
!isLoading && }
+ headerStyle={{ textTransform: 'capitalize' }}
+ body={(rowData) =>
+ !isLoading && (
+
+ )
+ }
sortable={sortable}
/>
diff --git a/src/pages/ProjectManagerPage/Users/ProjectUsers.tsx b/src/pages/ProjectManagerPage/Users/ProjectUsers.tsx
index fc10baae9..1459f7783 100644
--- a/src/pages/ProjectManagerPage/Users/ProjectUsers.tsx
+++ b/src/pages/ProjectManagerPage/Users/ProjectUsers.tsx
@@ -13,10 +13,9 @@ import AssignAccessGroupsDialog from './AssignAccessGroupsDialog'
import {api }from '@api/rest/project'
import { useUpdateProjectUsersMutation } from '@queries/project/updateProject'
import { toast } from 'react-toastify'
-import LocalProjectList from './ProjectList'
import ProjectList from '@containers/projectList'
import { getAllProjectUsers, mapUsersByAccessGroups } from './mappers'
-import { access } from 'fs'
+import { SelectedAccessGroupUsers } from './types'
type Props = {}
@@ -29,8 +28,6 @@ const ProjectUsers = ({}: Props) => {
const { data: accessGroupList = [] } = useGetAccessGroupsQuery({
projectName: '_',
})
- console.log('ag list', accessGroupList)
-
const [updateUser] = useUpdateProjectUsersMutation()
@@ -42,30 +39,31 @@ const ProjectUsers = ({}: Props) => {
const [selectedProjects, setSelectedProjects] = useState([])
const [selectedUsers, setSelectedUsers] = useState([])
const [showDialog, setShowDialog] = useState(false)
+ const [selectedAccessGroupUsers, setSelectedAccessGroupUsers] = useState()
- const result = api.useGetProjectUsersQuery({ projectName: selectedProjects[0] || '_' })
- const mappedUsers = mapUsersByAccessGroups(result.data)
const onSelectProjects = (selection: $Any) => {
- console.log('on select projects...')
setSelectedProjects([selection])
}
const actionEnabled = selectedProjects.length > 0 && selectedUsers.length > 0
- // console.log({userList})
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 allProjectUsers = getAllProjectUsers(mappedUsers)
const unasignedUsers = activeNonManagerUsers.filter((user: UserNode) => !allProjectUsers.includes(user.name))
- // keeps track of the filters whilst adding/removing filters
- // const [filters, setFilters] = useState([
- // { id: 'user_filter', type: 'string', label: 'user' },
- // { id: 'project_filter', type: 'string', label: 'project' },
- // ])
+ const getAccessGroupUsers = (accessGroup?: string): string[] => {
+ if (!selectedAccessGroupUsers || !accessGroup) {
+ return []
+ }
+ return selectedAccessGroupUsers.accessGroup === accessGroup ? selectedAccessGroupUsers.users : []
+ }
const onFiltersChange = (changes: $Any) => {
console.log('on change? ', changes)
@@ -75,14 +73,13 @@ const ProjectUsers = ({}: Props) => {
console.log('on filters finish: ', changes)
}
+ const updateSelectedAccessGroupUsers = (accessGroup: string, selectedUsers: string[]) => {
+ setSelectedAccessGroupUsers({ accessGroup, users: selectedUsers })
+ }
+
const onSave = async (changes: $Any) => {
- console.log('saving????')
- console.log(changes);
for (const user of selectedUsers) {
- console.log('user: ', user)
- console.log(selectedProjects)
const accessGroups = changes.filter((ag: $Any) => ag.selected).map((ag: $Any) => ag.name)
- console.log('ags: ', accessGroups)
try {
await updateUser({
@@ -92,7 +89,6 @@ const ProjectUsers = ({}: Props) => {
}).unwrap()
} catch (error: $Any) {
console.log(error)
- toast.error('Unable to update profile')
toast.error(error.details)
}
}
@@ -143,30 +139,35 @@ const ProjectUsers = ({}: Props) => {
isLoading={isLoading}
onSelectUsers={(selection: string[]) => setSelectedUsers(selection)}
sortable
+ isUnassigned
/>
- {Object.keys(mappedUsers).map((accessGroup) => {
- return (
-
-
- mappedUsers[accessGroup].includes(user.name),
- )}
- isLoading={isLoading}
- />
-
- )
- })}
+ {Object.keys(mappedUsers)
+ .map((accessGroup) => {
+ return (
+
+
+ mappedUsers[accessGroup].includes(user.name),
+ )}
+ onSelectUsers={(selection: string[]) =>
+ updateSelectedAccessGroupUsers(accessGroup, selection)
+ }
+ isLoading={isLoading}
+ />
+
+ )
+ })}
diff --git a/src/pages/ProjectManagerPage/Users/UserRow.tsx b/src/pages/ProjectManagerPage/Users/UserRow.tsx
new file mode 100644
index 000000000..ebd5e8983
--- /dev/null
+++ b/src/pages/ProjectManagerPage/Users/UserRow.tsx
@@ -0,0 +1,102 @@
+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'
+
+const StyledProfileRow = styled.div`
+ display: flex;
+ align-items: center;
+ gap: var(--base-gap-large);
+ button {
+ visibility: hidden;
+ .shortcut {
+ padding: 4px;
+ background-color: var(--md-sys-color-primary-container);
+ border-radius: var(--border-radius-m);
+ }
+ }
+ &.actionable:hover {
+ button {
+ visibility: visible;
+ }
+ }
+ &.selected {
+ button {
+ visibility: visible;
+ }
+ }
+`
+export const UserRow = ({
+ rowData,
+ selected = false,
+ isUnassigned = false,
+ showButtonsOnHover = false,
+}: $Any) => {
+ const { name, self, isMissing } = rowData
+ return (
+
+ {/* @ts-ignore */}
+
+
+ {name}
+
+ {
+ e.stopPropagation()
+ }}
+ >
+ {isUnassigned ? (
+ <>
+ {' '}
+ Add A {' '}
+ >
+ ) : (
+ 'Add more'
+ )}
+
+
+ {!isUnassigned && (
+ {
+ e.stopPropagation()
+ }}
+ >
+ {isUnassigned ? (
+ 'Remove'
+ ) : (
+ <>
+ {' '}
+ Remove R {' '}
+ >
+ )}
+
+ )}
+
+ )
+}
+
+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={[]}
/>
-
-
- setShowDialog(true)}
- // active={canCommit}
- // saving={commitUpdating}
- />
@@ -127,7 +113,10 @@ const ProjectUsers = ({}: Props) => {
userList={unassignedUsers}
tableList={unassignedUsers}
isLoading={isLoading}
- onAdd={() => { }}
+ onAdd={() => {
+ setActionedUsers(selectedUsers)
+ setShowDialog(true)
+ }}
onSelectUsers={(selection: string[]) => setSelectedUsers(selection)}
sortable
isUnassigned
@@ -136,36 +125,42 @@ const ProjectUsers = ({}: Props) => {
- {Object.keys(mappedUsers).sort().map((accessGroup) => {
- return (
-
-
- mappedUsers[accessGroup].includes(user.name),
- )}
- onSelectUsers={(selection: string[]) =>
- updateSelectedAccessGroupUsers(accessGroup, selection)
- }
- onAdd={() => {}}
- onRemove={onRemove(accessGroup)}
- isLoading={isLoading}
- />
-
- )
- })}
+ {Object.keys(mappedUsers)
+ .sort()
+ .map((accessGroup) => {
+ return (
+
+
+ mappedUsers[accessGroup].includes(user.name),
+ )}
+ onSelectUsers={(selection: string[]) =>
+ updateSelectedAccessGroupUsers(accessGroup, selection)
+ }
+ onAdd={() => {
+ setActionedUsers(getAccessGroupUsers(accessGroup))
+ setShowDialog(true)
+ }}
+ onRemove={onRemove(accessGroup)}
+ isLoading={isLoading}
+ />
+
+ )
+ })}
{showDialog && (
({ ...item, selected: false }))}
onSave={onSave}
onClose={function (): void {
diff --git a/src/pages/ProjectManagerPage/Users/hooks.ts b/src/pages/ProjectManagerPage/Users/hooks.ts
index 68ce58baf..e394697da 100644
--- a/src/pages/ProjectManagerPage/Users/hooks.ts
+++ b/src/pages/ProjectManagerPage/Users/hooks.ts
@@ -5,6 +5,20 @@ import { useUpdateProjectUsersMutation } from "@queries/project/updateProject"
import { useDispatch } from "react-redux"
const useProjectAccessGroupData = () => {
+
+ const udpateApiCache = (project: string, user: string, accessGroups: string[]) => {
+ dispatch(
+ // @ts-ignore
+ api.util.updateQueryData(
+ 'getProjectUsers',
+ { projectName: project},
+ (draft: $Any) => {
+ draft[user] = accessGroups
+ },
+ ),
+ )
+ }
+
const dispatch = useDispatch()
const [updateUser] = useUpdateProjectUsersMutation()
@@ -24,16 +38,7 @@ const useProjectAccessGroupData = () => {
userName: user,
update: updatedAccessGroups,
})
- dispatch(
- // @ts-ignore
- api.util.updateQueryData(
- 'getProjectUsers',
- { projectName: selectedProjects[0] },
- (draft: $Any) => {
- draft[user] = updatedAccessGroups
- },
- ),
- )
+ udpateApiCache(selectedProjects[0], user, updatedAccessGroups)
}
const updateUserAccessGroups = async (users: $Any, changes: $Any ): Promise => {
for (const user of users) {
@@ -41,10 +46,11 @@ const useProjectAccessGroupData = () => {
try {
await updateUser({
- projectName: selectedProjects,
+ projectName: selectedProjects[0],
userName: user,
update: accessGroups,
}).unwrap()
+ udpateApiCache(selectedProjects[0], user, accessGroups)
} catch (error: $Any) {
console.log(error)
return(error.details)
From 967ecd46c9d1bce1ee293dd72fecca754f1e2dc9 Mon Sep 17 00:00:00 2001
From: Florin Tudor
Date: Fri, 8 Nov 2024 08:42:32 +0100
Subject: [PATCH 18/35] feature(Users): Updated add/remove logic UI & payload
logic, minor UI changes
---
.../Users/AssignAccessGroupsDialog.tsx | 77 ++++++++--
.../Users/ProjectUserList.tsx | 35 +++--
.../ProjectManagerPage/Users/ProjectUsers.tsx | 132 +++++++++++++-----
.../ProjectManagerPage/Users/UserRow.tsx | 27 ++--
src/pages/ProjectManagerPage/Users/hooks.ts | 65 ++++++---
src/pages/ProjectManagerPage/Users/types.ts | 9 +-
6 files changed, 244 insertions(+), 101 deletions(-)
diff --git a/src/pages/ProjectManagerPage/Users/AssignAccessGroupsDialog.tsx b/src/pages/ProjectManagerPage/Users/AssignAccessGroupsDialog.tsx
index a23c654a6..5b4132e7c 100644
--- a/src/pages/ProjectManagerPage/Users/AssignAccessGroupsDialog.tsx
+++ b/src/pages/ProjectManagerPage/Users/AssignAccessGroupsDialog.tsx
@@ -3,26 +3,72 @@ 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 { AccessGroupUsers, SelectionStatus } from './types'
+
+const icons: {[key in SelectionStatus] : string | undefined} = {
+ [SelectionStatus.None]: 'add',
+ [SelectionStatus.Mixed]: 'remove',
+ [SelectionStatus.All]: 'check',
+}
+
+type AccessGroupItem = {
+ name: string
+ status: SelectionStatus
+}
type Props = {
accessGroups: $Any[]
users: string[]
+ userAccessGroups: AccessGroupUsers
onSave: (items: AccessGroupItem[], users: string[]) => void
onClose: () => void
}
-type AccessGroupItem = {
- name: string
- selected: boolean
-}
+const AssignAccessGroupsDialog = ({
+ accessGroups,
+ users,
+ userAccessGroups,
+ onSave,
+ onClose,
+}: Props) => {
+ const mapStates = () => {
+ 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
+ }
+
+ const initialStates = mapStates()
+ const initialStatesList = Object.keys(initialStates).map(agName => ({name: agName, status: initialStates[agName]}))
+
+ const [accessGroupItems, setAccessGroupItems] = useState(initialStatesList)
-const AssignAccessGroupsDialog = ({ accessGroups, users, onSave, onClose }: Props) => {
- const [accessGroupItems, setAccessGroupItems] = useState(accessGroups)
const toggleAccessGroup = (accessGroup: AccessGroupItem) => {
+ 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, ...prev.slice(idx + 1)]
+ return [...prev.slice(0, idx), {...accessGroup, status: newStatus}, ...prev.slice(idx + 1)]
})
}
const handleClose = () => {
@@ -30,7 +76,8 @@ const AssignAccessGroupsDialog = ({ accessGroups, users, onSave, onClose }: Prop
}
const handleSave = () => {
- onSave(accessGroupItems, users)
+ const changes = accessGroupItems.filter(item => initialStates[item.name] !== item.status)
+ onSave(changes, users)
onClose()
}
@@ -44,20 +91,20 @@ const AssignAccessGroupsDialog = ({ accessGroups, users, onSave, onClose }: Prop
>
- {accessGroupItems.map(({ name, selected }) => (
+ {accessGroupItems.map((item) => (
{
- toggleAccessGroup({ name, selected: !selected })
+ toggleAccessGroup(item)
}}
>
- {name}
-
+ {item.name}
+ {icons[item.status] !== undefined && }
))}
diff --git a/src/pages/ProjectManagerPage/Users/ProjectUserList.tsx b/src/pages/ProjectManagerPage/Users/ProjectUserList.tsx
index 5f0d9c61d..16d5a5816 100644
--- a/src/pages/ProjectManagerPage/Users/ProjectUserList.tsx
+++ b/src/pages/ProjectManagerPage/Users/ProjectUserList.tsx
@@ -3,7 +3,6 @@ import { DataTable } from 'primereact/datatable'
import { Column } from 'primereact/column'
import { TablePanel, Section } from '@ynput/ayon-react-components'
-import { useMemo } from 'react'
import clsx from 'clsx'
import useTableLoadingData from '@hooks/useTableLoadingData'
import { $Any } from '@types'
@@ -11,25 +10,27 @@ import { UserNode } from '@api/graphql'
import UserRow from './UserRow'
type Props = {
+ selectedProjects: string[]
selectedUsers: string[]
- userList: string[]
tableList: $Any
isLoading: boolean
header?: string
+ emptyMessage: string
sortable?: boolean
isUnassigned?: boolean
onContextMenu?: $Any
onSelectUsers?: (selectedUsers: string[]) => void
- onAdd: () => void
- onRemove?: (user: string) => void
+ onAdd: (user? : string) => void
+ onRemove?: (user?: string) => void
}
const ProjectUserList = ({
+ selectedProjects,
selectedUsers,
- userList,
tableList,
isLoading,
header,
+ emptyMessage,
sortable = false,
isUnassigned = false,
onAdd,
@@ -37,16 +38,23 @@ const ProjectUserList = ({
onContextMenu,
onSelectUsers,
}: Props) => {
- // Selection
- const selection = useMemo(() => {
- return userList.filter((user: string) => selectedUsers.includes(user))
- }, [selectedUsers, userList])
-
const onSelectionChange = (e: $Any) => {
const result = e.value.map((user: UserNode) => user.name)
onSelectUsers!(result)
}
+ const handleKeyDown = (event: React.KeyboardEvent) => {
+ if (selectedProjects.length === 0) {
+ return
+ }
+
+ if (event.key === 'r') {
+ onRemove && onRemove()
+ }
+ if (event.key === 'a') {
+ onAdd()
+ }
+ }
const tableData = useTableLoadingData(tableList, isLoading, 40, 'name')
const selectedUnassignedUsers = tableData.filter((user: $Any) => selectedUsers.includes(user.name))
@@ -61,10 +69,12 @@ const ProjectUserList = ({
selectionMode="multiple"
scrollable={true}
scrollHeight="flex"
+ emptyMessage={emptyMessage}
dataKey="name"
className={clsx('user-list-table', { loading: isLoading })}
rowClassName={(rowData: $Any) => clsx({ inactive: !rowData.active, loading: isLoading })}
onContextMenu={onContextMenu}
+ onKeyDown={handleKeyDown}
onSelectionChange={(selection) => {
return onSelectUsers && onSelectionChange(selection)
}}
@@ -78,13 +88,12 @@ const ProjectUserList = ({
{
- onAdd()
- }}
+ onAdd={(user?: string) => onAdd(user)}
onRemove={() => {
onRemove && onRemove(rowData.name)
}}
showButtonsOnHover={selectedUnassignedUsers.length == 0}
+ addButtonDisabled={selectedProjects.length === 0}
selected={selectedUnassignedUserNames.includes(rowData.name)}
/>
)
diff --git a/src/pages/ProjectManagerPage/Users/ProjectUsers.tsx b/src/pages/ProjectManagerPage/Users/ProjectUsers.tsx
index a61d97e12..77d855d43 100644
--- a/src/pages/ProjectManagerPage/Users/ProjectUsers.tsx
+++ b/src/pages/ProjectManagerPage/Users/ProjectUsers.tsx
@@ -1,6 +1,6 @@
import { UserNode } from '@api/graphql'
import { $Any } from '@types'
-import { Button, SaveButton, Spacer, Toolbar } from '@ynput/ayon-react-components'
+import { Button, Toolbar } from '@ynput/ayon-react-components'
import { Splitter, SplitterPanel } from 'primereact/splitter'
import { useState } from 'react'
import ProjectUserList from './ProjectUserList'
@@ -14,10 +14,17 @@ 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) => {
+import EmptyPlaceholder from '@components/EmptyPlaceholder/EmptyPlaceholder'
+import styled from 'styled-components'
+
+const StyledButton = styled(Button)`
+ .shortcut {
+ padding: 4px;
+ background-color: var(--md-sys-color-primary-container-dark);
+ border-radius: var(--border-radius-m);
+ }
+`
+const ProjectUsers = () => {
const { data: accessGroupList = [] } = useGetAccessGroupsQuery({
projectName: '_',
})
@@ -32,7 +39,6 @@ const ProjectUsers = ({}: Props) => {
const {
users: projectUsers,
- accessGroupUsers,
selectedProjects,
setSelectedProjects,
removeUserAccessGroup,
@@ -47,18 +53,25 @@ const ProjectUsers = ({}: Props) => {
const mappedUsers = mapUsersByAccessGroups(projectUsers)
const allProjectUsers = getAllProjectUsers(mappedUsers)
- const unassignedUsers = activeNonManagerUsers.filter((user: UserNode) => !allProjectUsers.includes(user.name))
-
+ const unassignedUsers = activeNonManagerUsers.filter(
+ (user: UserNode) => !allProjectUsers.includes(user.name),
+ )
- const [selectedUsers, setSelectedUsers] = useState([])
const [actionedUsers, setActionedUsers] = useState([])
const [showDialog, setShowDialog] = useState(false)
const [selectedAccessGroupUsers, setSelectedAccessGroupUsers] = useState<
SelectedAccessGroupUsers | undefined
>()
+ const getSelectedUsers = (): string[] => {
+ if (!selectedAccessGroupUsers) {
+ return []
+ }
+
+ return selectedAccessGroupUsers!.users
+ }
- const actionEnabled = selectedProjects.length > 0 && selectedUsers.length > 0
+ const actionEnabled = selectedProjects.length > 0 && getSelectedUsers().length > 0
const getAccessGroupUsers = (accessGroup?: string): string[] => {
if (!selectedAccessGroupUsers || !accessGroup) {
@@ -68,20 +81,37 @@ const ProjectUsers = ({}: Props) => {
? selectedAccessGroupUsers.users
: []
}
-
+ const handleAdd = (user?: string) => {
+ setActionedUsers(user ? [user] : getSelectedUsers())
+ setShowDialog(true)
+ }
const updateSelectedAccessGroupUsers = (accessGroup: string, selectedUsers: string[]) => {
setSelectedAccessGroupUsers({ accessGroup, users: selectedUsers })
}
+ const resetSelectedUsers = () => setSelectedAccessGroupUsers({ users: [] })
+
const onSave = async (changes: $Any, users: string[]) => {
const errorMessage = await updateUserAccessGroups(users, changes)
if (errorMessage) {
toast.error(errorMessage)
+ } else {
+ toast.success('Operation successful')
}
+ resetSelectedUsers()
}
- const onRemove = (accessGroup: string) => (user: string) => {
- removeUserAccessGroup(user, accessGroup)
+
+ const onRemove = (accessGroup: string) => async (user?: string) => {
+ if (user) {
+ await removeUserAccessGroup(user, accessGroup)
+ } else {
+ for (const user of selectedAccessGroupUsers!.users) {
+ await removeUserAccessGroup(user, accessGroup)
+ }
+ }
+ toast.success('Operation successful')
+ resetSelectedUsers()
}
return (
@@ -94,6 +124,30 @@ const ProjectUsers = ({}: Props) => {
// onFinish={(v) => onFiltersFinish(v)} // when changes are applied
options={[]}
/>
+ {
+ e.stopPropagation()
+ setActionedUsers(getSelectedUsers())
+ setShowDialog(true)
+ }}
+ >
+ Add A {' '}
+
+
+ {
+ e.stopPropagation()
+ }}
+ >
+ Remove R {' '}
+
@@ -107,39 +161,48 @@ const ProjectUsers = ({}: Props) => {
- {
- setActionedUsers(selectedUsers)
- setShowDialog(true)
- }}
- onSelectUsers={(selection: string[]) => setSelectedUsers(selection)}
- sortable
- isUnassigned
- />
+ {selectedProjects.length > 0 ? (
+ setSelectedAccessGroupUsers({ users: selection })}
+ sortable
+ isUnassigned
+ />
+ ) : (
+
+
+ Select a project on the left side to manage users access groups
+
+
+ )}
-
- {Object.keys(mappedUsers)
- .sort()
+
+ {accessGroupList
+ .map((item) => item.name)
.map((accessGroup) => {
return (
- mappedUsers[accessGroup].includes(user.name),
+ tableList={activeNonManagerUsers.filter(
+ (user: UserNode) =>
+ mappedUsers[accessGroup] && mappedUsers[accessGroup].includes(user.name),
)}
onSelectUsers={(selection: string[]) =>
updateSelectedAccessGroupUsers(accessGroup, selection)
@@ -161,6 +224,7 @@ const ProjectUsers = ({}: Props) => {
{showDialog && (
({ ...item, selected: false }))}
onSave={onSave}
onClose={function (): void {
diff --git a/src/pages/ProjectManagerPage/Users/UserRow.tsx b/src/pages/ProjectManagerPage/Users/UserRow.tsx
index 8f08a6550..b6f5f86be 100644
--- a/src/pages/ProjectManagerPage/Users/UserRow.tsx
+++ b/src/pages/ProjectManagerPage/Users/UserRow.tsx
@@ -17,12 +17,7 @@ const StyledProfileRow = styled.div`
border-radius: var(--border-radius-m);
}
}
- &.actionable:hover {
- button {
- visibility: visible;
- }
- }
- &.selected {
+ &:hover {
button {
visibility: visible;
}
@@ -33,7 +28,8 @@ type Props = {
selected: boolean
isUnassigned: boolean
showButtonsOnHover: boolean
- onAdd: () => void
+ addButtonDisabled: boolean
+ onAdd: (user?: string) => void
onRemove?: () => void
}
@@ -42,6 +38,7 @@ export const UserRow = ({
selected = false,
isUnassigned = false,
showButtonsOnHover = false,
+ addButtonDisabled = false,
onAdd,
onRemove,
}: Props) => {
@@ -71,17 +68,18 @@ export const UserRow = ({
{
e.stopPropagation()
- onAdd()
+ onAdd(rowData.name)
}}
>
{isUnassigned ? (
<>
- {' '}
- Add A {' '}
+ Add
>
) : (
'Add more'
@@ -98,14 +96,7 @@ export const UserRow = ({
onRemove!()
}}
>
- {isUnassigned ? (
- 'Remove'
- ) : (
- <>
- {' '}
- Remove R {' '}
- >
- )}
+ Remove
)}
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 }) => {
- {
- copyToClipboard(JSON.stringify(formData, null, 2))
- }}
- />
-
- {UserPermissionsLevel.readOnly === accessLevel && 'Read-only'}
-
-
- >
+ userPermissions.canViewAnatomy(projectName) && (
+ <>
+ {
+ copyToClipboard(JSON.stringify(formData, null, 2))
+ }}
+ />
+
+ {PermissionLevel.readOnly === accessLevel && 'Read-only'}
+
+
+ >
+ )
}
>
- {userPermissions.canViewAnatomy() ? (
+ {userPermissions.canViewAnatomy(projectName) ? (
{
@@ -50,8 +50,7 @@ const ProjectManagerPage = () => {
withDefault(StringParam, projectName),
)
- const userPermissions = useUserProjectPermissions(selectedProject, isUser)
- console.log('up: ', userPermissions)
+ const userPermissions = useUserProjectPermissions(!isUser)
// UPDATE DATA
const [updateProject] = useUpdateProjectMutation()
@@ -88,7 +87,7 @@ const ProjectManagerPage = () => {
const links = []
if (!isUser || userPermissions.projectSettingsAreEnabled()) {
- if (userPermissions.canViewAnatomy() || module === 'anatomy') {
+ if (userPermissions.canViewAny(UserPermissionsEntity.anatomy) || module === 'anatomy') {
links.push({
name: 'Anatomy',
path: '/manageProjects/anatomy',
@@ -98,7 +97,7 @@ const ProjectManagerPage = () => {
})
}
- if (userPermissions.canViewSettings() || module === 'projectSettings') {
+ if (userPermissions.canViewAny(UserPermissionsEntity.settings) || module === 'projectSettings') {
links.push({
name: 'Project settings',
path: '/manageProjects/projectSettings',
@@ -107,7 +106,7 @@ const ProjectManagerPage = () => {
shortcut: 'P+P',
})
}
- if (userPermissions.canViewSettings() || module === 'userSettings') {
+ if (userPermissions.canViewAny(UserPermissionsEntity.users) || module === 'userSettings') {
links.push({
name: 'Project Users',
path: '/manageProjects/userSettings',
From 735515e38a0f1736924c41d9a64f3a844e55b953 Mon Sep 17 00:00:00 2001
From: Florin Tudor
Date: Tue, 12 Nov 2024 21:10:13 +0100
Subject: [PATCH 26/35] feature(Users): UI/UX tweaks and fixes
---
.../ProjectManagerPageLayout.jsx | 4 +-
.../Users/ProjectUserAccess.styled.ts | 37 +++++++
.../Users/ProjectUserAccess.tsx | 101 +++++++++++++-----
.../Users/ProjectUserAccessUserList.tsx | 12 +++
.../ProjectManagerPage/Users/UserRow.tsx | 42 ++++----
5 files changed, 146 insertions(+), 50 deletions(-)
create mode 100644 src/pages/ProjectManagerPage/Users/ProjectUserAccess.styled.ts
diff --git a/src/pages/ProjectManagerPage/ProjectManagerPageLayout.jsx b/src/pages/ProjectManagerPage/ProjectManagerPageLayout.jsx
index b2780010c..c5c0ff14a 100644
--- a/src/pages/ProjectManagerPage/ProjectManagerPageLayout.jsx
+++ b/src/pages/ProjectManagerPage/ProjectManagerPageLayout.jsx
@@ -2,10 +2,10 @@ import React from 'react'
import PropTypes from 'prop-types'
import { Section, Toolbar } from '@ynput/ayon-react-components'
-const ProjectManagerPageLayout = ({ projectList, children, passthrough, toolbar }) => {
+const ProjectManagerPageLayout = ({ projectList, children, passthrough, toolbar, ...props }) => {
if (passthrough) return children
return (
-
+
{projectList && projectList}
{
unassignedUsers,
filters?.find((el: Filter) => el.label === 'User'),
)
+ const filteredNonManagerUsers = getFilteredUsers(
+ activeNonManagerUsers,
+ filters?.find((el: Filter) => el.label === 'User'),
+ )
const selectedUsers = getSelectedUsers(selectedAccessGroupUsers, filteredUnassignedUsers)
const addActionEnabled = filteredSelectedProjects.length > 0 && selectedUsers.length > 0
@@ -106,7 +120,7 @@ const ProjectUserAccess = () => {
if (errorMessage) {
toast.error(errorMessage)
} else {
- toast.success('Operation successful')
+ toast.success('Access added')
}
resetSelectedUsers()
}
@@ -119,12 +133,25 @@ const ProjectUserAccess = () => {
await removeUserAccessGroup(user, accessGroup)
}
}
- toast.success('Operation successful')
+ toast.success('Access removed')
resetSelectedUsers()
}
+ const isUser = useSelector((state: $Any) => state.user.data.isUser)
+ const userPermissions = useUserProjectPermissions(!isUser)
+
+ if (!userPermissions?.canViewAny(UserPermissionsEntity.users)) {
+ return (
+
+ )
+ }
+
return (
-
+ // @ts-ignore
+
{/* @ts-ignore */}
{
setShowDialog(true)
}}
>
- Add A {' '}
+ Add access
{
onRemove(selectedAccessGroupUsers!.accessGroup!)()
}}
>
- Remove R {' '}
+ Remove access
@@ -165,46 +192,60 @@ const ProjectUserAccess = () => {
size={25}
minSize={10}
>
+ Projects
+ onSelectionChange={setSelectedProjects}
+ />
+ No project access
+
{filteredSelectedProjects.length > 0 ? (
- setSelectedAccessGroupUsers({ users: selection })}
- sortable
- isUnassigned
- />
+
+
setSelectedAccessGroupUsers({ users: selection })}
+ sortable
+ isUnassigned
+ />
+
) : (
-
-
- Select a project on the left side to manage users access groups
-
-
+
+
+
+ Select a project on the left side to manage users access groups
+
+
+
)}
-
- {filteredSelectedProjects.length > 0 && (
-
+
+ Access groups
+ {filteredSelectedProjects.length > 0 ? (
+
{getFilteredAccessGroups(accessGroupList, filters)
.map((item: AccessGroupObject) => item.name)
.map((accessGroup) => {
const selectedUsers = getAccessGroupUsers(selectedAccessGroupUsers!, accessGroup)
return (
{
selectedUsers={selectedUsers}
header={accessGroup}
emptyMessage="No users assigned"
- tableList={activeNonManagerUsers.filter(
+ tableList={filteredNonManagerUsers.filter(
(user: UserNode) =>
mappedUsers[accessGroup] &&
mappedUsers[accessGroup].includes(user.name),
@@ -233,6 +274,8 @@ const ProjectUserAccess = () => {
)
})}
+ ) : (
+
)}
@@ -248,7 +291,7 @@ const ProjectUserAccess = () => {
}}
/>
)}
-
+
)
}
export default ProjectUserAccess
diff --git a/src/pages/ProjectManagerPage/Users/ProjectUserAccessUserList.tsx b/src/pages/ProjectManagerPage/Users/ProjectUserAccessUserList.tsx
index d2adf9f10..831c879de 100644
--- a/src/pages/ProjectManagerPage/Users/ProjectUserAccessUserList.tsx
+++ b/src/pages/ProjectManagerPage/Users/ProjectUserAccessUserList.tsx
@@ -8,6 +8,8 @@ import { $Any } from '@types'
import { UserNode } from '@api/graphql'
import UserRow from './UserRow'
import { Filter } from '@components/SearchFilter/types'
+import { StyledEmptyPlaceholder, StyledEmptyPlaceholderWrapper } from './ProjectUserAccess.styled'
+
type Props = {
selectedProjects: string[]
@@ -61,6 +63,16 @@ const ProjectUserAccessUserList = ({
const selectedUnassignedUsers = tableList.filter((user: $Any) => selectedUsers.includes(user.name))
const selectedUnassignedUserNames = selectedUnassignedUsers.map((user: $Any) => user.name)
// Render
+
+ if (tableList.length === 0) {
+ return (
+
+ {header}
+
+
+ )
+ }
+
return (
diff --git a/src/pages/ProjectManagerPage/Users/UserRow.tsx b/src/pages/ProjectManagerPage/Users/UserRow.tsx
index b6f5f86be..eb11ba7e9 100644
--- a/src/pages/ProjectManagerPage/Users/UserRow.tsx
+++ b/src/pages/ProjectManagerPage/Users/UserRow.tsx
@@ -12,7 +12,11 @@ const StyledProfileRow = styled.div`
button {
visibility: hidden;
.shortcut {
- padding: 4px;
+ font-size: 11px;
+ line-height: 16px;
+ font-weight: 700;
+ padding: 1px 4px;
+ vertical-align: middle;
background-color: var(--md-sys-color-primary-container);
border-radius: var(--border-radius-m);
}
@@ -23,6 +27,17 @@ const StyledProfileRow = styled.div`
}
}
`
+const StyledButton = styled(Button)`
+ padding: 0;
+ &.hasIcon {
+ padding: 2px 4px;
+ }
+ .icon {
+ height: 20px;
+ width: 20px;
+ }
+`
+
type Props = {
rowData: $Any
selected: boolean
@@ -46,18 +61,7 @@ export const UserRow = ({
return (
{/* @ts-ignore */}
-
+
{name}
-
{isUnassigned ? (
<>
- Add
+ Add A
>
) : (
'Add more'
)}
-
+
{!isUnassigned && (
-
- Remove
-
+ 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={
+ <>
+ handleToggleAll(!allSelected)}
+ />
+
+ handleSave()} />
+ >
+ }
isOpen={true}
onClose={handleClose}
>
@@ -95,7 +117,7 @@ const ProjectUserAccessAssignDialog = ({
{
-
const udpateApiCache = (project: string, user: string, accessGroups: string[]) => {
dispatch(api.util.invalidateTags([{ type: 'project', id: project }]))
dispatch(
// @ts-ignore
- api.util.updateQueryData(
- 'getProjectsUsers',
- { projects: [project]},
- (draft: $Any) => {
- draft[project][user] = accessGroups
- },
- ),
+ api.util.updateQueryData('getProjectsUsers', { projects: [project] }, (draft: $Any) => {
+ draft[project][user] = accessGroups
+ }),
)
}
@@ -39,7 +34,13 @@ const useProjectAccessGroupData = () => {
const removeUserAccessGroup = (user: string, accessGroup: string) => {
for (const project of selectedProjects) {
// @ts-ignore
- const updatedAccessGroups = users![project][user].filter((item: string) => item !== accessGroup)
+ if (!users![project][user]) {
+ continue
+ }
+ // @ts-ignore
+ const updatedAccessGroups = users![project][user]?.filter(
+ (item: string) => item !== accessGroup,
+ )
try {
updateUser({
projectName: project,
@@ -54,7 +55,10 @@ const useProjectAccessGroupData = () => {
}
}
- const updateUserAccessGroups = async (selectedUsers: $Any, changes: {name: string, status: SelectionStatus}[] ): Promise => {
+ const updateUserAccessGroups = async (
+ selectedUsers: $Any,
+ changes: { name: string; status: SelectionStatus }[],
+ ): Promise => {
const updatedAccessGroups = (
existing: string[],
changes: { name: string; status: SelectionStatus }[],
@@ -71,23 +75,23 @@ const useProjectAccessGroupData = () => {
return [...existingSet]
}
- for (const user of selectedUsers) {
- // @ts-ignore
- 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
- }
+ for (const project of selectedProjects) {
+ for (const user of selectedUsers) {
+ // @ts-ignore
+ const accessGroups = updatedAccessGroups(users?.[project][user] || [], changes)
+
+ try {
+ await updateUser({
+ projectName: project,
+ userName: user,
+ update: accessGroups,
+ }).unwrap()
+ udpateApiCache(project, user, accessGroups)
+ } catch (error: $Any) {
+ console.log(error)
+ return error.details
}
+ }
}
}
@@ -101,7 +105,6 @@ const useProjectAccessGroupData = () => {
}
}
-
const useProjectAccessSearchFilterBuiler = ({
projects,
users,
@@ -110,9 +113,21 @@ const useProjectAccessSearchFilterBuiler = ({
[key: string]: FilterValues[]
}) => {
const options: Option[] = [
- { id: 'project', label: 'Project', icon: 'deployed_code', values: projects, allowsCustomValues: true },
+ {
+ 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 },
+ {
+ id: 'accessGroup',
+ label: 'Access Group',
+ icon: 'key',
+ values: accessGroups,
+ allowsCustomValues: true,
+ },
]
return options
diff --git a/src/pages/ProjectManagerPage/Users/mappers.ts b/src/pages/ProjectManagerPage/Users/mappers.ts
index 158d0e2a3..8b5c86cfe 100644
--- a/src/pages/ProjectManagerPage/Users/mappers.ts
+++ b/src/pages/ProjectManagerPage/Users/mappers.ts
@@ -1,8 +1,7 @@
import { AccessGroupObject } from '@api/rest/accessGroups'
import { AccessGroupUsers, SelectedAccessGroupUsers } from './types'
import { Filter } from '@components/SearchFilter/types'
-import { UserNode } from '@api/graphql'
-import { ListProjectsItemModel } from '@api/rest/project'
+import { ProjectNode, UserNode } from '@api/graphql'
import { GetProjectsUsersApiResponse } from '@queries/project/getProject'
const getAllProjectUsers = (groupedUsers: AccessGroupUsers): string[] => {
@@ -14,7 +13,9 @@ const getAllProjectUsers = (groupedUsers: AccessGroupUsers): string[] => {
return [...new Set(allUsers)]
}
-const mapUsersByAccessGroups = (response: GetProjectsUsersApiResponse | undefined): AccessGroupUsers => {
+const mapUsersByAccessGroups = (
+ response: GetProjectsUsersApiResponse | undefined,
+): AccessGroupUsers => {
if (!response) {
return {}
}
@@ -38,108 +39,95 @@ const mapUsersByAccessGroups = (response: GetProjectsUsersApiResponse | undefine
return groupedUsers
}
-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 getSelectedUsers = (
+ selectedAccessGroupUsers: SelectedAccessGroupUsers | undefined,
+ filteredUsers: UserNode[],
+ skipFiltering = false,
+): string[] => {
+ if (!selectedAccessGroupUsers) {
+ return []
}
- const filterProjects = filters && accessGroupFilters.values!.map((match: Filter) => match.id)
- if (accessGroupFilters!.inverted) {
- return accessGroupList.filter(
- (accessGroup: AccessGroupObject) => !filterProjects.includes(accessGroup.name),
- )
+ if (skipFiltering) {
+ return selectedAccessGroupUsers.users
}
- return accessGroupList.filter((accessGroup: AccessGroupObject) =>
- filterProjects.includes(accessGroup.name),
- )
+
+ const filteredUserNames = filteredUsers.map((user: UserNode) => user.name)
+ return selectedAccessGroupUsers!.users.filter((user: string) => filteredUserNames.includes(user))
}
- const getSelectedUsers = (
- selectedAccessGroupUsers: SelectedAccessGroupUsers | undefined,
- filteredUsers: UserNode[],
- skipFiltering = false,
- ): string[] => {
- if (!selectedAccessGroupUsers) {
- return []
- }
+const getAccessGroupUsers = (
+ selectedAccessGroupUsers: SelectedAccessGroupUsers,
+ accessGroup?: string,
+): string[] => {
+ if (!selectedAccessGroupUsers || !accessGroup) {
+ return []
+ }
+ return selectedAccessGroupUsers.accessGroup === accessGroup ? selectedAccessGroupUsers.users : []
+}
- if (skipFiltering) {
- return selectedAccessGroupUsers.users
- }
+const getFilteredSelectedProjects = (projects: string[], filters: Filter) => {
+ if (!filters) {
+ return projects
+ }
- const filteredUserNames = filteredUsers.map((user: UserNode) => user.name)
- return selectedAccessGroupUsers!.users.filter((user: string) =>
- filteredUserNames.includes(user),
- )
+ 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 getAccessGroupUsers = (
- selectedAccessGroupUsers: SelectedAccessGroupUsers,
- accessGroup?: string,
- ): string[] => {
- if (!selectedAccessGroupUsers || !accessGroup) {
- return []
- }
- return selectedAccessGroupUsers.accessGroup === accessGroup
- ? selectedAccessGroupUsers.users
- : []
+const exactFilter = (entities: T[], filters: Filter): T[] => {
+ const filterUsers = filters && filters.values!.map((match: Filter) => match.id)
+ if (filters!.inverted) {
+ return entities.filter((entity: T) => !filterUsers.includes(entity.name))
}
+ return entities.filter((entity: T) => filterUsers.includes(entity.name))
+}
- const getFilteredSelectedProjects = (projects: string[], filters: Filter) => {
- if (!filters) {
- return projects
- }
+const fuzzyFilter = (users: T[], filters: Filter): T[] => {
+ const filterString = filters.values![0].id
+ if (filters!.inverted) {
+ return users.filter((user: T) => user.name.indexOf(filterString) == -1)
+ }
+ return users.filter((user: T) => user.name.indexOf(filterString) != -1)
+}
- 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 getFilteredProjects = (projects: ProjectNode[], filter: Filter): ProjectNode[] => {
+ if (!filter || !filter.values || filter.values.length == 0) {
+ return projects
}
- const getFilteredProjects = (projects: ListProjectsItemModel[], filter: Filter) => {
- if (!filter) {
- return projects
- }
+ if (filter.values!.length == 1 && filter.values[0]!.isCustom) {
+ return fuzzyFilter(projects, filter)
+ }
- const filterProjects = filter.values!.map((match: Filter) => match.id)
- if (filter!.inverted) {
- return projects.filter((project: ListProjectsItemModel) => !filterProjects.includes(project.name))
- }
+ return exactFilter(projects, filter)
+}
- return projects.filter((project: ListProjectsItemModel) => filterProjects.includes(project.name))
+const getFilteredUsers = (users: UserNode[], filter?: Filter): UserNode[] => {
+ if (!filter || !filter.values || filter.values.length == 0) {
+ return users
}
-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 (filter.values!.length == 1 && filter.values[0]!.isCustom) {
+ return fuzzyFilter(users, filter)
}
- 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, filter)
+}
- if (!filters || !filters.values || filters.values.length == 0) {
- return users
+const getFilteredAccessGroups = (accessGroupList: AccessGroupObject[], filter: Filter): AccessGroupObject[] => {
+ if (!filter || !filter.values || filter.values.length == 0) {
+ return accessGroupList
}
-
- if (filters.values!.length == 1 && filters.values[0]!.isCustom) {
- return fuzzyFilter(users, filters)
+ if (filter.values!.length == 1 && filter.values[0]!.isCustom) {
+ return fuzzyFilter(accessGroupList, filter)
}
- return exactFilter(users, filters)
+ return exactFilter(accessGroupList, filter)
}
export {
From 3ce50148d19f558598af91a60d889b7d59642e43 Mon Sep 17 00:00:00 2001
From: Florin Tudor
Date: Wed, 13 Nov 2024 09:42:47 +0100
Subject: [PATCH 28/35] feature(Users): Added context menu for unassigned users
pane, other small refactoring
---
.../ProjectManagerPage/ProjectManagerPage.jsx | 2 +-
.../Users/ProjectUserAccess.tsx | 81 +++++++++++--------
.../Users/ProjectUserAccessUserList.tsx | 8 +-
3 files changed, 54 insertions(+), 37 deletions(-)
diff --git a/src/pages/ProjectManagerPage/ProjectManagerPage.jsx b/src/pages/ProjectManagerPage/ProjectManagerPage.jsx
index 7d2db255f..1cf60e585 100644
--- a/src/pages/ProjectManagerPage/ProjectManagerPage.jsx
+++ b/src/pages/ProjectManagerPage/ProjectManagerPage.jsx
@@ -108,7 +108,7 @@ const ProjectManagerPage = () => {
}
if (userPermissions.canViewAny(UserPermissionsEntity.users) || module === 'userSettings') {
links.push({
- name: 'Project Users',
+ name: 'Project access',
path: '/manageProjects/userSettings',
module: 'userSettings',
accessLevels: [],
diff --git a/src/pages/ProjectManagerPage/Users/ProjectUserAccess.tsx b/src/pages/ProjectManagerPage/Users/ProjectUserAccess.tsx
index 3724a57a7..81f6e78d4 100644
--- a/src/pages/ProjectManagerPage/Users/ProjectUserAccess.tsx
+++ b/src/pages/ProjectManagerPage/Users/ProjectUserAccess.tsx
@@ -1,4 +1,4 @@
-import { UserNode } from '@api/graphql'
+import { ProjectNode, UserNode } from '@api/graphql'
import { $Any } from '@types'
import { Button, Toolbar } from '@ynput/ayon-react-components'
import { Splitter, SplitterPanel } from 'primereact/splitter'
@@ -6,7 +6,7 @@ import { useState } from 'react'
import ProjectUserAccessUserList from './ProjectUserAccessUserList'
import { useGetAccessGroupsQuery } from '@queries/accessGroups/getAccessGroups'
import ProjectUserAccessAssignDialog from './ProjectUserAccessAssignDialog'
-import { SelectedAccessGroupUsers } from './types'
+import { SelectedAccessGroupUsers, SelectionStatus } from './types'
import { useProjectAccessGroupData } from './hooks'
import { toast } from 'react-toastify'
import { useGetUsersQuery } from '@queries/user/getUsers'
@@ -31,6 +31,7 @@ import { useListProjectsQuery } from '@queries/project/getProject'
import useUserProjectPermissions, { UserPermissionsEntity } from '@hooks/useUserProjectPermissions'
import ProjectManagerPageLayout from '../ProjectManagerPageLayout'
import { StyledEmptyPlaceholder, StyledEmptyPlaceholderWrapper } from './ProjectUserAccess.styled'
+import useCreateContext from '@hooks/useCreateContext'
const StyledHeader = styled.p`
font-size: 16px;
@@ -84,8 +85,11 @@ const ProjectUserAccess = () => {
}
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 filteredProjects = getFilteredProjects(
+ // @ts-ignore Weird one, the response type seems to be mismatched?
+ (projects || []).filter((project: ProjectNode) => project.active), // Always filtering out inactive projects
+ projectFilters,
+ )
const filteredSelectedProjects = getFilteredSelectedProjects(selectedProjects, projectFilters)
const userFilter = filters?.find((el: Filter) => el.label === 'User')
@@ -99,8 +103,39 @@ const ProjectUserAccess = () => {
getSelectedUsers(selectedAccessGroupUsers, [], true).length > 0 &&
selectedAccessGroupUsers?.accessGroup != undefined
- const handleAdd = (user?: string) => {
- setActionedUsers(user ? [user] : selectedUsers)
+ const filteredAccessGroups = getFilteredAccessGroups(
+ accessGroupList,
+ filters.find((filter: Filter) => filter.label === 'Access Group'),
+ )
+
+
+ const [ctxMenuShow] = useCreateContext([])
+
+ const handleAddContextMenu = (e: $Any) => {
+ let actionedUsers = selectedUsers
+ if (!selectedUsers.includes(e.data.name)) {
+ actionedUsers = [e.data.name]
+ setSelectedAccessGroupUsers({ users: [e.data.name] })
+ }
+
+ ctxMenuShow(e.originalEvent, [
+ {
+ id: 'add',
+ label: 'Add to access groups',
+ command: () => handleAdd(actionedUsers),
+ },
+ ])
+ }
+
+ const handleAdd = (users?: string[]) => {
+ const actionedUsers = users ? users : selectedUsers
+ setActionedUsers(actionedUsers)
+ if (filteredAccessGroups.length == 1) {
+ onSave(actionedUsers, [{ name: filteredAccessGroups[0].name, status: SelectionStatus.All }])
+
+ return
+ }
+
setShowDialog(true)
}
@@ -120,13 +155,10 @@ const ProjectUserAccess = () => {
resetSelectedUsers()
}
- const onRemove = (accessGroup: string) => async (user?: string) => {
- if (user) {
+ const onRemove = (accessGroup: string) => async (users?: string[]) => {
+ const userList = users ? users : selectedAccessGroupUsers!.users
+ for (const user of userList) {
await removeUserAccessGroup(user, accessGroup)
- } else {
- for (const user of selectedAccessGroupUsers!.users) {
- await removeUserAccessGroup(user, accessGroup)
- }
}
toast.success('Access removed')
resetSelectedUsers()
@@ -158,11 +190,7 @@ const ProjectUserAccess = () => {
disabled={!addActionEnabled}
data-tooltip={false ? 'No project selected' : undefined}
icon={'add'}
- onClick={(e) => {
- e.stopPropagation()
- setActionedUsers(selectedUsers)
- setShowDialog(true)
- }}
+ onClick={() => handleAdd()}
>
Add access
@@ -209,6 +237,7 @@ const ProjectUserAccess = () => {
selectedUsers={selectedUsers}
tableList={filteredUnassignedUsers}
isLoading={isLoading}
+ onContextMenu={handleAddContextMenu}
onAdd={handleAdd}
onSelectUsers={(selection) => setSelectedAccessGroupUsers({ users: selection })}
sortable
@@ -229,10 +258,7 @@ const ProjectUserAccess = () => {
Access groups
{filteredSelectedProjects.length > 0 ? (
- {getFilteredAccessGroups(
- accessGroupList,
- filters.find((filter: Filter) => filter.label === 'Access Group'),
- )
+ {filteredAccessGroups
.map((item: AccessGroupObject) => item.name)
.map((accessGroup) => {
const selectedUsers = getAccessGroupUsers(selectedAccessGroupUsers!, accessGroup)
@@ -261,10 +287,7 @@ const ProjectUserAccess = () => {
onSelectUsers={(selection: string[]) =>
updateSelectedAccessGroupUsers(accessGroup, selection)
}
- onAdd={() => {
- setActionedUsers(selectedUsers)
- setShowDialog(true)
- }}
+ onAdd={() => handleAdd()}
onRemove={onRemove(accessGroup)}
isLoading={isLoading}
/>
@@ -282,13 +305,7 @@ const ProjectUserAccess = () => {
filter.label === 'Access Group'),
- ).map((item) => ({
- ...item,
- selected: false,
- }))}
+ accessGroups={filteredAccessGroups.map((item) => ({ ...item, selected: false }))}
onSave={onSave}
onClose={function (): void {
setShowDialog(false)
diff --git a/src/pages/ProjectManagerPage/Users/ProjectUserAccessUserList.tsx b/src/pages/ProjectManagerPage/Users/ProjectUserAccessUserList.tsx
index 831c879de..04a545d7b 100644
--- a/src/pages/ProjectManagerPage/Users/ProjectUserAccessUserList.tsx
+++ b/src/pages/ProjectManagerPage/Users/ProjectUserAccessUserList.tsx
@@ -23,8 +23,8 @@ type Props = {
isUnassigned?: boolean
onContextMenu?: $Any
onSelectUsers?: (selectedUsers: string[]) => void
- onAdd: (user? : string) => void
- onRemove?: (user?: string) => void
+ onAdd: (users? : string[]) => void
+ onRemove?: (users?: string[]) => void
}
const ProjectUserAccessUserList = ({
@@ -101,9 +101,9 @@ const ProjectUserAccessUserList = ({
onAdd(user)}
+ onAdd={(user?: string) => onAdd(user ? [user] : undefined)}
onRemove={() => {
- onRemove && onRemove(rowData.name)
+ onRemove && onRemove([rowData.name])
}}
showButtonsOnHover={selectedUnassignedUsers.length == 0}
addButtonDisabled={selectedProjects.length === 0}
From 76e44cb084632b2d2e523cc66c590ca092d4b3a4 Mon Sep 17 00:00:00 2001
From: Florin Tudor
Date: Wed, 13 Nov 2024 10:52:58 +0100
Subject: [PATCH 29/35] feature(Users): Added context menu for already assigned
users
---
.../Users/ProjectUserAccess.tsx | 20 ++++++++-
.../Users/ProjectUserAccessUserList.tsx | 3 ++
.../ProjectManagerPage/Users/UserRow.tsx | 42 ++++++++++---------
3 files changed, 45 insertions(+), 20 deletions(-)
diff --git a/src/pages/ProjectManagerPage/Users/ProjectUserAccess.tsx b/src/pages/ProjectManagerPage/Users/ProjectUserAccess.tsx
index 81f6e78d4..c698c87ca 100644
--- a/src/pages/ProjectManagerPage/Users/ProjectUserAccess.tsx
+++ b/src/pages/ProjectManagerPage/Users/ProjectUserAccess.tsx
@@ -113,7 +113,7 @@ const ProjectUserAccess = () => {
const handleAddContextMenu = (e: $Any) => {
let actionedUsers = selectedUsers
- if (!selectedUsers.includes(e.data.name)) {
+ if (!actionedUsers.includes(e.data.name)) {
actionedUsers = [e.data.name]
setSelectedAccessGroupUsers({ users: [e.data.name] })
}
@@ -127,6 +127,22 @@ const ProjectUserAccess = () => {
])
}
+ const handleRemoveContextMenu = (e: $Any, accessGroup: string) => {
+ let actionedUsers = selectedAccessGroupUsers?.users || []
+ if (!actionedUsers.includes(e.data.name)) {
+ actionedUsers = [e.data.name]
+ setSelectedAccessGroupUsers({ users: [e.data.name] })
+ }
+
+ ctxMenuShow(e.originalEvent, [
+ {
+ id: 'remove',
+ label: 'Remove from access group',
+ command: () => onRemove(accessGroup)(actionedUsers),
+ },
+ ])
+ }
+
const handleAdd = (users?: string[]) => {
const actionedUsers = users ? users : selectedUsers
setActionedUsers(actionedUsers)
@@ -278,7 +294,9 @@ const ProjectUserAccess = () => {
selectedProjects={filteredSelectedProjects}
selectedUsers={selectedUsers}
header={accessGroup}
+ showAddMoreButton={filteredAccessGroups.length > 1}
emptyMessage="No users assigned"
+ onContextMenu={(e: $Any) => handleRemoveContextMenu(e, accessGroup)}
tableList={filteredNonManagerUsers.filter(
(user: UserNode) =>
mappedUsers[accessGroup] &&
diff --git a/src/pages/ProjectManagerPage/Users/ProjectUserAccessUserList.tsx b/src/pages/ProjectManagerPage/Users/ProjectUserAccessUserList.tsx
index 04a545d7b..daebda318 100644
--- a/src/pages/ProjectManagerPage/Users/ProjectUserAccessUserList.tsx
+++ b/src/pages/ProjectManagerPage/Users/ProjectUserAccessUserList.tsx
@@ -21,6 +21,7 @@ type Props = {
emptyMessage: string
sortable?: boolean
isUnassigned?: boolean
+ showAddMoreButton?: boolean
onContextMenu?: $Any
onSelectUsers?: (selectedUsers: string[]) => void
onAdd: (users? : string[]) => void
@@ -37,6 +38,7 @@ const ProjectUserAccessUserList = ({
emptyMessage,
sortable = false,
isUnassigned = false,
+ showAddMoreButton = false,
onAdd,
onRemove,
onContextMenu,
@@ -101,6 +103,7 @@ const ProjectUserAccessUserList = ({
onAdd(user ? [user] : undefined)}
onRemove={() => {
onRemove && onRemove([rowData.name])
diff --git a/src/pages/ProjectManagerPage/Users/UserRow.tsx b/src/pages/ProjectManagerPage/Users/UserRow.tsx
index eb11ba7e9..924d6fb3c 100644
--- a/src/pages/ProjectManagerPage/Users/UserRow.tsx
+++ b/src/pages/ProjectManagerPage/Users/UserRow.tsx
@@ -44,6 +44,7 @@ type Props = {
isUnassigned: boolean
showButtonsOnHover: boolean
addButtonDisabled: boolean
+ showAddMoreButton: boolean
onAdd: (user?: string) => void
onRemove?: () => void
}
@@ -54,6 +55,7 @@ export const UserRow = ({
isUnassigned = false,
showButtonsOnHover = false,
addButtonDisabled = false,
+ showAddMoreButton = false,
onAdd,
onRemove,
}: Props) => {
@@ -70,25 +72,27 @@ export const UserRow = ({
>
{name}
- {
- e.stopPropagation()
- onAdd(rowData.name)
- }}
- >
- {isUnassigned ? (
- <>
- Add A
- >
- ) : (
- 'Add more'
- )}
-
+ {(isUnassigned || showAddMoreButton) && (
+ {
+ e.stopPropagation()
+ onAdd(rowData.name)
+ }}
+ >
+ {isUnassigned ? (
+ <>
+ Add A
+ >
+ ) : (
+ 'Add more'
+ )}
+
+ )}
{!isUnassigned && (
Date: Wed, 13 Nov 2024 15:34:14 +0100
Subject: [PATCH 30/35] feature(Users): Sorting, marking as inactive projects
based on permissions
---
src/containers/projectList.jsx | 72 +++++++++++--------
src/context/shortcutsContext.jsx | 4 ++
src/hooks/useUserProjectPermissions.ts | 2 +-
.../ProjectManagerPage/ProjectManagerPage.jsx | 24 ++++++-
.../Users/ProjectUserAccess.tsx | 15 +++-
.../Users/ProjectUserAccessProjectList.tsx | 33 +++++++--
6 files changed, 108 insertions(+), 42 deletions(-)
diff --git a/src/containers/projectList.jsx b/src/containers/projectList.jsx
index 84f1e9165..355dbb8b6 100644
--- a/src/containers/projectList.jsx
+++ b/src/containers/projectList.jsx
@@ -121,6 +121,8 @@ const ProjectList = ({
wrap,
onSelectAll,
onSelectAllDisabled,
+ customSort,
+ isActiveCallable,
}) => {
const navigate = useAyonNavigate()
const tableRef = useRef(null)
@@ -181,18 +183,20 @@ const ProjectList = ({
pinned: project.active ? pinnedProjects.includes(project.name) : false,
}))
.sort((a, b) => {
- if (!a.active && b.active) {
- return 1 // a goes to the bottom
- } else if (a.active && !b.active) {
- return -1 // b goes to the bottom
- } else if (a.pinned && !b.pinned) {
- return -1 // a comes before b
- } else if (!a.pinned && b.pinned) {
- return 1 // b comes before a
- } else {
- // If both have the same pinned status, sort alphabetically by name
- return a.name.localeCompare(b.name)
+ const aActive = a.active ? 10 : -10
+ const bActive = a.active ? 10 : -10
+ const aPinned = a.pinned ? 1 : -1
+ const bPinned = b.pinned ? 1 : -1
+ const mainComparison = bActive + bPinned - aActive - aPinned
+ if (mainComparison !== 0) {
+ return mainComparison
}
+
+ if (customSort) {
+ return customSort(a.name, b.name)
+ }
+
+ return a.name.localeCompare(b.name)
})
const projectList = projectListWithPinned
@@ -449,17 +453,20 @@ const ProjectList = ({
(
-
- {formatName(rowData, showNull)}
- {formatName(rowData, showNull, 'code')}
-
- )}
+ body={(rowData) => {
+ const isActiveCallableValue = isActiveCallable ? isActiveCallable(rowData.name) : true
+ return (
+
+ {formatName(rowData, showNull)}
+ {formatName(rowData, showNull, 'code')}
+
+ )
+ }}
style={{ minWidth: 150, ...style }}
/>
{!hideCode && !collapsed && (
@@ -467,15 +474,18 @@ const ProjectList = ({
field="code"
header="Code"
style={{ maxWidth: 80 }}
- body={(rowData) => (
-
- {rowData.code}
-
- )}
+ body={(rowData) => {
+ const isActiveCallableValue = isActiveCallable ? isActiveCallable(rowData.name) : true
+ return (
+
+ {rowData.code}
+
+ )
+ }}
/>
)}
{!collapsed && (
diff --git a/src/context/shortcutsContext.jsx b/src/context/shortcutsContext.jsx
index acb8b7427..a8b984268 100644
--- a/src/context/shortcutsContext.jsx
+++ b/src/context/shortcutsContext.jsx
@@ -38,6 +38,10 @@ function ShortcutsProvider(props) {
key: 'p+p',
action: () => navigate('/manageProjects/projectSettings?' + searchParams.toString()),
},
+ {
+ key: 'p+a',
+ action: () => navigate('/manageProjects/userSettings?' + searchParams.toString()),
+ },
// project settings anatomy
{ key: 'a+a', action: () => navigate('/manageProjects/anatomy?' + searchParams.toString()) },
// studio settings
diff --git a/src/hooks/useUserProjectPermissions.ts b/src/hooks/useUserProjectPermissions.ts
index d2ff83c55..ab8040a31 100644
--- a/src/hooks/useUserProjectPermissions.ts
+++ b/src/hooks/useUserProjectPermissions.ts
@@ -159,5 +159,5 @@ const useUserProjectPermissions = (
return new UserPermissions(permissions, hasLimitedPermissions)
}
-export { PermissionLevel }
+export { UserPermissions, PermissionLevel }
export default useUserProjectPermissions
diff --git a/src/pages/ProjectManagerPage/ProjectManagerPage.jsx b/src/pages/ProjectManagerPage/ProjectManagerPage.jsx
index 1cf60e585..1e1fcfc9d 100644
--- a/src/pages/ProjectManagerPage/ProjectManagerPage.jsx
+++ b/src/pages/ProjectManagerPage/ProjectManagerPage.jsx
@@ -112,7 +112,7 @@ const ProjectManagerPage = () => {
path: '/manageProjects/userSettings',
module: 'userSettings',
accessLevels: [],
- shortcut: 'P+U',
+ shortcut: 'P+A',
})
}
}
@@ -159,6 +159,28 @@ const ProjectManagerPage = () => {
onNewProject={() => setShowNewProject(true)}
onDeleteProject={handleDeleteProject}
onActivateProject={handleActivateProject}
+ customSort={(a, b) => {
+ if (module === 'anatomy') {
+ const aPerm = userPermissions.canView(UserPermissionsEntity.anatomy, a) ? 1 : -1
+ const bPerm = userPermissions.canView(UserPermissionsEntity.anatomy, b) ? 1 : -1
+ return bPerm - aPerm
+ }
+ if (module === 'siteSettings') {
+ const aPerm = userPermissions.canView(UserPermissionsEntity.settings, a) ? 1 : -1
+ const bPerm = userPermissions.canView(UserPermissionsEntity.settings, b) ? 1 : -1
+ return bPerm - aPerm
+ }
+ return 0
+ }}
+ isActiveCallable={(projectName) => {
+ if (module === 'anatomy') {
+ return userPermissions.canView(UserPermissionsEntity.anatomy, projectName)
+ }
+ if (module === 'siteSettings') {
+ return userPermissions.canView(UserPermissionsEntity.settings, projectName)
+ }
+ return true
+ }}
>
{module === 'anatomy' && }
{module === 'projectSettings' && }
diff --git a/src/pages/ProjectManagerPage/Users/ProjectUserAccess.tsx b/src/pages/ProjectManagerPage/Users/ProjectUserAccess.tsx
index c698c87ca..32e607b26 100644
--- a/src/pages/ProjectManagerPage/Users/ProjectUserAccess.tsx
+++ b/src/pages/ProjectManagerPage/Users/ProjectUserAccess.tsx
@@ -108,7 +108,6 @@ const ProjectUserAccess = () => {
filters.find((filter: Filter) => filter.label === 'Access Group'),
)
-
const [ctxMenuShow] = useCreateContext([])
const handleAddContextMenu = (e: $Any) => {
@@ -191,6 +190,17 @@ const ProjectUserAccess = () => {
/>
)
}
+ const handleProjectSelectionChange = (selection: string[]) => {
+ if (selection.length <= 1) {
+ setSelectedProjects(selection)
+ return
+ }
+
+ const filteredSelection = selection.filter((projectName) =>
+ userPermissions.canEdit(UserPermissionsEntity.users, projectName),
+ )
+ setSelectedProjects(filteredSelection)
+ }
return (
// @ts-ignore
@@ -237,7 +247,8 @@ const ProjectUserAccess = () => {
// @ts-ignore
projects={filteredProjects}
isLoading={projectsIsLoading}
- onSelectionChange={setSelectedProjects}
+ userPermissions={userPermissions}
+ onSelectionChange={handleProjectSelectionChange}
/>
diff --git a/src/pages/ProjectManagerPage/Users/ProjectUserAccessProjectList.tsx b/src/pages/ProjectManagerPage/Users/ProjectUserAccessProjectList.tsx
index 77bd089fa..9acf457e5 100644
--- a/src/pages/ProjectManagerPage/Users/ProjectUserAccessProjectList.tsx
+++ b/src/pages/ProjectManagerPage/Users/ProjectUserAccessProjectList.tsx
@@ -6,6 +6,7 @@ import useTableLoadingData from '@hooks/useTableLoadingData'
import { $Any } from '@types'
import { TablePanel } from '@ynput/ayon-react-components'
import { ProjectNode } from '@api/graphql'
+import { UserPermissions, UserPermissionsEntity } from '@hooks/useUserProjectPermissions'
const formatName = (rowData: $Any, field: string) => {
return rowData[field]
@@ -26,6 +27,11 @@ const StyledProjectName = styled.div`
opacity: 0;
}
+ &:not(.isActive) {
+ font-style: italic;
+ color: var(--md-ref-palette-secondary50);
+ }
+
&:not(.isOpen) {
span:first-child {
opacity: 0;
@@ -40,17 +46,27 @@ type Props = {
projects: ProjectNode[]
selection: string[]
isLoading: boolean
+ userPermissions: UserPermissions
onSelectionChange: (selection: $Any) => void
}
-const ProjectUserAccessProjectList = ({ projects, isLoading, selection, onSelectionChange }: Props) => {
+const ProjectUserAccessProjectList = ({ projects, isLoading, selection, userPermissions, onSelectionChange }: Props) => {
const tableData = useTableLoadingData(projects, isLoading, 10, 'name')
const selected = tableData.filter((project: ProjectNode) => selection.includes(project.name))
return (
{
+ const aPerm = userPermissions.canView(UserPermissionsEntity.users, a.name) ? 1 : -1
+ const bPerm = userPermissions.canView(UserPermissionsEntity.users, b.name) ? 1 : -1
+ const mainComparison = bPerm - aPerm
+ if (mainComparison !== 0) {
+ return mainComparison
+ }
+
+ return a.name.localeCompare(b.name)
+ })}
selection={selected}
multiple={true}
scrollable={true}
@@ -65,11 +81,14 @@ const ProjectUserAccessProjectList = ({ projects, isLoading, selection, onSelect
(
-
- {formatName(rowData, 'name')}
-
- )}
+ body={(rowData) => {
+ const isActive = userPermissions.canView(UserPermissionsEntity.users, rowData.name)
+ return (
+
+ {formatName(rowData, 'name')}
+
+ )
+ }}
style={{ minWidth: 150 }}
/>
From 466a6e125dff3745b63f044389675bc061b8b02f Mon Sep 17 00:00:00 2001
From: Florin Tudor
Date: Thu, 14 Nov 2024 07:47:22 +0100
Subject: [PATCH 31/35] feature(Users): Filtering add/remove actions depending
on selected project & active filters
---
.../Users/ProjectUserAccess.tsx | 21 +++++++++++++------
.../Users/ProjectUserAccessProjectList.tsx | 6 +++---
.../Users/ProjectUserAccessUserList.tsx | 7 ++++++-
.../ProjectManagerPage/Users/UserRow.tsx | 6 ++++--
src/pages/ProjectManagerPage/Users/mappers.ts | 17 ++++++++++++++-
5 files changed, 44 insertions(+), 13 deletions(-)
diff --git a/src/pages/ProjectManagerPage/Users/ProjectUserAccess.tsx b/src/pages/ProjectManagerPage/Users/ProjectUserAccess.tsx
index 32e607b26..159e7849a 100644
--- a/src/pages/ProjectManagerPage/Users/ProjectUserAccess.tsx
+++ b/src/pages/ProjectManagerPage/Users/ProjectUserAccess.tsx
@@ -11,6 +11,7 @@ import { useProjectAccessGroupData } from './hooks'
import { toast } from 'react-toastify'
import { useGetUsersQuery } from '@queries/user/getUsers'
import {
+ canAllEditUsers,
getAccessGroupUsers,
getAllProjectUsers,
getFilteredAccessGroups,
@@ -84,10 +85,16 @@ const ProjectUserAccess = () => {
console.error(error)
}
+ const isUser = useSelector((state: $Any) => state.user.data.isUser)
+ const userPermissions = useUserProjectPermissions(!isUser)
+
const projectFilters = filters.find((filter: Filter) => filter.label === 'Project')
const filteredProjects = getFilteredProjects(
// @ts-ignore Weird one, the response type seems to be mismatched?
- (projects || []).filter((project: ProjectNode) => project.active), // Always filtering out inactive projects
+ (projects || []).filter(
+ (project: ProjectNode) =>
+ project.active && userPermissions?.canView(UserPermissionsEntity.users, project.name),
+ ),
projectFilters,
)
const filteredSelectedProjects = getFilteredSelectedProjects(selectedProjects, projectFilters)
@@ -97,10 +104,12 @@ const ProjectUserAccess = () => {
const filteredNonManagerUsers = getFilteredUsers(activeNonManagerUsers, userFilter)
const selectedUsers = getSelectedUsers(selectedAccessGroupUsers, filteredUnassignedUsers)
- const addActionEnabled = filteredSelectedProjects.length > 0 && selectedUsers.length > 0
- const removeActionEnabled =
+ const hasEditRightsOnProject =
filteredSelectedProjects.length > 0 &&
- getSelectedUsers(selectedAccessGroupUsers, [], true).length > 0 &&
+ canAllEditUsers(filteredSelectedProjects, userPermissions)
+ const addActionEnabled = hasEditRightsOnProject && selectedUsers.length > 0
+ const removeActionEnabled = hasEditRightsOnProject
+ getSelectedUsers(selectedAccessGroupUsers, [], true).length > 0 &&
selectedAccessGroupUsers?.accessGroup != undefined
const filteredAccessGroups = getFilteredAccessGroups(
@@ -179,8 +188,6 @@ const ProjectUserAccess = () => {
resetSelectedUsers()
}
- const isUser = useSelector((state: $Any) => state.user.data.isUser)
- const userPermissions = useUserProjectPermissions(!isUser)
if (!userPermissions?.canViewAny(UserPermissionsEntity.users)) {
return (
@@ -264,6 +271,7 @@ const ProjectUserAccess = () => {
selectedUsers={selectedUsers}
tableList={filteredUnassignedUsers}
isLoading={isLoading}
+ readOnly={!hasEditRightsOnProject}
onContextMenu={handleAddContextMenu}
onAdd={handleAdd}
onSelectUsers={(selection) => setSelectedAccessGroupUsers({ users: selection })}
@@ -305,6 +313,7 @@ const ProjectUserAccess = () => {
selectedProjects={filteredSelectedProjects}
selectedUsers={selectedUsers}
header={accessGroup}
+ readOnly={!hasEditRightsOnProject}
showAddMoreButton={filteredAccessGroups.length > 1}
emptyMessage="No users assigned"
onContextMenu={(e: $Any) => handleRemoveContextMenu(e, accessGroup)}
diff --git a/src/pages/ProjectManagerPage/Users/ProjectUserAccessProjectList.tsx b/src/pages/ProjectManagerPage/Users/ProjectUserAccessProjectList.tsx
index 9acf457e5..b6eeaeb70 100644
--- a/src/pages/ProjectManagerPage/Users/ProjectUserAccessProjectList.tsx
+++ b/src/pages/ProjectManagerPage/Users/ProjectUserAccessProjectList.tsx
@@ -58,8 +58,8 @@ const ProjectUserAccessProjectList = ({ projects, isLoading, selection, userPerm
{
- const aPerm = userPermissions.canView(UserPermissionsEntity.users, a.name) ? 1 : -1
- const bPerm = userPermissions.canView(UserPermissionsEntity.users, b.name) ? 1 : -1
+ const aPerm = userPermissions.canEdit(UserPermissionsEntity.users, a.name) ? 1 : -1
+ const bPerm = userPermissions.canEdit(UserPermissionsEntity.users, b.name) ? 1 : -1
const mainComparison = bPerm - aPerm
if (mainComparison !== 0) {
return mainComparison
@@ -82,7 +82,7 @@ const ProjectUserAccessProjectList = ({ projects, isLoading, selection, userPerm
field="name"
header="Project name"
body={(rowData) => {
- const isActive = userPermissions.canView(UserPermissionsEntity.users, rowData.name)
+ const isActive = userPermissions.canEdit(UserPermissionsEntity.users, rowData.name)
return (
{formatName(rowData, 'name')}
diff --git a/src/pages/ProjectManagerPage/Users/ProjectUserAccessUserList.tsx b/src/pages/ProjectManagerPage/Users/ProjectUserAccessUserList.tsx
index daebda318..fdea7117b 100644
--- a/src/pages/ProjectManagerPage/Users/ProjectUserAccessUserList.tsx
+++ b/src/pages/ProjectManagerPage/Users/ProjectUserAccessUserList.tsx
@@ -17,6 +17,7 @@ type Props = {
tableList: $Any
filters?: Filter
isLoading: boolean
+ readOnly: boolean,
header?: string
emptyMessage: string
sortable?: boolean
@@ -34,6 +35,7 @@ const ProjectUserAccessUserList = ({
tableList,
filters,
isLoading,
+ readOnly,
header,
emptyMessage,
sortable = false,
@@ -62,7 +64,9 @@ const ProjectUserAccessUserList = ({
}
}
- const selectedUnassignedUsers = tableList.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
@@ -104,6 +108,7 @@ const ProjectUserAccessUserList = ({
rowData={rowData}
isUnassigned={isUnassigned}
showAddMoreButton={showAddMoreButton}
+ readOnly={readOnly}
onAdd={(user?: string) => onAdd(user ? [user] : undefined)}
onRemove={() => {
onRemove && onRemove([rowData.name])
diff --git a/src/pages/ProjectManagerPage/Users/UserRow.tsx b/src/pages/ProjectManagerPage/Users/UserRow.tsx
index 924d6fb3c..31a87a141 100644
--- a/src/pages/ProjectManagerPage/Users/UserRow.tsx
+++ b/src/pages/ProjectManagerPage/Users/UserRow.tsx
@@ -45,6 +45,7 @@ type Props = {
showButtonsOnHover: boolean
addButtonDisabled: boolean
showAddMoreButton: boolean
+ readOnly: boolean
onAdd: (user?: string) => void
onRemove?: () => void
}
@@ -56,6 +57,7 @@ export const UserRow = ({
showButtonsOnHover = false,
addButtonDisabled = false,
showAddMoreButton = false,
+ readOnly,
onAdd,
onRemove,
}: Props) => {
@@ -72,7 +74,7 @@ export const UserRow = ({
>
{name}
- {(isUnassigned || showAddMoreButton) && (
+ {!readOnly && (isUnassigned || showAddMoreButton) && (
)}
- {!isUnassigned && (
+ {!readOnly && !isUnassigned && (
{
let allUsers: string[] = []
@@ -119,7 +120,10 @@ const getFilteredUsers = (users: UserNode[], filter?: Filter): UserNode[] => {
return exactFilter(users, filter)
}
-const getFilteredAccessGroups = (accessGroupList: AccessGroupObject[], filter: Filter): AccessGroupObject[] => {
+const getFilteredAccessGroups = (
+ accessGroupList: AccessGroupObject[],
+ filter: Filter,
+): AccessGroupObject[] => {
if (!filter || !filter.values || filter.values.length == 0) {
return accessGroupList
}
@@ -130,7 +134,18 @@ const getFilteredAccessGroups = (accessGroupList: AccessGroupObject[], filter: F
return exactFilter(accessGroupList, filter)
}
+const canAllEditUsers = (projects: string[], userPermissions?: UserPermissions) => {
+ for (const project of projects) {
+ if (!userPermissions?.canEdit(UserPermissionsEntity.users, project)) {
+ return false
+ }
+ }
+
+ return true
+}
+
export {
+ canAllEditUsers,
mapUsersByAccessGroups,
getAllProjectUsers,
getFilteredAccessGroups,
From 29e9f83832e50817dbbf8d4facb50d7ac4dd85fd Mon Sep 17 00:00:00 2001
From: Florin Tudor
Date: Thu, 14 Nov 2024 10:32:24 +0100
Subject: [PATCH 32/35] feature(Users): Adding hover key shortcuts, removing
global anatomy shortcut while in project user acess groups page
---
.../Users/ProjectUserAccess.tsx | 65 +++++++++++++++----
.../Users/ProjectUserAccessUserList.tsx | 17 ++---
src/pages/ProjectManagerPage/Users/types.ts | 5 ++
3 files changed, 63 insertions(+), 24 deletions(-)
diff --git a/src/pages/ProjectManagerPage/Users/ProjectUserAccess.tsx b/src/pages/ProjectManagerPage/Users/ProjectUserAccess.tsx
index 159e7849a..ec15a95fc 100644
--- a/src/pages/ProjectManagerPage/Users/ProjectUserAccess.tsx
+++ b/src/pages/ProjectManagerPage/Users/ProjectUserAccess.tsx
@@ -2,11 +2,11 @@ import { ProjectNode, UserNode } from '@api/graphql'
import { $Any } from '@types'
import { Button, Toolbar } from '@ynput/ayon-react-components'
import { Splitter, SplitterPanel } from 'primereact/splitter'
-import { useState } from 'react'
+import { useEffect, useMemo, useState } from 'react'
import ProjectUserAccessUserList from './ProjectUserAccessUserList'
import { useGetAccessGroupsQuery } from '@queries/accessGroups/getAccessGroups'
import ProjectUserAccessAssignDialog from './ProjectUserAccessAssignDialog'
-import { SelectedAccessGroupUsers, SelectionStatus } from './types'
+import { HoveredUser, SelectedAccessGroupUsers, SelectionStatus } from './types'
import { useProjectAccessGroupData } from './hooks'
import { toast } from 'react-toastify'
import { useGetUsersQuery } from '@queries/user/getUsers'
@@ -33,6 +33,8 @@ import useUserProjectPermissions, { UserPermissionsEntity } from '@hooks/useUser
import ProjectManagerPageLayout from '../ProjectManagerPageLayout'
import { StyledEmptyPlaceholder, StyledEmptyPlaceholderWrapper } from './ProjectUserAccess.styled'
import useCreateContext from '@hooks/useCreateContext'
+import Shortcuts from '@containers/Shortcuts'
+import { useShortcutsContext } from '@context/shortcutsContext'
const StyledHeader = styled.p`
font-size: 16px;
@@ -79,12 +81,14 @@ const ProjectUserAccess = () => {
const [selectedAccessGroupUsers, setSelectedAccessGroupUsers] = useState<
SelectedAccessGroupUsers | undefined
>()
+ const [hoveredUser, setHoveredUser] = useState()
const { data: projects, isLoading: projectsIsLoading, isError, error } = useListProjectsQuery({})
if (isError) {
console.error(error)
}
+ const { setDisabled } = useShortcutsContext()
const isUser = useSelector((state: $Any) => state.user.data.isUser)
const userPermissions = useUserProjectPermissions(!isUser)
@@ -188,15 +192,39 @@ const ProjectUserAccess = () => {
resetSelectedUsers()
}
+ useEffect(() => {
+ setDisabled(['a+a'])
+ return () => {
+ setDisabled([])
+ }
+ }, [])
+
+ const shortcuts = useMemo(
+ () => [
+ {
+ key: 'a',
+ action: () => {
+ if (!hoveredUser?.user || hoveredUser?.accessGroup !== undefined) {
+ return
+ }
+
+ handleAdd([hoveredUser.user])
+ },
+ },
+ {
+ key: 'r',
+ action: () => {
+ if (!hoveredUser?.user || !hoveredUser?.accessGroup) {
+ return
+ }
+
+ onRemove(hoveredUser!.accessGroup!)([hoveredUser.user])
+ },
+ },
+ ],
+ [hoveredUser],
+ )
- if (!userPermissions?.canViewAny(UserPermissionsEntity.users)) {
- return (
-
- )
- }
const handleProjectSelectionChange = (selection: string[]) => {
if (selection.length <= 1) {
setSelectedProjects(selection)
@@ -204,14 +232,25 @@ const ProjectUserAccess = () => {
}
const filteredSelection = selection.filter((projectName) =>
- userPermissions.canEdit(UserPermissionsEntity.users, projectName),
+ userPermissions?.canEdit(UserPermissionsEntity.users, projectName),
)
setSelectedProjects(filteredSelection)
}
+ if (!userPermissions?.canViewAny(UserPermissionsEntity.users)) {
+ return (
+
+ )
+ }
+
return (
// @ts-ignore
+ {/* @ts-ignore */}
+
{/* @ts-ignore */}
{
readOnly={!hasEditRightsOnProject}
onContextMenu={handleAddContextMenu}
onAdd={handleAdd}
+ onHoverRow={(userName: string) => setHoveredUser({ user: userName })}
onSelectUsers={(selection) => setSelectedAccessGroupUsers({ users: selection })}
sortable
isUnassigned
@@ -322,6 +362,9 @@ const ProjectUserAccess = () => {
mappedUsers[accessGroup] &&
mappedUsers[accessGroup].includes(user.name),
)}
+ onHoverRow={(userName: string) =>
+ setHoveredUser({ accessGroup, user: userName })
+ }
onSelectUsers={(selection: string[]) =>
updateSelectedAccessGroupUsers(accessGroup, selection)
}
diff --git a/src/pages/ProjectManagerPage/Users/ProjectUserAccessUserList.tsx b/src/pages/ProjectManagerPage/Users/ProjectUserAccessUserList.tsx
index fdea7117b..32ddd159f 100644
--- a/src/pages/ProjectManagerPage/Users/ProjectUserAccessUserList.tsx
+++ b/src/pages/ProjectManagerPage/Users/ProjectUserAccessUserList.tsx
@@ -24,6 +24,7 @@ type Props = {
isUnassigned?: boolean
showAddMoreButton?: boolean
onContextMenu?: $Any
+ onHoverRow: $Any
onSelectUsers?: (selectedUsers: string[]) => void
onAdd: (users? : string[]) => void
onRemove?: (users?: string[]) => void
@@ -45,24 +46,13 @@ const ProjectUserAccessUserList = ({
onRemove,
onContextMenu,
onSelectUsers,
+ onHoverRow,
}: Props) => {
const onSelectionChange = (e: $Any) => {
const result = e.value.map((user: UserNode) => user.name)
onSelectUsers!(result)
}
- const handleKeyDown = (event: React.KeyboardEvent) => {
- if (selectedProjects.length === 0) {
- return
- }
-
- if (event.key === 'r') {
- onRemove && onRemove()
- }
- if (event.key === 'a') {
- onAdd()
- }
- }
const selectedUnassignedUsers = tableList.filter((user: $Any) =>
selectedUsers.includes(user.name),
@@ -93,7 +83,8 @@ const ProjectUserAccessUserList = ({
className={clsx('user-list-table', { loading: isLoading })}
rowClassName={(rowData: $Any) => clsx({ inactive: !rowData.active, loading: isLoading })}
onContextMenu={onContextMenu}
- onKeyDown={handleKeyDown}
+ onRowMouseEnter={(e) => onHoverRow(e.data.name)}
+ onRowMouseLeave={() => onHoverRow()}
onSelectionChange={(selection) => {
return onSelectUsers && onSelectionChange(selection)
}}
diff --git a/src/pages/ProjectManagerPage/Users/types.ts b/src/pages/ProjectManagerPage/Users/types.ts
index e6fb52806..fcc3edfd4 100644
--- a/src/pages/ProjectManagerPage/Users/types.ts
+++ b/src/pages/ProjectManagerPage/Users/types.ts
@@ -2,6 +2,11 @@ export type AccessGroupUsers = {
[key: string]: string[]
}
+export type HoveredUser = {
+ accessGroup?: string
+ user: string
+}
+
export type SelectedAccessGroupUsers = {
accessGroup?: string
users: string[]
From 532406031fd8785ce4c6249f1c65b11acce62623 Mon Sep 17 00:00:00 2001
From: Innders <49156310+Innders@users.noreply.github.com>
Date: Thu, 14 Nov 2024 12:45:03 +0000
Subject: [PATCH 33/35] fix: project access overflow issues
---
src/pages/ProjectManagerPage/ProjectManagerPageLayout.jsx | 2 +-
src/pages/ProjectManagerPage/Users/ProjectUserAccess.tsx | 7 +++++--
2 files changed, 6 insertions(+), 3 deletions(-)
diff --git a/src/pages/ProjectManagerPage/ProjectManagerPageLayout.jsx b/src/pages/ProjectManagerPage/ProjectManagerPageLayout.jsx
index c5c0ff14a..a182db80d 100644
--- a/src/pages/ProjectManagerPage/ProjectManagerPageLayout.jsx
+++ b/src/pages/ProjectManagerPage/ProjectManagerPageLayout.jsx
@@ -5,7 +5,7 @@ import { Section, Toolbar } from '@ynput/ayon-react-components'
const ProjectManagerPageLayout = ({ projectList, children, passthrough, toolbar, ...props }) => {
if (passthrough) return children
return (
-
+
{projectList && projectList}
-
+
{
)}
-
+
Access groups
{filteredSelectedProjects.length > 0 ? (
From 6dd3ca87ffa3f1f62784c2e0150c048eb4660247 Mon Sep 17 00:00:00 2001
From: Florin Tudor
Date: Fri, 15 Nov 2024 08:36:58 +0100
Subject: [PATCH 34/35] fix(Editor): Persisting filters, UI/UX tweaks, various
fixes
---
src/containers/projectList.jsx | 9 +-
.../ProjectManagerPage/ProjectManagerPage.jsx | 4 +-
.../ProjectManagerPageLayout.jsx | 3 +-
.../Users/ProjectUserAccess.styled.ts | 1 +
.../Users/ProjectUserAccess.tsx | 85 ++++++++---
.../ProjectUserAccessAssignDialog.styled.js | 5 +-
.../Users/ProjectUserAccessAssignDialog.tsx | 137 +++++++++---------
.../ProjectUserAccessSearchFilterWrapper.tsx | 35 +++--
.../ProjectManagerPage/Users/UserRow.tsx | 5 +-
src/pages/ProjectManagerPage/Users/hooks.ts | 32 +++-
src/pages/ProjectManagerPage/Users/mappers.ts | 56 +++++--
src/pages/SettingsPage/AccessGroups/index.jsx | 1 +
.../UsersSettings/UsersSettings.jsx | 5 +-
.../UserDashboardPage/UserDashboardPage.jsx | 1 +
14 files changed, 243 insertions(+), 136 deletions(-)
diff --git a/src/containers/projectList.jsx b/src/containers/projectList.jsx
index 355dbb8b6..a12a81656 100644
--- a/src/containers/projectList.jsx
+++ b/src/containers/projectList.jsx
@@ -123,6 +123,7 @@ const ProjectList = ({
onSelectAllDisabled,
customSort,
isActiveCallable,
+ hideAddProjectButton = false,
}) => {
const navigate = useAyonNavigate()
const tableRef = useRef(null)
@@ -407,7 +408,7 @@ const ProjectList = ({
disabled={onSelectAllDisabled}
/>
)}
- {(isProjectManager || userPermissions.canCreateProject()) ? (
+ {!hideAddProjectButton && (isProjectManager || userPermissions.canCreateProject()) ? (
{/*
*/}
@@ -416,7 +417,7 @@ const ProjectList = ({
{/*
*/}
- ): null}
+ ) : null}
{isCollapsible && (
@@ -475,7 +476,9 @@ const ProjectList = ({
header="Code"
style={{ maxWidth: 80 }}
body={(rowData) => {
- const isActiveCallableValue = isActiveCallable ? isActiveCallable(rowData.name) : true
+ const isActiveCallableValue = isActiveCallable
+ ? isActiveCallable(rowData.name)
+ : true
return (
{
const bPerm = userPermissions.canView(UserPermissionsEntity.anatomy, b) ? 1 : -1
return bPerm - aPerm
}
- if (module === 'siteSettings') {
+ if (module === 'projectSettings') {
const aPerm = userPermissions.canView(UserPermissionsEntity.settings, a) ? 1 : -1
const bPerm = userPermissions.canView(UserPermissionsEntity.settings, b) ? 1 : -1
return bPerm - aPerm
@@ -176,7 +176,7 @@ const ProjectManagerPage = () => {
if (module === 'anatomy') {
return userPermissions.canView(UserPermissionsEntity.anatomy, projectName)
}
- if (module === 'siteSettings') {
+ if (module === 'projectSettings') {
return userPermissions.canView(UserPermissionsEntity.settings, projectName)
}
return true
diff --git a/src/pages/ProjectManagerPage/ProjectManagerPageLayout.jsx b/src/pages/ProjectManagerPage/ProjectManagerPageLayout.jsx
index a182db80d..7c4769a75 100644
--- a/src/pages/ProjectManagerPage/ProjectManagerPageLayout.jsx
+++ b/src/pages/ProjectManagerPage/ProjectManagerPageLayout.jsx
@@ -2,7 +2,7 @@ import React from 'react'
import PropTypes from 'prop-types'
import { Section, Toolbar } from '@ynput/ayon-react-components'
-const ProjectManagerPageLayout = ({ projectList, children, passthrough, toolbar, ...props }) => {
+const ProjectManagerPageLayout = ({ projectList, children, passthrough, toolbar, sectionStyle, ...props }) => {
if (passthrough) return children
return (
@@ -10,6 +10,7 @@ const ProjectManagerPageLayout = ({ projectList, children, passthrough, toolbar,
{toolbar && {toolbar} }
diff --git a/src/pages/ProjectManagerPage/Users/ProjectUserAccess.styled.ts b/src/pages/ProjectManagerPage/Users/ProjectUserAccess.styled.ts
index 38c1ffd6d..a72eb9afe 100644
--- a/src/pages/ProjectManagerPage/Users/ProjectUserAccess.styled.ts
+++ b/src/pages/ProjectManagerPage/Users/ProjectUserAccess.styled.ts
@@ -27,6 +27,7 @@ export const StyledEmptyPlaceholderWrapper = styled.div`
position: relative;
height: 100%;
+ border-radius: var(--border-radius-m);
background: var(--md-sys-color-surface-container-low);
.header {
diff --git a/src/pages/ProjectManagerPage/Users/ProjectUserAccess.tsx b/src/pages/ProjectManagerPage/Users/ProjectUserAccess.tsx
index 820b22d44..974139b76 100644
--- a/src/pages/ProjectManagerPage/Users/ProjectUserAccess.tsx
+++ b/src/pages/ProjectManagerPage/Users/ProjectUserAccess.tsx
@@ -7,7 +7,7 @@ import ProjectUserAccessUserList from './ProjectUserAccessUserList'
import { useGetAccessGroupsQuery } from '@queries/accessGroups/getAccessGroups'
import ProjectUserAccessAssignDialog from './ProjectUserAccessAssignDialog'
import { HoveredUser, SelectedAccessGroupUsers, SelectionStatus } from './types'
-import { useProjectAccessGroupData } from './hooks'
+import { useProjectAccessGroupData, userPageFilters } from './hooks'
import { toast } from 'react-toastify'
import { useGetUsersQuery } from '@queries/user/getUsers'
import {
@@ -50,11 +50,13 @@ const StyledButton = styled(Button)`
border-radius: var(--border-radius-m);
}
`
+
const ProjectUserAccess = () => {
const { data: accessGroupList = [] } = useGetAccessGroupsQuery({
projectName: '_',
})
+
const {
users: projectUsers,
selectedProjects,
@@ -63,6 +65,8 @@ const ProjectUserAccess = () => {
updateUserAccessGroups,
} = useProjectAccessGroupData()
+ const [filters, setFilters] = userPageFilters()
+
const selfName = useSelector((state: $Any) => state.user.name)
let { data: userList = [], isLoading } = useGetUsersQuery({ selfName })
const activeNonManagerUsers = userList.filter(
@@ -77,7 +81,6 @@ const ProjectUserAccess = () => {
const [actionedUsers, setActionedUsers] = useState([])
const [showDialog, setShowDialog] = useState(false)
- const [filters, setFilters] = useState<$Any>([])
const [selectedAccessGroupUsers, setSelectedAccessGroupUsers] = useState<
SelectedAccessGroupUsers | undefined
>()
@@ -92,7 +95,7 @@ const ProjectUserAccess = () => {
const isUser = useSelector((state: $Any) => state.user.data.isUser)
const userPermissions = useUserProjectPermissions(!isUser)
- const projectFilters = filters.find((filter: Filter) => filter.label === 'Project')
+ const projectFilters = (filters || []).find((filter: Filter) => filter.label === 'Project')
const filteredProjects = getFilteredProjects(
// @ts-ignore Weird one, the response type seems to be mismatched?
(projects || []).filter(
@@ -101,18 +104,18 @@ const ProjectUserAccess = () => {
),
projectFilters,
)
- const filteredSelectedProjects = getFilteredSelectedProjects(selectedProjects, projectFilters)
+ const filteredSelectedProjects = getFilteredSelectedProjects(selectedProjects, filteredProjects)
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 filteredUnassignedUsers = getFilteredUsers(unassignedUsers, userFilter)
+ const selectedUnassignedUsers = getSelectedUsers(selectedAccessGroupUsers, filteredUnassignedUsers)
const hasEditRightsOnProject =
filteredSelectedProjects.length > 0 &&
canAllEditUsers(filteredSelectedProjects, userPermissions)
- const addActionEnabled = hasEditRightsOnProject && selectedUsers.length > 0
- const removeActionEnabled = hasEditRightsOnProject
+ const addActionEnabled = hasEditRightsOnProject && selectedUnassignedUsers.length > 0
+ const removeActionEnabled = hasEditRightsOnProject &&
getSelectedUsers(selectedAccessGroupUsers, [], true).length > 0 &&
selectedAccessGroupUsers?.accessGroup != undefined
@@ -124,7 +127,7 @@ const ProjectUserAccess = () => {
const [ctxMenuShow] = useCreateContext([])
const handleAddContextMenu = (e: $Any) => {
- let actionedUsers = selectedUsers
+ let actionedUsers = selectedUnassignedUsers
if (!actionedUsers.includes(e.data.name)) {
actionedUsers = [e.data.name]
setSelectedAccessGroupUsers({ users: [e.data.name] })
@@ -133,9 +136,24 @@ const ProjectUserAccess = () => {
ctxMenuShow(e.originalEvent, [
{
id: 'add',
- label: 'Add to access groups',
+ icon: 'add',
+ label: 'Add access',
command: () => handleAdd(actionedUsers),
},
+ {
+ id: 'remove',
+ icon: 'remove',
+ label: 'Remove access',
+ disabled: true,
+ command: () => handleAdd(actionedUsers),
+ },
+ {
+ id: 'remove_all',
+ icon: 'remove_moderator',
+ label: 'Remove all access',
+ disabled: true,
+ command: () => onRemove()(actionedUsers),
+ },
])
}
@@ -147,16 +165,35 @@ const ProjectUserAccess = () => {
}
ctxMenuShow(e.originalEvent, [
+ {
+ id: 'add',
+ icon: 'add',
+ label: 'Add access',
+ command: () => handleAdd(actionedUsers),
+ },
{
id: 'remove',
- label: 'Remove from access group',
+ icon: 'remove',
+ label: 'Remove access',
command: () => onRemove(accessGroup)(actionedUsers),
},
+ {
+ id: 'remove',
+ icon: 'remove_moderator',
+ label: 'Remove all access',
+ command: () => onRemove()(actionedUsers),
+ },
])
}
const handleAdd = (users?: string[]) => {
- const actionedUsers = users ? users : selectedUsers
+ const selectedUsers = getSelectedUsers(selectedAccessGroupUsers, filteredNonManagerUsers)
+ // Selection is picked based on access group being set or not. Might be redundant, check later if is necessary
+ const actionedUsers = users
+ ? users
+ : selectedAccessGroupUsers?.accessGroup
+ ? selectedUsers
+ : selectedUnassignedUsers
setActionedUsers(actionedUsers)
if (filteredAccessGroups.length == 1) {
onSave(actionedUsers, [{ name: filteredAccessGroups[0].name, status: SelectionStatus.All }])
@@ -183,7 +220,7 @@ const ProjectUserAccess = () => {
resetSelectedUsers()
}
- const onRemove = (accessGroup: string) => async (users?: string[]) => {
+ const onRemove = (accessGroup?: string) => async (users?: string[]) => {
const userList = users ? users : selectedAccessGroupUsers!.users
for (const user of userList) {
await removeUserAccessGroup(user, accessGroup)
@@ -204,17 +241,16 @@ const ProjectUserAccess = () => {
{
key: 'a',
action: () => {
- if (!hoveredUser?.user || hoveredUser?.accessGroup !== undefined) {
+ if(!selectedAccessGroupUsers?.users || hoveredUser?.user) {
return
}
-
- handleAdd([hoveredUser.user])
+ handleAdd(selectedAccessGroupUsers?.users)
},
},
{
key: 'r',
action: () => {
- if (!hoveredUser?.user || !hoveredUser?.accessGroup) {
+ if (!selectedAccessGroupUsers?.users || !hoveredUser?.accessGroup) {
return
}
@@ -248,13 +284,15 @@ const ProjectUserAccess = () => {
return (
// @ts-ignore
-
+
{/* @ts-ignore */}
-
+
{/* @ts-ignore */}
setFilters(results)}
/>
{
disabled={!removeActionEnabled}
onClick={(e) => {
e.stopPropagation()
- setActionedUsers(selectedUsers)
+ setActionedUsers(selectedUnassignedUsers)
onRemove(selectedAccessGroupUsers!.accessGroup!)()
}}
>
@@ -304,10 +342,10 @@ const ProjectUserAccess = () => {
{filteredSelectedProjects.length > 0 ? (
{
{filteredAccessGroups
.map((item: AccessGroupObject) => item.name)
.map((accessGroup) => {
- const selectedUsers = getAccessGroupUsers(selectedAccessGroupUsers!, accessGroup)
return (
{
>
1}
diff --git a/src/pages/ProjectManagerPage/Users/ProjectUserAccessAssignDialog.styled.js b/src/pages/ProjectManagerPage/Users/ProjectUserAccessAssignDialog.styled.js
index 53302a80e..84c9a4673 100644
--- a/src/pages/ProjectManagerPage/Users/ProjectUserAccessAssignDialog.styled.js
+++ b/src/pages/ProjectManagerPage/Users/ProjectUserAccessAssignDialog.styled.js
@@ -10,7 +10,7 @@ export const ProjectItem = styled.div`
align-self: stretch;
border-radius: var(--border-radius-m);
cursor: pointer;
- min-height: 28px;
+ min-height: 32px;
overflow: hidden;
user-select: none;
@@ -46,4 +46,7 @@ export const List = styled.div`
`
export const Button = styled(BaseButton)`
+&.all-selected {
+ background-color: var(--md-sys-color-primary-container);
+}
`
\ No newline at end of file
diff --git a/src/pages/ProjectManagerPage/Users/ProjectUserAccessAssignDialog.tsx b/src/pages/ProjectManagerPage/Users/ProjectUserAccessAssignDialog.tsx
index a182f8372..9ed1b9875 100644
--- a/src/pages/ProjectManagerPage/Users/ProjectUserAccessAssignDialog.tsx
+++ b/src/pages/ProjectManagerPage/Users/ProjectUserAccessAssignDialog.tsx
@@ -1,9 +1,11 @@
-import { useState } from 'react'
+import { useMemo, useState } from 'react'
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'
import { AccessGroupUsers, SelectionStatus } from './types'
+import Shortcuts from '@containers/Shortcuts'
+import { mapInitialAccessGroupStates } from './mappers'
const icons: {[key in SelectionStatus] : string | undefined} = {
[SelectionStatus.None]: 'add',
@@ -31,34 +33,7 @@ const ProjectUserAccessAssignDialog = ({
onSave,
onClose,
}: Props) => {
- const mapStates = () => {
- 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
- }
-
- const initialStates = mapStates()
+ const initialStates = mapInitialAccessGroupStates(accessGroups, users, userAccessGroups)
const initialStatesList = Object.keys(initialStates).map(agName => ({name: agName, status: initialStates[agName]}))
const [accessGroupItems, setAccessGroupItems] = useState(initialStatesList)
@@ -92,46 +67,72 @@ const ProjectUserAccessAssignDialog = ({
onClose()
}
+ const shortcuts = useMemo(
+ () => [
+ {
+ key: 'ctrl+Enter',
+ action: handleSave,
+ },
+ {
+ key: 'ctrl+a',
+ action: () => handleToggleAll(!allSelected),
+ },
+ ],
+ [allSelected],
+ )
+
return (
-
- handleToggleAll(!allSelected)}
- />
-
- handleSave()} />
- >
- }
- isOpen={true}
- onClose={handleClose}
- >
-
-
- {accessGroupItems.map((item) => (
- {
- toggleAccessGroup(item)
- }}
- >
- {item.name}
- {icons[item.status] !== undefined && }
-
- ))}
-
-
-
+ <>
+ {/* @ts-ignore */}
+
+
+ handleToggleAll(!allSelected)}
+ />
+
+ handleSave()} />
+ >
+ }
+ isOpen={true}
+ onClose={handleClose}
+ >
+
+
+ {accessGroupItems.map((item) => (
+ {
+ if (e.key === 'l' && e.metaKey) {
+ handleToggleAll(!allSelected)
+ }
+ if (e.key == 'Enter' || e.key == ' ') {
+ toggleAccessGroup(item)
+ e.preventDefault()
+ }
+ }}
+ onClick={() => toggleAccessGroup(item)}
+ >
+ {item.name}
+ {icons[item.status] !== undefined && }
+
+ ))}
+
+
+
+ >
)
}
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 = {