Skip to content

Commit 37aac4a

Browse files
feat: grading tools
1 parent e542cad commit 37aac4a

File tree

12 files changed

+250
-4
lines changed

12 files changed

+250
-4
lines changed

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 SingleLearnerContent from './components/SingleLearnerContent';
5+
import AllLearnersContent from './components/AllLearnersContent';
6+
import messages from './messages';
7+
import GradingActionRow from './components/GradingActionRow';
8+
9+
const GradingPage = () => {
10+
const intl = useIntl();
11+
const [selectedTools, setSelectedTools] = useState('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+
{selectedTools === 'single' ? <SingleLearnerContent /> : <AllLearnersContent />}
35+
</Card>
36+
</>
37+
);
38+
};
39+
40+
export default GradingPage;
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
const AllLearnersContent = () => {
2+
return <div>All Learners Content</div>;
3+
};
4+
5+
export default AllLearnersContent;
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: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { useIntl } from '@openedx/frontend-base';
2+
import messages from '../messages';
3+
import SpecifyProblem from '../../components/SpecifyProblem';
4+
5+
const SingleLearnerContent = () => {
6+
const intl = useIntl();
7+
8+
return (
9+
<>
10+
<p className="x-small text-primary mt-3">{intl.formatMessage(messages.descriptionSingleLearner)}</p>
11+
<SpecifyProblem />
12+
</>
13+
);
14+
};
15+
16+
export default SingleLearnerContent;

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.

0 commit comments

Comments
 (0)