Merge pull request #9045 from keithjgrant/6189-schedules-list

Convert SchedulesList, WorkflowApprovalList to tables

Reviewed-by: https://github.com/apps/softwarefactory-project-zuul
This commit is contained in:
softwarefactory-project-zuul[bot]
2021-02-03 14:08:53 +00:00
committed by GitHub
9 changed files with 359 additions and 277 deletions

View File

@@ -8,8 +8,14 @@
"modules": true "modules": true
} }
}, },
"plugins": ["react-hooks", "jsx-a11y", "i18next"], "plugins": ["react-hooks", "jsx-a11y", "i18next"],
"extends": ["airbnb", "prettier", "prettier/react", "plugin:jsx-a11y/strict", "plugin:i18next/recommended"], "extends": [
"airbnb",
"prettier",
"prettier/react",
"plugin:jsx-a11y/strict",
"plugin:i18next/recommended"
],
"settings": { "settings": {
"react": { "react": {
"version": "16.5.2" "version": "16.5.2"
@@ -24,7 +30,70 @@
"window": true "window": true
}, },
"rules": { "rules": {
"i18next/no-literal-string": [2, {"markupOnly": true, "ignoreAttribute": ["to", "streamType", "path", "component", "variant", "key", "position", "promptName", "color","promptId", "headingLevel", "size", "target", "autoComplete","trigger", "from", "name", "fieldId", "css", "gutter", "dataCy", "tooltipMaxWidth", "mode", "aria-labelledby","aria-hidden","sortKey", "ouiaId", "credentialTypeNamespace", "link", "value", "credentialTypeKind", "linkTo", "scrollToAlignment", "displayKey", "sortedColumnKey", "maxHeight", "role", "aria-haspopup", "dropDirection", "resizeOrientation", "src", "theme"], "ignore":["Ansible", "Tower", "JSON", "YAML", "lg", "START"],"ignoreComponent":["code", "Omit","PotentialLink", "TypeRedirect", "Radio", "RunOnRadio", "NodeTypeLetter", "SelectableItem", "Dash"], "ignoreCallee": ["describe"] }], "i18next/no-literal-string": [
2,
{
"markupOnly": true,
"ignoreAttribute": [
"to",
"streamType",
"path",
"component",
"variant",
"key",
"position",
"promptName",
"color",
"promptId",
"headingLevel",
"size",
"target",
"autoComplete",
"trigger",
"from",
"name",
"fieldId",
"css",
"gutter",
"dataCy",
"tooltipMaxWidth",
"mode",
"aria-labelledby",
"aria-hidden",
"sortKey",
"ouiaId",
"credentialTypeNamespace",
"link",
"value",
"credentialTypeKind",
"linkTo",
"scrollToAlignment",
"displayKey",
"sortedColumnKey",
"maxHeight",
"role",
"aria-haspopup",
"dropDirection",
"resizeOrientation",
"src",
"theme",
"gridColumns"
],
"ignore": ["Ansible", "Tower", "JSON", "YAML", "lg", "START"],
"ignoreComponent": [
"code",
"Omit",
"PotentialLink",
"TypeRedirect",
"Radio",
"RunOnRadio",
"NodeTypeLetter",
"SelectableItem",
"Dash"
],
"ignoreCallee": ["describe"]
}
],
"camelcase": "off", "camelcase": "off",
"arrow-parens": "off", "arrow-parens": "off",
"comma-dangle": "off", "comma-dangle": "off",

View File

@@ -9,7 +9,7 @@ const ActionsGrid = styled.div`
align-items: center; align-items: center;
${props => { ${props => {
const columns = '40px '.repeat(props.numActions || 1); const columns = props.gridColumns || '40px '.repeat(props.numActions || 1);
return css` return css`
grid-template-columns: ${columns}; grid-template-columns: ${columns};
`; `;
@@ -17,7 +17,7 @@ const ActionsGrid = styled.div`
`; `;
ActionsGrid.displayName = 'ActionsGrid'; ActionsGrid.displayName = 'ActionsGrid';
export default function ActionsTd({ children, ...props }) { export default function ActionsTd({ children, gridColumns, ...props }) {
const numActions = children.length || 1; const numActions = children.length || 1;
const width = numActions * 40; const width = numActions * 40;
return ( return (
@@ -28,7 +28,7 @@ export default function ActionsTd({ children, ...props }) {
`} `}
{...props} {...props}
> >
<ActionsGrid numActions={numActions}> <ActionsGrid numActions={numActions} gridColumns={gridColumns}>
{React.Children.map(children, (child, i) => {React.Children.map(children, (child, i) =>
React.cloneElement(child, { React.cloneElement(child, {
column: i + 1, column: i + 1,

View File

@@ -6,11 +6,9 @@ import { t } from '@lingui/macro';
import { SchedulesAPI } from '../../../api'; import { SchedulesAPI } from '../../../api';
import AlertModal from '../../AlertModal'; import AlertModal from '../../AlertModal';
import ErrorDetail from '../../ErrorDetail'; import ErrorDetail from '../../ErrorDetail';
import PaginatedTable, { HeaderRow, HeaderCell } from '../../PaginatedTable';
import DataListToolbar from '../../DataListToolbar'; import DataListToolbar from '../../DataListToolbar';
import PaginatedDataList, { import { ToolbarAddButton, ToolbarDeleteButton } from '../../PaginatedDataList';
ToolbarAddButton,
ToolbarDeleteButton,
} from '../../PaginatedDataList';
import useRequest, { useDeleteItems } from '../../../util/useRequest'; import useRequest, { useDeleteItems } from '../../../util/useRequest';
import { getQSConfig, parseQueryString } from '../../../util/qs'; import { getQSConfig, parseQueryString } from '../../../util/qs';
import ScheduleListItem from './ScheduleListItem'; import ScheduleListItem from './ScheduleListItem';
@@ -119,19 +117,28 @@ function ScheduleList({
return ( return (
<> <>
<PaginatedDataList <PaginatedTable
contentError={contentError} contentError={contentError}
hasContentLoading={isLoading || isDeleteLoading} hasContentLoading={isLoading || isDeleteLoading}
items={schedules} items={schedules}
itemCount={itemCount} itemCount={itemCount}
qsConfig={QS_CONFIG} qsConfig={QS_CONFIG}
onRowClick={handleSelect} onRowClick={handleSelect}
renderItem={item => ( headerRow={
<HeaderRow qsConfig={QS_CONFIG}>
<HeaderCell sortKey="name">{i18n._(t`Name`)}</HeaderCell>
<HeaderCell>{i18n._(t`Type`)}</HeaderCell>
<HeaderCell sortKey="next_run">{i18n._(t`Next Run`)}</HeaderCell>
<HeaderCell>{i18n._(t`Actions`)}</HeaderCell>
</HeaderRow>
}
renderRow={(item, index) => (
<ScheduleListItem <ScheduleListItem
isSelected={selected.some(row => row.id === item.id)} isSelected={selected.some(row => row.id === item.id)}
key={item.id} key={item.id}
onSelect={() => handleSelect(item)} onSelect={() => handleSelect(item)}
schedule={item} schedule={item}
rowIndex={index}
/> />
)} )}
toolbarSearchColumns={[ toolbarSearchColumns={[
@@ -153,16 +160,6 @@ function ScheduleList({
key: 'modified_by__username__icontains', key: 'modified_by__username__icontains',
}, },
]} ]}
toolbarSortColumns={[
{
name: i18n._(t`Name`),
key: 'name',
},
{
name: i18n._(t`Next Run`),
key: 'next_run',
},
]}
toolbarSearchableKeys={searchableKeys} toolbarSearchableKeys={searchableKeys}
toolbarRelatedSearchableKeys={relatedSearchableKeys} toolbarRelatedSearchableKeys={relatedSearchableKeys}
renderToolbar={props => ( renderToolbar={props => (

View File

@@ -59,44 +59,61 @@ describe('ScheduleList', () => {
test('should check and uncheck the row item', async () => { test('should check and uncheck the row item', async () => {
expect( expect(
wrapper.find('DataListCheck[id="select-schedule-1"]').props().checked wrapper
.find('.pf-c-table__check')
.first()
.find('input')
.props().checked
).toBe(false); ).toBe(false);
await act(async () => { await act(async () => {
wrapper wrapper
.find('DataListCheck[id="select-schedule-1"]') .find('.pf-c-table__check')
.first()
.find('input')
.invoke('onChange')(true); .invoke('onChange')(true);
}); });
wrapper.update(); wrapper.update();
expect( expect(
wrapper.find('DataListCheck[id="select-schedule-1"]').props().checked wrapper
.find('.pf-c-table__check')
.first()
.find('input')
.props().checked
).toBe(true); ).toBe(true);
await act(async () => { await act(async () => {
wrapper wrapper
.find('DataListCheck[id="select-schedule-1"]') .find('.pf-c-table__check')
.first()
.find('input')
.invoke('onChange')(false); .invoke('onChange')(false);
}); });
wrapper.update(); wrapper.update();
expect( expect(
wrapper.find('DataListCheck[id="select-schedule-1"]').props().checked wrapper
.find('.pf-c-table__check')
.first()
.find('input')
.props().checked
).toBe(false); ).toBe(false);
}); });
test('should check all row items when select all is checked', async () => { 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(5);
wrapper.find('.pf-c-table__check input').forEach(el => {
expect(el.props().checked).toBe(false); expect(el.props().checked).toBe(false);
}); });
await act(async () => { await act(async () => {
wrapper.find('Checkbox#select-all').invoke('onChange')(true); wrapper.find('Checkbox#select-all').invoke('onChange')(true);
}); });
wrapper.update(); wrapper.update();
wrapper.find('DataListCheck').forEach(el => { wrapper.find('.pf-c-table__check input').forEach(el => {
expect(el.props().checked).toBe(true); expect(el.props().checked).toBe(true);
}); });
await act(async () => { await act(async () => {
wrapper.find('Checkbox#select-all').invoke('onChange')(false); wrapper.find('Checkbox#select-all').invoke('onChange')(false);
}); });
wrapper.update(); wrapper.update();
wrapper.find('DataListCheck').forEach(el => { wrapper.find('.pf-c-table__check input').forEach(el => {
expect(el.props().checked).toBe(false); expect(el.props().checked).toBe(false);
}); });
}); });
@@ -104,7 +121,8 @@ describe('ScheduleList', () => {
test('should call api delete schedules for each selected schedule', async () => { test('should call api delete schedules for each selected schedule', async () => {
await act(async () => { await act(async () => {
wrapper wrapper
.find('DataListCheck[id="select-schedule-3"]') .find('.pf-c-table__check input')
.at(3)
.invoke('onChange')(); .invoke('onChange')();
}); });
wrapper.update(); wrapper.update();
@@ -122,7 +140,8 @@ describe('ScheduleList', () => {
expect(wrapper.find('Modal').length).toBe(0); expect(wrapper.find('Modal').length).toBe(0);
await act(async () => { await act(async () => {
wrapper wrapper
.find('DataListCheck[id="select-schedule-2"]') .find('.pf-c-table__check input')
.at(2)
.invoke('onChange')(); .invoke('onChange')();
}); });
wrapper.update(); wrapper.update();

View File

@@ -4,31 +4,16 @@ import { bool, func } from 'prop-types';
import { withI18n } from '@lingui/react'; import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { import { Button } from '@patternfly/react-core';
Button, import { Tr, Td } from '@patternfly/react-table';
DataListAction as _DataListAction,
DataListCheck,
DataListItem,
DataListItemRow,
DataListItemCells,
Tooltip,
} from '@patternfly/react-core';
import { PencilAltIcon } from '@patternfly/react-icons'; import { PencilAltIcon } from '@patternfly/react-icons';
import styled from 'styled-components';
import DataListCell from '../../DataListCell';
import { DetailList, Detail } from '../../DetailList'; import { DetailList, Detail } from '../../DetailList';
import { ActionsTd, ActionItem } from '../../PaginatedTable';
import { ScheduleToggle } from '..'; import { ScheduleToggle } from '..';
import { Schedule } from '../../../types'; import { Schedule } from '../../../types';
import { formatDateString } from '../../../util/dates'; import { formatDateString } from '../../../util/dates';
const DataListAction = styled(_DataListAction)` function ScheduleListItem({ i18n, isSelected, onSelect, schedule, rowIndex }) {
align-items: center;
display: grid;
grid-gap: 16px;
grid-template-columns: 92px 40px;
`;
function ScheduleListItem({ i18n, isSelected, onSelect, schedule }) {
const labelId = `check-action-${schedule.id}`; const labelId = `check-action-${schedule.id}`;
const jobTypeLabels = { const jobTypeLabels = {
@@ -62,69 +47,56 @@ function ScheduleListItem({ i18n, isSelected, onSelect, schedule }) {
} }
return ( return (
<DataListItem <Tr id={`schedule-row-${schedule.id}`}>
key={schedule.id} <Td
aria-labelledby={labelId} select={{
id={`${schedule.id}`} rowIndex,
> isSelected,
<DataListItemRow> onSelect,
<DataListCheck disable: false,
id={`select-schedule-${schedule.id}`} }}
checked={isSelected} dataLabel={i18n._(t`Selected`)}
onChange={onSelect} />
aria-labelledby={labelId} <Td id={labelId} dataLabel={i18n._(t`Name`)}>
/> <Link to={`${scheduleBaseUrl}/details`}>
<DataListItemCells <b>{schedule.name}</b>
dataListCells={[ </Link>
<DataListCell key="name"> </Td>
<Link to={`${scheduleBaseUrl}/details`}> <Td dataLabel={i18n._(t`Type`)}>
<b>{schedule.name}</b> {
</Link> jobTypeLabels[
</DataListCell>, schedule.summary_fields.unified_job_template.unified_job_type
<DataListCell key="type"> ]
{ }
jobTypeLabels[ </Td>
schedule.summary_fields.unified_job_template.unified_job_type <Td dataLabel={i18n._(t`Next Run`)}>
] {schedule.next_run && (
} <DetailList stacked>
</DataListCell>, <Detail
<DataListCell key="next_run"> label={i18n._(t`Next Run`)}
{schedule.next_run && ( value={formatDateString(schedule.next_run)}
<DetailList stacked> />
<Detail </DetailList>
label={i18n._(t`Next Run`)} )}
value={formatDateString(schedule.next_run)} </Td>
/> <ActionsTd dataLabel={i18n._(t`Actions`)} gridColumns="auto 40px">
</DetailList> <ScheduleToggle schedule={schedule} />
)} <ActionItem
</DataListCell>, visible={schedule.summary_fields.user_capabilities.edit}
]} tooltip={i18n._(t`Edit Schedule`)}
/>
<DataListAction
aria-label={i18n._(t`actions`)}
aria-labelledby={labelId}
id={labelId}
key="actions"
> >
<ScheduleToggle schedule={schedule} /> <Button
{schedule.summary_fields.user_capabilities.edit ? ( aria-label={i18n._(t`Edit Schedule`)}
<Tooltip content={i18n._(t`Edit Schedule`)} position="top"> css="grid-column: 2"
<Button variant="plain"
aria-label={i18n._(t`Edit Schedule`)} component={Link}
css="grid-column: 2" to={`${scheduleBaseUrl}/edit`}
variant="plain" >
component={Link} <PencilAltIcon />
to={`${scheduleBaseUrl}/edit`} </Button>
> </ActionItem>
<PencilAltIcon /> </ActionsTd>
</Button> </Tr>
</Tooltip>
) : (
''
)}
</DataListAction>
</DataListItemRow>
</DataListItem>
); );
} }

View File

@@ -50,39 +50,47 @@ describe('ScheduleListItem', () => {
describe('User has edit permissions', () => { describe('User has edit permissions', () => {
beforeAll(() => { beforeAll(() => {
wrapper = mountWithContexts( wrapper = mountWithContexts(
<ScheduleListItem <table>
isSelected={false} <tbody>
onSelect={onSelect} <ScheduleListItem
schedule={mockSchedule} isSelected={false}
/> onSelect={onSelect}
schedule={mockSchedule}
/>
</tbody>
</table>
); );
}); });
afterAll(() => { afterAll(() => {
wrapper.unmount(); wrapper.unmount();
}); });
test('Name correctly shown with correct link', () => { test('Name correctly shown with correct link', () => {
expect( expect(
wrapper wrapper
.find('DataListCell') .find('Td')
.first() .at(1)
.text() .text()
).toBe('Mock Schedule'); ).toBe('Mock Schedule');
expect( expect(
wrapper wrapper
.find('DataListCell') .find('Td')
.first() .at(1)
.find('Link') .find('Link')
.props().to .props().to
).toBe('/templates/job_template/12/schedules/6/details'); ).toBe('/templates/job_template/12/schedules/6/details');
}); });
test('Type correctly shown', () => { test('Type correctly shown', () => {
expect( expect(
wrapper wrapper
.find('DataListCell') .find('Td')
.at(1) .at(2)
.text() .text()
).toBe('Playbook Run'); ).toBe('Playbook Run');
}); });
test('Edit button shown with correct link', () => { test('Edit button shown with correct link', () => {
expect(wrapper.find('PencilAltIcon').length).toBe(1); expect(wrapper.find('PencilAltIcon').length).toBe(1);
expect( expect(
@@ -92,6 +100,7 @@ describe('ScheduleListItem', () => {
.props().to .props().to
).toBe('/templates/job_template/12/schedules/6/edit'); ).toBe('/templates/job_template/12/schedules/6/edit');
}); });
test('Toggle button enabled', () => { test('Toggle button enabled', () => {
expect( expect(
wrapper wrapper
@@ -100,63 +109,74 @@ describe('ScheduleListItem', () => {
.props().isDisabled .props().isDisabled
).toBe(false); ).toBe(false);
}); });
test('Clicking checkbox makes expected callback', () => {
test('Clicking checkbox selects item', () => {
wrapper wrapper
.find('DataListCheck') .find('Td')
.first() .first()
.find('input') .find('input')
.simulate('change'); .simulate('change');
expect(onSelect).toHaveBeenCalledTimes(1); expect(onSelect).toHaveBeenCalledTimes(1);
}); });
}); });
describe('User has read-only permissions', () => { describe('User has read-only permissions', () => {
beforeAll(() => { beforeAll(() => {
wrapper = mountWithContexts( wrapper = mountWithContexts(
<ScheduleListItem <table>
isSelected={false} <tbody>
onSelect={onSelect} <ScheduleListItem
schedule={{ isSelected={false}
...mockSchedule, onSelect={onSelect}
summary_fields: { schedule={{
...mockSchedule.summary_fields, ...mockSchedule,
user_capabilities: { summary_fields: {
edit: false, ...mockSchedule.summary_fields,
delete: false, user_capabilities: {
}, edit: false,
}, delete: false,
}} },
/> },
}}
/>
</tbody>
</table>
); );
}); });
afterAll(() => { afterAll(() => {
wrapper.unmount(); wrapper.unmount();
}); });
test('Name correctly shown with correct link', () => { test('Name correctly shown with correct link', () => {
expect( expect(
wrapper wrapper
.find('DataListCell') .find('Td')
.first() .at(1)
.text() .text()
).toBe('Mock Schedule'); ).toBe('Mock Schedule');
expect( expect(
wrapper wrapper
.find('DataListCell') .find('Td')
.first() .at(1)
.find('Link') .find('Link')
.props().to .props().to
).toBe('/templates/job_template/12/schedules/6/details'); ).toBe('/templates/job_template/12/schedules/6/details');
}); });
test('Type correctly shown', () => { test('Type correctly shown', () => {
expect( expect(
wrapper wrapper
.find('DataListCell') .find('Td')
.at(1) .at(2)
.text() .text()
).toBe('Playbook Run'); ).toBe('Playbook Run');
}); });
test('Edit button hidden', () => { test('Edit button hidden', () => {
expect(wrapper.find('PencilAltIcon').length).toBe(0); expect(wrapper.find('PencilAltIcon').length).toBe(0);
}); });
test('Toggle button disabled', () => { test('Toggle button disabled', () => {
expect( expect(
wrapper wrapper

View File

@@ -4,9 +4,11 @@ import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { Card, PageSection } from '@patternfly/react-core'; import { Card, PageSection } from '@patternfly/react-core';
import { WorkflowApprovalsAPI } from '../../../api'; import { WorkflowApprovalsAPI } from '../../../api';
import PaginatedDataList, { import PaginatedTable, {
ToolbarDeleteButton, HeaderRow,
} from '../../../components/PaginatedDataList'; HeaderCell,
} from '../../../components/PaginatedTable';
import { ToolbarDeleteButton } from '../../../components/PaginatedDataList';
import AlertModal from '../../../components/AlertModal'; import AlertModal from '../../../components/AlertModal';
import ErrorDetail from '../../../components/ErrorDetail'; import ErrorDetail from '../../../components/ErrorDetail';
import DataListToolbar from '../../../components/DataListToolbar'; import DataListToolbar from '../../../components/DataListToolbar';
@@ -155,7 +157,7 @@ function WorkflowApprovalsList({ i18n }) {
<> <>
<PageSection> <PageSection>
<Card> <Card>
<PaginatedDataList <PaginatedTable
contentError={contentError} contentError={contentError}
hasContentLoading={ hasContentLoading={
isWorkflowApprovalsLoading || isWorkflowApprovalsLoading ||
@@ -181,16 +183,6 @@ function WorkflowApprovalsList({ i18n }) {
]} ]}
toolbarSearchableKeys={searchableKeys} toolbarSearchableKeys={searchableKeys}
toolbarRelatedSearchableKeys={relatedSearchableKeys} toolbarRelatedSearchableKeys={relatedSearchableKeys}
toolbarSortColumns={[
{
name: i18n._(t`Name`),
key: 'name',
},
{
name: i18n._(t`Started`),
key: 'started',
},
]}
renderToolbar={props => ( renderToolbar={props => (
<DataListToolbar <DataListToolbar
{...props} {...props}
@@ -227,7 +219,15 @@ function WorkflowApprovalsList({ i18n }) {
]} ]}
/> />
)} )}
renderItem={workflowApproval => ( headerRow={
<HeaderRow qsConfig={QS_CONFIG}>
<HeaderCell sortKey="name">{i18n._(t`Name`)}</HeaderCell>
<HeaderCell>{i18n._(t`Job`)}</HeaderCell>
<HeaderCell sortKey="started">{i18n._(t`Started`)}</HeaderCell>
<HeaderCell>{i18n._(t`Status`)}</HeaderCell>
</HeaderRow>
}
renderRow={(workflowApproval, index) => (
<WorkflowApprovalListItem <WorkflowApprovalListItem
key={workflowApproval.id} key={workflowApproval.id}
workflowApproval={workflowApproval} workflowApproval={workflowApproval}
@@ -237,6 +237,7 @@ function WorkflowApprovalsList({ i18n }) {
)} )}
onSelect={() => handleSelect(workflowApproval)} onSelect={() => handleSelect(workflowApproval)}
onSuccessfulAction={fetchWorkflowApprovals} onSuccessfulAction={fetchWorkflowApprovals}
rowIndex={index}
/> />
)} )}
/> />

View File

@@ -2,27 +2,14 @@ import React from 'react';
import { withI18n } from '@lingui/react'; import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { string, bool, func } from 'prop-types'; import { string, bool, func } from 'prop-types';
import { import { Label } from '@patternfly/react-core';
DataListCheck, import { Tr, Td } from '@patternfly/react-table';
DataListItem,
DataListItemCells,
DataListItemRow,
Label,
} from '@patternfly/react-core';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import styled from 'styled-components'; import styled from 'styled-components';
import DataListCell from '../../../components/DataListCell';
import { WorkflowApproval } from '../../../types'; import { WorkflowApproval } from '../../../types';
import { formatDateString } from '../../../util/dates'; import { formatDateString } from '../../../util/dates';
import WorkflowApprovalStatus from '../shared/WorkflowApprovalStatus'; import WorkflowApprovalStatus from '../shared/WorkflowApprovalStatus';
const StatusCell = styled(DataListCell)`
@media screen and (min-width: 768px) {
display: flex;
justify-content: flex-end;
}
`;
const JobLabel = styled.b` const JobLabel = styled.b`
margin-right: 24px; margin-right: 24px;
`; `;
@@ -32,6 +19,7 @@ function WorkflowApprovalListItem({
isSelected, isSelected,
onSelect, onSelect,
detailUrl, detailUrl,
rowIndex,
i18n, i18n,
}) { }) {
const labelId = `check-action-${workflowApproval.id}`; const labelId = `check-action-${workflowApproval.id}`;
@@ -62,44 +50,39 @@ function WorkflowApprovalListItem({
}; };
return ( return (
<DataListItem <Tr id={`workflow-approval-row-${workflowApproval.id}`}>
key={workflowApproval.id} <Td
aria-labelledby={labelId} select={{
id={`${workflowApproval.id}`} rowIndex,
> isSelected,
<DataListItemRow> onSelect,
<DataListCheck }}
id={`select-workflowApproval-${workflowApproval.id}`} dataLabel={i18n._(t`Selected`)}
checked={isSelected} />
onChange={onSelect} <Td id={labelId} dataLabel={i18n._(t`Name`)}>
aria-labelledby={labelId} <Link to={`${detailUrl}`}>
/> <b>{workflowApproval.name}</b>
<DataListItemCells </Link>
dataListCells={[ </Td>
<DataListCell key="title"> <Td>
<Link to={`${detailUrl}`}> <>
<b>{workflowApproval.name}</b> <JobLabel>{i18n._(t`Job`)}</JobLabel>
</Link> {workflowJob && workflowJob?.id ? (
</DataListCell>, <Link to={`/jobs/workflow/${workflowJob?.id}`}>
<DataListCell key="job"> {`${workflowJob?.id} - ${workflowJob?.name}`}
<> </Link>
<JobLabel>{i18n._(t`Job`)}</JobLabel> ) : (
{workflowJob && workflowJob?.id ? ( i18n._(t`Deleted`)
<Link to={`/jobs/workflow/${workflowJob?.id}`}> )}
{`${workflowJob?.id} - ${workflowJob?.name}`} </>
</Link> </Td>
) : ( <Td dataLabel={i18n._(t`Started`)}>
i18n._(t`Deleted`) {formatDateString(workflowApproval.started)}
)} </Td>
</> <Td dataLabel={i18n._(t`Status`)}>
</DataListCell>, <div>{getStatus()}</div>
<StatusCell key="status"> </Td>
<div>{getStatus()}</div> </Tr>
</StatusCell>,
]}
/>
</DataListItemRow>
</DataListItem>
); );
} }

View File

@@ -12,91 +12,112 @@ describe('<WorkflowApprovalListItem />', () => {
afterEach(() => { afterEach(() => {
wrapper.unmount(); wrapper.unmount();
}); });
test('should display never expires status', () => { test('should display never expires status', () => {
wrapper = mountWithContexts( wrapper = mountWithContexts(
<WorkflowApprovalListItem <table>
isSelected={false} <tbody>
detailUrl={`/workflow_approvals/${workflowApproval.id}`} <WorkflowApprovalListItem
onSelect={() => {}} isSelected={false}
workflowApproval={workflowApproval} detailUrl={`/workflow_approvals/${workflowApproval.id}`}
/> onSelect={() => {}}
workflowApproval={workflowApproval}
/>
</tbody>
</table>
); );
expect(wrapper.find('Label[children="Never expires"]').length).toBe(1); expect(wrapper.find('Label[children="Never expires"]').length).toBe(1);
}); });
test('should display timed out status', () => { test('should display timed out status', () => {
wrapper = mountWithContexts( wrapper = mountWithContexts(
<WorkflowApprovalListItem <table>
isSelected={false} <tbody>
detailUrl={`/workflow_approvals/${workflowApproval.id}`} <WorkflowApprovalListItem
onSelect={() => {}} isSelected={false}
workflowApproval={{ detailUrl={`/workflow_approvals/${workflowApproval.id}`}
...workflowApproval, onSelect={() => {}}
status: 'failed', workflowApproval={{
timed_out: true, ...workflowApproval,
}} status: 'failed',
/> timed_out: true,
}}
/>
</tbody>
</table>
); );
expect(wrapper.find('Label[children="Timed out"]').length).toBe(1); expect(wrapper.find('Label[children="Timed out"]').length).toBe(1);
}); });
test('should display canceled status', () => { test('should display canceled status', () => {
wrapper = mountWithContexts( wrapper = mountWithContexts(
<WorkflowApprovalListItem <table>
isSelected={false} <tbody>
detailUrl={`/workflow_approvals/${workflowApproval.id}`} <WorkflowApprovalListItem
onSelect={() => {}} isSelected={false}
workflowApproval={{ detailUrl={`/workflow_approvals/${workflowApproval.id}`}
...workflowApproval, onSelect={() => {}}
canceled_on: '2020-10-09T19:59:26.974046Z', workflowApproval={{
status: 'canceled', ...workflowApproval,
}} canceled_on: '2020-10-09T19:59:26.974046Z',
/> status: 'canceled',
}}
/>
</tbody>
</table>
); );
expect(wrapper.find('Label[children="Canceled"]').length).toBe(1); expect(wrapper.find('Label[children="Canceled"]').length).toBe(1);
}); });
test('should display approved status', () => { test('should display approved status', () => {
wrapper = mountWithContexts( wrapper = mountWithContexts(
<WorkflowApprovalListItem <table>
isSelected={false} <tbody>
detailUrl={`/workflow_approvals/${workflowApproval.id}`} <WorkflowApprovalListItem
onSelect={() => {}} isSelected={false}
workflowApproval={{ detailUrl={`/workflow_approvals/${workflowApproval.id}`}
...workflowApproval, onSelect={() => {}}
status: 'successful', workflowApproval={{
summary_fields: { ...workflowApproval,
...workflowApproval.summary_fields, status: 'successful',
approved_or_denied_by: { summary_fields: {
id: 1, ...workflowApproval.summary_fields,
username: 'admin', approved_or_denied_by: {
first_name: '', id: 1,
last_name: '', username: 'admin',
}, first_name: '',
}, last_name: '',
}} },
/> },
}}
/>
</tbody>
</table>
); );
expect(wrapper.find('Label[children="Approved"]').length).toBe(1); expect(wrapper.find('Label[children="Approved"]').length).toBe(1);
}); });
test('should display denied status', () => { test('should display denied status', () => {
wrapper = mountWithContexts( wrapper = mountWithContexts(
<WorkflowApprovalListItem <table>
isSelected={false} <tbody>
detailUrl={`/workflow_approvals/${workflowApproval.id}`} <WorkflowApprovalListItem
onSelect={() => {}} isSelected={false}
workflowApproval={{ detailUrl={`/workflow_approvals/${workflowApproval.id}`}
...workflowApproval, onSelect={() => {}}
failed: true, workflowApproval={{
status: 'failed', ...workflowApproval,
summary_fields: { failed: true,
...workflowApproval.summary_fields, status: 'failed',
approved_or_denied_by: { summary_fields: {
id: 1, ...workflowApproval.summary_fields,
username: 'admin', approved_or_denied_by: {
first_name: '', id: 1,
last_name: '', username: 'admin',
}, first_name: '',
}, last_name: '',
}} },
/> },
}}
/>
</tbody>
</table>
); );
expect(wrapper.find('Label[children="Denied"]').length).toBe(1); expect(wrapper.find('Label[children="Denied"]').length).toBe(1);
}); });