Skip to content

Commit

Permalink
Replaced Combo Box with PeoplePicker, reworked assignable users to al…
Browse files Browse the repository at this point in the history
…low filtering
  • Loading branch information
Matthew Fortunka committed Feb 18, 2025
1 parent 7817520 commit e772458
Show file tree
Hide file tree
Showing 3 changed files with 83 additions and 61 deletions.
4 changes: 2 additions & 2 deletions api_app/api/routes/workspace_users.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)


Expand Down
9 changes: 5 additions & 4 deletions api_app/services/aad_authentication.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"]:
Expand Down
131 changes: 76 additions & 55 deletions ui/app/src/components/workspaces/WorkspaceUsersAssignNew.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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<WorkspaceUsersAssignProps> = (props: WorkspaceUsersAssignProps) => {
const workspaceCtx = useContext(WorkspaceContext);
const { workspace } = workspaceCtx;

const navigate = useNavigate();
const apiCall = useAuthApiCall();
const picker = React.useRef(null);

const [userOptions, setUserOptions] = useState<IComboBoxOption[]>([]);
const [roleOptions, setRoleOptions] = useState<IDropdownOption[]>([]);

const [selectedUser, setSelectedUser] = useState<string | null>(null);
const [selectedRole, setSelectedRole] = useState<string | null>(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<IPersonaProps[]> => {
if (filter) {
return filterPersonasByText(filter);
} else {
return [];
}
};

const dismissPanel = useCallback(() => navigate('../'), [navigate]);

const getAssignableUsers = useCallback(async () => {
const filterPersonasByText = async (filterText: string): Promise<IPersonaProps[]> => {
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<IDropdownOption[]>([]);
const [selectedUser, setSelectedUser] = useState<string | null>(null);
const [selectedRole, setSelectedRole] = useState<string | null>(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 {
Expand All @@ -87,14 +112,13 @@ export const WorkSpaceUsersAssignNew: React.FunctionComponent<WorkspaceUsersAssi
}, [apiCall, workspace.id]);

useEffect(() => {
getAssignableUsers();
getWorkspaceRoles();
}, [getAssignableUsers, getWorkspaceRoles]);
}, [getWorkspaceRoles]);

const assign = useCallback(async () => {
setAssigning(true);

const encodedUser=selectedUser?.replaceAll('#', '%23');
const encodedUser = selectedUser?.replaceAll('#', '%23');

const scopeId = "";
try {
Expand All @@ -120,18 +144,6 @@ export const WorkSpaceUsersAssignNew: React.FunctionComponent<WorkspaceUsersAssi
return footer;
}, [selectedUser, selectedRole, assign, assigning]);

const onRenderOption = (option?: ISelectableOption<any>): JSX.Element | null => {
if (!option) {
return null;
}
return (
<div style={{ padding: '8px 0' }}>
<div style={{ fontWeight: 'bold' }}>{option.data?.name}</div>
<div>{option.text}</div>
</div>
);
};

return (
<Panel
headerText="Assign user to a role"
Expand All @@ -147,13 +159,21 @@ export const WorkSpaceUsersAssignNew: React.FunctionComponent<WorkspaceUsersAssi
<Stack tokens={{ childrenGap: 20 }} styles={{ root: { paddingTop: 20 } }}>
<Stack tokens={{ childrenGap: 10 }} verticalAlign="center">
<Label>User</Label>
<ComboBox
placeholder="Enter a user's email address"
options={userOptions}
styles={{ root: { width: '100%' } }}
disabled={assigning}
onChange={onUserChange}
onRenderOption={onRenderOption}
<NormalPeoplePicker
onResolveSuggestions={onFilterChanged}
pickerSuggestionsProps={suggestionProps}
className={'ms-PeoplePicker'}
key={'normal'}
selectionAriaLabel={'Selected user'}
removeButtonAriaLabel={'Remove'}
inputProps={{
'aria-label': 'User Picker',
}}
componentRef={picker}
resolveDelay={300}
required={true}
itemLimit={1}
onChange={onChange}
/>
</Stack>
<Stack tokens={{ childrenGap: 10 }} verticalAlign="center">
Expand All @@ -168,16 +188,17 @@ export const WorkSpaceUsersAssignNew: React.FunctionComponent<WorkspaceUsersAssi
</Stack>
{
assigning && <Stack>
<Stack.Item style={{ paddingTop: '10px', paddingBottom: '10px' }}>
<Spinner />
</Stack.Item>
<Stack.Item style={{ paddingTop: '10px', paddingBottom: '10px' }}>
<Spinner />
</Stack.Item>
</Stack>
}
{
hasAssignmentError && <ExceptionLayout e={assignmentError} />
}
{
hasAssignmentError && <ExceptionLayout e={assignmentError} />
}
</Stack>
</Panel>

)
}

0 comments on commit e772458

Please sign in to comment.