Skip to content

Commit

Permalink
feat: interactive rubric
Browse files Browse the repository at this point in the history
  • Loading branch information
leangseu-edx committed Sep 5, 2023
1 parent f5e7ba4 commit 04b3190
Show file tree
Hide file tree
Showing 21 changed files with 371 additions and 94 deletions.
1 change: 1 addition & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
"core-js": "3.31.1",
"filesize": "^8.0.6",
"jest-when": "^3.6.0",
"lodash.camelcase": "^4.3.0",
"prop-types": "15.8.1",
"query-string": "^8.1.0",
"react": "^17.0.2",
Expand Down
31 changes: 17 additions & 14 deletions src/components/Rubric/CriterionContainer/CriterionFeedback.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,39 +3,42 @@ 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 { feedbackRequirement } from 'data/services/lms/constants';
import messages from './messages';
import { useCriterionData } from './hooks';

export const stateKeys = StrictDict({
value: 'value',
});
import messages from './messages';

/**
* <CriterionFeedback />
*/
const CriterionFeedback = ({
criterion,
}) => {
const [value, setValue] = useKeyedState(stateKeys.value, '');
const { formatMessage } = useIntl();

let commentMessage = formatMessage(messages.addComments);
if (criterion.feedbackRequired === feedbackRequirement.optional) {
commentMessage += ` ${formatMessage(messages.optional)}`;
}

const {
feedback: {
value,
isInvalid,
onChange,
},
} = useCriterionData({
criterion,
});

if (
!criterion.feedbackEnabled
|| criterion.feedbackRequired === feedbackRequirement.disabled
) {
return null;
}

const onChange = ({ target }) => { setValue(target.value); };
let commentMessage = formatMessage(messages.addComments);
if (criterion.feedbackRequired === feedbackRequirement.optional) {
commentMessage += ` ${formatMessage(messages.optional)}`;
}

const isInvalid = value === '';

return (
<Form.Group isInvalid={isInvalid}>
<Form.Control
Expand Down
20 changes: 12 additions & 8 deletions src/components/Rubric/CriterionContainer/RadioCriterion.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,10 @@ 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 messages from './messages';
import { useCriterionData } from './hooks';

export const stateKeys = StrictDict({
value: 'value',
});
import messages from './messages';

/**
* <RadioCriterion />
Expand All @@ -18,10 +15,17 @@ const RadioCriterion = ({
isGrading,
criterion,
}) => {
const [value, setValue] = useKeyedState(stateKeys.value, '');
const { formatMessage } = useIntl();
const onChange = ({ target }) => { setValue(target.value); };
const isInvalid = value === '';

const {
radio: {
value,
isInvalid,
onChange,
},
} = useCriterionData({
criterion,
});

return (
<Form.RadioSet name={criterion.name} value={value}>
Expand Down
85 changes: 85 additions & 0 deletions src/components/Rubric/CriterionContainer/hooks.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { mocked } from 'ts-jest/utils';

import { usePageData } from 'data/services/lms/hooks/selectors';
import { updatePageData } from 'data/services/lms/hooks/actions';

jest.mock('data/services/lms/hooks/selectors');
jest.mock('data/services/lms/hooks/actions');

const mockedUsePageData = mocked(usePageData);
const mockedUpdatePageData = mocked(updatePageData);

import { useCriterionData } from './hooks';

describe('useCriterionData', () => {
const criterion = {
name: 'criterion-1',
}

const mutateFn = jest.fn();

mockedUsePageData.mockReturnValue({
rubric: {
optionsSelected: {
'criterion-1': 'option-1',
'criterion-2': 'option-2',
},
criterionFeedback: {
'criterion-1': 'feedback-1',
'criterion-2': 'feedback-2',
},
},
} as any);

mockedUpdatePageData.mockReturnValue({
mutate: mutateFn,
} as any);

it('should return radio and feedback values', () => {
const { radio, feedback } = useCriterionData({ criterion });

expect(radio.value).toBe('option-1');
expect(radio.isInvalid).toBe(false);
expect(feedback.value).toBe('feedback-1');
expect(feedback.isInvalid).toBe(false);
});

test('radio onChange should call mutate with updated rubric', () => {
const { radio } = useCriterionData({ criterion });

radio.onChange({ target: { value: 'option-3' } });

expect(mutateFn).toHaveBeenCalledWith({
rubric: {
optionsSelected: {
'criterion-1': 'option-3',
'criterion-2': 'option-2',
},
criterionFeedback: {
'criterion-1': 'feedback-1',
'criterion-2': 'feedback-2',
},
},
});
});

test('feedback onChange should call mutate with updated rubric', () => {
const { feedback } = useCriterionData({ criterion });

feedback.onChange({ target: { value: 'feedback-3' } });

expect(mutateFn).toHaveBeenCalledWith({
rubric: {
optionsSelected: {
'criterion-1': 'option-1',
'criterion-2': 'option-2',
},
criterionFeedback: {
'criterion-1': 'feedback-3',
'criterion-2': 'feedback-2',
},
},
});
});
});

48 changes: 48 additions & 0 deletions src/components/Rubric/CriterionContainer/hooks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { usePageData } from 'data/services/lms/hooks/selectors';
import { updatePageData } from 'data/services/lms/hooks/actions';

export const useCriterionData = ({
criterion,
}) => {
const data = usePageData();
const mutation = updatePageData();

const updateRadio = ({ target }) => {
mutation.mutate({
...data,
rubric: {
...data.rubric,
optionsSelected: {
...data.rubric.optionsSelected,
[criterion.name]: target.value,
}
}
} as any);
};

const updateFeedback = ({ target }) => {
mutation.mutate({
...data,
rubric: {
...data.rubric,
criterionFeedback: {
...data.rubric.criterionFeedback,
[criterion.name]: target.value,
}
}
} as any);
};

return {
radio: {
value: data.rubric.optionsSelected[criterion.name],
isInvalid: false,
onChange: updateRadio,
},
feedback: {
value: data.rubric.criterionFeedback[criterion.name],
isInvalid: false,
onChange: updateFeedback,
},
};
};
37 changes: 12 additions & 25 deletions src/components/Rubric/RubricFeedback.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,31 +3,22 @@ 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',
});
import { useRubricData } from './hooks';

/**
* <RubricFeedback />
*/
const RubricFeedback = ({
isGrading,
}) => {
const [value, setValue] = useKeyedState('');
const RubricFeedback = ({ isGrading }) => {
const { formatMessage } = useIntl();
const feedbackPrompt = useRubricConfig().feedbackConfig.defaultText;

const onChange = (event) => {
setValue(event.target.value);
};
const {
feedbackPrompt, value, isInvalid, onChange,
} = useRubricData({
isGrading,
});

const inputLabel = formatMessage(
isGrading ? messages.addComments : messages.comments,
Expand All @@ -51,15 +42,11 @@ const RubricFeedback = ({
onChange={onChange}
disabled={!isGrading}
/>
{
/*
{isInvalid && (
<Form.Control.Feedback type="invalid" className="feedback-error-msg">
<FormattedMessage {...messages.overallFeedbackError} />
</Form.Control.Feedback>
)}
*/
}
{isInvalid && (
<Form.Control.Feedback type="invalid" className="feedback-error-msg">
{formatMessage(messages.overallFeedbackError)}
</Form.Control.Feedback>
)}
</Form.Group>
);
};
Expand Down
32 changes: 0 additions & 32 deletions src/components/Rubric/hooks.js

This file was deleted.

28 changes: 28 additions & 0 deletions src/components/Rubric/hooks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { usePageData, useRubricConfig } from 'data/services/lms/hooks/selectors';
import { updatePageData } from 'data/services/lms/hooks/actions';

export const useRubricData = ({
isGrading,
}) => {
const { feedbackConfig } = useRubricConfig();
const data = usePageData();
const mutation = updatePageData();

const onOverallFeedbackChange = ({ target }) => {
mutation.mutate({
...data,
rubric: {
...data.rubric,
overallFeedback: target.value,
}
} as any);
};

return {
feedbackPrompt: feedbackConfig.defaultText,
value: data.rubric.overallFeedback,
onChange: onOverallFeedbackChange,
disabled: !isGrading,
isInvalid: false,
};
};
2 changes: 1 addition & 1 deletion src/components/Rubric/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export const Rubric = ({ isGrading }) => {
<CriterionContainer {...{ isGrading, key: criterion.name, criterion }} />
))}
<hr />
{isGrading && <RubricFeedback />}
{isGrading && <RubricFeedback isGrading={isGrading} />}
</Card.Section>
{isGrading && (
<div className="rubric-footer">
Expand Down
2 changes: 1 addition & 1 deletion src/data/services/lms/fakeData/pageData/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export const emptySubmission = {

export const peerAssessment = {
progress: progressStates.peer(),
rubric: rubricStates.criteriaFeedbackEnabled.empty,
rubric: rubricStates.criteriaFeedbackEnabled.filled,
submission: submissionStates.individialSubmission,
};

Expand Down
Loading

0 comments on commit 04b3190

Please sign in to comment.