Re-add workflow approval bulk actions to workflow approvals list

This commit is contained in:
Michael Abashian 2023-01-19 12:43:33 -05:00
parent 8fb831d3de
commit 808ab9803e
5 changed files with 334 additions and 2 deletions

View File

@ -12,11 +12,16 @@ import PaginatedTable, {
import AlertModal from 'components/AlertModal';
import ErrorDetail from 'components/ErrorDetail';
import DataListToolbar from 'components/DataListToolbar';
import useRequest, { useDeleteItems } from 'hooks/useRequest';
import useRequest, {
useDeleteItems,
useDismissableError,
} from 'hooks/useRequest';
import useSelected from 'hooks/useSelected';
import { getQSConfig, parseQueryString } from 'util/qs';
import WorkflowApprovalListItem from './WorkflowApprovalListItem';
import useWsWorkflowApprovals from './useWsWorkflowApprovals';
import WorkflowApprovalListApproveButton from './WorkflowApprovalListApproveButton';
import WorkflowApprovalListDenyButton from './WorkflowApprovalListDenyButton';
const QS_CONFIG = getQSConfig('workflow_approvals', {
page: 1,
@ -104,7 +109,50 @@ function WorkflowApprovalsList() {
clearSelected();
};
const isLoading = isWorkflowApprovalsLoading || isDeleteLoading;
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 () => {
await denyWorkflowApprovals();
clearSelected();
};
const { error: actionError, dismissError: dismissActionError } =
useDismissableError(approveApprovalError || denyApprovalError);
const isLoading =
isWorkflowApprovalsLoading ||
isDeleteLoading ||
isApproveLoading ||
isDenyLoading;
return (
<>
@ -138,6 +186,16 @@ function WorkflowApprovalsList() {
onSelectAll={selectAll}
qsConfig={QS_CONFIG}
additionalControls={[
<WorkflowApprovalListApproveButton
key="approve"
onApprove={handleApprove}
selectedItems={selected}
/>,
<WorkflowApprovalListDenyButton
key="deny"
onDeny={handleDeny}
selectedItems={selected}
/>,
<ToolbarDeleteButton
key="delete"
onDelete={handleDelete}
@ -193,6 +251,19 @@ function WorkflowApprovalsList() {
<ErrorDetail error={deletionError} />
</AlertModal>
)}
{actionError && (
<AlertModal
isOpen={actionError}
variant="error"
title={t`Error!`}
onClose={dismissActionError}
>
{approveApprovalError
? t`Failed to approve one or more workflow approval.`
: t`Failed to deny one or more workflow approval.`}
<ErrorDetail error={actionError} />
</AlertModal>
)}
</>
);
}

View File

@ -0,0 +1,76 @@
import React, { useContext } from 'react';
import { t } from '@lingui/macro';
import PropTypes from 'prop-types';
import { Button, DropdownItem, Tooltip } from '@patternfly/react-core';
import { KebabifiedContext } from 'contexts/Kebabified';
import { WorkflowApproval } from 'types';
function cannotApprove(item) {
return !item.can_approve_or_deny;
}
function WorkflowApprovalListApproveButton({ onApprove, selectedItems }) {
const { isKebabified } = useContext(KebabifiedContext);
const renderTooltip = () => {
if (selectedItems.length === 0) {
return t`Select a row to approve`;
}
const itemsUnableToApprove = selectedItems
.filter(cannotApprove)
.map((item) => item.name)
.join(', ');
if (selectedItems.some(cannotApprove)) {
return t`You are unable to act on the following workflow approvals: ${itemsUnableToApprove}`;
}
return t`Approve`;
};
const isDisabled =
selectedItems.length === 0 || selectedItems.some(cannotApprove);
return (
/* eslint-disable-next-line react/jsx-no-useless-fragment */
<>
{isKebabified ? (
<DropdownItem
key="approve"
isDisabled={isDisabled}
component="button"
onClick={onApprove}
>
{t`Approve`}
</DropdownItem>
) : (
<Tooltip content={renderTooltip()} position="top">
<div>
<Button
ouiaId="workflow-approval-approve-button"
isDisabled={isDisabled}
aria-label={t`Approve`}
variant="primary"
onClick={onApprove}
>
{t`Approve`}
</Button>
</div>
</Tooltip>
)}
</>
);
}
WorkflowApprovalListApproveButton.propTypes = {
onApprove: PropTypes.func.isRequired,
selectedItems: PropTypes.arrayOf(WorkflowApproval),
};
WorkflowApprovalListApproveButton.defaultProps = {
selectedItems: [],
};
export default WorkflowApprovalListApproveButton;

View File

@ -0,0 +1,56 @@
import React from 'react';
import { mountWithContexts } from '../../../../testUtils/enzymeHelpers';
import WorkflowApprovalListApproveButton from './WorkflowApprovalListApproveButton';
const workflowApproval = {
id: 1,
name: 'Foo',
can_approve_or_deny: true,
url: '/api/v2/workflow_approvals/218/',
};
describe('<WorkflowApprovalListApproveButton />', () => {
test('should render button', () => {
const wrapper = mountWithContexts(
<WorkflowApprovalListApproveButton
onApprove={() => {}}
selectedItems={[]}
/>
);
expect(wrapper.find('button')).toHaveLength(1);
});
test('should invoke onApprove prop', () => {
const onApprove = jest.fn();
const wrapper = mountWithContexts(
<WorkflowApprovalListApproveButton
onApprove={onApprove}
selectedItems={[workflowApproval]}
/>
);
wrapper.find('button').simulate('click');
wrapper.update();
expect(onApprove).toHaveBeenCalled();
});
test('should disable button when no approve/deny permissions', () => {
const wrapper = mountWithContexts(
<WorkflowApprovalListApproveButton
onApprove={() => {}}
selectedItems={[{ ...workflowApproval, can_approve_or_deny: false }]}
/>
);
expect(wrapper.find('button[disabled]')).toHaveLength(1);
});
test('should render tooltip', () => {
const wrapper = mountWithContexts(
<WorkflowApprovalListApproveButton
onApprove={() => {}}
selectedItems={[workflowApproval]}
/>
);
expect(wrapper.find('Tooltip')).toHaveLength(1);
expect(wrapper.find('Tooltip').prop('content')).toEqual('Approve');
});
});

View File

@ -0,0 +1,76 @@
import React, { useContext } from 'react';
import { t } from '@lingui/macro';
import PropTypes from 'prop-types';
import { Button, DropdownItem, Tooltip } from '@patternfly/react-core';
import { KebabifiedContext } from 'contexts/Kebabified';
import { WorkflowApproval } from 'types';
function cannotDeny(item) {
return !item.can_approve_or_deny;
}
function WorkflowApprovalListDenyButton({ onDeny, selectedItems }) {
const { isKebabified } = useContext(KebabifiedContext);
const renderTooltip = () => {
if (selectedItems.length === 0) {
return t`Select a row to deny`;
}
const itemsUnableToDeny = selectedItems
.filter(cannotDeny)
.map((item) => item.name)
.join(', ');
if (selectedItems.some(cannotDeny)) {
return t`You are unable to act on the following workflow approvals: ${itemsUnableToDeny}`;
}
return t`Deny`;
};
const isDisabled =
selectedItems.length === 0 || selectedItems.some(cannotDeny);
return (
/* eslint-disable-next-line react/jsx-no-useless-fragment */
<>
{isKebabified ? (
<DropdownItem
key="deny"
isDisabled={isDisabled}
component="button"
onClick={onDeny}
>
{t`Deny`}
</DropdownItem>
) : (
<Tooltip content={renderTooltip()} position="top">
<div>
<Button
ouiaId="workflow-approval-deny-button"
isDisabled={isDisabled}
aria-label={t`Deny`}
variant="danger"
onClick={onDeny}
>
{t`Deny`}
</Button>
</div>
</Tooltip>
)}
</>
);
}
WorkflowApprovalListDenyButton.propTypes = {
onDeny: PropTypes.func.isRequired,
selectedItems: PropTypes.arrayOf(WorkflowApproval),
};
WorkflowApprovalListDenyButton.defaultProps = {
selectedItems: [],
};
export default WorkflowApprovalListDenyButton;

View File

@ -0,0 +1,53 @@
import React from 'react';
import { mountWithContexts } from '../../../../testUtils/enzymeHelpers';
import WorkflowApprovalListDenyButton from './WorkflowApprovalListDenyButton';
const workflowApproval = {
id: 1,
name: 'Foo',
can_approve_or_deny: true,
url: '/api/v2/workflow_approvals/218/',
};
describe('<WorkflowApprovalListDenyButton />', () => {
test('should render button', () => {
const wrapper = mountWithContexts(
<WorkflowApprovalListDenyButton onDeny={() => {}} selectedItems={[]} />
);
expect(wrapper.find('button')).toHaveLength(1);
});
test('should invoke onDeny prop', () => {
const onDeny = jest.fn();
const wrapper = mountWithContexts(
<WorkflowApprovalListDenyButton
onDeny={onDeny}
selectedItems={[workflowApproval]}
/>
);
wrapper.find('button').simulate('click');
wrapper.update();
expect(onDeny).toHaveBeenCalled();
});
test('should disable button when no approve/deny permissions', () => {
const wrapper = mountWithContexts(
<WorkflowApprovalListDenyButton
onDeny={() => {}}
selectedItems={[{ ...workflowApproval, can_approve_or_deny: false }]}
/>
);
expect(wrapper.find('button[disabled]')).toHaveLength(1);
});
test('should render tooltip', () => {
const wrapper = mountWithContexts(
<WorkflowApprovalListDenyButton
onDeny={() => {}}
selectedItems={[workflowApproval]}
/>
);
expect(wrapper.find('Tooltip')).toHaveLength(1);
expect(wrapper.find('Tooltip').prop('content')).toEqual('Deny');
});
});