Skip to content

Commit

Permalink
[Cases] Assignees enhancements (#144836)
Browse files Browse the repository at this point in the history
WIP

This PR implements some enhancements for the assignees feature that
wasn't completed in 8.5.

Issue: #141057
Fixes: #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


![image](https://user-images.githubusercontent.com/56361221/200646181-9744622f-fe11-41c5-97ac-ce7b777d47a1.png)

#### Case List Page Avatars


![image](https://user-images.githubusercontent.com/56361221/200646269-b637743f-35f1-48d0-91bd-faee32784613.png)


</details>

### Limit assignee selection

Leverage the `limit` prop exposed by the `UserProfilesSelectable` here:
#144618

<details><summary>Adding limit message</summary>


![image](https://user-images.githubusercontent.com/56361221/200653672-9c195031-3117-4ac9-b6e9-98ac11ee170e.png)


</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


![image](https://user-images.githubusercontent.com/56361221/200659972-a6eca466-0d4c-4736-9a2e-62b422f99944.png)

#### All cases filter


![image](https://user-images.githubusercontent.com/56361221/200660181-da13092b-6f6a-4b2d-98cd-325ebf8d75b1.png)


</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>


![image](https://user-images.githubusercontent.com/56361221/200891826-08f15531-3a47-40c1-9cc6-12558b645083.png)


![image](https://user-images.githubusercontent.com/56361221/200892014-92cd3142-15d0-4250-b83e-b32b1c9dd03f.png)


</details>

Co-authored-by: Kibana Machine <[email protected]>
  • Loading branch information
jonathan-buttner and kibanamachine authored Nov 14, 2022
1 parent ec849e5 commit 1e77d8d
Show file tree
Hide file tree
Showing 15 changed files with 373 additions and 150 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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();
});
});
});
Original file line number Diff line number Diff line change
@@ -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);
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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),
[]
);

Expand Down
10 changes: 10 additions & 0 deletions x-pack/plugins/cases/public/components/all_cases/translations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
});
Loading

0 comments on commit 1e77d8d

Please sign in to comment.