@@ -46,14 +44,14 @@ const CriterionContainer = ({
CriterionContainer.propTypes = {
isGrading: PropTypes.bool.isRequired,
criterion: PropTypes.shape({
- name: PropTypes.string,
- description: PropTypes.string,
+ name: PropTypes.string.isRequired,
+ description: PropTypes.string.isRequired,
options: PropTypes.arrayOf(
PropTypes.shape({
- description: PropTypes.string,
- name: PropTypes.string,
+ name: PropTypes.string.isRequired,
+ description: PropTypes.string.isRequired,
}),
- ),
+ ).isRequired,
}).isRequired,
};
diff --git a/src/components/Rubric/CriterionContainer/index.test.jsx b/src/components/Rubric/CriterionContainer/index.test.jsx
new file mode 100644
index 00000000..ccdc9d78
--- /dev/null
+++ b/src/components/Rubric/CriterionContainer/index.test.jsx
@@ -0,0 +1,49 @@
+import React from 'react';
+import { shallow } from '@edx/react-unit-test-utils';
+
+import CriterionContainer from '.';
+
+jest.mock('./RadioCriterion', () => 'RadioCriterion');
+jest.mock('./CriterionFeedback', () => 'CriterionFeedback');
+jest.mock('./ReviewCriterion', () => 'ReviewCriterion');
+
+describe('
', () => {
+ const props = {
+ isGrading: true,
+ criterion: {
+ name: 'criterion-1',
+ description: 'description-1',
+ options: [
+ {
+ name: 'option-1',
+ description: 'description-1',
+ points: 1,
+ },
+ {
+ name: 'option-2',
+ description: 'description-2',
+ points: 2,
+ },
+ ],
+ },
+ };
+ describe('renders', () => {
+ test('is grading', () => {
+ const wrapper = shallow(
);
+ expect(wrapper.snapshot).toMatchSnapshot();
+
+ expect(wrapper.instance.findByType('RadioCriterion')).toHaveLength(1);
+ expect(wrapper.instance.findByType('ReviewCriterion')).toHaveLength(0);
+ expect(wrapper.instance.findByType('CriterionFeedback')).toHaveLength(1);
+ });
+
+ test('is not grading', () => {
+ const wrapper = shallow(
);
+ expect(wrapper.snapshot).toMatchSnapshot();
+
+ expect(wrapper.instance.findByType('RadioCriterion')).toHaveLength(0);
+ expect(wrapper.instance.findByType('ReviewCriterion')).toHaveLength(1);
+ expect(wrapper.instance.findByType('CriterionFeedback')).toHaveLength(0);
+ });
+ });
+});
diff --git a/src/components/Rubric/RubricFeedback.jsx b/src/components/Rubric/RubricFeedback.jsx
index 6d215df2..c6985a57 100644
--- a/src/components/Rubric/RubricFeedback.jsx
+++ b/src/components/Rubric/RubricFeedback.jsx
@@ -3,34 +3,25 @@ import PropTypes from 'prop-types';
import { Form } from '@edx/paragon';
import { useIntl } from '@edx/frontend-platform/i18n';
-import { StrictDict, useKeyedState } from '@edx/react-unit-test-utils';
-
-import { useRubricConfig } from 'data/services/lms/hooks/selectors';
import InfoPopover from 'components/InfoPopover';
import messages from './messages';
-export const stateKeys = StrictDict({
- value: 'value',
-});
-
/**
*
*/
const RubricFeedback = ({
- isGrading,
+ overallFeedbackPrompt,
+ overallFeedback,
+ overallFeedbackDisabled,
+ overallFeedbackIsInvalid,
+ onOverallFeedbackChange,
}) => {
- const [value, setValue] = useKeyedState('');
const { formatMessage } = useIntl();
- const feedbackPrompt = useRubricConfig().feedbackConfig.defaultText;
-
- const onChange = (event) => {
- setValue(event.target.value);
- };
const inputLabel = formatMessage(
- isGrading ? messages.addComments : messages.comments,
+ !overallFeedbackDisabled ? messages.addComments : messages.comments,
);
return (
@@ -40,32 +31,38 @@ const RubricFeedback = ({
{formatMessage(messages.overallComments)}
- {feedbackPrompt}
+ {overallFeedbackPrompt}
- {
- /*
- {isInvalid && (
-
-
-
- )}
- */
- }
+ {overallFeedbackIsInvalid && (
+
+ {formatMessage(messages.overallFeedbackError)}
+
+ )}
);
};
+RubricFeedback.defaultProps = {
+ overallFeedback: '',
+ overallFeedbackDisabled: false,
+ overallFeedbackIsInvalid: false,
+};
+
RubricFeedback.propTypes = {
- isGrading: PropTypes.bool.isRequired,
+ overallFeedbackPrompt: PropTypes.string.isRequired,
+ overallFeedback: PropTypes.string,
+ overallFeedbackDisabled: PropTypes.bool,
+ onOverallFeedbackChange: PropTypes.func.isRequired,
+ overallFeedbackIsInvalid: PropTypes.bool,
};
export default RubricFeedback;
diff --git a/src/components/Rubric/RubricFeedback.test.jsx b/src/components/Rubric/RubricFeedback.test.jsx
new file mode 100644
index 00000000..e1e491e1
--- /dev/null
+++ b/src/components/Rubric/RubricFeedback.test.jsx
@@ -0,0 +1,42 @@
+import React from 'react';
+import { shallow } from '@edx/react-unit-test-utils';
+
+import RubricFeedback from './RubricFeedback';
+import messages from './messages';
+
+describe('
', () => {
+ const props = {
+ overallFeedbackPrompt: 'overallFeedbackPrompt',
+ overallFeedback: 'overallFeedback',
+ overallFeedbackDisabled: false,
+ overallFeedbackIsInvalid: false,
+ onOverallFeedbackChange: jest.fn().mockName('onOverallFeedbackChange'),
+ };
+
+ describe('renders', () => {
+ test('overall feedback is enabled', () => {
+ const wrapper = shallow(
);
+ expect(wrapper.snapshot).toMatchSnapshot();
+
+ expect(wrapper.instance.findByType('Form.Control.Feedback').length).toBe(0);
+ expect(wrapper.instance.findByType('Form.Control')[0].props.disabled).toBe(false);
+ expect(wrapper.instance.findByType('Form.Control')[0].props.floatingLabel).toBe(messages.addComments.defaultMessage);
+ });
+
+ test('overall feedback is disabled', () => {
+ const wrapper = shallow(
);
+ expect(wrapper.snapshot).toMatchSnapshot();
+
+ expect(wrapper.instance.findByType('Form.Control.Feedback').length).toBe(0);
+ expect(wrapper.instance.findByType('Form.Control')[0].props.disabled).toBe(true);
+ expect(wrapper.instance.findByType('Form.Control')[0].props.floatingLabel).toBe(messages.comments.defaultMessage);
+ });
+
+ test('overall feedback is invalid', () => {
+ const wrapper = shallow(
);
+ expect(wrapper.snapshot).toMatchSnapshot();
+
+ expect(wrapper.instance.findByType('Form.Control.Feedback').length).toBe(1);
+ });
+ });
+});
diff --git a/src/components/Rubric/__snapshots__/RubricFeedback.test.jsx.snap b/src/components/Rubric/__snapshots__/RubricFeedback.test.jsx.snap
new file mode 100644
index 00000000..a5a655c2
--- /dev/null
+++ b/src/components/Rubric/__snapshots__/RubricFeedback.test.jsx.snap
@@ -0,0 +1,94 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`
renders overall feedback is disabled 1`] = `
+
+
+
+ Overall comments
+
+
+
+ overallFeedbackPrompt
+
+
+
+
+
+`;
+
+exports[`
renders overall feedback is enabled 1`] = `
+
+
+
+ Overall comments
+
+
+
+ overallFeedbackPrompt
+
+
+
+
+
+`;
+
+exports[`
renders overall feedback is invalid 1`] = `
+
+
+
+ Overall comments
+
+
+
+ overallFeedbackPrompt
+
+
+
+
+
+ The overall feedback is required
+
+
+`;
diff --git a/src/components/Rubric/__snapshots__/index.test.jsx.snap b/src/components/Rubric/__snapshots__/index.test.jsx.snap
new file mode 100644
index 00000000..ad94dd11
--- /dev/null
+++ b/src/components/Rubric/__snapshots__/index.test.jsx.snap
@@ -0,0 +1,152 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`
renders is grading 1`] = `
+
+
+
+ Rubric
+
+
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`
renders is not grading, no submit button or feedback get render 1`] = `
+
+
+
+ Rubric
+
+
+
+
+
+
+
+`;
diff --git a/src/components/Rubric/hooks.js b/src/components/Rubric/hooks.js
deleted file mode 100644
index 2d6b074e..00000000
--- a/src/components/Rubric/hooks.js
+++ /dev/null
@@ -1,32 +0,0 @@
-import { StrictDict } from 'utils';
-import { useRubricConfig } from 'data/services/lms/hooks/selectors';
-
-export const ButtonStates = StrictDict({
- default: 'default',
- pending: 'pending',
- complete: 'complete',
- error: 'error',
-});
-
-export const useRubricData = ({
- isGrading,
-}) => {
- const criteria = useRubricConfig().criteria.map(
- (_, index) => ({
- isGrading,
- key: index,
- orderNum: index,
- }),
- );
- const submitButtonState = ButtonStates.default;
-
- return {
- criteria,
- showFooter: isGrading,
- buttonProps: {
- onClick: () => ({}),
- state: submitButtonState,
- disabledStates: [ButtonStates.pending, ButtonStates.complete],
- },
- };
-};
diff --git a/src/components/Rubric/hooks.test.ts b/src/components/Rubric/hooks.test.ts
new file mode 100644
index 00000000..e72e1e1d
--- /dev/null
+++ b/src/components/Rubric/hooks.test.ts
@@ -0,0 +1,134 @@
+import {
+ usePageData,
+ useRubricConfig,
+} from 'data/services/lms/hooks/selectors';
+import { mockUseKeyedState } from '@edx/react-unit-test-utils';
+import { submitRubric } from 'data/services/lms/hooks/actions';
+import { useRubricData, stateKeys } from './hooks';
+import { RubricData } from 'data/services/lms/types';
+
+import { when } from 'jest-when';
+
+jest.mock('data/services/lms/hooks/selectors', () => ({
+ usePageData: jest.fn(),
+ useRubricConfig: jest.fn(),
+}));
+
+jest.mock('data/services/lms/hooks/actions', () => ({
+ submitRubric: jest.fn(),
+}));
+
+const state = mockUseKeyedState(stateKeys);
+
+describe('useRubricData', () => {
+ const mutateFn = jest.fn();
+
+ const mockRubricData: RubricData = {
+ optionsSelected: {
+ 'criterion-1': 'option-1',
+ 'criterion-2': 'option-2',
+ },
+ criterionFeedback: {
+ 'criterion-1': 'feedback-1',
+ 'criterion-2': 'feedback-2',
+ },
+ overallFeedback: 'overall-feedback',
+ };
+
+ when(usePageData).mockReturnValue({
+ rubric: mockRubricData,
+ });
+
+ when(useRubricConfig).mockReturnValue({
+ criteria: [
+ {
+ name: 'criterion-1',
+ options: [
+ { label: 'Option 1', value: 'option-1' },
+ { label: 'Option 2', value: 'option-2' },
+ ],
+ },
+ {
+ name: 'criterion-2',
+ options: [
+ { label: 'Option 1', value: 'option-1' },
+ { label: 'Option 2', value: 'option-2' },
+ ],
+ },
+ ],
+ feedbackConfig: {
+ enabled: true,
+ },
+ } as any);
+
+ when(submitRubric).mockReturnValue({
+ mutate: mutateFn,
+ } as any);
+
+ describe('state keys', () => {
+ beforeEach(() => {
+ state.mock();
+ });
+ afterEach(() => {
+ state.resetVals();
+ });
+
+ it('initializes state values from page data', () => {
+ useRubricData({ isGrading: true });
+ state.expectInitializedWith(stateKeys.rubric, mockRubricData);
+ state.expectInitializedWith(
+ stateKeys.overallFeedback,
+ mockRubricData.overallFeedback
+ );
+ });
+ it('returns the correct getter/setter for state', () => {
+ const out = useRubricData({ isGrading: true });
+ expect(out.rubricData).toEqual(mockRubricData);
+
+ out.setRubricData('foo' as any);
+ expect(state.values[stateKeys.rubric]).toEqual('foo');
+ expect(out.overallFeedback).toEqual(mockRubricData.overallFeedback);
+ out.onOverallFeedbackChange({
+ target: {
+ value: 'bar',
+ },
+ } as any);
+ expect(state.values[stateKeys.overallFeedback]).toEqual('bar');
+ });
+ });
+
+ it('should return the correct data', () => {
+ const { rubricData, criteria } = useRubricData({ isGrading: true });
+
+ expect(rubricData).toEqual(mockRubricData);
+
+ expect(criteria).toEqual([
+ {
+ name: 'criterion-1',
+ options: [
+ { label: 'Option 1', value: 'option-1' },
+ { label: 'Option 2', value: 'option-2' },
+ ],
+ optionsValue: 'option-1',
+ optionsIsInvalid: false,
+ optionsOnChange: expect.any(Function),
+ feedbackValue: 'feedback-1',
+ feedbackIsInvalid: false,
+ feedbackOnChange: expect.any(Function),
+ },
+ {
+ name: 'criterion-2',
+ options: [
+ { label: 'Option 1', value: 'option-1' },
+ { label: 'Option 2', value: 'option-2' },
+ ],
+ optionsValue: 'option-2',
+ optionsIsInvalid: false,
+ optionsOnChange: expect.any(Function),
+ feedbackValue: 'feedback-2',
+ feedbackIsInvalid: false,
+ feedbackOnChange: expect.any(Function),
+ },
+ ]);
+ });
+});
diff --git a/src/components/Rubric/hooks.ts b/src/components/Rubric/hooks.ts
new file mode 100644
index 00000000..e578f18b
--- /dev/null
+++ b/src/components/Rubric/hooks.ts
@@ -0,0 +1,73 @@
+import { useKeyedState, StrictDict } from '@edx/react-unit-test-utils';
+import { usePageData, useRubricConfig } from 'data/services/lms/hooks/selectors';
+import { submitRubric } from 'data/services/lms/hooks/actions';
+import { RubricHookData } from './types';
+
+export const stateKeys = StrictDict({
+ rubric: 'rubric',
+ overallFeedback: 'overallFeedback',
+});
+
+export const useRubricData = ({ isGrading }): RubricHookData => {
+ const data = usePageData();
+ const { criteria, feedbackConfig } = useRubricConfig();
+
+ const [rubricData, setRubricData] = useKeyedState(stateKeys.rubric, data.rubric);
+ const [overallFeedback, setOverallFeedback] = useKeyedState(
+ stateKeys.overallFeedback, data.rubric.overallFeedback
+ );
+ const submitRubricMutation = submitRubric();
+
+ const onOverallFeedbackChange = (e: React.ChangeEvent
) =>
+ setOverallFeedback(e.target.value);
+
+ const onSubmit = () => {
+ submitRubricMutation.mutate({
+ overallFeedback,
+ optionsSelected: rubricData.optionsSelected,
+ criterionFeedback: rubricData.criterionFeedback,
+ } as any);
+ };
+
+ return {
+ rubricData,
+ setRubricData,
+ criteria: criteria.map((criterion) => ({
+ ...criterion,
+ optionsValue: rubricData.optionsSelected[criterion.name],
+ optionsIsInvalid: false,
+ optionsOnChange: (e: React.ChangeEvent) => {
+ setRubricData({
+ ...rubricData,
+ optionsSelected: {
+ ...rubricData.optionsSelected,
+ [criterion.name]: e.target.value,
+ },
+ });
+ },
+
+ feedbackValue: rubricData.criterionFeedback[criterion.name],
+ feedbackIsInvalid: false,
+ feedbackOnChange: (e: React.ChangeEvent) => {
+ setRubricData({
+ ...rubricData,
+ criterionFeedback: {
+ ...rubricData.criterionFeedback,
+ [criterion.name]: e.target.value,
+ },
+ });
+ },
+ })),
+ onSubmit,
+ submitStatus: submitRubricMutation.status,
+
+ // overall feedback
+ overallFeedback,
+ onOverallFeedbackChange,
+ overallFeedbackDisabled: !isGrading,
+ overallFeedbackIsInvalid: false,
+ overallFeedbackPrompt: feedbackConfig.defaultText,
+ };
+};
+
+export default useRubricData;
diff --git a/src/components/Rubric/index.jsx b/src/components/Rubric/index.jsx
index 56f9551f..7d6630db 100644
--- a/src/components/Rubric/index.jsx
+++ b/src/components/Rubric/index.jsx
@@ -5,49 +5,70 @@ import { Card, StatefulButton } from '@edx/paragon';
import { useIntl } from '@edx/frontend-platform/i18n';
import { StrictDict } from '@edx/react-unit-test-utils';
-import { useRubricConfig } from 'data/services/lms/hooks/selectors';
-
import CriterionContainer from './CriterionContainer';
import RubricFeedback from './RubricFeedback';
+import { useRubricData } from './hooks';
import messages from './messages';
import './Rubric.scss';
export const ButtonStates = StrictDict({
- default: 'default',
- pending: 'pending',
- complete: 'complete',
+ idle: 'idle',
+ loading: 'loading',
error: 'error',
+ success: 'success',
});
/**
*
*/
export const Rubric = ({ isGrading }) => {
- const { criteria } = useRubricConfig();
+ const {
+ criteria,
+ onSubmit,
+ overallFeedbackPrompt,
+ overallFeedback,
+ overallFeedbackDisabled,
+ onOverallFeedbackChange,
+ submitStatus,
+ } = useRubricData({
+ isGrading,
+ });
+
const { formatMessage } = useIntl();
return (
{formatMessage(messages.rubric)}
- {criteria.map(criterion => (
-
+ {criteria.map((criterion) => (
+
))}
- {isGrading && }
+ {isGrading && (
+
+ )}
{isGrading && (
({})}
- state="default"
- disabledStates={['pending', 'complete']}
+ onClick={onSubmit}
+ state={submitStatus}
+ disabledStates={['loading', 'success']}
labels={{
- [ButtonStates.default]: formatMessage(messages.submitGrade),
- [ButtonStates.pending]: formatMessage(messages.submittingGrade),
- [ButtonStates.complete]: formatMessage(messages.gradeSubmitted),
+ [ButtonStates.idle]: formatMessage(messages.submitGrade),
+ [ButtonStates.loading]: formatMessage(messages.submittingGrade),
+ [ButtonStates.success]: formatMessage(messages.gradeSubmitted),
}}
/>
@@ -55,6 +76,7 @@ export const Rubric = ({ isGrading }) => {
);
};
+
Rubric.propTypes = {
isGrading: PropTypes.bool.isRequired,
};
diff --git a/src/components/Rubric/index.test.jsx b/src/components/Rubric/index.test.jsx
new file mode 100644
index 00000000..259515ad
--- /dev/null
+++ b/src/components/Rubric/index.test.jsx
@@ -0,0 +1,87 @@
+import React from 'react';
+import { shallow } from '@edx/react-unit-test-utils';
+import { when } from 'jest-when';
+
+import { useRubricData } from './hooks';
+import { Rubric } from '.';
+
+jest.mock('./RubricFeedback', () => 'RubricFeedback');
+jest.mock('./CriterionContainer', () => 'CriterionContainer');
+
+jest.mock('./hooks', () => ({
+ useRubricData: jest.fn(),
+}));
+
+describe('', () => {
+ const mockRubricDataResponse = {
+ criteria: [
+ {
+ name: 'criterion-1',
+ optionsValue: 'option-1',
+ optionsIsInvalid: false,
+ optionsOnChange: jest.fn().mockName('optionsOnChange'),
+ options: [
+ {
+ name: 'option-1',
+ points: 1,
+ },
+ {
+ name: 'option-2',
+ points: 2,
+ },
+ ],
+ },
+ {
+ name: 'criterion-2',
+ optionsValue: 'option-1',
+ optionsIsInvalid: false,
+ optionsOnChange: jest.fn().mockName('optionsOnChange'),
+ options: [
+ {
+ name: 'option-1',
+ points: 1,
+ },
+ {
+ name: 'option-2',
+ points: 2,
+ },
+ ],
+ },
+ ],
+ onSubmit: jest.fn().mockName('onSubmit'),
+ overallFeedbackPrompt: 'overallFeedbackPrompt',
+ overallFeedback: 'overallFeedback',
+ overallFeedbackDisabled: true,
+ onOverallFeedbackChange: jest.fn().mockName('onOverallFeedbackChange'),
+ submitStatus: 'idle',
+ };
+
+ when(useRubricData).mockReturnValue(mockRubricDataResponse);
+
+ describe('renders', () => {
+ test('is grading', () => {
+ const wrapper = shallow();
+ expect(wrapper.snapshot).toMatchSnapshot();
+ });
+ test('is not grading, no submit button or feedback get render', () => {
+ const wrapper = shallow();
+ expect(wrapper.snapshot).toMatchSnapshot();
+
+ expect(wrapper.instance.findByType('RubricFeedback').length).toBe(0);
+ expect(wrapper.instance.findByType('StatefulButton').length).toBe(0);
+ });
+ });
+
+ describe('behavior', () => {
+ const wrapper = shallow();
+ it('has CriterionContainer equal to the number of criteria', () => {
+ expect(wrapper.instance.findByType('CriterionContainer').length).toBe(mockRubricDataResponse.criteria.length);
+ });
+
+ test('StatefulButton onClick calls onSubmit', () => {
+ expect(mockRubricDataResponse.onSubmit).not.toHaveBeenCalled();
+ wrapper.instance.findByType('StatefulButton')[0].props.onClick();
+ expect(mockRubricDataResponse.onSubmit).toHaveBeenCalled();
+ });
+ });
+});
diff --git a/src/components/Rubric/types.ts b/src/components/Rubric/types.ts
new file mode 100644
index 00000000..48bf3aa5
--- /dev/null
+++ b/src/components/Rubric/types.ts
@@ -0,0 +1,26 @@
+import { CriterionConfig, MutationStatus, RubricData } from "data/services/lms/types";
+
+export type Criterion = {
+ optionsValue: string | null;
+ optionsIsInvalid: boolean;
+ optionsOnChange: (e: React.ChangeEvent) => void;
+
+ feedbackValue: string | null;
+ feedbackIsInvalid: boolean;
+ feedbackOnChange: (e: React.ChangeEvent) => void;
+} & CriterionConfig;
+
+export type RubricHookData = {
+ rubricData: RubricData;
+ setRubricData: (data: RubricData) => void;
+ criteria: Criterion[];
+ onSubmit: () => void;
+ submitStatus: MutationStatus;
+
+ // overall feedback
+ overallFeedback: string;
+ onOverallFeedbackChange: (e: React.ChangeEvent) => void;
+ overallFeedbackDisabled: boolean;
+ overallFeedbackIsInvalid: boolean;
+ overallFeedbackPrompt: string;
+};
\ No newline at end of file
diff --git a/src/data/services/lms/fakeData/oraConfig.js b/src/data/services/lms/fakeData/oraConfig.js
index 18a30877..8c6d4c8a 100644
--- a/src/data/services/lms/fakeData/oraConfig.js
+++ b/src/data/services/lms/fakeData/oraConfig.js
@@ -52,22 +52,22 @@ const rubricConfig = {
criteria: [
genCriterion({
index: 0,
- config: { feedback_enabled: true, feedback_required: true },
+ config: { feedback_enabled: true, feedback_required: 'required' },
optionPointsArray: [0, 2, 5, 7],
}),
genCriterion({
index: 1,
- config: { feedback_enabled: true, feedback_required: true },
+ config: { feedback_enabled: true, feedback_required: 'disabled' },
optionPointsArray: [0, 2, 5, 7],
}),
genCriterion({
index: 2,
- config: { feedback_enabled: true, feedback_required: true },
+ config: { feedback_enabled: true, feedback_required: 'optional' },
optionPointsArray: [0, 2, 5, 7],
}),
genCriterion({
index: 3,
- config: { feedback_enabled: true, feedback_required: true },
+ config: { feedback_enabled: true, feedback_required: 'optional' },
optionPointsArray: [0, 2, 5, 7],
}),
],
diff --git a/src/data/services/lms/fakeData/pageData/index.jsx b/src/data/services/lms/fakeData/pageData/index.jsx
index 3afe557c..b3ac5afe 100644
--- a/src/data/services/lms/fakeData/pageData/index.jsx
+++ b/src/data/services/lms/fakeData/pageData/index.jsx
@@ -10,7 +10,7 @@ export const emptySubmission = {
export const peerAssessment = {
progress: progressStates.peer(),
- rubric: rubricStates.criteriaFeedbackEnabled.empty,
+ rubric: rubricStates.criteriaFeedbackEnabled.filled,
submission: submissionStates.individialSubmission,
};
diff --git a/src/data/services/lms/hooks/actions.test.ts b/src/data/services/lms/hooks/actions.test.ts
new file mode 100644
index 00000000..e28748d8
--- /dev/null
+++ b/src/data/services/lms/hooks/actions.test.ts
@@ -0,0 +1,30 @@
+import { useQueryClient, useMutation } from '@tanstack/react-query';
+import { when } from 'jest-when';
+
+import { createMutationAction } from './actions';
+
+jest.mock('@tanstack/react-query', () => ({
+ useQueryClient: jest.fn(),
+ useMutation: jest.fn(),
+}));
+
+describe('actions', () => {
+ const queryClient = { setQueryData: jest.fn() };
+
+ when(useQueryClient).mockReturnValue(queryClient);
+ when(useMutation).mockImplementation(({ mutationFn }) => {
+ return {
+ mutate: mutationFn,
+ };
+ });
+
+ describe('createMutationAction', () => {
+ it('returns a mutation function', () => {
+ const aribtraryMutationFn = jest.fn();
+ const mutation = createMutationAction(aribtraryMutationFn) as any;
+
+ mutation.mutate('foo', 'bar');
+ expect(aribtraryMutationFn).toHaveBeenCalledWith('foo', 'bar', queryClient);
+ });
+ });
+});
diff --git a/src/data/services/lms/hooks/actions.ts b/src/data/services/lms/hooks/actions.ts
new file mode 100644
index 00000000..1645180b
--- /dev/null
+++ b/src/data/services/lms/hooks/actions.ts
@@ -0,0 +1,30 @@
+import { useQueryClient, useMutation } from '@tanstack/react-query';
+import { queryKeys } from '../constants';
+import { ActionMutationFunction, RubricData } from '../types';
+
+import fakeData from '../fakeData';
+
+export const createMutationAction = (mutationFn: ActionMutationFunction) => {
+ const queryClient = useQueryClient();
+
+ return useMutation({
+ mutationFn: (...args) => mutationFn(...args, queryClient),
+ });
+};
+
+export const submitRubric = () =>
+ createMutationAction(async (data: RubricData, queryClient) => {
+ // TODO: submit rubric
+ await new Promise((resolve) => setTimeout(() => {
+ fakeData.pageData.shapes.peerAssessment.rubric = {
+ criterion_feedback: data.criterionFeedback,
+ options_selected: data.optionsSelected,
+ overall_feedback: data.overallFeedback,
+ } as any;
+ resolve(null);
+ }, 1000));
+
+ queryClient.invalidateQueries([queryKeys.pageData, true])
+
+ return Promise.resolve(data);
+ });
\ No newline at end of file
diff --git a/src/data/services/lms/hooks/api.test.ts b/src/data/services/lms/hooks/data.test.ts
similarity index 86%
rename from src/data/services/lms/hooks/api.test.ts
rename to src/data/services/lms/hooks/data.test.ts
index eab9f214..07f1ab2b 100644
--- a/src/data/services/lms/hooks/api.test.ts
+++ b/src/data/services/lms/hooks/data.test.ts
@@ -8,7 +8,7 @@ import * as types from '../types';
import { queryKeys } from '../constants';
import fakeData from '../fakeData';
-import { useORAConfig, usePageData } from './api';
+import { useORAConfig, usePageData } from './data';
jest.mock('@tanstack/react-query', () => ({ useQuery: jest.fn() }));
@@ -26,7 +26,7 @@ interface MockUsePageDataQuery { (QueryArgs): MockPageDataQuery }
interface MockPageDataUseConfigHook { (): MockPageDataQuery }
let out;
-describe('lms api hooks', () => {
+describe('lms data hooks', () => {
describe('useORAConfig', () => {
const mockUseQuery = (hasData: boolean): MockUseORAQuery => ({ queryKey, queryFn }) => ({
data: hasData ? camelCaseObject(fakeData.oraConfig.assessmentText) : undefined,
@@ -74,8 +74,16 @@ describe('lms api hooks', () => {
});
});
describe('usePageData', () => {
+ const pageDataCamelCase = (data: any) => ({
+ ...data,
+ rubric: {
+ optionsSelected: {...data.rubric.options_selected},
+ criterionFeedback: {...data.rubric.criterion_feedback},
+ overallFeedback: data.rubric.overall_feedback,
+ },
+ });
const mockUseQuery = (data?: types.PageData): MockUsePageDataQuery => ({ queryKey, queryFn }) => ({
- data: data ? camelCaseObject(data) : undefined,
+ data: data ? pageDataCamelCase(data) : {},
queryKey,
queryFn,
});
@@ -104,10 +112,10 @@ describe('lms api hooks', () => {
});
it('initializes query with promise pointing to empty submission page data', async () => {
const response = await out.queryFn();
- expect(response).toEqual(fakeData.pageData.shapes.emptySubmission);
+ expect(response).toEqual(pageDataCamelCase(fakeData.pageData.shapes.emptySubmission));
});
it('returns camelCase object from data if data has been returned', () => {
- expect(out.data).toEqual(camelCaseObject(fakeData.pageData.shapes.emptySubmission));
+ expect(out.data).toEqual(pageDataCamelCase(fakeData.pageData.shapes.emptySubmission));
});
});
describe('assessment', () => {
@@ -121,10 +129,10 @@ describe('lms api hooks', () => {
});
it('initializes query with promise pointing to peer assessment page data', async () => {
const response = await out.queryFn();
- expect(response).toEqual(fakeData.pageData.shapes.peerAssessment);
+ expect(response).toEqual(pageDataCamelCase(fakeData.pageData.shapes.peerAssessment));
});
it('returns camelCase object from data if data has been returned', () => {
- expect(out.data).toEqual(camelCaseObject(fakeData.pageData.shapes.peerAssessment));
+ expect(out.data).toEqual(pageDataCamelCase(fakeData.pageData.shapes.peerAssessment));
});
});
it('returns empty object from data if data has not been returned', () => {
diff --git a/src/data/services/lms/hooks/api.ts b/src/data/services/lms/hooks/data.ts
similarity index 59%
rename from src/data/services/lms/hooks/api.ts
rename to src/data/services/lms/hooks/data.ts
index 1926c403..c6cc9171 100644
--- a/src/data/services/lms/hooks/api.ts
+++ b/src/data/services/lms/hooks/data.ts
@@ -13,7 +13,9 @@ export const useORAConfig = (): types.QueryData => {
queryKey: [queryKeys.oraConfig],
// queryFn: () => getAuthenticatedClient().get(...),
queryFn: () => {
- const result = window.location.pathname.endsWith('text') ? fakeData.oraConfig.assessmentText : fakeData.oraConfig.assessmentTinyMCE;
+ const result = window.location.pathname.endsWith('text')
+ ? fakeData.oraConfig.assessmentText
+ : fakeData.oraConfig.assessmentTinyMCE;
return Promise.resolve(result);
},
});
@@ -26,17 +28,29 @@ export const useORAConfig = (): types.QueryData => {
export const usePageData = (): types.QueryData => {
const route = useMatch(routes.peerAssessment);
const isAssessment = !!route && route.pattern.path === routes.peerAssessment;
- const returnData = isAssessment
- ? fakeData.pageData.shapes.peerAssessment
- : fakeData.pageData.shapes.emptySubmission;
const { data, ...status } = useQuery({
queryKey: [queryKeys.pageData, isAssessment],
// queryFn: () => getAuthenticatedClient().get(...),
- queryFn: () => Promise.resolve(returnData),
+ queryFn: () => {
+ const assessmentData = isAssessment
+ ? fakeData.pageData.shapes.peerAssessment
+ : fakeData.pageData.shapes.emptySubmission;
+
+ const returnData = assessmentData ? {
+ ...assessmentData,
+ rubric: {
+ optionsSelected: {...assessmentData.rubric.options_selected},
+ criterionFeedback: {...assessmentData.rubric.criterion_feedback},
+ overallFeedback: assessmentData.rubric.overall_feedback,
+ },
+ }: {};
+
+ return Promise.resolve(returnData as any);
+ },
});
return {
...status,
- data: data ? camelCaseObject(data) : {},
+ data,
};
};
diff --git a/src/data/services/lms/hooks/selectors.test.ts b/src/data/services/lms/hooks/selectors.test.ts
index faa4f1b6..0a9bb0f6 100644
--- a/src/data/services/lms/hooks/selectors.test.ts
+++ b/src/data/services/lms/hooks/selectors.test.ts
@@ -1,7 +1,7 @@
import { when } from 'jest-when';
import { keyStore } from '@edx/react-unit-test-utils';
-import * as api from './api';
+import * as data from './data';
import * as selectors from './selectors';
const statusData = {
@@ -14,7 +14,7 @@ const statusData = {
const testValue = 'some-test-data';
-const apiKeys = keyStore(api);
+const dataKeys = keyStore(data);
const selectorKeys = keyStore(selectors);
const mockHook = (module, key, returnValue) => {
@@ -22,9 +22,9 @@ const mockHook = (module, key, returnValue) => {
when(spy).calledWith().mockReturnValueOnce(returnValue);
};
-describe('lms api selector hooks', () => {
+describe('lms data selector hooks', () => {
const mockORAConfig = (returnValue) => {
- mockHook(api, apiKeys.useORAConfig, returnValue);
+ mockHook(data, dataKeys.useORAConfig, returnValue);
};
const mockORAData = (returnValue) => {
mockHook(selectors, selectorKeys.useORAConfigData, returnValue);
@@ -83,7 +83,7 @@ describe('lms api selector hooks', () => {
});
describe('Page Data selectors', () => {
const mockPageDataQuery = (returnValue) => {
- mockHook(api, apiKeys.usePageData, returnValue);
+ mockHook(data, dataKeys.usePageData, returnValue);
};
const mockPageData = (returnValue) => {
mockHook(selectors, selectorKeys.usePageData, returnValue);
diff --git a/src/data/services/lms/hooks/selectors.ts b/src/data/services/lms/hooks/selectors.ts
index f1ef82d0..99272791 100644
--- a/src/data/services/lms/hooks/selectors.ts
+++ b/src/data/services/lms/hooks/selectors.ts
@@ -1,9 +1,9 @@
-import * as api from './api';
+import * as data from './data';
import * as types from '../types';
export const useORAConfigDataStatus = (): types.QueryStatus => {
- const queryStatus = api.useORAConfig();
+ const queryStatus = data.useORAConfig();
return {
isLoading: queryStatus.isLoading,
isFetching: queryStatus.isFetching,
@@ -14,11 +14,11 @@ export const useORAConfigDataStatus = (): types.QueryStatus => {
};
export const useIsORAConfigLoaded = (): boolean => (
- api.useORAConfig().status === 'success'
+ data.useORAConfig().status === 'success'
);
export const useORAConfigData = (): types.ORAConfig => (
- api.useORAConfig().data
+ data.useORAConfig().data
);
export const useSubmissionConfig = (): types.SubmissionConfig => (
@@ -34,7 +34,7 @@ export const useRubricConfig = (): types.RubricConfig => useORAConfigData().rubr
export const useLeaderboardConfig = (): types.LeaderboardConfig => useORAConfigData().leaderboardConfig;
export const usePageDataStatus = () => {
- const queryStatus = api.usePageData();
+ const queryStatus = data.usePageData();
return {
isLoading: queryStatus.isLoading,
isFetching: queryStatus.isFetching,
@@ -44,10 +44,10 @@ export const usePageDataStatus = () => {
};
};
export const useIsPageDataLoaded = (): boolean => (
- api.usePageData().status === 'success'
+ data.usePageData().status === 'success'
);
-export const usePageData = (): types.PageData => api.usePageData().data;
+export const usePageData = (): types.PageData => data.usePageData().data;
export const useSubmissionTeamInfo = (): types.SubmissionTeamData => usePageData().submission.teamInfo;
diff --git a/src/data/services/lms/index.js b/src/data/services/lms/index.js
index 08e4502d..98d7d489 100644
--- a/src/data/services/lms/index.js
+++ b/src/data/services/lms/index.js
@@ -1,10 +1,12 @@
import { StrictDict } from '@edx/react-unit-test-utils';
import urls from './urls';
-import api from './hooks/api';
+import data from './hooks/data';
import selectors from './hooks/selectors';
+import actions from './hooks/actions';
export default StrictDict({
- api,
+ data,
selectors,
urls,
+ actions,
});
diff --git a/src/data/services/lms/types/react-query.ts b/src/data/services/lms/types/react-query.ts
index 400e0fc7..20fc8ee4 100644
--- a/src/data/services/lms/types/react-query.ts
+++ b/src/data/services/lms/types/react-query.ts
@@ -1,3 +1,5 @@
+import { QueryClient } from "@tanstack/query-core";
+
// React-Query fields
export interface QueryStatus {
isLoading: boolean,
@@ -14,3 +16,7 @@ export interface QueryData extends QueryStatus {
error: unknown,
data: Response,
}
+
+export type MutationStatus = 'idle' | 'loading' | 'error' | 'success'
+
+export type ActionMutationFunction = (args: any, queryClient: QueryClient) => Promise
diff --git a/src/setupTest.js b/src/setupTest.js
index 34bd72ce..00e08236 100644
--- a/src/setupTest.js
+++ b/src/setupTest.js
@@ -1,2 +1,76 @@
import 'core-js/stable';
import 'regenerator-runtime/runtime';
+
+jest.mock('@edx/frontend-platform/i18n', () => {
+ const i18n = jest.requireActual('@edx/frontend-platform/i18n');
+ const { formatMessage } = jest.requireActual('@edx/react-unit-test-utils');
+ const formatDate = jest.fn(date => new Date(date).toLocaleDateString()).mockName('useIntl.formatDate');
+ return {
+ ...i18n,
+ useIntl: () => ({
+ formatMessage,
+ formatDate,
+ }),
+ defineMessages: m => m,
+ FormattedMessage: () => 'FormattedMessage',
+ };
+});
+
+jest.mock('@edx/paragon', () => jest.requireActual('@edx/react-unit-test-utils').mockComponents({
+ Alert: {
+ Heading: 'Alert.Heading',
+ },
+ AlertModal: 'AlertModal',
+ ActionRow: 'ActionRow',
+ Badge: 'Badge',
+ Button: 'Button',
+ Card: {
+ Body: 'Card.Body',
+ Section: 'Card.Section',
+ Footer: 'Card.Footer',
+ },
+ Col: 'Col',
+ Collapsible: {
+ Advanced: 'Collapsible.Advanced',
+ Body: 'Collapsible.Body',
+ Trigger: 'Collapsible.Trigger',
+ Visible: 'Collapsible.Visible',
+ },
+ Container: 'Container',
+ DataTable: {
+ EmptyTable: 'DataTable.EmptyTable',
+ Table: 'DataTable.Table',
+ TableControlBar: 'DataTable.TableControlBar',
+ TableController: 'DataTable.TableController',
+ TableFooter: 'DataTable.TableFooter',
+ },
+ Dropdown: {
+ Item: 'Dropdown.Item',
+ Menu: 'Dropdown.Menu',
+ Toggle: 'Dropdown.Toggle',
+ },
+ Form: {
+ Control: {
+ Feedback: 'Form.Control.Feedback',
+ },
+ Group: 'Form.Group',
+ Label: 'Form.Label',
+ Radio: 'Form.Radio',
+ RadioSet: 'Form.RadioSet',
+ },
+ FormControlFeedback: 'FormControlFeedback',
+ FullscreenModal: 'FullscreenModal',
+ Hyperlink: 'Hyperlink',
+ Icon: 'Icon',
+ IconButton: 'IconButton',
+ MultiSelectDropdownFilter: 'MultiSelectDropdownFilter',
+ OverlayTrigger: 'OverlayTrigger',
+ PageBanner: 'PageBanner',
+ Popover: {
+ Content: 'Popover.Content',
+ },
+ Row: 'Row',
+ StatefulButton: 'StatefulButton',
+ TextFilter: 'TextFilter',
+ Spinner: 'Spinner',
+}));
diff --git a/src/views/PeerAssessmentView/AssessmentContentLayout.jsx b/src/views/PeerAssessmentView/AssessmentContentLayout.jsx
index 55f600f5..80f0fc8f 100644
--- a/src/views/PeerAssessmentView/AssessmentContentLayout.jsx
+++ b/src/views/PeerAssessmentView/AssessmentContentLayout.jsx
@@ -18,7 +18,7 @@ const AssessmentContentLayout = () => {
- {showRubric && ()}
+ {showRubric && ()}
diff --git a/tsconfig.json b/tsconfig.json
index f168de28..173f94f6 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -1,7 +1,7 @@
{
"extends": "@edx/typescript-config",
"compilerOptions": {
- "rootDir": ".",
+ "rootDir": "./src",
"outDir": "dist",
"baseUrl": "./src",
"paths": {