diff --git a/.gitignore b/.gitignore index 57d6a88..67c1e50 100755 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,7 @@ dist/ ### Development environments ### .idea .vscode + +### transifex ### +src/i18n/transifex_input.json +temp diff --git a/.tx/config b/.tx/config new file mode 100644 index 0000000..814e696 --- /dev/null +++ b/.tx/config @@ -0,0 +1,8 @@ +[main] +host = https://www.transifex.com + +[edx-platform.frontend-app-gradebook] +file_filter = src/i18n/messages/.json +source_file = src/i18n/transifex_input.json +source_lang = en +type = KEYVALUEJSON diff --git a/Makefile b/Makefile index 24ab053..7510e1a 100755 --- a/Makefile +++ b/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 diff --git a/package-lock.json b/package-lock.json index 5df3de1..9b94618 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 1876eaf..de4f094 100755 --- a/package.json +++ b/package.json @@ -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" diff --git a/src/components/BulkManagementTab/BulkManagementAlerts.jsx b/src/components/BulkManagementTab/BulkManagementAlerts.jsx index ac8bcff..3ada301 100644 --- a/src/components/BulkManagementTab/BulkManagementAlerts.jsx +++ b/src/components/BulkManagementTab/BulkManagementAlerts.jsx @@ -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'; /** * * Alerts to display at the top of the BulkManagement tab */ -export const BulkManagementAlerts = ({ bulkImportError, uploadSuccess }) => ( +export const BulkManagementAlerts = ({ + bulkImportError, + uploadSuccess, +}) => ( <> ( show={uploadSuccess} dismissible={false} > - {messages.successDialog} + ); diff --git a/src/components/BulkManagementTab/BulkManagementAlerts.test.jsx b/src/components/BulkManagementTab/BulkManagementAlerts.test.jsx index 8110ced..51a2794 100644 --- a/src/components/BulkManagementTab/BulkManagementAlerts.test.jsx +++ b/src/components/BulkManagementTab/BulkManagementAlerts.test.jsx @@ -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( + , ); expect(el.childAt(1).props().show).toEqual(true); }); diff --git a/src/components/BulkManagementTab/FileUploadForm.jsx b/src/components/BulkManagementTab/FileUploadForm.jsx index c1ab3f2..3f1a42a 100644 --- a/src/components/BulkManagementTab/FileUploadForm.jsx +++ b/src/components/BulkManagementTab/FileUploadForm.jsx @@ -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'; /** * @@ -56,14 +56,15 @@ export class FileUploadForm extends React.Component { } render() { + const { gradeExportUrl } = this.props; return ( <> -
+ } onChange={this.handleFileInputChange} ref={this.fileInputRef} /> @@ -71,7 +72,7 @@ export class FileUploadForm extends React.Component { ); diff --git a/src/components/BulkManagementTab/FileUploadForm.test.jsx b/src/components/BulkManagementTab/FileUploadForm.test.jsx index 7db6058..2e907dd 100644 --- a/src/components/BulkManagementTab/FileUploadForm.test.jsx +++ b/src/components/BulkManagementTab/FileUploadForm.test.jsx @@ -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(); + el = TestRenderer.create( + , + { 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(); - const ref = 'a-fake-ref'; - el.instance().fileInputRef = { current: ref }; - expect(el.instance().fileInput).toEqual(ref); + el = TestRenderer.create( + , + { 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(); - fileInput = jest.spyOn(el.instance(), 'fileInput', 'get'); + el = TestRenderer.create( + , + { 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(); + 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()]); }); }); }); diff --git a/src/components/BulkManagementTab/HistoryTable.jsx b/src/components/BulkManagementTab/HistoryTable.jsx index 2b0dd50..a24e3bf 100644 --- a/src/components/BulkManagementTab/HistoryTable.jsx +++ b/src/components/BulkManagementTab/HistoryTable.jsx @@ -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; - /** * * 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, +}) => ( <>

- {hints[0]} +
- {hints[1]} +

({ + 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(); expect(hints.childAt(1).is('br')).toEqual(true); - expect(hints.childAt(2).text()).toEqual(messages.BulkManagementTab.hints[1]); + expect(hints.childAt(2).getElement()).toEqual(); }); describe('history table', () => { let table; diff --git a/src/components/BulkManagementTab/__snapshots__/BulkManagementAlerts.test.jsx.snap b/src/components/BulkManagementTab/__snapshots__/BulkManagementAlerts.test.jsx.snap index 4c7305d..e973e0e 100644 --- a/src/components/BulkManagementTab/__snapshots__/BulkManagementAlerts.test.jsx.snap +++ b/src/components/BulkManagementTab/__snapshots__/BulkManagementAlerts.test.jsx.snap @@ -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. + `; @@ -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. + `; diff --git a/src/components/BulkManagementTab/__snapshots__/FileUploadForm.test.jsx.snap b/src/components/BulkManagementTab/__snapshots__/FileUploadForm.test.jsx.snap index dbc3665..f79ff1e 100644 --- a/src/components/BulkManagementTab/__snapshots__/FileUploadForm.test.jsx.snap +++ b/src/components/BulkManagementTab/__snapshots__/FileUploadForm.test.jsx.snap @@ -16,7 +16,13 @@ exports[`FileUploadForm component snapshot snapshot - loads export form w/ alert + } 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 + `; diff --git a/src/components/BulkManagementTab/__snapshots__/HistoryTable.test.jsx.snap b/src/components/BulkManagementTab/__snapshots__/HistoryTable.test.jsx.snap index 4765ade..bb2da01 100644 --- a/src/components/BulkManagementTab/__snapshots__/HistoryTable.test.jsx.snap +++ b/src/components/BulkManagementTab/__snapshots__/HistoryTable.test.jsx.snap @@ -44,9 +44,17 @@ Array [ exports[`HistoryTable component snapshot snapshot - loads hints display, formatted table 1`] = `

- Results appear in the table below. +
- Grade processing may take a few seconds. +

, , 1`] = `

- Use this feature by downloading a CSV for bulk management, overriding grades locally, and coming back here to upload. +

diff --git a/src/components/BulkManagementTab/index.jsx b/src/components/BulkManagementTab/index.jsx index 3e4df7b..16491f3 100644 --- a/src/components/BulkManagementTab/index.jsx +++ b/src/components/BulkManagementTab/index.jsx @@ -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 = () => (
-

{messages.BulkManagementTab.heading}

+

diff --git a/src/components/BulkManagementTab/test.jsx b/src/components/BulkManagementTab/index.test.jsx similarity index 85% rename from src/components/BulkManagementTab/test.jsx rename to src/components/BulkManagementTab/index.test.jsx index 7974898..0f37525 100644 --- a/src/components/BulkManagementTab/test.jsx +++ b/src/components/BulkManagementTab/index.test.jsx @@ -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(( +

+ +

+ )); }); test('heading, then alerts, then upload form, then table', () => { expect(el.childAt(0).is('h4')).toEqual(true); diff --git a/src/components/BulkManagementTab/messages.js b/src/components/BulkManagementTab/messages.js new file mode 100644 index 0000000..4e6517c --- /dev/null +++ b/src/components/BulkManagementTab/messages.js @@ -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; diff --git a/src/components/GradebookFilters/AssignmentFilter/__snapshots__/test.jsx.snap b/src/components/GradebookFilters/AssignmentFilter/__snapshots__/test.jsx.snap index 95168af..da2fc3d 100644 --- a/src/components/GradebookFilters/AssignmentFilter/__snapshots__/test.jsx.snap +++ b/src/components/GradebookFilters/AssignmentFilter/__snapshots__/test.jsx.snap @@ -7,7 +7,13 @@ exports[`AssignmentFilter Component snapshots basic snapshot 1`] = ` + } onChange={[MockFunction handleChange]} options={ Array [ diff --git a/src/components/GradebookFilters/AssignmentFilter/index.jsx b/src/components/GradebookFilters/AssignmentFilter/index.jsx index bb190db..1110624 100644 --- a/src/components/GradebookFilters/AssignmentFilter/index.jsx +++ b/src/components/GradebookFilters/AssignmentFilter/index.jsx @@ -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 {
} 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, diff --git a/src/components/GradebookFilters/AssignmentGradeFilter/__snapshots__/test.jsx.snap b/src/components/GradebookFilters/AssignmentGradeFilter/__snapshots__/test.jsx.snap index 0e66efa..2ad68a9 100644 --- a/src/components/GradebookFilters/AssignmentGradeFilter/__snapshots__/test.jsx.snap +++ b/src/components/GradebookFilters/AssignmentGradeFilter/__snapshots__/test.jsx.snap @@ -7,14 +7,26 @@ exports[`AssignmentGradeFilter Component snapshots buttons and groups disabled i + } onChange={[MockFunction handleSetMin]} value="2" /> + } onChange={[MockFunction handleSetMax]} value="98" /> @@ -42,14 +54,26 @@ exports[`AssignmentGradeFilter Component snapshots smoke test 1`] = ` + } onChange={[MockFunction handleSetMin]} value="2" /> + } onChange={[MockFunction handleSetMax]} value="98" /> diff --git a/src/components/GradebookFilters/AssignmentGradeFilter/index.jsx b/src/components/GradebookFilters/AssignmentGradeFilter/index.jsx index 88d29e8..adf0a31 100644 --- a/src/components/GradebookFilters/AssignmentGradeFilter/index.jsx +++ b/src/components/GradebookFilters/AssignmentGradeFilter/index.jsx @@ -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 (
} value={assignmentGradeMin} disabled={!this.props.selectedAssignment} onChange={this.handleSetMin} /> } value={assignmentGradeMax} disabled={!this.props.selectedAssignment} onChange={this.handleSetMax} diff --git a/src/components/GradebookFilters/AssignmentTypeFilter/__snapshots__/test.jsx.snap b/src/components/GradebookFilters/AssignmentTypeFilter/__snapshots__/test.jsx.snap index b545ede..f21b12a 100644 --- a/src/components/GradebookFilters/AssignmentTypeFilter/__snapshots__/test.jsx.snap +++ b/src/components/GradebookFilters/AssignmentTypeFilter/__snapshots__/test.jsx.snap @@ -7,7 +7,13 @@ exports[`AssignmentTypeFilter Component snapshots SelectGroup disabled if no ass + } onChange={[MockFunction handleChange]} options={ Array [ @@ -40,7 +46,13 @@ exports[`AssignmentTypeFilter Component snapshots smoke test 1`] = ` + } onChange={[MockFunction handleChange]} options={ Array [ diff --git a/src/components/GradebookFilters/AssignmentTypeFilter/index.jsx b/src/components/GradebookFilters/AssignmentTypeFilter/index.jsx index 25163c4..989736c 100644 --- a/src/components/GradebookFilters/AssignmentTypeFilter/index.jsx +++ b/src/components/GradebookFilters/AssignmentTypeFilter/index.jsx @@ -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 {
} value={this.props.selectedAssignmentType} onChange={this.handleChange} disabled={this.props.assignmentFilterOptions.length === 0} diff --git a/src/components/GradebookFilters/CourseGradeFilter/__snapshots__/test.jsx.snap b/src/components/GradebookFilters/CourseGradeFilter/__snapshots__/test.jsx.snap index dbdfdab..beb2dc6 100644 --- a/src/components/GradebookFilters/CourseGradeFilter/__snapshots__/test.jsx.snap +++ b/src/components/GradebookFilters/CourseGradeFilter/__snapshots__/test.jsx.snap @@ -7,13 +7,25 @@ exports[`CourseGradeFilter Component snapshots basic snapshot 1`] = ` > + } onChange={[MockFunction handleUpdateMin]} value="5" /> + } onChange={[MockFunction handleUpdateMax]} value="92" /> diff --git a/src/components/GradebookFilters/CourseGradeFilter/index.jsx b/src/components/GradebookFilters/CourseGradeFilter/index.jsx index ca1ef38..ad0117f 100644 --- a/src/components/GradebookFilters/CourseGradeFilter/index.jsx +++ b/src/components/GradebookFilters/CourseGradeFilter/index.jsx @@ -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 ( <>
} value={courseGradeMin} onChange={this.handleUpdateMin} /> } value={courseGradeMax} onChange={this.handleUpdateMax} /> diff --git a/src/components/GradebookFilters/PercentGroup.jsx b/src/components/GradebookFilters/PercentGroup.jsx index bf915b5..9f9398a 100644 --- a/src/components/GradebookFilters/PercentGroup.jsx +++ b/src/components/GradebookFilters/PercentGroup.jsx @@ -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, diff --git a/src/components/GradebookFilters/SelectGroup.jsx b/src/components/GradebookFilters/SelectGroup.jsx index a548cbd..5c0697f 100644 --- a/src/components/GradebookFilters/SelectGroup.jsx +++ b/src/components/GradebookFilters/SelectGroup.jsx @@ -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, diff --git a/src/components/GradebookFilters/StudentGroupsFilter/index.jsx b/src/components/GradebookFilters/StudentGroupsFilter/index.jsx index 4aa9d75..e1fd0c7 100644 --- a/src/components/GradebookFilters/StudentGroupsFilter/index.jsx +++ b/src/components/GradebookFilters/StudentGroupsFilter/index.jsx @@ -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 ( <> { beforeEach(() => { props = { ...props, + intl: { formatMessage: (msg) => msg.defaultMessage }, cohortsByName: { [props.cohorts[0].name]: props.cohorts[0], [props.cohorts[1].name]: props.cohorts[1], diff --git a/src/components/GradebookFilters/__snapshots__/test.jsx.snap b/src/components/GradebookFilters/__snapshots__/test.jsx.snap index 7568e39..926aad7 100644 --- a/src/components/GradebookFilters/__snapshots__/test.jsx.snap +++ b/src/components/GradebookFilters/__snapshots__/test.jsx.snap @@ -22,7 +22,13 @@ exports[`GradebookFilters Component snapshots basic snapshot 1`] = ` + } >
+ } > + } > - + } > - Include Course Team Members + diff --git a/src/components/GradebookFilters/index.jsx b/src/components/GradebookFilters/index.jsx index e1ce8c9..05c080b 100644 --- a/src/components/GradebookFilters/index.jsx +++ b/src/components/GradebookFilters/index.jsx @@ -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) => ( - + } + defaultOpen + className="filter-group mb-3" + > {content} ); 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)} />
- {this.collapsibleGroup('Assignments', ( + {this.collapsibleGroup(messages.assignments, (
@@ -70,20 +77,20 @@ export class GradebookFilters extends React.Component {
))} - {this.collapsibleGroup('Overall Grade', ( + {this.collapsibleGroup(messages.overallGrade, ( ))} - {this.collapsibleGroup('Student Groups', ( + {this.collapsibleGroup(messages.studentGroups, ( ))} - {this.collapsibleGroup('Include Course Team Members', ( + {this.collapsibleGroup(messages.includeCourseTeamMembers, ( - Include Course Team Members + ))} @@ -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)); diff --git a/src/components/GradebookFilters/messages.js b/src/components/GradebookFilters/messages.js new file mode 100644 index 0000000..317d800 --- /dev/null +++ b/src/components/GradebookFilters/messages.js @@ -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; diff --git a/src/components/GradebookFilters/test.jsx b/src/components/GradebookFilters/test.jsx index 3e0d826..3ba2ec5 100644 --- a/src/components/GradebookFilters/test.jsx +++ b/src/components/GradebookFilters/test.jsx @@ -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(), diff --git a/src/components/GradebookHeader/__snapshots__/test.jsx.snap b/src/components/GradebookHeader/__snapshots__/test.jsx.snap index 95223c4..8d27d3e 100644 --- a/src/components/GradebookHeader/__snapshots__/test.jsx.snap +++ b/src/components/GradebookHeader/__snapshots__/test.jsx.snap @@ -13,20 +13,31 @@ exports[`GradebookHeader component snapshots default values (grades frozen, cann > << - Back to Dashboard +

- Gradebook +

- fakeID

- You are not authorized to view the gradebook for this course. +
`; @@ -44,20 +55,31 @@ exports[`GradebookHeader component snapshots grades frozen, can view. grades fro > << - Back to Dashboard +

- Gradebook +

- fakeID

- The grades for this course are now frozen. Editing of grades is no longer allowed. +
`; @@ -75,26 +97,41 @@ exports[`GradebookHeader component snapshots grades frozen, cannot view unauthor > << - Back to Dashboard +

- Gradebook +

- fakeID

- The grades for this course are now frozen. Editing of grades is no longer allowed. +
- You are not authorized to view the gradebook for this course. +
`; diff --git a/src/components/GradebookHeader/index.jsx b/src/components/GradebookHeader/index.jsx index 848713a..8879d93 100644 --- a/src/components/GradebookHeader/index.jsx +++ b/src/components/GradebookHeader/index.jsx @@ -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" > - Back to Dashboard + + -

Gradebook

-

{this.props.courseId}

+

+ +

+

{this.props.courseId}

{this.props.areGradesFrozen && (
- The grades for this course are now frozen. Editing of grades is no longer allowed. +
)} {(this.props.canUserViewGradebook === false) && (
- You are not authorized to view the gradebook for this course. +
)}
diff --git a/src/components/GradebookHeader/messages.js b/src/components/GradebookHeader/messages.js new file mode 100644 index 0000000..aeb67f1 --- /dev/null +++ b/src/components/GradebookHeader/messages.js @@ -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; diff --git a/src/components/GradebookHeader/test.jsx b/src/components/GradebookHeader/test.jsx index ff77248..e943cc7 100644 --- a/src/components/GradebookHeader/test.jsx +++ b/src/components/GradebookHeader/test.jsx @@ -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: { diff --git a/src/components/GradesTab/BulkManagementControls.jsx b/src/components/GradesTab/BulkManagementControls.jsx index 633ea25..a735c1b 100644 --- a/src/components/GradesTab/BulkManagementControls.jsx +++ b/src/components/GradesTab/BulkManagementControls.jsx @@ -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 && (
)} onClick={this.handleClickExportGrades} /> )} onClick={this.handleClickDownloadInterventions} />
diff --git a/src/components/GradesTab/EditModal/HistoryHeader.jsx b/src/components/GradesTab/EditModal/HistoryHeader.jsx index e0413f8..6c944e4 100644 --- a/src/components/GradesTab/EditModal/HistoryHeader.jsx +++ b/src/components/GradesTab/EditModal/HistoryHeader.jsx @@ -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]), }; diff --git a/src/components/GradesTab/EditModal/ModalHeaders.jsx b/src/components/GradesTab/EditModal/ModalHeaders.jsx index 870f274..df52d98 100644 --- a/src/components/GradesTab/EditModal/ModalHeaders.jsx +++ b/src/components/GradesTab/EditModal/ModalHeaders.jsx @@ -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 = ({
} value={modalState.assignmentName} /> } value={modalState.updateUserName} /> } value={originalGrade} /> } value={currentGrade} />
diff --git a/src/components/GradesTab/EditModal/OverrideTable/__snapshots__/test.jsx.snap b/src/components/GradesTab/EditModal/OverrideTable/__snapshots__/test.jsx.snap index 58b0090..e916190 100644 --- a/src/components/GradesTab/EditModal/OverrideTable/__snapshots__/test.jsx.snap +++ b/src/components/GradesTab/EditModal/OverrideTable/__snapshots__/test.jsx.snap @@ -6,19 +6,35 @@ exports[`OverrideTable Component snapshots basic snapshot shows a row for each e Array [ Object { "key": "date", - "label": "Date", + "label": , }, Object { "key": "grader", - "label": "Grader", + "label": , }, Object { "key": "reason", - "label": "Reason", + "label": , }, Object { "key": "adjustedGrade", - "label": "Adjusted grade", + "label": , }, ] } diff --git a/src/components/GradesTab/EditModal/OverrideTable/index.jsx b/src/components/GradesTab/EditModal/OverrideTable/index.jsx index 0e58bdc..f06b706 100644 --- a/src/components/GradesTab/EditModal/OverrideTable/index.jsx +++ b/src/components/GradesTab/EditModal/OverrideTable/index.jsx @@ -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' }, -]; - /** * * Table containing previous grade override entries, and an "edit" row @@ -31,7 +28,15 @@ export const OverrideTable = ({ } return (
, key: columns.date }, + { label: , key: columns.grader }, + { label: , key: columns.reason }, + { + label: , + key: columns.adjustedGrade, + }, + ]} data={[ ...gradeOverrides, { diff --git a/src/components/GradesTab/EditModal/OverrideTable/messages.js b/src/components/GradesTab/EditModal/OverrideTable/messages.js new file mode 100644 index 0000000..7bf7ff5 --- /dev/null +++ b/src/components/GradesTab/EditModal/OverrideTable/messages.js @@ -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; diff --git a/src/components/GradesTab/EditModal/__snapshots__/ModalHeaders.test.jsx.snap b/src/components/GradesTab/EditModal/__snapshots__/ModalHeaders.test.jsx.snap index 8efe704..8b18422 100644 --- a/src/components/GradesTab/EditModal/__snapshots__/ModalHeaders.test.jsx.snap +++ b/src/components/GradesTab/EditModal/__snapshots__/ModalHeaders.test.jsx.snap @@ -4,22 +4,46 @@ exports[`ModalHeaders Component snapshots gradeOverrideHistoryError is and empty
+ } value="Qwerty" /> + } value="Uiop" /> + } value={20} /> + } value={2} />
@@ -29,22 +53,46 @@ exports[`ModalHeaders Component snapshots gradeOverrideHistoryError is empty and
+ } value="Qwerty" /> + } value="Uiop" /> + } value={20} /> + } value={2} />
diff --git a/src/components/GradesTab/EditModal/__snapshots__/test.jsx.snap b/src/components/GradesTab/EditModal/__snapshots__/test.jsx.snap index a5fc671..6b5d9ae 100644 --- a/src/components/GradesTab/EditModal/__snapshots__/test.jsx.snap +++ b/src/components/GradesTab/EditModal/__snapshots__/test.jsx.snap @@ -13,10 +13,18 @@ exports[`EditMoal Component snapshots gradeOverrideHistoryError is and empty and />
- Showing most recent actions (max 5). To see more, please contact support. +
- Note: Once you save, your changes will be visible to students. +
} @@ -26,14 +34,30 @@ exports[`EditMoal Component snapshots gradeOverrideHistoryError is and empty and onClick={[MockFunction this.handleAdjustedGradeClick]} variant="primary" > - Save Grade + , ] } - closeText="Cancel" + closeText={ + + } onClose={[MockFunction this.closeAssignmentModal]} open={true} - title="Edit Grades" + title={ + + } /> `; @@ -50,10 +74,18 @@ exports[`EditMoal Component snapshots gradeOverrideHistoryError is empty and ope />
- Showing most recent actions (max 5). To see more, please contact support. +
- Note: Once you save, your changes will be visible to students. +
} @@ -63,13 +95,29 @@ exports[`EditMoal Component snapshots gradeOverrideHistoryError is empty and ope onClick={[MockFunction this.handleAdjustedGradeClick]} variant="primary" > - Save Grade + , ] } - closeText="Cancel" + closeText={ + + } onClose={[MockFunction this.closeAssignmentModal]} open={false} - title="Edit Grades" + title={ + + } /> `; diff --git a/src/components/GradesTab/EditModal/index.jsx b/src/components/GradesTab/EditModal/index.jsx index 85e281e..dcf911d 100644 --- a/src/components/GradesTab/EditModal/index.jsx +++ b/src/components/GradesTab/EditModal/index.jsx @@ -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 ( } + closeText={} body={(
@@ -58,15 +60,13 @@ export class EditModal extends React.Component { dismissible={false} /> -
Showing most recent actions (max 5). To see more, please contact - support. -
-
Note: Once you save, your changes will be visible to students.
+
+
)} buttons={[ , ]} onClose={this.closeAssignmentModal} diff --git a/src/components/GradesTab/EditModal/messages.js b/src/components/GradesTab/EditModal/messages.js new file mode 100644 index 0000000..f59d600 --- /dev/null +++ b/src/components/GradesTab/EditModal/messages.js @@ -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; diff --git a/src/components/GradesTab/FilterBadges/FilterBadge.jsx b/src/components/GradesTab/FilterBadges/FilterBadge.jsx index a55efe0..d6f5dbe 100644 --- a/src/components/GradesTab/FilterBadges/FilterBadge.jsx +++ b/src/components/GradesTab/FilterBadges/FilterBadge.jsx @@ -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 && (
- {displayName}{!hideValue && `: ${value}`} + + + + {!hideValue ? `: ${value}` : ''}
`; @@ -55,7 +63,11 @@ exports[`PageButtons component snapshots nextPage disabled if not provided 1`] = } variant="outline-primary" > - Previous Page + `; @@ -91,7 +107,11 @@ exports[`PageButtons component snapshots prevPage disabled if not provided 1`] = } variant="outline-primary" > - Previous Page + `; diff --git a/src/components/GradesTab/PageButtons/index.jsx b/src/components/GradesTab/PageButtons/index.jsx index 3bf1c95..0c2fb1b 100644 --- a/src/components/GradesTab/PageButtons/index.jsx +++ b/src/components/GradesTab/PageButtons/index.jsx @@ -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 + ); diff --git a/src/components/GradesTab/PageButtons/messages.js b/src/components/GradesTab/PageButtons/messages.js new file mode 100644 index 0000000..5531a1e --- /dev/null +++ b/src/components/GradesTab/PageButtons/messages.js @@ -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; diff --git a/src/components/GradesTab/ScoreViewInput.jsx b/src/components/GradesTab/ScoreViewInput.jsx index b1e34a0..7412087 100644 --- a/src/components/GradesTab/ScoreViewInput.jsx +++ b/src/components/GradesTab/ScoreViewInput.jsx @@ -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'; /** * * redux-connected select control for grade format (percent vs absolute) */ -export const ScoreViewInput = ({ format, toggleFormat }) => ( +export const ScoreViewInput = ({ format, intl, toggleFormat }) => ( - Score View: + : - - + + ); @@ -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)); diff --git a/src/components/GradesTab/ScoreViewInput.test.jsx b/src/components/GradesTab/ScoreViewInput.test.jsx index 15083db..1c73418 100644 --- a/src/components/GradesTab/ScoreViewInput.test.jsx +++ b/src/components/GradesTab/ScoreViewInput.test.jsx @@ -35,6 +35,7 @@ describe('ScoreViewInput', () => { let el; beforeEach(() => { props.toggleFormat = jest.fn(); + props.intl = { formatMessage: (msg) => msg.defaultMessage }; el = shallow(); }); const assertions = [ diff --git a/src/components/GradesTab/SearchControls.jsx b/src/components/GradesTab/SearchControls.jsx index 49fb324..72a764d 100644 --- a/src/components/GradesTab/SearchControls.jsx +++ b/src/components/GradesTab/SearchControls.jsx @@ -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 ( <> -

Step 1: Filter the Grade Report

+

} onChange={this.onChange} onClear={this.onClear} value={this.props.searchValue} /> - Search by username, email, or student key +
diff --git a/src/components/GradesTab/StatusAlerts.jsx b/src/components/GradesTab/StatusAlerts.jsx index 14658c9..9da3ba3 100644 --- a/src/components/GradesTab/StatusAlerts.jsx +++ b/src/components/GradesTab/StatusAlerts.jsx @@ -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) + ? '' + : ; + } + + get maxValidityMessage() { + return (this.props.limitValidity.isMaxValid) + ? '' + : ; + } + 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 { <> } onClose={this.props.handleCloseSuccessBanner} open={this.props.showSuccessBanner} /> diff --git a/src/components/GradesTab/StatusAlerts.test.jsx b/src/components/GradesTab/StatusAlerts.test.jsx index d9b9b8e..0088068 100644 --- a/src/components/GradesTab/StatusAlerts.test.jsx +++ b/src/components/GradesTab/StatusAlerts.test.jsx @@ -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( + <> + + + , + ); + } else { + expect( + el.instance().courseGradeFilterAlertDialogText, + // eslint-disable-next-line react/jsx-curly-brace-presence + ).toEqual(<>{''}); + } + } 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(<>{''}); } }); }); diff --git a/src/components/GradesTab/UsersLabel.jsx b/src/components/GradesTab/UsersLabel.jsx index 9a2999c..ac6df0f 100644 --- a/src/components/GradesTab/UsersLabel.jsx +++ b/src/components/GradesTab/UsersLabel.jsx @@ -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) => ({val}); return ( - <> - Showing {bold(filteredUsersCount)} of {bold(totalUsersCount)} total learners - + ); }; UsersLabel.propTypes = { diff --git a/src/components/GradesTab/__snapshots__/ScoreViewInput.test.jsx.snap b/src/components/GradesTab/__snapshots__/ScoreViewInput.test.jsx.snap index df8a230..061bc26 100644 --- a/src/components/GradesTab/__snapshots__/ScoreViewInput.test.jsx.snap +++ b/src/components/GradesTab/__snapshots__/ScoreViewInput.test.jsx.snap @@ -5,7 +5,12 @@ exports[`ScoreViewInput component snapshot - select box with percent and absolut controlId="ScoreView" > - Score View: + + :

- Step 1: Filter the Grade Report +

- Edit Filters + +
+ } onChange={[MockFunction onChange]} onClear={[MockFunction onClear]} onSubmit={[MockFunction fetchGrades]} @@ -29,7 +44,11 @@ exports[`SearchControls Component Snapshots basic snapshot 1`] = ` - Search by username, email, or student key +
diff --git a/src/components/GradesTab/__snapshots__/StatusAlerts.test.jsx.snap b/src/components/GradesTab/__snapshots__/StatusAlerts.test.jsx.snap index eaed627..4c54341 100644 --- a/src/components/GradesTab/__snapshots__/StatusAlerts.test.jsx.snap +++ b/src/components/GradesTab/__snapshots__/StatusAlerts.test.jsx.snap @@ -4,7 +4,13 @@ exports[`StatusAlerts snapshots basic snapshot 1`] = ` + } onClose={[MockFunction handleCloseSuccessBanner]} open={true} /> diff --git a/src/components/GradesTab/__snapshots__/UsersLabel.test.jsx.snap b/src/components/GradesTab/__snapshots__/UsersLabel.test.jsx.snap index b60d7fe..9263159 100644 --- a/src/components/GradesTab/__snapshots__/UsersLabel.test.jsx.snap +++ b/src/components/GradesTab/__snapshots__/UsersLabel.test.jsx.snap @@ -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`] = ` - - Showing - - 23 - - of - - 140 - - total learners - + + 23 + , + "totalUsers": + 140 + , + } + } +/> `; diff --git a/src/components/GradesTab/__snapshots__/test.jsx.snap b/src/components/GradesTab/__snapshots__/test.jsx.snap index f3b2627..dd149a7 100644 --- a/src/components/GradesTab/__snapshots__/test.jsx.snap +++ b/src/components/GradesTab/__snapshots__/test.jsx.snap @@ -9,7 +9,11 @@ exports[`GradesTab Component snapshots basic snapshot 1`] = ` />

- Step 2: View or Modify Individual Grades +

- * available for learners in the Master's track only + * +

diff --git a/src/components/GradesTab/index.jsx b/src/components/GradesTab/index.jsx index 979895b..951dc3f 100644 --- a/src/components/GradesTab/index.jsx +++ b/src/components/GradesTab/index.jsx @@ -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 { -

Step 2: View or Modify Individual Grades

+

@@ -54,7 +57,7 @@ export class GradesTab extends React.Component { -

* available for learners in the Master's track only

+

*

); diff --git a/src/components/GradesTab/messages.js b/src/components/GradesTab/messages.js new file mode 100644 index 0000000..1045a5f --- /dev/null +++ b/src/components/GradesTab/messages.js @@ -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; diff --git a/src/data/constants/app.js b/src/data/constants/app.js index 196902f..97d709b 100644 --- a/src/data/constants/app.js +++ b/src/data/constants/app.js @@ -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 diff --git a/src/data/constants/filters.js b/src/data/constants/filters.js index afac9bb..9db88dc 100644 --- a/src/data/constants/filters.js +++ b/src/data/constants/filters.js @@ -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'], }, }); diff --git a/src/data/constants/filters.messages.js b/src/data/constants/filters.messages.js new file mode 100644 index 0000000..6a5f619 --- /dev/null +++ b/src/data/constants/filters.messages.js @@ -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; diff --git a/src/data/selectors/grades.js b/src/data/selectors/grades.js index a8bf471..891a49e 100644 --- a/src/data/selectors/grades.js +++ b/src/data/selectors/grades.js @@ -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] : [] ); }; diff --git a/src/i18n/index.jsx b/src/i18n/index.jsx new file mode 100644 index 0000000..e030d88 --- /dev/null +++ b/src/i18n/index.jsx @@ -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; diff --git a/src/i18n/messages/ar.json b/src/i18n/messages/ar.json new file mode 100644 index 0000000..2c63c08 --- /dev/null +++ b/src/i18n/messages/ar.json @@ -0,0 +1,2 @@ +{ +} diff --git a/src/i18n/messages/es_419.json b/src/i18n/messages/es_419.json new file mode 100644 index 0000000..2c63c08 --- /dev/null +++ b/src/i18n/messages/es_419.json @@ -0,0 +1,2 @@ +{ +} diff --git a/src/i18n/messages/fr.json b/src/i18n/messages/fr.json new file mode 100644 index 0000000..2c63c08 --- /dev/null +++ b/src/i18n/messages/fr.json @@ -0,0 +1,2 @@ +{ +} diff --git a/src/i18n/messages/zh_CN.json b/src/i18n/messages/zh_CN.json new file mode 100644 index 0000000..2c63c08 --- /dev/null +++ b/src/i18n/messages/zh_CN.json @@ -0,0 +1,2 @@ +{ +} diff --git a/src/i18n/transifex_input.json b/src/i18n/transifex_input.json new file mode 100644 index 0000000..6f33e58 --- /dev/null +++ b/src/i18n/transifex_input.json @@ -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." +} \ No newline at end of file diff --git a/src/index.jsx b/src/index.jsx index c86d3f4..682c905 100755 --- a/src/index.jsx +++ b/src/index.jsx @@ -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, diff --git a/src/index.test.jsx b/src/index.test.jsx index ab70d7d..b4fdf99 100644 --- a/src/index.test.jsx +++ b/src/index.test.jsx @@ -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, }); }); diff --git a/src/setupTest.js b/src/setupTest.js index 70c7014..99a30d7 100755 --- a/src/setupTest.js +++ b/src/setupTest.js @@ -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', + }; +});