mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[Cases] Assignees enhancements (#144836)
WIP This PR implements some enhancements for the assignees feature that wasn't completed in 8.5. Issue: https://github.com/elastic/kibana/issues/141057 Fixes: https://github.com/elastic/kibana/issues/140889 ### List sorting The current user is not brought to the front of lists (only in the popovers). Unknown users are still placed at the end of the list. <details><summary>Current user is sorted like other users</summary> #### Case View Page  #### Case List Page Avatars  </details> ### Limit assignee selection Leverage the `limit` prop exposed by the `UserProfilesSelectable` here: https://github.com/elastic/kibana/pull/144618 <details><summary>Adding limit message</summary>  </details> ### Show the selected count Show the selected count even when it is zero so the component doesn't jump around. <details><summary>Selected count</summary> #### View case page  #### All cases filter  </details> ### Expandable assignees column Added a button to expand/collapse the assignee avatars column on the all cases list page <details><summary>Cases list page assignees column</summary>   </details> Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
ec849e5bd8
commit
1e77d8d10d
15 changed files with 373 additions and 150 deletions
|
@ -199,7 +199,6 @@ export const AllCasesList = React.memo<AllCasesListProps>(
|
|||
const { columns } = useCasesColumns({
|
||||
filterStatus: filterOptions.status ?? StatusAll,
|
||||
userProfiles: userProfiles ?? new Map(),
|
||||
currentUserProfile,
|
||||
isSelectorView,
|
||||
connectors,
|
||||
onRowClick,
|
||||
|
|
|
@ -0,0 +1,154 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { screen, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import type { AppMockRenderer } from '../../common/mock';
|
||||
import { createAppMockRenderer } from '../../common/mock';
|
||||
import { userProfiles, userProfilesMap } from '../../containers/user_profiles/api.mock';
|
||||
import type { AssigneesColumnProps } from './assignees_column';
|
||||
import { AssigneesColumn } from './assignees_column';
|
||||
|
||||
describe('AssigneesColumn', () => {
|
||||
const defaultProps: AssigneesColumnProps = {
|
||||
assignees: userProfiles,
|
||||
userProfiles: userProfilesMap,
|
||||
compressedDisplayLimit: 2,
|
||||
};
|
||||
|
||||
let appMockRender: AppMockRenderer;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
appMockRender = createAppMockRenderer();
|
||||
});
|
||||
|
||||
it('renders a long dash if the assignees is an empty array', async () => {
|
||||
const props = {
|
||||
...defaultProps,
|
||||
assignees: [],
|
||||
};
|
||||
|
||||
appMockRender.render(<AssigneesColumn {...props} />);
|
||||
|
||||
expect(
|
||||
screen.queryByTestId('case-table-column-assignee-damaged_raccoon')
|
||||
).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId('case-table-column-expand-button')).not.toBeInTheDocument();
|
||||
// u2014 is the unicode for a long dash
|
||||
expect(screen.getByText('\u2014')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('only renders 2 avatars when the limit is 2', async () => {
|
||||
const props = {
|
||||
...defaultProps,
|
||||
};
|
||||
|
||||
appMockRender.render(<AssigneesColumn {...props} />);
|
||||
|
||||
expect(screen.getByTestId('case-table-column-assignee-damaged_raccoon')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('case-table-column-assignee-physical_dinosaur')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders all 3 avatars when the limit is 5', async () => {
|
||||
const props = {
|
||||
...defaultProps,
|
||||
compressedDisplayLimit: 5,
|
||||
};
|
||||
|
||||
appMockRender.render(<AssigneesColumn {...props} />);
|
||||
|
||||
expect(screen.getByTestId('case-table-column-assignee-damaged_raccoon')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('case-table-column-assignee-physical_dinosaur')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('case-table-column-assignee-wet_dingo')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows the show more avatars button when the limit is 2', async () => {
|
||||
const props = {
|
||||
...defaultProps,
|
||||
compressedDisplayLimit: 2,
|
||||
};
|
||||
|
||||
appMockRender.render(<AssigneesColumn {...props} />);
|
||||
|
||||
expect(screen.getByTestId('case-table-column-expand-button')).toBeInTheDocument();
|
||||
expect(screen.getByText('+1 more')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not show the show more button when the limit is 5', async () => {
|
||||
const props = {
|
||||
...defaultProps,
|
||||
compressedDisplayLimit: 5,
|
||||
};
|
||||
|
||||
appMockRender.render(<AssigneesColumn {...props} />);
|
||||
|
||||
expect(screen.queryByTestId('case-table-column-expand-button')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not show the show more button when the limit is the same number of the assignees', async () => {
|
||||
const props = {
|
||||
...defaultProps,
|
||||
compressedDisplayLimit: userProfiles.length,
|
||||
};
|
||||
|
||||
appMockRender.render(<AssigneesColumn {...props} />);
|
||||
|
||||
expect(screen.queryByTestId('case-table-column-expand-button')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('displays the show less avatars button when the show more is clicked', async () => {
|
||||
const props = {
|
||||
...defaultProps,
|
||||
compressedDisplayLimit: 2,
|
||||
};
|
||||
|
||||
appMockRender.render(<AssigneesColumn {...props} />);
|
||||
|
||||
expect(screen.queryByTestId('case-table-column-assignee-wet_dingo')).not.toBeInTheDocument();
|
||||
|
||||
expect(screen.getByTestId('case-table-column-expand-button')).toBeInTheDocument();
|
||||
expect(screen.getByText('+1 more')).toBeInTheDocument();
|
||||
|
||||
userEvent.click(screen.getByTestId('case-table-column-expand-button'));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('show less')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('case-table-column-assignee-wet_dingo')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('shows more avatars and then hides them when the expand row button is clicked multiple times', async () => {
|
||||
const props = {
|
||||
...defaultProps,
|
||||
compressedDisplayLimit: 2,
|
||||
};
|
||||
|
||||
appMockRender.render(<AssigneesColumn {...props} />);
|
||||
|
||||
expect(screen.queryByTestId('case-table-column-assignee-wet_dingo')).not.toBeInTheDocument();
|
||||
|
||||
expect(screen.getByTestId('case-table-column-expand-button')).toBeInTheDocument();
|
||||
expect(screen.getByText('+1 more')).toBeInTheDocument();
|
||||
|
||||
userEvent.click(screen.getByTestId('case-table-column-expand-button'));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('show less')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('case-table-column-assignee-wet_dingo')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
userEvent.click(screen.getByTestId('case-table-column-expand-button'));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('+1 more')).toBeInTheDocument();
|
||||
expect(screen.queryByTestId('case-table-column-assignee-wet_dingo')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,97 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import type { UserProfileWithAvatar } from '@kbn/user-profile-components';
|
||||
import type { Case } from '../../../common/ui/types';
|
||||
import { getEmptyTagValue } from '../empty_value';
|
||||
import { UserToolTip } from '../user_profiles/user_tooltip';
|
||||
import { useAssignees } from '../../containers/user_profiles/use_assignees';
|
||||
import { getUsernameDataTestSubj } from '../user_profiles/data_test_subject';
|
||||
import { SmallUserAvatar } from '../user_profiles/small_user_avatar';
|
||||
import * as i18n from './translations';
|
||||
|
||||
const COMPRESSED_AVATAR_LIMIT = 3;
|
||||
|
||||
export interface AssigneesColumnProps {
|
||||
assignees: Case['assignees'];
|
||||
userProfiles: Map<string, UserProfileWithAvatar>;
|
||||
compressedDisplayLimit?: number;
|
||||
}
|
||||
|
||||
const AssigneesColumnComponent: React.FC<AssigneesColumnProps> = ({
|
||||
assignees,
|
||||
userProfiles,
|
||||
compressedDisplayLimit = COMPRESSED_AVATAR_LIMIT,
|
||||
}) => {
|
||||
const [isAvatarListExpanded, setIsAvatarListExpanded] = useState<boolean>(false);
|
||||
|
||||
const { allAssignees } = useAssignees({
|
||||
caseAssignees: assignees,
|
||||
userProfiles,
|
||||
});
|
||||
|
||||
const toggleExpandedAvatars = useCallback(
|
||||
() => setIsAvatarListExpanded((prevState) => !prevState),
|
||||
[]
|
||||
);
|
||||
|
||||
const numHiddenAvatars = allAssignees.length - compressedDisplayLimit;
|
||||
const shouldShowExpandListButton = numHiddenAvatars > 0;
|
||||
|
||||
const limitedAvatars = useMemo(
|
||||
() => allAssignees.slice(0, compressedDisplayLimit),
|
||||
[allAssignees, compressedDisplayLimit]
|
||||
);
|
||||
|
||||
const avatarsToDisplay = useMemo(() => {
|
||||
if (isAvatarListExpanded || !shouldShowExpandListButton) {
|
||||
return allAssignees;
|
||||
}
|
||||
|
||||
return limitedAvatars;
|
||||
}, [allAssignees, isAvatarListExpanded, limitedAvatars, shouldShowExpandListButton]);
|
||||
|
||||
if (allAssignees.length <= 0) {
|
||||
return getEmptyTagValue();
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiFlexGroup gutterSize="xs" data-test-subj="case-table-column-assignee" wrap>
|
||||
{avatarsToDisplay.map((assignee) => {
|
||||
const dataTestSubjName = getUsernameDataTestSubj(assignee);
|
||||
return (
|
||||
<EuiFlexItem
|
||||
grow={false}
|
||||
key={assignee.uid}
|
||||
data-test-subj={`case-table-column-assignee-${dataTestSubjName}`}
|
||||
>
|
||||
<UserToolTip userInfo={assignee.profile}>
|
||||
<SmallUserAvatar userInfo={assignee.profile} />
|
||||
</UserToolTip>
|
||||
</EuiFlexItem>
|
||||
);
|
||||
})}
|
||||
|
||||
{shouldShowExpandListButton ? (
|
||||
<EuiButtonEmpty
|
||||
size="xs"
|
||||
data-test-subj="case-table-column-expand-button"
|
||||
onClick={toggleExpandedAvatars}
|
||||
style={{ alignSelf: 'center' }}
|
||||
>
|
||||
{isAvatarListExpanded ? i18n.SHOW_LESS : i18n.SHOW_MORE(numHiddenAvatars)}
|
||||
</EuiButtonEmpty>
|
||||
) : null}
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
||||
|
||||
AssigneesColumnComponent.displayName = 'AssigneesColumn';
|
||||
|
||||
export const AssigneesColumn = React.memo(AssigneesColumnComponent);
|
|
@ -15,7 +15,6 @@ import { useCasesContext } from '../cases_context/use_cases_context';
|
|||
import type { CurrentUserProfile } from '../types';
|
||||
import { EmptyMessage } from '../user_profiles/empty_message';
|
||||
import { NoMatches } from '../user_profiles/no_matches';
|
||||
import { SelectedStatusMessage } from '../user_profiles/selected_status_message';
|
||||
import { bringCurrentUserToFrontAndSort, orderAssigneesIncludingNone } from '../user_profiles/sort';
|
||||
import type { AssigneesFilteringSelection } from '../user_profiles/types';
|
||||
import * as i18n from './translations';
|
||||
|
@ -53,12 +52,7 @@ const AssigneesFilterPopoverComponent: React.FC<AssigneesFilterPopoverProps> = (
|
|||
);
|
||||
|
||||
const selectedStatusMessage = useCallback(
|
||||
(selectedCount: number) => (
|
||||
<SelectedStatusMessage
|
||||
selectedCount={selectedCount}
|
||||
message={i18n.TOTAL_ASSIGNEES_FILTERED(selectedCount)}
|
||||
/>
|
||||
),
|
||||
(selectedCount: number) => i18n.TOTAL_ASSIGNEES_FILTERED(selectedCount),
|
||||
[]
|
||||
);
|
||||
|
||||
|
|
|
@ -137,3 +137,13 @@ export const NO_ASSIGNEES = i18n.translate(
|
|||
defaultMessage: 'No assignees',
|
||||
}
|
||||
);
|
||||
|
||||
export const SHOW_LESS = i18n.translate('xpack.cases.allCasesView.showLessAvatars', {
|
||||
defaultMessage: 'show less',
|
||||
});
|
||||
|
||||
export const SHOW_MORE = (count: number) =>
|
||||
i18n.translate('xpack.cases.allCasesView.showMoreAvatars', {
|
||||
defaultMessage: '+{count} more',
|
||||
values: { count },
|
||||
});
|
||||
|
|
|
@ -18,14 +18,13 @@ import type { AppMockRenderer } from '../../common/mock';
|
|||
import { createAppMockRenderer, readCasesPermissions, TestProviders } from '../../common/mock';
|
||||
import { renderHook } from '@testing-library/react-hooks';
|
||||
import { CaseStatuses } from '../../../common';
|
||||
import { userProfilesMap, userProfiles } from '../../containers/user_profiles/api.mock';
|
||||
import { userProfilesMap } from '../../containers/user_profiles/api.mock';
|
||||
|
||||
describe('useCasesColumns ', () => {
|
||||
let appMockRender: AppMockRenderer;
|
||||
const useCasesColumnsProps: GetCasesColumn = {
|
||||
filterStatus: CaseStatuses.open,
|
||||
userProfiles: userProfilesMap,
|
||||
currentUserProfile: userProfiles[0],
|
||||
isSelectorView: false,
|
||||
showSolutionColumn: true,
|
||||
};
|
||||
|
@ -58,6 +57,7 @@ describe('useCasesColumns ', () => {
|
|||
"field": "assignees",
|
||||
"name": "Assignees",
|
||||
"render": [Function],
|
||||
"width": "180px",
|
||||
},
|
||||
Object {
|
||||
"field": "tags",
|
||||
|
@ -112,6 +112,86 @@ describe('useCasesColumns ', () => {
|
|||
`);
|
||||
});
|
||||
|
||||
it('returns the assignees column without the width specified when in the modal view', async () => {
|
||||
const license = licensingMock.createLicense({
|
||||
license: { type: 'platinum' },
|
||||
});
|
||||
|
||||
appMockRender = createAppMockRenderer({ license });
|
||||
|
||||
const { result } = renderHook(
|
||||
() => useCasesColumns({ ...useCasesColumnsProps, isSelectorView: true }),
|
||||
{
|
||||
wrapper: appMockRender.AppWrapper,
|
||||
}
|
||||
);
|
||||
|
||||
expect(result.current).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"columns": Array [
|
||||
Object {
|
||||
"name": "Name",
|
||||
"render": [Function],
|
||||
"width": "20%",
|
||||
},
|
||||
Object {
|
||||
"field": "assignees",
|
||||
"name": "Assignees",
|
||||
"render": [Function],
|
||||
"width": undefined,
|
||||
},
|
||||
Object {
|
||||
"field": "tags",
|
||||
"name": "Tags",
|
||||
"render": [Function],
|
||||
"width": "15%",
|
||||
},
|
||||
Object {
|
||||
"align": "right",
|
||||
"field": "totalAlerts",
|
||||
"name": "Alerts",
|
||||
"render": [Function],
|
||||
"width": "80px",
|
||||
},
|
||||
Object {
|
||||
"align": "right",
|
||||
"field": "owner",
|
||||
"name": "Solution",
|
||||
"render": [Function],
|
||||
},
|
||||
Object {
|
||||
"align": "right",
|
||||
"field": "totalComment",
|
||||
"name": "Comments",
|
||||
"render": [Function],
|
||||
},
|
||||
Object {
|
||||
"field": "createdAt",
|
||||
"name": "Created on",
|
||||
"render": [Function],
|
||||
"sortable": true,
|
||||
},
|
||||
Object {
|
||||
"name": "External Incident",
|
||||
"render": [Function],
|
||||
},
|
||||
Object {
|
||||
"name": "Status",
|
||||
"render": [Function],
|
||||
},
|
||||
Object {
|
||||
"name": "Severity",
|
||||
"render": [Function],
|
||||
},
|
||||
Object {
|
||||
"align": "right",
|
||||
"render": [Function],
|
||||
},
|
||||
],
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('does not render the solution columns', async () => {
|
||||
const license = licensingMock.createLicense({
|
||||
license: { type: 'platinum' },
|
||||
|
@ -138,6 +218,7 @@ describe('useCasesColumns ', () => {
|
|||
"field": "assignees",
|
||||
"name": "Assignees",
|
||||
"render": [Function],
|
||||
"width": "180px",
|
||||
},
|
||||
Object {
|
||||
"field": "tags",
|
||||
|
@ -209,6 +290,7 @@ describe('useCasesColumns ', () => {
|
|||
"field": "assignees",
|
||||
"name": "Assignees",
|
||||
"render": [Function],
|
||||
"width": "180px",
|
||||
},
|
||||
Object {
|
||||
"field": "tags",
|
||||
|
|
|
@ -43,12 +43,8 @@ import { TruncatedText } from '../truncated_text';
|
|||
import { getConnectorIcon } from '../utils';
|
||||
import type { CasesOwners } from '../../client/helpers/can_use_cases';
|
||||
import { severities } from '../severity/config';
|
||||
import { UserToolTip } from '../user_profiles/user_tooltip';
|
||||
import { useAssignees } from '../../containers/user_profiles/use_assignees';
|
||||
import { getUsernameDataTestSubj } from '../user_profiles/data_test_subject';
|
||||
import type { CurrentUserProfile } from '../types';
|
||||
import { SmallUserAvatar } from '../user_profiles/small_user_avatar';
|
||||
import { useCasesFeatures } from '../../common/use_cases_features';
|
||||
import { AssigneesColumn } from './assignees_column';
|
||||
|
||||
type CasesColumns =
|
||||
| EuiTableActionsColumnType<Case>
|
||||
|
@ -76,47 +72,9 @@ const StyledEuiBadge = euiStyled(EuiBadge)`
|
|||
const renderStringField = (field: string, dataTestSubj: string) =>
|
||||
field != null ? <span data-test-subj={dataTestSubj}>{field}</span> : getEmptyTagValue();
|
||||
|
||||
const AssigneesColumn: React.FC<{
|
||||
assignees: Case['assignees'];
|
||||
userProfiles: Map<string, UserProfileWithAvatar>;
|
||||
currentUserProfile: CurrentUserProfile;
|
||||
}> = ({ assignees, userProfiles, currentUserProfile }) => {
|
||||
const { allAssignees } = useAssignees({
|
||||
caseAssignees: assignees,
|
||||
userProfiles,
|
||||
currentUserProfile,
|
||||
});
|
||||
|
||||
if (allAssignees.length <= 0) {
|
||||
return getEmptyTagValue();
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiFlexGroup gutterSize="none" data-test-subj="case-table-column-assignee" wrap>
|
||||
{allAssignees.map((assignee) => {
|
||||
const dataTestSubjName = getUsernameDataTestSubj(assignee);
|
||||
return (
|
||||
<EuiFlexItem
|
||||
grow={false}
|
||||
key={assignee.uid}
|
||||
data-test-subj={`case-table-column-assignee-${dataTestSubjName}`}
|
||||
>
|
||||
<UserToolTip userInfo={assignee.profile}>
|
||||
<SmallUserAvatar userInfo={assignee.profile} />
|
||||
</UserToolTip>
|
||||
</EuiFlexItem>
|
||||
);
|
||||
})}
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
||||
|
||||
AssigneesColumn.displayName = 'AssigneesColumn';
|
||||
|
||||
export interface GetCasesColumn {
|
||||
filterStatus: string;
|
||||
userProfiles: Map<string, UserProfileWithAvatar>;
|
||||
currentUserProfile: CurrentUserProfile;
|
||||
isSelectorView: boolean;
|
||||
connectors?: ActionConnector[];
|
||||
onRowClick?: (theCase: Case) => void;
|
||||
|
@ -131,7 +89,6 @@ export interface UseCasesColumnsReturnValue {
|
|||
export const useCasesColumns = ({
|
||||
filterStatus,
|
||||
userProfiles,
|
||||
currentUserProfile,
|
||||
isSelectorView,
|
||||
connectors = [],
|
||||
onRowClick,
|
||||
|
@ -184,12 +141,9 @@ export const useCasesColumns = ({
|
|||
field: 'assignees',
|
||||
name: i18n.ASSIGNEES,
|
||||
render: (assignees: Case['assignees']) => (
|
||||
<AssigneesColumn
|
||||
assignees={assignees}
|
||||
userProfiles={userProfiles}
|
||||
currentUserProfile={currentUserProfile}
|
||||
/>
|
||||
<AssigneesColumn assignees={assignees} userProfiles={userProfiles} />
|
||||
),
|
||||
width: !isSelectorView ? '180px' : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -104,7 +104,6 @@ const AssignUsersComponent: React.FC<AssignUsersProps> = ({
|
|||
const { assigneesWithProfiles, assigneesWithoutProfiles, allAssignees } = useAssignees({
|
||||
caseAssignees,
|
||||
userProfiles,
|
||||
currentUserProfile,
|
||||
});
|
||||
|
||||
const [selectedAssignees, setSelectedAssignees] = useState<Assignee[] | undefined>();
|
||||
|
|
|
@ -36,7 +36,7 @@ describe('SuggestUsersPopover', () => {
|
|||
};
|
||||
});
|
||||
|
||||
it.skip('calls onUsersChange when 1 user is selected', async () => {
|
||||
it('calls onUsersChange when 1 user is selected', async () => {
|
||||
const onUsersChange = jest.fn();
|
||||
const props = { ...defaultProps, onUsersChange };
|
||||
appMockRender.render(<SuggestUsersPopover {...props} />);
|
||||
|
@ -182,7 +182,7 @@ describe('SuggestUsersPopover', () => {
|
|||
expect(screen.getByText('1 assigned')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it.skip('shows the 1 assigned total after clicking on a user', async () => {
|
||||
it('shows the 1 assigned total after clicking on a user', async () => {
|
||||
appMockRender.render(<SuggestUsersPopover {...defaultProps} />);
|
||||
|
||||
await waitForEuiPopoverOpen();
|
||||
|
|
|
@ -11,12 +11,12 @@ import { UserProfilesPopover } from '@kbn/user-profile-components';
|
|||
|
||||
import { EuiButtonIcon, EuiToolTip } from '@elastic/eui';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { MAX_ASSIGNEES_PER_CASE } from '../../../../common/constants';
|
||||
import { useSuggestUserProfiles } from '../../../containers/user_profiles/use_suggest_user_profiles';
|
||||
import { useCasesContext } from '../../cases_context/use_cases_context';
|
||||
import type { AssigneeWithProfile } from '../../user_profiles/types';
|
||||
import * as i18n from '../translations';
|
||||
import { bringCurrentUserToFrontAndSort } from '../../user_profiles/sort';
|
||||
import { SelectedStatusMessage } from '../../user_profiles/selected_status_message';
|
||||
import { EmptyMessage } from '../../user_profiles/empty_message';
|
||||
import { NoMatches } from '../../user_profiles/no_matches';
|
||||
import type { CurrentUserProfile } from '../../types';
|
||||
|
@ -79,12 +79,12 @@ const SuggestUsersPopoverComponent: React.FC<SuggestUsersPopoverProps> = ({
|
|||
);
|
||||
|
||||
const selectedStatusMessage = useCallback(
|
||||
(selectedCount: number) => (
|
||||
<SelectedStatusMessage
|
||||
selectedCount={selectedCount}
|
||||
message={i18n.TOTAL_USERS_ASSIGNED(selectedCount)}
|
||||
/>
|
||||
),
|
||||
(selectedCount: number) => i18n.TOTAL_USERS_ASSIGNED(selectedCount),
|
||||
[]
|
||||
);
|
||||
|
||||
const limitReachedMessage = useCallback(
|
||||
(limit: number) => i18n.MAX_SELECTED_ASSIGNEES(limit),
|
||||
[]
|
||||
);
|
||||
|
||||
|
@ -131,6 +131,8 @@ const SuggestUsersPopoverComponent: React.FC<SuggestUsersPopoverProps> = ({
|
|||
selectedOptions: selectedUsers ?? selectedProfiles,
|
||||
isLoading: isLoadingData,
|
||||
height: 'full',
|
||||
limit: MAX_ASSIGNEES_PER_CASE,
|
||||
limitReachedMessage,
|
||||
searchPlaceholder: i18n.SEARCH_USERS,
|
||||
clearButtonLabel: i18n.REMOVE_ASSIGNEES,
|
||||
emptyMessage: <EmptyMessage />,
|
||||
|
|
|
@ -1,24 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { SelectedStatusMessage } from './selected_status_message';
|
||||
|
||||
describe('SelectedStatusMessage', () => {
|
||||
it('does not render if the count is 0', () => {
|
||||
const { container } = render(<SelectedStatusMessage selectedCount={0} message={'hello'} />);
|
||||
|
||||
expect(container.firstChild).toBeNull();
|
||||
expect(screen.queryByText('hello')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders the message when the count is great than 0', () => {
|
||||
render(<SelectedStatusMessage selectedCount={1} message={'hello'} />);
|
||||
|
||||
expect(screen.getByText('hello')).toBeInTheDocument();
|
||||
});
|
||||
});
|
|
@ -1,22 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
const SelectedStatusMessageComponent: React.FC<{
|
||||
selectedCount: number;
|
||||
message: string;
|
||||
}> = ({ selectedCount, message }) => {
|
||||
if (selectedCount <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <>{message}</>;
|
||||
};
|
||||
SelectedStatusMessageComponent.displayName = 'SelectedStatusMessage';
|
||||
|
||||
export const SelectedStatusMessage = React.memo(SelectedStatusMessageComponent);
|
|
@ -65,3 +65,10 @@ export const INVALID_ASSIGNEES = i18n.translate('xpack.cases.create.invalidAssig
|
|||
maxAssignees: MAX_ASSIGNEES_PER_CASE,
|
||||
},
|
||||
});
|
||||
|
||||
export const MAX_SELECTED_ASSIGNEES = (limit: number) =>
|
||||
i18n.translate('xpack.cases.userProfile.maxSelectedAssignees', {
|
||||
defaultMessage:
|
||||
"You've selected the maximum number of {count, plural, one {# assignee} other {# assignees}}",
|
||||
values: { count: limit },
|
||||
});
|
||||
|
|
|
@ -13,7 +13,7 @@ import { useAssignees } from './use_assignees';
|
|||
describe('useAssignees', () => {
|
||||
it('returns an empty array when the caseAssignees is empty', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useAssignees({ caseAssignees: [], userProfiles: new Map(), currentUserProfile: undefined })
|
||||
useAssignees({ caseAssignees: [], userProfiles: new Map() })
|
||||
);
|
||||
|
||||
expect(result.current.allAssignees).toHaveLength(0);
|
||||
|
@ -26,7 +26,6 @@ describe('useAssignees', () => {
|
|||
useAssignees({
|
||||
caseAssignees: userProfiles.map((profile) => ({ uid: profile.uid })),
|
||||
userProfiles: userProfilesMap,
|
||||
currentUserProfile: undefined,
|
||||
})
|
||||
);
|
||||
|
||||
|
@ -41,7 +40,6 @@ describe('useAssignees', () => {
|
|||
useAssignees({
|
||||
caseAssignees: unsorted.map((profile) => ({ uid: profile.uid })),
|
||||
userProfiles: userProfilesMap,
|
||||
currentUserProfile: undefined,
|
||||
})
|
||||
);
|
||||
|
||||
|
@ -56,7 +54,6 @@ describe('useAssignees', () => {
|
|||
useAssignees({
|
||||
caseAssignees: unknownProfiles,
|
||||
userProfiles: userProfilesMap,
|
||||
currentUserProfile: undefined,
|
||||
})
|
||||
);
|
||||
|
||||
|
@ -71,7 +68,6 @@ describe('useAssignees', () => {
|
|||
useAssignees({
|
||||
caseAssignees: assignees,
|
||||
userProfiles: userProfilesMap,
|
||||
currentUserProfile: undefined,
|
||||
})
|
||||
);
|
||||
|
||||
|
@ -86,28 +82,6 @@ describe('useAssignees', () => {
|
|||
{ uid: '1' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('returns assignees with profiles with the current user at the front', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useAssignees({
|
||||
caseAssignees: userProfiles,
|
||||
userProfiles: userProfilesMap,
|
||||
currentUserProfile: userProfiles[2],
|
||||
})
|
||||
);
|
||||
|
||||
expect(result.current.assigneesWithProfiles).toHaveLength(3);
|
||||
expect(result.current.allAssignees).toHaveLength(3);
|
||||
|
||||
const asAssignees = userProfiles.map(asAssigneeWithProfile);
|
||||
|
||||
expect(result.current.assigneesWithProfiles).toEqual([
|
||||
asAssignees[2],
|
||||
asAssignees[0],
|
||||
asAssignees[1],
|
||||
]);
|
||||
expect(result.current.allAssignees).toEqual([asAssignees[2], asAssignees[0], asAssignees[1]]);
|
||||
});
|
||||
});
|
||||
|
||||
const asAssigneeWithProfile = (profile: UserProfileWithAvatar) => ({ uid: profile.uid, profile });
|
||||
|
|
|
@ -8,8 +8,7 @@
|
|||
import type { UserProfileWithAvatar } from '@kbn/user-profile-components';
|
||||
import { useMemo } from 'react';
|
||||
import type { CaseAssignees } from '../../../common/api';
|
||||
import type { CurrentUserProfile } from '../../components/types';
|
||||
import { bringCurrentUserToFrontAndSort } from '../../components/user_profiles/sort';
|
||||
import { sortProfiles } from '../../components/user_profiles/sort';
|
||||
import type { Assignee, AssigneeWithProfile } from '../../components/user_profiles/types';
|
||||
|
||||
interface PartitionedAssignees {
|
||||
|
@ -20,11 +19,9 @@ interface PartitionedAssignees {
|
|||
export const useAssignees = ({
|
||||
caseAssignees,
|
||||
userProfiles,
|
||||
currentUserProfile,
|
||||
}: {
|
||||
caseAssignees: CaseAssignees;
|
||||
userProfiles: Map<string, UserProfileWithAvatar>;
|
||||
currentUserProfile: CurrentUserProfile;
|
||||
}): {
|
||||
assigneesWithProfiles: AssigneeWithProfile[];
|
||||
assigneesWithoutProfiles: Assignee[];
|
||||
|
@ -46,14 +43,14 @@ export const useAssignees = ({
|
|||
{ usersWithProfiles: [], usersWithoutProfiles: [] }
|
||||
);
|
||||
|
||||
const orderedProf = bringCurrentUserToFrontAndSort(currentUserProfile, usersWithProfiles);
|
||||
const orderedProf = sortProfiles(usersWithProfiles);
|
||||
|
||||
const assigneesWithProfile2 = orderedProf?.map((profile) => ({ uid: profile.uid, profile }));
|
||||
const withProfiles = orderedProf?.map((profile) => ({ uid: profile.uid, profile }));
|
||||
return {
|
||||
assigneesWithProfiles: assigneesWithProfile2 ?? [],
|
||||
assigneesWithProfiles: withProfiles ?? [],
|
||||
assigneesWithoutProfiles: usersWithoutProfiles,
|
||||
};
|
||||
}, [caseAssignees, currentUserProfile, userProfiles]);
|
||||
}, [caseAssignees, userProfiles]);
|
||||
|
||||
const allAssignees = useMemo(
|
||||
() => [...assigneesWithProfiles, ...assigneesWithoutProfiles],
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue