Merge pull request #9182 from keithjgrant/6189-hosts-users-teams-tables

Convert Hosts/Users/Teams list to tables

Reviewed-by: https://github.com/apps/softwarefactory-project-zuul
This commit is contained in:
softwarefactory-project-zuul[bot] 2021-02-09 13:16:59 +00:00 committed by GitHub
commit 2c9ef3bae6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 375 additions and 378 deletions

View File

@ -8,10 +8,14 @@ import { HostsAPI } from '../../../api';
import AlertModal from '../../../components/AlertModal';
import DataListToolbar from '../../../components/DataListToolbar';
import ErrorDetail from '../../../components/ErrorDetail';
import PaginatedDataList, {
import {
ToolbarAddButton,
ToolbarDeleteButton,
} from '../../../components/PaginatedDataList';
import PaginatedTable, {
HeaderRow,
HeaderCell,
} from '../../../components/PaginatedTable';
import useRequest, { useDeleteItems } from '../../../util/useRequest';
import {
encodeQueryString,
@ -130,7 +134,7 @@ function HostList({ i18n }) {
return (
<PageSection>
<Card>
<PaginatedDataList
<PaginatedTable
contentError={contentError}
hasContentLoading={isLoading || isDeleteLoading}
items={hosts}
@ -157,14 +161,15 @@ function HostList({ i18n }) {
key: 'modified_by__username__icontains',
},
]}
toolbarSortColumns={[
{
name: i18n._(t`Name`),
key: 'name',
},
]}
toolbarSearchableKeys={searchableKeys}
toolbarRelatedSearchableKeys={relatedSearchableKeys}
headerRow={
<HeaderRow qsConfig={QS_CONFIG}>
<HeaderCell sortKey="name">{i18n._(t`Name`)}</HeaderCell>
<HeaderCell>{i18n._(t`Inventory`)}</HeaderCell>
<HeaderCell>{i18n._(t`Actions`)}</HeaderCell>
</HeaderRow>
}
renderToolbar={props => (
<DataListToolbar
{...props}
@ -193,13 +198,14 @@ function HostList({ i18n }) {
]}
/>
)}
renderItem={host => (
renderRow={(host, index) => (
<HostListItem
key={host.id}
host={host}
detailUrl={`${match.url}/${host.id}/details`}
isSelected={selected.some(row => row.id === host.id)}
onSelect={() => handleSelect(host)}
rowIndex={index}
/>
)}
emptyStateControls={

View File

@ -134,8 +134,9 @@ describe('<HostList />', () => {
act(() => {
wrapper
.find('input#select-host-1')
.closest('DataListCheck')
.find('.pf-c-table__check')
.first()
.find('input')
.invoke('onChange')();
});
wrapper.update();
@ -147,8 +148,9 @@ describe('<HostList />', () => {
).toEqual(true);
act(() => {
wrapper
.find('input#select-host-1')
.closest('DataListCheck')
.find('.pf-c-table__check')
.first()
.find('input')
.invoke('onChange')();
});
wrapper.update();

View File

@ -1,91 +1,67 @@
import 'styled-components/macro';
import React, { Fragment } from 'react';
import React from 'react';
import { string, bool, func } from 'prop-types';
import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
import {
Button,
DataListAction as _DataListAction,
DataListCheck,
DataListItem,
DataListItemRow,
DataListItemCells,
Tooltip,
} from '@patternfly/react-core';
import { Button } from '@patternfly/react-core';
import { Tr, Td } from '@patternfly/react-table';
import { Link } from 'react-router-dom';
import { PencilAltIcon } from '@patternfly/react-icons';
import styled from 'styled-components';
import DataListCell from '../../../components/DataListCell';
import Sparkline from '../../../components/Sparkline';
import { ActionsTd, ActionItem } from '../../../components/PaginatedTable';
import { Host } from '../../../types';
import HostToggle from '../../../components/HostToggle';
const DataListAction = styled(_DataListAction)`
align-items: center;
display: grid;
grid-gap: 24px;
grid-template-columns: 92px 40px;
`;
function HostListItem({ i18n, host, isSelected, onSelect, detailUrl }) {
function HostListItem({
i18n,
host,
isSelected,
onSelect,
detailUrl,
rowIndex,
}) {
const labelId = `check-action-${host.id}`;
return (
<DataListItem key={host.id} aria-labelledby={labelId} id={`${host.id}`}>
<DataListItemRow>
<DataListCheck
id={`select-host-${host.id}`}
checked={isSelected}
onChange={onSelect}
aria-labelledby={labelId}
/>
<DataListItemCells
dataListCells={[
<DataListCell key="name">
<Link to={`${detailUrl}`}>
<b>{host.name}</b>
</Link>
</DataListCell>,
<DataListCell key="recentJobs">
<Sparkline jobs={host.summary_fields.recent_jobs} />
</DataListCell>,
<DataListCell key="inventory">
{host.summary_fields.inventory && (
<Fragment>
<b css="margin-right: 24px">{i18n._(t`Inventory`)}</b>
<Link
to={`/inventories/inventory/${host.summary_fields.inventory.id}/details`}
>
{host.summary_fields.inventory.name}
</Link>
</Fragment>
)}
</DataListCell>,
]}
/>
<DataListAction
aria-label={i18n._(t`actions`)}
aria-labelledby={labelId}
id={labelId}
<Tr id={`host-row-${host.id}`}>
<Td
select={{
rowIndex,
isSelected,
onSelect,
}}
dataLabel={i18n._(t`Selected`)}
/>
<Td id={labelId} dataLabel={i18n._(t`Name`)}>
<Link to={`${detailUrl}`}>
<b>{host.name}</b>
</Link>
</Td>
<Td dataLabel={i18n._(t`Inventory`)}>
{host.summary_fields.inventory && (
<Link
to={`/inventories/inventory/${host.summary_fields.inventory.id}/details`}
>
{host.summary_fields.inventory.name}
</Link>
)}
</Td>
<ActionsTd dataLabel={i18n._(t`Actions`)} gridColumns="auto 40px">
<HostToggle host={host} />
<ActionItem
visible={host.summary_fields.user_capabilities.edit}
tooltip={i18n._(t`Edit Host`)}
>
<HostToggle host={host} />
{host.summary_fields.user_capabilities.edit ? (
<Tooltip content={i18n._(t`Edit Host`)} position="top">
<Button
aria-label={i18n._(t`Edit Host`)}
variant="plain"
component={Link}
to={`/hosts/${host.id}/edit`}
>
<PencilAltIcon />
</Button>
</Tooltip>
) : (
''
)}
</DataListAction>
</DataListItemRow>
</DataListItem>
<Button
aria-label={i18n._(t`Edit Host`)}
variant="plain"
component={Link}
to={`/hosts/${host.id}/edit`}
>
<PencilAltIcon />
</Button>
</ActionItem>
</ActionsTd>
</Tr>
);
}

View File

@ -25,12 +25,16 @@ describe('<HostsListItem />', () => {
beforeEach(() => {
wrapper = mountWithContexts(
<HostsListItem
isSelected={false}
detailUrl="/host/1"
onSelect={() => {}}
host={mockHost}
/>
<table>
<tbody>
<HostsListItem
isSelected={false}
detailUrl="/host/1"
onSelect={() => {}}
host={mockHost}
/>
</tbody>
</table>
);
});
@ -46,12 +50,16 @@ describe('<HostsListItem />', () => {
const copyMockHost = Object.assign({}, mockHost);
copyMockHost.summary_fields.user_capabilities.edit = false;
wrapper = mountWithContexts(
<HostsListItem
isSelected={false}
detailUrl="/host/1"
onSelect={() => {}}
host={copyMockHost}
/>
<table>
<tbody>
<HostsListItem
isSelected={false}
detailUrl="/host/1"
onSelect={() => {}}
host={copyMockHost}
/>
</tbody>
</table>
);
expect(wrapper.find('PencilAltIcon').exists()).toBeFalsy();
});

View File

@ -9,7 +9,11 @@ import useRequest, { useDeleteItems } from '../../../util/useRequest';
import AlertModal from '../../../components/AlertModal';
import DataListToolbar from '../../../components/DataListToolbar';
import ErrorDetail from '../../../components/ErrorDetail';
import PaginatedDataList, {
import PaginatedTable, {
HeaderRow,
HeaderCell,
} from '../../../components/PaginatedTable';
import {
ToolbarAddButton,
ToolbarDeleteButton,
} from '../../../components/PaginatedDataList';
@ -112,7 +116,7 @@ function TeamList({ i18n }) {
<Fragment>
<PageSection>
<Card>
<PaginatedDataList
<PaginatedTable
contentError={contentError}
hasContentLoading={hasContentLoading}
items={teams}
@ -143,14 +147,15 @@ function TeamList({ i18n }) {
key: 'modified_by__username__icontains',
},
]}
toolbarSortColumns={[
{
name: i18n._(t`Name`),
key: 'name',
},
]}
toolbarSearchableKeys={searchableKeys}
toolbarRelatedSearchableKeys={relatedSearchableKeys}
headerRow={
<HeaderRow qsConfig={QS_CONFIG}>
<HeaderCell sortKey="name">{i18n._(t`Name`)}</HeaderCell>
<HeaderCell>{i18n._(t`Organization`)}</HeaderCell>
<HeaderCell>{i18n._(t`Actions`)}</HeaderCell>
</HeaderRow>
}
renderToolbar={props => (
<DataListToolbar
{...props}
@ -176,13 +181,14 @@ function TeamList({ i18n }) {
]}
/>
)}
renderItem={o => (
renderRow={(team, index) => (
<TeamListItem
key={o.id}
team={o}
detailUrl={`${match.url}/${o.id}`}
isSelected={selected.some(row => row.id === o.id)}
onSelect={() => handleSelect(o)}
key={team.id}
team={team}
detailUrl={`${match.url}/${team.id}`}
isSelected={selected.some(row => row.id === team.id)}
onSelect={() => handleSelect(team)}
rowIndex={index}
/>
)}
emptyStateControls={

View File

@ -1,33 +1,23 @@
import 'styled-components/macro';
import React, { Fragment } from 'react';
import React from 'react';
import { string, bool, func } from 'prop-types';
import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
import {
Button,
DataListAction as _DataListAction,
DataListCheck,
DataListItem,
DataListItemCells,
DataListItemRow,
Tooltip,
} from '@patternfly/react-core';
import styled from 'styled-components';
import { Button } from '@patternfly/react-core';
import { Tr, Td } from '@patternfly/react-table';
import { Link } from 'react-router-dom';
import { PencilAltIcon } from '@patternfly/react-icons';
import DataListCell from '../../../components/DataListCell';
import { ActionsTd, ActionItem } from '../../../components/PaginatedTable';
import { Team } from '../../../types';
const DataListAction = styled(_DataListAction)`
align-items: center;
display: grid;
grid-gap: 16px;
grid-template-columns: 40px;
`;
function TeamListItem({ team, isSelected, onSelect, detailUrl, i18n }) {
function TeamListItem({
team,
isSelected,
onSelect,
detailUrl,
rowIndex,
i18n,
}) {
TeamListItem.propTypes = {
team: Team.isRequired,
detailUrl: string.isRequired,
@ -38,57 +28,45 @@ function TeamListItem({ team, isSelected, onSelect, detailUrl, i18n }) {
const labelId = `check-action-${team.id}`;
return (
<DataListItem key={team.id} aria-labelledby={labelId} id={`${team.id}`}>
<DataListItemRow>
<DataListCheck
id={`select-team-${team.id}`}
checked={isSelected}
onChange={onSelect}
aria-labelledby={labelId}
/>
<DataListItemCells
dataListCells={[
<DataListCell key="name">
<Link id={labelId} to={`${detailUrl}`}>
<b>{team.name}</b>
</Link>
</DataListCell>,
<DataListCell key="organization">
{team.summary_fields.organization && (
<Fragment>
<b>{i18n._(t`Organization`)}</b>{' '}
<Link
to={`/organizations/${team.summary_fields.organization.id}/details`}
>
<b>{team.summary_fields.organization.name}</b>
</Link>
</Fragment>
)}
</DataListCell>,
]}
/>
<DataListAction
aria-label={i18n._(t`Actions`)}
aria-labelledby={labelId}
id={labelId}
<Tr id={`team-row-${team.id}`}>
<Td
select={{
rowIndex,
isSelected,
onSelect,
}}
dataLabel={i18n._(t`Selected`)}
/>
<Td id={labelId} dataLabel={i18n._(t`Name`)}>
<Link id={labelId} to={`${detailUrl}`}>
<b>{team.name}</b>
</Link>
</Td>
<Td dataLabel={i18n._(t`Organization`)}>
{team.summary_fields.organization && (
<Link
to={`/organizations/${team.summary_fields.organization.id}/details`}
>
<b>{team.summary_fields.organization.name}</b>
</Link>
)}
</Td>
<ActionsTd dataLabel={i18n._(t`Actions`)}>
<ActionItem
visible={team.summary_fields.user_capabilities.edit}
tooltip={i18n._(t`Edit Team`)}
>
{team.summary_fields.user_capabilities.edit ? (
<Tooltip content={i18n._(t`Edit Team`)} position="top">
<Button
aria-label={i18n._(t`Edit Team`)}
variant="plain"
component={Link}
to={`/teams/${team.id}/edit`}
>
<PencilAltIcon />
</Button>
</Tooltip>
) : (
''
)}
</DataListAction>
</DataListItemRow>
</DataListItem>
<Button
aria-label={i18n._(t`Edit Team`)}
variant="plain"
component={Link}
to={`/teams/${team.id}/edit`}
>
<PencilAltIcon />
</Button>
</ActionItem>
</ActionsTd>
</Tr>
);
}
export default withI18n()(TeamListItem);

View File

@ -11,20 +11,24 @@ describe('<TeamListItem />', () => {
mountWithContexts(
<I18nProvider>
<MemoryRouter initialEntries={['/teams']} initialIndex={0}>
<TeamListItem
team={{
id: 1,
name: 'Team 1',
summary_fields: {
user_capabilities: {
edit: true,
},
},
}}
detailUrl="/team/1"
isSelected
onSelect={() => {}}
/>
<table>
<tbody>
<TeamListItem
team={{
id: 1,
name: 'Team 1',
summary_fields: {
user_capabilities: {
edit: true,
},
},
}}
detailUrl="/team/1"
isSelected
onSelect={() => {}}
/>
</tbody>
</table>
</MemoryRouter>
</I18nProvider>
);
@ -33,20 +37,24 @@ describe('<TeamListItem />', () => {
const wrapper = mountWithContexts(
<I18nProvider>
<MemoryRouter initialEntries={['/teams']} initialIndex={0}>
<TeamListItem
team={{
id: 1,
name: 'Team',
summary_fields: {
user_capabilities: {
edit: true,
},
},
}}
detailUrl="/team/1"
isSelected
onSelect={() => {}}
/>
<table>
<tbody>
<TeamListItem
team={{
id: 1,
name: 'Team',
summary_fields: {
user_capabilities: {
edit: true,
},
},
}}
detailUrl="/team/1"
isSelected
onSelect={() => {}}
/>
</tbody>
</table>
</MemoryRouter>
</I18nProvider>
);
@ -56,20 +64,24 @@ describe('<TeamListItem />', () => {
const wrapper = mountWithContexts(
<I18nProvider>
<MemoryRouter initialEntries={['/teams']} initialIndex={0}>
<TeamListItem
team={{
id: 1,
name: 'Team',
summary_fields: {
user_capabilities: {
edit: false,
},
},
}}
detailUrl="/team/1"
isSelected
onSelect={() => {}}
/>
<table>
<tbody>
<TeamListItem
team={{
id: 1,
name: 'Team',
summary_fields: {
user_capabilities: {
edit: false,
},
},
}}
detailUrl="/team/1"
isSelected
onSelect={() => {}}
/>
</tbody>
</table>
</MemoryRouter>
</I18nProvider>
);

View File

@ -7,7 +7,11 @@ import { UsersAPI } from '../../../api';
import AlertModal from '../../../components/AlertModal';
import DataListToolbar from '../../../components/DataListToolbar';
import ErrorDetail from '../../../components/ErrorDetail';
import PaginatedDataList, {
import PaginatedTable, {
HeaderRow,
HeaderCell,
} from '../../../components/PaginatedTable';
import {
ToolbarAddButton,
ToolbarDeleteButton,
} from '../../../components/PaginatedDataList';
@ -101,7 +105,7 @@ function UserList({ i18n }) {
<>
<PageSection>
<Card>
<PaginatedDataList
<PaginatedTable
contentError={contentError}
hasContentLoading={hasContentLoading}
items={users}
@ -124,20 +128,6 @@ function UserList({ i18n }) {
key: 'last_name__icontains',
},
]}
toolbarSortColumns={[
{
name: i18n._(t`Username`),
key: 'username',
},
{
name: i18n._(t`First Name`),
key: 'first_name',
},
{
name: i18n._(t`Last Name`),
key: 'last_name',
},
]}
toolbarSearchableKeys={searchableKeys}
toolbarRelatedSearchableKeys={relatedSearchableKeys}
renderToolbar={props => (
@ -167,13 +157,29 @@ function UserList({ i18n }) {
]}
/>
)}
renderItem={o => (
headerRow={
<HeaderRow qsConfig={QS_CONFIG}>
<HeaderCell sortKey="username">
{i18n._(t`Username`)}
</HeaderCell>
<HeaderCell sortKey="first_name">
{i18n._(t`First Name`)}
</HeaderCell>
<HeaderCell sortKey="last_name">
{i18n._(t`Last Name`)}
</HeaderCell>
<HeaderCell>{i18n._(t`Role`)}</HeaderCell>
<HeaderCell>{i18n._(t`Actions`)}</HeaderCell>
</HeaderRow>
}
renderRow={(user, index) => (
<UserListItem
key={o.id}
user={o}
detailUrl={`${match.url}/${o.id}/details`}
isSelected={selected.some(row => row.id === o.id)}
onSelect={() => handleSelect(o)}
key={user.id}
user={user}
detailUrl={`${match.url}/${user.id}/details`}
isSelected={selected.some(row => row.id === user.id)}
onSelect={() => handleSelect(user)}
rowIndex={index}
/>
)}
emptyStateControls={

View File

@ -129,51 +129,66 @@ describe('UsersList with full permissions', () => {
test('should check and uncheck the row item', async () => {
expect(
wrapper.find('DataListCheck[id="select-user-1"]').props().checked
wrapper
.find('.pf-c-table__check input')
.first()
.props().checked
).toBe(false);
await act(async () => {
wrapper.find('DataListCheck[id="select-user-1"]').invoke('onChange')(
true
);
wrapper
.find('.pf-c-table__check input')
.first()
.invoke('onChange')(true);
});
wrapper.update();
expect(
wrapper.find('DataListCheck[id="select-user-1"]').props().checked
wrapper
.find('.pf-c-table__check input')
.first()
.props().checked
).toBe(true);
await act(async () => {
wrapper.find('DataListCheck[id="select-user-1"]').invoke('onChange')(
false
);
wrapper
.find('.pf-c-table__check input')
.first()
.invoke('onChange')(false);
});
wrapper.update();
expect(
wrapper.find('DataListCheck[id="select-user-1"]').props().checked
wrapper
.find('.pf-c-table__check input')
.first()
.props().checked
).toBe(false);
});
test('should check all row items when select all is checked', async () => {
wrapper.find('DataListCheck').forEach(el => {
expect(wrapper.find('.pf-c-table__check input')).toHaveLength(2);
wrapper.find('.pf-c-table__check input').forEach(el => {
expect(el.props().checked).toBe(false);
});
await act(async () => {
wrapper.find('Checkbox#select-all').invoke('onChange')(true);
});
wrapper.update();
wrapper.find('DataListCheck').forEach(el => {
wrapper.find('.pf-c-table__check input').forEach(el => {
expect(el.props().checked).toBe(true);
});
await act(async () => {
wrapper.find('Checkbox#select-all').invoke('onChange')(false);
});
wrapper.update();
wrapper.find('DataListCheck').forEach(el => {
wrapper.find('.pf-c-table__check input').forEach(el => {
expect(el.props().checked).toBe(false);
});
});
test('should call api delete users for each selected user', async () => {
await act(async () => {
wrapper.find('DataListCheck[id="select-user-1"]').invoke('onChange')();
wrapper
.find('.pf-c-table__check input')
.first()
.invoke('onChange')();
});
wrapper.update();
await act(async () => {
@ -185,10 +200,12 @@ describe('UsersList with full permissions', () => {
test('should show error modal when user is not successfully deleted from api', async () => {
UsersAPI.destroy.mockImplementationOnce(() => Promise.reject(new Error()));
// expect(wrapper.debug()).toBe(false);
expect(wrapper.find('Modal').length).toBe(0);
await act(async () => {
wrapper.find('DataListCheck[id="select-user-1"]').invoke('onChange')();
wrapper
.find('.pf-c-table__check input')
.first()
.invoke('onChange')();
});
wrapper.update();
await act(async () => {

View File

@ -3,24 +3,22 @@ import React, { Fragment } from 'react';
import { string, bool, func } from 'prop-types';
import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
import {
Button,
DataListAction,
DataListCheck,
DataListItem,
DataListItemCells,
DataListItemRow,
Label,
Tooltip,
} from '@patternfly/react-core';
import { Button, Label } from '@patternfly/react-core';
import { Tr, Td } from '@patternfly/react-table';
import { Link } from 'react-router-dom';
import { PencilAltIcon } from '@patternfly/react-icons';
import DataListCell from '../../../components/DataListCell';
import { ActionsTd, ActionItem } from '../../../components/PaginatedTable';
import { User } from '../../../types';
function UserListItem({ user, isSelected, onSelect, detailUrl, i18n }) {
function UserListItem({
user,
isSelected,
onSelect,
detailUrl,
rowIndex,
i18n,
}) {
const labelId = `check-action-${user.id}`;
let user_type;
@ -36,84 +34,64 @@ function UserListItem({ user, isSelected, onSelect, detailUrl, i18n }) {
const socialAuthUser = user.auth.length > 0;
return (
<DataListItem key={user.id} aria-labelledby={labelId} id={`${user.id}`}>
<DataListItemRow>
<DataListCheck
id={`select-user-${user.id}`}
checked={isSelected}
onChange={onSelect}
aria-labelledby={labelId}
/>
<DataListItemCells
dataListCells={[
<DataListCell key="username" aria-label={i18n._(t`username`)}>
<span id={labelId}>
<Link to={`${detailUrl}`} id={labelId}>
<b>{user.username}</b>
</Link>
</span>
{ldapUser && (
<span css="margin-left: 12px">
<Label aria-label={i18n._(t`ldap user`)}>
{i18n._(t`LDAP`)}
</Label>
</span>
)}
{socialAuthUser && (
<span css="margin-left: 12px">
<Label aria-label={i18n._(t`social login`)}>
{i18n._(t`SOCIAL`)}
</Label>
</span>
)}
</DataListCell>,
<DataListCell
key="first-name"
aria-label={i18n._(t`user first name`)}
>
{user.first_name && (
<Fragment>
<b css="margin-right: 24px">{i18n._(t`First Name`)}</b>
{user.first_name}
</Fragment>
)}
</DataListCell>,
<DataListCell
key="last-name"
aria-label={i18n._(t`user last name`)}
>
{user.last_name && (
<Fragment>
<b css="margin-right: 24px">{i18n._(t`Last Name`)}</b>
{user.last_name}
</Fragment>
)}
</DataListCell>,
<DataListCell key="user-type" aria-label={i18n._(t`user type`)}>
{user_type}
</DataListCell>,
]}
/>
<DataListAction
aria-label={i18n._(t`actions`)}
aria-labelledby={labelId}
id={labelId}
<Tr id={`user-row-${user.id}`}>
<Td
select={{
rowIndex,
isSelected,
onSelect,
}}
/>
<Td id={labelId} dataLabel={i18n._(t`Username`)}>
<Link to={`${detailUrl}`} id={labelId}>
<b>{user.username}</b>
</Link>
{ldapUser && (
<span css="margin-left: 12px">
<Label aria-label={i18n._(t`ldap user`)}>{i18n._(t`LDAP`)}</Label>
</span>
)}
{socialAuthUser && (
<span css="margin-left: 12px">
<Label aria-label={i18n._(t`social login`)}>
{i18n._(t`SOCIAL`)}
</Label>
</span>
)}
</Td>
<Td dataLabel={i18n._(t`First Name`)}>
{user.first_name && (
<Fragment>
<b css="margin-right: 24px">{i18n._(t`First Name`)}</b>
{user.first_name}
</Fragment>
)}
</Td>
<Td dataLabel={i18n._(t`Last Name`)}>
{user.last_name && (
<Fragment>
<b css="margin-right: 24px">{i18n._(t`Last Name`)}</b>
{user.last_name}
</Fragment>
)}
</Td>
<Td dataLabel={i18n._(t`Role`)}>{user_type}</Td>
<ActionsTd dataLabel={i18n._(t`Actions`)}>
<ActionItem
visible={user.summary_fields.user_capabilities.edit}
tooltip={i18n._(t`Edit User`)}
>
{user.summary_fields.user_capabilities.edit && (
<Tooltip content={i18n._(t`Edit User`)} position="top">
<Button
aria-label={i18n._(t`Edit User`)}
variant="plain"
component={Link}
to={`/users/${user.id}/edit`}
>
<PencilAltIcon />
</Button>
</Tooltip>
)}
</DataListAction>
</DataListItemRow>
</DataListItem>
<Button
aria-label={i18n._(t`Edit User`)}
variant="plain"
component={Link}
to={`/users/${user.id}/edit`}
>
<PencilAltIcon />
</Button>
</ActionItem>
</ActionsTd>
</Tr>
);
}

View File

@ -18,12 +18,16 @@ describe('UserListItem with full permissions', () => {
wrapper = mountWithContexts(
<I18nProvider>
<MemoryRouter initialEntries={['/users']} initialIndex={0}>
<UserListItem
user={mockDetails}
detailUrl="/user/1"
isSelected
onSelect={() => {}}
/>
<table>
<tbody>
<UserListItem
user={mockDetails}
detailUrl="/user/1"
isSelected
onSelect={() => {}}
/>
</tbody>
</table>
</MemoryRouter>
</I18nProvider>
);
@ -36,9 +40,9 @@ describe('UserListItem with full permissions', () => {
});
test('should display user data', () => {
expect(
wrapper.find('DataListCell[aria-label="user type"]').prop('children')
).toEqual('System Administrator');
expect(wrapper.find('td[data-label="Role"]').prop('children')).toEqual(
'System Administrator'
);
expect(
wrapper.find('Label[aria-label="social login"]').prop('children')
).toEqual('SOCIAL');
@ -50,19 +54,23 @@ describe('UserListItem without full permissions', () => {
wrapper = mountWithContexts(
<I18nProvider>
<MemoryRouter initialEntries={['/users']} initialIndex={0}>
<UserListItem
user={{
...mockDetails,
summary_fields: {
user_capabilities: {
edit: false,
},
},
}}
detailUrl="/user/1"
isSelected
onSelect={() => {}}
/>
<table>
<tbody>
<UserListItem
user={{
...mockDetails,
summary_fields: {
user_capabilities: {
edit: false,
},
},
}}
detailUrl="/user/1"
isSelected
onSelect={() => {}}
/>
</tbody>
</table>
</MemoryRouter>
</I18nProvider>
);