Skip to content

Commit

Permalink
feat: edit allowance action
Browse files Browse the repository at this point in the history
  • Loading branch information
varshamenon4 committed Aug 20, 2024
1 parent a2da333 commit 76611f8
Show file tree
Hide file tree
Showing 13 changed files with 370 additions and 31 deletions.
4 changes: 2 additions & 2 deletions src/pages/ExamsPage/components/AddAllowanceModal.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ const AddAllowanceModal = ({ isOpen, close }) => {
value={form['allowance-type']}
data-testid="allowance-type"
>
<option value="additional-minutes">{ formatMessage(messages.addAllowanceAdditionalMinutesOption) }</option>
<option value="additional-minutes">{ formatMessage(messages.allowanceAdditionalMinutesOption) }</option>
<option value="time-multiplier">{ formatMessage(messages.addAllowanceTimeMultiplierOption) }</option>
</Form.Control>
</Form.Group>
Expand All @@ -202,7 +202,7 @@ const AddAllowanceModal = ({ isOpen, close }) => {
onChange={handleChange}
data-testid="additional-time-minutes"
/>
{ additionalTimeError && <Form.Control.Feedback type="invalid">{ formatMessage(messages.addAllowanceMinutesErrorFeedback) }</Form.Control.Feedback> }
{ additionalTimeError && <Form.Control.Feedback type="invalid">{ formatMessage(messages.allowanceMinutesErrorFeedback) }</Form.Control.Feedback> }
</Form.Group>
)
: (
Expand Down
2 changes: 1 addition & 1 deletion src/pages/ExamsPage/components/AddAllowanceModal.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ describe('AddAllowanceModal', () => {
fireEvent.click(screen.getByTestId('create-allowance-stateful-button'));
expect(screen.getByText('Enter learners')).toBeInTheDocument();
expect(screen.getByText('Select exams')).toBeInTheDocument();
expect(screen.getByText('Enter minutes')).toBeInTheDocument();
expect(screen.getByText('Enter minutes greater than 0')).toBeInTheDocument();
});

it('should show an alert if the request fails', () => {
Expand Down
1 change: 1 addition & 0 deletions src/pages/ExamsPage/components/AllowanceList.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ const AllowanceList = () => {
examName,
allowanceType: formatMessage(messages.allowanceTypeMinutes),
extraTimeMins,
username,
};

const user = acc.find(u => u.userId === userId);
Expand Down
1 change: 1 addition & 0 deletions src/pages/ExamsPage/components/AllowanceList.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ jest.mock('../hooks', () => ({
useButtonStateFromRequestStatus: jest.fn(),
useCreateAllowance: jest.fn(),
useFilteredExamsData: jest.fn(),
useEditAllowance: jest.fn(),
useDeleteAllowance: jest.fn(),
}));

Expand Down
143 changes: 137 additions & 6 deletions src/pages/ExamsPage/components/AllowanceListActions.jsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,144 @@
import { PropTypes } from 'prop-types';
import {
ActionRow,
ActionRow, Alert,
Button,
Form,
Icon,
IconButtonWithTooltip,
ModalDialog,
useToggle,
} from '@openedx/paragon';
import { DeleteOutline, EditOutline } from '@openedx/paragon/icons';
import { DeleteOutline, EditOutline, Info } from '@openedx/paragon/icons';
import { useIntl } from '@edx/frontend-platform/i18n';

import React, { useState } from 'react';
import messages from '../messages';
import { useDeleteAllowance } from '../hooks';
import { useDeleteAllowance, useEditAllowance } from '../hooks';
import { useClearRequest, useRequestError } from '../../../data/redux/hooks';
import * as constants from '../../../data/constants';

const EditModal = (isOpen, close, allowance, formatMessage) => {
const editAllowance = useEditAllowance();
const [additionalTimeError, setAdditionalTimeError] = useState(false);
const requestError = useRequestError(constants.RequestKeys.createAllowance);
const resetRequestError = useClearRequest(constants.RequestKeys.createAllowance);

const initialFormState = {
username: allowance.username,
'exam-id': allowance.examId,
'exam-name': allowance.examName,
};
const [form, setForm] = useState(initialFormState);

const handleChange = (event) => {
const { name, value } = event.target;
resetRequestError();
setForm(prev => ({ ...prev, [name]: value }));
};

const onClose = () => {
resetRequestError();
setForm(initialFormState);
close();
};

const onSubmit = () => {
const extraTimeMins = +form['extra-time-mins'] || 0;
// todo: We should maybe move the handling of this validation to the backend,
// as this should also be handled for the add allowance modal.
const valid = (
extraTimeMins
&& extraTimeMins > 0
);
setAdditionalTimeError(!valid);
if (valid) {
const payload = {
username: form.username,
exam_id: form['exam-id'],
extra_time_mins: extraTimeMins,
};
editAllowance(allowance.id, payload, () => {
setForm(initialFormState);
onClose();
});
}
};

return (
<ModalDialog
title="edit allowance"
isOpen={isOpen}
onClose={onClose}
variant="default"
hasCloseButton
isFullscreenOnMobile
>
<ModalDialog.Header>
<ModalDialog.Title data-testid="edit-allowance-header">
{formatMessage(messages.editAllowanceHeader)}
</ModalDialog.Title>
</ModalDialog.Header>
<ModalDialog.Body>
{ requestError
&& (
<Alert
variant="danger"
icon={Info}
>
<Alert.Heading>{ formatMessage(messages.addAllowanceFailedAlertHeader) }</Alert.Heading>
<p>
{ requestError.detail }
</p>
</Alert>
)}
<Form id="edit-allowance-form" onSubmit={onSubmit}>
<Form.Group>
<Form.Label>{ formatMessage(messages.allowanceUsernameField) }</Form.Label>
<Form.Control
name="username"
disabled
value={form.username}
/>
</Form.Group>
<Form.Group>
<Form.Label>{ formatMessage(messages.editAllowanceExamField) }</Form.Label>
<Form.Control
name="exam"
disabled
value={form['exam-name']}
/>
</Form.Group>
<Form.Group>
<Form.Label>{ formatMessage(messages.allowanceTypeField) }</Form.Label>
<Form.Text muted>
{formatMessage(messages.allowanceAdditionalMinutesOption)}
</Form.Text>
</Form.Group>
<Form.Group controlId="form-allowance-value-minutes" isInvalid={additionalTimeError}>
<Form.Label>{ formatMessage(messages.allowanceMinutesField) }</Form.Label>
<Form.Control
name="extra-time-mins"
value={form['extra-time-mins'] || ''}
onChange={handleChange}
data-testid="extra-time-mins"
/>
{ additionalTimeError && <Form.Control.Feedback type="invalid">{ formatMessage(messages.allowanceMinutesErrorFeedback) }</Form.Control.Feedback> }
</Form.Group>
</Form>
</ModalDialog.Body>
<ModalDialog.Footer>
<ActionRow>
<ModalDialog.CloseButton variant="tertiary" onClick={onClose}>
{formatMessage(messages.allowanceCancelButton)}
</ModalDialog.CloseButton>
<Button variant="primary" onClick={onSubmit}>
{formatMessage(messages.editAllowanceSave)}
</Button>
</ActionRow>
</ModalDialog.Footer>
</ModalDialog>
);
};

const DeleteModal = (isOpen, onCancel, onDelete, formatMessage) => (
<ModalDialog
Expand All @@ -33,7 +160,7 @@ const DeleteModal = (isOpen, onCancel, onDelete, formatMessage) => (
<ModalDialog.Footer>
<ActionRow>
<ModalDialog.CloseButton variant="tertiary" onClick={onCancel}>
{formatMessage(messages.deleteAllowanceCancel)}
{formatMessage(messages.allowanceCancelButton)}
</ModalDialog.CloseButton>
<Button variant="primary" onClick={onDelete}>
{formatMessage(messages.deleteAllowanceDelete)}
Expand All @@ -45,8 +172,10 @@ const DeleteModal = (isOpen, onCancel, onDelete, formatMessage) => (

const AllowanceListActions = ({ allowance }) => {
const { formatMessage } = useIntl();
const [isDeleteModalOpen, setDeleteModalOpen, setDeleteModalClosed] = useToggle(false);

const [isEditModalOpen, setEditModalOpen, setEditModalClosed] = useToggle(false);

const [isDeleteModalOpen, setDeleteModalOpen, setDeleteModalClosed] = useToggle(false);
const deleteAllowance = useDeleteAllowance();

const handleDelete = () => {
Expand All @@ -61,10 +190,11 @@ const AllowanceListActions = ({ allowance }) => {
src={EditOutline}
iconAs={Icon}
alt={formatMessage(messages.editAllowanceButton)}
onClick={setDeleteModalOpen}
onClick={setEditModalOpen}
variant="primary"
className="mr-2"
size="sm"
data-testid="edit-allowance-icon"
/>
<IconButtonWithTooltip
tooltipPlacement="top"
Expand All @@ -77,6 +207,7 @@ const AllowanceListActions = ({ allowance }) => {
size="sm"
data-testid="delete-allowance-icon"
/>
{EditModal(isEditModalOpen, setEditModalClosed, allowance, formatMessage)}
{DeleteModal(isDeleteModalOpen, setDeleteModalClosed, handleDelete, formatMessage)}
</div>
);
Expand Down
84 changes: 77 additions & 7 deletions src/pages/ExamsPage/components/AllowanceListActions.test.jsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,43 @@
import { render, screen } from '@testing-library/react';

import { useDeleteAllowance } from '../hooks';
import { useDeleteAllowance, useEditAllowance } from '../hooks';
import AllowanceListActions from './AllowanceListActions';
import * as reduxHooks from '../../../data/redux/hooks';

jest.mock('../hooks', () => ({
useDeleteAllowance: jest.fn(),
}));

// nomally mocked for unit tests but required for rendering/snapshots
jest.unmock('react');
// const mockEditAllowance = jest.fn();
const mockClearRequest = jest.fn();
const mockRequestError = jest.fn();

const mockAllowance = {
id: 1,
examId: 1,
username: 'edx',
examName: 'edX Exams',
allowanceType: 'Minutes',
extraTimeMins: 45,
};

jest.mock('../hooks', () => ({
useDeleteAllowance: jest.fn(),
useEditAllowance: jest.fn(),
}));

jest.mock('../../../data/redux/hooks', () => ({
useRequestError: jest.fn(),
useClearRequest: jest.fn(),
}));

// nomally mocked for unit tests but required for rendering/snapshots
jest.unmock('react');

describe('AllowanceListActions', () => {
beforeEach(() => {
jest.resetAllMocks();
// hooks.useEditAllowance().mockReturnValue(mockEditAllowance);
reduxHooks.useRequestError.mockReturnValue(mockRequestError);
reduxHooks.useClearRequest.mockReturnValue(mockClearRequest);
});

describe('snapshots', () => {
test('displayed buttons should match snapshot', () => {
expect(render(<AllowanceListActions allowance={mockAllowance} />)).toMatchSnapshot();
Expand All @@ -31,6 +50,13 @@ describe('AllowanceListActions', () => {
expect(screen.getByText('Are you sure you want to delete this allowance?')).toBeInTheDocument();
});

// todo: figure out why this isn't working
it('should open the edit modal when the edit icon is clicked', () => {
render(<AllowanceListActions allowance={mockAllowance} />);
screen.getByTestId('edit-allowance-icon').click();
expect(screen.getByTestId('edit-allowance-header')).toBeInTheDocument();
});

describe('delete modal', () => {
it('should delete the allowance when the delete button is clicked', () => {
const mockDeleteAllowance = jest.fn();
Expand Down Expand Up @@ -59,4 +85,48 @@ describe('AllowanceListActions', () => {
expect(screen.queryByText('Are you sure you want to delete this allowance?')).not.toBeInTheDocument();
});
});

describe('edit modal', () => {
// todo: figure out why this fails
it('should edit the allowance when the save button is clicked', () => {
const mockEditAllowance = jest.fn();
useEditAllowance.mockReturnValue(mockEditAllowance);
render(<AllowanceListActions allowance={mockAllowance} />);
screen.getByTestId('edit-allowance-icon').click();
screen.getByText('Save').click();
const expectedData = {
username: mockAllowance.username,
exam_id: mockAllowance.examId,
extra_time_mins: mockAllowance.extraTimeMins,
};
expect(mockEditAllowance).toHaveBeenCalledWith(mockAllowance.id, expectedData, expect.any(Function));
});

it('should display errors when the save button is clicked', () => {
const mockEditAllowance = jest.fn();
useEditAllowance.mockReturnValue(mockEditAllowance);
render(<AllowanceListActions allowance={mockAllowance} />);
screen.getByTestId('edit-allowance-icon').click();
screen.getByText('Save').click();
expect(screen.getByText('Enter minutes greater than 0')).toBeInTheDocument();
});

it('should close the modal when the cancel button is clicked', () => {
render(<AllowanceListActions allowance={mockAllowance} />);
screen.getByTestId('edit-allowance-icon').click();
screen.getByText('Cancel').click();
expect(screen.queryByTestId('edit-allowance-header')).not.toBeInTheDocument();
});

// todo: figure out why this fails
it('should close the modal when the edit is successful', () => {
const mockEditAllowance = jest.fn();
useEditAllowance.mockReturnValue(mockEditAllowance);
render(<AllowanceListActions allowance={mockAllowance} />);
screen.getByTestId('edit-allowance-icon').click();
screen.getByText('Save').click();
// expect(mockEditAllowance.mock.calls).toBe(1);
expect(screen.queryByTestId('edit-allowance-header')).not.toBeInTheDocument();
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,7 @@ exports[`AllowanceList when listing allowances should match snapshot 1`] = `
<button
aria-label="Edit allowance"
class="btn-icon btn-icon-primary btn-icon-sm mr-2"
data-testid="edit-allowance-icon"
type="button"
>
<span
Expand Down Expand Up @@ -299,6 +300,7 @@ exports[`AllowanceList when listing allowances should match snapshot 1`] = `
<button
aria-label="Edit allowance"
class="btn-icon btn-icon-primary btn-icon-sm mr-2"
data-testid="edit-allowance-icon"
type="button"
>
<span
Expand Down Expand Up @@ -576,6 +578,7 @@ exports[`AllowanceList when listing allowances should match snapshot 1`] = `
<button
aria-label="Edit allowance"
class="btn-icon btn-icon-primary btn-icon-sm mr-2"
data-testid="edit-allowance-icon"
type="button"
>
<span
Expand Down Expand Up @@ -667,6 +670,7 @@ exports[`AllowanceList when listing allowances should match snapshot 1`] = `
<button
aria-label="Edit allowance"
class="btn-icon btn-icon-primary btn-icon-sm mr-2"
data-testid="edit-allowance-icon"
type="button"
>
<span
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ exports[`AllowanceListActions snapshots displayed buttons should match snapshot
<button
aria-label="Edit allowance"
class="btn-icon btn-icon-primary btn-icon-sm mr-2"
data-testid="edit-allowance-icon"
type="button"
>
<span
Expand Down Expand Up @@ -77,6 +78,7 @@ exports[`AllowanceListActions snapshots displayed buttons should match snapshot
<button
aria-label="Edit allowance"
class="btn-icon btn-icon-primary btn-icon-sm mr-2"
data-testid="edit-allowance-icon"
type="button"
>
<span
Expand Down
Loading

0 comments on commit 76611f8

Please sign in to comment.