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:
4
.gitignore
vendored
4
.gitignore
vendored
@@ -17,3 +17,7 @@ dist/
|
||||
### Development environments ###
|
||||
.idea
|
||||
.vscode
|
||||
|
||||
### transifex ###
|
||||
src/i18n/transifex_input.json
|
||||
temp
|
||||
|
||||
8
.tx/config
Normal file
8
.tx/config
Normal 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
|
||||
64
Makefile
64
Makefile
@@ -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
8
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -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()]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
`;
|
||||
|
||||
@@ -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>
|
||||
`;
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 />
|
||||
@@ -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 />
|
||||
|
||||
@@ -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);
|
||||
36
src/components/BulkManagementTab/messages.js
Normal file
36
src/components/BulkManagementTab/messages.js
Normal 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;
|
||||
@@ -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 [
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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 [
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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));
|
||||
|
||||
71
src/components/GradebookFilters/messages.js
Normal file
71
src/components/GradebookFilters/messages.js
Normal 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;
|
||||
@@ -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(),
|
||||
|
||||
@@ -13,20 +13,31 @@ exports[`GradebookHeader component snapshots default values (grades frozen, cann
|
||||
>
|
||||
<<
|
||||
</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
|
||||
>
|
||||
<<
|
||||
</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
|
||||
>
|
||||
<<
|
||||
</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>
|
||||
`;
|
||||
|
||||
@@ -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>
|
||||
|
||||
26
src/components/GradebookHeader/messages.js
Normal file
26
src/components/GradebookHeader/messages.js
Normal 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;
|
||||
@@ -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: {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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]),
|
||||
};
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
/>,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
{
|
||||
|
||||
26
src/components/GradesTab/EditModal/OverrideTable/messages.js
Normal file
26
src/components/GradesTab/EditModal/OverrideTable/messages.js
Normal 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;
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
`;
|
||||
|
||||
@@ -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}
|
||||
|
||||
51
src/components/GradesTab/EditModal/messages.js
Normal file
51
src/components/GradesTab/EditModal/messages.js
Normal 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;
|
||||
@@ -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([
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
|
||||
@@ -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>
|
||||
`;
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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) {
|
||||
|
||||
36
src/components/GradesTab/GradebookTable/messages.js
Normal file
36
src/components/GradesTab/GradebookTable/messages.js
Normal 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;
|
||||
@@ -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 });
|
||||
|
||||
@@ -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>
|
||||
`;
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
16
src/components/GradesTab/PageButtons/messages.js
Normal file
16
src/components/GradesTab/PageButtons/messages.js
Normal 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;
|
||||
@@ -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));
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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} />{''}</>);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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>,
|
||||
}
|
||||
}
|
||||
/>
|
||||
`;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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's track only</p>
|
||||
<p>* <FormattedMessage {...messages.mastersHint} /></p>
|
||||
<EditModal />
|
||||
</>
|
||||
);
|
||||
|
||||
76
src/components/GradesTab/messages.js
Normal file
76
src/components/GradesTab/messages.js
Normal 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;
|
||||
@@ -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
|
||||
|
||||
@@ -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'],
|
||||
},
|
||||
});
|
||||
|
||||
41
src/data/constants/filters.messages.js
Normal file
41
src/data/constants/filters.messages.js
Normal 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;
|
||||
@@ -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
14
src/i18n/index.jsx
Normal 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;
|
||||
2
src/i18n/messages/ar.json
Normal file
2
src/i18n/messages/ar.json
Normal file
@@ -0,0 +1,2 @@
|
||||
{
|
||||
}
|
||||
2
src/i18n/messages/es_419.json
Normal file
2
src/i18n/messages/es_419.json
Normal file
@@ -0,0 +1,2 @@
|
||||
{
|
||||
}
|
||||
2
src/i18n/messages/fr.json
Normal file
2
src/i18n/messages/fr.json
Normal file
@@ -0,0 +1,2 @@
|
||||
{
|
||||
}
|
||||
2
src/i18n/messages/zh_CN.json
Normal file
2
src/i18n/messages/zh_CN.json
Normal file
@@ -0,0 +1,2 @@
|
||||
{
|
||||
}
|
||||
8
src/i18n/transifex_input.json
Normal file
8
src/i18n/transifex_input.json
Normal 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."
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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',
|
||||
};
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user