refactor: replacing injectIntl with useIntl() (#458)

This commit is contained in:
Jacobo Dominguez
2025-08-13 14:47:19 -06:00
committed by GitHub
parent d71edbd2f2
commit f49c6a55f2
10 changed files with 194 additions and 214 deletions

View File

@@ -8,7 +8,7 @@ import {
TextFilter,
MultiSelectDropdownFilter,
} from '@openedx/paragon';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { useIntl } from '@edx/frontend-platform/i18n';
import { gradingStatuses, submissionFields } from 'data/services/lms/constants';
import lmsMessages from 'data/services/lms/messages';
@@ -25,113 +25,108 @@ import messages from './messages';
/**
* <SubmissionsTable />
*/
export class SubmissionsTable extends React.Component {
get gradeStatusOptions() {
return Object.keys(gradingStatuses).map(statusKey => ({
name: this.translate(lmsMessages[gradingStatuses[statusKey]]),
value: gradingStatuses[statusKey],
}));
}
export const SubmissionsTable = ({
isIndividual,
listData,
loadSelectionForReview,
}) => {
const intl = useIntl();
get userLabel() {
return this.translate(this.props.isIndividual ? messages.username : messages.teamName);
}
const translate = (...args) => intl.formatMessage(...args);
get userAccessor() {
return this.props.isIndividual
? submissionFields.username
: submissionFields.teamName;
}
const gradeStatusOptions = Object.keys(gradingStatuses).map(statusKey => ({
name: translate(lmsMessages[gradingStatuses[statusKey]]),
value: gradingStatuses[statusKey],
}));
get dateSubmittedLabel() {
return this.translate(this.props.isIndividual
? messages.learnerSubmissionDate
: messages.teamSubmissionDate);
}
const userLabel = translate(isIndividual ? messages.username : messages.teamName);
formatDate = ({ value }) => {
const userAccessor = isIndividual
? submissionFields.username
: submissionFields.teamName;
const dateSubmittedLabel = translate(isIndividual
? messages.learnerSubmissionDate
: messages.teamSubmissionDate);
const formatDate = ({ value }) => {
const date = new Date(moment(value));
return date.toLocaleString();
};
formatGrade = ({ value: score }) => (
const formatGrade = ({ value: score }) => (
score === null ? '-' : `${score.pointsEarned}/${score.pointsPossible}`
);
formatStatus = ({ value }) => (<StatusBadge status={value} />);
const formatStatus = ({ value }) => (<StatusBadge status={value} />);
translate = (...args) => this.props.intl.formatMessage(...args);
handleViewAllResponsesClick = (data) => () => {
const handleViewAllResponsesClick = (data) => () => {
const getSubmissionUUID = (row) => row.original.submissionUUID;
this.props.loadSelectionForReview(data.map(getSubmissionUUID));
loadSelectionForReview(data.map(getSubmissionUUID));
};
render() {
if (!this.props.listData.length) {
return null;
}
return (
<div className="submissions-table">
<DataTable
data-testid="data-table"
isFilterable
FilterStatusComponent={FilterStatusComponent}
numBreakoutFilters={2}
defaultColumnValues={{ Filter: TextFilter }}
isSelectable
isSortable
isPaginated
itemCount={this.props.listData.length}
initialState={{ pageSize: 10, pageIndex: 0 }}
data={this.props.listData}
tableActions={[
<TableAction handleClick={this.handleViewAllResponsesClick} />,
]}
bulkActions={[
<SelectedBulkAction handleClick={this.handleViewAllResponsesClick} />,
]}
columns={[
{
Header: this.userLabel,
accessor: this.userAccessor,
},
{
Header: this.dateSubmittedLabel,
accessor: submissionFields.dateSubmitted,
Cell: this.formatDate,
disableFilters: true,
},
{
Header: this.translate(messages.grade),
accessor: submissionFields.score,
Cell: this.formatGrade,
disableFilters: true,
},
{
Header: this.translate(messages.gradingStatus),
accessor: submissionFields.gradingStatus,
Cell: this.formatStatus,
Filter: MultiSelectDropdownFilter,
filter: 'includesValue',
filterChoices: this.gradeStatusOptions,
},
]}
>
<DataTable.TableControlBar />
<DataTable.Table />
<DataTable.TableFooter />
</DataTable>
</div>
);
if (!listData.length) {
return null;
}
}
return (
<div className="submissions-table">
<DataTable
data-testid="data-table"
isFilterable
FilterStatusComponent={FilterStatusComponent}
numBreakoutFilters={2}
defaultColumnValues={{ Filter: TextFilter }}
isSelectable
isSortable
isPaginated
itemCount={listData.length}
initialState={{ pageSize: 10, pageIndex: 0 }}
data={listData}
tableActions={[
<TableAction handleClick={handleViewAllResponsesClick} />,
]}
bulkActions={[
<SelectedBulkAction handleClick={handleViewAllResponsesClick} />,
]}
columns={[
{
Header: userLabel,
accessor: userAccessor,
},
{
Header: dateSubmittedLabel,
accessor: submissionFields.dateSubmitted,
Cell: formatDate,
disableFilters: true,
},
{
Header: translate(messages.grade),
accessor: submissionFields.score,
Cell: formatGrade,
disableFilters: true,
},
{
Header: translate(messages.gradingStatus),
accessor: submissionFields.gradingStatus,
Cell: formatStatus,
Filter: MultiSelectDropdownFilter,
filter: 'includesValue',
filterChoices: gradeStatusOptions,
},
]}
>
<DataTable.TableControlBar />
<DataTable.Table />
<DataTable.TableFooter />
</DataTable>
</div>
);
};
SubmissionsTable.defaultProps = {
listData: [],
};
SubmissionsTable.propTypes = {
// injected
intl: intlShape.isRequired,
// redux
isIndividual: PropTypes.bool.isRequired,
listData: PropTypes.arrayOf(PropTypes.shape({
@@ -155,4 +150,4 @@ export const mapDispatchToProps = {
loadSelectionForReview: thunkActions.grading.loadSelectionForReview,
};
export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(SubmissionsTable));
export default connect(mapStateToProps, mapDispatchToProps)(SubmissionsTable);

View File

@@ -10,7 +10,6 @@ import { selectors, thunkActions } from 'data/redux';
import { gradingStatuses as statuses, submissionFields } from 'data/services/lms/constants';
import StatusBadge from 'components/StatusBadge';
import { formatMessage } from 'testUtils';
import messages from './messages';
import {
SubmissionsTable,
@@ -117,7 +116,6 @@ describe('SubmissionsTable component', () => {
};
beforeEach(() => {
props.loadSelectionForReview = jest.fn();
props.intl = { formatMessage };
});
describe('render tests', () => {
const mockMethod = (methodName) => {
@@ -272,13 +270,26 @@ describe('SubmissionsTable component', () => {
});
describe('handleViewAllResponsesClick', () => {
it('calls loadSelectionForReview with submissionUUID from all rows if there are no selectedRows', () => {
// Test the integration by simulating the function call directly
// Since handleViewAllResponsesClick is internal to the functional component,
// we test through the component behavior
const data = [
{ original: { submissionUUID: '123' } },
{ original: { submissionUUID: '456' } },
{ original: { submissionUUID: '789' } },
];
el.instance.children[0].props.tableActions[0].props.handleClick(data)();
expect(el.shallowRenderer._instance.props.loadSelectionForReview).toHaveBeenCalledWith(['123', '456', '789']); // eslint-disable-line no-underscore-dangle
// Create a test instance that we can call the function on
const testEl = shallow(<SubmissionsTable {...props} />);
const tableProps = testEl.instance.findByTestId('data-table')[0].props;
// Get the handleClick function from the TableAction props
const handleClickFunction = tableProps.tableActions[0].props.handleClick;
// Call the function as TableAction would call it
handleClickFunction(data)();
expect(props.loadSelectionForReview).toHaveBeenCalledWith(['123', '456', '789']);
});
});
});

View File

@@ -4,7 +4,7 @@ import { connect } from 'react-redux';
import { Icon, IconButton } from '@openedx/paragon';
import { ChevronLeft, ChevronRight } from '@openedx/paragon/icons';
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
import { selectors, thunkActions } from 'data/redux';
import messages from './messages';
@@ -13,7 +13,6 @@ import messages from './messages';
* <SubmissionNavigation />
*/
export const SubmissionNavigation = ({
intl,
hasPrevSubmission,
hasNextSubmission,
loadPrev,
@@ -21,42 +20,43 @@ export const SubmissionNavigation = ({
activeIndex,
selectionLength,
allowNavigation,
}) => (
<span className="submission-navigation">
<IconButton
className="ml-1"
size="inline"
disabled={!hasPrevSubmission || !allowNavigation}
alt={intl.formatMessage(messages.loadPrevious)}
src={ChevronLeft}
iconAs={Icon}
onClick={loadPrev}
/>
<span className="ml-1">
<FormattedMessage
{...messages.navigationLabel}
values={{ current: activeIndex + 1, total: selectionLength }}
}) => {
const intl = useIntl();
return (
<span className="submission-navigation">
<IconButton
className="ml-1"
size="inline"
disabled={!hasPrevSubmission || !allowNavigation}
alt={intl.formatMessage(messages.loadPrevious)}
src={ChevronLeft}
iconAs={Icon}
onClick={loadPrev}
/>
<span className="ml-1">
<FormattedMessage
{...messages.navigationLabel}
values={{ current: activeIndex + 1, total: selectionLength }}
/>
</span>
<IconButton
className="ml-1"
size="inline"
disabled={!hasNextSubmission || !allowNavigation}
alt={intl.formatMessage(messages.loadNext)}
src={ChevronRight}
iconAs={Icon}
onClick={loadNext}
/>
</span>
<IconButton
className="ml-1"
size="inline"
disabled={!hasNextSubmission || !allowNavigation}
alt={intl.formatMessage(messages.loadNext)}
src={ChevronRight}
iconAs={Icon}
onClick={loadNext}
/>
</span>
);
);
};
SubmissionNavigation.defaultProps = {
hasPrevSubmission: false,
hasNextSubmission: false,
allowNavigation: false,
};
SubmissionNavigation.propTypes = {
// injected
intl: intlShape.isRequired,
// redux
allowNavigation: PropTypes.bool,
activeIndex: PropTypes.number.isRequired,
@@ -80,4 +80,4 @@ export const mapDispatchToProps = {
loadPrev: thunkActions.grading.loadPrev,
};
export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(SubmissionNavigation));
export default connect(mapStateToProps, mapDispatchToProps)(SubmissionNavigation);

View File

@@ -3,8 +3,6 @@ import { shallow } from '@edx/react-unit-test-utils';
import { selectors, thunkActions } from 'data/redux';
import { formatMessage } from 'testUtils';
import {
SubmissionNavigation,
mapStateToProps,
@@ -28,7 +26,6 @@ jest.mock('data/redux/requests/selectors', () => ({
describe('SubmissionNavigation component', () => {
describe('component', () => {
const props = {
intl: { formatMessage },
activeIndex: 4,
selectionLength: 5,
};

View File

@@ -2,7 +2,7 @@ import React from 'react';
import { useDispatch } from 'react-redux';
import { FullscreenModal } from '@openedx/paragon';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { useIntl } from '@edx/frontend-platform/i18n';
import LoadingMessage from 'components/LoadingMessage';
import DemoWarning from 'containers/DemoWarning';
@@ -18,7 +18,8 @@ import './ReviewModal.scss';
/**
* <ReviewModal />
*/
export const ReviewModal = ({ intl }) => {
export const ReviewModal = () => {
const intl = useIntl();
const dispatch = useDispatch();
const {
isLoading,
@@ -48,9 +49,5 @@ export const ReviewModal = ({ intl }) => {
</FullscreenModal>
);
};
ReviewModal.propTypes = {
// injected
intl: intlShape.isRequired,
};
export default injectIntl(ReviewModal);
export default ReviewModal;

View File

@@ -19,9 +19,8 @@ jest.mock('./hooks', () => ({
rendererHooks: jest.fn(),
}));
const dispatch = useDispatch();
describe('ReviewModal component', () => {
const dispatch = useDispatch();
const hookProps = {
isLoading: false,
title: 'test-ora-name',
@@ -34,7 +33,7 @@ describe('ReviewModal component', () => {
const render = (newVals) => {
hooks.rendererHooks.mockReturnValueOnce({ ...hookProps, ...newVals });
return shallow(<ReviewModal intl={{ formatMessage }} />);
return shallow(<ReviewModal />);
};
describe('component', () => {
describe('snapshots', () => {
@@ -52,7 +51,10 @@ describe('ReviewModal component', () => {
describe('behavior', () => {
it('initializes renderer hook with dispatch and intl props', () => {
render();
expect(hooks.rendererHooks).toHaveBeenCalledWith({ dispatch, intl: { formatMessage } });
expect(hooks.rendererHooks).toHaveBeenCalledWith({
dispatch,
intl: { formatMessage, formatDate: expect.any(Function) },
});
});
});
});

View File

@@ -3,11 +3,7 @@ import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { Form } from '@openedx/paragon';
import {
FormattedMessage,
injectIntl,
intlShape,
} from '@edx/frontend-platform/i18n';
import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
import { feedbackRequirement } from 'data/services/lms/constants';
import { actions, selectors } from 'data/redux';
@@ -18,65 +14,60 @@ import messages from './messages';
/**
* <RubricFeedback />
*/
export class RubricFeedback extends React.Component {
constructor(props) {
super(props);
this.onChange = this.onChange.bind(this);
export const RubricFeedback = ({
isGrading,
value,
feedbackPrompt,
config,
isInvalid,
setValue,
}) => {
const intl = useIntl();
const onChange = (event) => {
setValue(event.target.value);
};
const inputLabel = intl.formatMessage(
isGrading ? messages.addComments : messages.comments,
);
if (config === feedbackRequirement.disabled) {
return null;
}
onChange(event) {
this.props.setValue(event.target.value);
}
get inputLabel() {
return this.props.intl.formatMessage(
this.props.isGrading ? messages.addComments : messages.comments,
);
}
render() {
const {
isGrading, value, feedbackPrompt, config, isInvalid,
} = this.props;
if (config === feedbackRequirement.disabled) {
return null;
}
return (
<Form.Group>
<Form.Label className="criteria-label">
<span className="criteria-title">
<FormattedMessage {...messages.overallComments} />
</span>
<InfoPopover>
<div>{feedbackPrompt}</div>
</InfoPopover>
</Form.Label>
<Form.Control
as="textarea"
className="rubric-feedback feedback-input"
floatingLabel={this.inputLabel}
value={value}
onChange={this.onChange}
disabled={!isGrading}
/>
{isInvalid && (
<Form.Control.Feedback type="invalid" className="feedback-error-msg">
<FormattedMessage {...messages.overallFeedbackError} />
</Form.Control.Feedback>
)}
</Form.Group>
);
}
}
return (
<Form.Group>
<Form.Label className="criteria-label">
<span className="criteria-title">
<FormattedMessage {...messages.overallComments} />
</span>
<InfoPopover>
<div>{feedbackPrompt}</div>
</InfoPopover>
</Form.Label>
<Form.Control
as="textarea"
className="rubric-feedback feedback-input"
floatingLabel={inputLabel}
value={value}
onChange={onChange}
disabled={!isGrading}
/>
{isInvalid && (
<Form.Control.Feedback type="invalid" className="feedback-error-msg">
<FormattedMessage {...messages.overallFeedbackError} />
</Form.Control.Feedback>
)}
</Form.Group>
);
};
RubricFeedback.defaultProps = {
value: { grading: '', review: '' },
};
RubricFeedback.propTypes = {
// injected
intl: intlShape.isRequired,
// redux
config: PropTypes.string.isRequired,
isGrading: PropTypes.bool.isRequired,
@@ -98,6 +89,4 @@ export const mapDispatchToProps = {
setValue: actions.grading.setRubricFeedback,
};
export default injectIntl(
connect(mapStateToProps, mapDispatchToProps)(RubricFeedback),
);
export default connect(mapStateToProps, mapDispatchToProps)(RubricFeedback);

View File

@@ -7,8 +7,6 @@ import {
gradeStatuses,
} from 'data/services/lms/constants';
import { formatMessage } from 'testUtils';
import {
RubricFeedback,
mapDispatchToProps,
@@ -43,7 +41,6 @@ jest.mock('data/redux/grading/selectors', () => ({
describe('Rubric Feedback component', () => {
const props = {
intl: { formatMessage },
config: 'config string',
isGrading: true,
value: 'some value',

View File

@@ -2,7 +2,7 @@ import React from 'react';
import { useDispatch } from 'react-redux';
import { Card, StatefulButton } from '@openedx/paragon';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { useIntl } from '@edx/frontend-platform/i18n';
import DemoAlert from 'components/DemoAlert';
import CriterionContainer from 'containers/CriterionContainer';
@@ -18,7 +18,8 @@ const { ButtonStates } = hooks;
/**
* <Rubric />
*/
export const Rubric = ({ intl }) => {
export const Rubric = () => {
const intl = useIntl();
const dispatch = useDispatch();
const {
criteria,
@@ -53,9 +54,5 @@ export const Rubric = ({ intl }) => {
</>
);
};
Rubric.propTypes = {
// injected
intl: intlShape.isRequired,
};
export default injectIntl(Rubric);
export default Rubric;

View File

@@ -1,8 +1,6 @@
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { formatMessage } from 'testUtils';
import * as hooks from './hooks';
import { Rubric } from '.';
@@ -15,9 +13,6 @@ jest.mock('./hooks', () => ({
}));
describe('Rubric Container', () => {
const props = {
intl: { formatMessage },
};
const hookProps = {
criteria: [
{ prop: 'hook-criteria-props-1', key: 1 },
@@ -30,10 +25,10 @@ describe('Rubric Container', () => {
};
test('snapshot: show footer', () => {
hooks.rendererHooks.mockReturnValueOnce({ ...hookProps, showFooter: true });
expect(shallow(<Rubric {...props} />).snapshot).toMatchSnapshot();
expect(shallow(<Rubric />).snapshot).toMatchSnapshot();
});
test('shapshot: hide footer', () => {
hooks.rendererHooks.mockReturnValueOnce(hookProps);
expect(shallow(<Rubric {...props} />).snapshot).toMatchSnapshot();
expect(shallow(<Rubric />).snapshot).toMatchSnapshot();
});
});