diff --git a/x-pack/plugins/cases/public/components/actions/assignees/edit_assignees_flyout.tsx b/x-pack/plugins/cases/public/components/actions/assignees/edit_assignees_flyout.tsx index 998a9eb05bc2a..2364cb6daff99 100644 --- a/x-pack/plugins/cases/public/components/actions/assignees/edit_assignees_flyout.tsx +++ b/x-pack/plugins/cases/public/components/actions/assignees/edit_assignees_flyout.tsx @@ -22,6 +22,7 @@ import { EuiTitle, } from '@elastic/eui'; +import type { UserProfileWithAvatar } from '@kbn/user-profile-components'; import { useBulkGetUserProfiles } from '../../../containers/user_profiles/use_bulk_get_user_profiles'; import type { Case } from '../../../../common'; import { EditAssigneesSelectable } from './edit_assignees_selectable'; @@ -42,6 +43,25 @@ const fullHeight = css` } `; +const getUnknownUsers = ( + assignees: Set, + userProfiles?: Map +) => { + const unknownUsers: string[] = []; + + if (!userProfiles) { + return unknownUsers; + } + + for (const assignee of assignees) { + if (!userProfiles.has(assignee)) { + unknownUsers.push(assignee); + } + } + + return unknownUsers; +}; + const EditAssigneesFlyoutComponent: React.FC = ({ selectedCases, onClose, @@ -56,6 +76,8 @@ const EditAssigneesFlyoutComponent: React.FC = ({ uids: Array.from(assignees.values()), }); + const unknownUsers = getUnknownUsers(assignees, userProfiles); + const [assigneesSelection, setAssigneesSelection] = useState({ selectedItems: [], unSelectedItems: [], @@ -95,6 +117,7 @@ const EditAssigneesFlyoutComponent: React.FC = ({ isLoading={isLoadingUserProfiles} userProfiles={userProfiles ?? new Map()} onChangeAssignees={setAssigneesSelection} + unknownUsers={unknownUsers} /> )} diff --git a/x-pack/plugins/cases/public/components/actions/assignees/edit_assignees_selectable.test.tsx b/x-pack/plugins/cases/public/components/actions/assignees/edit_assignees_selectable.test.tsx index 89dc5fa73bc71..45f91005d0754 100644 --- a/x-pack/plugins/cases/public/components/actions/assignees/edit_assignees_selectable.test.tsx +++ b/x-pack/plugins/cases/public/components/actions/assignees/edit_assignees_selectable.test.tsx @@ -29,6 +29,7 @@ describe('EditAssigneesSelectable', () => { isLoading: false, userProfiles: userProfilesMap, onChangeAssignees: jest.fn(), + unknownUsers: [], }; /** @@ -43,6 +44,7 @@ describe('EditAssigneesSelectable', () => { ], isLoading: false, userProfiles: userProfilesMap, + unknownUsers: [], onChangeAssignees: jest.fn(), }; @@ -361,4 +363,51 @@ describe('EditAssigneesSelectable', () => { await waitForComponentToUpdate(); }); + + it('shows unknown users', async () => { + const result = appMock.render(); + + await waitFor(() => { + expect(result.getByText('Unknown')).toBeInTheDocument(); + }); + + await waitForComponentToUpdate(); + }); + + it('selects unknown users', async () => { + const result = appMock.render(); + + await waitFor(() => { + expect(result.getByText('Unknown')).toBeInTheDocument(); + }); + + userEvent.click(result.getByText('Unknown')); + + expect(props.onChangeAssignees).toBeCalledWith({ + selectedItems: ['u_J41Oh6L9ki-Vo2tOogS8WRTENzhHurGtRc87NgEAlkc_0', '123'], + unSelectedItems: [], + }); + + await waitForComponentToUpdate(); + }); + + it('deselects unknown users', async () => { + const selectedCases = [{ ...basicCase, assignees: [{ uid: '123' }] }]; + const result = appMock.render( + + ); + + await waitFor(() => { + expect(result.getByText('Unknown')).toBeInTheDocument(); + }); + + userEvent.click(result.getByText('Unknown')); + + expect(props.onChangeAssignees).toBeCalledWith({ + selectedItems: [], + unSelectedItems: ['123'], + }); + + await waitForComponentToUpdate(); + }); }); diff --git a/x-pack/plugins/cases/public/components/actions/assignees/edit_assignees_selectable.tsx b/x-pack/plugins/cases/public/components/actions/assignees/edit_assignees_selectable.tsx index b68f279570ad6..f698efa08980a 100644 --- a/x-pack/plugins/cases/public/components/actions/assignees/edit_assignees_selectable.tsx +++ b/x-pack/plugins/cases/public/components/actions/assignees/edit_assignees_selectable.tsx @@ -21,7 +21,7 @@ import { } from '@elastic/eui'; import type { UserProfileWithAvatar } from '@kbn/user-profile-components'; -import { UserAvatar, getUserDisplayName } from '@kbn/user-profile-components'; +import { getUserDisplayName } from '@kbn/user-profile-components'; import { useIsUserTyping } from '../../../common/use_is_user_typing'; import { useSuggestUserProfiles } from '../../../containers/user_profiles/use_suggest_user_profiles'; import type { Case } from '../../../../common'; @@ -31,27 +31,32 @@ import type { ItemSelectableOption, ItemsSelectionState } from '../types'; import { useCasesContext } from '../../cases_context/use_cases_context'; import { EmptyMessage } from '../../user_profiles/empty_message'; import { NoMatches } from '../../user_profiles/no_matches'; +import { SmallUserAvatar } from '../../user_profiles/small_user_avatar'; interface Props { selectedCases: Case[]; userProfiles: Map; isLoading: boolean; + unknownUsers: string[]; onChangeAssignees: (args: ItemsSelectionState) => void; } -type AssigneeSelectableOption = ItemSelectableOption>; +type AssigneeSelectableOption = ItemSelectableOption< + Partial & { unknownUser?: boolean } +>; const EditAssigneesSelectableComponent: React.FC = ({ selectedCases, userProfiles, isLoading, + unknownUsers, onChangeAssignees, }) => { const { owner: owners } = useCasesContext(); const { euiTheme } = useEuiTheme(); const { isUserTyping, onContentChange, onDebounce } = useIsUserTyping(); - // TODO: Include unknown users - const userProfileIds = [...userProfiles.keys()]; + + const userProfileIds = [...userProfiles.keys(), ...unknownUsers]; const [searchValue, setSearchValue] = useState(''); const { data: searchResultUserProfiles, isLoading: isLoadingSuggest } = useSuggestUserProfiles({ @@ -62,13 +67,12 @@ const EditAssigneesSelectableComponent: React.FC = ({ const itemToSelectableOption = useCallback( (item: { key: string; data: Record }): AssigneeSelectableOption => { - // TODO: Fix types - const userProfileFromData = item.data as unknown as UserProfileWithAvatar; + const userProfileFromData = item.data as unknown as Partial; const userProfile = isEmpty(userProfileFromData) ? userProfiles.get(item.key) : userProfileFromData; - if (userProfile) { + if (isUserProfile(userProfile)) { return toSelectableOption(userProfile); } @@ -80,12 +84,12 @@ const EditAssigneesSelectableComponent: React.FC = ({ return toSelectableOption(profileInSuggestedUsers); } - // TODO: Put unknown label return { key: item.key, - label: item.key, + label: i18n.UNKNOWN, + data: { unknownUser: true }, 'data-test-subj': `cases-actions-assignees-edit-selectable-assignee-${item.key}`, - } as AssigneeSelectableOption; + } as unknown as AssigneeSelectableOption; }, [searchResultUserProfiles, userProfiles] ); @@ -111,10 +115,7 @@ const EditAssigneesSelectableComponent: React.FC = ({ (option: AssigneeSelectableOption, search: string) => { const icon = option.itemIcon ?? 'empty'; const dataTestSubj = `cases-actions-assignees-edit-selectable-assignee-${option.key}-icon-${icon}`; - - if (!option.user) { - return {option.label}; - } + const userInfo = option.user ? { user: option.user, data: option.data } : undefined; return ( @@ -129,7 +130,7 @@ const EditAssigneesSelectableComponent: React.FC = ({ - + @@ -142,7 +143,7 @@ const EditAssigneesSelectableComponent: React.FC = ({ {option.label} - {option.user.email && option.user.email !== option.label ? ( + {option.user?.email && option.user?.email !== option.label ? ( {searchValue ? ( @@ -250,11 +251,16 @@ const getDisplayOptions = ({ /** * In the initial view, when the user does not perform any search, * we want to filter out options that are not in the initial user profile - * mapping or profiles returned by the search result that are not selected + * mapping or profiles returned by the search result that are not selected. + * We want to keep unknown users as they can only be available from the + * selected cases and not from search results */ const filteredOptions = isEmpty(searchValue) ? options.filter( - (option) => initialUserProfiles.has(option?.data?.uid) || option?.data?.itemIcon !== 'empty' + (option) => + initialUserProfiles.has(option?.data?.uid) || + option?.data?.itemIcon !== 'empty' || + option.data?.unknownUser ) : [...options]; @@ -285,3 +291,7 @@ const isMatchingOption =