mirror of
https://github.com/ansible/awx.git
synced 2026-01-18 05:01:19 -03:30
Refactors and redesigns workflow approval to impove UX
This commit is contained in:
parent
65771b7629
commit
1fca505b61
@ -20,12 +20,7 @@ function NavExpandableGroup(props) {
|
||||
if (routes.length === 1 && groupId === 'settings') {
|
||||
const [{ path }] = routes;
|
||||
return (
|
||||
<NavItem
|
||||
itemId={groupId}
|
||||
isActive={isActivePath(path)}
|
||||
key={path}
|
||||
// ouiaId={path}
|
||||
>
|
||||
<NavItem itemId={groupId} isActive={isActivePath(path)} key={path}>
|
||||
<Link to={path}>{groupTitle}</Link>
|
||||
</NavItem>
|
||||
);
|
||||
@ -40,12 +35,7 @@ function NavExpandableGroup(props) {
|
||||
title={groupTitle}
|
||||
>
|
||||
{routes.map(({ path, title }) => (
|
||||
<NavItem
|
||||
groupId={groupId}
|
||||
isActive={isActivePath(path)}
|
||||
key={path}
|
||||
// ouiaId={path}
|
||||
>
|
||||
<NavItem groupId={groupId} isActive={isActivePath(path)} key={path}>
|
||||
<Link to={path}>{title}</Link>
|
||||
</NavItem>
|
||||
))}
|
||||
|
||||
@ -15,6 +15,9 @@ function JobCancelButton({
|
||||
buttonText,
|
||||
style = {},
|
||||
job = {},
|
||||
isDisabled,
|
||||
tooltip,
|
||||
cancelationMessage,
|
||||
}) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const { error: cancelError, request: cancelJob } = useRequest(
|
||||
@ -28,33 +31,40 @@ function JobCancelButton({
|
||||
useDismissableError(cancelError);
|
||||
|
||||
const isAlreadyCancelled = cancelError?.response?.status === 405;
|
||||
|
||||
const renderTooltip = () => {
|
||||
if (tooltip) {
|
||||
return tooltip;
|
||||
}
|
||||
return isAlreadyCancelled ? null : title;
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<Tooltip content={isAlreadyCancelled ? null : title}>
|
||||
{showIconButton ? (
|
||||
<Button
|
||||
isDisabled={isAlreadyCancelled}
|
||||
aria-label={title}
|
||||
ouiaId="cancel-job-button"
|
||||
onClick={() => setIsOpen(true)}
|
||||
variant="plain"
|
||||
style={style}
|
||||
>
|
||||
<MinusCircleIcon />
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
isDisabled={isAlreadyCancelled}
|
||||
aria-label={title}
|
||||
variant="secondary"
|
||||
ouiaId="cancel-job-button"
|
||||
onClick={() => setIsOpen(true)}
|
||||
style={style}
|
||||
>
|
||||
{buttonText || t`Cancel Job`}
|
||||
</Button>
|
||||
)}
|
||||
<Tooltip content={renderTooltip()}>
|
||||
<div>
|
||||
{showIconButton ? (
|
||||
<Button
|
||||
isDisabled={isDisabled || isAlreadyCancelled}
|
||||
aria-label={title}
|
||||
ouiaId="cancel-job-button"
|
||||
onClick={() => setIsOpen(true)}
|
||||
variant="plain"
|
||||
style={style}
|
||||
>
|
||||
<MinusCircleIcon />
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
isDisabled={isDisabled || isAlreadyCancelled}
|
||||
aria-label={title}
|
||||
variant="secondary"
|
||||
ouiaId="cancel-job-button"
|
||||
onClick={() => setIsOpen(true)}
|
||||
style={style}
|
||||
>
|
||||
{buttonText || t`Cancel Job`}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</Tooltip>
|
||||
{isOpen && (
|
||||
<AlertModal
|
||||
@ -86,7 +96,7 @@ function JobCancelButton({
|
||||
</Button>,
|
||||
]}
|
||||
>
|
||||
{t`Are you sure you want to cancel this job?`}
|
||||
{cancelationMessage ?? t`Are you sure you want to cancel this job?`}
|
||||
</AlertModal>
|
||||
)}
|
||||
{error && !isAlreadyCancelled && (
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
|
||||
import { t } from '@lingui/macro';
|
||||
import { Link, useHistory, useParams } from 'react-router-dom';
|
||||
@ -26,13 +26,14 @@ import {
|
||||
import useRequest, { useDismissableError } from 'hooks/useRequest';
|
||||
import { WorkflowApproval } from 'types';
|
||||
import StatusLabel from 'components/StatusLabel';
|
||||
import JobCancelButton from 'components/JobCancelButton';
|
||||
import WorkflowApprovalButton from '../shared/WorkflowApprovalButton';
|
||||
import WorkflowDenyButton from '../shared/WorkflowDenyButton';
|
||||
import {
|
||||
getDetailPendingLabel,
|
||||
getStatus,
|
||||
} from '../shared/WorkflowApprovalUtils';
|
||||
|
||||
import WorkflowApprovalControls from '../shared/WorkflowApprovalControls';
|
||||
|
||||
const Divider = styled(PFDivider)`
|
||||
margin-top: var(--pf-global--spacer--lg);
|
||||
margin-bottom: var(--pf-global--spacer--lg);
|
||||
@ -49,7 +50,6 @@ const WFDetailList = styled(DetailList)`
|
||||
|
||||
function WorkflowApprovalDetail({ workflowApproval }) {
|
||||
const { id: workflowApprovalId } = useParams();
|
||||
const [isKebabOpen, setIsKebabModalOpen] = useState(false);
|
||||
const history = useHistory();
|
||||
const {
|
||||
request: deleteWorkflowApproval,
|
||||
@ -65,50 +65,6 @@ function WorkflowApprovalDetail({ workflowApproval }) {
|
||||
const { error: deleteError, dismissError: dismissDeleteError } =
|
||||
useDismissableError(deleteApprovalError);
|
||||
|
||||
const {
|
||||
error: approveApprovalError,
|
||||
isLoading: isApproveLoading,
|
||||
request: approveWorkflowApproval,
|
||||
} = useRequest(
|
||||
useCallback(async () => {
|
||||
await WorkflowApprovalsAPI.approve(workflowApprovalId);
|
||||
history.push(`/workflow_approvals/${workflowApprovalId}`);
|
||||
}, [workflowApprovalId, history]),
|
||||
{}
|
||||
);
|
||||
|
||||
const { error: approveError, dismissError: dismissApproveError } =
|
||||
useDismissableError(approveApprovalError);
|
||||
|
||||
const {
|
||||
error: denyApprovalError,
|
||||
isLoading: isDenyLoading,
|
||||
request: denyWorkflowApproval,
|
||||
} = useRequest(
|
||||
useCallback(async () => {
|
||||
await WorkflowApprovalsAPI.deny(workflowApprovalId);
|
||||
history.push(`/workflow_approvals/${workflowApprovalId}`);
|
||||
}, [workflowApprovalId, history]),
|
||||
{}
|
||||
);
|
||||
|
||||
const { error: denyError, dismissError: dismissDenyError } =
|
||||
useDismissableError(denyApprovalError);
|
||||
|
||||
const {
|
||||
error: cancelApprovalError,
|
||||
isLoading: isCancelLoading,
|
||||
request: cancelWorkflowApprovals,
|
||||
} = useRequest(
|
||||
useCallback(async () => {
|
||||
await WorkflowJobsAPI.cancel(
|
||||
workflowApproval.summary_fields.source_workflow_job.id
|
||||
);
|
||||
history.push(`/workflow_approvals/${workflowApprovalId}`);
|
||||
}, [workflowApproval, workflowApprovalId, history]),
|
||||
{}
|
||||
);
|
||||
|
||||
const workflowJobTemplateId =
|
||||
workflowApproval.summary_fields.workflow_job_template.id;
|
||||
|
||||
@ -146,26 +102,13 @@ function WorkflowApprovalDetail({ workflowApproval }) {
|
||||
fetchWorkflowJob();
|
||||
}, [fetchWorkflowJob]);
|
||||
|
||||
const handleCancel = async () => {
|
||||
setIsKebabModalOpen(false);
|
||||
await cancelWorkflowApprovals();
|
||||
};
|
||||
|
||||
const { error: cancelError, dismissError: dismissCancelError } =
|
||||
useDismissableError(cancelApprovalError);
|
||||
|
||||
const sourceWorkflowJob =
|
||||
workflowApproval?.summary_fields?.source_workflow_job;
|
||||
|
||||
const sourceWorkflowJobTemplate =
|
||||
workflowApproval?.summary_fields?.workflow_job_template;
|
||||
|
||||
const isLoading =
|
||||
isApproveLoading ||
|
||||
isCancelLoading ||
|
||||
isDeleteLoading ||
|
||||
isDenyLoading ||
|
||||
isLoadingWorkflowJob;
|
||||
const isLoading = isDeleteLoading || isLoadingWorkflowJob;
|
||||
|
||||
if (isLoadingWorkflowJob) {
|
||||
return <ContentLoading />;
|
||||
@ -342,16 +285,23 @@ function WorkflowApprovalDetail({ workflowApproval }) {
|
||||
<CardActionsRow>
|
||||
{workflowApproval.status === 'pending' &&
|
||||
workflowApproval.can_approve_or_deny && (
|
||||
<WorkflowApprovalControls
|
||||
selected={[workflowApproval]}
|
||||
onHandleApprove={approveWorkflowApproval}
|
||||
onHandleDeny={denyWorkflowApproval}
|
||||
onHandleCancel={handleCancel}
|
||||
onHandleToggleToolbarKebab={(isOpen) =>
|
||||
setIsKebabModalOpen(isOpen)
|
||||
}
|
||||
isKebabOpen={isKebabOpen}
|
||||
/>
|
||||
<>
|
||||
<WorkflowApprovalButton
|
||||
workflowApproval={workflowApproval}
|
||||
isDetailView
|
||||
/>
|
||||
<WorkflowDenyButton
|
||||
workflowApproval={workflowApproval}
|
||||
isDetailView
|
||||
/>
|
||||
<JobCancelButton
|
||||
title={t`Cancel Workflow`}
|
||||
job={workflowApproval.summary_fields.source_workflow_job}
|
||||
buttonText={t`Cancel Workflow`}
|
||||
cancelationMessage={t`This will cancel all subsequent nodes in this workflow.
|
||||
`}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{workflowApproval.status !== 'pending' &&
|
||||
workflowApproval.summary_fields?.user_capabilities?.delete && (
|
||||
@ -376,39 +326,6 @@ function WorkflowApprovalDetail({ workflowApproval }) {
|
||||
<ErrorDetail error={deleteError} />
|
||||
</AlertModal>
|
||||
)}
|
||||
{approveError && (
|
||||
<AlertModal
|
||||
isOpen={approveError}
|
||||
variant="error"
|
||||
title={t`Error!`}
|
||||
onClose={dismissApproveError}
|
||||
>
|
||||
{t`Failed to approve workflow approval.`}
|
||||
<ErrorDetail error={approveError} />
|
||||
</AlertModal>
|
||||
)}
|
||||
{cancelError && (
|
||||
<AlertModal
|
||||
isOpen={cancelError}
|
||||
variant="error"
|
||||
title={t`Error!`}
|
||||
onClose={dismissCancelError}
|
||||
>
|
||||
{t`Failed to approve workflow approval.`}
|
||||
<ErrorDetail error={cancelError} />
|
||||
</AlertModal>
|
||||
)}
|
||||
{denyError && (
|
||||
<AlertModal
|
||||
isOpen={denyError}
|
||||
variant="error"
|
||||
title={t`Error!`}
|
||||
onClose={dismissDenyError}
|
||||
>
|
||||
{t`Failed to deny workflow approval.`}
|
||||
<ErrorDetail error={denyError} />
|
||||
</AlertModal>
|
||||
)}
|
||||
</CardBody>
|
||||
);
|
||||
}
|
||||
|
||||
@ -326,7 +326,6 @@ describe('<WorkflowApprovalDetail />', () => {
|
||||
expect(wrapper.find('VariablesDetail').prop('value')).toEqual(
|
||||
'{"foo": "bar", "baz": "qux", "first_one": 10}'
|
||||
);
|
||||
expect(wrapper.find('WorkflowApprovalControls').length).toBe(1);
|
||||
});
|
||||
|
||||
test('should show expiration date/time', async () => {
|
||||
@ -521,7 +520,10 @@ describe('<WorkflowApprovalDetail />', () => {
|
||||
});
|
||||
waitForElement(wrapper, 'WorkflowApprovalDetail', (el) => el.length > 0);
|
||||
await act(async () => {
|
||||
wrapper.find('DropdownToggleAction').invoke('onClick')();
|
||||
wrapper
|
||||
.find('Button[ouiaId="workflow-approve-button"]')
|
||||
.at(0)
|
||||
.invoke('onClick')();
|
||||
});
|
||||
expect(WorkflowApprovalsAPI.approve).toHaveBeenCalledTimes(1);
|
||||
await waitForElement(
|
||||
@ -550,16 +552,8 @@ describe('<WorkflowApprovalDetail />', () => {
|
||||
);
|
||||
});
|
||||
waitForElement(wrapper, 'WorkflowApprovalDetail', (el) => el.length > 0);
|
||||
await act(async () => wrapper.find('Toggle').prop('onToggle')(true));
|
||||
wrapper.update();
|
||||
await waitForElement(
|
||||
wrapper,
|
||||
'WorkflowApprovalDetail DropdownItem[ouiaId="workflow-deny-button"]'
|
||||
);
|
||||
await act(async () => {
|
||||
wrapper
|
||||
.find('DropdownItem[ouiaId="workflow-deny-button"]')
|
||||
.invoke('onClick')();
|
||||
wrapper.find('Button[ouiaId="workflow-deny-button"]').invoke('onClick')();
|
||||
});
|
||||
expect(WorkflowApprovalsAPI.deny).toHaveBeenCalledTimes(1);
|
||||
await waitForElement(
|
||||
@ -577,93 +571,6 @@ describe('<WorkflowApprovalDetail />', () => {
|
||||
);
|
||||
});
|
||||
|
||||
test('delete button should be hidden when user cannot delete', async () => {
|
||||
let wrapper;
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<WorkflowApprovalDetail
|
||||
workflowApproval={{
|
||||
...workflowApproval,
|
||||
status: 'successful',
|
||||
summary_fields: {
|
||||
...workflowApproval.summary_fields,
|
||||
user_capabilities: {
|
||||
delete: false,
|
||||
start: false,
|
||||
},
|
||||
approved_or_denied_by: {
|
||||
id: 1,
|
||||
username: 'Foobar',
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
});
|
||||
waitForElement(wrapper, 'WorkflowApprovalDetail', (el) => el.length > 0);
|
||||
expect(wrapper.find('DeleteButton').length).toBe(0);
|
||||
});
|
||||
|
||||
test('delete button should be hidden when job is pending and approve, action buttons should render', async () => {
|
||||
let wrapper;
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<WorkflowApprovalDetail
|
||||
workflowApproval={{
|
||||
...workflowApproval,
|
||||
summary_fields: {
|
||||
...workflowApproval.summary_fields,
|
||||
user_capabilities: {
|
||||
delete: true,
|
||||
start: false,
|
||||
},
|
||||
},
|
||||
can_approve_or_deny: true,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
});
|
||||
waitForElement(wrapper, 'WorkflowApprovalDetail', (el) => el.length > 0);
|
||||
|
||||
expect(wrapper.find('DeleteButton').length).toBe(0);
|
||||
expect(wrapper.find('WorkflowApprovalControls').length).toBe(1);
|
||||
await act(async () => wrapper.find('Toggle').prop('onToggle')(true));
|
||||
wrapper.update();
|
||||
|
||||
const denyItem = wrapper.find(
|
||||
'DropdownItem[ouiaId="workflow-deny-button"]'
|
||||
);
|
||||
const cancelItem = wrapper.find(
|
||||
'DropdownItem[ouiaId="workflow-cancel-button"]'
|
||||
);
|
||||
expect(denyItem).toHaveLength(1);
|
||||
expect(denyItem.prop('description')).toBe(
|
||||
'This will continue the workflow along failure and always paths.'
|
||||
);
|
||||
expect(cancelItem).toHaveLength(1);
|
||||
expect(cancelItem.prop('description')).toBe(
|
||||
'This will cancel the workflow and no subsequent nodes will execute.'
|
||||
);
|
||||
expect(
|
||||
wrapper.find('DropdownItem[ouiaId="workflow-delete-button"]')
|
||||
).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('Delete button is visible and approve action is not', async () => {
|
||||
let wrapper;
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<WorkflowApprovalDetail
|
||||
workflowApproval={mockWorkflowApprovals.results[1]}
|
||||
/>
|
||||
);
|
||||
});
|
||||
waitForElement(wrapper, 'WorkflowApprovalDetail', (el) => el.length > 0);
|
||||
expect(wrapper.find('DeleteButton').length).toBe(1);
|
||||
expect(wrapper.find('DeleteButton').prop('isDisabled')).toBe(false);
|
||||
expect(wrapper.find('WorkflowApprovalControls').length).toBe(0);
|
||||
});
|
||||
|
||||
test('Error dialog shown for failed deletion', async () => {
|
||||
WorkflowApprovalsAPI.destroy.mockImplementationOnce(() =>
|
||||
Promise.reject(new Error())
|
||||
|
||||
@ -1,3 +1 @@
|
||||
import WorkflowApprovalDetail from './WorkflowApprovalDetail';
|
||||
|
||||
export default WorkflowApprovalDetail;
|
||||
export { default } from './WorkflowApprovalDetail';
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import { useLocation, useRouteMatch } from 'react-router-dom';
|
||||
import { t, Plural } from '@lingui/macro';
|
||||
import { Card, PageSection } from '@patternfly/react-core';
|
||||
import { WorkflowApprovalsAPI, WorkflowJobsAPI } from 'api';
|
||||
import { WorkflowApprovalsAPI } from 'api';
|
||||
import PaginatedTable, {
|
||||
HeaderRow,
|
||||
HeaderCell,
|
||||
@ -12,15 +12,11 @@ import PaginatedTable, {
|
||||
import AlertModal from 'components/AlertModal';
|
||||
import ErrorDetail from 'components/ErrorDetail';
|
||||
import DataListToolbar from 'components/DataListToolbar';
|
||||
import useRequest, {
|
||||
useDeleteItems,
|
||||
useDismissableError,
|
||||
} from 'hooks/useRequest';
|
||||
import useRequest, { useDeleteItems } from 'hooks/useRequest';
|
||||
import useSelected from 'hooks/useSelected';
|
||||
import { getQSConfig, parseQueryString } from 'util/qs';
|
||||
import WorkflowApprovalListItem from './WorkflowApprovalListItem';
|
||||
import useWsWorkflowApprovals from './useWsWorkflowApprovals';
|
||||
import WorkflowApprovalControls from '../shared/WorkflowApprovalControls';
|
||||
|
||||
const QS_CONFIG = getQSConfig('workflow_approvals', {
|
||||
page: 1,
|
||||
@ -31,7 +27,6 @@ const QS_CONFIG = getQSConfig('workflow_approvals', {
|
||||
function WorkflowApprovalsList() {
|
||||
const location = useLocation();
|
||||
const match = useRouteMatch();
|
||||
const [isKebabOpen, setIsKebabModalOpen] = useState(false);
|
||||
|
||||
const {
|
||||
result: { results, count, relatedSearchableKeys, searchableKeys },
|
||||
@ -109,79 +104,7 @@ function WorkflowApprovalsList() {
|
||||
clearSelected();
|
||||
};
|
||||
|
||||
const {
|
||||
error: approveApprovalError,
|
||||
isLoading: isApproveLoading,
|
||||
request: approveWorkflowApprovals,
|
||||
} = useRequest(
|
||||
useCallback(
|
||||
async () =>
|
||||
Promise.all(selected.map(({ id }) => WorkflowApprovalsAPI.approve(id))),
|
||||
[selected]
|
||||
),
|
||||
{}
|
||||
);
|
||||
|
||||
const handleApprove = async () => {
|
||||
await approveWorkflowApprovals();
|
||||
clearSelected();
|
||||
};
|
||||
|
||||
const {
|
||||
error: denyApprovalError,
|
||||
isLoading: isDenyLoading,
|
||||
request: denyWorkflowApprovals,
|
||||
} = useRequest(
|
||||
useCallback(
|
||||
async () =>
|
||||
Promise.all(selected.map(({ id }) => WorkflowApprovalsAPI.deny(id))),
|
||||
[selected]
|
||||
),
|
||||
{}
|
||||
);
|
||||
|
||||
const handleDeny = async () => {
|
||||
setIsKebabModalOpen(false);
|
||||
await denyWorkflowApprovals();
|
||||
clearSelected();
|
||||
};
|
||||
|
||||
const {
|
||||
error: cancelApprovalError,
|
||||
isLoading: isCancelLoading,
|
||||
request: cancelWorkflowApprovals,
|
||||
} = useRequest(
|
||||
useCallback(
|
||||
async () =>
|
||||
Promise.all(
|
||||
selected.map(({ summary_fields }) =>
|
||||
WorkflowJobsAPI.cancel(summary_fields.source_workflow_job.id)
|
||||
)
|
||||
),
|
||||
[selected]
|
||||
),
|
||||
{}
|
||||
);
|
||||
|
||||
const handleCancel = async () => {
|
||||
setIsKebabModalOpen(false);
|
||||
await cancelWorkflowApprovals();
|
||||
clearSelected();
|
||||
};
|
||||
|
||||
const { error: approveError, dismissError: dismissApproveError } =
|
||||
useDismissableError(approveApprovalError);
|
||||
const { error: cancelError, dismissError: dismissCancelError } =
|
||||
useDismissableError(cancelApprovalError);
|
||||
const { error: denyError, dismissError: dismissDenyError } =
|
||||
useDismissableError(denyApprovalError);
|
||||
|
||||
const isLoading =
|
||||
isWorkflowApprovalsLoading ||
|
||||
isDeleteLoading ||
|
||||
isApproveLoading ||
|
||||
isDenyLoading ||
|
||||
isCancelLoading;
|
||||
const isLoading = isWorkflowApprovalsLoading || isDeleteLoading;
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -215,17 +138,6 @@ function WorkflowApprovalsList() {
|
||||
onSelectAll={selectAll}
|
||||
qsConfig={QS_CONFIG}
|
||||
additionalControls={[
|
||||
<WorkflowApprovalControls
|
||||
key="approvalControls"
|
||||
onHandleApprove={handleApprove}
|
||||
selected={selected}
|
||||
onHandleDeny={handleDeny}
|
||||
onHandleCancel={handleCancel}
|
||||
onHandleToggleToolbarKebab={(isOpen) =>
|
||||
setIsKebabModalOpen(isOpen)
|
||||
}
|
||||
isKebabOpen={isKebabOpen}
|
||||
/>,
|
||||
<ToolbarDeleteButton
|
||||
key="delete"
|
||||
onDelete={handleDelete}
|
||||
@ -252,6 +164,7 @@ function WorkflowApprovalsList() {
|
||||
<HeaderCell>{t`Workflow Job`}</HeaderCell>
|
||||
<HeaderCell sortKey="started">{t`Started`}</HeaderCell>
|
||||
<HeaderCell>{t`Status`}</HeaderCell>
|
||||
<HeaderCell>{t`Actions`}</HeaderCell>
|
||||
</HeaderRow>
|
||||
}
|
||||
renderRow={(workflowApproval, index) => (
|
||||
@ -263,7 +176,6 @@ function WorkflowApprovalsList() {
|
||||
(row) => row.id === workflowApproval.id
|
||||
)}
|
||||
onSelect={() => handleSelect(workflowApproval)}
|
||||
onSuccessfulAction={fetchWorkflowApprovals}
|
||||
rowIndex={index}
|
||||
/>
|
||||
)}
|
||||
@ -281,39 +193,6 @@ function WorkflowApprovalsList() {
|
||||
<ErrorDetail error={deletionError} />
|
||||
</AlertModal>
|
||||
)}
|
||||
{approveError && (
|
||||
<AlertModal
|
||||
isOpen={approveError}
|
||||
variant="error"
|
||||
title={t`Error!`}
|
||||
onClose={dismissApproveError}
|
||||
>
|
||||
{t`Failed to approve one or more workflow approval.`}
|
||||
<ErrorDetail error={approveError} />
|
||||
</AlertModal>
|
||||
)}
|
||||
{cancelError && (
|
||||
<AlertModal
|
||||
isOpen={cancelError}
|
||||
variant="error"
|
||||
title={t`Error!`}
|
||||
onClose={dismissCancelError}
|
||||
>
|
||||
{t`Failed to cancel one or more workflow approval.`}
|
||||
<ErrorDetail error={cancelError} />
|
||||
</AlertModal>
|
||||
)}
|
||||
{denyError && (
|
||||
<AlertModal
|
||||
isOpen={denyError}
|
||||
variant="error"
|
||||
title={t`Error!`}
|
||||
onClose={dismissDenyError}
|
||||
>
|
||||
{t`Failed to deny one or more workflow approval.`}
|
||||
<ErrorDetail error={denyError} />
|
||||
</AlertModal>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@ -79,27 +79,7 @@ describe('<WorkflowApprovalList />', () => {
|
||||
).toEqual(true);
|
||||
});
|
||||
|
||||
test('WorkflowapprovalControls is inactive. Delete button is active', async () => {
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(<WorkflowApprovalList />);
|
||||
});
|
||||
wrapper.update();
|
||||
await act(async () => {
|
||||
wrapper.find('WorkflowApprovalListItem').first().invoke('onSelect')();
|
||||
});
|
||||
wrapper.update();
|
||||
expect(wrapper.find('Button[aria-label="Delete"]').prop('isDisabled')).toBe(
|
||||
true
|
||||
);
|
||||
expect(
|
||||
wrapper
|
||||
.find('WorkflowApprovalControls')
|
||||
.find('DropdownToggle')
|
||||
.prop('isDisabled')
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
test('WorkflowapprovalControls is inactive. Delete button is active', async () => {
|
||||
test('Delete button is active', async () => {
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(<WorkflowApprovalList />);
|
||||
});
|
||||
@ -111,33 +91,6 @@ describe('<WorkflowApprovalList />', () => {
|
||||
expect(wrapper.find('Button[aria-label="Delete"]').prop('isDisabled')).toBe(
|
||||
false
|
||||
);
|
||||
expect(
|
||||
wrapper
|
||||
.find('WorkflowApprovalControls')
|
||||
.find('DropdownToggle')
|
||||
.prop('isDisabled')
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
test('should disable WorkflowApprovalControls toggle with active delete button', async () => {
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(<WorkflowApprovalList />);
|
||||
});
|
||||
wrapper.update();
|
||||
|
||||
await act(async () => {
|
||||
wrapper.find('WorkflowApprovalListItem').at(3).invoke('onSelect')();
|
||||
});
|
||||
wrapper.update();
|
||||
expect(
|
||||
wrapper
|
||||
.find('WorkflowApprovalControls')
|
||||
.find('DropdownToggle')
|
||||
.prop('isDisabled')
|
||||
).toEqual(true);
|
||||
expect(wrapper.find('Button[aria-label="Delete"]').prop('isDisabled')).toBe(
|
||||
false
|
||||
);
|
||||
});
|
||||
|
||||
test('should call delete api', async () => {
|
||||
|
||||
@ -6,11 +6,15 @@ import { Link } from 'react-router-dom';
|
||||
import { WorkflowApproval } from 'types';
|
||||
import { formatDateString } from 'util/dates';
|
||||
import StatusLabel from 'components/StatusLabel';
|
||||
import JobCancelButton from 'components/JobCancelButton';
|
||||
import { ActionItem, ActionsTd } from 'components/PaginatedTable';
|
||||
import {
|
||||
getPendingLabel,
|
||||
getStatus,
|
||||
getTooltip,
|
||||
} from '../shared/WorkflowApprovalUtils';
|
||||
import WorkflowApprovalButton from '../shared/WorkflowApprovalButton';
|
||||
import WorkflowDenyButton from '../shared/WorkflowDenyButton';
|
||||
|
||||
function WorkflowApprovalListItem({
|
||||
workflowApproval,
|
||||
@ -19,8 +23,13 @@ function WorkflowApprovalListItem({
|
||||
detailUrl,
|
||||
rowIndex,
|
||||
}) {
|
||||
const hasBeenActedOn =
|
||||
workflowApproval.status === 'successful' ||
|
||||
workflowApproval.status === 'failed' ||
|
||||
workflowApproval.status === 'canceled';
|
||||
const labelId = `check-action-${workflowApproval.id}`;
|
||||
const workflowJob = workflowApproval?.summary_fields?.source_workflow_job;
|
||||
const status = getStatus(workflowApproval);
|
||||
return (
|
||||
<Tr id={`workflow-approval-row-${workflowApproval.id}`}>
|
||||
<Td
|
||||
@ -62,10 +71,48 @@ function WorkflowApprovalListItem({
|
||||
) : (
|
||||
<StatusLabel
|
||||
tooltipContent={getTooltip(workflowApproval)}
|
||||
status={getStatus(workflowApproval)}
|
||||
status={status}
|
||||
/>
|
||||
)}
|
||||
</Td>
|
||||
<ActionsTd dataLabel={t`Actions`}>
|
||||
<ActionItem
|
||||
visible
|
||||
tooltip={
|
||||
hasBeenActedOn
|
||||
? t`This workflow has already been ${status}`
|
||||
: t`Approve`
|
||||
}
|
||||
>
|
||||
<WorkflowApprovalButton workflowApproval={workflowApproval} />
|
||||
</ActionItem>
|
||||
<ActionItem
|
||||
visible
|
||||
tooltip={
|
||||
hasBeenActedOn
|
||||
? t`This workflow has already been ${status}`
|
||||
: t`Deny`
|
||||
}
|
||||
>
|
||||
<WorkflowDenyButton workflowApproval={workflowApproval} />
|
||||
</ActionItem>
|
||||
<ActionItem visible>
|
||||
<JobCancelButton
|
||||
title={t`Cancel Workflow`}
|
||||
showIconButton
|
||||
job={workflowApproval.summary_fields.source_workflow_job}
|
||||
buttonText={t`Cancel Workflow`}
|
||||
isDisabled={hasBeenActedOn}
|
||||
tooltip={
|
||||
hasBeenActedOn
|
||||
? t`This workflow has already been ${status}`
|
||||
: t`Cancel`
|
||||
}
|
||||
cancelationMessage={t`This will cancel all subsequent nodes in this workflow
|
||||
`}
|
||||
/>
|
||||
</ActionItem>
|
||||
</ActionsTd>
|
||||
</Tr>
|
||||
);
|
||||
}
|
||||
|
||||
@ -0,0 +1,57 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { t } from '@lingui/macro';
|
||||
import { Button } from '@patternfly/react-core';
|
||||
import { OutlinedThumbsUpIcon } from '@patternfly/react-icons';
|
||||
import { WorkflowApprovalsAPI } from 'api';
|
||||
import useRequest, { useDismissableError } from 'hooks/useRequest';
|
||||
|
||||
import AlertModal from 'components/AlertModal';
|
||||
import ErrorDetail from 'components/ErrorDetail';
|
||||
import { getStatus } from './WorkflowApprovalUtils';
|
||||
|
||||
function WorkflowApprovalButton({ isDetailView, workflowApproval }) {
|
||||
const { id } = workflowApproval;
|
||||
const hasBeenActedOn = workflowApproval.status === 'successful';
|
||||
const { error: approveApprovalError, request: approveWorkflowApprovals } =
|
||||
useRequest(
|
||||
useCallback(async () => WorkflowApprovalsAPI.approve(id), [id]),
|
||||
{}
|
||||
);
|
||||
|
||||
const handleApprove = async () => {
|
||||
await approveWorkflowApprovals();
|
||||
};
|
||||
|
||||
const { error: approveError, dismissError: dismissApproveError } =
|
||||
useDismissableError(approveApprovalError);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
isDisabled={hasBeenActedOn}
|
||||
variant={isDetailView ? 'primary' : 'plain'}
|
||||
ouiaId="workflow-approve-button"
|
||||
onClick={() => handleApprove()}
|
||||
aria-label={
|
||||
hasBeenActedOn
|
||||
? t`This workflow has already been ${getStatus(workflowApproval)}`
|
||||
: t`Approve`
|
||||
}
|
||||
>
|
||||
{isDetailView ? t`Approve` : <OutlinedThumbsUpIcon />}
|
||||
</Button>
|
||||
{approveError && (
|
||||
<AlertModal
|
||||
isOpen={approveError}
|
||||
variant="error"
|
||||
title={t`Error!`}
|
||||
onClose={dismissApproveError}
|
||||
>
|
||||
{t`Failed to approve ${workflowApproval.name}.`}
|
||||
<ErrorDetail error={approveError} />
|
||||
</AlertModal>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
export default WorkflowApprovalButton;
|
||||
@ -0,0 +1,56 @@
|
||||
import React from 'react';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
|
||||
import {
|
||||
mountWithContexts,
|
||||
shallowWithContexts,
|
||||
} from '../../../../testUtils/enzymeHelpers';
|
||||
import { WorkflowApprovalsAPI } from 'api';
|
||||
import WorkflowApprovalButton from './WorkflowApprovalButton';
|
||||
import mockData from '../data.workflowApprovals.json';
|
||||
|
||||
jest.mock('api');
|
||||
|
||||
describe('<WorkflowApprovalButton/> shallow mount', () => {
|
||||
let wrapper;
|
||||
|
||||
let mockApprovalList = mockData.results;
|
||||
|
||||
test('initially render successfully', () => {
|
||||
wrapper = shallowWithContexts(
|
||||
<WorkflowApprovalButton workflowApproval={mockApprovalList[0]} />
|
||||
);
|
||||
|
||||
expect(wrapper.find('WorkflowApprovalButton')).toHaveLength(1);
|
||||
wrapper
|
||||
.find('Button')
|
||||
.forEach((button) => expect(button.prop('isDisabled')).toBe(false));
|
||||
});
|
||||
});
|
||||
|
||||
describe('<WorkflowApprovalButton/>, full mount', () => {
|
||||
let wrapper;
|
||||
let mockApprovalList = mockData.results;
|
||||
const approveButton = 'Button[ouiaId="workflow-approve-button"]';
|
||||
afterEach(() => {
|
||||
wrapper.unmount();
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
test('should be disabled', () => {
|
||||
wrapper = mountWithContexts(
|
||||
<WorkflowApprovalButton workflowApproval={mockApprovalList[2]} />
|
||||
);
|
||||
expect(wrapper.find(approveButton)).toHaveLength(1);
|
||||
expect(wrapper.find(approveButton).prop('isDisabled')).toBe(true);
|
||||
});
|
||||
test('should handle approve', async () => {
|
||||
act(() => {
|
||||
wrapper = mountWithContexts(
|
||||
<WorkflowApprovalButton workflowApproval={mockApprovalList[0]} />
|
||||
);
|
||||
});
|
||||
await act(() => wrapper.find(approveButton).prop('onClick')());
|
||||
expect(WorkflowApprovalsAPI.approve).toBeCalledWith(218);
|
||||
});
|
||||
});
|
||||
@ -1,146 +0,0 @@
|
||||
import React from 'react';
|
||||
import { t } from '@lingui/macro';
|
||||
import {
|
||||
Dropdown as PFDropdown,
|
||||
DropdownItem,
|
||||
Tooltip,
|
||||
DropdownToggle,
|
||||
DropdownToggleAction,
|
||||
} from '@patternfly/react-core';
|
||||
import { CaretDownIcon } from '@patternfly/react-icons';
|
||||
import { useKebabifiedMenu } from 'contexts/Kebabified';
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Dropdown = styled(PFDropdown)`
|
||||
--pf-c-dropdown__toggle--disabled--BackgroundColor: var(
|
||||
--pf-global--disabled-color--200
|
||||
);
|
||||
|
||||
&&& {
|
||||
button {
|
||||
color: var(--pf-c-dropdown__toggle--m-primary--Color);
|
||||
}
|
||||
div.pf-m-disabled > button {
|
||||
color: var(--pf-global--disabled-color--100);
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
function isPending(item) {
|
||||
return item.status === 'pending';
|
||||
}
|
||||
function isComplete(item) {
|
||||
return item.status !== 'pending';
|
||||
}
|
||||
|
||||
function WorkflowApprovalControls({
|
||||
selected,
|
||||
onHandleDeny,
|
||||
onHandleCancel,
|
||||
onHandleApprove,
|
||||
onHandleToggleToolbarKebab,
|
||||
isKebabOpen,
|
||||
}) {
|
||||
const { isKebabified } = useKebabifiedMenu();
|
||||
|
||||
const hasSelectedItems = selected.length > 0;
|
||||
const isApproveDenyOrCancelDisabled =
|
||||
!hasSelectedItems ||
|
||||
!selected.every(
|
||||
(item) => item.status === 'pending' && item.can_approve_or_deny
|
||||
);
|
||||
|
||||
const renderTooltip = (action) => {
|
||||
if (!hasSelectedItems) {
|
||||
return t`Select items to approve, deny, or cancel`;
|
||||
}
|
||||
if (!selected.some(isPending)) {
|
||||
return t`Cannot approve, cancel or deny completed workflow approvals`;
|
||||
}
|
||||
|
||||
const completedItems = selected
|
||||
.filter(isComplete)
|
||||
.map((item) => item.name)
|
||||
.join(', ');
|
||||
if (selected.some(isPending) && selected.some(isComplete)) {
|
||||
return t`The following selected items are complete and cannot be acted on: ${completedItems}`;
|
||||
}
|
||||
return action;
|
||||
};
|
||||
|
||||
const dropdownItems = [
|
||||
<Tooltip key="deny" content={renderTooltip(t`Deny`)}>
|
||||
<div>
|
||||
<DropdownItem
|
||||
isDisabled={isApproveDenyOrCancelDisabled}
|
||||
onClick={onHandleDeny}
|
||||
ouiaId="workflow-deny-button"
|
||||
description={t`This will continue the workflow along failure and always paths.`}
|
||||
>
|
||||
{t`Deny`}
|
||||
</DropdownItem>
|
||||
</div>
|
||||
</Tooltip>,
|
||||
<Tooltip key="cancel" content={renderTooltip(t`Cancel`)}>
|
||||
<div>
|
||||
<DropdownItem
|
||||
isDisabled={isApproveDenyOrCancelDisabled}
|
||||
ouiaId="workflow-cancel-button"
|
||||
onClick={onHandleCancel}
|
||||
description={t`This will cancel the workflow and no subsequent nodes will execute.`}
|
||||
>
|
||||
{t`Cancel`}
|
||||
</DropdownItem>
|
||||
</div>
|
||||
</Tooltip>,
|
||||
];
|
||||
if (isKebabified) {
|
||||
dropdownItems.unshift(
|
||||
<Tooltip content={renderTooltip(t`Approve`)}>
|
||||
<div>
|
||||
<DropdownItem
|
||||
isDisabled={isApproveDenyOrCancelDisabled}
|
||||
onClick={onHandleApprove}
|
||||
ouiaId="workflow-approve-button"
|
||||
description={t`This will continue the workflow`}
|
||||
>{t`Approve`}</DropdownItem>
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
return dropdownItems;
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
content={renderTooltip(t`Approve, cancel or deny`)}
|
||||
key="workflowApproveOrDenyControls"
|
||||
>
|
||||
<Dropdown
|
||||
toggle={
|
||||
<DropdownToggle
|
||||
isPrimary
|
||||
splitButtonVariant="action"
|
||||
toggleIndicator={CaretDownIcon}
|
||||
splitButtonItems={[
|
||||
<DropdownToggleAction
|
||||
key="action"
|
||||
type="button"
|
||||
onClick={onHandleApprove}
|
||||
>
|
||||
{t`Approve`}
|
||||
</DropdownToggleAction>,
|
||||
]}
|
||||
data-cy="actions-kebab-toogle"
|
||||
isDisabled={isApproveDenyOrCancelDisabled}
|
||||
onToggle={onHandleToggleToolbarKebab}
|
||||
/>
|
||||
}
|
||||
isOpen={isKebabOpen}
|
||||
isPlain
|
||||
ouiaId="actions-dropdown"
|
||||
dropdownItems={dropdownItems}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
export default WorkflowApprovalControls;
|
||||
@ -1,94 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
import {
|
||||
mountWithContexts,
|
||||
shallowWithContexts,
|
||||
} from '../../../../testUtils/enzymeHelpers';
|
||||
import WorkflowApprovalControls from './WorkflowApprovalControls';
|
||||
import mockData from '../data.workflowApprovals.json';
|
||||
|
||||
describe('<WorkflowApprovalControls/> shallow mount', () => {
|
||||
let wrapper;
|
||||
let onHandleDeny = jest.fn();
|
||||
let onHandleCancel = jest.fn();
|
||||
let onHandleToggleToolbarKebab = jest.fn();
|
||||
let mockApprovalList = mockData.results;
|
||||
|
||||
test('initially render successfully', () => {
|
||||
wrapper = shallowWithContexts(
|
||||
<WorkflowApprovalControls
|
||||
selected={[]}
|
||||
onHandleDeny={onHandleDeny}
|
||||
onHandleCancel={onHandleCancel}
|
||||
onHandleToggleToolbarKebab={onHandleToggleToolbarKebab}
|
||||
isKebabOpen={false}
|
||||
/>
|
||||
);
|
||||
expect(wrapper.find('WorkflowApprovalControls')).toHaveLength(1);
|
||||
expect(wrapper.find('WorkflowApprovalControls').prop('isKebabOpen')).toBe(
|
||||
false
|
||||
);
|
||||
});
|
||||
test('is open with the correct dropdown options', async () => {
|
||||
wrapper = shallowWithContexts(
|
||||
<WorkflowApprovalControls
|
||||
selected={mockApprovalList}
|
||||
onHandleDeny={onHandleDeny}
|
||||
onHandleCancel={onHandleCancel}
|
||||
onHandleToggleToolbarKebab={onHandleToggleToolbarKebab}
|
||||
isKebabOpen={true}
|
||||
/>
|
||||
);
|
||||
expect(wrapper.find('WorkflowApprovalControls')).toHaveLength(1);
|
||||
expect(wrapper.find('WorkflowApprovalControls').prop('isKebabOpen')).toBe(
|
||||
true
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('<WorkflowApprovalControls/>, full mount', () => {
|
||||
let wrapper;
|
||||
let onHandleDeny = jest.fn();
|
||||
let onHandleCancel = jest.fn();
|
||||
let onHandleToggleToolbarKebab = jest.fn();
|
||||
let mockApprovalList = mockData.results;
|
||||
const cancelButton = 'DropdownItem[ouiaId="workflow-cancel-button"]';
|
||||
const denyButton = 'DropdownItem[ouiaId="workflow-deny-button"]';
|
||||
|
||||
test('initially render successfully', () => {
|
||||
wrapper = mountWithContexts(
|
||||
<WorkflowApprovalControls
|
||||
selected={[]}
|
||||
onHandleDeny={onHandleDeny}
|
||||
onHandleCancel={onHandleCancel}
|
||||
onHandleToggleToolbarKebab={onHandleToggleToolbarKebab}
|
||||
isKebabOpen={false}
|
||||
/>
|
||||
);
|
||||
expect(wrapper.find('WorkflowApprovalControls')).toHaveLength(1);
|
||||
expect(wrapper.find('WorkflowApprovalControls').prop('isKebabOpen')).toBe(
|
||||
false
|
||||
);
|
||||
expect(wrapper.find('Tooltip').prop('content')).toBe(
|
||||
'Select items to approve, deny, or cancel'
|
||||
);
|
||||
});
|
||||
|
||||
test('should call correct functions', () => {
|
||||
wrapper = mountWithContexts(
|
||||
<WorkflowApprovalControls
|
||||
selected={[mockApprovalList[1], mockApprovalList[2]]}
|
||||
onHandleDeny={onHandleDeny}
|
||||
onHandleCancel={onHandleCancel}
|
||||
onHandleToggleToolbarKebab={onHandleToggleToolbarKebab}
|
||||
isKebabOpen={true}
|
||||
/>
|
||||
);
|
||||
wrapper.find(denyButton).prop('onClick')();
|
||||
expect(onHandleDeny).toHaveBeenCalled();
|
||||
wrapper.find(cancelButton).prop('onClick')();
|
||||
expect(onHandleCancel).toHaveBeenCalled();
|
||||
wrapper.find('DropdownToggle').prop('onToggle')(false);
|
||||
expect(onHandleToggleToolbarKebab).toHaveBeenCalledWith(false);
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,58 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { t } from '@lingui/macro';
|
||||
import { Button } from '@patternfly/react-core';
|
||||
import { OutlinedThumbsDownIcon } from '@patternfly/react-icons';
|
||||
import { WorkflowApprovalsAPI } from 'api';
|
||||
import useRequest, { useDismissableError } from 'hooks/useRequest';
|
||||
|
||||
import AlertModal from 'components/AlertModal';
|
||||
import ErrorDetail from 'components/ErrorDetail';
|
||||
import { getStatus } from './WorkflowApprovalUtils';
|
||||
|
||||
function WorkflowDenyButton({ isDetailView, workflowApproval }) {
|
||||
const hasBeenActedOn = workflowApproval.status === 'failed';
|
||||
const { id } = workflowApproval;
|
||||
|
||||
const { error: denyApprovalError, request: denyWorkflowApprovals } =
|
||||
useRequest(
|
||||
useCallback(async () => WorkflowApprovalsAPI.deny(id), [id]),
|
||||
{}
|
||||
);
|
||||
|
||||
const handleDeny = async () => {
|
||||
await denyWorkflowApprovals();
|
||||
};
|
||||
|
||||
const { error: denyError, dismissError: dismissDenyError } =
|
||||
useDismissableError(denyApprovalError);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
aria-label={
|
||||
hasBeenActedOn
|
||||
? t`This workflow has already been ${getStatus(workflowApproval)}`
|
||||
: t`Deny`
|
||||
}
|
||||
ouiaId="workflow-deny-button"
|
||||
isDisabled={hasBeenActedOn}
|
||||
variant={isDetailView ? 'secondary' : 'plain'}
|
||||
onClick={() => handleDeny()}
|
||||
>
|
||||
{isDetailView ? t`Deny` : <OutlinedThumbsDownIcon />}
|
||||
</Button>
|
||||
{denyError && (
|
||||
<AlertModal
|
||||
isOpen={denyError}
|
||||
variant="error"
|
||||
title={t`Error!`}
|
||||
onClose={dismissDenyError}
|
||||
>
|
||||
{t`Failed to deny ${workflowApproval.name}.`}
|
||||
<ErrorDetail error={denyError} />
|
||||
</AlertModal>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
export default WorkflowDenyButton;
|
||||
@ -0,0 +1,80 @@
|
||||
import React from 'react';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
|
||||
import {
|
||||
mountWithContexts,
|
||||
shallowWithContexts,
|
||||
} from '../../../../testUtils/enzymeHelpers';
|
||||
import { WorkflowApprovalsAPI } from 'api';
|
||||
import WorkflowDenyButton from './WorkflowDenyButton';
|
||||
import mockData from '../data.workflowApprovals.json';
|
||||
|
||||
jest.mock('api');
|
||||
|
||||
describe('<WorkflowDenyButton/> shallow mount', () => {
|
||||
let wrapper;
|
||||
|
||||
let mockApprovalList = mockData.results;
|
||||
|
||||
test('initially render successfully', () => {
|
||||
wrapper = shallowWithContexts(
|
||||
<WorkflowDenyButton workflowApproval={mockApprovalList[0]} />
|
||||
);
|
||||
|
||||
expect(wrapper.find('WorkflowDenyButton')).toHaveLength(1);
|
||||
wrapper
|
||||
.find('Button')
|
||||
.forEach((button) => expect(button.prop('isDisabled')).toBe(false));
|
||||
});
|
||||
});
|
||||
|
||||
describe('<WorkflowDenyButton/>, full mount', () => {
|
||||
let wrapper;
|
||||
let mockApprovalList = mockData.results;
|
||||
const denyButton = 'Button[ouiaId="workflow-deny-button"]';
|
||||
afterEach(() => {
|
||||
wrapper.unmount();
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
test('should be disabled', () => {
|
||||
wrapper = mountWithContexts(
|
||||
<WorkflowDenyButton workflowApproval={mockApprovalList[1]} />
|
||||
);
|
||||
expect(wrapper.find(denyButton)).toHaveLength(1);
|
||||
expect(wrapper.find(denyButton).prop('isDisabled')).toBe(true);
|
||||
});
|
||||
|
||||
test('should handle deny', async () => {
|
||||
act(() => {
|
||||
wrapper = mountWithContexts(
|
||||
<WorkflowDenyButton workflowApproval={mockApprovalList[0]} />
|
||||
);
|
||||
});
|
||||
await act(() => wrapper.find(denyButton).prop('onClick')());
|
||||
expect(WorkflowApprovalsAPI.deny).toBeCalledWith(218);
|
||||
});
|
||||
|
||||
test('Should handle deny error', async () => {
|
||||
WorkflowApprovalsAPI.deny.mockRejectedValue(
|
||||
new Error({
|
||||
response: {
|
||||
config: {
|
||||
method: 'post',
|
||||
url: '/api/v2/workflow',
|
||||
},
|
||||
data: 'An error occurred',
|
||||
status: 403,
|
||||
},
|
||||
})
|
||||
);
|
||||
act(() => {
|
||||
wrapper = mountWithContexts(
|
||||
<WorkflowDenyButton workflowApproval={mockApprovalList[0]} />
|
||||
);
|
||||
});
|
||||
await act(() => wrapper.find(denyButton).prop('onClick')());
|
||||
wrapper.update();
|
||||
expect(wrapper.find('ErrorDetail')).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
Loading…
x
Reference in New Issue
Block a user