Skip to content

Commit

Permalink
feat: interactive rubric
Browse files Browse the repository at this point in the history
chore: remove smart camelize

chore: update logic and rewrite in typescript

chore: revert back to jsx

chore: rename api.ts to data.ts

chore: update test
  • Loading branch information
leangseu-edx committed Sep 19, 2023
1 parent 57a5036 commit 4c40485
Show file tree
Hide file tree
Showing 36 changed files with 1,506 additions and 171 deletions.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@
"lint:fix": "fedx-scripts eslint --fix --ext .js --ext .jsx .",
"snapshot": "fedx-scripts jest --updateSnapshot",
"start": "fedx-scripts webpack-dev-server --progress",
"test": "fedx-scripts jest --coverage --passWithNoTests"
"test": "fedx-scripts jest --coverage --passWithNoTests",
"test:debug": "node --inspect-brk node_modules/.bin/fedx-scripts jest --runInBand --watch"
},
"husky": {
"hooks": {
Expand Down
41 changes: 18 additions & 23 deletions src/components/Rubric/CriterionContainer/CriterionFeedback.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,49 +3,41 @@ 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';

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

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

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

const { feedbackValue, feedbackIsInvalid, feedbackOnChange } = 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.Group isInvalid={feedbackIsInvalid}>
<Form.Control
as="textarea"
className="criterion-feedback feedback-input"
floatingLabel={commentMessage}
value={value}
onChange={onChange}
value={feedbackValue}
onChange={feedbackOnChange}
/>
{isInvalid && (
{feedbackIsInvalid && (
<Form.Control.Feedback type="invalid" className="feedback-error-msg">
{formatMessage(messages.criterionFeedbackError)}
</Form.Control.Feedback>
Expand All @@ -56,8 +48,11 @@ const CriterionFeedback = ({

CriterionFeedback.propTypes = {
criterion: PropTypes.shape({
feedbackEnabled: PropTypes.bool,
feedbackRequired: PropTypes.string,
feedbackValue: PropTypes.string.isRequired,
feedbackIsInvalid: PropTypes.bool.isRequired,
feedbackOnChange: PropTypes.func.isRequired,
feedbackEnabled: PropTypes.bool.isRequired,
feedbackRequired: PropTypes.oneOf(Object.values(feedbackRequirement)).isRequired,
}).isRequired,
};

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { feedbackRequirement } from 'data/services/lms/constants';

import CriterionFeedback from './CriterionFeedback';

describe('<CriterionFeedback />', () => {
const props = {
criterion: {
feedbackValue: 'feedback-1',
feedbackIsInvalid: false,
feedbackOnChange: jest.fn().mockName('feedbackOnChange'),
feedbackEnabled: true,
feedbackRequired: feedbackRequirement.required,
},
};
describe('renders', () => {
test('feedbackEnabled', () => {
const wrapper = shallow(<CriterionFeedback {...props} />);
expect(wrapper.snapshot).toMatchSnapshot();
});

test('feedbackDisabled render empty', () => {
const wrapper = shallow(
<CriterionFeedback
{...props}
criterion={{ ...props.criterion, feedbackEnabled: false }}
/>,
);
expect(wrapper.snapshot).toMatchSnapshot();
expect(wrapper.isEmptyRender()).toBe(true);
});

test('feedbackRequired disabled render empty', () => {
const wrapper = shallow(
<CriterionFeedback
{...props}
criterion={{
...props.criterion,
feedbackRequired: feedbackRequirement.disabled,
}}
/>,
);
expect(wrapper.snapshot).toMatchSnapshot();
expect(wrapper.isEmptyRender()).toBe(true);
});

test('feedbackRequired: optional', () => {
const wrapper = shallow(
<CriterionFeedback
{...props}
criterion={{
...props.criterion,
feedbackRequired: feedbackRequirement.optional,
}}
/>,
);
expect(wrapper.snapshot).toMatchSnapshot();
expect(wrapper.instance.findByType('Form.Control')[0].props.floatingLabel).toContain('Optional');
});

test('feedbackIsInvalid', () => {
const wrapper = shallow(
<CriterionFeedback
{...props}
criterion={{ ...props.criterion, feedbackIsInvalid: true }}
/>,
);
expect(wrapper.snapshot).toMatchSnapshot();
expect(wrapper.instance.findByType('Form.Control.Feedback')[0].props.type).toBe('invalid');
});
});
});
47 changes: 22 additions & 25 deletions src/components/Rubric/CriterionContainer/RadioCriterion.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,44 +3,37 @@ 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';

export const stateKeys = StrictDict({
value: 'value',
});

/**
* <RadioCriterion />
*/
const RadioCriterion = ({
isGrading,
criterion,
}) => {
const [value, setValue] = useKeyedState(stateKeys.value, '');
const RadioCriterion = ({ isGrading, criterion }) => {
const { formatMessage } = useIntl();
const onChange = ({ target }) => { setValue(target.value); };
const isInvalid = value === '';

const { optionsValue, optionsIsInvalid, optionsOnChange } = criterion;

return (
<Form.RadioSet name={criterion.name} value={value}>
<Form.RadioSet name={criterion.name} value={optionsValue}>
{criterion.options.map((option) => (
<Form.Radio
className="criteria-option"
key={option.name}
value={option.name}
description={formatMessage(messages.optionPoints, { points: option.points })}
onChange={onChange}
description={formatMessage(messages.optionPoints, {
points: option.points,
})}
onChange={optionsOnChange}
disabled={!isGrading}
>
{option.name}
</Form.Radio>
))}
{isInvalid && (
<Form.Control.Feedback type="invalid" className="feedback-error-msg">
{formatMessage(messages.rubricSelectedError)}
</Form.Control.Feedback>
{optionsIsInvalid && (
<Form.Control.Feedback type="invalid" className="feedback-error-msg">
{formatMessage(messages.rubricSelectedError)}
</Form.Control.Feedback>
)}
</Form.RadioSet>
);
Expand All @@ -49,12 +42,16 @@ const RadioCriterion = ({
RadioCriterion.propTypes = {
isGrading: PropTypes.bool.isRequired,
criterion: PropTypes.shape({
name: PropTypes.string,
options: PropTypes.arrayOf(PropTypes.shape({
name: PropTypes.string,
description: PropTypes.string,
points: PropTypes.number,
})),
optionsValue: PropTypes.string.isRequired,
optionsIsInvalid: PropTypes.bool.isRequired,
optionsOnChange: PropTypes.func.isRequired,
name: PropTypes.string.isRequired,
options: PropTypes.arrayOf(
PropTypes.shape({
name: PropTypes.string.isRequired,
points: PropTypes.number.isRequired,
}),
).isRequired,
}).isRequired,
};

Expand Down
67 changes: 67 additions & 0 deletions src/components/Rubric/CriterionContainer/RadioCriterion.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';

import RadioCriterion from './RadioCriterion';

describe('<RadioCriterion />', () => {
const props = {
isGrading: true,
criterion: {
name: 'criterion-1',
optionsValue: 'option-1',
optionsIsInvalid: true,
optionsOnChange: jest.fn().mockName('optionsOnChange'),
options: [
{
name: 'option-1',
description: 'description-1',
points: 1,
},
{
name: 'option-2',
description: 'description-2',
points: 2,
},
],
},
};
describe('renders', () => {
test('options is invalid', () => {
const wrapper = shallow(<RadioCriterion {...props} />);
expect(wrapper.snapshot).toMatchSnapshot();

expect(wrapper.instance.findByType('Form.Radio').length).toEqual(
props.criterion.options.length,
);
wrapper.instance.findByType('Form.Radio').forEach((radio) => {
expect(radio.props.disabled).toEqual(false);
});
expect(
wrapper.instance.findByType('Form.Control.Feedback')[0].props.type,
).toEqual('invalid');
});

test('options is valid no invalid feedback get render', () => {
const wrapper = shallow(
<RadioCriterion
{...props}
criterion={{ ...props.criterion, optionsIsInvalid: false }}
/>,
);
expect(wrapper.snapshot).toMatchSnapshot();

expect(
wrapper.instance.findByType('Form.Control.Feedback').length,
).toEqual(0);
});

test('not isGrading all radios will be disabled', () => {
const wrapper = shallow(<RadioCriterion {...props} isGrading={false} />);
expect(wrapper.snapshot).toMatchSnapshot();

wrapper.instance.findByType('Form.Radio').forEach((radio) => {
expect(radio.props.disabled).toEqual(true);
});
});
});
});
7 changes: 3 additions & 4 deletions src/components/Rubric/CriterionContainer/ReviewCriterion.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,10 @@ ReviewCriterion.propTypes = {
criterion: PropTypes.shape({
options: PropTypes.arrayOf(
PropTypes.shape({
description: PropTypes.string,
name: PropTypes.string,
points: PropTypes.number,
name: PropTypes.string.isRequired,
points: PropTypes.number.isRequired,
}),
),
).isRequired,
}).isRequired,
};

Expand Down
29 changes: 29 additions & 0 deletions src/components/Rubric/CriterionContainer/ReviewCriterion.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';

import ReviewCriterion from './ReviewCriterion';

describe('<ReviewCriterion />', () => {
const props = {
criterion: {
options: [
{
name: 'option-1',
description: 'description-1',
points: 1,
},
{
name: 'option-2',
description: 'description-2',
points: 2,
},
],
},
};

test('renders', () => {
const wrapper = shallow(<ReviewCriterion {...props} />);
expect(wrapper.snapshot).toMatchSnapshot();
expect(wrapper.instance.findByType('FormControlFeedback').length).toEqual(props.criterion.options.length);
});
});
Loading

0 comments on commit 4c40485

Please sign in to comment.