From e7724580508abd40adb531ea41f407a1c07af513 Mon Sep 17 00:00:00 2001 From: Matthew Fortunka Date: Tue, 18 Feb 2025 14:09:58 +0000 Subject: [PATCH] Replaced Combo Box with PeoplePicker, reworked assignable users to allow filtering --- api_app/api/routes/workspace_users.py | 4 +- api_app/services/aad_authentication.py | 9 +- .../workspaces/WorkspaceUsersAssignNew.tsx | 131 ++++++++++-------- 3 files changed, 83 insertions(+), 61 deletions(-) diff --git a/api_app/api/routes/workspace_users.py b/api_app/api/routes/workspace_users.py index 37597aad5..d18fd0360 100644 --- a/api_app/api/routes/workspace_users.py +++ b/api_app/api/routes/workspace_users.py @@ -18,9 +18,9 @@ async def get_workspace_users(workspace=Depends(get_workspace_by_id_from_path)) @workspaces_users_admin_router.get("/workspaces/{workspace_id}/assignable-users", response_model=AssignableUsersInResponse, name=strings.API_GET_ASSIGNABLE_USERS) -async def get_assignable_users(workspace=Depends(get_workspace_by_id_from_path)) -> AssignableUsersInResponse: +async def get_assignable_users(filter: str = "", maxResultCount: int = 5, workspace=Depends(get_workspace_by_id_from_path)) -> AssignableUsersInResponse: access_service = get_access_service() - assignable_users = access_service.get_assignable_users() + assignable_users = access_service.get_assignable_users(filter, maxResultCount) return AssignableUsersInResponse(assignable_users=assignable_users) diff --git a/api_app/services/aad_authentication.py b/api_app/services/aad_authentication.py index 8227bdbf3..4ffca5e4f 100644 --- a/api_app/services/aad_authentication.py +++ b/api_app/services/aad_authentication.py @@ -317,11 +317,12 @@ def get_workspace_users(self, workspace: Workspace) -> List[User]: return users_inc_groups - def get_assignable_users(self) -> List[AssignableUser]: + def get_assignable_users(self, filter: str = "", maxResultCount: int = 5) -> List[AssignableUser]: msgraph_token = self._get_msgraph_token() - users_endpoint = f"{MICROSOFT_GRAPH_URL}/v1.0/users/" - graph_data = requests.get(users_endpoint, headers=self._get_auth_header(msgraph_token)).json() - + users_endpoint = f"{MICROSOFT_GRAPH_URL}/v1.0/users?$filter=startswith(displayName,'{filter}')&$top={maxResultCount}" + + graph_data = requests.get(users_endpoint, + headers=self._get_auth_header(msgraph_token)).json() result = [] for user_data in graph_data["value"]: diff --git a/ui/app/src/components/workspaces/WorkspaceUsersAssignNew.tsx b/ui/app/src/components/workspaces/WorkspaceUsersAssignNew.tsx index 67da8073f..86979ba08 100644 --- a/ui/app/src/components/workspaces/WorkspaceUsersAssignNew.tsx +++ b/ui/app/src/components/workspaces/WorkspaceUsersAssignNew.tsx @@ -1,4 +1,10 @@ -import { ComboBox, Dropdown, IComboBoxOption, IDropdownOption, ISelectableOption, Label, Panel, PanelType, PrimaryButton, Spinner, Stack } from "@fluentui/react"; +import * as React from 'react'; +import { Dropdown, IDropdownOption, Label, Panel, PanelType, PrimaryButton, Spinner, Stack } from "@fluentui/react"; +import { IPersonaProps } from '@fluentui/react/lib/Persona'; +import { + IBasePickerSuggestionsProps, + NormalPeoplePicker +} from '@fluentui/react/lib/Pickers'; import { useCallback, useContext, useEffect, useState } from "react"; import { useNavigate } from "react-router-dom"; import { WorkspaceContext } from "../../contexts/WorkspaceContext"; @@ -21,52 +27,71 @@ interface WorkspaceRole { displayName: string; } +const suggestionProps: IBasePickerSuggestionsProps = { + suggestionsHeaderText: 'Suggested Users', + noResultsFoundText: 'No results found', + loadingText: 'Loading', + showRemoveButtons: true, + suggestionsAvailableAlertText: 'People Picker Suggestions available', + suggestionsContainerAriaLabel: 'Suggested contacts', +}; + export const WorkSpaceUsersAssignNew: React.FunctionComponent = (props: WorkspaceUsersAssignProps) => { const workspaceCtx = useContext(WorkspaceContext); const { workspace } = workspaceCtx; const navigate = useNavigate(); const apiCall = useAuthApiCall(); + const picker = React.useRef(null); - const [userOptions, setUserOptions] = useState([]); - const [roleOptions, setRoleOptions] = useState([]); - - const [selectedUser, setSelectedUser] = useState(null); - const [selectedRole, setSelectedRole] = useState(null); - - const [assigning, setAssigning] = useState(false); - const [hasAssignmentError, setHasAssignmentError] = useState(false); - const [assignmentError, setAssignmentError] = useState({} as APIError); - - const onUserChange = (event: any, option: any) => { - setSelectedUser(option ? option.key : null); - }; - - const onRoleChange = (event: any, option: any) => { - setSelectedRole(option ? option.key : null); + const onFilterChanged = async (filter: string): Promise => { + if (filter) { + return filterPersonasByText(filter); + } else { + return []; + } }; - const dismissPanel = useCallback(() => navigate('../'), [navigate]); - - const getAssignableUsers = useCallback(async () => { + const filterPersonasByText = async (filterText: string): Promise => { try { const scopeId = ""; - const response = await apiCall(`${ApiEndpoint.Workspaces}/${workspace.id}/${ApiEndpoint.AssignableUsers}`, HttpMethod.Get, scopeId); + const response = await apiCall(`${ApiEndpoint.Workspaces}/${workspace.id}/${ApiEndpoint.AssignableUsers}?filter=${filterText}`, HttpMethod.Get, scopeId); const assignableUsers = response.assignable_users; - const options: IComboBoxOption[] = assignableUsers.map((assignableUser: AssignableUser) => ({ - key: assignableUser.email, - text: assignableUser.email, - data: { name: assignableUser.name }, + const options: IPersonaProps[] = assignableUsers.map((assignableUser: AssignableUser) => ({ + text: assignableUser.name, + secondaryText: assignableUser.email })); - setUserOptions(options); + return options; } catch (err: any) { err.userMessage = 'Error retrieving assignable users'; } + return []; + }; - }, [apiCall, workspace.id]); + const onChange = (items?: IPersonaProps[] | undefined): void => { + if (items && items.length > 0) { + setSelectedUser(items[0].secondaryText as string); + } + else { + setSelectedUser(null); + } + }; + + const [roleOptions, setRoleOptions] = useState([]); + const [selectedUser, setSelectedUser] = useState(null); + const [selectedRole, setSelectedRole] = useState(null); + const [assigning, setAssigning] = useState(false); + const [hasAssignmentError, setHasAssignmentError] = useState(false); + const [assignmentError, setAssignmentError] = useState({} as APIError); + + const onRoleChange = (event: any, option: any) => { + setSelectedRole(option ? option.key : null); + }; + + const dismissPanel = useCallback(() => navigate('../'), [navigate]); const getWorkspaceRoles = useCallback(async () => { try { @@ -87,14 +112,13 @@ export const WorkSpaceUsersAssignNew: React.FunctionComponent { - getAssignableUsers(); getWorkspaceRoles(); - }, [getAssignableUsers, getWorkspaceRoles]); + }, [getWorkspaceRoles]); const assign = useCallback(async () => { setAssigning(true); - const encodedUser=selectedUser?.replaceAll('#', '%23'); + const encodedUser = selectedUser?.replaceAll('#', '%23'); const scopeId = ""; try { @@ -120,18 +144,6 @@ export const WorkSpaceUsersAssignNew: React.FunctionComponent): JSX.Element | null => { - if (!option) { - return null; - } - return ( -
-
{option.data?.name}
-
{option.text}
-
- ); - }; - return ( - @@ -168,16 +188,17 @@ export const WorkSpaceUsersAssignNew: React.FunctionComponent { assigning && - - - + + + } - { - hasAssignmentError && - } + { + hasAssignmentError && + } ) } +