Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: implement cancel button for pending assignment #1099

Merged
merged 8 commits into from
Dec 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,10 @@ AssignmentDetailsTableCell.propTypes = {
contentKey: PropTypes.string.isRequired,
contentTitle: PropTypes.string,
contentQuantity: PropTypes.number,
errorReason: PropTypes.string,
errorReason: PropTypes.shape({
actionType: PropTypes.string,
errorReason: PropTypes.string,
}),
learnerState: PropTypes.string,
state: PropTypes.string,
}).isRequired,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ import PropTypes from 'prop-types';
import {
Icon, IconButton, OverlayTrigger, Stack, Tooltip,
} from '@edx/paragon';
import { Mail, DoNotDisturbOn } from '@edx/paragon/icons';
import { Mail } from '@edx/paragon/icons';
import PendingAssignmentCancelButton from './PendingAssignmentCancelButton';

const AssignmentRowActionTableCell = ({ row }) => {
const isLearnerStateWaiting = row.original.learnerState === 'waiting';
Expand All @@ -26,21 +27,7 @@ const AssignmentRowActionTableCell = ({ row }) => {
/>
</OverlayTrigger>
)}
<OverlayTrigger
key="Cancel"
placement="top"
overlay={<Tooltip id={`tooltip-cancel-${row.original.uuid}`}>Cancel assignment</Tooltip>}
>
<IconButton
variant="danger"
src={DoNotDisturbOn}
iconAs={Icon}
alt={`Cancel assignment ${emailAltText}`}
// eslint-disable-next-line no-console
onClick={() => console.log(`Canceling ${row.original.uuid}`)}
data-testid={`cancel-assignment-${row.original.uuid}`}
/>
</OverlayTrigger>
<PendingAssignmentCancelButton row={row} />
</Stack>
);
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import React from 'react';
import PropTypes from 'prop-types';
import {
Chip,
} from '@edx/paragon';
import NotifyingLearner from './assignments-status-chips/NotifyingLearner';
import WaitingForLearner from './assignments-status-chips/WaitingForLearner';
import PropTypes from 'prop-types';
import FailedBadEmail from './assignments-status-chips/FailedBadEmail';
import FailedCancellation from './assignments-status-chips/FailedCancellation';
import FailedSystem from './assignments-status-chips/FailedSystem';
import NotifyingLearner from './assignments-status-chips/NotifyingLearner';
import WaitingForLearner from './assignments-status-chips/WaitingForLearner';

const AssignmentStatusTableCell = ({ row }) => {
const { original } = row;
Expand Down Expand Up @@ -36,13 +36,18 @@ const AssignmentStatusTableCell = ({ row }) => {

if (learnerState === 'failed') {
// Determine which failure chip to display based on the error reason.
if (errorReason === 'email_error') {
return (
<FailedBadEmail learnerEmail={learnerEmail} />
);
if (errorReason.actionType === 'notified') {
if (errorReason.errorReason === 'email_error') {
return (
<FailedBadEmail learnerEmail={learnerEmail} />
);
}
return <FailedSystem />;
}

return <FailedSystem />;
if (errorReason.actionType === 'cancelled') {
return <FailedCancellation />;
}
}

// Note: The given `learnerState` not officially supported with a `ModalPopup`, but display it anyway.
Expand All @@ -54,7 +59,10 @@ AssignmentStatusTableCell.propTypes = {
original: PropTypes.shape({
learnerEmail: PropTypes.string,
learnerState: PropTypes.string.isRequired,
errorReason: PropTypes.string,
errorReason: PropTypes.shape({
actionType: PropTypes.string,
errorReason: PropTypes.string,
}),
actions: PropTypes.arrayOf(PropTypes.shape({
actionType: PropTypes.string.isRequired,
errorReason: PropTypes.string,
Expand Down
34 changes: 28 additions & 6 deletions src/components/learner-credit-management/AssignmentTableCancel.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,35 @@ import React from 'react';
import PropTypes from 'prop-types';
import { Button } from '@edx/paragon';
import { DoNotDisturbOn } from '@edx/paragon/icons';
import CancelAssignmentModal from './CancelAssignmentModal';
import useCancelContentAssignments from './data/hooks/useCancelContentAssignments';

const AssignmentTableCancelAction = ({ selectedFlatRows, ...rest }) => (
// eslint-disable-next-line no-console
<Button variant="danger" iconBefore={DoNotDisturbOn} onClick={() => console.log('Cancel', selectedFlatRows, rest)}>
{`Cancel (${selectedFlatRows.length})`}
</Button>
);
const AssignmentTableCancelAction = ({ selectedFlatRows }) => {
const assignmentUuids = selectedFlatRows.map(row => row.id);
const assignmentConfigurationUuid = selectedFlatRows[0].original.assignmentConfiguration;
const {
assignButtonState,
cancelContentAssignments,
close,
isOpen,
open,
} = useCancelContentAssignments(assignmentConfigurationUuid, assignmentUuids);

return (
<>
<Button variant="danger" iconBefore={DoNotDisturbOn} onClick={open}>
{`Cancel (${assignmentUuids.length})`}
</Button>
<CancelAssignmentModal
cancelContentAssignments={cancelContentAssignments}
close={close}
isOpen={isOpen}
assignButtonState={assignButtonState}
uuidCount={assignmentUuids.length}
/>
</>
);
};

AssignmentTableCancelAction.propTypes = {
selectedFlatRows: PropTypes.arrayOf(PropTypes.shape()),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import React from 'react';
import React, { useMemo } from 'react';
import PropTypes from 'prop-types';
import { Helmet } from 'react-helmet';
import { Container, Toast } from '@edx/paragon';

import Hero from '../Hero';
import { useSuccessfulAssignmentToastContextValue } from './data';
import { useSuccessfulAssignmentToastContextValue, useSuccessfulCancellationToastContextValue } from './data';

const PAGE_TITLE = 'Learner Credit Management';

Expand All @@ -20,22 +20,37 @@ const BudgetDetailPageWrapper = ({
const budgetDisplayName = subsidyAccessPolicy?.displayName || 'Overview';
const helmetPageTitle = budgetDisplayName ? `${budgetDisplayName} - ${PAGE_TITLE}` : PAGE_TITLE;

const successfulAssignmentToastContextValue = useSuccessfulAssignmentToastContextValue();
const successfulAssignmentToast = useSuccessfulAssignmentToastContextValue();
const successfulCancellationToast = useSuccessfulCancellationToastContextValue();

const {
isSuccessfulAssignmentAllocationToastOpen,
successfulAssignmentAllocationToastMessage,
closeToastForAssignmentAllocation,
} = successfulAssignmentToastContextValue;
} = successfulAssignmentToast;

const {
isSuccessfulAssignmentCancellationToastOpen,
successfulAssignmentCancellationToastMessage,
closeToastForAssignmentCancellation,
} = successfulCancellationToast;

const values = useMemo(() => ({
successfulAssignmentToast,
successfulCancellationToast,
}), [successfulAssignmentToast, successfulCancellationToast]);

return (
<BudgetDetailPageContext.Provider value={successfulAssignmentToastContextValue}>
<BudgetDetailPageContext.Provider
value={values}
>
<Helmet title={helmetPageTitle} />
{includeHero && <Hero title={PAGE_TITLE} />}
<Container className="py-3" fluid>
{children}
</Container>
{/**
Successful assignment allocation Toast notification. It is rendered here to guarantee that the
Successful assignment allocation and cancellation Toast notifications. It is rendered here to guarantee that the
Toast component will not be unmounted when the user programmatically navigates to the "Activity"
tab, which will unmount the course cards that rendered the assignment modal. Thus, the Toast must
be rendered within the component tree that's common to both the "Activity" and "Overview" tabs.
Expand All @@ -46,6 +61,13 @@ const BudgetDetailPageWrapper = ({
>
{successfulAssignmentAllocationToastMessage}
</Toast>

<Toast
onClose={closeToastForAssignmentCancellation}
show={isSuccessfulAssignmentCancellationToastOpen}
>
{successfulAssignmentCancellationToastMessage}
</Toast>
</BudgetDetailPageContext.Provider>
);
};
Expand Down
74 changes: 74 additions & 0 deletions src/components/learner-credit-management/CancelAssignmentModal.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import React, { useContext } from 'react';
import PropTypes from 'prop-types';
import {
ActionRow, ModalDialog, StatefulButton,
} from '@edx/paragon';
import { DoNotDisturbOn } from '@edx/paragon/icons';
import { BudgetDetailPageContext } from './BudgetDetailPageWrapper';

const CancelAssignmentModal = ({
assignButtonState,
cancelContentAssignments,
close,
isOpen,
uuidCount,
}) => {
const {
successfulCancellationToast: { displayToastForAssignmentCancellation },
} = useContext(BudgetDetailPageContext);

const handleOnClick = async () => {
await cancelContentAssignments();
displayToastForAssignmentCancellation(uuidCount);
};

return (
<ModalDialog
hasCloseButton
isOpen={isOpen}
onClose={close}
title="Cancel dialog"
>
<ModalDialog.Header>
<ModalDialog.Title>
Cancel assignment?
</ModalDialog.Title>
</ModalDialog.Header>

<ModalDialog.Body>
<p>This action cannot be undone.</p>
<p>The learner will be notified that you have canceled the assignment. The funds associated with
this course assignment will move from &quot;assigned&quot; back to &quot;available&quot;.
</p>
</ModalDialog.Body>

<ModalDialog.Footer>
<ActionRow>
<ModalDialog.CloseButton variant="tertiary">Go back</ModalDialog.CloseButton>
<StatefulButton
iconBefore={assignButtonState === 'default' ? DoNotDisturbOn : null}
labels={{
default: uuidCount > 1 ? `Cancel assignments (${uuidCount})` : 'Cancel assignment',
pending: 'Canceling...',
complete: 'Canceled',
error: 'Try again',
}}
variant="danger"
state={assignButtonState}
onClick={handleOnClick}
/>
</ActionRow>
</ModalDialog.Footer>
</ModalDialog>
);
};

CancelAssignmentModal.propTypes = {
assignButtonState: PropTypes.string.isRequired,
cancelContentAssignments: PropTypes.func.isRequired,
close: PropTypes.func.isRequired,
isOpen: PropTypes.bool.isRequired,
uuidCount: PropTypes.number,
};

export default CancelAssignmentModal;
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import React from 'react';
import PropTypes from 'prop-types';
import {
Icon, IconButtonWithTooltip,
} from '@edx/paragon';
import { DoNotDisturbOn } from '@edx/paragon/icons';
import useCancelContentAssignments from './data/hooks/useCancelContentAssignments';
import CancelAssignmentModal from './CancelAssignmentModal';

const PendingAssignmentCancelButton = ({ row }) => {
const emailAltText = row.original.learnerEmail ? `for ${row.original.learnerEmail}` : '';
const {
assignButtonState,
cancelContentAssignments,
close,
isOpen,
open,
} = useCancelContentAssignments(row.original.assignmentConfiguration, [row.original.uuid]);
return (
<>
<IconButtonWithTooltip
alt={`Cancel assignment ${emailAltText}`}
data-testid={`cancel-assignment-${row.original.uuid}`}
iconAs={Icon}
onClick={open}
src={DoNotDisturbOn}
tooltipContent="Cancel assignment"
tooltipPlacement="top"
variant="danger"
/>
<CancelAssignmentModal
assignButtonState={assignButtonState}
close={close}
cancelContentAssignments={cancelContentAssignments}
isOpen={isOpen}
/>
</>
);
};

PendingAssignmentCancelButton.propTypes = {
row: PropTypes.shape({
original: PropTypes.shape({
assignmentConfiguration: PropTypes.string.isRequired,
learnerEmail: PropTypes.string,
uuid: PropTypes.string.isRequired,
}).isRequired,
}).isRequired,
};

export default PendingAssignmentCancelButton;
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import React, { useState } from 'react';
import { Chip, useToggle, Hyperlink } from '@edx/paragon';
import { Error } from '@edx/paragon/icons';
import BaseModalPopup from './BaseModalPopup';

const FailedCancellation = () => {
const [isOpen, open, close] = useToggle(false);
const [target, setTarget] = useState(null);

return (
<>
<Chip
disabled={false}
iconBefore={Error}
onClick={open}
onKeyPress={open}
ref={setTarget}
tabIndex={0}
variant="light"
>
Failed: Cancellation
</Chip>
<BaseModalPopup
positionRef={target}
isOpen={isOpen}
onClose={close}
>
<BaseModalPopup.Heading icon={Error} iconClassName="text-danger">
Failed: Cancellation
</BaseModalPopup.Heading>
<BaseModalPopup.Content>
<p>This assignment was not canceled. Something went wrong behind the scenes.</p>
<div className="micro">
<p className="h6">Suggested resolution steps</p>
<ul className="text-gray pl-3">
<li>
Wait and try to cancel this assignment again later
</li>
<li>
If the issue continues, contact customer support.
</li>
<li>
Get more troubleshooting help at{' '}
<Hyperlink destination="https://edx.org" target="_blank">
Help Center: Course Assignments
</Hyperlink>
</li>
</ul>
</div>
</BaseModalPopup.Content>
</BaseModalPopup>
</>
);
};

export default FailedCancellation;
Loading