Skip to content

Commit

Permalink
fix: support remind/cancel-all
Browse files Browse the repository at this point in the history
  • Loading branch information
iloveagent57 committed Dec 14, 2023
1 parent 4a03e0e commit 1333f19
Show file tree
Hide file tree
Showing 10 changed files with 158 additions and 28 deletions.
25 changes: 25 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,31 @@ module.exports = {

NB: In order for webpack to properly resolve scss imports locally, you must use a `~` before the import, like so: `@import "~@edx/brand/paragon/fonts";`

### Running tests

You can run all tests as follows:
```
nvm use
npm install
npm run test
```

Additionally, you can run a single test file with
```
npm run test -- ProvisioningFormSubmissionButton.test.jsx
```

or run a given test function by appending a `.only` to the test function (or appending `.skip` to do the inverse).
For example: `test.only('your test function', () => {...})`.

You can use watch mode with tests as follows:
```
npm run test:watch BudgetDetailPage.test.jsx
# or to skip coverage reporting
npm run test:watch-no-cov BudgetDetailpage.test.jsx
```
Note the watcher has its own set of commands to help run test functions that match a regex (`t my regex`), etc.
Use the `w` command to get a list of valid watch commands.

## Getting Help

Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"debug-test": "node --inspect-brk node_modules/.bin/jest --runInBand --coverage",
"test": "TZ=UTC fedx-scripts jest --coverage --passWithNoTests --maxWorkers=50%",
"test:watch": "npm run test -- --watch",
"test:watch-no-cov": "npm run test -- --watch --collectCoverage=false",
"snapshot": "fedx-scripts jest --updateSnapshot"
},
"license": "AGPL-3.0",
Expand Down
34 changes: 30 additions & 4 deletions src/components/learner-credit-management/AssignmentTableCancel.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,18 @@ import { DoNotDisturbOn } from '@edx/paragon/icons';
import CancelAssignmentModal from './CancelAssignmentModal';
import useCancelContentAssignments from './data/hooks/useCancelContentAssignments';

const AssignmentTableCancelAction = ({ selectedFlatRows }) => {
const calculateTotalToCancel = ({
assignmentUuids,
isEntireTableSelected,
tableItemCount,
}) => {
if (isEntireTableSelected) {
return tableItemCount;
}
return assignmentUuids.length;
};

const AssignmentTableCancelAction = ({ selectedFlatRows, isEntireTableSelected, tableInstance }) => {
const assignmentUuids = selectedFlatRows.map(row => row.id);
const assignmentConfigurationUuid = selectedFlatRows[0].original.assignmentConfiguration;
const {
Expand All @@ -14,30 +25,45 @@ const AssignmentTableCancelAction = ({ selectedFlatRows }) => {
close,
isOpen,
open,
} = useCancelContentAssignments(assignmentConfigurationUuid, assignmentUuids);
} = useCancelContentAssignments(assignmentConfigurationUuid, assignmentUuids, isEntireTableSelected);

const tableItemCount = tableInstance.itemCount;
const totalToCancel = calculateTotalToCancel({
assignmentUuids,
isEntireTableSelected,
tableItemCount,
});

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

AssignmentTableCancelAction.propTypes = {
selectedFlatRows: PropTypes.arrayOf(PropTypes.shape()),
isEntireTableSelected: PropTypes.bool,
tableInstance: PropTypes.shape({
itemCount: PropTypes.number.isRequired,
}),
};

AssignmentTableCancelAction.defaultProps = {
selectedFlatRows: [],
isEntireTableSelected: false,
tableInstance: {
itemCount: 0,
},
};

export default AssignmentTableCancelAction;
38 changes: 31 additions & 7 deletions src/components/learner-credit-management/AssignmentTableRemind.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,44 +5,68 @@ import { Mail } from '@edx/paragon/icons';
import useRemindContentAssignments from './data/hooks/useRemindContentAssignments';
import RemindAssignmentModal from './RemindAssignmentModal';

const AssignmentTableRemindAction = ({ selectedFlatRows }) => {
const calculateTotalToRemind = ({
assignmentUuids,
isEntireTableSelected,
learnerStateCounts,
}) => {
if (isEntireTableSelected) {
const waitingAssignmentCounts = learnerStateCounts.filter(({ learnerState }) => (learnerState === 'waiting'));
return waitingAssignmentCounts.length ? waitingAssignmentCounts[0].count : 0;
}
return assignmentUuids.length;
};

const AssignmentTableRemindAction = ({ selectedFlatRows, isEntireTableSelected, learnerStateCounts }) => {
const assignmentUuids = selectedFlatRows.filter(row => row.original.learnerState === 'waiting').map(({ id }) => id);
const assignmentConfigurationUuid = selectedFlatRows[0].original.assignmentConfiguration;
const selectedRemindableRows = selectedFlatRows.filter(row => row.original.learnerState === 'waiting').length;
const {
remindButtonState,
remindContentAssignments,
close,
isOpen,
open,
} = useRemindContentAssignments(assignmentConfigurationUuid, assignmentUuids);
} = useRemindContentAssignments(assignmentConfigurationUuid, assignmentUuids, isEntireTableSelected);

const selectedRemindableRowCount = calculateTotalToRemind({
assignmentUuids,
isEntireTableSelected,
learnerStateCounts,
});

return (
<>
<Button
disabled={selectedRemindableRows === 0}
alt={`Send reminder to ${selectedRemindableRows} learners`}
disabled={selectedRemindableRowCount === 0}
alt={`Send reminder to ${selectedRemindableRowCount} learners`}
iconBefore={Mail}
onClick={open}
>
{`Remind (${selectedRemindableRows})`}
{`Remind (${selectedRemindableRowCount})`}
</Button>
<RemindAssignmentModal
remindContentAssignments={remindContentAssignments}
close={close}
isOpen={isOpen}
remindButtonState={remindButtonState}
uuidCount={assignmentUuids.length}
uuidCount={selectedRemindableRowCount}
/>
</>
);
};

AssignmentTableRemindAction.propTypes = {
selectedFlatRows: PropTypes.arrayOf(PropTypes.shape()),
isEntireTableSelected: PropTypes.bool,
learnerStateCounts: PropTypes.arrayOf(PropTypes.shape({
learnerState: PropTypes.string.isRequired,
count: PropTypes.number.isRequired,
})).isRequired,
};

AssignmentTableRemindAction.defaultProps = {
selectedFlatRows: [],
isEntireTableSelected: false,
};

export default AssignmentTableRemindAction;
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ const BudgetAssignmentsTable = ({
pageCount={tableData.numPages || 1}
EmptyTableComponent={CustomDataTableEmptyState}
bulkActions={[
<AssignmentTableRemindAction />,
<AssignmentTableRemindAction learnerStateCounts={tableData.learnerStateCounts} />,
<AssignmentTableCancelAction />,
]}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import useBudgetId from './useBudgetId';
const useCancelContentAssignments = (
assignmentConfigurationUuid,
assignmentUuids,
cancelAll = false,
) => {
const [isOpen, open, close] = useToggle(false);
const [cancelButtonState, setCancelButtonState] = useState('default');
Expand All @@ -19,7 +20,11 @@ const useCancelContentAssignments = (
const cancelContentAssignments = useCallback(async () => {
setCancelButtonState('pending');
try {
await EnterpriseAccessApiService.cancelContentAssignments(assignmentConfigurationUuid, assignmentUuids);
if (cancelAll) {
await EnterpriseAccessApiService.cancelAllContentAssignments(assignmentConfigurationUuid);
} else {
await EnterpriseAccessApiService.cancelContentAssignments(assignmentConfigurationUuid, assignmentUuids);
}
setCancelButtonState('complete');
queryClient.invalidateQueries({
queryKey: learnerCreditManagementQueryKeys.budget(subsidyAccessPolicyId),
Expand All @@ -28,7 +33,7 @@ const useCancelContentAssignments = (
logError(err);
setCancelButtonState('error');
}
}, [assignmentConfigurationUuid, assignmentUuids, queryClient, subsidyAccessPolicyId]);
}, [assignmentConfigurationUuid, assignmentUuids, cancelAll, queryClient, subsidyAccessPolicyId]);

return {
cancelButtonState,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import useBudgetId from './useBudgetId';
const useRemindContentAssignments = (
assignmentConfigurationUuid,
assignmentUuids,
remindAll = false,
) => {
const [isOpen, open, close] = useToggle(false);
const [remindButtonState, setRemindButtonState] = useState('default');
Expand All @@ -19,7 +20,11 @@ const useRemindContentAssignments = (
const remindContentAssignments = useCallback(async () => {
setRemindButtonState('pending');
try {
await EnterpriseAccessApiService.remindContentAssignments(assignmentConfigurationUuid, assignmentUuids);
if (remindAll) {
await EnterpriseAccessApiService.remindAllContentAssignments(assignmentConfigurationUuid);
} else {
await EnterpriseAccessApiService.remindContentAssignments(assignmentConfigurationUuid, assignmentUuids);
}
setRemindButtonState('complete');
queryClient.invalidateQueries({
queryKey: learnerCreditManagementQueryKeys.budget(subsidyAccessPolicyId),
Expand All @@ -28,7 +33,7 @@ const useRemindContentAssignments = (
logError(err);
setRemindButtonState('error');
}
}, [assignmentConfigurationUuid, assignmentUuids, queryClient, subsidyAccessPolicyId]);
}, [assignmentConfigurationUuid, assignmentUuids, remindAll, queryClient, subsidyAccessPolicyId]);

return {
remindButtonState,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1453,7 +1453,7 @@ describe('<BudgetDetailPage />', () => {
});

it('cancels assignments in bulk', async () => {
EnterpriseAccessApiService.cancelContentAssignments.mockResolvedValueOnce({ status: 200 });
EnterpriseAccessApiService.cancelAllContentAssignments.mockResolvedValueOnce({ status: 200 });
useParams.mockReturnValue({
budgetId: mockSubsidyAccessPolicyUUID,
activeTabKey: 'activity',
Expand Down Expand Up @@ -1520,16 +1520,18 @@ describe('<BudgetDetailPage />', () => {
expect(modalDialog).toBeInTheDocument();
const cancelDialogButton = getButtonElement('Cancel assignments (2)');
userEvent.click(cancelDialogButton);
expect(
EnterpriseAccessApiService.cancelContentAssignments,
).toHaveBeenCalled();
await waitFor(
() => expect(
EnterpriseAccessApiService.cancelAllContentAssignments,
).toHaveBeenCalled(),
);
await waitFor(
() => expect(screen.getByText('Assignments canceled (2)')).toBeInTheDocument(),
);
});

it('reminds assignments in bulk', async () => {
EnterpriseAccessApiService.remindContentAssignments.mockResolvedValueOnce({ status: 200 });
EnterpriseAccessApiService.remindAllContentAssignments.mockResolvedValueOnce({ status: 202 });
useParams.mockReturnValue({
budgetId: mockSubsidyAccessPolicyUUID,
activeTabKey: 'activity',
Expand All @@ -1553,7 +1555,7 @@ describe('<BudgetDetailPage />', () => {
useBudgetContentAssignments.mockReturnValue({
isLoading: false,
contentAssignments: {
count: 2,
count: 3,
results: [
{
uuid: 'test-uuid1',
Expand All @@ -1575,10 +1577,20 @@ describe('<BudgetDetailPage />', () => {
errorReason: null,
state: 'allocated',
},
{
uuid: 'test-uuid3',
contentKey: mockCourseKey,
contentQuantity: -29900,
learnerState: 'notifying',
recentAction: { actionType: 'assigned', timestamp: '2023-11-27' },
actions: [mockSuccessfulNotifiedAction],
errorReason: null,
state: 'allocated',
},
],
learnerStateCounts: [
{ learnerState: 'waiting', count: 1 },
{ learnerState: 'waiting', count: 1 },
{ learnerState: 'waiting', count: 2 },
{ learnerState: 'notifying', count: 1 },
],
numPages: 1,
currentPage: 1,
Expand All @@ -1596,9 +1608,11 @@ describe('<BudgetDetailPage />', () => {
expect(modalDialog).toBeInTheDocument();
const remindDialogButton = getButtonElement('Send reminders (2)');
userEvent.click(remindDialogButton);
expect(
EnterpriseAccessApiService.remindContentAssignments,
).toHaveBeenCalled();
await waitFor(
() => expect(
EnterpriseAccessApiService.remindAllContentAssignments,
).toHaveBeenCalled(),
);
await waitFor(
() => expect(screen.getByText('Reminders sent (2)')).toBeInTheDocument(),
);
Expand Down
16 changes: 16 additions & 0 deletions src/data/services/EnterpriseAccessApiService.js
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,14 @@ class EnterpriseAccessApiService {
return EnterpriseAccessApiService.apiClient().post(url, options);
}

/**
* Cancel ALL content assignments for a specific AssignmentConfiguration.
*/
static cancelAllContentAssignments(assignmentConfigurationUUID) {
const url = `${EnterpriseAccessApiService.baseUrl}/assignment-configurations/${assignmentConfigurationUUID}/admin/assignments/cancel-all/`;
return EnterpriseAccessApiService.apiClient().post(url);
}

/**
* Remind content assignments for a specific AssignmentConfiguration.
*/
Expand All @@ -195,6 +203,14 @@ class EnterpriseAccessApiService {
return EnterpriseAccessApiService.apiClient().post(url, options);
}

/**
* Remind ALL content assignments for a specific AssignmentConfiguration.
*/
static remindAllContentAssignments(assignmentConfigurationUUID) {
const url = `${EnterpriseAccessApiService.baseUrl}/assignment-configurations/${assignmentConfigurationUUID}/admin/assignments/remind-all/`;
return EnterpriseAccessApiService.apiClient().post(url);
}

/**
* Retrieve a specific subsidy access policy.
* @param {string} subsidyAccessPolicyUUID The UUID of the subsidy access policy to retrieve.
Expand Down
16 changes: 15 additions & 1 deletion src/data/services/tests/EnterpriseAccessApiService.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ describe('EnterpriseAccessApiService', () => {
);
});

test('remindContentAssignments calls enterprise-access cancel POST API to remind learners', () => {
test('remindContentAssignments calls enterprise-access remind POST API to remind learners', () => {
const options = {
assignment_uuids: mockAssignmentUUIDs,
};
Expand All @@ -211,4 +211,18 @@ describe('EnterpriseAccessApiService', () => {
options,
);
});

test('cancelAllContentAssignments calls enterprise-access cancel-all POST API to cancel all assignments', () => {
EnterpriseAccessApiService.cancelAllContentAssignments(mockAssignmentConfigurationUUID);
expect(axios.post).toBeCalledWith(
`${enterpriseAccessBaseUrl}/api/v1/assignment-configurations/${mockAssignmentConfigurationUUID}/admin/assignments/cancel-all/`,
);
});

test('remindAllContentAssignments calls enterprise-access remind-all POST API to remind all learners', () => {
EnterpriseAccessApiService.remindAllContentAssignments(mockAssignmentConfigurationUUID);
expect(axios.post).toBeCalledWith(
`${enterpriseAccessBaseUrl}/api/v1/assignment-configurations/${mockAssignmentConfigurationUUID}/admin/assignments/remind-all/`,
);
});
});

0 comments on commit 1333f19

Please sign in to comment.