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

@@ -9,7 +9,13 @@
} }
}, },
"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,33 +47,29 @@ 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}
/> />
<DataListItemCells <Td id={labelId} dataLabel={i18n._(t`Name`)}>
dataListCells={[
<DataListCell key="name">
<Link to={`${scheduleBaseUrl}/details`}> <Link to={`${scheduleBaseUrl}/details`}>
<b>{schedule.name}</b> <b>{schedule.name}</b>
</Link> </Link>
</DataListCell>, </Td>
<DataListCell key="type"> <Td dataLabel={i18n._(t`Type`)}>
{ {
jobTypeLabels[ jobTypeLabels[
schedule.summary_fields.unified_job_template.unified_job_type schedule.summary_fields.unified_job_template.unified_job_type
] ]
} }
</DataListCell>, </Td>
<DataListCell key="next_run"> <Td dataLabel={i18n._(t`Next Run`)}>
{schedule.next_run && ( {schedule.next_run && (
<DetailList stacked> <DetailList stacked>
<Detail <Detail
@@ -97,18 +78,13 @@ function ScheduleListItem({ i18n, isSelected, onSelect, schedule }) {
/> />
</DetailList> </DetailList>
)} )}
</DataListCell>, </Td>
]} <ActionsTd dataLabel={i18n._(t`Actions`)} gridColumns="auto 40px">
/>
<DataListAction
aria-label={i18n._(t`actions`)}
aria-labelledby={labelId}
id={labelId}
key="actions"
>
<ScheduleToggle schedule={schedule} /> <ScheduleToggle schedule={schedule} />
{schedule.summary_fields.user_capabilities.edit ? ( <ActionItem
<Tooltip content={i18n._(t`Edit Schedule`)} position="top"> visible={schedule.summary_fields.user_capabilities.edit}
tooltip={i18n._(t`Edit Schedule`)}
>
<Button <Button
aria-label={i18n._(t`Edit Schedule`)} aria-label={i18n._(t`Edit Schedule`)}
css="grid-column: 2" css="grid-column: 2"
@@ -118,13 +94,9 @@ function ScheduleListItem({ i18n, isSelected, onSelect, schedule }) {
> >
<PencilAltIcon /> <PencilAltIcon />
</Button> </Button>
</Tooltip> </ActionItem>
) : ( </ActionsTd>
'' </Tr>
)}
</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(
<table>
<tbody>
<ScheduleListItem <ScheduleListItem
isSelected={false} isSelected={false}
onSelect={onSelect} onSelect={onSelect}
schedule={mockSchedule} 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,18 +109,22 @@ 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(
<table>
<tbody>
<ScheduleListItem <ScheduleListItem
isSelected={false} isSelected={false}
onSelect={onSelect} onSelect={onSelect}
@@ -126,37 +139,44 @@ describe('ScheduleListItem', () => {
}, },
}} }}
/> />
</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,26 +50,21 @@ 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}
aria-labelledby={labelId}
/> />
<DataListItemCells <Td id={labelId} dataLabel={i18n._(t`Name`)}>
dataListCells={[
<DataListCell key="title">
<Link to={`${detailUrl}`}> <Link to={`${detailUrl}`}>
<b>{workflowApproval.name}</b> <b>{workflowApproval.name}</b>
</Link> </Link>
</DataListCell>, </Td>
<DataListCell key="job"> <Td>
<> <>
<JobLabel>{i18n._(t`Job`)}</JobLabel> <JobLabel>{i18n._(t`Job`)}</JobLabel>
{workflowJob && workflowJob?.id ? ( {workflowJob && workflowJob?.id ? (
@@ -92,14 +75,14 @@ function WorkflowApprovalListItem({
i18n._(t`Deleted`) i18n._(t`Deleted`)
)} )}
</> </>
</DataListCell>, </Td>
<StatusCell key="status"> <Td dataLabel={i18n._(t`Started`)}>
{formatDateString(workflowApproval.started)}
</Td>
<Td dataLabel={i18n._(t`Status`)}>
<div>{getStatus()}</div> <div>{getStatus()}</div>
</StatusCell>, </Td>
]} </Tr>
/>
</DataListItemRow>
</DataListItem>
); );
} }

View File

@@ -12,19 +12,26 @@ describe('<WorkflowApprovalListItem />', () => {
afterEach(() => { afterEach(() => {
wrapper.unmount(); wrapper.unmount();
}); });
test('should display never expires status', () => { test('should display never expires status', () => {
wrapper = mountWithContexts( wrapper = mountWithContexts(
<table>
<tbody>
<WorkflowApprovalListItem <WorkflowApprovalListItem
isSelected={false} isSelected={false}
detailUrl={`/workflow_approvals/${workflowApproval.id}`} detailUrl={`/workflow_approvals/${workflowApproval.id}`}
onSelect={() => {}} onSelect={() => {}}
workflowApproval={workflowApproval} 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(
<table>
<tbody>
<WorkflowApprovalListItem <WorkflowApprovalListItem
isSelected={false} isSelected={false}
detailUrl={`/workflow_approvals/${workflowApproval.id}`} detailUrl={`/workflow_approvals/${workflowApproval.id}`}
@@ -35,11 +42,15 @@ describe('<WorkflowApprovalListItem />', () => {
timed_out: true, 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(
<table>
<tbody>
<WorkflowApprovalListItem <WorkflowApprovalListItem
isSelected={false} isSelected={false}
detailUrl={`/workflow_approvals/${workflowApproval.id}`} detailUrl={`/workflow_approvals/${workflowApproval.id}`}
@@ -50,11 +61,15 @@ describe('<WorkflowApprovalListItem />', () => {
status: 'canceled', 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(
<table>
<tbody>
<WorkflowApprovalListItem <WorkflowApprovalListItem
isSelected={false} isSelected={false}
detailUrl={`/workflow_approvals/${workflowApproval.id}`} detailUrl={`/workflow_approvals/${workflowApproval.id}`}
@@ -73,11 +88,15 @@ describe('<WorkflowApprovalListItem />', () => {
}, },
}} }}
/> />
</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(
<table>
<tbody>
<WorkflowApprovalListItem <WorkflowApprovalListItem
isSelected={false} isSelected={false}
detailUrl={`/workflow_approvals/${workflowApproval.id}`} detailUrl={`/workflow_approvals/${workflowApproval.id}`}
@@ -97,6 +116,8 @@ describe('<WorkflowApprovalListItem />', () => {
}, },
}} }}
/> />
</tbody>
</table>
); );
expect(wrapper.find('Label[children="Denied"]').length).toBe(1); expect(wrapper.find('Label[children="Denied"]').length).toBe(1);
}); });