Skip to content

Commit 0e7d0af

Browse files
feat: grading tools
1 parent 6e0596e commit 0e7d0af

12 files changed

Lines changed: 257 additions & 0 deletions

src/components/SpecifyProblem.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
const SpecifyProblem = () => {
2+
return <div>Specify Problem</div>;
3+
};
4+
5+
export default SpecifyProblem;

src/grading/GradingPage.tsx

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { useState } from 'react';
2+
import { useIntl } from '@openedx/frontend-base';
3+
import { Button, ButtonGroup, Card } from '@openedx/paragon';
4+
import GradingLearnerContent from './components/GradingLearnerContent';
5+
import messages from './messages';
6+
import GradingActionRow from './components/GradingActionRow';
7+
import { GradingToolsType } from './types';
8+
9+
const GradingPage = () => {
10+
const intl = useIntl();
11+
const [selectedTools, setSelectedTools] = useState<GradingToolsType>('single');
12+
13+
return (
14+
<>
15+
<div className="d-flex justify-content-between align-items-center">
16+
<h3 className="text-primary-700">{intl.formatMessage(messages.pageTitle)}</h3>
17+
<GradingActionRow />
18+
</div>
19+
<Card className="bg-light-200 p-4 mt-4.5">
20+
<ButtonGroup className="d-block">
21+
<Button
22+
onClick={() => setSelectedTools('single')}
23+
variant={selectedTools === 'single' ? 'primary' : 'outline-primary'}
24+
>
25+
{intl.formatMessage(messages.singleLearner)}
26+
</Button>
27+
<Button
28+
onClick={() => setSelectedTools('all')}
29+
variant={selectedTools === 'all' ? 'primary' : 'outline-primary'}
30+
>
31+
{intl.formatMessage(messages.allLearners)}
32+
</Button>
33+
</ButtonGroup>
34+
<GradingLearnerContent toolType={selectedTools} />
35+
</Card>
36+
</>
37+
);
38+
};
39+
40+
export default GradingPage;
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { screen } from '@testing-library/react';
2+
import userEvent from '@testing-library/user-event';
3+
import { renderWithIntl } from '@src/testUtils';
4+
import GradingActionRow from '@src/grading/components/GradingActionRow';
5+
import messages from '../messages';
6+
7+
describe('GradingActionRow', () => {
8+
beforeEach(() => {
9+
jest.clearAllMocks();
10+
});
11+
12+
it('renders ActionRow with gradebook and configuration buttons', () => {
13+
renderWithIntl(<GradingActionRow />);
14+
expect(screen.getByRole('button', { name: messages.viewGradebook.defaultMessage })).toBeInTheDocument();
15+
expect(screen.getByRole('button', { name: messages.configurationAlt.defaultMessage })).toBeInTheDocument();
16+
});
17+
18+
it('opens configuration menu when configuration button is clicked', async () => {
19+
renderWithIntl(<GradingActionRow />);
20+
const user = userEvent.setup();
21+
await user.click(screen.getByRole('button', { name: messages.configurationAlt.defaultMessage }));
22+
expect(screen.getByText('View Grading Configuration')).toBeInTheDocument();
23+
expect(screen.getByText('View Course Grading Settings')).toBeInTheDocument();
24+
});
25+
26+
it('opens and closes GradingConfigurationModal when menu item is clicked', async () => {
27+
renderWithIntl(<GradingActionRow />);
28+
const user = userEvent.setup();
29+
await user.click(screen.getByRole('button', { name: messages.configurationAlt.defaultMessage }));
30+
const gradingConfigButton = screen.getByText('View Grading Configuration');
31+
await user.click(gradingConfigButton);
32+
expect(screen.getByRole('dialog', { name: messages.gradingConfiguration.defaultMessage })).toBeInTheDocument();
33+
34+
// Close modal
35+
await user.click(screen.getAllByRole('button', { name: messages.close.defaultMessage })[0]);
36+
expect(screen.queryByRole('dialog', { name: messages.gradingConfiguration.defaultMessage })).not.toBeInTheDocument();
37+
});
38+
});
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { useIntl } from '@openedx/frontend-base';
2+
import { useToggle, ActionRow, Button, IconButton, ModalPopup, Menu, MenuItem } from '@openedx/paragon';
3+
import { TrendingUp, MoreVert, OpenInNew } from '@openedx/paragon/icons';
4+
import { useState } from 'react';
5+
import messages from '../messages';
6+
import GradingConfigurationModal from './GradingConfigurationModal';
7+
8+
const GradingActionRow = () => {
9+
const intl = useIntl();
10+
const [configurationMenuTarget, setConfigurationMenuTarget] = useState<HTMLButtonElement | null>(null);
11+
const [isOpenMenu, openMenu, closeMenu] = useToggle(false);
12+
const [isOpenConfigModal, openConfigModal, closeConfigModal] = useToggle(false);
13+
14+
const handleConfigurationMenuClick = (event: React.MouseEvent<HTMLButtonElement>) => {
15+
setConfigurationMenuTarget(event?.currentTarget);
16+
openMenu();
17+
};
18+
return (
19+
<>
20+
<ActionRow>
21+
<Button iconBefore={TrendingUp} variant="outline-primary">{intl.formatMessage(messages.viewGradebook)}</Button>
22+
<IconButton
23+
alt={intl.formatMessage(messages.configurationAlt)}
24+
className="lead"
25+
iconAs={MoreVert}
26+
onClick={handleConfigurationMenuClick}
27+
/>
28+
</ActionRow>
29+
<ModalPopup positionRef={configurationMenuTarget} onClose={closeMenu} isOpen={isOpenMenu}>
30+
<Menu>
31+
<MenuItem onClick={openConfigModal}>
32+
{intl.formatMessage(messages.viewGradingConfiguration)}
33+
</MenuItem>
34+
<MenuItem iconAfter={OpenInNew} onClick={() => {}}>
35+
{intl.formatMessage(messages.viewCourseGradingSettings)}
36+
</MenuItem>
37+
</Menu>
38+
</ModalPopup>
39+
<GradingConfigurationModal isOpen={isOpenConfigModal} onClose={closeConfigModal} />
40+
</>
41+
);
42+
};
43+
44+
export default GradingActionRow;
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { Button, ModalDialog } from '@openedx/paragon';
2+
import { useIntl } from '@openedx/frontend-base';
3+
import messages from '../messages';
4+
5+
interface GradingConfigurationModalProps {
6+
isOpen: boolean,
7+
onClose: () => void,
8+
}
9+
10+
const GradingConfigurationModal = ({ isOpen, onClose }: GradingConfigurationModalProps) => {
11+
const intl = useIntl();
12+
13+
return (
14+
<ModalDialog title={intl.formatMessage(messages.gradingConfiguration)} isOpen={isOpen} onClose={onClose} isOverflowVisible={false}>
15+
<ModalDialog.Header>
16+
<h3>{intl.formatMessage(messages.gradingConfiguration)}</h3>
17+
</ModalDialog.Header>
18+
<ModalDialog.Body>
19+
<p>x</p>
20+
</ModalDialog.Body>
21+
<ModalDialog.Footer>
22+
<Button onClick={onClose}>{intl.formatMessage(messages.close)}</Button>
23+
</ModalDialog.Footer>
24+
</ModalDialog>
25+
);
26+
};
27+
28+
export default GradingConfigurationModal;
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { useIntl } from '@openedx/frontend-base';
2+
import messages from '../messages';
3+
import SpecifyProblem from '../../components/SpecifyProblem';
4+
import { GradingToolsType } from '../types';
5+
6+
interface GradingLearnerContentProps {
7+
toolType: GradingToolsType,
8+
}
9+
10+
const GradingLearnerContent = ({ toolType }: GradingLearnerContentProps) => {
11+
const intl = useIntl();
12+
13+
return (
14+
<>
15+
<p className="x-small text-primary mt-3">
16+
{
17+
toolType === 'single'
18+
? intl.formatMessage(messages.descriptionSingleLearner)
19+
: intl.formatMessage(messages.descriptionAllLearners)
20+
}
21+
</p>
22+
<SpecifyProblem />
23+
</>
24+
);
25+
};
26+
27+
export default GradingLearnerContent;

src/grading/data/api.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
// import { camelCaseObject, getAuthenticatedHttpClient } from '@openedx/frontend-base';
2+
// import { getApiBaseUrl } from '@src/data/api';
3+
4+
// export const getCourseInfo = async (courseId) => {
5+
// const { data } = await getAuthenticatedHttpClient()
6+
// .get(`${getApiBaseUrl()}/api/instructor/v2/courses/${courseId}`);
7+
// return camelCaseObject(data);
8+
// };

src/grading/data/apiHook.ts

Whitespace-only changes.

src/grading/data/queryKeys.ts

Whitespace-only changes.

src/grading/messages.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { defineMessages } from '@openedx/frontend-base';
2+
3+
const messages = defineMessages({
4+
pageTitle: {
5+
id: 'instruct.grading.pageTitle',
6+
defaultMessage: 'Grading Tools',
7+
description: 'Title for the grading page'
8+
},
9+
configurationAlt: {
10+
id: 'instruct.grading.configurationAlt',
11+
defaultMessage: 'Grading Configuration and Settings',
12+
description: 'Alt text for the configuration icon button'
13+
},
14+
viewGradebook: {
15+
id: 'instruct.grading.viewGradebook',
16+
defaultMessage: 'View Gradebook',
17+
description: 'Text for the button to view the gradebook'
18+
},
19+
singleLearner: {
20+
id: 'instruct.grading.singleLearner',
21+
defaultMessage: 'Single Learner',
22+
description: 'Single Learner button label to display corresponding grading tools'
23+
},
24+
allLearners: {
25+
id: 'instruct.grading.allLearners',
26+
defaultMessage: 'All Learners',
27+
description: 'All learners button label to display corresponding grading tools'
28+
},
29+
descriptionSingleLearner: {
30+
id: 'instruct.grading.descriptionSingleLearner',
31+
defaultMessage: 'These grading tools allow for grade review and adjustment for a specific learner on a specific problem.',
32+
description: 'Description for single learner grading tools'
33+
},
34+
descriptionAllLearners: {
35+
id: 'instruct.grading.descriptionAllLearners',
36+
defaultMessage: 'These grading tools allow for grade review and adjustment all enrolled learners on a specific problem. ',
37+
description: 'Description for all learners grading tools'
38+
},
39+
gradingConfiguration: {
40+
id: 'instruct.grading.gradingConfiguration',
41+
defaultMessage: 'Grading Configuration',
42+
description: 'Title for the grading configuration modal'
43+
},
44+
close: {
45+
id: 'instruct.grading.modals.close',
46+
defaultMessage: 'Close',
47+
description: 'Text for the close button in the grading configuration modal'
48+
},
49+
viewGradingConfiguration: {
50+
id: 'instruct.grading.viewGradingConfiguration',
51+
defaultMessage: 'View Grading Configuration',
52+
description: 'View grading configuration menu item label'
53+
},
54+
viewCourseGradingSettings: {
55+
id: 'instruct.grading.viewCourseGradingSettings',
56+
defaultMessage: 'View Course Grading Settings',
57+
description: 'View course grading settings menu item label'
58+
},
59+
});
60+
61+
export default messages;

0 commit comments

Comments
 (0)