diff --git a/src/components/SpecifyProblem.tsx b/src/components/SpecifyProblem.tsx
new file mode 100644
index 00000000..ef4b1433
--- /dev/null
+++ b/src/components/SpecifyProblem.tsx
@@ -0,0 +1,5 @@
+const SpecifyProblem = () => {
+ return
Specify Problem
;
+};
+
+export default SpecifyProblem;
diff --git a/src/courseInfo/types.ts b/src/courseInfo/types.ts
index 51fe1f66..87dd3209 100644
--- a/src/courseInfo/types.ts
+++ b/src/courseInfo/types.ts
@@ -26,6 +26,8 @@ export interface CourseInfoResponse {
dataResearcher: boolean,
[key: string]: boolean,
},
+ gradebookUrl: string,
+ studioGradingUrl?: string,
}
interface EnrollmentCounts extends Record {
diff --git a/src/data/apiHook.test.tsx b/src/data/apiHook.test.tsx
index b4562063..df1ab272 100644
--- a/src/data/apiHook.test.tsx
+++ b/src/data/apiHook.test.tsx
@@ -33,7 +33,8 @@ const mockCourseData = {
permissions: {
admin: true,
dataResearcher: false,
- }
+ },
+ gradebookUrl: 'http://example.com/gradebook',
};
const createWrapper = () => {
diff --git a/src/grading/GradingPage.test.tsx b/src/grading/GradingPage.test.tsx
new file mode 100644
index 00000000..37980e9c
--- /dev/null
+++ b/src/grading/GradingPage.test.tsx
@@ -0,0 +1,135 @@
+import { screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { renderWithIntl } from '@src/testUtils';
+import GradingPage from '@src/grading/GradingPage';
+import messages from '@src/grading/messages';
+
+// Mock child components, each component should have its own test suite
+jest.mock('@src/grading/components/GradingLearnerContent', () => {
+ return function MockGradingLearnerContent({ toolType }: { toolType: string }) {
+ return Grading Content for: {toolType}
;
+ };
+});
+
+jest.mock('@src/grading/components/GradingActionRow', () => {
+ return function MockGradingActionRow() {
+ return Grading Action Row
;
+ };
+});
+
+jest.mock('@src/components/PendingTasks', () => {
+ return {
+ PendingTasks: function MockPendingTasks() {
+ return Pending Tasks
;
+ }
+ };
+});
+
+describe('GradingPage', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('renders the page title correctly', () => {
+ renderWithIntl();
+ expect(screen.getByText(messages.pageTitle.defaultMessage)).toBeInTheDocument();
+ });
+
+ it('renders all child components', () => {
+ renderWithIntl();
+ expect(screen.getByText('Grading Action Row')).toBeInTheDocument();
+ expect(screen.getByText('Grading Content for: single')).toBeInTheDocument();
+ expect(screen.getByText('Pending Tasks')).toBeInTheDocument();
+ });
+
+ it('renders both button options with correct labels', () => {
+ renderWithIntl();
+ expect(screen.getByRole('button', { name: messages.singleLearner.defaultMessage })).toBeInTheDocument();
+ expect(screen.getByRole('button', { name: messages.allLearners.defaultMessage })).toBeInTheDocument();
+ });
+
+ it('defaults to single learner tool being selected', () => {
+ renderWithIntl();
+ const singleLearnerButton = screen.getByRole('button', { name: messages.singleLearner.defaultMessage });
+ const allLearnersButton = screen.getByRole('button', { name: messages.allLearners.defaultMessage });
+
+ // Single learner should have primary variant (selected state)
+ expect(singleLearnerButton).toHaveClass('btn-primary');
+ expect(allLearnersButton).toHaveClass('btn-outline-primary');
+
+ // GradingLearnerContent should receive 'single' as initial toolType
+ expect(screen.getByText('Grading Content for: single')).toBeInTheDocument();
+ });
+
+ it('switches to All Learners when All Learners button is clicked', async () => {
+ renderWithIntl();
+ const user = userEvent.setup();
+
+ const allLearnersButton = screen.getByRole('button', { name: messages.allLearners.defaultMessage });
+
+ await user.click(allLearnersButton);
+
+ // All learners should now be selected
+ expect(allLearnersButton).toHaveClass('btn-primary');
+ expect(screen.getByRole('button', { name: messages.singleLearner.defaultMessage })).toHaveClass('btn-outline-primary');
+
+ // GradingLearnerContent should receive 'all' as toolType
+ expect(screen.getByText('Grading Content for: all')).toBeInTheDocument();
+ });
+
+ it('switches back to Single Learner when Single Learner button is clicked', async () => {
+ renderWithIntl();
+ const user = userEvent.setup();
+
+ const singleLearnerButton = screen.getByRole('button', { name: messages.singleLearner.defaultMessage });
+ const allLearnersButton = screen.getByRole('button', { name: messages.allLearners.defaultMessage });
+
+ // First switch to all learners
+ await user.click(allLearnersButton);
+ expect(screen.getByText('Grading Content for: all')).toBeInTheDocument();
+
+ // Then switch back to single learner
+ await user.click(singleLearnerButton);
+
+ // Single learner should be selected again
+ expect(singleLearnerButton).toHaveClass('btn-primary');
+ expect(allLearnersButton).toHaveClass('btn-outline-primary');
+ expect(screen.getByText('Grading Content for: single')).toBeInTheDocument();
+ });
+
+ it('maintains correct button states during multiple interactions', async () => {
+ renderWithIntl();
+ const user = userEvent.setup();
+
+ const singleLearnerButton = screen.getByRole('button', { name: messages.singleLearner.defaultMessage });
+ const allLearnersButton = screen.getByRole('button', { name: messages.allLearners.defaultMessage });
+
+ // Initial state - single learner selected
+ expect(singleLearnerButton).toHaveClass('btn-primary');
+ expect(allLearnersButton).toHaveClass('btn-outline-primary');
+
+ // Click all learners multiple times - should remain selected
+ await user.click(allLearnersButton);
+ await user.click(allLearnersButton);
+
+ expect(allLearnersButton).toHaveClass('btn-primary');
+ expect(singleLearnerButton).toHaveClass('btn-outline-primary');
+ expect(screen.getByText('Grading Content for: all')).toBeInTheDocument();
+
+ // Click single learner multiple times - should remain selected
+ await user.click(singleLearnerButton);
+ await user.click(singleLearnerButton);
+
+ expect(singleLearnerButton).toHaveClass('btn-primary');
+ expect(allLearnersButton).toHaveClass('btn-outline-primary');
+ expect(screen.getByText('Grading Content for: single')).toBeInTheDocument();
+ });
+
+ it('passes correct toolType prop to GradingLearnerContent component', () => {
+ renderWithIntl();
+
+ // Initially should pass 'single'
+ expect(screen.getByText('Grading Content for: single')).toBeInTheDocument();
+ expect(screen.queryByText('Grading Content for: all')).not.toBeInTheDocument();
+ });
+});
diff --git a/src/grading/GradingPage.tsx b/src/grading/GradingPage.tsx
index e00ff9bf..289707e8 100644
--- a/src/grading/GradingPage.tsx
+++ b/src/grading/GradingPage.tsx
@@ -1,8 +1,41 @@
+import { useState } from 'react';
+import { useIntl } from '@openedx/frontend-base';
+import { Button, ButtonGroup, Card } from '@openedx/paragon';
+import { PendingTasks } from '@src/components/PendingTasks';
+import GradingActionRow from '@src/grading/components/GradingActionRow';
+import GradingLearnerContent from '@src/grading/components/GradingLearnerContent';
+import messages from '@src/grading/messages';
+import { GradingToolsType } from '@src/grading/types';
+
const GradingPage = () => {
+ const intl = useIntl();
+ const [selectedTools, setSelectedTools] = useState('single');
+
return (
-
-
Grading
-
+ <>
+
+
{intl.formatMessage(messages.pageTitle)}
+
+
+
+
+
+
+
+
+
+
+ >
);
};
diff --git a/src/grading/components/GradingActionRow.test.tsx b/src/grading/components/GradingActionRow.test.tsx
new file mode 100644
index 00000000..0226ec8c
--- /dev/null
+++ b/src/grading/components/GradingActionRow.test.tsx
@@ -0,0 +1,58 @@
+import { screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { useCourseInfo } from '@src/data/apiHook';
+import GradingActionRow from '@src/grading/components/GradingActionRow';
+import { useGradingConfiguration } from '@src/grading/data/apiHook';
+import messages from '@src/grading/messages';
+import { renderWithIntl } from '@src/testUtils';
+
+jest.mock('react-router-dom', () => ({
+ ...jest.requireActual('react-router-dom'),
+ useParams: () => ({
+ courseId: 'course-v1:edX+DemoX+Demo_Course',
+ }),
+}));
+
+jest.mock('@src/data/apiHook', () => ({
+ useCourseInfo: jest.fn(),
+}));
+
+jest.mock('@src/grading/data/apiHook', () => ({
+ useGradingConfiguration: jest.fn(),
+}));
+
+describe('GradingActionRow', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ (useCourseInfo as jest.Mock).mockReturnValue({ data: { gradebookUrl: 'https://example.com/gradebook', studioGradingUrl: 'https://example.com/studio' } });
+ // TODO: Update this mock to use similar structure when API is ready, currently just returning random text to ensure component renders without error
+ (useGradingConfiguration as jest.Mock).mockReturnValue({ data: 'Some random text' });
+ });
+
+ it('renders ActionRow with gradebook and configuration buttons', () => {
+ renderWithIntl();
+ expect(screen.getByRole('link', { name: messages.viewGradebook.defaultMessage })).toBeInTheDocument();
+ expect(screen.getByRole('button', { name: messages.configurationAlt.defaultMessage })).toBeInTheDocument();
+ });
+
+ it('opens configuration menu when configuration button is clicked', async () => {
+ renderWithIntl();
+ const user = userEvent.setup();
+ await user.click(screen.getByRole('button', { name: messages.configurationAlt.defaultMessage }));
+ expect(screen.getByText('View Grading Configuration')).toBeInTheDocument();
+ expect(screen.getByText('View Course Grading Settings')).toBeInTheDocument();
+ });
+
+ it('opens and closes GradingConfigurationModal when menu item is clicked', async () => {
+ renderWithIntl();
+ const user = userEvent.setup();
+ await user.click(screen.getByRole('button', { name: messages.configurationAlt.defaultMessage }));
+ const gradingConfigButton = screen.getByText('View Grading Configuration');
+ await user.click(gradingConfigButton);
+ expect(screen.getByRole('dialog', { name: messages.gradingConfiguration.defaultMessage })).toBeInTheDocument();
+
+ // Close modal
+ await user.click(screen.getAllByRole('button', { name: messages.close.defaultMessage })[0]);
+ expect(screen.queryByRole('dialog', { name: messages.gradingConfiguration.defaultMessage })).not.toBeInTheDocument();
+ });
+});
diff --git a/src/grading/components/GradingActionRow.tsx b/src/grading/components/GradingActionRow.tsx
new file mode 100644
index 00000000..88592225
--- /dev/null
+++ b/src/grading/components/GradingActionRow.tsx
@@ -0,0 +1,54 @@
+import { useState } from 'react';
+import { useParams } from 'react-router-dom';
+import { useIntl } from '@openedx/frontend-base';
+import { useToggle, ActionRow, Button, IconButton, ModalPopup, Menu, MenuItem } from '@openedx/paragon';
+import { TrendingUp, MoreVert, OpenInNew } from '@openedx/paragon/icons';
+import { useCourseInfo } from '@src/data/apiHook';
+import GradingConfigurationModal from '@src/grading/components/GradingConfigurationModal';
+import messages from '@src/grading/messages';
+
+const GradingActionRow = () => {
+ const { courseId = '' } = useParams<{ courseId: string }>();
+ const intl = useIntl();
+ const { data = { gradebookUrl: '', studioGradingUrl: '' } } = useCourseInfo(courseId);
+ const [configurationMenuTarget, setConfigurationMenuTarget] = useState(null);
+ const [isOpenMenu, openMenu, closeMenu] = useToggle(false);
+ const [isOpenConfigModal, openConfigModal, closeConfigModal] = useToggle(false);
+
+ const handleConfigurationMenuClick = (event: React.MouseEvent) => {
+ setConfigurationMenuTarget(event?.currentTarget);
+ openMenu();
+ };
+
+ const handleConfigModalOpen = () => {
+ openConfigModal();
+ closeMenu();
+ };
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+ >
+ );
+};
+
+export default GradingActionRow;
diff --git a/src/grading/components/GradingConfigurationModal.test.tsx b/src/grading/components/GradingConfigurationModal.test.tsx
new file mode 100644
index 00000000..6679c19b
--- /dev/null
+++ b/src/grading/components/GradingConfigurationModal.test.tsx
@@ -0,0 +1,55 @@
+import { screen } from '@testing-library/react';
+import { renderWithIntl } from '@src/testUtils';
+import { useGradingConfiguration } from '@src/grading/data/apiHook';
+import GradingConfigurationModal from '@src/grading/components/GradingConfigurationModal';
+import messages from '@src/grading/messages';
+
+jest.mock('react-router-dom', () => ({
+ ...jest.requireActual('react-router-dom'),
+ useParams: () => ({
+ courseId: 'course-v1:edX+DemoX+Demo_Course',
+ }),
+}));
+
+jest.mock('../data/apiHook', () => ({
+ useGradingConfiguration: jest.fn(),
+}));
+
+describe('GradingConfigurationModal', () => {
+ const mockOnClose = jest.fn();
+
+ beforeEach(() => {
+ (useGradingConfiguration as jest.Mock).mockReturnValue({ data: null });
+ });
+
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('renders modal when isOpen is true', () => {
+ renderWithIntl();
+ expect(screen.getByRole('dialog')).toBeInTheDocument();
+ });
+
+ it('does not render modal when isOpen is false', () => {
+ renderWithIntl();
+ expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
+ });
+
+ it('displays grading configuration data when available', () => {
+ (useGradingConfiguration as jest.Mock).mockReturnValue({ data: 'Test grading configuration' });
+ renderWithIntl();
+ expect(screen.getByText('Test grading configuration')).toBeInTheDocument();
+ });
+
+ it('displays no grading configuration message when data is null', () => {
+ (useGradingConfiguration as jest.Mock).mockReturnValue({ data: null });
+ renderWithIntl();
+ expect(screen.getByText(messages.noGradingConfiguration.defaultMessage)).toBeInTheDocument();
+ });
+
+ it('calls useGradingConfiguration with courseId from params', () => {
+ renderWithIntl();
+ expect(useGradingConfiguration).toHaveBeenCalledWith('course-v1:edX+DemoX+Demo_Course');
+ });
+});
diff --git a/src/grading/components/GradingConfigurationModal.tsx b/src/grading/components/GradingConfigurationModal.tsx
new file mode 100644
index 00000000..8abe0985
--- /dev/null
+++ b/src/grading/components/GradingConfigurationModal.tsx
@@ -0,0 +1,32 @@
+import { useParams } from 'react-router-dom';
+import { Button, ModalDialog } from '@openedx/paragon';
+import { useIntl } from '@openedx/frontend-base';
+import messages from '@src/grading/messages';
+import { useGradingConfiguration } from '@src/grading/data/apiHook';
+
+interface GradingConfigurationModalProps {
+ isOpen: boolean,
+ onClose: () => void,
+}
+
+const GradingConfigurationModal = ({ isOpen, onClose }: GradingConfigurationModalProps) => {
+ const intl = useIntl();
+ const { courseId = '' } = useParams<{ courseId: string }>();
+ const { data = null } = useGradingConfiguration(courseId);
+
+ return (
+
+
+ {intl.formatMessage(messages.gradingConfiguration)}
+
+
+ {data ?? intl.formatMessage(messages.noGradingConfiguration)}
+
+
+
+
+
+ );
+};
+
+export default GradingConfigurationModal;
diff --git a/src/grading/components/GradingLearnerContent.tsx b/src/grading/components/GradingLearnerContent.tsx
new file mode 100644
index 00000000..5b9e518e
--- /dev/null
+++ b/src/grading/components/GradingLearnerContent.tsx
@@ -0,0 +1,27 @@
+import { useIntl } from '@openedx/frontend-base';
+import SpecifyProblem from '@src/components/SpecifyProblem';
+import messages from '@src/grading/messages';
+import { GradingToolsType } from '@src/grading/types';
+
+interface GradingLearnerContentProps {
+ toolType: GradingToolsType,
+}
+
+const GradingLearnerContent = ({ toolType }: GradingLearnerContentProps) => {
+ const intl = useIntl();
+
+ return (
+ <>
+
+ {
+ toolType === 'single'
+ ? intl.formatMessage(messages.descriptionSingleLearner)
+ : intl.formatMessage(messages.descriptionAllLearners)
+ }
+
+
+ >
+ );
+};
+
+export default GradingLearnerContent;
diff --git a/src/grading/data/api.test.ts b/src/grading/data/api.test.ts
new file mode 100644
index 00000000..e1a1eab3
--- /dev/null
+++ b/src/grading/data/api.test.ts
@@ -0,0 +1,48 @@
+import { camelCaseObject, getAppConfig, getAuthenticatedHttpClient } from '@openedx/frontend-base';
+import { getGradingConfiguration } from '@src/grading/data/api';
+
+jest.mock('@openedx/frontend-base', () => ({
+ ...jest.requireActual('@openedx/frontend-base'),
+ camelCaseObject: jest.fn((obj) => obj),
+ getAppConfig: jest.fn(),
+ getAuthenticatedHttpClient: jest.fn(),
+}));
+
+const mockGetAppConfig = getAppConfig as jest.MockedFunction;
+const mockGetAuthenticatedHttpClient = getAuthenticatedHttpClient as jest.MockedFunction;
+const mockCamelCaseObject = camelCaseObject as jest.MockedFunction;
+
+describe('getGradingConfiguration', () => {
+ const mockHttpClient = {
+ get: jest.fn(),
+ };
+ const mockConfigData = { grading_policy: 'test_policy' };
+ const mockCamelCaseData = { gradingPolicy: 'test_policy' };
+
+ beforeEach(() => {
+ mockGetAppConfig.mockReturnValue({ LMS_BASE_URL: 'https://test-lms.com' });
+ mockGetAuthenticatedHttpClient.mockReturnValue(mockHttpClient as any);
+ mockCamelCaseObject.mockReturnValue(mockCamelCaseData);
+ mockHttpClient.get.mockResolvedValue({ data: mockConfigData });
+ });
+
+ afterEach(() => {
+ jest.resetAllMocks();
+ });
+
+ it('fetches grading configuration successfully', async () => {
+ const courseId = 'test-course-123';
+ const result = await getGradingConfiguration(courseId);
+ expect(mockGetAppConfig).toHaveBeenCalled();
+ expect(mockGetAuthenticatedHttpClient).toHaveBeenCalled();
+ expect(mockHttpClient.get).toHaveBeenCalledWith('https://test-lms.com/api/instructor/v2/courses/test-course-123/grading-config');
+ expect(mockCamelCaseObject).toHaveBeenCalledWith(mockConfigData);
+ expect(result).toBe(mockCamelCaseData);
+ });
+
+ it('throws error when API call fails', async () => {
+ const error = new Error('Network error');
+ mockHttpClient.get.mockRejectedValue(error);
+ await expect(getGradingConfiguration('test-course')).rejects.toThrow('Network error');
+ });
+});
diff --git a/src/grading/data/api.ts b/src/grading/data/api.ts
new file mode 100644
index 00000000..9b1262f9
--- /dev/null
+++ b/src/grading/data/api.ts
@@ -0,0 +1,8 @@
+import { camelCaseObject, getAuthenticatedHttpClient } from '@openedx/frontend-base';
+import { getApiBaseUrl } from '@src/data/api';
+
+export const getGradingConfiguration = async (courseId: string) => {
+ const { data } = await getAuthenticatedHttpClient()
+ .get(`${getApiBaseUrl()}/api/instructor/v2/courses/${courseId}/grading-config`);
+ return camelCaseObject(data);
+};
diff --git a/src/grading/data/apiHook.test.tsx b/src/grading/data/apiHook.test.tsx
new file mode 100644
index 00000000..5572556a
--- /dev/null
+++ b/src/grading/data/apiHook.test.tsx
@@ -0,0 +1,50 @@
+import { renderHook, waitFor } from '@testing-library/react';
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
+import * as api from '@src/grading/data/api';
+import { useGradingConfiguration } from '@src/grading/data/apiHook';
+
+const createWrapper = () => {
+ const queryClient = new QueryClient({
+ defaultOptions: {
+ queries: { retry: false },
+ mutations: { retry: false },
+ },
+ });
+
+ const Wrapper = ({ children }: { children: React.ReactNode }) => (
+ {children}
+ );
+
+ Wrapper.displayName = 'TestWrapper';
+ return Wrapper;
+};
+
+describe('useGradingConfiguration', () => {
+ const wrapper = createWrapper();
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('returns data when getGradingConfiguration resolves', async () => {
+ const mockData = { gradingPolicy: 'test_policy' };
+ jest.spyOn(api, 'getGradingConfiguration').mockResolvedValueOnce(mockData);
+ const { result } = renderHook(() => useGradingConfiguration('course-v1:abc123'), { wrapper });
+ await waitFor(() => expect(result.current.isSuccess).toBe(true));
+ expect(result.current.data).toEqual(mockData);
+ });
+
+ it('returns error when getGradingConfiguration rejects', async () => {
+ jest.spyOn(api, 'getGradingConfiguration').mockRejectedValueOnce(new Error('Network error'));
+ const { result } = renderHook(() => useGradingConfiguration('course-v1:abc123'), { wrapper });
+ await waitFor(() => expect(result.current.isError).toBe(true));
+ expect(result.current.error).toBeInstanceOf(Error);
+ expect(result?.current?.error?.message).toBe('Network error');
+ });
+
+ it('does not run query if courseId is falsy', () => {
+ const spy = jest.spyOn(api, 'getGradingConfiguration');
+ renderHook(() => useGradingConfiguration(''), { wrapper });
+ expect(spy).not.toHaveBeenCalled();
+ });
+});
diff --git a/src/grading/data/apiHook.ts b/src/grading/data/apiHook.ts
new file mode 100644
index 00000000..0ad850c6
--- /dev/null
+++ b/src/grading/data/apiHook.ts
@@ -0,0 +1,11 @@
+import { useQuery } from '@tanstack/react-query';
+import { getGradingConfiguration } from '@src/grading/data/api';
+import { gradingQueryKeys } from '@src/grading/data/queryKeys';
+
+export const useGradingConfiguration = (courseId: string) => (
+ useQuery({
+ queryKey: gradingQueryKeys.gradingConfiguration(courseId),
+ queryFn: () => getGradingConfiguration(courseId),
+ enabled: !!courseId,
+ })
+);
diff --git a/src/grading/data/queryKeys.ts b/src/grading/data/queryKeys.ts
new file mode 100644
index 00000000..9b4ff53d
--- /dev/null
+++ b/src/grading/data/queryKeys.ts
@@ -0,0 +1,7 @@
+import { appId } from '@src/constants';
+
+export const gradingQueryKeys = {
+ all: [appId, 'grading'] as const,
+ byCourse: (courseId: string) => [...gradingQueryKeys.all, courseId] as const,
+ gradingConfiguration: (courseId: string) => [...gradingQueryKeys.byCourse(courseId), 'gradingConfiguration'] as const,
+};
diff --git a/src/grading/messages.ts b/src/grading/messages.ts
new file mode 100644
index 00000000..279fcd23
--- /dev/null
+++ b/src/grading/messages.ts
@@ -0,0 +1,66 @@
+import { defineMessages } from '@openedx/frontend-base';
+
+const messages = defineMessages({
+ pageTitle: {
+ id: 'instruct.grading.pageTitle',
+ defaultMessage: 'Grading Tools',
+ description: 'Title for the grading page'
+ },
+ configurationAlt: {
+ id: 'instruct.grading.configurationAlt',
+ defaultMessage: 'Grading Configuration and Settings',
+ description: 'Alt text for the configuration icon button'
+ },
+ viewGradebook: {
+ id: 'instruct.grading.viewGradebook',
+ defaultMessage: 'View Gradebook',
+ description: 'Text for the button to view the gradebook'
+ },
+ singleLearner: {
+ id: 'instruct.grading.singleLearner',
+ defaultMessage: 'Single Learner',
+ description: 'Single Learner button label to display corresponding grading tools'
+ },
+ allLearners: {
+ id: 'instruct.grading.allLearners',
+ defaultMessage: 'All Learners',
+ description: 'All learners button label to display corresponding grading tools'
+ },
+ descriptionSingleLearner: {
+ id: 'instruct.grading.descriptionSingleLearner',
+ defaultMessage: 'These grading tools allow for grade review and adjustment for a specific learner on a specific problem.',
+ description: 'Description for single learner grading tools'
+ },
+ descriptionAllLearners: {
+ id: 'instruct.grading.descriptionAllLearners',
+ defaultMessage: 'These grading tools allow for grade review and adjustment all enrolled learners on a specific problem.',
+ description: 'Description for all learners grading tools'
+ },
+ gradingConfiguration: {
+ id: 'instruct.grading.gradingConfiguration',
+ defaultMessage: 'Grading Configuration',
+ description: 'Title for the grading configuration modal'
+ },
+ close: {
+ id: 'instruct.grading.modals.close',
+ defaultMessage: 'Close',
+ description: 'Text for the close button in the grading configuration modal'
+ },
+ viewGradingConfiguration: {
+ id: 'instruct.grading.viewGradingConfiguration',
+ defaultMessage: 'View Grading Configuration',
+ description: 'View grading configuration menu item label'
+ },
+ viewCourseGradingSettings: {
+ id: 'instruct.grading.viewCourseGradingSettings',
+ defaultMessage: 'View Course Grading Settings',
+ description: 'View course grading settings menu item label'
+ },
+ noGradingConfiguration: {
+ id: 'instruct.grading.noGradingConfiguration',
+ defaultMessage: 'No grading configuration found for this course.',
+ description: 'Message to display when there is no grading configuration for the course'
+ }
+});
+
+export default messages;
diff --git a/src/grading/types.ts b/src/grading/types.ts
new file mode 100644
index 00000000..7dd9b2de
--- /dev/null
+++ b/src/grading/types.ts
@@ -0,0 +1 @@
+export type GradingToolsType = 'single' | 'all';