refactor: add transifex support to user-facing messages (#203)

* clean up and test segment integration

* add transifex config

* move user-facing messages into messages files and translate in usage

* lint cleanup

* fix introduced typos

* remove dead code

* remove should-be-ignored temp translation files

* make HistoryHeader use node-type to support translations

* fix apostrophe

* fix snapshot

* v1.4.42
This commit is contained in:
Ben Warzeski
2021-07-22 10:45:18 -04:00
committed by GitHub
parent a4df8f7238
commit c6e33307ba
86 changed files with 1349 additions and 296 deletions

4
.gitignore vendored
View File

@@ -17,3 +17,7 @@ dist/
### Development environments ###
.idea
.vscode
### transifex ###
src/i18n/transifex_input.json
temp

8
.tx/config Normal file
View File

@@ -0,0 +1,8 @@
[main]
host = https://www.transifex.com
[edx-platform.frontend-app-gradebook]
file_filter = src/i18n/messages/<lang>.json
source_file = src/i18n/transifex_input.json
source_lang = en
type = KEYVALUEJSON

View File

@@ -2,8 +2,64 @@ npm-install-%: ## install specified % npm package
npm install $* --save-dev
git add package.json
validate-no-uncommitted-package-lock-changes:
git diff --exit-code package-lock.json
transifex_resource = frontend-app-gradebook
transifex_langs = "ar,fr,es_419,zh_CN"
test:
npm run test
transifex_utils = ./node_modules/.bin/transifex-utils.js
i18n = ./src/i18n
transifex_input = $(i18n)/transifex_input.json
tx_url1 = https://www.transifex.com/api/2/project/edx-platform/resource/$(transifex_resource)/translation/en/strings/
tx_url2 = https://www.transifex.com/api/2/project/edx-platform/resource/$(transifex_resource)/source/
# This directory must match .babelrc .
transifex_temp = ./temp/babel-plugin-react-intl
NPM_TESTS=build i18n_extract lint test is-es5
.PHONY: test
test: $(addprefix test.npm.,$(NPM_TESTS)) ## validate ci suite
.PHONY: test.npm.*
test.npm.%: validate-no-uncommitted-package-lock-changes
test -d node_modules || $(MAKE) requirements
npm run $(*)
.PHONY: requirements
requirements: ## install ci requirements
npm ci
i18n.extract:
# Pulling display strings from .jsx files into .json files...
rm -rf $(transifex_temp)
npm run-script i18n_extract
i18n.concat:
# Gathering JSON messages into one file...
$(transifex_utils) $(transifex_temp) $(transifex_input)
extract_translations: | requirements i18n.extract i18n.concat
# Despite the name, we actually need this target to detect changes in the incoming translated message files as well.
detect_changed_source_translations:
# Checking for changed translations...
git diff --exit-code $(i18n)
# Pushes translations to Transifex. You must run make extract_translations first.
push_translations:
# Pushing strings to Transifex...
tx push -s
# Fetching hashes from Transifex...
./node_modules/reactifex/bash_scripts/get_hashed_strings.sh $(tx_url1)
# Writing out comments to file...
$(transifex_utils) $(transifex_temp) --comments
# Pushing comments to Transifex...
./node_modules/reactifex/bash_scripts/put_comments.sh $(tx_url2)
# Pulls translations from Transifex.
pull_translations:
tx pull -f --mode reviewed --language=$(transifex_langs)
# This target is used by Travis.
validate-no-uncommitted-package-lock-changes:
# Checking for package-lock.json changes...
git diff --exit-code package-lock.json

8
package-lock.json generated
View File

@@ -1,6 +1,6 @@
{
"name": "@edx/frontend-app-gradebook",
"version": "1.4.36",
"version": "1.4.41",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@@ -24779,6 +24779,12 @@
"prop-types": "^15.6.2"
}
},
"reactifex": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/reactifex/-/reactifex-1.1.1.tgz",
"integrity": "sha512-HH2N/b5tRxh7ypIgCRsiBl/CTxRkTEPf9DhIstaM6hne4WiwM5/bBbWuvVlRZc/i3FdqZED3pZ//6n4mtxma4w==",
"dev": true
},
"read-pkg": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-2.0.0.tgz",

View File

@@ -1,6 +1,6 @@
{
"name": "@edx/frontend-app-gradebook",
"version": "1.4.41",
"version": "1.4.42",
"description": "edx editable gradebook-ui to manipulate grade overrides on subsections",
"repository": {
"type": "git",
@@ -10,6 +10,7 @@
"build": "fedx-scripts webpack",
"coveralls": "cat ./coverage/lcov.info | coveralls",
"is-es5": "es-check es5 ./dist/*.js",
"i18n_extract": "BABEL_ENV=i18n fedx-scripts babel src --quiet > /dev/null",
"lint": "fedx-scripts eslint --ext .jsx,.js src/",
"lint-fix": "fedx-scripts eslint --fix --ext .jsx,.js src/",
"prepush": "npm run lint",
@@ -75,6 +76,7 @@
"jest": "24.9.0",
"react-dev-utils": "^5.0.3",
"react-test-renderer": "^16.10.1",
"reactifex": "1.1.1",
"redux-mock-store": "^1.5.3",
"semantic-release": "^17.2.3",
"travis-deploy-once": "^5.0.11"

View File

@@ -1,20 +1,22 @@
/* eslint-disable react/sort-comp, react/button-has-type */
import React from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import { connect } from 'react-redux';
import { Alert } from '@edx/paragon';
import * as appConstants from 'data/constants/app';
import selectors from 'data/selectors';
const { messages: { BulkManagementTab: messages } } = appConstants;
import messages from './messages';
/**
* <BulkManagementAlerts />
* Alerts to display at the top of the BulkManagement tab
*/
export const BulkManagementAlerts = ({ bulkImportError, uploadSuccess }) => (
export const BulkManagementAlerts = ({
bulkImportError,
uploadSuccess,
}) => (
<>
<Alert
variant="danger"
@@ -28,7 +30,7 @@ export const BulkManagementAlerts = ({ bulkImportError, uploadSuccess }) => (
show={uploadSuccess}
dismissible={false}
>
{messages.successDialog}
<FormattedMessage {...messages.successDialog} />
</Alert>
</>
);

View File

@@ -1,12 +1,17 @@
import React from 'react';
import { shallow } from 'enzyme';
import { Alert } from '@edx/paragon';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import selectors from 'data/selectors';
import * as appConstants from 'data/constants/app';
import messages from './messages';
import { BulkManagementAlerts, mapStateToProps } from './BulkManagementAlerts';
jest.mock('@edx/frontend-platform/i18n', () => ({
defineMessages: m => m,
FormattedMessage: () => 'FormattedMessage',
}));
jest.mock('@edx/paragon', () => ({
Alert: () => 'Alert',
}));
@@ -61,8 +66,8 @@ describe('BulkManagementAlerts', () => {
});
test('open success alert with messages.successDialog content', () => {
expect(el.childAt(1).is(Alert)).toEqual(true);
expect(el.childAt(1).children().text()).toEqual(
appConstants.messages.BulkManagementTab.successDialog,
expect(el.childAt(1).children().getElement()).toEqual(
<FormattedMessage {...messages.successDialog} />,
);
expect(el.childAt(1).props().show).toEqual(true);
});

View File

@@ -3,6 +3,8 @@ import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import {
Button,
Form,
@@ -10,11 +12,9 @@ import {
FormGroup,
} from '@edx/paragon';
import { messages } from 'data/constants/app';
import selectors from 'data/selectors';
import thunkActions from 'data/thunkActions';
const { csvUploadLabel, importBtnText } = messages.BulkManagementTab;
import messages from './messages';
/**
* <FileUploadForm />
@@ -56,14 +56,15 @@ export class FileUploadForm extends React.Component {
}
render() {
const { gradeExportUrl } = this.props;
return (
<>
<Form action={this.props.gradeExportUrl} method="post">
<Form action={gradeExportUrl} method="post">
<FormGroup controlId="csv">
<FormControl
className="d-none"
type="file"
label={csvUploadLabel}
label={<FormattedMessage {...messages.csvUploadLabel} />}
onChange={this.handleFileInputChange}
ref={this.fileInputRef}
/>
@@ -71,7 +72,7 @@ export class FileUploadForm extends React.Component {
</Form>
<Button variant="primary" onClick={this.handleClickImportGrades}>
{importBtnText}
<FormattedMessage {...messages.importBtnText} />
</Button>
</>
);

View File

@@ -1,23 +1,25 @@
/* eslint-disable import/no-named-as-default */
import React from 'react';
import { shallow, mount } from 'enzyme';
import { shallow } from 'enzyme';
import TestRenderer from 'react-test-renderer';
import {
Button,
Form,
FormControl,
FormGroup,
} from '@edx/paragon';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import selectors from 'data/selectors';
import thunkActions from 'data/thunkActions';
import * as appConstants from 'data/constants/app';
import { FileUploadForm, mapStateToProps, mapDispatchToProps } from './FileUploadForm';
const {
messages: { BulkManagementTab: messages },
} = appConstants;
import messages from './messages';
jest.mock('@edx/frontend-platform/i18n', () => ({
defineMessages: m => m,
FormattedMessage: () => 'FormattedMessage',
}));
jest.mock('data/selectors', () => ({
__esModule: true,
default: {
@@ -39,10 +41,16 @@ jest.mock('data/thunkActions', () => ({
jest.mock('./BulkManagementAlerts', () => 'BulkManagementAlerts');
jest.mock('./ResultsSummary', () => 'ResultsSummary');
const mockRef = { click: jest.fn(), files: [] };
describe('FileUploadForm', () => {
beforeEach(() => {
mockRef.click.mockClear();
});
describe('component', () => {
let props;
let el;
let inst;
beforeEach(() => {
props = {
gradeExportUrl: 'fakeUrl',
@@ -71,95 +79,105 @@ describe('FileUploadForm', () => {
});
describe('render', () => {
beforeEach(() => {
el = mount(<FileUploadForm {...props} />);
el = TestRenderer.create(
<FileUploadForm {...props} />,
{ createNodeMock: () => mockRef },
);
inst = el.root;
});
describe('alert form', () => {
let form;
beforeEach(() => {
form = el.find(Form);
form = inst.findByType(Form);
});
test('post action points to gradeExportUrl', () => {
expect(form.props().action).toEqual(props.gradeExportUrl);
expect(form.props().method).toEqual('post');
expect(form.props.action).toEqual(props.gradeExportUrl);
expect(form.props.method).toEqual('post');
});
describe('file input', () => {
let formGroup;
beforeEach(() => {
formGroup = el.find(FormGroup);
formGroup = inst.findByType(FormGroup);
});
test('group with controlId="csv"', () => {
expect(formGroup.props().controlId).toEqual('csv');
expect(formGroup.props.controlId).toEqual('csv');
});
test('file control with onChange from handleFileInputChange', () => {
const control = el.find(FormControl);
const control = inst.findByType(FormControl);
expect(
control.props().onChange,
).toEqual(el.instance().handleFileInputChange);
control.props.onChange,
).toEqual(el.getInstance().handleFileInputChange);
});
test('fileInputRef points to control', () => {
expect(el.find(FormControl).getElement().ref).toBe(el.instance().fileInputRef);
expect(
// eslint-disable-next-line no-underscore-dangle
inst.findByType(FormControl)._fiber.ref,
).toEqual(el.getInstance().fileInputRef);
});
});
});
describe('import button', () => {
let btn;
beforeEach(() => {
btn = el.find(Button);
btn = inst.findByType(Button);
});
test('handleClickImportGrade on click', () => {
expect(btn.props().onClick).toEqual(el.instance().handleClickImportGrades);
expect(btn.props.onClick).toEqual(el.getInstance().handleClickImportGrades);
});
test('text from messages.importBtn', () => {
expect(btn.children().text()).toEqual(messages.importBtnText);
const messageEl = btn.findByType(FormattedMessage);
expect(messageEl.props).toEqual(messages.importBtnText);
});
});
});
describe('fileInput helper', () => {
test('links to fileInputRef.current', () => {
el = mount(<FileUploadForm {...props} />);
const ref = 'a-fake-ref';
el.instance().fileInputRef = { current: ref };
expect(el.instance().fileInput).toEqual(ref);
el = TestRenderer.create(
<FileUploadForm {...props} />,
{ createNodeMock: () => mockRef },
);
expect(el.getInstance().fileInput).not.toEqual(undefined);
expect(el.getInstance().fileInput).toEqual(el.getInstance().fileInputRef.current);
});
});
describe('behavior', () => {
let fileInput;
beforeEach(() => {
el = mount(<FileUploadForm {...props} />);
fileInput = jest.spyOn(el.instance(), 'fileInput', 'get');
el = TestRenderer.create(
<FileUploadForm {...props} />,
{ createNodeMock: () => mockRef },
);
fileInput = jest.spyOn(el.getInstance(), 'fileInput', 'get');
});
describe('handleFileInputChange', () => {
it('does nothing (does not fail) if fileInput has not loaded', () => {
fileInput.mockReturnValue(null);
el.instance().handleClickImportGrades();
el.getInstance().handleClickImportGrades();
expect(mockRef.click).not.toHaveBeenCalled();
});
it('calls fileInput.click if is loaded', () => {
const click = jest.fn();
fileInput.mockReturnValue({ click });
el.instance().handleClickImportGrades();
expect(click).toHaveBeenCalled();
el.getInstance().handleClickImportGrades();
expect(mockRef.click).toHaveBeenCalled();
});
});
describe('handleClickImportGrades', () => {
it('does nothing if file input has not loaded with files', () => {
fileInput.mockReturnValue(null);
el.instance().handleFileInputChange();
el.getInstance().handleFileInputChange();
expect(props.submitFileUploadFormData).not.toHaveBeenCalled();
fileInput.mockReturnValue({ files: [] });
el.instance().handleFileInputChange();
el.getInstance().handleFileInputChange();
expect(props.submitFileUploadFormData).not.toHaveBeenCalled();
});
it('calls submitFileUploadFormData and then clears fileInput if has files', () => {
fileInput.mockReturnValue({ files: ['some', 'files'], value: 'a value' });
const formData = { fake: 'form data' };
jest.spyOn(el.instance(), 'formData', 'get').mockReturnValue(formData);
jest.spyOn(el.getInstance(), 'formData', 'get').mockReturnValue(formData);
const submit = jest.fn(() => ({ then: (thenCB) => { thenCB(); } }));
el.setProps({
submitFileUploadFormData: submit,
});
el.instance().handleFileInputChange();
el.update(<FileUploadForm {...props} submitFileUploadFormData={submit} />);
el.getInstance().handleFileInputChange();
expect(submit).toHaveBeenCalledWith(formData);
expect(el.instance().fileInput.value).toEqual(null);
expect(el.getInstance().fileInput.value).toEqual(null);
});
});
describe('formData', () => {
@@ -169,7 +187,7 @@ describe('FileUploadForm', () => {
fileInput.mockReturnValue({ files: [file], value });
const expected = new FormData();
expected.append('csv', file);
expect([...el.instance().formData.entries()]).toEqual([...expected.entries()]);
expect([...el.getInstance().formData.entries()]).toEqual([...expected.entries()]);
});
});
});

View File

@@ -3,11 +3,14 @@ import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import { Table } from '@edx/paragon';
import { bulkManagementColumns, messages } from 'data/constants/app';
import { bulkManagementColumns } from 'data/constants/app';
import selectors from 'data/selectors';
import ResultsSummary from './ResultsSummary';
import messages from './messages';
export const mapHistoryRows = ({
resultsSummary,
@@ -21,19 +24,19 @@ export const mapHistoryRows = ({
...rest,
});
const { hints } = messages.BulkManagementTab;
/**
* <HistoryTable />
* Table with history of bulk management uploads, including a results summary which
* displays total, skipped, and failed uploads
*/
export const HistoryTable = ({ bulkManagementHistory }) => (
export const HistoryTable = ({
bulkManagementHistory,
}) => (
<>
<p>
{hints[0]}
<FormattedMessage {...messages.hint1} />
<br />
{hints[1]}
<FormattedMessage {...messages.hint2} />
</p>
<Table

View File

@@ -2,13 +2,19 @@
import React from 'react';
import { shallow } from 'enzyme';
import { Table } from '@edx/paragon';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import selectors from 'data/selectors';
import { bulkManagementColumns, messages } from 'data/constants/app';
import { bulkManagementColumns } from 'data/constants/app';
import ResultsSummary from './ResultsSummary';
import { HistoryTable, mapStateToProps } from './HistoryTable';
import messages from './messages';
jest.mock('@edx/frontend-platform/i18n', () => ({
defineMessages: m => m,
FormattedMessage: () => 'FormattedMessage',
}));
jest.mock('@edx/paragon', () => ({
Table: () => 'Table',
}));
@@ -61,9 +67,9 @@ describe('HistoryTable', () => {
});
test('hints with break in between', () => {
const hints = el.find('p');
expect(hints.childAt(0).text()).toEqual(messages.BulkManagementTab.hints[0]);
expect(hints.childAt(0).getElement()).toEqual(<FormattedMessage {...messages.hint1} />);
expect(hints.childAt(1).is('br')).toEqual(true);
expect(hints.childAt(2).text()).toEqual(messages.BulkManagementTab.hints[1]);
expect(hints.childAt(2).getElement()).toEqual(<FormattedMessage {...messages.hint2} />);
});
describe('history table', () => {
let table;

View File

@@ -12,7 +12,11 @@ exports[`BulkManagementAlerts component no errer, no upload success snapshot - b
show={false}
variant="success"
>
CSV processing. File uploads may take several minutes to complete.
<FormattedMessage
defaultMessage="CSV processing. File uploads may take several minutes to complete."
description="Success Dialog message in BulkManagement Tab File Upload Form"
id="gradebook.BulkManagementTab.successDialog"
/>
</Alert>
</Fragment>
`;
@@ -31,7 +35,11 @@ exports[`BulkManagementAlerts component no errer, no upload success snapshot - d
show={true}
variant="success"
>
CSV processing. File uploads may take several minutes to complete.
<FormattedMessage
defaultMessage="CSV processing. File uploads may take several minutes to complete."
description="Success Dialog message in BulkManagement Tab File Upload Form"
id="gradebook.BulkManagementTab.successDialog"
/>
</Alert>
</Fragment>
`;

View File

@@ -16,7 +16,13 @@ exports[`FileUploadForm component snapshot snapshot - loads export form w/ alert
<ForwardRef
as="input"
className="d-none"
label="Upload Grade CSV"
label={
<FormattedMessage
defaultMessage="Upload Grade CSV"
description="Button in BulkManagementTab Alerts"
id="gradebook.BulkManagementTab.csvUploadLabel"
/>
}
onChange={[MockFunction this.handleFileInputChange]}
plaintext={false}
type="file"
@@ -29,7 +35,11 @@ exports[`FileUploadForm component snapshot snapshot - loads export form w/ alert
onClick={[MockFunction this.handleClickImportGrades]}
variant="primary"
>
Import Grades
<FormattedMessage
defaultMessage="Import Grades"
description="Button in BulkManagement Tab File Upload Form"
id="gradebook.BulkManagementTab.importBtnText"
/>
</ForwardRef>
</React.Fragment>
`;

View File

@@ -44,9 +44,17 @@ Array [
exports[`HistoryTable component snapshot snapshot - loads hints display, formatted table 1`] = `
<Fragment>
<p>
Results appear in the table below.
<FormattedMessage
defaultMessage="Results appear in the table below."
description="Hint text on BulkManagement Tab History Table"
id="gradebook.BulkManagementTab.hint1"
/>
<br />
Grade processing may take a few seconds.
<FormattedMessage
defaultMessage="Grade processing may take a few seconds."
description="Hint text on BulkManagement Tab History Table"
id="gradebook.BulkManagementTab.hint2"
/>
</p>
<Table
className="table-striped"

View File

@@ -3,7 +3,11 @@
exports[`BulkManagementTab component snapshot snapshot - loads heading from messages.BulkManagementTab.heading, <BulkManagementAlerts />, <FileUploadForm />, <HistoryTable /> 1`] = `
<div>
<h4>
Use this feature by downloading a CSV for bulk management, overriding grades locally, and coming back here to upload.
<FormattedMessage
defaultMessage="Use this feature by downloading a CSV for bulk management, overriding grades locally, and coming back here to upload."
description="Heading text for BulkManagement Tab"
id="gradebook.BulkManagementTab.heading"
/>
</h4>
<BulkManagementAlerts />
<FileUploadForm />

View File

@@ -1,7 +1,8 @@
/* eslint-disable react/button-has-type, import/no-named-as-default */
import React from 'react';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import { messages } from 'data/constants/app';
import messages from './messages';
import BulkManagementAlerts from './BulkManagementAlerts';
import FileUploadForm from './FileUploadForm';
import HistoryTable from './HistoryTable';
@@ -12,7 +13,7 @@ import HistoryTable from './HistoryTable';
*/
export const BulkManagementTab = () => (
<div>
<h4>{messages.BulkManagementTab.heading}</h4>
<h4><FormattedMessage {...(messages.heading)} /></h4>
<BulkManagementAlerts />
<FileUploadForm />
<HistoryTable />

View File

@@ -1,12 +1,13 @@
/* eslint-disable import/no-named-as-default */
import React from 'react';
import { shallow } from 'enzyme';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import { messages } from 'data/constants/app';
import { BulkManagementTab } from '.';
import BulkManagementAlerts from './BulkManagementAlerts';
import FileUploadForm from './FileUploadForm';
import HistoryTable from './HistoryTable';
import messages from './messages';
jest.mock('./BulkManagementAlerts', () => 'BulkManagementAlerts');
jest.mock('./FileUploadForm', () => 'FileUploadForm');
@@ -30,7 +31,11 @@ describe('BulkManagementTab', () => {
});
test('heading - h4 loaded from messages', () => {
const heading = el.find('h4');
expect(heading.text()).toEqual(messages.BulkManagementTab.heading);
expect(heading.getElement()).toEqual((
<h4>
<FormattedMessage {...messages.heading} />
</h4>
));
});
test('heading, then alerts, then upload form, then table', () => {
expect(el.childAt(0).is('h4')).toEqual(true);

View File

@@ -0,0 +1,36 @@
import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
csvUploadLabel: {
id: 'gradebook.BulkManagementTab.csvUploadLabel',
defaultMessage: 'Upload Grade CSV',
description: 'Button in BulkManagementTab Alerts',
},
heading: {
id: 'gradebook.BulkManagementTab.heading',
defaultMessage: 'Use this feature by downloading a CSV for bulk management, overriding grades locally, and coming back here to upload.',
description: 'Heading text for BulkManagement Tab',
},
hint1: {
id: 'gradebook.BulkManagementTab.hint1',
defaultMessage: 'Results appear in the table below.',
description: 'Hint text on BulkManagement Tab History Table',
},
hint2: {
id: 'gradebook.BulkManagementTab.hint2',
defaultMessage: 'Grade processing may take a few seconds.',
description: 'Hint text on BulkManagement Tab History Table',
},
importBtnText: {
id: 'gradebook.BulkManagementTab.importBtnText',
defaultMessage: 'Import Grades',
description: 'Button in BulkManagement Tab File Upload Form',
},
successDialog: {
id: 'gradebook.BulkManagementTab.successDialog',
defaultMessage: 'CSV processing. File uploads may take several minutes to complete.',
description: 'Success Dialog message in BulkManagement Tab File Upload Form',
},
});
export default messages;

View File

@@ -7,7 +7,13 @@ exports[`AssignmentFilter Component snapshots basic snapshot 1`] = `
<SelectGroup
disabled={false}
id="assignment"
label="Assignment"
label={
<FormattedMessage
defaultMessage="Assignment"
description="Assignment filter select label in Gradebook Filters"
id="gradebook.GradebookFilters.assignmentFilterLabel"
/>
}
onChange={[MockFunction handleChange]}
options={
Array [

View File

@@ -3,10 +3,13 @@ import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import selectors from 'data/selectors';
import actions from 'data/actions';
import thunkActions from 'data/thunkActions';
import messages from '../messages';
import SelectGroup from '../SelectGroup';
const { fetchGradesIfAssignmentGradeFiltersSet } = thunkActions.grades;
@@ -46,7 +49,7 @@ export class AssignmentFilter extends React.Component {
<div className="student-filters">
<SelectGroup
id="assignment"
label="Assignment"
label={<FormattedMessage {...messages.assignment} />}
value={this.props.selectedAssignment}
onChange={this.handleChange}
disabled={this.props.assignmentFilterOptions.length === 0}
@@ -64,7 +67,6 @@ AssignmentFilter.defaultProps = {
AssignmentFilter.propTypes = {
updateQueryParams: PropTypes.func.isRequired,
// redux
assignmentFilterOptions: PropTypes.arrayOf(PropTypes.shape({
label: PropTypes.string,

View File

@@ -7,14 +7,26 @@ exports[`AssignmentGradeFilter Component snapshots buttons and groups disabled i
<PercentGroup
disabled={true}
id="assignmentGradeMin"
label="Min Grade"
label={
<FormattedMessage
defaultMessage="Min Grade"
description="Min-grade filter select label in Gradebook Filters"
id="gradebook.GradebookFilters.minGradeFilterLabel"
/>
}
onChange={[MockFunction handleSetMin]}
value="2"
/>
<PercentGroup
disabled={true}
id="assignmentGradeMax"
label="Max Grade"
label={
<FormattedMessage
defaultMessage="Max Grade"
description="Max-grade filter select label in Gradebook Filters"
id="gradebook.GradebookFilters.maxGradeFilterLabel"
/>
}
onChange={[MockFunction handleSetMax]}
value="98"
/>
@@ -42,14 +54,26 @@ exports[`AssignmentGradeFilter Component snapshots smoke test 1`] = `
<PercentGroup
disabled={false}
id="assignmentGradeMin"
label="Min Grade"
label={
<FormattedMessage
defaultMessage="Min Grade"
description="Min-grade filter select label in Gradebook Filters"
id="gradebook.GradebookFilters.minGradeFilterLabel"
/>
}
onChange={[MockFunction handleSetMin]}
value="2"
/>
<PercentGroup
disabled={false}
id="assignmentGradeMax"
label="Max Grade"
label={
<FormattedMessage
defaultMessage="Max Grade"
description="Max-grade filter select label in Gradebook Filters"
id="gradebook.GradebookFilters.maxGradeFilterLabel"
/>
}
onChange={[MockFunction handleSetMax]}
value="98"
/>

View File

@@ -3,12 +3,14 @@ import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import { Button } from '@edx/paragon';
import selectors from 'data/selectors';
import actions from 'data/actions';
import thunkActions from 'data/thunkActions';
import messages from '../messages';
import PercentGroup from '../PercentGroup';
export class AssignmentGradeFilter extends React.Component {
@@ -34,19 +36,21 @@ export class AssignmentGradeFilter extends React.Component {
}
render() {
const { assignmentGradeMin, assignmentGradeMax } = this.props.localAssignmentLimits;
const {
localAssignmentLimits: { assignmentGradeMax, assignmentGradeMin },
} = this.props;
return (
<div className="grade-filter-inputs">
<PercentGroup
id="assignmentGradeMin"
label="Min Grade"
label={<FormattedMessage {...messages.minGrade} />}
value={assignmentGradeMin}
disabled={!this.props.selectedAssignment}
onChange={this.handleSetMin}
/>
<PercentGroup
id="assignmentGradeMax"
label="Max Grade"
label={<FormattedMessage {...messages.maxGrade} />}
value={assignmentGradeMax}
disabled={!this.props.selectedAssignment}
onChange={this.handleSetMax}

View File

@@ -7,7 +7,13 @@ exports[`AssignmentTypeFilter Component snapshots SelectGroup disabled if no ass
<SelectGroup
disabled={true}
id="assignment-types"
label="Assignment Types"
label={
<FormattedMessage
defaultMessage="Assignment Types"
description="Assignment Types filter select label in Gradebook Filters"
id="gradebook.GradebookFilters.assignmentTypesLabel"
/>
}
onChange={[MockFunction handleChange]}
options={
Array [
@@ -40,7 +46,13 @@ exports[`AssignmentTypeFilter Component snapshots smoke test 1`] = `
<SelectGroup
disabled={false}
id="assignment-types"
label="Assignment Types"
label={
<FormattedMessage
defaultMessage="Assignment Types"
description="Assignment Types filter select label in Gradebook Filters"
id="gradebook.GradebookFilters.assignmentTypesLabel"
/>
}
onChange={[MockFunction handleChange]}
options={
Array [

View File

@@ -3,9 +3,13 @@ import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import selectors from 'data/selectors';
import actions from 'data/actions';
import SelectGroup from '../SelectGroup';
import messages from '../messages';
export class AssignmentTypeFilter extends React.Component {
constructor(props) {
@@ -34,7 +38,7 @@ export class AssignmentTypeFilter extends React.Component {
<div className="student-filters">
<SelectGroup
id="assignment-types"
label="Assignment Types"
label={<FormattedMessage {...messages.assignmentTypes} />}
value={this.props.selectedAssignmentType}
onChange={this.handleChange}
disabled={this.props.assignmentFilterOptions.length === 0}

View File

@@ -7,13 +7,25 @@ exports[`CourseGradeFilter Component snapshots basic snapshot 1`] = `
>
<PercentGroup
id="minimum-grade"
label="Min Grade"
label={
<FormattedMessage
defaultMessage="Min Grade"
description="Min-grade filter select label in Gradebook Filters"
id="gradebook.GradebookFilters.minGradeFilterLabel"
/>
}
onChange={[MockFunction handleUpdateMin]}
value="5"
/>
<PercentGroup
id="maximum-grade"
label="Max Grade"
label={
<FormattedMessage
defaultMessage="Max Grade"
description="Max-grade filter select label in Gradebook Filters"
id="gradebook.GradebookFilters.maxGradeFilterLabel"
/>
}
onChange={[MockFunction handleUpdateMax]}
value="92"
/>

View File

@@ -2,13 +2,15 @@
import React from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import {
Button,
} from '@edx/paragon';
import { Button } from '@edx/paragon';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import selectors from 'data/selectors';
import actions from 'data/actions';
import thunkActions from 'data/thunkActions';
import messages from '../messages';
import PercentGroup from '../PercentGroup';
export class CourseGradeFilter extends React.Component {
@@ -41,19 +43,21 @@ export class CourseGradeFilter extends React.Component {
}
render() {
const { courseGradeMin, courseGradeMax } = this.props.localCourseLimits;
const {
localCourseLimits: { courseGradeMin, courseGradeMax },
} = this.props;
return (
<>
<div className="grade-filter-inputs">
<PercentGroup
id="minimum-grade"
label="Min Grade"
label={<FormattedMessage {...messages.minGrade} />}
value={courseGradeMin}
onChange={this.handleUpdateMin}
/>
<PercentGroup
id="maximum-grade"
label="Max Grade"
label={<FormattedMessage {...messages.maxGrade} />}
value={courseGradeMax}
onChange={this.handleUpdateMax}
/>

View File

@@ -30,7 +30,7 @@ PercentGroup.defaultProps = {
};
PercentGroup.propTypes = {
id: PropTypes.string.isRequired,
label: PropTypes.string.isRequired,
label: PropTypes.node.isRequired,
value: PropTypes.string.isRequired,
onChange: PropTypes.func.isRequired,
disabled: PropTypes.bool,

View File

@@ -23,7 +23,7 @@ const SelectGroup = ({
);
SelectGroup.propTypes = {
id: PropTypes.string.isRequired,
label: PropTypes.string.isRequired,
label: PropTypes.node.isRequired,
value: PropTypes.string.isRequired,
onChange: PropTypes.func.isRequired,
disabled: PropTypes.bool,

View File

@@ -3,10 +3,13 @@ import React from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import actions from 'data/actions';
import selectors from 'data/selectors';
import thunkActions from 'data/thunkActions';
import messages from '../messages';
import SelectGroup from '../SelectGroup';
export const optionFactory = ({ data, defaultOption, key }) => [
@@ -28,7 +31,7 @@ export class StudentGroupsFilter extends React.Component {
mapCohortsEntries() {
return optionFactory({
data: this.props.cohorts,
defaultOption: 'Cohort-All',
defaultOption: this.translate(messages.cohortAll),
key: 'id',
});
}
@@ -36,7 +39,7 @@ export class StudentGroupsFilter extends React.Component {
mapTracksEntries() {
return optionFactory({
data: this.props.tracks,
defaultOption: 'Track-All',
defaultOption: this.translate(messages.trackAll),
key: 'slug',
});
}
@@ -65,19 +68,23 @@ export class StudentGroupsFilter extends React.Component {
this.props.fetchGrades();
}
translate(message) {
return this.props.intl.formatMessage(message);
}
render() {
return (
<>
<SelectGroup
id="Tracks"
label="Tracks"
label={this.translate(messages.tracks)}
value={this.props.selectedTrackEntry.name}
onChange={this.updateTracks}
options={this.mapTracksEntries()}
/>
<SelectGroup
id="Cohorts"
label="Cohorts"
label={this.translate(messages.cohorts)}
value={this.props.selectedCohortEntry.name}
disabled={this.props.cohorts.length === 0}
onChange={this.updateCohorts}
@@ -100,6 +107,9 @@ StudentGroupsFilter.defaultProps = {
StudentGroupsFilter.propTypes = {
updateQueryParams: PropTypes.func.isRequired,
// injected
intl: intlShape.isRequired,
// redux
cohorts: PropTypes.arrayOf(PropTypes.shape({
name: PropTypes.string,
@@ -139,4 +149,4 @@ export const mapDispatchToProps = {
updateTrack: actions.filters.update.track,
};
export default connect(mapStateToProps, mapDispatchToProps)(StudentGroupsFilter);
export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(StudentGroupsFilter));

View File

@@ -63,6 +63,7 @@ describe('StudentGroupsFilter', () => {
beforeEach(() => {
props = {
...props,
intl: { formatMessage: (msg) => msg.defaultMessage },
cohortsByName: {
[props.cohorts[0].name]: props.cohorts[0],
[props.cohorts[1].name]: props.cohorts[1],

View File

@@ -22,7 +22,13 @@ exports[`GradebookFilters Component snapshots basic snapshot 1`] = `
<Collapsible
className="filter-group mb-3"
defaultOpen={true}
title="Assignments"
title={
<FormattedMessage
defaultMessage="Assignments"
description="Assignment filter group label in Gradebook Filters"
id="gradebook.GradebookFilters.assignmentsFilterLabel"
/>
}
>
<div>
<Connect(AssignmentTypeFilter)
@@ -39,7 +45,13 @@ exports[`GradebookFilters Component snapshots basic snapshot 1`] = `
<Collapsible
className="filter-group mb-3"
defaultOpen={true}
title="Overall Grade"
title={
<FormattedMessage
defaultMessage="Overall Grade"
description="Overall Grade filter group label in Gradebook Filters"
id="gradebook.GradebookFilters.overallGradeFilterLabel"
/>
}
>
<Connect(CourseGradeFilter)
updateQueryParams={[MockFunction]}
@@ -48,22 +60,38 @@ exports[`GradebookFilters Component snapshots basic snapshot 1`] = `
<Collapsible
className="filter-group mb-3"
defaultOpen={true}
title="Student Groups"
title={
<FormattedMessage
defaultMessage="Student Groups"
description="Student Groups filter group label in Gradebook Filters"
id="gradebook.GradebookFilters.studentGroupsFilterLabel"
/>
}
>
<Connect(StudentGroupsFilter)
<InjectIntl(ShimmedIntlComponent)
updateQueryParams={[MockFunction]}
/>
</Collapsible>
<Collapsible
className="filter-group mb-3"
defaultOpen={true}
title="Include Course Team Members"
title={
<FormattedMessage
defaultMessage="Include Course Team Members"
description="Include Course Team Members filter label in Gradebook Filters"
id="gradebook.GradebookFilters.includeCourseTeamMembersFilterLabel"
/>
}
>
<Checkbox
checked={true}
onChange={[MockFunction handleIncludeTeamMembersChange]}
>
Include Course Team Members
<FormattedMessage
defaultMessage="Include Course Team Members"
description="Include Course Team Members filter label in Gradebook Filters"
id="gradebook.GradebookFilters.includeCourseTeamMembersFilterLabel"
/>
</Checkbox>
</Collapsible>
</React.Fragment>

View File

@@ -10,11 +10,13 @@ import {
Form,
} from '@edx/paragon';
import { Close } from '@edx/paragon/icons';
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import actions from 'data/actions';
import selectors from 'data/selectors';
import thunkActions from 'data/thunkActions';
import messages from './messages';
import AssignmentTypeFilter from './AssignmentTypeFilter';
import AssignmentFilter from './AssignmentFilter';
import AssignmentGradeFilter from './AssignmentGradeFilter';
@@ -39,13 +41,18 @@ export class GradebookFilters extends React.Component {
}
collapsibleGroup = (title, content) => (
<Collapsible title={title} defaultOpen className="filter-group mb-3">
<Collapsible
title={<FormattedMessage {...title} />}
defaultOpen
className="filter-group mb-3"
>
{content}
</Collapsible>
);
render() {
const {
intl,
updateQueryParams,
} = this.props;
return (
@@ -57,12 +64,12 @@ export class GradebookFilters extends React.Component {
onClick={this.props.closeMenu}
iconAs={Icon}
src={Close}
alt="Close Filters"
aria-label="Close Filters"
alt={intl.formatMessage(messages.closeFilters)}
aria-label={intl.formatMessage(messages.closeFilters)}
/>
</div>
{this.collapsibleGroup('Assignments', (
{this.collapsibleGroup(messages.assignments, (
<div>
<AssignmentTypeFilter updateQueryParams={updateQueryParams} />
<AssignmentFilter updateQueryParams={updateQueryParams} />
@@ -70,20 +77,20 @@ export class GradebookFilters extends React.Component {
</div>
))}
{this.collapsibleGroup('Overall Grade', (
{this.collapsibleGroup(messages.overallGrade, (
<CourseGradeFilter updateQueryParams={updateQueryParams} />
))}
{this.collapsibleGroup('Student Groups', (
{this.collapsibleGroup(messages.studentGroups, (
<StudentGroupsFilter updateQueryParams={updateQueryParams} />
))}
{this.collapsibleGroup('Include Course Team Members', (
{this.collapsibleGroup(messages.includeCourseTeamMembers, (
<Form.Checkbox
checked={this.state.includeCourseRoleMembers}
onChange={this.handleIncludeTeamMembersChange}
>
Include Course Team Members
<FormattedMessage {...messages.includeCourseTeamMembers} />
</Form.Checkbox>
))}
</>
@@ -95,6 +102,8 @@ GradebookFilters.defaultProps = {
};
GradebookFilters.propTypes = {
updateQueryParams: PropTypes.func.isRequired,
// injected
intl: intlShape.isRequired,
// redux
closeMenu: PropTypes.func.isRequired,
fetchGrades: PropTypes.func.isRequired,
@@ -112,4 +121,4 @@ export const mapDispatchToProps = {
updateIncludeCourseRoleMembers: actions.filters.update.includeCourseRoleMembers,
};
export default connect(mapStateToProps, mapDispatchToProps)(GradebookFilters);
export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(GradebookFilters));

View File

@@ -0,0 +1,71 @@
import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
assignments: {
id: 'gradebook.GradebookFilters.assignmentsFilterLabel',
defaultMessage: 'Assignments',
description: 'Assignment filter group label in Gradebook Filters',
},
overallGrade: {
id: 'gradebook.GradebookFilters.overallGradeFilterLabel',
defaultMessage: 'Overall Grade',
description: 'Overall Grade filter group label in Gradebook Filters',
},
studentGroups: {
id: 'gradebook.GradebookFilters.studentGroupsFilterLabel',
defaultMessage: 'Student Groups',
description: 'Student Groups filter group label in Gradebook Filters',
},
includeCourseTeamMembers: {
id: 'gradebook.GradebookFilters.includeCourseTeamMembersFilterLabel',
defaultMessage: 'Include Course Team Members',
description: 'Include Course Team Members filter label in Gradebook Filters',
},
assignment: {
id: 'gradebook.GradebookFilters.assignmentFilterLabel',
defaultMessage: 'Assignment',
description: 'Assignment filter select label in Gradebook Filters',
},
assignmentTypes: {
id: 'gradebook.GradebookFilters.assignmentTypesLabel',
defaultMessage: 'Assignment Types',
description: 'Assignment Types filter select label in Gradebook Filters',
},
maxGrade: {
id: 'gradebook.GradebookFilters.maxGradeFilterLabel',
defaultMessage: 'Max Grade',
description: 'Max-grade filter select label in Gradebook Filters',
},
minGrade: {
id: 'gradebook.GradebookFilters.minGradeFilterLabel',
defaultMessage: 'Min Grade',
description: 'Min-grade filter select label in Gradebook Filters',
},
cohorts: {
id: 'gradebook.GradebookFilters.cohorts',
defaultMessage: 'Cohorts',
description: 'Cohorts filter select label in Gradebook Filters',
},
cohortAll: {
id: 'gradebook.GradebookFilters.cohortsAll',
defaultMessage: 'Cohort-All',
description: 'Cohorts filter select default in Gradebook Filters',
},
tracks: {
id: 'gradebook.GradebookFilters.tracks',
defaultMessage: 'Tracks',
description: 'Tracks filter select label in Gradebook Filters',
},
trackAll: {
id: 'gradebook.GradebookFilters.trackAll',
defaultMessage: 'Track-All',
description: 'Tracks filter select default in Gradebook Filters',
},
closeFilters: {
id: 'gradebook.GradebookFilters.closeFilters',
defaultMessage: 'Close Filters',
description: 'Button label for Close button in Gradebook Filters',
},
});
export default messages;

View File

@@ -46,6 +46,7 @@ describe('GradebookFilters', () => {
beforeEach(() => {
props = {
...props,
intl: { formatMessage: (msg) => msg.defaultMessage },
closeMenu: jest.fn().mockName('this.props.closeMenu'),
fetchGrades: jest.fn(),
updateIncludeCourseRoleMembers: jest.fn(),

View File

@@ -13,20 +13,31 @@ exports[`GradebookHeader component snapshots default values (grades frozen, cann
>
&lt;&lt;
</span>
Back to Dashboard
<FormattedMessage
defaultMessage="Back to Dashboard"
description="Button text to take user back to LMS dashboard in Gradebook Header"
id="gradebook.GradebookHeader.backButton"
/>
</a>
<h1>
Gradebook
<FormattedMessage
defaultMessage="Gradebook"
description="Top-level app title in Gradebook Header component"
id="gradebook.GradebookHeader.appLabel"
/>
</h1>
<h3>
fakeID
</h3>
<div
className="alert alert-warning"
role="alert"
>
You are not authorized to view the gradebook for this course.
<FormattedMessage
defaultMessage="You are not authorized to view the gradebook for this course."
description="Warning message in Gradebook Header when user is not allowed to view the app"
id="gradebook.GradebookHeader.unauthorizedWarning"
/>
</div>
</div>
`;
@@ -44,20 +55,31 @@ exports[`GradebookHeader component snapshots grades frozen, can view. grades fro
>
&lt;&lt;
</span>
Back to Dashboard
<FormattedMessage
defaultMessage="Back to Dashboard"
description="Button text to take user back to LMS dashboard in Gradebook Header"
id="gradebook.GradebookHeader.backButton"
/>
</a>
<h1>
Gradebook
<FormattedMessage
defaultMessage="Gradebook"
description="Top-level app title in Gradebook Header component"
id="gradebook.GradebookHeader.appLabel"
/>
</h1>
<h3>
fakeID
</h3>
<div
className="alert alert-warning"
role="alert"
>
The grades for this course are now frozen. Editing of grades is no longer allowed.
<FormattedMessage
defaultMessage="The grades for this course are now frozen. Editing of grades is no longer allowed."
description="Warning message in Gradebook Header for frozen messages"
id="gradebook.GradebookHeader.frozenWarning"
/>
</div>
</div>
`;
@@ -75,26 +97,41 @@ exports[`GradebookHeader component snapshots grades frozen, cannot view unauthor
>
&lt;&lt;
</span>
Back to Dashboard
<FormattedMessage
defaultMessage="Back to Dashboard"
description="Button text to take user back to LMS dashboard in Gradebook Header"
id="gradebook.GradebookHeader.backButton"
/>
</a>
<h1>
Gradebook
<FormattedMessage
defaultMessage="Gradebook"
description="Top-level app title in Gradebook Header component"
id="gradebook.GradebookHeader.appLabel"
/>
</h1>
<h3>
fakeID
</h3>
<div
className="alert alert-warning"
role="alert"
>
The grades for this course are now frozen. Editing of grades is no longer allowed.
<FormattedMessage
defaultMessage="The grades for this course are now frozen. Editing of grades is no longer allowed."
description="Warning message in Gradebook Header for frozen messages"
id="gradebook.GradebookHeader.frozenWarning"
/>
</div>
<div
className="alert alert-warning"
role="alert"
>
You are not authorized to view the gradebook for this course.
<FormattedMessage
defaultMessage="You are not authorized to view the gradebook for this course."
description="Warning message in Gradebook Header when user is not allowed to view the app"
id="gradebook.GradebookHeader.unauthorizedWarning"
/>
</div>
</div>
`;

View File

@@ -2,9 +2,13 @@ import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import { configuration } from 'config';
import selectors from 'data/selectors';
import messages from './messages';
export class GradebookHeader extends React.Component {
lmsInstructorDashboardUrl = courseId => (
`${configuration.LMS_BASE_URL}/courses/${courseId}/instructor`
@@ -17,19 +21,22 @@ export class GradebookHeader extends React.Component {
href={this.lmsInstructorDashboardUrl(this.props.courseId)}
className="mb-3"
>
<span aria-hidden="true">{'<< '}</span> Back to Dashboard
<span aria-hidden="true">{'<< '}</span>
<FormattedMessage {...messages.backToDashboard} />
</a>
<h1>Gradebook</h1>
<h3> {this.props.courseId}</h3>
<h1>
<FormattedMessage {...messages.gradebook} />
</h1>
<h3>{this.props.courseId}</h3>
{this.props.areGradesFrozen
&& (
<div className="alert alert-warning" role="alert">
The grades for this course are now frozen. Editing of grades is no longer allowed.
<FormattedMessage {...messages.frozenWarning} />
</div>
)}
{(this.props.canUserViewGradebook === false) && (
<div className="alert alert-warning" role="alert">
You are not authorized to view the gradebook for this course.
<FormattedMessage {...messages.unauthorizedWarning} />
</div>
)}
</div>

View File

@@ -0,0 +1,26 @@
import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
backToDashboard: {
id: 'gradebook.GradebookHeader.backButton',
defaultMessage: 'Back to Dashboard',
description: 'Button text to take user back to LMS dashboard in Gradebook Header',
},
gradebook: {
id: 'gradebook.GradebookHeader.appLabel',
defaultMessage: 'Gradebook',
description: 'Top-level app title in Gradebook Header component',
},
frozenWarning: {
id: 'gradebook.GradebookHeader.frozenWarning',
defaultMessage: 'The grades for this course are now frozen. Editing of grades is no longer allowed.',
description: 'Warning message in Gradebook Header for frozen messages',
},
unauthorizedWarning: {
id: 'gradebook.GradebookHeader.unauthorizedWarning',
defaultMessage: 'You are not authorized to view the gradebook for this course.',
description: 'Warning message in Gradebook Header when user is not allowed to view the app',
},
});
export default messages;

View File

@@ -4,6 +4,11 @@ import { shallow } from 'enzyme';
import selectors from 'data/selectors';
import { GradebookHeader, mapStateToProps } from '.';
jest.mock('@edx/frontend-platform/i18n', () => ({
defineMessages: messages => messages,
FormattedMessage: 'FormattedMessage',
}));
jest.mock('data/selectors', () => ({
__esModule: true,
default: {

View File

@@ -4,11 +4,14 @@ import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { StatefulButton, Icon } from '@edx/paragon';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import { StrictDict } from 'utils';
import actions from 'data/actions';
import selectors from 'data/selectors';
import messages from './messages';
export const basicButtonProps = () => ({
variant: 'outline-primary',
icons: {
@@ -63,11 +66,11 @@ export class BulkManagementControls extends React.Component {
return this.props.showBulkManagement && (
<div>
<StatefulButton
{...this.buttonProps('Bulk Management')}
{...this.buttonProps(<FormattedMessage {...messages.bulkManagement} />)}
onClick={this.handleClickExportGrades}
/>
<StatefulButton
{...this.buttonProps('Interventions')}
{...this.buttonProps(<FormattedMessage {...messages.interventions} />)}
onClick={this.handleClickDownloadInterventions}
/>
</div>

View File

@@ -19,7 +19,7 @@ HistoryHeader.defaultProps = {
};
HistoryHeader.propTypes = {
id: PropTypes.string.isRequired,
label: PropTypes.string.isRequired,
label: PropTypes.node.isRequired,
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
};

View File

@@ -2,7 +2,11 @@ import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import selectors from 'data/selectors';
import messages from './messages';
import HistoryHeader from './HistoryHeader';
/**
@@ -18,22 +22,22 @@ export const ModalHeaders = ({
<div>
<HistoryHeader
id="assignment"
label="Assignment"
label={<FormattedMessage {...messages.assignmentHeader} />}
value={modalState.assignmentName}
/>
<HistoryHeader
id="student"
label="Student"
label={<FormattedMessage {...messages.studentHeader} />}
value={modalState.updateUserName}
/>
<HistoryHeader
id="original-grade"
label="Original Grade"
label={<FormattedMessage {...messages.originalGradeHeader} />}
value={originalGrade}
/>
<HistoryHeader
id="current-grade"
label="Current Grade"
label={<FormattedMessage {...messages.currentGradeHeader} />}
value={currentGrade}
/>
</div>

View File

@@ -6,19 +6,35 @@ exports[`OverrideTable Component snapshots basic snapshot shows a row for each e
Array [
Object {
"key": "date",
"label": "Date",
"label": <FormattedMessage
defaultMessage="Date"
description="Edit Modal Override Table Date column header"
id="gradebook.GradesTab.EditModal.Overrides.dateHeader"
/>,
},
Object {
"key": "grader",
"label": "Grader",
"label": <FormattedMessage
defaultMessage="Grader"
description="Edit Modal Override Table Grader column header"
id="gradebook.GradesTab.EditModal.Overrides.graderHeader"
/>,
},
Object {
"key": "reason",
"label": "Reason",
"label": <FormattedMessage
defaultMessage="Reason"
description="Edit Modal Override Table Reason column header"
id="gradebook.GradesTab.EditModal.Overrides.reasonHeader"
/>,
},
Object {
"key": "adjustedGrade",
"label": "Adjusted grade",
"label": <FormattedMessage
defaultMessage="Adjusted grade"
description="Edit Modal Override Table Adjusted grade column header"
id="gradebook.GradesTab.EditModal.Overrides.adjustedGradeHeader"
/>,
},
]
}

View File

@@ -4,18 +4,15 @@ import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { Table } from '@edx/paragon';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import { gradeOverrideHistoryColumns as columns } from 'data/constants/app';
import selectors from 'data/selectors';
import messages from './messages';
import ReasonInput from './ReasonInput';
import AdjustedGradeInput from './AdjustedGradeInput';
const GRADE_OVERRIDE_HISTORY_COLUMNS = [
{ label: 'Date', key: 'date' },
{ label: 'Grader', key: 'grader' },
{ label: 'Reason', key: 'reason' },
{ label: 'Adjusted grade', key: 'adjustedGrade' },
];
/**
* <OverrideTable />
* Table containing previous grade override entries, and an "edit" row
@@ -31,7 +28,15 @@ export const OverrideTable = ({
}
return (
<Table
columns={GRADE_OVERRIDE_HISTORY_COLUMNS}
columns={[
{ label: <FormattedMessage {...messages.dateHeader} />, key: columns.date },
{ label: <FormattedMessage {...messages.graderHeader} />, key: columns.grader },
{ label: <FormattedMessage {...messages.reasonHeader} />, key: columns.reason },
{
label: <FormattedMessage {...messages.adjustedGradeHeader} />,
key: columns.adjustedGrade,
},
]}
data={[
...gradeOverrides,
{

View File

@@ -0,0 +1,26 @@
import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
adjustedGradeHeader: {
id: 'gradebook.GradesTab.EditModal.Overrides.adjustedGradeHeader',
defaultMessage: 'Adjusted grade',
description: 'Edit Modal Override Table Adjusted grade column header',
},
dateHeader: {
id: 'gradebook.GradesTab.EditModal.Overrides.dateHeader',
defaultMessage: 'Date',
description: 'Edit Modal Override Table Date column header',
},
graderHeader: {
id: 'gradebook.GradesTab.EditModal.Overrides.graderHeader',
defaultMessage: 'Grader',
description: 'Edit Modal Override Table Grader column header',
},
reasonHeader: {
id: 'gradebook.GradesTab.EditModal.Overrides.reasonHeader',
defaultMessage: 'Reason',
description: 'Edit Modal Override Table Reason column header',
},
});
export default messages;

View File

@@ -4,22 +4,46 @@ exports[`ModalHeaders Component snapshots gradeOverrideHistoryError is and empty
<div>
<HistoryHeader
id="assignment"
label="Assignment"
label={
<FormattedMessage
defaultMessage="Assignment"
description="Edit Modal Assignment header"
id="gradebook.GradesTab.EditModal.headers.assignment"
/>
}
value="Qwerty"
/>
<HistoryHeader
id="student"
label="Student"
label={
<FormattedMessage
defaultMessage="Student"
description="Edit Modal Student header"
id="gradebook.GradesTab.EditModal.headers.student"
/>
}
value="Uiop"
/>
<HistoryHeader
id="original-grade"
label="Original Grade"
label={
<FormattedMessage
defaultMessage="Original Grade"
description="Edit Modal Original Grade header"
id="gradebook.GradesTab.EditModal.headers.originalGrade"
/>
}
value={20}
/>
<HistoryHeader
id="current-grade"
label="Current Grade"
label={
<FormattedMessage
defaultMessage="Current Grade"
description="Edit Modal Current Grade header"
id="gradebook.GradesTab.EditModal.headers.currentGrade"
/>
}
value={2}
/>
</div>
@@ -29,22 +53,46 @@ exports[`ModalHeaders Component snapshots gradeOverrideHistoryError is empty and
<div>
<HistoryHeader
id="assignment"
label="Assignment"
label={
<FormattedMessage
defaultMessage="Assignment"
description="Edit Modal Assignment header"
id="gradebook.GradesTab.EditModal.headers.assignment"
/>
}
value="Qwerty"
/>
<HistoryHeader
id="student"
label="Student"
label={
<FormattedMessage
defaultMessage="Student"
description="Edit Modal Student header"
id="gradebook.GradesTab.EditModal.headers.student"
/>
}
value="Uiop"
/>
<HistoryHeader
id="original-grade"
label="Original Grade"
label={
<FormattedMessage
defaultMessage="Original Grade"
description="Edit Modal Original Grade header"
id="gradebook.GradesTab.EditModal.headers.originalGrade"
/>
}
value={20}
/>
<HistoryHeader
id="current-grade"
label="Current Grade"
label={
<FormattedMessage
defaultMessage="Current Grade"
description="Edit Modal Current Grade header"
id="gradebook.GradesTab.EditModal.headers.currentGrade"
/>
}
value={2}
/>
</div>

View File

@@ -13,10 +13,18 @@ exports[`EditMoal Component snapshots gradeOverrideHistoryError is and empty and
/>
<OverrideTable />
<div>
Showing most recent actions (max 5). To see more, please contact support.
<FormattedMessage
defaultMessage="Showing most recent actions (max 5). To see more, please contact support"
description="Edit Modal visibility hint message"
id="gradebook.GradesTab.EditModal.contactSupport"
/>
</div>
<div>
Note: Once you save, your changes will be visible to students.
<FormattedMessage
defaultMessage="Note: Once you save, your changes will be visible to students."
description="Edit Modal saved changes effect hint"
id="gradebook.GradesTab.EditModal.saveVisibility"
/>
</div>
</div>
}
@@ -26,14 +34,30 @@ exports[`EditMoal Component snapshots gradeOverrideHistoryError is and empty and
onClick={[MockFunction this.handleAdjustedGradeClick]}
variant="primary"
>
Save Grade
<FormattedMessage
defaultMessage="Save Grades"
description="Edit Modal Save button label"
id="gradebook.GradesTab.EditModal.saveGrade"
/>
</Button>,
]
}
closeText="Cancel"
closeText={
<FormattedMessage
defaultMessage="Cancel"
description="Edit Modal close button text"
id="gradebook.GradesTab.EditModal.closeText"
/>
}
onClose={[MockFunction this.closeAssignmentModal]}
open={true}
title="Edit Grades"
title={
<FormattedMessage
defaultMessage="Edit Grades"
description="Edit Modal title"
id="gradebook.GradesTab.EditModal.title"
/>
}
/>
`;
@@ -50,10 +74,18 @@ exports[`EditMoal Component snapshots gradeOverrideHistoryError is empty and ope
/>
<OverrideTable />
<div>
Showing most recent actions (max 5). To see more, please contact support.
<FormattedMessage
defaultMessage="Showing most recent actions (max 5). To see more, please contact support"
description="Edit Modal visibility hint message"
id="gradebook.GradesTab.EditModal.contactSupport"
/>
</div>
<div>
Note: Once you save, your changes will be visible to students.
<FormattedMessage
defaultMessage="Note: Once you save, your changes will be visible to students."
description="Edit Modal saved changes effect hint"
id="gradebook.GradesTab.EditModal.saveVisibility"
/>
</div>
</div>
}
@@ -63,13 +95,29 @@ exports[`EditMoal Component snapshots gradeOverrideHistoryError is empty and ope
onClick={[MockFunction this.handleAdjustedGradeClick]}
variant="primary"
>
Save Grade
<FormattedMessage
defaultMessage="Save Grades"
description="Edit Modal Save button label"
id="gradebook.GradesTab.EditModal.saveGrade"
/>
</Button>,
]
}
closeText="Cancel"
closeText={
<FormattedMessage
defaultMessage="Cancel"
description="Edit Modal close button text"
id="gradebook.GradesTab.EditModal.closeText"
/>
}
onClose={[MockFunction this.closeAssignmentModal]}
open={false}
title="Edit Grades"
title={
<FormattedMessage
defaultMessage="Edit Grades"
description="Edit Modal title"
id="gradebook.GradesTab.EditModal.title"
/>
}
/>
`;

View File

@@ -8,11 +8,13 @@ import {
Modal,
StatusAlert,
} from '@edx/paragon';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import selectors from 'data/selectors';
import actions from 'data/actions';
import thunkActions from 'data/thunkActions';
import messages from './messages';
import OverrideTable from './OverrideTable';
import ModalHeaders from './ModalHeaders';
@@ -46,8 +48,8 @@ export class EditModal extends React.Component {
return (
<Modal
open={this.props.open}
title="Edit Grades"
closeText="Cancel"
title={<FormattedMessage {...messages.title} />}
closeText={<FormattedMessage {...messages.closeText} />}
body={(
<div>
<ModalHeaders />
@@ -58,15 +60,13 @@ export class EditModal extends React.Component {
dismissible={false}
/>
<OverrideTable />
<div>Showing most recent actions (max 5). To see more, please contact
support.
</div>
<div>Note: Once you save, your changes will be visible to students.</div>
<div><FormattedMessage {...messages.visibility} /></div>
<div><FormattedMessage {...messages.saveVisibility} /></div>
</div>
)}
buttons={[
<Button variant="primary" onClick={this.handleAdjustedGradeClick}>
Save Grade
<FormattedMessage {...messages.saveGrade} />
</Button>,
]}
onClose={this.closeAssignmentModal}

View File

@@ -0,0 +1,51 @@
import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
assignmentHeader: {
id: 'gradebook.GradesTab.EditModal.headers.assignment',
defaultMessage: 'Assignment',
description: 'Edit Modal Assignment header',
},
currentGradeHeader: {
id: 'gradebook.GradesTab.EditModal.headers.currentGrade',
defaultMessage: 'Current Grade',
description: 'Edit Modal Current Grade header',
},
originalGradeHeader: {
id: 'gradebook.GradesTab.EditModal.headers.originalGrade',
defaultMessage: 'Original Grade',
description: 'Edit Modal Original Grade header',
},
studentHeader: {
id: 'gradebook.GradesTab.EditModal.headers.student',
defaultMessage: 'Student',
description: 'Edit Modal Student header',
},
title: {
id: 'gradebook.GradesTab.EditModal.title',
defaultMessage: 'Edit Grades',
description: 'Edit Modal title',
},
closeText: {
id: 'gradebook.GradesTab.EditModal.closeText',
defaultMessage: 'Cancel',
description: 'Edit Modal close button text',
},
visibility: {
id: 'gradebook.GradesTab.EditModal.contactSupport',
defaultMessage: 'Showing most recent actions (max 5). To see more, please contact support',
description: 'Edit Modal visibility hint message',
},
saveVisibility: {
id: 'gradebook.GradesTab.EditModal.saveVisibility',
defaultMessage: 'Note: Once you save, your changes will be visible to students.',
description: 'Edit Modal saved changes effect hint',
},
saveGrade: {
id: 'gradebook.GradesTab.EditModal.saveGrade',
defaultMessage: 'Save Grades',
description: 'Edit Modal Save button label',
},
});
export default messages;

View File

@@ -3,6 +3,7 @@ import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import { Button } from '@edx/paragon';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import selectors from 'data/selectors';
@@ -15,7 +16,6 @@ import selectors from 'data/selectors';
* @param {string} filterName - api filter name (for redux connector)
*/
export const FilterBadge = ({
handleClose,
config: {
displayName,
isDefault,
@@ -23,11 +23,15 @@ export const FilterBadge = ({
value,
connectedFilters,
},
handleClose,
}) => !isDefault && (
<div>
<span className="badge badge-info">
<span>
{displayName}{!hideValue && `: ${value}`}
<FormattedMessage {...displayName} />
</span>
<span>
{!hideValue ? `: ${value}` : ''}
</span>
<Button
className="btn-info"
@@ -48,7 +52,9 @@ FilterBadge.propTypes = {
// redux
config: PropTypes.shape({
connectedFilters: PropTypes.arrayOf(PropTypes.string),
displayName: PropTypes.string.isRequired,
displayName: PropTypes.shape({
defaultMessage: PropTypes.string,
}).isRequired,
isDefault: PropTypes.bool.isRequired,
hideValue: PropTypes.bool,
value: PropTypes.oneOfType([

View File

@@ -1,5 +1,6 @@
import React from 'react';
import { shallow } from 'enzyme';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import { Button } from '@edx/paragon';
import selectors from 'data/selectors';
@@ -20,7 +21,9 @@ jest.mock('data/selectors', () => ({
describe('FilterBadge', () => {
describe('component', () => {
const config = {
displayName: 'a common name',
displayName: {
defaultMessage: 'a common name',
},
isDefault: false,
hideValue: false,
value: 'a common value',
@@ -58,7 +61,11 @@ describe('FilterBadge', () => {
expect(el).toMatchSnapshot();
});
it('shows displayName but not value in span', () => {
expect(el.find('span.badge').childAt(0).text()).toEqual(config.displayName);
expect(el.find('span.badge').childAt(0).getElement()).toEqual(
<span>
<FormattedMessage {...config.displayName} />
</span>,
);
});
it('calls a handleClose event for connected filters on button click', () => {
expect(el.find(Button).props().onClick).toEqual(handleClose(config.connectedFilters));
@@ -72,8 +79,15 @@ describe('FilterBadge', () => {
expect(el).toMatchSnapshot();
});
it('shows displayName and value in span', () => {
expect(el.find('span.badge').childAt(0).text()).toEqual(
`${config.displayName}: ${config.value}`,
expect(el.find('span.badge').childAt(0).getElement()).toEqual(
<span>
<FormattedMessage {...config.displayName} />
</span>,
);
expect(el.find('span.badge').childAt(1).getElement()).toEqual(
<span>
{`: ${config.value}`}
</span>,
);
});
it('calls a handleClose event for connected filters on button click', () => {

View File

@@ -8,7 +8,11 @@ exports[`FilterBadge component with non-default value (active) if hideValue is f
className="badge badge-info"
>
<span>
a common name
<FormattedMessage
defaultMessage="a common name"
/>
</span>
<span>
: a common value
</span>
<Button
@@ -40,8 +44,11 @@ exports[`FilterBadge component with non-default value (active) if hideValue is t
className="badge badge-info"
>
<span>
a common name
<FormattedMessage
defaultMessage="a common name"
/>
</span>
<span />
<Button
aria-label="close"
className="btn-info"

View File

@@ -7,8 +7,9 @@ import {
OverlayTrigger,
Tooltip,
} from '@edx/paragon';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import { Headings } from 'data/constants/grades';
import messages from './messages';
export const totalGradePercentageMessage = 'Total Grade values are always displayed as a percentage.';
@@ -23,12 +24,21 @@ const TotalGradeLabelReplacement = () => (
trigger={['hover', 'focus']}
key="left-basic"
placement="left"
overlay={(<Tooltip id="course-grade-tooltip">{totalGradePercentageMessage}</Tooltip>)}
overlay={(
<Tooltip id="course-grade-tooltip">
<FormattedMessage {...messages.totalGradePercentage} />
</Tooltip>
)}
>
<div>
{Headings.totalGrade}
<FormattedMessage {...messages.totalGradeHeading} />
<div id="courseGradeTooltipIcon">
<Icon className="fa fa-info-circle" screenReaderText={totalGradePercentageMessage} />
<Icon
className="fa fa-info-circle"
screenReaderText={(
<FormattedMessage {...messages.totalGradePercentage} />
)}
/>
</div>
</div>
</OverlayTrigger>
@@ -41,8 +51,12 @@ const TotalGradeLabelReplacement = () => (
*/
const UsernameLabelReplacement = () => (
<div>
<div>Username</div>
<div className="font-weight-normal student-key">Student Key*</div>
<div>
<FormattedMessage {...messages.usernameHeading} />
</div>
<div className="font-weight-normal student-key">
<FormattedMessage {...messages.studentKeyLabel} />
</div>
</div>
);

View File

@@ -4,7 +4,11 @@ exports[`LabelReplacements TotalGradeLabelReplacement displays overlay tooltip 1
<Tooltip
id="course-grade-tooltip"
>
Total Grade values are always displayed as a percentage.
<FormattedMessage
defaultMessage="Total Grade values are always displayed as a percentage"
description="Gradebook table message that total grades are displayed in percent format"
id="gradebook.GradesTab.table.totalGradePercentage"
/>
</Tooltip>
`;
@@ -16,7 +20,11 @@ exports[`LabelReplacements TotalGradeLabelReplacement snapshot 1`] = `
<Tooltip
id="course-grade-tooltip"
>
Total Grade values are always displayed as a percentage.
<FormattedMessage
defaultMessage="Total Grade values are always displayed as a percentage"
description="Gradebook table message that total grades are displayed in percent format"
id="gradebook.GradesTab.table.totalGradePercentage"
/>
</Tooltip>
}
placement="left"
@@ -28,13 +36,23 @@ exports[`LabelReplacements TotalGradeLabelReplacement snapshot 1`] = `
}
>
<div>
Total Grade (%)
<FormattedMessage
defaultMessage="Total Grade (%)"
description="Gradebook table total grade column header"
id="gradebook.GradesTab.table.headings.totalGrade"
/>
<div
id="courseGradeTooltipIcon"
>
<Icon
className="fa fa-info-circle"
screenReaderText="Total Grade values are always displayed as a percentage."
screenReaderText={
<FormattedMessage
defaultMessage="Total Grade values are always displayed as a percentage"
description="Gradebook table message that total grades are displayed in percent format"
id="gradebook.GradesTab.table.totalGradePercentage"
/>
}
/>
</div>
</div>
@@ -45,12 +63,20 @@ exports[`LabelReplacements TotalGradeLabelReplacement snapshot 1`] = `
exports[`LabelReplacements UsernameLabelReplacement snapshot 1`] = `
<div>
<div>
Username
<FormattedMessage
defaultMessage="Username"
description="Gradebook table username column header"
id="gradebook.GradesTab.table.headings.username"
/>
</div>
<div
className="font-weight-normal student-key"
>
Student Key*
<FormattedMessage
defaultMessage="Student Key*"
description="Gradebook table Student Key label"
id="gradebook.GradesTab.table.labels.studentKey"
/>
</div>
</div>
`;

View File

@@ -16,7 +16,11 @@ exports[`GradebookTable component snapshot - fields1 and 2 between email and tot
},
Object {
"key": "Email",
"label": "Email",
"label": <FormattedMessage
defaultMessage="Email"
description="Gradebook table email column header"
id="gradebook.GradesTab.table.headings.email"
/>,
},
Object {
"key": "field1",

View File

@@ -4,10 +4,12 @@ import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { Table } from '@edx/paragon';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import { Headings } from 'data/constants/grades';
import selectors from 'data/selectors';
import { Headings } from 'data/constants/grades';
import messages from './messages';
import Fields from './Fields';
import LabelReplacements from './LabelReplacements';
import GradeButton from './GradeButton';
@@ -28,14 +30,17 @@ export class GradebookTable extends React.Component {
}
mapHeaders(heading) {
const replacement = {
[Headings.totalGrade]: <LabelReplacements.TotalGradeLabelReplacement />,
[Headings.username]: <LabelReplacements.UsernameLabelReplacement />,
}[heading];
return {
label: replacement !== undefined ? replacement : heading,
key: heading,
};
let label;
if (heading === Headings.totalGrade) {
label = <LabelReplacements.TotalGradeLabelReplacement />;
} else if (heading === Headings.username) {
label = <LabelReplacements.UsernameLabelReplacement />;
} else if (heading === Headings.email) {
label = <FormattedMessage {...messages.emailHeading} />;
} else {
label = heading;
}
return { label, key: heading };
}
mapRows(entry) {

View File

@@ -0,0 +1,36 @@
import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
emailHeading: {
id: 'gradebook.GradesTab.table.headings.email',
defaultMessage: 'Email',
description: 'Gradebook table email column header',
},
totalGradeHeading: {
id: 'gradebook.GradesTab.table.headings.totalGrade',
defaultMessage: 'Total Grade (%)',
description: 'Gradebook table total grade column header',
},
usernameHeading: {
id: 'gradebook.GradesTab.table.headings.username',
defaultMessage: 'Username',
description: 'Gradebook table username column header',
},
studentKeyLabel: {
id: 'gradebook.GradesTab.table.labels.studentKey',
defaultMessage: 'Student Key*',
description: 'Gradebook table Student Key label',
},
usernameLabel: {
id: 'gradebook.GradesTab.table.labels.username',
defaultMessage: 'Username',
description: 'Gradebook table username label',
},
totalGradePercentage: {
id: 'gradebook.GradesTab.table.totalGradePercentage',
defaultMessage: 'Total Grade values are always displayed as a percentage',
description: 'Gradebook table message that total grades are displayed in percent format',
},
});
export default messages;

View File

@@ -2,11 +2,13 @@ import React from 'react';
import { shallow } from 'enzyme';
import { Table } from '@edx/paragon';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import selectors from 'data/selectors';
import { Headings } from 'data/constants/grades';
import LabelReplacements from './LabelReplacements';
import Fields from './Fields';
import messages from './messages';
import { GradebookTable, mapStateToProps } from '.';
jest.mock('@edx/paragon', () => ({
@@ -94,7 +96,7 @@ describe('GradebookTable', () => {
test('email sets key and label from header', () => {
const heading = headings[1];
expect(heading.key).toEqual(Headings.email);
expect(heading.label).toEqual(Headings.email);
expect(heading.label).toEqual(<FormattedMessage {...messages.emailHeading} />);
});
test('subsections set key and label from header', () => {
expect(headings[2]).toEqual({ key: fields.field1, label: fields.field1 });

View File

@@ -19,7 +19,11 @@ exports[`PageButtons component snapshots buttons enabled with both endpoints pro
}
variant="outline-primary"
>
Previous Page
<FormattedMessage
defaultMessage="Previous Page"
description="Grades tab Previous Page button text"
id="gradebook.GradesTab.PageButtons.prevPage"
/>
</Button>
<Button
disabled={false}
@@ -31,7 +35,11 @@ exports[`PageButtons component snapshots buttons enabled with both endpoints pro
}
variant="outline-primary"
>
Next Page
<FormattedMessage
defaultMessage="Next Page"
description="Grades tab Next Page button text"
id="gradebook.GradesTab.PageButtons.nextPage"
/>
</Button>
</div>
`;
@@ -55,7 +63,11 @@ exports[`PageButtons component snapshots nextPage disabled if not provided 1`] =
}
variant="outline-primary"
>
Previous Page
<FormattedMessage
defaultMessage="Previous Page"
description="Grades tab Previous Page button text"
id="gradebook.GradesTab.PageButtons.prevPage"
/>
</Button>
<Button
disabled={true}
@@ -67,7 +79,11 @@ exports[`PageButtons component snapshots nextPage disabled if not provided 1`] =
}
variant="outline-primary"
>
Next Page
<FormattedMessage
defaultMessage="Next Page"
description="Grades tab Next Page button text"
id="gradebook.GradesTab.PageButtons.nextPage"
/>
</Button>
</div>
`;
@@ -91,7 +107,11 @@ exports[`PageButtons component snapshots prevPage disabled if not provided 1`] =
}
variant="outline-primary"
>
Previous Page
<FormattedMessage
defaultMessage="Previous Page"
description="Grades tab Previous Page button text"
id="gradebook.GradesTab.PageButtons.prevPage"
/>
</Button>
<Button
disabled={false}
@@ -103,7 +123,11 @@ exports[`PageButtons component snapshots prevPage disabled if not provided 1`] =
}
variant="outline-primary"
>
Next Page
<FormattedMessage
defaultMessage="Next Page"
description="Grades tab Next Page button text"
id="gradebook.GradesTab.PageButtons.nextPage"
/>
</Button>
</div>
`;

View File

@@ -3,9 +3,11 @@ import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { Button } from '@edx/paragon';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import selectors from 'data/selectors';
import thunkActions from 'data/thunkActions';
import messages from './messages';
export class PageButtons extends React.Component {
constructor(props) {
@@ -34,7 +36,7 @@ export class PageButtons extends React.Component {
disabled={!this.props.prevPage}
onClick={this.getPrevGrades}
>
Previous Page
<FormattedMessage {...messages.prevPage} />
</Button>
<Button
style={{ margin: '20px' }}
@@ -42,7 +44,7 @@ export class PageButtons extends React.Component {
disabled={!this.props.nextPage}
onClick={this.getNextGrades}
>
Next Page
<FormattedMessage {...messages.nextPage} />
</Button>
</div>
);

View File

@@ -0,0 +1,16 @@
import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
prevPage: {
id: 'gradebook.GradesTab.PageButtons.prevPage',
defaultMessage: 'Previous Page',
description: 'Grades tab Previous Page button text',
},
nextPage: {
id: 'gradebook.GradesTab.PageButtons.nextPage',
defaultMessage: 'Next Page',
description: 'Grades tab Next Page button text',
},
});
export default messages;

View File

@@ -3,24 +3,26 @@ import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { FormControl, FormGroup, FormLabel } from '@edx/paragon';
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import actions from 'data/actions';
import selectors from 'data/selectors';
import messages from './messages';
/**
* <ScoreViewInput />
* redux-connected select control for grade format (percent vs absolute)
*/
export const ScoreViewInput = ({ format, toggleFormat }) => (
export const ScoreViewInput = ({ format, intl, toggleFormat }) => (
<FormGroup controlId="ScoreView">
<FormLabel>Score View:</FormLabel>
<FormLabel><FormattedMessage {...messages.scoreView} />:</FormLabel>
<FormControl
as="select"
value={format}
onChange={toggleFormat}
>
<option value="percent">Percent</option>
<option value="absolute">Absolute</option>
<option value="percent">{intl.formatMessage(messages.percent)}</option>
<option value="absolute">{intl.formatMessage(messages.absolute)}</option>
</FormControl>
</FormGroup>
);
@@ -28,6 +30,9 @@ ScoreViewInput.defaultProps = {
format: 'percent',
};
ScoreViewInput.propTypes = {
// injected
intl: intlShape.isRequired,
// redux
format: PropTypes.string,
toggleFormat: PropTypes.func.isRequired,
};
@@ -40,4 +45,4 @@ export const mapDispatchToProps = {
toggleFormat: actions.grades.toggleGradeFormat,
};
export default connect(mapStateToProps, mapDispatchToProps)(ScoreViewInput);
export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(ScoreViewInput));

View File

@@ -35,6 +35,7 @@ describe('ScoreViewInput', () => {
let el;
beforeEach(() => {
props.toggleFormat = jest.fn();
props.intl = { formatMessage: (msg) => msg.defaultMessage };
el = shallow(<ScoreViewInput {...props} />);
});
const assertions = [

View File

@@ -3,11 +3,14 @@ import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { Button, Icon, SearchField } from '@edx/paragon';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import actions from 'data/actions';
import selectors from 'data/selectors';
import thunkActions from 'data/thunkActions';
import messages from './messages';
/**
* Controls for filtering the GradebookTable. Contains the "Edit Filters" button for opening the filter drawer
* as well as the search box for searching by username/email.
@@ -32,25 +35,25 @@ export class SearchControls extends React.Component {
render() {
return (
<>
<h4>Step 1: Filter the Grade Report</h4>
<h4><FormattedMessage {...messages.filterStepHeading} /></h4>
<div className="d-flex justify-content-between">
<Button
id="edit-filters-btn"
className="btn-primary align-self-start"
onClick={this.props.toggleFilterDrawer}
>
<Icon className="fa fa-filter" /> Edit Filters
<Icon className="fa fa-filter" /> <FormattedMessage {...messages.editFilters} />
</Button>
<div>
<SearchField
onSubmit={this.props.fetchGrades}
inputLabel="Search for a learner"
inputLabel={<FormattedMessage {...messages.searchLabel} />}
onChange={this.onChange}
onClear={this.onClear}
value={this.props.searchValue}
/>
<small className="form-text text-muted search-help-text">
Search by username, email, or student key
<FormattedMessage {...messages.searchHint} />
</small>
</div>
</div>

View File

@@ -3,12 +3,11 @@ import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { StatusAlert } from '@edx/paragon';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import selectors from 'data/selectors';
import actions from 'data/actions';
export const maxCourseGradeInvalidMessage = 'Maximum course grade value must be between 0 and 100. ';
export const minCourseGradeInvalidMessage = 'Minimum course grade value must be between 0 and 100. ';
import messages from './messages';
export class StatusAlerts extends React.Component {
get isCourseGradeFilterAlertOpen() {
@@ -18,15 +17,24 @@ export class StatusAlerts extends React.Component {
);
}
get minValidityMessage() {
return (this.props.limitValidity.isMinValid)
? ''
: <FormattedMessage {...messages.minGradeInvalid} />;
}
get maxValidityMessage() {
return (this.props.limitValidity.isMaxValid)
? ''
: <FormattedMessage {...messages.maxGradeInvalid} />;
}
get courseGradeFilterAlertDialogText() {
let dialogText = '';
if (!this.props.limitValidity.isMinValid) {
dialogText += minCourseGradeInvalidMessage;
}
if (!this.props.limitValidity.isMaxValid) {
dialogText += maxCourseGradeInvalidMessage;
}
return dialogText;
return (
<>
{this.minValidityMessage}{this.maxValidityMessage}
</>
);
}
render() {
@@ -34,7 +42,7 @@ export class StatusAlerts extends React.Component {
<>
<StatusAlert
alertType="success"
dialog="The grade has been successfully edited. You may see a slight delay before updates appear in the Gradebook."
dialog={<FormattedMessage {...messages.editSuccessAlert} />}
onClose={this.props.handleCloseSuccessBanner}
open={this.props.showSuccessBanner}
/>

View File

@@ -1,14 +1,15 @@
import React from 'react';
import { shallow } from 'enzyme';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import actions from 'data/actions';
import selectors from 'data/selectors';
import messages from './messages';
import {
StatusAlerts,
mapDispatchToProps,
mapStateToProps,
maxCourseGradeInvalidMessage,
minCourseGradeInvalidMessage,
} from './StatusAlerts';
jest.mock('@edx/paragon', () => ({
@@ -77,18 +78,24 @@ describe('StatusAlerts', () => {
!isMinValid || !isMaxValid,
);
if (!isMaxValid) {
if (!isMinValid) {
expect(el.instance().courseGradeFilterAlertDialogText).toEqual(
<>
<FormattedMessage {...messages.minGradeInvalid} />
<FormattedMessage {...messages.maxGradeInvalid} />
</>,
);
} else {
expect(
el.instance().courseGradeFilterAlertDialogText,
// eslint-disable-next-line react/jsx-curly-brace-presence
).toEqual(<>{''}<FormattedMessage {...messages.maxGradeInvalid} /></>);
}
} else if (!isMinValid) {
expect(
el.instance().courseGradeFilterAlertDialogText,
).toEqual(
expect.stringContaining(maxCourseGradeInvalidMessage),
);
}
if (!isMinValid) {
expect(
el.instance().courseGradeFilterAlertDialogText,
).toEqual(
expect.stringContaining(minCourseGradeInvalidMessage),
);
// eslint-disable-next-line react/jsx-curly-brace-presence
).toEqual(<><FormattedMessage {...messages.minGradeInvalid} />{''}</>);
}
});
});

View File

@@ -3,6 +3,8 @@ import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import selectors from 'data/selectors';
/**
@@ -18,9 +20,15 @@ export const UsersLabel = ({
}
const bold = (val) => (<span className="font-weight-bold">{val}</span>);
return (
<>
Showing {bold(filteredUsersCount)} of {bold(totalUsersCount)} total learners
</>
<FormattedMessage
id="gradebook.GradesTab.usersVisibilityLabel'"
defaultMessage="Showing {filteredUsers} of {totalUsers} total learners"
description="Users visibility label"
values={{
filteredUsers: bold(filteredUsersCount),
totalUsers: bold(totalUsersCount),
}}
/>
);
};
UsersLabel.propTypes = {

View File

@@ -5,7 +5,12 @@ exports[`ScoreViewInput component snapshot - select box with percent and absolut
controlId="ScoreView"
>
<FormLabel>
Score View:
<FormattedMessage
defaultMessage="Score View"
description="Score format select dropdown label"
id="gradebook.GradesTab.scoreViewLabel"
/>
:
</FormLabel>
<FormControl
as="select"

View File

@@ -3,7 +3,11 @@
exports[`SearchControls Component Snapshots basic snapshot 1`] = `
<React.Fragment>
<h4>
Step 1: Filter the Grade Report
<FormattedMessage
defaultMessage="Step 1: Filter the Grade Report"
description="Filter controls container heading string"
id="gradebook.GradesTab.filterHeading"
/>
</h4>
<div
className="d-flex justify-content-between"
@@ -16,11 +20,22 @@ exports[`SearchControls Component Snapshots basic snapshot 1`] = `
<Icon
className="fa fa-filter"
/>
Edit Filters
<FormattedMessage
defaultMessage="Edit Filters"
description="Button text on Grades tab to open/close the Filters tab"
id="gradebook.GradesTab.editFilterLabel"
/>
</Button>
<div>
<SearchField
inputLabel="Search for a learner"
inputLabel={
<FormattedMessage
defaultMessage="Search for a learner"
description="Search description label"
id="gradebook.GradesTab.search.label"
/>
}
onChange={[MockFunction onChange]}
onClear={[MockFunction onClear]}
onSubmit={[MockFunction fetchGrades]}
@@ -29,7 +44,11 @@ exports[`SearchControls Component Snapshots basic snapshot 1`] = `
<small
className="form-text text-muted search-help-text"
>
Search by username, email, or student key
<FormattedMessage
defaultMessage="Search by username, email, or student key"
description="Search hint label"
id="gradebook.GradesTab.search.hint"
/>
</small>
</div>
</div>

View File

@@ -4,7 +4,13 @@ exports[`StatusAlerts snapshots basic snapshot 1`] = `
<React.Fragment>
<StatusAlert
alertType="success"
dialog="The grade has been successfully edited. You may see a slight delay before updates appear in the Gradebook."
dialog={
<FormattedMessage
defaultMessage="The grade has been successfully edited. You may see a slight delay before updates appear in the Gradebook."
description="Alert text for successful edit action"
id="gradebook.GradesTab.editSuccessAlert"
/>
}
onClose={[MockFunction handleCloseSuccessBanner]}
open={true}
/>

View File

@@ -1,19 +1,23 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`UsersLabel component snapshot - displays label with number of filtered users out of total 1`] = `
<Fragment>
Showing
<span
className="font-weight-bold"
>
23
</span>
of
<span
className="font-weight-bold"
>
140
</span>
total learners
</Fragment>
<FormattedMessage
defaultMessage="Showing {filteredUsers} of {totalUsers} total learners"
description="Users visibility label"
id="gradebook.GradesTab.usersVisibilityLabel'"
values={
Object {
"filteredUsers": <span
className="font-weight-bold"
>
23
</span>,
"totalUsers": <span
className="font-weight-bold"
>
140
</span>,
}
}
/>
`;

View File

@@ -9,7 +9,11 @@ exports[`GradesTab Component snapshots basic snapshot 1`] = `
/>
<StatusAlerts />
<h4>
Step 2: View or Modify Individual Grades
<FormattedMessage
defaultMessage="Step 2: View or Modify Individual Grades"
description="Alert text for invalid minimum course grade"
id="gradebook.GradesTab.gradebookStepHeading"
/>
</h4>
<UsersLabel />
<div
@@ -21,7 +25,12 @@ exports[`GradesTab Component snapshots basic snapshot 1`] = `
<GradebookTable />
<PageButtons />
<p>
* available for learners in the Master's track only
*
<FormattedMessage
defaultMessage="available for learners in the Master's track only"
description="Masters feature availability hint on Grades Tab"
id="gradebook.GradesTab.mastersHint"
/>
</p>
<EditModal />
</React.Fragment>

View File

@@ -3,6 +3,8 @@ import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import actions from 'data/actions';
import thunkActions from 'data/thunkActions';
@@ -17,6 +19,7 @@ import StatusAlerts from './StatusAlerts';
import SpinnerIcon from './SpinnerIcon';
import ScoreViewInput from './ScoreViewInput';
import UsersLabel from './UsersLabel';
import messages from './messages';
export class GradesTab extends React.Component {
constructor(props) {
@@ -43,7 +46,7 @@ export class GradesTab extends React.Component {
<FilterBadges handleClose={this.handleFilterBadgeClose} />
<StatusAlerts />
<h4>Step 2: View or Modify Individual Grades</h4>
<h4><FormattedMessage {...messages.gradebookStepHeading} /></h4>
<UsersLabel />
<div className="d-flex justify-content-between align-items-center mb-2">
@@ -54,7 +57,7 @@ export class GradesTab extends React.Component {
<GradebookTable />
<PageButtons />
<p>* available for learners in the Master&apos;s track only</p>
<p>* <FormattedMessage {...messages.mastersHint} /></p>
<EditModal />
</>
);

View File

@@ -0,0 +1,76 @@
import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
bulkManagement: {
id: 'gradebook.GradesTab.BulkManagementControls.bulkManagementLabel',
defaultMessage: 'Bulk Management',
description: 'Button text for bulk grades download control in GradesTab',
},
interventions: {
id: 'gradebook.GradesTab.BulkManagementControls.interventionsLabel',
defaultMessage: 'Interventions',
description: 'Button text for intervention report download control in GradesTab',
},
scoreView: {
id: 'gradebook.GradesTab.scoreViewLabel',
defaultMessage: 'Score View',
description: 'Score format select dropdown label',
},
absolute: {
id: 'gradebook.GradesTab.absoluteOption',
defaultMessage: 'Absolute',
description: 'Score format select dropdown option',
},
percent: {
id: 'gradebook.GradesTab.percentOption',
defaultMessage: 'Percent',
description: 'Score format select dropdown option',
},
filterStepHeading: {
id: 'gradebook.GradesTab.filterHeading',
defaultMessage: 'Step 1: Filter the Grade Report',
description: 'Filter controls container heading string',
},
editFilters: {
id: 'gradebook.GradesTab.editFilterLabel',
defaultMessage: 'Edit Filters',
description: 'Button text on Grades tab to open/close the Filters tab',
},
searchLabel: {
id: 'gradebook.GradesTab.search.label',
defaultMessage: 'Search for a learner',
description: 'Search description label',
},
searchHint: {
id: 'gradebook.GradesTab.search.hint',
defaultMessage: 'Search by username, email, or student key',
description: 'Search hint label',
},
editSuccessAlert: {
id: 'gradebook.GradesTab.editSuccessAlert',
defaultMessage: 'The grade has been successfully edited. You may see a slight delay before updates appear in the Gradebook.',
description: 'Alert text for successful edit action',
},
maxGradeInvalid: {
id: 'gradebook.GradesTab.maxCourseGradeInvalid',
defaultMessage: 'Maximum course grade must be between 0 and 100',
description: 'Alert text for invalid maximum course grade',
},
minGradeInvalid: {
id: 'gradebook.GradesTab.minCourseGradeInvalid',
defaultMessage: 'Minimum course grade must be between 0 and 100',
description: 'Alert text for invalid minimum course grade',
},
gradebookStepHeading: {
id: 'gradebook.GradesTab.gradebookStepHeading',
defaultMessage: 'Step 2: View or Modify Individual Grades',
description: 'Alert text for invalid minimum course grade',
},
mastersHint: {
id: 'gradebook.GradesTab.mastersHint',
defaultMessage: "available for learners in the Master's track only",
description: 'Masters feature availability hint on Grades Tab',
},
});
export default messages;

View File

@@ -52,6 +52,13 @@ export const bulkManagementColumns = [
},
];
export const gradeOverrideHistoryColumns = StrictDict({
adjustedGrade: 'adjustedGrade',
date: 'date',
grader: 'grader',
reason: 'reason',
});
/**
* Display strings for various app components.
* Note: this is a temporary storage location for these strings, before we put them in

View File

@@ -1,5 +1,7 @@
import { StrictDict } from 'utils';
import messages from './filters.messages';
export const filters = StrictDict({
assignment: 'assignment',
assignmentGrade: 'assignmentGrade',
@@ -28,34 +30,34 @@ const initialFilters = {
export const filterConfig = StrictDict({
[filters.assignment]: {
displayName: 'Assignment',
displayName: messages[filters.assignment],
connectedFilters: ['assignment', 'assignmentGradeMax', 'assignmentGradeMax'],
},
[filters.assignmentType]: {
displayName: 'Assignment Type',
displayName: messages[filters.assignmentType],
connectedFilters: ['assignmentType'],
},
[filters.assignmentGrade]: {
displayName: 'Assignment Grade',
displayName: messages[filters.assignmentGrade],
filterOrder: ['assignmentGradeMin', 'assignmentGradeMax'],
connectedFilters: ['assignmentGradeMax', 'assignmentGradeMin'],
},
[filters.cohort]: {
displayName: 'Cohort',
displayName: messages[filters.cohort],
connectedFilters: ['cohort'],
},
[filters.courseGrade]: {
displayName: 'Course Grade',
displayName: messages[filters.courseGrade],
filterOrder: ['courseGradeMin', 'courseGradeMax'],
connectedFilters: ['courseGradeMax', 'courseGradeMin'],
},
[filters.includeCourseRoleMembers]: {
displayName: 'Includeing Course Team Members',
displayName: messages[filters.includeCourseRoleMembers],
connectedFilters: ['includeCourseRoleMembers'],
hideValue: true,
},
[filters.track]: {
displayName: 'Track',
displayName: messages[filters.track],
connectedFilters: ['track'],
},
});

View File

@@ -0,0 +1,41 @@
import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
assignment: {
id: 'gradebook.GradesTab.FilterBadges.assignment',
defaultMessage: 'Assignment',
description: 'Assignment FilterBadge label',
},
assignmentGrade: {
id: 'gradebook.GradesTab.FilterBadges.assignmentGrade',
defaultMessage: 'Assignment Grade',
description: 'Assignment Grade FilterBadge label',
},
assignmentType: {
id: 'gradebook.GradesTab.FilterBadges.assignmentType',
defaultMessage: 'Assignment Type',
description: 'Assignment Type FilterBadge label',
},
cohort: {
id: 'gradebook.GradesTab.FilterBadges.cohort',
defaultMessage: 'Cohort',
description: 'Cohort FilterBadge label',
},
courseGrade: {
id: 'gradebook.GradesTab.FilterBadges.courseGrade',
defaultMessage: 'Course Grade',
description: 'Course Grade FilterBadge label',
},
includeCourseRoleMembers: {
id: 'gradebook.GradesTab.FilterBadges.includeCourseRoleMembers',
defaultMessage: 'Include Course Team Members',
description: 'Include Course Team Members FilterBadge label',
},
track: {
id: 'gradebook.GradesTab.FilterBadges.track',
defaultMessage: 'Track',
description: 'Track FilterBadge label',
},
});
export default messages;

View File

@@ -105,11 +105,11 @@ export const headingMapper = (category, label = 'All') => {
filter = filters.byLabel;
}
const { username, email, totalGrade } = Headings;
const fillerLabels = (entry) => entry.filter(filter).map(s => s.label);
const filteredLabels = (entry) => entry.filter(filter).map(s => s.label);
return (entry) => (
entry
? [username, email, ...fillerLabels(entry), totalGrade]
? [username, email, ...filteredLabels(entry), totalGrade]
: []
);
};

14
src/i18n/index.jsx Normal file
View File

@@ -0,0 +1,14 @@
import arMessages from './messages/ar.json';
// no need to import en messages-- they are in the defaultMessage field
import es419Messages from './messages/es_419.json';
import frMessages from './messages/fr.json';
import zhcnMessages from './messages/zh_CN.json';
const messages = {
ar: arMessages,
'es-419': es419Messages,
fr: frMessages,
'zh-cn': zhcnMessages,
};
export default messages;

View File

@@ -0,0 +1,2 @@
{
}

View File

@@ -0,0 +1,2 @@
{
}

View File

@@ -0,0 +1,2 @@
{
}

View File

@@ -0,0 +1,2 @@
{
}

View File

@@ -0,0 +1,8 @@
{
"gradebook.BulkManagementTab.csvUploadLabel": "Upload Grade CSV",
"gradebook.BulkManagementTab.heading": "Use this feature by downloading a CSV for bulk management, overriding grades locally, and coming back here to upload.",
"gradebook.BulkManagementTab.hint1": "Results appear in the table below.",
"gradebook.BulkManagementTab.hint2": "Grade processing may take a few seconds.",
"gradebook.BulkManagementTab.importBtnText": "Import Grades",
"gradebook.BulkManagementTab.successDialog": "CSV processing. File uploads may take several minutes to complete."
}

View File

@@ -3,14 +3,15 @@ import 'regenerator-runtime/runtime';
import React from 'react';
import ReactDOM from 'react-dom';
import {
APP_READY,
initialize,
subscribe,
} from '@edx/frontend-platform';
import { messages as footerMessages } from '@edx/frontend-component-footer';
import appMessages from './i18n';
import App from './App';
subscribe(APP_READY, () => {
@@ -19,6 +20,7 @@ subscribe(APP_READY, () => {
initialize({
messages: [
appMessages,
footerMessages,
],
requireAuthenticatedUser: true,

View File

@@ -8,6 +8,7 @@ import {
} from '@edx/frontend-platform';
import { messages as footerMessages } from '@edx/frontend-component-footer';
import appMessages from './i18n';
import App from './App';
import '.';
@@ -43,7 +44,7 @@ describe('app registry', () => {
});
test('initialize is called with footerMessages and requireAuthenticatedUser', () => {
expect(initialize).toHaveBeenCalledWith({
messages: [footerMessages],
messages: [appMessages, footerMessages],
requireAuthenticatedUser: true,
});
});

View File

@@ -8,3 +8,16 @@ Enzyme.configure({ adapter: new Adapter() });
// These configuration values are usually set in webpack's EnvironmentPlugin however
// Jest does not use webpack so we need to set these so for testing
process.env.LMS_BASE_URL = 'http://localhost:18000';
jest.mock('@edx/frontend-platform/i18n', () => {
const i18n = jest.requireActual('@edx/frontend-platform/i18n');
const PropTypes = jest.requireActual('prop-types');
return {
...i18n,
intlShape: PropTypes.shape({
formatMessage: jest.fn(msg => msg.defaultMessage),
}),
defineMessages: m => m,
FormattedMessage: () => 'FormattedMessage',
};
});