Merge pull request #7993 from mabashian/7364-jobs-list-cancel

Adds cancel button to jobs list

Reviewed-by: https://github.com/apps/softwarefactory-project-zuul
This commit is contained in:
softwarefactory-project-zuul[bot]
2020-09-11 15:07:30 +00:00
committed by GitHub
9 changed files with 589 additions and 171 deletions

View File

@@ -1,13 +1,29 @@
import Base from '../Base';
import RelaunchMixin from '../mixins/Relaunch.mixin';
const BASE_URLS = {
playbook: '/jobs/',
project: '/project_updates/',
system: '/system_jobs/',
inventory: '/inventory_updates/',
command: '/ad_hoc_commands/',
workflow: '/workflow_jobs/',
const getBaseURL = type => {
switch (type) {
case 'playbook':
case 'job':
return '/jobs/';
case 'project':
case 'project_update':
return '/project_updates/';
case 'system':
case 'system_job':
return '/system_jobs/';
case 'inventory':
case 'inventory_update':
return '/inventory_updates/';
case 'command':
case 'ad_hoc_command':
return '/ad_hoc_commands/';
case 'workflow':
case 'workflow_job':
return '/workflow_jobs/';
default:
throw new Error('Unable to find matching job type');
}
};
class Jobs extends RelaunchMixin(Base) {
@@ -16,16 +32,20 @@ class Jobs extends RelaunchMixin(Base) {
this.baseUrl = '/api/v2/jobs/';
}
cancel(id, type) {
return this.http.post(`/api/v2${getBaseURL(type)}${id}/cancel/`);
}
readDetail(id, type) {
return this.http.get(`/api/v2${BASE_URLS[type]}${id}/`);
return this.http.get(`/api/v2${getBaseURL(type)}${id}/`);
}
readEvents(id, type = 'playbook', params = {}) {
let endpoint;
if (type === 'playbook') {
endpoint = `/api/v2${BASE_URLS[type]}${id}/job_events/`;
endpoint = `/api/v2${getBaseURL(type)}${id}/job_events/`;
} else {
endpoint = `/api/v2${BASE_URLS[type]}${id}/events/`;
endpoint = `/api/v2${getBaseURL(type)}${id}/events/`;
}
return this.http.get(endpoint, { params });
}

View File

@@ -1,4 +1,4 @@
import React, { Fragment, useState } from 'react';
import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
@@ -42,14 +42,21 @@ function DataListToolbar({
pagination,
}) {
const showExpandCollapse = onCompact && onExpand;
const [kebabIsOpen, setKebabIsOpen] = useState(false);
const [advancedSearchShown, setAdvancedSearchShown] = useState(false);
const [isKebabOpen, setIsKebabOpen] = useState(false);
const [isKebabModalOpen, setIsKebabModalOpen] = useState(false);
const [isAdvancedSearchShown, setIsAdvancedSearchShown] = useState(false);
const onShowAdvancedSearch = shown => {
setAdvancedSearchShown(shown);
setKebabIsOpen(false);
setIsAdvancedSearchShown(shown);
setIsKebabOpen(false);
};
useEffect(() => {
if (!isKebabModalOpen) {
setIsKebabOpen(false);
}
}, [isKebabModalOpen]);
return (
<Toolbar
id={`${qsConfig.namespace}-list-toolbar`}
@@ -91,7 +98,7 @@ function DataListToolbar({
</ToolbarToggleGroup>
{showExpandCollapse && (
<ToolbarGroup>
<Fragment>
<>
<ToolbarItem>
<ExpandCollapse
isCompact={isCompact}
@@ -99,33 +106,42 @@ function DataListToolbar({
onExpand={onExpand}
/>
</ToolbarItem>
</Fragment>
</>
</ToolbarGroup>
)}
{advancedSearchShown && (
{isAdvancedSearchShown && (
<ToolbarItem>
<Dropdown
toggle={<KebabToggle onToggle={setKebabIsOpen} />}
isOpen={kebabIsOpen}
isPlain
dropdownItems={additionalControls.map(control => {
return (
<KebabifiedProvider value={{ isKebabified: true }}>
{control}
</KebabifiedProvider>
);
})}
/>
<KebabifiedProvider
value={{
isKebabified: true,
onKebabModalChange: setIsKebabModalOpen,
}}
>
<Dropdown
toggle={
<KebabToggle
onToggle={isOpen => {
if (!isKebabModalOpen) {
setIsKebabOpen(isOpen);
}
}}
/>
}
isOpen={isKebabOpen}
isPlain
dropdownItems={additionalControls}
/>
</KebabifiedProvider>
</ToolbarItem>
)}
{!advancedSearchShown && (
{!isAdvancedSearchShown && (
<ToolbarGroup>
{additionalControls.map(control => (
<ToolbarItem key={control.key}>{control}</ToolbarItem>
))}
</ToolbarGroup>
)}
{!advancedSearchShown && pagination && itemCount > 0 && (
{!isAdvancedSearchShown && pagination && itemCount > 0 && (
<ToolbarItem variant="pagination">{pagination}</ToolbarItem>
)}
</ToolbarContent>

View File

@@ -8,9 +8,13 @@ import AlertModal from '../AlertModal';
import DatalistToolbar from '../DataListToolbar';
import ErrorDetail from '../ErrorDetail';
import PaginatedDataList, { ToolbarDeleteButton } from '../PaginatedDataList';
import useRequest, { useDeleteItems } from '../../util/useRequest';
import useRequest, {
useDeleteItems,
useDismissableError,
} from '../../util/useRequest';
import { getQSConfig, parseQueryString } from '../../util/qs';
import JobListItem from './JobListItem';
import JobListCancelButton from './JobListCancelButton';
import useWsJobs from './useWsJobs';
import {
AdHocCommandsAPI,
@@ -88,6 +92,30 @@ function JobList({ i18n, defaultParams, showTypeColumn = false }) {
const jobs = useWsJobs(results, fetchJobsById, QS_CONFIG);
const isAllSelected = selected.length === jobs.length && selected.length > 0;
const {
error: cancelJobsError,
isLoading: isCancelLoading,
request: cancelJobs,
} = useRequest(
useCallback(async () => {
return Promise.all(
selected.map(job => {
if (['new', 'pending', 'waiting', 'running'].includes(job.status)) {
return JobsAPI.cancel(job.id, job.type);
}
return Promise.resolve();
})
);
}, [selected]),
{}
);
const {
error: cancelError,
dismissError: dismissCancelError,
} = useDismissableError(cancelJobsError);
const {
isLoading: isDeleteLoading,
deleteItems: deleteJobs,
@@ -123,6 +151,11 @@ function JobList({ i18n, defaultParams, showTypeColumn = false }) {
}
);
const handleJobCancel = async () => {
await cancelJobs();
setSelected([]);
};
const handleJobDelete = async () => {
await deleteJobs();
setSelected([]);
@@ -145,7 +178,7 @@ function JobList({ i18n, defaultParams, showTypeColumn = false }) {
<Card>
<PaginatedDataList
contentError={contentError}
hasContentLoading={isLoading || isDeleteLoading}
hasContentLoading={isLoading || isDeleteLoading || isCancelLoading}
items={jobs}
itemCount={count}
pluralizedItemName={i18n._(t`Jobs`)}
@@ -242,6 +275,11 @@ function JobList({ i18n, defaultParams, showTypeColumn = false }) {
itemsToDelete={selected}
pluralizedItemName="Jobs"
/>,
<JobListCancelButton
key="cancel"
onCancel={handleJobCancel}
jobsToCancel={selected}
/>,
]}
/>
)}
@@ -256,15 +294,28 @@ function JobList({ i18n, defaultParams, showTypeColumn = false }) {
)}
/>
</Card>
<AlertModal
isOpen={deletionError}
variant="error"
title={i18n._(t`Error!`)}
onClose={clearDeletionError}
>
{i18n._(t`Failed to delete one or more jobs.`)}
<ErrorDetail error={deletionError} />
</AlertModal>
{deletionError && (
<AlertModal
isOpen
variant="error"
title={i18n._(t`Error!`)}
onClose={clearDeletionError}
>
{i18n._(t`Failed to delete one or more jobs.`)}
<ErrorDetail error={deletionError} />
</AlertModal>
)}
{cancelError && (
<AlertModal
isOpen
variant="error"
title={i18n._(t`Error!`)}
onClose={dismissCancelError}
>
{i18n._(t`Failed to cancel one or more jobs.`)}
<ErrorDetail error={cancelError} />
</AlertModal>
)}
</>
);
}

View File

@@ -23,6 +23,10 @@ const mockResults = [
url: '/api/v2/project_updates/1',
name: 'job 1',
type: 'project_update',
status: 'running',
related: {
cancel: '/api/v2/project_updates/1/cancel',
},
summary_fields: {
user_capabilities: {
delete: true,
@@ -35,6 +39,10 @@ const mockResults = [
url: '/api/v2/jobs/2',
name: 'job 2',
type: 'job',
status: 'running',
related: {
cancel: '/api/v2/jobs/2/cancel',
},
summary_fields: {
user_capabilities: {
delete: true,
@@ -47,6 +55,10 @@ const mockResults = [
url: '/api/v2/inventory_updates/3',
name: 'job 3',
type: 'inventory_update',
status: 'running',
related: {
cancel: '/api/v2/inventory_updates/3/cancel',
},
summary_fields: {
user_capabilities: {
delete: true,
@@ -59,6 +71,10 @@ const mockResults = [
url: '/api/v2/workflow_jobs/4',
name: 'job 4',
type: 'workflow_job',
status: 'running',
related: {
cancel: '/api/v2/workflow_jobs/4/cancel',
},
summary_fields: {
user_capabilities: {
delete: true,
@@ -71,6 +87,10 @@ const mockResults = [
url: '/api/v2/system_jobs/5',
name: 'job 5',
type: 'system_job',
status: 'running',
related: {
cancel: '/api/v2/system_jobs/5/cancel',
},
summary_fields: {
user_capabilities: {
delete: true,
@@ -83,6 +103,10 @@ const mockResults = [
url: '/api/v2/ad_hoc_commands/6',
name: 'job 6',
type: 'ad_hoc_command',
status: 'running',
related: {
cancel: '/api/v2/ad_hoc_commands/6/cancel',
},
summary_fields: {
user_capabilities: {
delete: true,
@@ -273,4 +297,72 @@ describe('<JobList />', () => {
el => el.props().isOpen === true && el.props().title === 'Error!'
);
});
test('should send all corresponding delete API requests', async () => {
JobsAPI.cancel = jest.fn();
let wrapper;
await act(async () => {
wrapper = mountWithContexts(<JobList />);
});
await waitForLoaded(wrapper);
act(() => {
wrapper.find('DataListToolbar').invoke('onSelectAll')(true);
});
wrapper.update();
wrapper.find('JobListItem');
expect(
wrapper.find('JobListCancelButton').prop('jobsToCancel')
).toHaveLength(6);
await act(async () => {
wrapper.find('JobListCancelButton').invoke('onCancel')();
});
expect(JobsAPI.cancel).toHaveBeenCalledTimes(6);
expect(JobsAPI.cancel).toHaveBeenCalledWith(1, 'project_update');
expect(JobsAPI.cancel).toHaveBeenCalledWith(2, 'job');
expect(JobsAPI.cancel).toHaveBeenCalledWith(3, 'inventory_update');
expect(JobsAPI.cancel).toHaveBeenCalledWith(4, 'workflow_job');
expect(JobsAPI.cancel).toHaveBeenCalledWith(5, 'system_job');
expect(JobsAPI.cancel).toHaveBeenCalledWith(6, 'ad_hoc_command');
jest.restoreAllMocks();
});
test('error is shown when job not successfully cancelled', async () => {
JobsAPI.cancel.mockImplementation(() => {
throw new Error({
response: {
config: {
method: 'post',
url: '/api/v2/jobs/2/cancel',
},
data: 'An error occurred',
},
});
});
let wrapper;
await act(async () => {
wrapper = mountWithContexts(<JobList />);
});
await waitForLoaded(wrapper);
await act(async () => {
wrapper
.find('JobListItem')
.at(1)
.invoke('onSelect')();
});
wrapper.update();
await act(async () => {
wrapper.find('JobListCancelButton').invoke('onCancel')();
});
wrapper.update();
await waitForElement(
wrapper,
'Modal',
el => el.props().isOpen === true && el.props().title === 'Error!'
);
});
});

View File

@@ -0,0 +1,156 @@
import React, { useContext, useEffect, useState } from 'react';
import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
import { arrayOf, func } from 'prop-types';
import { Button, DropdownItem, Tooltip } from '@patternfly/react-core';
import { KebabifiedContext } from '../../contexts/Kebabified';
import AlertModal from '../AlertModal';
import { Job } from '../../types';
function cannotCancel(job) {
return !job.summary_fields.user_capabilities.start;
}
function JobListCancelButton({ i18n, jobsToCancel, onCancel }) {
const { isKebabified, onKebabModalChange } = useContext(KebabifiedContext);
const [isModalOpen, setIsModalOpen] = useState(false);
const numJobsToCancel = jobsToCancel.length;
const zeroOrOneJobSelected = numJobsToCancel < 2;
const handleCancelJob = () => {
onCancel();
toggleModal();
};
const toggleModal = () => {
setIsModalOpen(!isModalOpen);
};
useEffect(() => {
if (isKebabified) {
onKebabModalChange(isModalOpen);
}
}, [isKebabified, isModalOpen, onKebabModalChange]);
const renderTooltip = () => {
const jobsUnableToCancel = jobsToCancel
.filter(cannotCancel)
.map(job => job.name);
const numJobsUnableToCancel = jobsUnableToCancel.length;
if (numJobsUnableToCancel > 0) {
return (
<div>
{i18n._(
'{numJobsUnableToCancel, plural, one {You do not have permission to cancel the following job:} other {You do not have permission to cancel the following jobs:}}',
{
numJobsUnableToCancel,
}
)}
{' '.concat(jobsUnableToCancel.join(', '))}
</div>
);
}
if (numJobsToCancel > 0) {
return i18n._(
'{numJobsToCancel, plural, one {Cancel selected job} other {Cancel selected jobs}}',
{
numJobsToCancel,
}
);
}
return i18n._(t`Select a job to cancel`);
};
const isDisabled =
jobsToCancel.length === 0 || jobsToCancel.some(cannotCancel);
const cancelJobText = i18n._(
'{zeroOrOneJobSelected, plural, one {Cancel job} other {Cancel jobs}}',
{
zeroOrOneJobSelected,
}
);
return (
<>
{isKebabified ? (
<DropdownItem
key="cancel-job"
isDisabled={isDisabled}
component="button"
onClick={toggleModal}
>
{cancelJobText}
</DropdownItem>
) : (
<Tooltip content={renderTooltip()} position="top">
<div>
<Button
variant="secondary"
aria-label={cancelJobText}
onClick={toggleModal}
isDisabled={isDisabled}
>
{cancelJobText}
</Button>
</div>
</Tooltip>
)}
{isModalOpen && (
<AlertModal
variant="danger"
title={cancelJobText}
isOpen={isModalOpen}
onClose={toggleModal}
actions={[
<Button
id="cancel-job-confirm-button"
key="delete"
variant="danger"
aria-label={cancelJobText}
onClick={handleCancelJob}
>
{cancelJobText}
</Button>,
<Button
id="cancel-job-return-button"
key="cancel"
variant="secondary"
aria-label={i18n._(t`Return`)}
onClick={toggleModal}
>
{i18n._(t`Return`)}
</Button>,
]}
>
<div>
{i18n._(
'{numJobsToCancel, plural, one {This action will cancel the following job:} other {This action will cancel the following jobs:}}',
{
numJobsToCancel,
}
)}
</div>
{jobsToCancel.map(job => (
<span key={job.id}>
<strong>{job.name}</strong>
<br />
</span>
))}
</AlertModal>
)}
</>
);
}
JobListCancelButton.propTypes = {
jobsToCancel: arrayOf(Job),
onCancel: func,
};
JobListCancelButton.defaultProps = {
jobsToCancel: [],
onCancel: () => {},
};
export default withI18n()(JobListCancelButton);

View File

@@ -0,0 +1,97 @@
import React from 'react';
import { mountWithContexts } from '../../../testUtils/enzymeHelpers';
import JobListCancelButton from './JobListCancelButton';
describe('<JobListCancelButton />', () => {
let wrapper;
afterEach(() => {
wrapper.unmount();
});
test('should be disabled when no rows are selected', () => {
wrapper = mountWithContexts(<JobListCancelButton jobsToCancel={[]} />);
expect(wrapper.find('JobListCancelButton button').props().disabled).toBe(
true
);
expect(wrapper.find('Tooltip').props().content).toBe(
'Select a job to cancel'
);
});
test('should be disabled when user does not have permissions to cancel selected job', () => {
wrapper = mountWithContexts(
<JobListCancelButton
jobsToCancel={[
{
id: 1,
name: 'some job',
summary_fields: {
user_capabilities: {
delete: false,
start: false,
},
},
},
]}
/>
);
expect(wrapper.find('JobListCancelButton button').props().disabled).toBe(
true
);
});
test('should be enabled when user does have permission to cancel selected job', () => {
wrapper = mountWithContexts(
<JobListCancelButton
jobsToCancel={[
{
id: 1,
name: 'some job',
summary_fields: {
user_capabilities: {
delete: true,
start: true,
},
},
},
]}
/>
);
expect(wrapper.find('JobListCancelButton button').props().disabled).toBe(
false
);
});
test('modal functions as expected', () => {
const onCancel = jest.fn();
wrapper = mountWithContexts(
<JobListCancelButton
jobsToCancel={[
{
id: 1,
name: 'some job',
summary_fields: {
user_capabilities: {
delete: true,
start: true,
},
},
},
]}
onCancel={onCancel}
/>
);
expect(wrapper.find('AlertModal').length).toBe(0);
wrapper.find('JobListCancelButton button').simulate('click');
wrapper.update();
expect(wrapper.find('AlertModal').length).toBe(1);
wrapper.find('button#cancel-job-return-button').simulate('click');
wrapper.update();
expect(onCancel).toHaveBeenCalledTimes(0);
expect(wrapper.find('AlertModal').length).toBe(0);
expect(wrapper.find('AlertModal').length).toBe(0);
wrapper.find('JobListCancelButton button').simulate('click');
wrapper.update();
expect(wrapper.find('AlertModal').length).toBe(1);
wrapper.find('button#cancel-job-confirm-button').simulate('click');
wrapper.update();
expect(onCancel).toHaveBeenCalledTimes(1);
});
});

View File

@@ -1,4 +1,4 @@
import React, { Fragment } from 'react';
import React, { useContext, useEffect, useState } from 'react';
import {
func,
bool,
@@ -12,7 +12,7 @@ import { Button, DropdownItem, Tooltip } from '@patternfly/react-core';
import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
import AlertModal from '../AlertModal';
import { Kebabified } from '../../contexts/Kebabified';
import { KebabifiedContext } from '../../contexts/Kebabified';
const requireNameOrUsername = props => {
const { name, username } = props;
@@ -59,53 +59,32 @@ function cannotDelete(item) {
return !item.summary_fields.user_capabilities.delete;
}
class ToolbarDeleteButton extends React.Component {
static propTypes = {
onDelete: func.isRequired,
itemsToDelete: arrayOf(ItemToDelete).isRequired,
pluralizedItemName: string,
errorMessage: string,
};
function ToolbarDeleteButton({
itemsToDelete,
pluralizedItemName,
errorMessage,
onDelete,
i18n,
}) {
const { isKebabified, onKebabModalChange } = useContext(KebabifiedContext);
const [isModalOpen, setIsModalOpen] = useState(false);
static defaultProps = {
pluralizedItemName: 'Items',
errorMessage: '',
};
constructor(props) {
super(props);
this.state = {
isModalOpen: false,
};
this.handleConfirmDelete = this.handleConfirmDelete.bind(this);
this.handleCancelDelete = this.handleCancelDelete.bind(this);
this.handleDelete = this.handleDelete.bind(this);
}
handleConfirmDelete() {
this.setState({ isModalOpen: true });
}
handleCancelDelete() {
this.setState({ isModalOpen: false });
}
handleDelete() {
const { onDelete } = this.props;
const handleDelete = () => {
onDelete();
this.setState({ isModalOpen: false });
}
toggleModal();
};
renderTooltip() {
const {
itemsToDelete,
pluralizedItemName,
errorMessage,
i18n,
} = this.props;
const toggleModal = () => {
setIsModalOpen(!isModalOpen);
};
useEffect(() => {
if (isKebabified) {
onKebabModalChange(isModalOpen);
}
}, [isKebabified, isModalOpen, onKebabModalChange]);
const renderTooltip = () => {
const itemsUnableToDelete = itemsToDelete
.filter(cannotDelete)
.map(item => item.name)
@@ -125,85 +104,89 @@ class ToolbarDeleteButton extends React.Component {
return i18n._(t`Delete`);
}
return i18n._(t`Select a row to delete`);
}
};
render() {
const { itemsToDelete, pluralizedItemName, i18n } = this.props;
const { isModalOpen } = this.state;
const modalTitle = i18n._(t`Delete ${pluralizedItemName}?`);
const modalTitle = i18n._(t`Delete ${pluralizedItemName}?`);
const isDisabled =
itemsToDelete.length === 0 || itemsToDelete.some(cannotDelete);
const isDisabled =
itemsToDelete.length === 0 || itemsToDelete.some(cannotDelete);
// NOTE: Once PF supports tooltips on disabled elements,
// we can delete the extra <div> around the <DeleteButton> below.
// See: https://github.com/patternfly/patternfly-react/issues/1894
return (
<Kebabified>
{({ isKebabified }) => (
<Fragment>
{isKebabified ? (
<DropdownItem
key="add"
isDisabled={isDisabled}
component="Button"
onClick={this.handleConfirmDelete}
>
{i18n._(t`Delete`)}
</DropdownItem>
) : (
<Tooltip content={this.renderTooltip()} position="top">
<div>
<Button
variant="secondary"
aria-label={i18n._(t`Delete`)}
onClick={this.handleConfirmDelete}
isDisabled={isDisabled}
>
{i18n._(t`Delete`)}
</Button>
</div>
</Tooltip>
)}
{isModalOpen && (
<AlertModal
variant="danger"
title={modalTitle}
isOpen={isModalOpen}
onClose={this.handleCancelDelete}
actions={[
<Button
key="delete"
variant="danger"
aria-label={i18n._(t`confirm delete`)}
onClick={this.handleDelete}
>
{i18n._(t`Delete`)}
</Button>,
<Button
key="cancel"
variant="secondary"
aria-label={i18n._(t`cancel delete`)}
onClick={this.handleCancelDelete}
>
{i18n._(t`Cancel`)}
</Button>,
]}
>
<div>{i18n._(t`This action will delete the following:`)}</div>
{itemsToDelete.map(item => (
<span key={item.id}>
<strong>{item.name || item.username}</strong>
<br />
</span>
))}
</AlertModal>
)}
</Fragment>
)}
</Kebabified>
);
}
// NOTE: Once PF supports tooltips on disabled elements,
// we can delete the extra <div> around the <DeleteButton> below.
// See: https://github.com/patternfly/patternfly-react/issues/1894
return (
<>
{isKebabified ? (
<DropdownItem
key="add"
isDisabled={isDisabled}
component="button"
onClick={toggleModal}
>
{i18n._(t`Delete`)}
</DropdownItem>
) : (
<Tooltip content={renderTooltip()} position="top">
<div>
<Button
variant="secondary"
aria-label={i18n._(t`Delete`)}
onClick={toggleModal}
isDisabled={isDisabled}
>
{i18n._(t`Delete`)}
</Button>
</div>
</Tooltip>
)}
{isModalOpen && (
<AlertModal
variant="danger"
title={modalTitle}
isOpen={isModalOpen}
onClose={toggleModal}
actions={[
<Button
key="delete"
variant="danger"
aria-label={i18n._(t`confirm delete`)}
onClick={handleDelete}
>
{i18n._(t`Delete`)}
</Button>,
<Button
key="cancel"
variant="secondary"
aria-label={i18n._(t`cancel delete`)}
onClick={toggleModal}
>
{i18n._(t`Cancel`)}
</Button>,
]}
>
<div>{i18n._(t`This action will delete the following:`)}</div>
{itemsToDelete.map(item => (
<span key={item.id}>
<strong>{item.name || item.username}</strong>
<br />
</span>
))}
</AlertModal>
)}
</>
);
}
ToolbarDeleteButton.propTypes = {
onDelete: func.isRequired,
itemsToDelete: arrayOf(ItemToDelete).isRequired,
pluralizedItemName: string,
errorMessage: string,
};
ToolbarDeleteButton.defaultProps = {
pluralizedItemName: 'Items',
errorMessage: '',
};
export default withI18n()(ToolbarDeleteButton);

View File

@@ -26,8 +26,8 @@ describe('<ToolbarDeleteButton />', () => {
const wrapper = mountWithContexts(
<ToolbarDeleteButton onDelete={() => {}} itemsToDelete={[itemA]} />
);
expect(wrapper.find('Modal')).toHaveLength(0);
wrapper.find('button').simulate('click');
expect(wrapper.find('ToolbarDeleteButton').state('isModalOpen')).toBe(true);
wrapper.update();
expect(wrapper.find('Modal')).toHaveLength(1);
});
@@ -37,15 +37,14 @@ describe('<ToolbarDeleteButton />', () => {
const wrapper = mountWithContexts(
<ToolbarDeleteButton onDelete={onDelete} itemsToDelete={[itemA]} />
);
wrapper.find('ToolbarDeleteButton').setState({ isModalOpen: true });
wrapper.find('button').simulate('click');
wrapper.update();
wrapper
.find('ModalBoxFooter button[aria-label="confirm delete"]')
.simulate('click');
wrapper.update();
expect(onDelete).toHaveBeenCalled();
expect(wrapper.find('ToolbarDeleteButton').state('isModalOpen')).toBe(
false
);
expect(wrapper.find('Modal')).toHaveLength(0);
});
test('should disable button when no delete permissions', () => {

View File

@@ -261,7 +261,9 @@ describe('<TemplateList />', () => {
},
});
wrapper.find('button[aria-label="Delete"]').prop('onClick')();
await act(async () => {
wrapper.find('button[aria-label="Delete"]').prop('onClick')();
});
wrapper.update();
await act(async () => {
await wrapper
@@ -302,7 +304,9 @@ describe('<TemplateList />', () => {
summary_fields: { user_capabilities: { delete: true } },
},
});
wrapper.find('button[aria-label="Delete"]').prop('onClick')();
await act(async () => {
wrapper.find('button[aria-label="Delete"]').prop('onClick')();
});
wrapper.update();
await act(async () => {
await wrapper