Compare commits
30 Commits
open-relea
...
bw/compone
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8e19b5774c | ||
|
|
f7a4309888 | ||
|
|
3e3a73e2bb | ||
|
|
af8d7182ef | ||
|
|
d898a9cc2f | ||
|
|
f4b839f4d8 | ||
|
|
f41b237d08 | ||
|
|
6dd2fb3dd6 | ||
|
|
b173681edb | ||
|
|
5fcde3b9e8 | ||
|
|
35ee68ea9d | ||
|
|
dde8e759b6 | ||
|
|
6b149e9ce0 | ||
|
|
4cf5ba7a07 | ||
|
|
7a506324a8 | ||
|
|
7f54cc4917 | ||
|
|
134ace9483 | ||
|
|
0e6f52fca9 | ||
|
|
ca64cc614a | ||
|
|
1ad297c46c | ||
|
|
f2bb0d7c2a | ||
|
|
f76f3d64c9 | ||
|
|
db56d76d37 | ||
|
|
ffecce993e | ||
|
|
ae1702d182 | ||
|
|
67789481fb | ||
|
|
543cd623e1 | ||
|
|
ba31b713e2 | ||
|
|
84fe2c6628 | ||
|
|
b87447b543 |
1
.env
1
.env
@@ -32,3 +32,4 @@ ENTERPRISE_MARKETING_UTM_CAMPAIGN=''
|
||||
ENTERPRISE_MARKETING_FOOTER_UTM_MEDIUM=''
|
||||
APP_ID=''
|
||||
MFE_CONFIG_API_URL=''
|
||||
DISPLAY_FEEDBACK_WIDGET='true'
|
||||
|
||||
@@ -39,3 +39,4 @@ ENTERPRISE_MARKETING_UTM_CAMPAIGN='example.com Referral'
|
||||
ENTERPRISE_MARKETING_FOOTER_UTM_MEDIUM='Footer'
|
||||
APP_ID=''
|
||||
MFE_CONFIG_API_URL=''
|
||||
DISPLAY_FEEDBACK_WIDGET='false'
|
||||
|
||||
@@ -3,3 +3,4 @@ dist/
|
||||
node_modules/
|
||||
src/postcss.config.js
|
||||
src/segment.js
|
||||
src/lightning.js
|
||||
|
||||
16
.github/workflows/ci.yml
vendored
16
.github/workflows/ci.yml
vendored
@@ -11,18 +11,22 @@ on:
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-20.04
|
||||
strategy:
|
||||
matrix:
|
||||
node: [16]
|
||||
npm: [8.5.x]
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Setup Nodejs Env
|
||||
run: echo "NODE_VER=`cat .nvmrc`" >> $GITHUB_ENV
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Setup Nodejs
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: ${{ env.NODE_VER }}
|
||||
node-version: ${{ matrix.node }}
|
||||
|
||||
- name: Install npm 8.5.x
|
||||
run: npm install -g npm@${{ matrix.npm }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
2
.github/workflows/lockfileversion-check.yml
vendored
2
.github/workflows/lockfileversion-check.yml
vendored
@@ -10,4 +10,4 @@ on:
|
||||
|
||||
jobs:
|
||||
version-check:
|
||||
uses: openedx/.github/.github/workflows/lockfileversion-check-v3.yml@master
|
||||
uses: openedx/.github/.github/workflows/lockfileversion-check.yml@master
|
||||
|
||||
5
.github/workflows/npm-publish.yml
vendored
5
.github/workflows/npm-publish.yml
vendored
@@ -15,13 +15,10 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Nodejs Env
|
||||
run: echo "NODE_VER=`cat .nvmrc`" >> $GITHUB_ENV
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: ${{ env.NODE_VER }}
|
||||
node-version: 12
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
15
Makefile
15
Makefile
@@ -4,6 +4,7 @@ npm-install-%: ## install specified % npm package
|
||||
export TRANSIFEX_RESOURCE = frontend-app-gradebook
|
||||
transifex_langs = "ar,de,es_419,fa_IR,fr,fr_CA,hi,it,pt,ru,uk,zh_CN"
|
||||
|
||||
intl_imports = ./node_modules/.bin/intl-imports.js
|
||||
transifex_utils = ./node_modules/.bin/transifex-utils.js
|
||||
i18n = ./src/i18n
|
||||
transifex_input = $(i18n)/transifex_input.json
|
||||
@@ -54,9 +55,23 @@ push_translations:
|
||||
# Pushing comments to Transifex...
|
||||
./node_modules/@edx/reactifex/bash_scripts/put_comments_v3.sh
|
||||
|
||||
ifeq ($(OPENEDX_ATLAS_PULL),)
|
||||
# Pulls translations from Transifex.
|
||||
pull_translations:
|
||||
tx pull -t -f --mode reviewed --languages=$(transifex_langs)
|
||||
else
|
||||
# Experimental: OEP-58 Pulls translations using atlas
|
||||
pull_translations:
|
||||
rm -rf src/i18n/messages
|
||||
mkdir src/i18n/messages
|
||||
cd src/i18n/messages \
|
||||
&& atlas pull --filter=$(transifex_langs) \
|
||||
translations/frontend-component-footer/src/i18n/messages:frontend-component-footer \
|
||||
translations/frontend-component-header/src/i18n/messages:frontend-component-header \
|
||||
translations/frontend-app-gradebook/src/i18n/messages:frontend-app-gradebook
|
||||
|
||||
$(intl_imports) frontend-component-header frontend-component-footer frontend-app-gradebook
|
||||
endif
|
||||
|
||||
# This target is used by CI.
|
||||
validate-no-uncommitted-package-lock-changes:
|
||||
|
||||
31211
package-lock.json
generated
31211
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
17
package.json
17
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@edx/frontend-app-gradebook",
|
||||
"version": "1.6.1",
|
||||
"version": "1.6.2",
|
||||
"description": "edx editable gradebook-ui to manipulate grade overrides on subsections",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -28,10 +28,10 @@
|
||||
"extends @edx/browserslist-config"
|
||||
],
|
||||
"dependencies": {
|
||||
"@edx/brand": "npm:@edx/brand-edx.org@^1.3.2",
|
||||
"@edx/frontend-component-footer": "^11.1.1",
|
||||
"@edx/frontend-component-header": "^3.1.1",
|
||||
"@edx/frontend-platform": "2.5.0",
|
||||
"@edx/brand": "npm:@edx/brand-openedx@^1.2.0",
|
||||
"@edx/frontend-component-footer": "^12.0.0",
|
||||
"@edx/frontend-component-header": "^4.0.0",
|
||||
"@edx/frontend-platform": "^4.2.0",
|
||||
"@edx/paragon": "^19.25.4",
|
||||
"@edx/reactifex": "^2.1.1",
|
||||
"@fortawesome/fontawesome-svg-core": "^1.2.25",
|
||||
@@ -47,12 +47,11 @@
|
||||
"enzyme-to-json": "^3.6.2",
|
||||
"font-awesome": "4.7.0",
|
||||
"history": "4.10.1",
|
||||
"prop-types": "15.7.2",
|
||||
"prop-types": "15.8.1",
|
||||
"query-string": "6.13.0",
|
||||
"react": "16.14.0",
|
||||
"react-dom": "16.14.0",
|
||||
"react-helmet": "^6.1.0",
|
||||
"react-intl": "^2.9.0",
|
||||
"react-redux": "^7.1.1",
|
||||
"react-router": "5.2.0",
|
||||
"react-router-dom": "5.2.0",
|
||||
@@ -68,7 +67,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@edx/browserslist-config": "^1.1.1",
|
||||
"@edx/frontend-build": "12.8.27",
|
||||
"@edx/frontend-build": "^12.4.15",
|
||||
"@testing-library/react": "^12.1.0",
|
||||
"axios": "0.21.2",
|
||||
"axios-mock-adapter": "^1.17.0",
|
||||
@@ -77,7 +76,7 @@
|
||||
"fetch-mock": "^6.5.2",
|
||||
"husky": "2.7.0",
|
||||
"identity-obj-proxy": "^3.0.0",
|
||||
"jest": "29.3.1",
|
||||
"jest": "^26.6.3",
|
||||
"react-dev-utils": "^12.0.1",
|
||||
"react-test-renderer": "^16.10.1",
|
||||
"reactifex": "1.1.1",
|
||||
|
||||
@@ -16,7 +16,7 @@ exports[`GradebookFilters render snapshot 1`] = `
|
||||
className="p-1"
|
||||
iconAs="Icon"
|
||||
onClick={[MockFunction hook.closeMenu]}
|
||||
src={[Function]}
|
||||
src="Close"
|
||||
/>
|
||||
</div>
|
||||
<Collapsible
|
||||
|
||||
@@ -3,7 +3,7 @@ import { actions, selectors, thunkActions } from 'data/redux/hooks';
|
||||
export const useGradebookFiltersData = ({ updateQueryParams }) => {
|
||||
const includeCourseRoleMembers = selectors.filters.useIncludeCourseRoleMembers();
|
||||
const updateIncludeCourseRoleMembers = actions.filters.useUpdateIncludeCourseRoleMembers();
|
||||
const closeMenu = thunkActions.app.useCloseFilterMenu();
|
||||
const closeMenu = thunkActions.app.filterMenu.useCloseMenu();
|
||||
const fetchGrades = thunkActions.grades.useFetchGrades();
|
||||
|
||||
const handleIncludeTeamMembersChange = ({ target: { checked } }) => {
|
||||
|
||||
@@ -9,7 +9,9 @@ jest.mock('data/redux/hooks', () => ({
|
||||
filters: { useIncludeCourseRoleMembers: jest.fn() },
|
||||
},
|
||||
thunkActions: {
|
||||
app: { useCloseFilterMenu: jest.fn() },
|
||||
app: {
|
||||
filterMenu: { useCloseMenu: jest.fn() },
|
||||
},
|
||||
grades: { useFetchGrades: jest.fn() },
|
||||
},
|
||||
}));
|
||||
@@ -18,7 +20,7 @@ selectors.filters.useIncludeCourseRoleMembers.mockReturnValue(true);
|
||||
const updateIncludeCourseRoleMembers = jest.fn();
|
||||
actions.filters.useUpdateIncludeCourseRoleMembers.mockReturnValue(updateIncludeCourseRoleMembers);
|
||||
const closeFilterMenu = jest.fn();
|
||||
thunkActions.app.useCloseFilterMenu.mockReturnValue(closeFilterMenu);
|
||||
thunkActions.app.filterMenu.useCloseMenu.mockReturnValue(closeFilterMenu);
|
||||
const fetchGrades = jest.fn();
|
||||
thunkActions.grades.useFetchGrades.mockReturnValue(fetchGrades);
|
||||
|
||||
@@ -34,7 +36,7 @@ describe('GradebookFiltersData component hooks', () => {
|
||||
it('initializes hooks', () => {
|
||||
expect(actions.filters.useUpdateIncludeCourseRoleMembers).toHaveBeenCalledWith();
|
||||
expect(selectors.filters.useIncludeCourseRoleMembers).toHaveBeenCalledWith();
|
||||
expect(thunkActions.app.useCloseFilterMenu).toHaveBeenCalledWith();
|
||||
expect(thunkActions.app.filterMenu.useCloseMenu).toHaveBeenCalledWith();
|
||||
expect(thunkActions.grades.useFetchGrades).toHaveBeenCalledWith();
|
||||
});
|
||||
});
|
||||
|
||||
131
src/components/GradebookHeader/__snapshots__/index.test.jsx.snap
Normal file
131
src/components/GradebookHeader/__snapshots__/index.test.jsx.snap
Normal file
@@ -0,0 +1,131 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`GradebookHeader component render default view shapshot 1`] = `
|
||||
<div
|
||||
className="gradebook-header"
|
||||
>
|
||||
<a
|
||||
className="mb-3"
|
||||
href="test-dashboard-url"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
>
|
||||
<<
|
||||
</span>
|
||||
Back to Dashboard
|
||||
</a>
|
||||
<h1>
|
||||
Gradebook
|
||||
</h1>
|
||||
<div
|
||||
className="subtitle-row d-flex justify-content-between align-items-center"
|
||||
>
|
||||
<h2>
|
||||
test-course-id
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`GradebookHeader component render frozen grades snapshot: show frozen warning 1`] = `
|
||||
<div
|
||||
className="gradebook-header"
|
||||
>
|
||||
<a
|
||||
className="mb-3"
|
||||
href="test-dashboard-url"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
>
|
||||
<<
|
||||
</span>
|
||||
Back to Dashboard
|
||||
</a>
|
||||
<h1>
|
||||
Gradebook
|
||||
</h1>
|
||||
<div
|
||||
className="subtitle-row d-flex justify-content-between align-items-center"
|
||||
>
|
||||
<h2>
|
||||
test-course-id
|
||||
</h2>
|
||||
</div>
|
||||
<div
|
||||
className="alert alert-warning"
|
||||
role="alert"
|
||||
>
|
||||
The grades for this course are now frozen. Editing of grades is no longer allowed.
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`GradebookHeader component render show bulk management snapshot: show toggle view message button with handleToggleViewClick method 1`] = `
|
||||
<div
|
||||
className="gradebook-header"
|
||||
>
|
||||
<a
|
||||
className="mb-3"
|
||||
href="test-dashboard-url"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
>
|
||||
<<
|
||||
</span>
|
||||
Back to Dashboard
|
||||
</a>
|
||||
<h1>
|
||||
Gradebook
|
||||
</h1>
|
||||
<div
|
||||
className="subtitle-row d-flex justify-content-between align-items-center"
|
||||
>
|
||||
<h2>
|
||||
test-course-id
|
||||
</h2>
|
||||
<Button
|
||||
onClick={[MockFunction hooks.handleToggleViewClick]}
|
||||
variant="tertiary"
|
||||
>
|
||||
toggle-view-message
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`GradebookHeader component render user cannot view gradebook snapshot: show unauthorized warning 1`] = `
|
||||
<div
|
||||
className="gradebook-header"
|
||||
>
|
||||
<a
|
||||
className="mb-3"
|
||||
href="test-dashboard-url"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
>
|
||||
<<
|
||||
</span>
|
||||
Back to Dashboard
|
||||
</a>
|
||||
<h1>
|
||||
Gradebook
|
||||
</h1>
|
||||
<div
|
||||
className="subtitle-row d-flex justify-content-between align-items-center"
|
||||
>
|
||||
<h2>
|
||||
test-course-id
|
||||
</h2>
|
||||
</div>
|
||||
<div
|
||||
className="alert alert-warning"
|
||||
role="alert"
|
||||
>
|
||||
You are not authorized to view the gradebook for this course.
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -1,261 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`GradebookHeader component snapshots default values (grades frozen, cannot view). unauthorized warning, but no grades frozen warning 1`] = `
|
||||
<div
|
||||
className="gradebook-header"
|
||||
>
|
||||
<a
|
||||
className="mb-3"
|
||||
href="http://localhost:18000/courses/fakeID/instructor"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
>
|
||||
<<
|
||||
</span>
|
||||
<FormattedMessage
|
||||
defaultMessage="Back to Dashboard"
|
||||
description="Button text to take user back to LMS dashboard in Gradebook Header"
|
||||
id="gradebook.GradebookHeader.backButton"
|
||||
/>
|
||||
</a>
|
||||
<h1>
|
||||
<FormattedMessage
|
||||
defaultMessage="Gradebook"
|
||||
description="Top-level app title in Gradebook Header component"
|
||||
id="gradebook.GradebookHeader.appLabel"
|
||||
/>
|
||||
</h1>
|
||||
<div
|
||||
className="subtitle-row d-flex justify-content-between align-items-center"
|
||||
>
|
||||
<h2>
|
||||
fakeID
|
||||
</h2>
|
||||
</div>
|
||||
<div
|
||||
className="alert alert-warning"
|
||||
role="alert"
|
||||
>
|
||||
<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>
|
||||
`;
|
||||
|
||||
exports[`GradebookHeader component snapshots grades frozen, can view. grades frozen warning but no unauthorized warning 1`] = `
|
||||
<div
|
||||
className="gradebook-header"
|
||||
>
|
||||
<a
|
||||
className="mb-3"
|
||||
href="http://localhost:18000/courses/fakeID/instructor"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
>
|
||||
<<
|
||||
</span>
|
||||
<FormattedMessage
|
||||
defaultMessage="Back to Dashboard"
|
||||
description="Button text to take user back to LMS dashboard in Gradebook Header"
|
||||
id="gradebook.GradebookHeader.backButton"
|
||||
/>
|
||||
</a>
|
||||
<h1>
|
||||
<FormattedMessage
|
||||
defaultMessage="Gradebook"
|
||||
description="Top-level app title in Gradebook Header component"
|
||||
id="gradebook.GradebookHeader.appLabel"
|
||||
/>
|
||||
</h1>
|
||||
<div
|
||||
className="subtitle-row d-flex justify-content-between align-items-center"
|
||||
>
|
||||
<h2>
|
||||
fakeID
|
||||
</h2>
|
||||
</div>
|
||||
<div
|
||||
className="alert alert-warning"
|
||||
role="alert"
|
||||
>
|
||||
<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>
|
||||
`;
|
||||
|
||||
exports[`GradebookHeader component snapshots grades frozen, cannot view unauthorized warning, and grades frozen warning. 1`] = `
|
||||
<div
|
||||
className="gradebook-header"
|
||||
>
|
||||
<a
|
||||
className="mb-3"
|
||||
href="http://localhost:18000/courses/fakeID/instructor"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
>
|
||||
<<
|
||||
</span>
|
||||
<FormattedMessage
|
||||
defaultMessage="Back to Dashboard"
|
||||
description="Button text to take user back to LMS dashboard in Gradebook Header"
|
||||
id="gradebook.GradebookHeader.backButton"
|
||||
/>
|
||||
</a>
|
||||
<h1>
|
||||
<FormattedMessage
|
||||
defaultMessage="Gradebook"
|
||||
description="Top-level app title in Gradebook Header component"
|
||||
id="gradebook.GradebookHeader.appLabel"
|
||||
/>
|
||||
</h1>
|
||||
<div
|
||||
className="subtitle-row d-flex justify-content-between align-items-center"
|
||||
>
|
||||
<h2>
|
||||
fakeID
|
||||
</h2>
|
||||
</div>
|
||||
<div
|
||||
className="alert alert-warning"
|
||||
role="alert"
|
||||
>
|
||||
<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"
|
||||
>
|
||||
<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>
|
||||
`;
|
||||
|
||||
exports[`GradebookHeader component snapshots show bulk management, active view is bulkManagementHistory view toggle view button to grades 1`] = `
|
||||
<div
|
||||
className="gradebook-header"
|
||||
>
|
||||
<a
|
||||
className="mb-3"
|
||||
href="http://localhost:18000/courses/fakeID/instructor"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
>
|
||||
<<
|
||||
</span>
|
||||
<FormattedMessage
|
||||
defaultMessage="Back to Dashboard"
|
||||
description="Button text to take user back to LMS dashboard in Gradebook Header"
|
||||
id="gradebook.GradebookHeader.backButton"
|
||||
/>
|
||||
</a>
|
||||
<h1>
|
||||
<FormattedMessage
|
||||
defaultMessage="Gradebook"
|
||||
description="Top-level app title in Gradebook Header component"
|
||||
id="gradebook.GradebookHeader.appLabel"
|
||||
/>
|
||||
</h1>
|
||||
<div
|
||||
className="subtitle-row d-flex justify-content-between align-items-center"
|
||||
>
|
||||
<h2>
|
||||
fakeID
|
||||
</h2>
|
||||
<Button
|
||||
onClick={[MockFunction this.handleToggleViewClick]}
|
||||
variant="tertiary"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Return to Gradebook"
|
||||
description="Button text for button navigating to Grades view."
|
||||
id="gradebook.GradebookHeader.toGradesView"
|
||||
/>
|
||||
</Button>
|
||||
</div>
|
||||
<div
|
||||
className="alert alert-warning"
|
||||
role="alert"
|
||||
>
|
||||
<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>
|
||||
`;
|
||||
|
||||
exports[`GradebookHeader component snapshots show bulk management, active view is grades view toggle view button to activity log 1`] = `
|
||||
<div
|
||||
className="gradebook-header"
|
||||
>
|
||||
<a
|
||||
className="mb-3"
|
||||
href="http://localhost:18000/courses/fakeID/instructor"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
>
|
||||
<<
|
||||
</span>
|
||||
<FormattedMessage
|
||||
defaultMessage="Back to Dashboard"
|
||||
description="Button text to take user back to LMS dashboard in Gradebook Header"
|
||||
id="gradebook.GradebookHeader.backButton"
|
||||
/>
|
||||
</a>
|
||||
<h1>
|
||||
<FormattedMessage
|
||||
defaultMessage="Gradebook"
|
||||
description="Top-level app title in Gradebook Header component"
|
||||
id="gradebook.GradebookHeader.appLabel"
|
||||
/>
|
||||
</h1>
|
||||
<div
|
||||
className="subtitle-row d-flex justify-content-between align-items-center"
|
||||
>
|
||||
<h2>
|
||||
fakeID
|
||||
</h2>
|
||||
<Button
|
||||
onClick={[MockFunction this.handleToggleViewClick]}
|
||||
variant="tertiary"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="View Bulk Management History"
|
||||
description="Button text for button navigating to Bulk Managment Activity Log"
|
||||
id="gradebook.GradebookHeader.toActivityLogButton"
|
||||
/>
|
||||
</Button>
|
||||
</div>
|
||||
<div
|
||||
className="alert alert-warning"
|
||||
role="alert"
|
||||
>
|
||||
<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>
|
||||
`;
|
||||
35
src/components/GradebookHeader/hooks.js
Normal file
35
src/components/GradebookHeader/hooks.js
Normal file
@@ -0,0 +1,35 @@
|
||||
import { views } from 'data/constants/app';
|
||||
import { actions, selectors } from 'data/redux/hooks';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
export const useGradebookHeaderData = () => {
|
||||
const activeView = selectors.app.useActiveView();
|
||||
const courseId = selectors.app.useCourseId();
|
||||
const areGradesFrozen = selectors.assignmentTypes.useAreGradesFrozen();
|
||||
const canUserViewGradebook = selectors.roles.useCanUserViewGradebook();
|
||||
const showBulkManagement = selectors.root.useShowBulkManagement();
|
||||
const setView = actions.app.useSetView();
|
||||
|
||||
const handleToggleViewClick = () => setView(
|
||||
activeView === views.grades
|
||||
? views.bulkManagementHistory
|
||||
: views.grades,
|
||||
);
|
||||
|
||||
const toggleViewMessage = activeView === views.grades
|
||||
? messages.toActivityLog
|
||||
: messages.toGradesView;
|
||||
|
||||
return {
|
||||
areGradesFrozen,
|
||||
canUserViewGradebook,
|
||||
courseId,
|
||||
showBulkManagement,
|
||||
|
||||
handleToggleViewClick,
|
||||
toggleViewMessage,
|
||||
};
|
||||
};
|
||||
|
||||
export default useGradebookHeaderData;
|
||||
90
src/components/GradebookHeader/hooks.test.js
Normal file
90
src/components/GradebookHeader/hooks.test.js
Normal file
@@ -0,0 +1,90 @@
|
||||
import { views } from 'data/constants/app';
|
||||
import { actions, selectors } from 'data/redux/hooks';
|
||||
|
||||
import messages from './messages';
|
||||
import useGradebookHeaderData from './hooks';
|
||||
|
||||
jest.mock('data/redux/hooks', () => ({
|
||||
actions: {
|
||||
app: {
|
||||
useSetView: jest.fn(),
|
||||
},
|
||||
},
|
||||
selectors: {
|
||||
app: {
|
||||
useActiveView: jest.fn(),
|
||||
useCourseId: jest.fn(),
|
||||
},
|
||||
assignmentTypes: {
|
||||
useAreGradesFrozen: jest.fn(),
|
||||
},
|
||||
roles: {
|
||||
useCanUserViewGradebook: jest.fn(),
|
||||
},
|
||||
root: {
|
||||
useShowBulkManagement: jest.fn(),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
const activeView = 'test-active-view';
|
||||
selectors.app.useActiveView.mockReturnValue(activeView);
|
||||
const courseId = 'test-course-id';
|
||||
selectors.app.useCourseId.mockReturnValue(courseId);
|
||||
const areGradesFrozen = 'test-are-grades-frozen';
|
||||
selectors.assignmentTypes.useAreGradesFrozen.mockReturnValue(areGradesFrozen);
|
||||
const canUserViewGradebook = 'test-can-user-view-gradebook';
|
||||
selectors.roles.useCanUserViewGradebook.mockReturnValue(canUserViewGradebook);
|
||||
const showBulkManagement = 'test-show-bulk-management';
|
||||
selectors.root.useShowBulkManagement.mockReturnValue(showBulkManagement);
|
||||
|
||||
const setView = jest.fn();
|
||||
actions.app.useSetView.mockReturnValue(setView);
|
||||
|
||||
let out;
|
||||
describe('useGradebookHeaderData hooks', () => {
|
||||
describe('initialization', () => {
|
||||
it('initializes redux hooks', () => {
|
||||
out = useGradebookHeaderData();
|
||||
expect(selectors.app.useActiveView).toHaveBeenCalled();
|
||||
expect(selectors.app.useCourseId).toHaveBeenCalled();
|
||||
expect(selectors.assignmentTypes.useAreGradesFrozen).toHaveBeenCalled();
|
||||
expect(selectors.roles.useCanUserViewGradebook).toHaveBeenCalled();
|
||||
expect(selectors.root.useShowBulkManagement).toHaveBeenCalled();
|
||||
expect(actions.app.useSetView).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
describe('output', () => {
|
||||
test('redux fields', () => {
|
||||
out = useGradebookHeaderData();
|
||||
expect(out.areGradesFrozen).toEqual(areGradesFrozen);
|
||||
expect(out.canUserViewGradebook).toEqual(canUserViewGradebook);
|
||||
expect(out.courseId).toEqual(courseId);
|
||||
expect(out.showBulkManagement).toEqual(showBulkManagement);
|
||||
});
|
||||
describe('handleToggleViewClick', () => {
|
||||
it('calls setView with bulkManagemnetHistory message if grades view is active', () => {
|
||||
selectors.app.useActiveView.mockReturnValueOnce(views.grades);
|
||||
out = useGradebookHeaderData();
|
||||
out.handleToggleViewClick();
|
||||
expect(setView).toHaveBeenCalledWith(views.bulkManagementHistory);
|
||||
});
|
||||
it('calls setView with grades view if grades view is not active', () => {
|
||||
out = useGradebookHeaderData();
|
||||
out.handleToggleViewClick();
|
||||
expect(setView).toHaveBeenCalledWith(views.grades);
|
||||
});
|
||||
});
|
||||
describe('toggleViewMessage', () => {
|
||||
it('returns toActivityLog message if grades view is active', () => {
|
||||
selectors.app.useActiveView.mockReturnValueOnce(views.grades);
|
||||
out = useGradebookHeaderData();
|
||||
expect(out.toggleViewMessage).toEqual(messages.toActivityLog);
|
||||
});
|
||||
it('returns toGradesView message if grades view is not active', () => {
|
||||
out = useGradebookHeaderData();
|
||||
expect(out.toggleViewMessage).toEqual(messages.toGradesView);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,106 +1,50 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Button } from '@edx/paragon';
|
||||
|
||||
import { views } from 'data/constants/app';
|
||||
import actions from 'data/actions';
|
||||
import selectors from 'data/selectors';
|
||||
|
||||
import { instructorDashboardUrl } from 'data/services/lms/urls';
|
||||
import useGradebookHeaderData from './hooks';
|
||||
import messages from './messages';
|
||||
|
||||
export class GradebookHeader extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.handleToggleViewClick = this.handleToggleViewClick.bind(this);
|
||||
}
|
||||
|
||||
handleToggleViewClick() {
|
||||
const newView = this.props.activeView === views.grades ? views.bulkManagementHistory : views.grades;
|
||||
this.props.setView(newView);
|
||||
}
|
||||
|
||||
get toggleViewMessage() {
|
||||
return this.props.activeView === views.grades
|
||||
? messages.toActivityLog
|
||||
: messages.toGradesView;
|
||||
}
|
||||
|
||||
lmsInstructorDashboardUrl = courseId => (
|
||||
`${getConfig().LMS_BASE_URL}/courses/${courseId}/instructor`
|
||||
);
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="gradebook-header">
|
||||
<a
|
||||
href={this.lmsInstructorDashboardUrl(this.props.courseId)}
|
||||
className="mb-3"
|
||||
>
|
||||
<span aria-hidden="true">{'<< '}</span>
|
||||
<FormattedMessage {...messages.backToDashboard} />
|
||||
</a>
|
||||
<h1>
|
||||
<FormattedMessage {...messages.gradebook} />
|
||||
</h1>
|
||||
<div className="subtitle-row d-flex justify-content-between align-items-center">
|
||||
<h2>{this.props.courseId}</h2>
|
||||
{ this.props.showBulkManagement && (
|
||||
<Button
|
||||
variant="tertiary"
|
||||
onClick={this.handleToggleViewClick}
|
||||
>
|
||||
<FormattedMessage {...this.toggleViewMessage} />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
{this.props.areGradesFrozen
|
||||
&& (
|
||||
<div className="alert alert-warning" role="alert">
|
||||
<FormattedMessage {...messages.frozenWarning} />
|
||||
</div>
|
||||
)}
|
||||
{(this.props.canUserViewGradebook === false) && (
|
||||
<div className="alert alert-warning" role="alert">
|
||||
<FormattedMessage {...messages.unauthorizedWarning} />
|
||||
</div>
|
||||
export const GradebookHeader = () => {
|
||||
const { formatMessage } = useIntl();
|
||||
const {
|
||||
areGradesFrozen,
|
||||
canUserViewGradebook,
|
||||
courseId,
|
||||
handleToggleViewClick,
|
||||
showBulkManagement,
|
||||
toggleViewMessage,
|
||||
} = useGradebookHeaderData();
|
||||
const dashboardUrl = instructorDashboardUrl();
|
||||
return (
|
||||
<div className="gradebook-header">
|
||||
<a href={dashboardUrl} className="mb-3">
|
||||
<span aria-hidden="true">{'<< '}</span>
|
||||
{formatMessage(messages.backToDashboard)}
|
||||
</a>
|
||||
<h1>{formatMessage(messages.gradebook)}</h1>
|
||||
<div className="subtitle-row d-flex justify-content-between align-items-center">
|
||||
<h2>{courseId}</h2>
|
||||
{showBulkManagement && (
|
||||
<Button variant="tertiary" onClick={handleToggleViewClick}>
|
||||
{formatMessage(toggleViewMessage)}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
GradebookHeader.defaultProps = {
|
||||
// redux
|
||||
courseId: '',
|
||||
areGradesFrozen: false,
|
||||
canUserViewGradebook: false,
|
||||
showBulkManagement: false,
|
||||
{areGradesFrozen && (
|
||||
<div className="alert alert-warning" role="alert">
|
||||
{formatMessage(messages.frozenWarning)}
|
||||
</div>
|
||||
)}
|
||||
{(canUserViewGradebook === false) && (
|
||||
<div className="alert alert-warning" role="alert">
|
||||
{formatMessage(messages.unauthorizedWarning)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
GradebookHeader.propTypes = {
|
||||
// redux
|
||||
activeView: PropTypes.string.isRequired,
|
||||
courseId: PropTypes.string,
|
||||
areGradesFrozen: PropTypes.bool,
|
||||
canUserViewGradebook: PropTypes.bool,
|
||||
setView: PropTypes.func.isRequired,
|
||||
showBulkManagement: PropTypes.bool,
|
||||
};
|
||||
|
||||
export const mapStateToProps = (state) => ({
|
||||
activeView: selectors.app.activeView(state),
|
||||
courseId: selectors.app.courseId(state),
|
||||
areGradesFrozen: selectors.assignmentTypes.areGradesFrozen(state),
|
||||
canUserViewGradebook: selectors.roles.canUserViewGradebook(state),
|
||||
showBulkManagement: selectors.root.showBulkManagement(state),
|
||||
});
|
||||
|
||||
export const mapDispatchToProps = {
|
||||
setView: actions.app.setView,
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(GradebookHeader);
|
||||
export default GradebookHeader;
|
||||
|
||||
77
src/components/GradebookHeader/index.test.jsx
Normal file
77
src/components/GradebookHeader/index.test.jsx
Normal file
@@ -0,0 +1,77 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Button } from '@edx/paragon';
|
||||
|
||||
import { formatMessage } from 'testUtils';
|
||||
import { instructorDashboardUrl } from 'data/services/lms/urls';
|
||||
|
||||
import useGradebookHeaderData from './hooks';
|
||||
import GradebookHeader from '.';
|
||||
|
||||
jest.mock('./hooks', () => ({ __esModule: true, default: jest.fn() }));
|
||||
jest.mock('data/services/lms/urls', () => ({
|
||||
instructorDashboardUrl: jest.fn(),
|
||||
}));
|
||||
|
||||
instructorDashboardUrl.mockReturnValue('test-dashboard-url');
|
||||
|
||||
const hookProps = {
|
||||
areGradesFrozen: false,
|
||||
canUserViewGradebook: true,
|
||||
courseId: 'test-course-id',
|
||||
handleToggleViewClick: jest.fn().mockName('hooks.handleToggleViewClick'),
|
||||
showBulkManagement: false,
|
||||
toggleViewMessage: { defaultMessage: 'toggle-view-message' },
|
||||
};
|
||||
useGradebookHeaderData.mockReturnValue(hookProps);
|
||||
|
||||
let el;
|
||||
describe('GradebookHeader component', () => {
|
||||
beforeAll(() => {
|
||||
el = shallow(<GradebookHeader />);
|
||||
});
|
||||
describe('behavior', () => {
|
||||
it('initializes hooks', () => {
|
||||
expect(useGradebookHeaderData).toHaveBeenCalledWith();
|
||||
expect(useIntl).toHaveBeenCalledWith();
|
||||
});
|
||||
});
|
||||
describe('render', () => {
|
||||
describe('default view', () => {
|
||||
test('shapshot', () => {
|
||||
expect(el).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
describe('show bulk management', () => {
|
||||
beforeEach(() => {
|
||||
useGradebookHeaderData.mockReturnValueOnce({ ...hookProps, showBulkManagement: true });
|
||||
el = shallow(<GradebookHeader />);
|
||||
});
|
||||
test('snapshot: show toggle view message button with handleToggleViewClick method', () => {
|
||||
expect(el).toMatchSnapshot();
|
||||
const { onClick, children } = el.find(Button).props();
|
||||
expect(onClick).toEqual(hookProps.handleToggleViewClick);
|
||||
expect(children).toEqual(formatMessage(hookProps.toggleViewMessage));
|
||||
});
|
||||
});
|
||||
describe('frozen grades', () => {
|
||||
beforeEach(() => {
|
||||
useGradebookHeaderData.mockReturnValueOnce({ ...hookProps, areGradesFrozen: true });
|
||||
el = shallow(<GradebookHeader />);
|
||||
});
|
||||
test('snapshot: show frozen warning', () => {
|
||||
expect(el).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
describe('user cannot view gradebook', () => {
|
||||
beforeEach(() => {
|
||||
useGradebookHeaderData.mockReturnValueOnce({ ...hookProps, canUserViewGradebook: false });
|
||||
el = shallow(<GradebookHeader />);
|
||||
});
|
||||
test('snapshot: show unauthorized warning', () => {
|
||||
expect(el).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,152 +0,0 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { Button } from '@edx/paragon';
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import actions from 'data/actions';
|
||||
import selectors from 'data/selectors';
|
||||
import { views } from 'data/constants/app';
|
||||
import messages from './messages';
|
||||
import { GradebookHeader, mapDispatchToProps, mapStateToProps } from '.';
|
||||
|
||||
jest.mock('@edx/paragon', () => ({
|
||||
Button: () => 'Button',
|
||||
}));
|
||||
jest.mock('@edx/frontend-platform/i18n', () => ({
|
||||
defineMessages: m => m,
|
||||
FormattedMessage: () => 'FormattedMessage',
|
||||
}));
|
||||
jest.mock('data/actions', () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
app: { setView: jest.fn() },
|
||||
},
|
||||
}));
|
||||
jest.mock('data/selectors', () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
app: {
|
||||
activeView: jest.fn(state => ({ aciveView: state })),
|
||||
courseId: jest.fn(state => ({ courseId: state })),
|
||||
},
|
||||
assignmentTypes: { areGradesFrozen: jest.fn(state => ({ areGradesFrozen: state })) },
|
||||
roles: { canUserViewGradebook: jest.fn(state => ({ canUserViewGradebook: state })) },
|
||||
root: { showBulkManagement: jest.fn(state => ({ showBulkManagement: state })) },
|
||||
},
|
||||
}));
|
||||
|
||||
const courseId = 'fakeID';
|
||||
describe('GradebookHeader component', () => {
|
||||
const props = {
|
||||
activeView: views.grades,
|
||||
areGradesFrozen: false,
|
||||
canUserViewGradebook: false,
|
||||
courseId,
|
||||
showBulkManagement: false,
|
||||
};
|
||||
beforeEach(() => {
|
||||
props.setView = jest.fn();
|
||||
});
|
||||
describe('snapshots', () => {
|
||||
let el;
|
||||
beforeEach(() => {
|
||||
el = shallow(<GradebookHeader {...props} />);
|
||||
el.instance().handleToggleViewClick = jest.fn().mockName('this.handleToggleViewClick');
|
||||
});
|
||||
describe('default values (grades frozen, cannot view).', () => {
|
||||
test('unauthorized warning, but no grades frozen warning', () => {
|
||||
expect(el.instance().render()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
describe('grades frozen, cannot view', () => {
|
||||
test('unauthorized warning, and grades frozen warning.', () => {
|
||||
el.setProps({ areGradesFrozen: true });
|
||||
expect(el.instance().render()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
describe('grades frozen, can view.', () => {
|
||||
test('grades frozen warning but no unauthorized warning', () => {
|
||||
el.setProps({ areGradesFrozen: true, canUserViewGradebook: true });
|
||||
expect(el.instance().render()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
describe('show bulk management, active view is grades view', () => {
|
||||
test('toggle view button to activity log', () => {
|
||||
el.setProps({ showBulkManagement: true });
|
||||
expect(el.find(Button).getElement()).toEqual((
|
||||
<Button
|
||||
variant="tertiary"
|
||||
onClick={el.instance().handleToggleViewClick}
|
||||
>
|
||||
<FormattedMessage {...messages.toActivityLog} />
|
||||
</Button>
|
||||
));
|
||||
expect(el.instance().render()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
describe('show bulk management, active view is bulkManagementHistory view', () => {
|
||||
test('toggle view button to grades', () => {
|
||||
el.setProps({ showBulkManagement: true, activeView: views.bulkManagementHistory });
|
||||
expect(el.find(Button).getElement()).toEqual((
|
||||
<Button
|
||||
variant="tertiary"
|
||||
onClick={el.instance().handleToggleViewClick}
|
||||
>
|
||||
<FormattedMessage {...messages.toGradesView} />
|
||||
</Button>
|
||||
));
|
||||
expect(el.instance().render()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('behavior', () => {
|
||||
let el;
|
||||
beforeEach(() => {
|
||||
el = shallow(<GradebookHeader {...props} />);
|
||||
});
|
||||
describe('handleToggleViewClick', () => {
|
||||
test('calls setView with activity view if activeView is grades', () => {
|
||||
el.instance().handleToggleViewClick();
|
||||
expect(props.setView).toHaveBeenCalledWith(views.bulkManagementHistory);
|
||||
});
|
||||
test('calls setView with grades view if activeView is bulkManagementHistory', () => {
|
||||
el.setProps({ activeView: views.bulkManagementHistory });
|
||||
el.instance().handleToggleViewClick();
|
||||
expect(props.setView).toHaveBeenCalledWith(views.grades);
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('mapStateToProps', () => {
|
||||
let mapped;
|
||||
const testState = { a: 'test', example: 'state' };
|
||||
beforeEach(() => {
|
||||
mapped = mapStateToProps(testState);
|
||||
});
|
||||
test('activeView from app.activeView', () => {
|
||||
expect(mapped.activeView).toEqual(selectors.app.activeView(testState));
|
||||
});
|
||||
test('courseId from app.courseId', () => {
|
||||
expect(mapped.courseId).toEqual(selectors.app.courseId(testState));
|
||||
});
|
||||
test('areGradesFrozen from assignmentTypes selector', () => {
|
||||
expect(
|
||||
mapped.areGradesFrozen,
|
||||
).toEqual(selectors.assignmentTypes.areGradesFrozen(testState));
|
||||
});
|
||||
test('canUserViewGradebook from roles selector', () => {
|
||||
expect(
|
||||
mapped.canUserViewGradebook,
|
||||
).toEqual(selectors.roles.canUserViewGradebook(testState));
|
||||
});
|
||||
test('showBulkManagement from root showBulkManagement selector', () => {
|
||||
expect(mapped.showBulkManagement).toEqual(selectors.root.showBulkManagement(testState));
|
||||
});
|
||||
});
|
||||
|
||||
describe('mapDispatchToProps', () => {
|
||||
test('setView from actions.app.setView', () => {
|
||||
expect(mapDispatchToProps.setView).toEqual(actions.app.setView);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,71 +0,0 @@
|
||||
/* eslint-disable react/sort-comp, react/button-has-type */
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { views } from 'data/constants/app';
|
||||
import actions from 'data/actions';
|
||||
import selectors from 'data/selectors';
|
||||
|
||||
import NetworkButton from 'components/NetworkButton';
|
||||
import ImportGradesButton from './ImportGradesButton';
|
||||
|
||||
import messages from './BulkManagementControls.messages';
|
||||
|
||||
/**
|
||||
* <BulkManagementControls />
|
||||
* Provides download buttons for Bulk Management and Intervention reports, only if
|
||||
* showBulkManagement is set in redus.
|
||||
*/
|
||||
export class BulkManagementControls extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.handleClickExportGrades = this.handleClickExportGrades.bind(this);
|
||||
this.handleViewActivityLog = this.handleViewActivityLog.bind(this);
|
||||
}
|
||||
|
||||
handleClickExportGrades() {
|
||||
this.props.downloadBulkGradesReport();
|
||||
window.location.assign(this.props.gradeExportUrl);
|
||||
}
|
||||
|
||||
handleViewActivityLog() {
|
||||
this.props.setView(views.bulkManagementHistory);
|
||||
}
|
||||
|
||||
render() {
|
||||
return this.props.showBulkManagement && (
|
||||
<div className="d-flex">
|
||||
<NetworkButton
|
||||
label={messages.downloadGradesBtn}
|
||||
onClick={this.handleClickExportGrades}
|
||||
/>
|
||||
<ImportGradesButton />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
BulkManagementControls.defaultProps = {
|
||||
showBulkManagement: false,
|
||||
};
|
||||
|
||||
BulkManagementControls.propTypes = {
|
||||
// redux
|
||||
downloadBulkGradesReport: PropTypes.func.isRequired,
|
||||
gradeExportUrl: PropTypes.string.isRequired,
|
||||
showBulkManagement: PropTypes.bool,
|
||||
setView: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export const mapStateToProps = (state) => ({
|
||||
gradeExportUrl: selectors.root.gradeExportUrl(state),
|
||||
showBulkManagement: selectors.root.showBulkManagement(state),
|
||||
});
|
||||
|
||||
export const mapDispatchToProps = {
|
||||
downloadBulkGradesReport: actions.grades.downloadReport.bulkGrades,
|
||||
setView: actions.app.setView,
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(BulkManagementControls);
|
||||
@@ -1,124 +0,0 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import actions from 'data/actions';
|
||||
import selectors from 'data/selectors';
|
||||
import { views } from 'data/constants/app';
|
||||
|
||||
import {
|
||||
BulkManagementControls,
|
||||
mapStateToProps,
|
||||
mapDispatchToProps,
|
||||
} from './BulkManagementControls';
|
||||
|
||||
jest.mock('./ImportGradesButton', () => 'ImportGradesButton');
|
||||
jest.mock('components/NetworkButton', () => 'NetworkButton');
|
||||
jest.mock('data/selectors', () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
root: {
|
||||
gradeExportUrl: (state) => ({ gradeExportUrl: state }),
|
||||
interventionExportUrl: (state) => ({ interventionExportUrl: state }),
|
||||
showBulkManagement: (state) => ({ showBulkManagement: state }),
|
||||
},
|
||||
},
|
||||
}));
|
||||
jest.mock('data/actions', () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
app: { setView: jest.fn() },
|
||||
grades: {
|
||||
downloadReport: {
|
||||
bulkGrades: jest.fn(),
|
||||
intervention: jest.fn(),
|
||||
},
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
describe('BulkManagementControls', () => {
|
||||
describe('component', () => {
|
||||
let el;
|
||||
let props = {
|
||||
gradeExportUrl: 'gradesGoHere',
|
||||
interventionExportUrl: 'interventionsGoHere',
|
||||
};
|
||||
beforeEach(() => {
|
||||
props = {
|
||||
...props,
|
||||
downloadBulkGradesReport: jest.fn(),
|
||||
downloadInterventionReport: jest.fn(),
|
||||
setView: jest.fn(),
|
||||
};
|
||||
});
|
||||
test('snapshot - empty if showBulkManagement is not truthy', () => {
|
||||
expect(shallow(<BulkManagementControls {...props} />)).toEqual({});
|
||||
});
|
||||
describe('behavior', () => {
|
||||
const oldWindowLocation = window.location;
|
||||
|
||||
beforeAll(() => {
|
||||
delete window.location;
|
||||
window.location = Object.defineProperties(
|
||||
{},
|
||||
{
|
||||
...Object.getOwnPropertyDescriptors(oldWindowLocation),
|
||||
assign: {
|
||||
configurable: true,
|
||||
value: jest.fn(),
|
||||
},
|
||||
},
|
||||
);
|
||||
});
|
||||
beforeEach(() => {
|
||||
window.location.assign.mockReset();
|
||||
el = shallow(<BulkManagementControls {...props} showBulkManagement />);
|
||||
});
|
||||
afterAll(() => {
|
||||
// restore `window.location` to the `jsdom` `Location` object
|
||||
window.location = oldWindowLocation;
|
||||
});
|
||||
describe('handleViewActivityLog', () => {
|
||||
it('calls props.setView(views.bulkManagementHistory)', () => {
|
||||
el.instance().handleViewActivityLog();
|
||||
expect(props.setView).toHaveBeenCalledWith(views.bulkManagementHistory);
|
||||
});
|
||||
});
|
||||
describe('handleClickExportGrades', () => {
|
||||
const assertions = [
|
||||
'calls props.downloadBulkGradesReport',
|
||||
'sets location to props.gradeExportUrl',
|
||||
];
|
||||
it(assertions.join(' and '), () => {
|
||||
el.instance().handleClickExportGrades();
|
||||
expect(props.downloadBulkGradesReport).toHaveBeenCalled();
|
||||
expect(window.location.assign).toHaveBeenCalledWith(props.gradeExportUrl);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('mapStateToProps', () => {
|
||||
let mapped;
|
||||
const testState = { do: 'not', test: 'me' };
|
||||
beforeEach(() => {
|
||||
mapped = mapStateToProps(testState);
|
||||
});
|
||||
test('gradeExportUrl from root.gradeExportUrl', () => {
|
||||
expect(mapped.gradeExportUrl).toEqual(selectors.root.gradeExportUrl(testState));
|
||||
});
|
||||
test('showBulkManagement from root.showBulkManagement', () => {
|
||||
expect(mapped.showBulkManagement).toEqual(selectors.root.showBulkManagement(testState));
|
||||
});
|
||||
});
|
||||
describe('mapDispatchToProps', () => {
|
||||
test('downloadBulkGradesReport from actions.grades.downloadReport.bulkGrades', () => {
|
||||
expect(
|
||||
mapDispatchToProps.downloadBulkGradesReport,
|
||||
).toEqual(actions.grades.downloadReport.bulkGrades);
|
||||
});
|
||||
test('setView from actions.app.setView', () => {
|
||||
expect(mapDispatchToProps.setView).toEqual(actions.app.setView);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,19 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`BulkManagementControls render snapshot - show - network and import buttons 1`] = `
|
||||
<div
|
||||
className="d-flex"
|
||||
>
|
||||
<NetworkButton
|
||||
label={
|
||||
Object {
|
||||
"defaultMessage": "Download Grades",
|
||||
"description": "A labeled button that allows an admin user to download course grades all at once (in bulk).",
|
||||
"id": "gradebook.GradesView.BulkManagementControls.bulkManagementLabel",
|
||||
}
|
||||
}
|
||||
onClick={[MockFunction]}
|
||||
/>
|
||||
<ImportGradesButton />
|
||||
</div>
|
||||
`;
|
||||
18
src/components/GradesView/BulkManagementControls/hooks.js
Normal file
18
src/components/GradesView/BulkManagementControls/hooks.js
Normal file
@@ -0,0 +1,18 @@
|
||||
import { actions, selectors } from 'data/redux/hooks';
|
||||
|
||||
export const useBulkManagementControlsData = () => {
|
||||
const gradeExportUrl = selectors.root.useGradeExportUrl();
|
||||
const showBulkManagement = selectors.root.useShowBulkManagement();
|
||||
const downloadBulkGradesReport = actions.grades.useDownloadBulkGradesReport();
|
||||
|
||||
const handleClickExportGrades = () => {
|
||||
downloadBulkGradesReport();
|
||||
window.location.assign(gradeExportUrl);
|
||||
};
|
||||
|
||||
return {
|
||||
show: showBulkManagement,
|
||||
handleClickExportGrades,
|
||||
};
|
||||
};
|
||||
export default useBulkManagementControlsData;
|
||||
@@ -0,0 +1,72 @@
|
||||
import { actions, selectors } from 'data/redux/hooks';
|
||||
|
||||
import useBulkManagementControlsData from './hooks';
|
||||
|
||||
jest.mock('data/redux/hooks', () => ({
|
||||
actions: {
|
||||
grades: {
|
||||
useDownloadBulkGradesReport: jest.fn(),
|
||||
},
|
||||
},
|
||||
selectors: {
|
||||
root: {
|
||||
useGradeExportUrl: jest.fn(),
|
||||
useShowBulkManagement: jest.fn(),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
const downloadBulkGrades = jest.fn();
|
||||
actions.grades.useDownloadBulkGradesReport.mockReturnValue(downloadBulkGrades);
|
||||
const gradeExportUrl = 'test-grade-export-url';
|
||||
selectors.root.useGradeExportUrl.mockReturnValue(gradeExportUrl);
|
||||
selectors.root.useShowBulkManagement.mockReturnValue(true);
|
||||
|
||||
let hook;
|
||||
describe('useBulkManagementControlsData', () => {
|
||||
const oldWindowLocation = window.location;
|
||||
beforeAll(() => {
|
||||
delete window.location;
|
||||
window.location = Object.defineProperties(
|
||||
{},
|
||||
{
|
||||
...Object.getOwnPropertyDescriptors(oldWindowLocation),
|
||||
assign: { configurable: true, value: jest.fn() },
|
||||
},
|
||||
);
|
||||
});
|
||||
beforeEach(() => {
|
||||
window.location.assign.mockReset();
|
||||
hook = useBulkManagementControlsData();
|
||||
});
|
||||
afterAll(() => {
|
||||
// restore `window.location` to the `jsdom` `Location` object
|
||||
window.location = oldWindowLocation;
|
||||
});
|
||||
describe('initialization', () => {
|
||||
it('initializes redux hooks', () => {
|
||||
expect(selectors.root.useGradeExportUrl).toHaveBeenCalledWith();
|
||||
expect(selectors.root.useShowBulkManagement).toHaveBeenCalledWith();
|
||||
expect(actions.grades.useDownloadBulkGradesReport).toHaveBeenCalledWith();
|
||||
});
|
||||
});
|
||||
describe('output', () => {
|
||||
it('forwards show from showBulkManagement', () => {
|
||||
expect(hook.show).toEqual(true);
|
||||
selectors.root.useShowBulkManagement.mockReturnValue(false);
|
||||
hook = useBulkManagementControlsData();
|
||||
expect(hook.show).toEqual(false);
|
||||
});
|
||||
describe('handleClickExportGrades', () => {
|
||||
beforeEach(() => {
|
||||
hook.handleClickExportGrades();
|
||||
});
|
||||
it('downloads bulk grades report', () => {
|
||||
expect(downloadBulkGrades).toHaveBeenCalledWith();
|
||||
});
|
||||
it('sets window location to grade export url', () => {
|
||||
expect(window.location.assign).toHaveBeenCalledWith(gradeExportUrl);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
33
src/components/GradesView/BulkManagementControls/index.jsx
Normal file
33
src/components/GradesView/BulkManagementControls/index.jsx
Normal file
@@ -0,0 +1,33 @@
|
||||
/* eslint-disable react/sort-comp, react/button-has-type */
|
||||
import React from 'react';
|
||||
|
||||
import NetworkButton from 'components/NetworkButton';
|
||||
import ImportGradesButton from '../ImportGradesButton';
|
||||
|
||||
import useBulkManagementControlsData from './hooks';
|
||||
import messages from './messages';
|
||||
|
||||
/**
|
||||
* <BulkManagementControls />
|
||||
* Provides download buttons for Bulk Management and Intervention reports, only if
|
||||
* showBulkManagement is set in redus.
|
||||
*/
|
||||
export const BulkManagementControls = () => {
|
||||
const {
|
||||
show,
|
||||
handleClickExportGrades,
|
||||
} = useBulkManagementControlsData();
|
||||
|
||||
if (!show) { return null; }
|
||||
return (
|
||||
<div className="d-flex">
|
||||
<NetworkButton
|
||||
label={messages.downloadGradesBtn}
|
||||
onClick={handleClickExportGrades}
|
||||
/>
|
||||
<ImportGradesButton />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default BulkManagementControls;
|
||||
@@ -0,0 +1,32 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import useBulkManagementControlsData from './hooks';
|
||||
import BulkManagementControls from '.';
|
||||
|
||||
jest.mock('../ImportGradesButton', () => 'ImportGradesButton');
|
||||
jest.mock('components/NetworkButton', () => 'NetworkButton');
|
||||
|
||||
jest.mock('./hooks', () => jest.fn());
|
||||
|
||||
const hookProps = {
|
||||
show: true,
|
||||
handleClickExportGrades: jest.fn(),
|
||||
};
|
||||
useBulkManagementControlsData.mockReturnValue(hookProps);
|
||||
|
||||
describe('BulkManagementControls', () => {
|
||||
describe('behavior', () => {
|
||||
shallow(<BulkManagementControls />);
|
||||
expect(useBulkManagementControlsData).toHaveBeenCalledWith();
|
||||
});
|
||||
describe('render', () => {
|
||||
test('snapshot - show - network and import buttons', () => {
|
||||
expect(shallow(<BulkManagementControls />)).toMatchSnapshot();
|
||||
});
|
||||
test('snapshot - empty if show is not truthy', () => {
|
||||
useBulkManagementControlsData.mockReturnValueOnce({ ...hookProps, show: false });
|
||||
expect(shallow(<BulkManagementControls />).isEmptyRender()).toEqual(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,68 +1,53 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import selectors from 'data/selectors';
|
||||
import { StrictDict } from 'utils';
|
||||
import { selectors } from 'data/redux/hooks';
|
||||
|
||||
import messages from './messages';
|
||||
import HistoryHeader from './HistoryHeader';
|
||||
|
||||
export const HistoryKeys = StrictDict({
|
||||
assignment: 'assignment',
|
||||
student: 'student',
|
||||
originalGrade: 'original-grade',
|
||||
currentGrade: 'current-grade',
|
||||
});
|
||||
|
||||
/**
|
||||
* <ModalHeaders />
|
||||
* Provides a list of HistoryHeaders for the student name, assignment,
|
||||
* original grade, and current override grade.
|
||||
*/
|
||||
export const ModalHeaders = ({
|
||||
modalState,
|
||||
originalGrade,
|
||||
currentGrade,
|
||||
}) => (
|
||||
<div>
|
||||
<HistoryHeader
|
||||
id="assignment"
|
||||
label={<FormattedMessage {...messages.assignmentHeader} />}
|
||||
value={modalState.assignmentName}
|
||||
/>
|
||||
<HistoryHeader
|
||||
id="student"
|
||||
label={<FormattedMessage {...messages.studentHeader} />}
|
||||
value={modalState.updateUserName}
|
||||
/>
|
||||
<HistoryHeader
|
||||
id="original-grade"
|
||||
label={<FormattedMessage {...messages.originalGradeHeader} />}
|
||||
value={originalGrade}
|
||||
/>
|
||||
<HistoryHeader
|
||||
id="current-grade"
|
||||
label={<FormattedMessage {...messages.currentGradeHeader} />}
|
||||
value={currentGrade}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
ModalHeaders.defaultProps = {
|
||||
currentGrade: null,
|
||||
originalGrade: null,
|
||||
};
|
||||
ModalHeaders.propTypes = {
|
||||
// redux
|
||||
currentGrade: PropTypes.number,
|
||||
originalGrade: PropTypes.number,
|
||||
modalState: PropTypes.shape({
|
||||
assignmentName: PropTypes.string.isRequired,
|
||||
updateUserName: PropTypes.string,
|
||||
}).isRequired,
|
||||
export const ModalHeaders = () => {
|
||||
const { assignmentName, updateUserName } = selectors.app.useModalData();
|
||||
const { currentGrade, originalGrade } = selectors.grades.useGradeData();
|
||||
const { formatMessage } = useIntl();
|
||||
return (
|
||||
<div>
|
||||
<HistoryHeader
|
||||
id={HistoryKeys.assignment}
|
||||
label={formatMessage(messages.assignmentHeader)}
|
||||
value={assignmentName}
|
||||
/>
|
||||
<HistoryHeader
|
||||
id={HistoryKeys.student}
|
||||
label={formatMessage(messages.studentHeader)}
|
||||
value={updateUserName}
|
||||
/>
|
||||
<HistoryHeader
|
||||
id={HistoryKeys.originalGrade}
|
||||
label={formatMessage(messages.originalGradeHeader)}
|
||||
value={originalGrade}
|
||||
/>
|
||||
<HistoryHeader
|
||||
id={HistoryKeys.currentGrade}
|
||||
label={formatMessage(messages.currentGradeHeader)}
|
||||
value={currentGrade}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const mapStateToProps = (state) => ({
|
||||
modalState: {
|
||||
assignmentName: selectors.app.modalState.assignmentName(state),
|
||||
updateUserName: selectors.app.modalState.updateUserName(state),
|
||||
},
|
||||
currentGrade: selectors.grades.gradeOverrideCurrentEarnedGradedOverride(state),
|
||||
originalGrade: selectors.grades.gradeOriginalEarnedGraded(state),
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps)(ModalHeaders);
|
||||
export default ModalHeaders;
|
||||
|
||||
@@ -1,93 +1,84 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import selectors from 'data/selectors';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { selectors } from 'data/redux/hooks';
|
||||
|
||||
import {
|
||||
ModalHeaders,
|
||||
mapStateToProps,
|
||||
} from './ModalHeaders';
|
||||
import { formatMessage } from 'testUtils';
|
||||
|
||||
import HistoryHeader from './HistoryHeader';
|
||||
import ModalHeaders, { HistoryKeys } from './ModalHeaders';
|
||||
import messages from './messages';
|
||||
|
||||
jest.mock('./HistoryHeader', () => 'HistoryHeader');
|
||||
|
||||
jest.mock('data/selectors', () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
app: {
|
||||
editUpdateData: jest.fn(state => ({ editUpdateData: state })),
|
||||
modalState: {
|
||||
assignmentName: jest.fn(state => ({ assignmentName: state })),
|
||||
updateUserName: jest.fn(state => ({ updateUserName: state })),
|
||||
},
|
||||
},
|
||||
grades: {
|
||||
gradeOverrideCurrentEarnedGradedOverride: jest.fn(state => ({ currentGrade: state })),
|
||||
gradeOriginalEarnedGraded: jest.fn(state => ({ originalGrade: state })),
|
||||
},
|
||||
jest.mock('data/redux/hooks', () => ({
|
||||
selectors: {
|
||||
app: { useModalData: jest.fn() },
|
||||
grades: { useGradeData: jest.fn() },
|
||||
},
|
||||
}));
|
||||
describe('ModalHeaders', () => {
|
||||
let el;
|
||||
const props = {
|
||||
currentGrade: 2,
|
||||
originalGrade: 20,
|
||||
modalState: {
|
||||
assignmentName: 'Qwerty',
|
||||
updateUserName: 'Uiop',
|
||||
},
|
||||
};
|
||||
|
||||
describe('Component', () => {
|
||||
describe('snapshots', () => {
|
||||
beforeEach(() => {
|
||||
});
|
||||
describe('gradeOverrideHistoryError is and empty and open is true', () => {
|
||||
test('modal open and StatusAlert showing', () => {
|
||||
el = shallow(<ModalHeaders {...props} />);
|
||||
expect(el).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
describe('gradeOverrideHistoryError is empty and open is false', () => {
|
||||
test('modal closed and StatusAlert closed', () => {
|
||||
el = shallow(
|
||||
<ModalHeaders {...props} open={false} gradeOverrideHistoryError="" />,
|
||||
);
|
||||
expect(el).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
const modalData = {
|
||||
assignmentName: 'test-assignment-name',
|
||||
updateUserName: 'test-user-name',
|
||||
};
|
||||
selectors.app.useModalData.mockReturnValue(modalData);
|
||||
const gradeData = {
|
||||
currentGrade: 'test-current-grade',
|
||||
originalGrade: 'test-original-grade',
|
||||
};
|
||||
selectors.grades.useGradeData.mockReturnValue(gradeData);
|
||||
|
||||
let el;
|
||||
describe('ModalHeaders', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
el = shallow(<ModalHeaders />);
|
||||
});
|
||||
describe('behavior', () => {
|
||||
it('initializes intl', () => {
|
||||
expect(useIntl).toHaveBeenCalled();
|
||||
});
|
||||
it('initializes redux hooks', () => {
|
||||
expect(selectors.app.useModalData).toHaveBeenCalled();
|
||||
expect(selectors.grades.useGradeData).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('mapStateToProps', () => {
|
||||
const testState = { he: 'lives in a', pineapple: 'under the sea' };
|
||||
let mapped;
|
||||
beforeEach(() => {
|
||||
mapped = mapStateToProps(testState);
|
||||
describe('render', () => {
|
||||
test('snapshot', () => {
|
||||
expect(el).toMatchSnapshot();
|
||||
});
|
||||
describe('modalState', () => {
|
||||
test('assignmentName from app.modalState.assignmentName', () => {
|
||||
expect(
|
||||
mapped.modalState.assignmentName,
|
||||
).toEqual(selectors.app.modalState.assignmentName(testState));
|
||||
});
|
||||
test('updateUserName from app.modalState.updateUserName', () => {
|
||||
expect(
|
||||
mapped.modalState.updateUserName,
|
||||
).toEqual(selectors.app.modalState.updateUserName(testState));
|
||||
test('assignment header', () => {
|
||||
const headerProps = el.find(HistoryHeader).at(0).props();
|
||||
expect(headerProps).toMatchObject({
|
||||
id: HistoryKeys.assignment,
|
||||
label: formatMessage(messages.assignmentHeader),
|
||||
value: modalData.assignmentName,
|
||||
});
|
||||
});
|
||||
describe('originalGrade', () => {
|
||||
test('from grades.gradeOverrideCurrentEarnedGradedOverride', () => {
|
||||
expect(mapped.currentGrade).toEqual(
|
||||
selectors.grades.gradeOverrideCurrentEarnedGradedOverride(testState),
|
||||
);
|
||||
test('student header', () => {
|
||||
const headerProps = el.find(HistoryHeader).at(1).props();
|
||||
expect(headerProps).toMatchObject({
|
||||
id: HistoryKeys.student,
|
||||
label: formatMessage(messages.studentHeader),
|
||||
value: modalData.updateUserName,
|
||||
});
|
||||
});
|
||||
describe('originalGrade', () => {
|
||||
test('from grades.gradeOriginalEarnedGrades', () => {
|
||||
expect(mapped.originalGrade).toEqual(
|
||||
selectors.grades.gradeOriginalEarnedGraded(testState),
|
||||
);
|
||||
test('originalGrade header', () => {
|
||||
const headerProps = el.find(HistoryHeader).at(2).props();
|
||||
expect(headerProps).toMatchObject({
|
||||
id: HistoryKeys.originalGrade,
|
||||
label: formatMessage(messages.originalGradeHeader),
|
||||
value: gradeData.originalGrade,
|
||||
});
|
||||
});
|
||||
test('currentGrade header', () => {
|
||||
const headerProps = el.find(HistoryHeader).at(3).props();
|
||||
expect(headerProps).toMatchObject({
|
||||
id: HistoryKeys.currentGrade,
|
||||
label: formatMessage(messages.currentGradeHeader),
|
||||
value: gradeData.currentGrade,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
/* eslint-disable react/sort-comp, react/button-has-type */
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { Form } from '@edx/paragon';
|
||||
|
||||
import selectors from 'data/selectors';
|
||||
import actions from 'data/actions';
|
||||
import { getLocalizedSlash } from 'i18n';
|
||||
|
||||
/**
|
||||
* <AdjustedGradeInput />
|
||||
* Input control for adjusting the grade of a unit
|
||||
* displays an "/ ${possibleGrade} if there is one in the data model.
|
||||
*/
|
||||
export class AdjustedGradeInput extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.onChange = this.onChange.bind(this);
|
||||
}
|
||||
|
||||
onChange = ({ target }) => {
|
||||
this.props.setModalState({ adjustedGradeValue: target.value });
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<span>
|
||||
<Form.Control
|
||||
type="text"
|
||||
name="adjustedGradeValue"
|
||||
value={this.props.value}
|
||||
onChange={this.onChange}
|
||||
/>
|
||||
{this.props.possibleGrade && ` ${getLocalizedSlash()} ${this.props.possibleGrade}`}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
AdjustedGradeInput.defaultProps = {
|
||||
possibleGrade: null,
|
||||
};
|
||||
AdjustedGradeInput.propTypes = {
|
||||
value: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.number,
|
||||
]).isRequired,
|
||||
possibleGrade: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.number,
|
||||
]),
|
||||
setModalState: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export const mapStateToProps = (state) => ({
|
||||
possibleGrade: selectors.root.editModalPossibleGrade(state),
|
||||
value: selectors.app.modalState.adjustedGradeValue(state),
|
||||
});
|
||||
|
||||
export const mapDispatchToProps = {
|
||||
setModalState: actions.app.setModalState,
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(AdjustedGradeInput);
|
||||
@@ -1,92 +0,0 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import actions from 'data/actions';
|
||||
import selectors from 'data/selectors';
|
||||
|
||||
import {
|
||||
AdjustedGradeInput,
|
||||
mapStateToProps,
|
||||
mapDispatchToProps,
|
||||
} from './AdjustedGradeInput';
|
||||
|
||||
jest.mock('@edx/paragon', () => ({
|
||||
Form: { Control: () => 'Form.Control' },
|
||||
}));
|
||||
jest.mock('data/selectors', () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
root: {
|
||||
editModalPossibleGrade: jest.fn(state => ({ updateUserName: state })),
|
||||
},
|
||||
app: {
|
||||
modalState: { adjustedGradeValue: jest.fn(state => ({ adjustedGradeValue: state })) },
|
||||
},
|
||||
},
|
||||
}));
|
||||
jest.mock('data/actions', () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
app: { setModalState: jest.fn() },
|
||||
},
|
||||
}));
|
||||
describe('AdjustedGradeInput', () => {
|
||||
let el;
|
||||
let props = {
|
||||
value: 1,
|
||||
possibleGrade: 5,
|
||||
};
|
||||
beforeEach(() => {
|
||||
props = {
|
||||
...props,
|
||||
setModalState: jest.fn(),
|
||||
};
|
||||
});
|
||||
describe('Component', () => {
|
||||
beforeEach(() => {
|
||||
el = shallow(<AdjustedGradeInput {...props} />);
|
||||
});
|
||||
describe('snapshots', () => {
|
||||
test('displays input control and "out of possible grade" label', () => {
|
||||
el.instance().onChange = jest.fn().mockName('this.onChange');
|
||||
expect(el.instance().render()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
describe('behavior', () => {
|
||||
describe('onChange', () => {
|
||||
it('calls props.setModalState event target value', () => {
|
||||
const value = 42;
|
||||
el.instance().onChange({ target: { value } });
|
||||
expect(props.setModalState).toHaveBeenCalledWith({
|
||||
adjustedGradeValue: value,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('mapStateToProps', () => {
|
||||
const testState = { like: 'no one', ever: 'was' };
|
||||
let mapped;
|
||||
beforeEach(() => {
|
||||
mapped = mapStateToProps(testState);
|
||||
});
|
||||
describe('modalState', () => {
|
||||
test('possibleGrade from root.editModalPossibleGrade', () => {
|
||||
expect(
|
||||
mapped.possibleGrade,
|
||||
).toEqual(selectors.root.editModalPossibleGrade(testState));
|
||||
});
|
||||
test('updateUserName from app.modalState.updateUserName', () => {
|
||||
expect(
|
||||
mapped.value,
|
||||
).toEqual(selectors.app.modalState.adjustedGradeValue(testState));
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('mapDispatchToProps', () => {
|
||||
test('setModalState from actions.app.setModalState', () => {
|
||||
expect(mapDispatchToProps.setModalState).toEqual(actions.app.setModalState);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`AdjustedGradeInput component render snapshot 1`] = `
|
||||
<span>
|
||||
<Form.Control
|
||||
name="adjustedGradeValue"
|
||||
onChange={[MockFunction hook.onChange]}
|
||||
type="text"
|
||||
value="test-value"
|
||||
/>
|
||||
some-hint-text
|
||||
</span>
|
||||
`;
|
||||
@@ -0,0 +1,21 @@
|
||||
import { actions, selectors } from 'data/redux/hooks';
|
||||
import { getLocalizedSlash } from 'i18n/utils';
|
||||
|
||||
const useAdjustedGradeInputData = () => {
|
||||
const possibleGrade = selectors.root.useEditModalPossibleGrade();
|
||||
const value = selectors.app.useModalData().adjustedGradeValue;
|
||||
const setModalState = actions.app.useSetModalState();
|
||||
const hintText = possibleGrade && ` ${getLocalizedSlash()} ${possibleGrade}`;
|
||||
|
||||
const onChange = ({ target }) => {
|
||||
setModalState({ adjustedGradeValue: target.value });
|
||||
};
|
||||
|
||||
return {
|
||||
value,
|
||||
onChange,
|
||||
hintText,
|
||||
};
|
||||
};
|
||||
|
||||
export default useAdjustedGradeInputData;
|
||||
@@ -0,0 +1,67 @@
|
||||
import { getLocalizedSlash } from 'i18n/utils';
|
||||
import { actions, selectors } from 'data/redux/hooks';
|
||||
import useAdjustedGradeInputData from './hooks';
|
||||
|
||||
jest.mock('data/redux/hooks', () => ({
|
||||
selectors: {
|
||||
root: {
|
||||
useEditModalPossibleGrade: jest.fn(),
|
||||
},
|
||||
app: {
|
||||
useModalData: jest.fn(),
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
app: {
|
||||
useSetModalState: jest.fn(),
|
||||
},
|
||||
},
|
||||
}));
|
||||
jest.mock('i18n/utils', () => ({ getLocalizedSlash: jest.fn() }));
|
||||
|
||||
const localizedSlash = 'localized-slash';
|
||||
getLocalizedSlash.mockReturnValue(localizedSlash);
|
||||
|
||||
const possibleGrade = 105;
|
||||
selectors.root.useEditModalPossibleGrade.mockReturnValue(possibleGrade);
|
||||
const modalData = { adjustedGradeValue: 70 };
|
||||
const setModalState = jest.fn();
|
||||
selectors.app.useModalData.mockReturnValue(modalData);
|
||||
actions.app.useSetModalState.mockReturnValue(setModalState);
|
||||
|
||||
let out;
|
||||
describe('useAdjustedGradeInputData hook', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
out = useAdjustedGradeInputData();
|
||||
});
|
||||
describe('behavior', () => {
|
||||
it('initializes redux hooks', () => {
|
||||
expect(selectors.root.useEditModalPossibleGrade).toHaveBeenCalled();
|
||||
expect(selectors.app.useModalData).toHaveBeenCalled();
|
||||
expect(actions.app.useSetModalState).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
describe('output', () => {
|
||||
it('forwards adjusted grade value as value from modal data', () => {
|
||||
expect(out.value).toEqual(modalData.adjustedGradeValue);
|
||||
});
|
||||
describe('hintText', () => {
|
||||
it('passes an undefined value if possibleGrade is not available', () => {
|
||||
selectors.root.useEditModalPossibleGrade.mockReturnValueOnce(undefined);
|
||||
out = useAdjustedGradeInputData();
|
||||
expect(out.hintText).toEqual(undefined);
|
||||
});
|
||||
it('passes localized slash and possible grade if available', () => {
|
||||
expect(out.hintText).toEqual(` ${localizedSlash} ${possibleGrade}`);
|
||||
});
|
||||
});
|
||||
describe('onChange', () => {
|
||||
it('sets modal state with event target value', () => {
|
||||
const testValue = 'test-value';
|
||||
out.onChange({ target: { value: testValue } });
|
||||
expect(setModalState).toHaveBeenCalledWith({ adjustedGradeValue: testValue });
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,33 @@
|
||||
import React from 'react';
|
||||
|
||||
import { Form } from '@edx/paragon';
|
||||
|
||||
import useAdjustedGradeInputData from './hooks';
|
||||
|
||||
/**
|
||||
* <AdjustedGradeInput />
|
||||
* Input control for adjusting the grade of a unit
|
||||
* displays an "/ ${possibleGrade} if there is one in the data model.
|
||||
*/
|
||||
export const AdjustedGradeInput = () => {
|
||||
const {
|
||||
value,
|
||||
onChange,
|
||||
hintText,
|
||||
} = useAdjustedGradeInputData();
|
||||
return (
|
||||
<span>
|
||||
<Form.Control
|
||||
type="text"
|
||||
name="adjustedGradeValue"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
/>
|
||||
{hintText}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
AdjustedGradeInput.propTypes = {};
|
||||
|
||||
export default AdjustedGradeInput;
|
||||
@@ -0,0 +1,38 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { Form } from '@edx/paragon';
|
||||
|
||||
import useAdjustedGradeInputData from './hooks';
|
||||
import AdjustedGradeInput from '.';
|
||||
|
||||
jest.mock('./hooks', () => jest.fn());
|
||||
|
||||
const hookProps = {
|
||||
hintText: 'some-hint-text',
|
||||
onChange: jest.fn().mockName('hook.onChange'),
|
||||
value: 'test-value',
|
||||
};
|
||||
useAdjustedGradeInputData.mockReturnValue(hookProps);
|
||||
|
||||
let el;
|
||||
describe('AdjustedGradeInput component', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
el = shallow(<AdjustedGradeInput />);
|
||||
});
|
||||
describe('behavior', () => {
|
||||
it('initializes hook data', () => {
|
||||
expect(useAdjustedGradeInputData).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
describe('render', () => {
|
||||
test('snapshot', () => {
|
||||
expect(el).toMatchSnapshot();
|
||||
const control = el.find(Form.Control);
|
||||
expect(control.props().value).toEqual(hookProps.value);
|
||||
expect(control.props().onChange).toEqual(hookProps.onChange);
|
||||
expect(el.contains(hookProps.hintText)).toEqual(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,55 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { Form } from '@edx/paragon';
|
||||
|
||||
import selectors from 'data/selectors';
|
||||
import actions from 'data/actions';
|
||||
|
||||
/**
|
||||
* <ReasonInput />
|
||||
* Input control for the "reason for change" field in the Edit modal.
|
||||
*/
|
||||
export class ReasonInput extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.ref = React.createRef();
|
||||
this.onChange = this.onChange.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.ref.current.focus();
|
||||
}
|
||||
|
||||
onChange = (event) => {
|
||||
this.props.setModalState({ reasonForChange: event.target.value });
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Form.Control
|
||||
type="text"
|
||||
name="reasonForChange"
|
||||
value={this.props.value}
|
||||
onChange={this.onChange}
|
||||
ref={this.ref}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
ReasonInput.propTypes = {
|
||||
// redux
|
||||
setModalState: PropTypes.func.isRequired,
|
||||
value: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export const mapStateToProps = (state) => ({
|
||||
value: selectors.app.modalState.reasonForChange(state),
|
||||
});
|
||||
|
||||
export const mapDispatchToProps = {
|
||||
setModalState: actions.app.setModalState,
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ReasonInput);
|
||||
@@ -1,90 +0,0 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import actions from 'data/actions';
|
||||
import selectors from 'data/selectors';
|
||||
|
||||
import {
|
||||
ReasonInput,
|
||||
mapStateToProps,
|
||||
mapDispatchToProps,
|
||||
} from './ReasonInput';
|
||||
|
||||
jest.mock('@edx/paragon', () => ({
|
||||
Form: { Control: () => 'Form.Control' },
|
||||
}));
|
||||
jest.mock('data/selectors', () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
app: {
|
||||
modalState: { reasonForChange: jest.fn(state => ({ reasonForChange: state })) },
|
||||
},
|
||||
},
|
||||
}));
|
||||
jest.mock('data/actions', () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
app: { setModalState: jest.fn() },
|
||||
},
|
||||
}));
|
||||
describe('ReasonInput', () => {
|
||||
let el;
|
||||
let props = {
|
||||
value: 'did not answer the question',
|
||||
};
|
||||
beforeEach(() => {
|
||||
props = {
|
||||
...props,
|
||||
setModalState: jest.fn(),
|
||||
};
|
||||
});
|
||||
describe('Component', () => {
|
||||
beforeEach(() => {
|
||||
el = shallow(<ReasonInput {...props} />, { disableLifecycleMethods: true });
|
||||
});
|
||||
describe('snapshots', () => {
|
||||
test('displays reason for change input control', () => {
|
||||
el.instance().onChange = jest.fn().mockName('this.onChange');
|
||||
expect(el.instance().render()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
describe('behavior', () => {
|
||||
describe('onChange', () => {
|
||||
it('calls props.setModalState event target value', () => {
|
||||
const value = 42;
|
||||
el.instance().onChange({ target: { value } });
|
||||
expect(props.setModalState).toHaveBeenCalledWith({
|
||||
reasonForChange: value,
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('componentDidMount', () => {
|
||||
it('focuses the input ref', () => {
|
||||
const focus = jest.fn();
|
||||
expect(el.instance().ref).toEqual({ current: null });
|
||||
el.instance().ref.current = { focus };
|
||||
el.instance().componentDidMount();
|
||||
expect(el.instance().ref.current.focus).toHaveBeenCalledWith();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('mapStateToProps', () => {
|
||||
const testState = { to: { catchThem: 'my real test', trainThem: 'my cause!' } };
|
||||
let mapped;
|
||||
beforeEach(() => {
|
||||
mapped = mapStateToProps(testState);
|
||||
});
|
||||
describe('modalState', () => {
|
||||
test('value from app.modalState.reasonForChange', () => {
|
||||
expect(mapped.value).toEqual(selectors.app.modalState.reasonForChange(testState));
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('mapDispatchToProps', () => {
|
||||
test('setModalState from actions.app.setModalState', () => {
|
||||
expect(mapDispatchToProps.setModalState).toEqual(actions.app.setModalState);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`ReasonInput component render snapshot 1`] = `
|
||||
<Form.Control
|
||||
data-testid="reason-input-control"
|
||||
name="reasonForChange"
|
||||
onChange={[MockFunction hook.onChange]}
|
||||
type="text"
|
||||
value="test-value"
|
||||
/>
|
||||
`;
|
||||
@@ -0,0 +1,25 @@
|
||||
import React from 'react';
|
||||
|
||||
import { actions, selectors } from 'data/redux/hooks';
|
||||
|
||||
const useReasonInputData = () => {
|
||||
const ref = React.useRef();
|
||||
const { reasonForChange } = selectors.app.useModalData();
|
||||
const setModalState = actions.app.useSetModalState();
|
||||
|
||||
React.useEffect(() => {
|
||||
ref.current.focus();
|
||||
}, [ref]);
|
||||
|
||||
const onChange = (event) => {
|
||||
setModalState({ reasonForChange: event.target.value });
|
||||
};
|
||||
|
||||
return {
|
||||
value: reasonForChange,
|
||||
onChange,
|
||||
ref,
|
||||
};
|
||||
};
|
||||
|
||||
export default useReasonInputData;
|
||||
@@ -0,0 +1,63 @@
|
||||
import React from 'react';
|
||||
|
||||
import { actions, selectors } from 'data/redux/hooks';
|
||||
import useReasonInputData from './hooks';
|
||||
|
||||
jest.mock('data/redux/hooks', () => ({
|
||||
selectors: {
|
||||
app: {
|
||||
useModalData: jest.fn(),
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
app: {
|
||||
useSetModalState: jest.fn(),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
const modalData = { reasonForChange: 'test-reason-for-change' };
|
||||
const setModalState = jest.fn();
|
||||
selectors.app.useModalData.mockReturnValue(modalData);
|
||||
actions.app.useSetModalState.mockReturnValue(setModalState);
|
||||
|
||||
const ref = { current: { focus: jest.fn() }, useRef: true };
|
||||
React.useRef.mockReturnValue(ref);
|
||||
|
||||
let out;
|
||||
describe('useReasonInputData hook', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
out = useReasonInputData();
|
||||
});
|
||||
describe('behavior', () => {
|
||||
it('initializes ref', () => {
|
||||
expect(React.useRef).toHaveBeenCalled();
|
||||
});
|
||||
it('initializes redux hooks', () => {
|
||||
expect(selectors.app.useModalData).toHaveBeenCalled();
|
||||
expect(actions.app.useSetModalState).toHaveBeenCalled();
|
||||
});
|
||||
it('focuses ref on load', () => {
|
||||
const [[cb, prereqs]] = React.useEffect.mock.calls;
|
||||
expect(prereqs).toEqual([ref]);
|
||||
cb();
|
||||
expect(ref.current.focus).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
describe('output', () => {
|
||||
it('forwards reasonForChange as value from modal data', () => {
|
||||
expect(out.value).toEqual(modalData.reasonForChange);
|
||||
});
|
||||
it('forwards ref', () => {
|
||||
expect(out.ref).toEqual(ref);
|
||||
});
|
||||
describe('onChange', () => {
|
||||
it('sets modal state with event target value', () => {
|
||||
const testValue = 'test-value';
|
||||
out.onChange({ target: { value: testValue } });
|
||||
expect(setModalState).toHaveBeenCalledWith({ reasonForChange: testValue });
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,27 @@
|
||||
import React from 'react';
|
||||
|
||||
import { Form } from '@edx/paragon';
|
||||
|
||||
import useReasonInputData from './hooks';
|
||||
|
||||
export const controlTestId = 'reason-input-control';
|
||||
|
||||
/**
|
||||
* <ReasonInput />
|
||||
* Input control for the "reason for change" field in the Edit modal.
|
||||
*/
|
||||
export const ReasonInput = () => {
|
||||
const { ref, value, onChange } = useReasonInputData();
|
||||
return (
|
||||
<Form.Control
|
||||
type="text"
|
||||
name="reasonForChange"
|
||||
data-testid={controlTestId}
|
||||
{...{ value, onChange, ref }}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
ReasonInput.propTypes = {};
|
||||
|
||||
export default ReasonInput;
|
||||
@@ -0,0 +1,37 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { Form } from '@edx/paragon';
|
||||
|
||||
import useReasonInputData from './hooks';
|
||||
import ReasonInput from '.';
|
||||
|
||||
jest.mock('./hooks', () => jest.fn());
|
||||
|
||||
const hookProps = {
|
||||
ref: 'reason-input-ref',
|
||||
onChange: jest.fn().mockName('hook.onChange'),
|
||||
value: 'test-value',
|
||||
};
|
||||
useReasonInputData.mockReturnValue(hookProps);
|
||||
|
||||
let el;
|
||||
describe('ReasonInput component', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
el = shallow(<ReasonInput />);
|
||||
});
|
||||
describe('behavior', () => {
|
||||
it('initializes hook data', () => {
|
||||
expect(useReasonInputData).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
describe('render', () => {
|
||||
test('snapshot', () => {
|
||||
expect(el).toMatchSnapshot();
|
||||
const control = el.find(Form.Control);
|
||||
expect(control.props().value).toEqual(hookProps.value);
|
||||
expect(control.props().onChange).toEqual(hookProps.onChange);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,26 @@
|
||||
import React from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
|
||||
import useReasonInputData from './hooks';
|
||||
import ReasonInput, { controlTestId } from '.';
|
||||
|
||||
jest.unmock('react');
|
||||
jest.unmock('@edx/paragon');
|
||||
jest.mock('./hooks', () => ({ __esModule: true, default: jest.fn() }));
|
||||
|
||||
const focus = jest.fn();
|
||||
const props = {
|
||||
value: 'test-value',
|
||||
onChange: jest.fn(),
|
||||
ref: { current: { focus }, useRef: jest.fn() },
|
||||
};
|
||||
useReasonInputData.mockReturnValue(props);
|
||||
|
||||
let el;
|
||||
describe('ReasonInput ref', () => {
|
||||
it('loads ref from hook', () => {
|
||||
el = render(<ReasonInput />);
|
||||
const control = el.getByTestId(controlTestId);
|
||||
expect(control).toEqual(props.ref.current);
|
||||
});
|
||||
});
|
||||
@@ -1,13 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`AdjustedGradeInput Component snapshots displays input control and "out of possible grade" label 1`] = `
|
||||
<span>
|
||||
<Control
|
||||
name="adjustedGradeValue"
|
||||
onChange={[MockFunction this.onChange]}
|
||||
type="text"
|
||||
value={1}
|
||||
/>
|
||||
/ 5
|
||||
</span>
|
||||
`;
|
||||
@@ -1,10 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`ReasonInput Component snapshots displays reason for change input control 1`] = `
|
||||
<Control
|
||||
name="reasonForChange"
|
||||
onChange={[MockFunction this.onChange]}
|
||||
type="text"
|
||||
value="did not answer the question"
|
||||
/>
|
||||
`;
|
||||
@@ -0,0 +1,25 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`OverrideTable component render snapshot 1`] = `
|
||||
<DataTable
|
||||
columns="test-columns"
|
||||
data={
|
||||
Array [
|
||||
Object {
|
||||
"test": "data",
|
||||
},
|
||||
Object {
|
||||
"andOther": "test-data",
|
||||
},
|
||||
Object {
|
||||
"adjustedGrade": <AdjustedGradeInput />,
|
||||
"date": Object {
|
||||
"formatted": 2000-01-01T00:00:00.000Z,
|
||||
},
|
||||
"reason": <ReasonInput />,
|
||||
},
|
||||
]
|
||||
}
|
||||
itemCount={2}
|
||||
/>
|
||||
`;
|
||||
@@ -1,64 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`OverrideTable Component snapshots basic snapshot shows a row for each entry and one editable row 1`] = `
|
||||
<DataTable
|
||||
columns={
|
||||
Array [
|
||||
Object {
|
||||
"Header": <FormattedMessage
|
||||
defaultMessage="Date"
|
||||
description="Edit Modal Override Table Date column header"
|
||||
id="gradebook.GradesView.EditModal.Overrides.dateHeader"
|
||||
/>,
|
||||
"accessor": "date",
|
||||
},
|
||||
Object {
|
||||
"Header": <FormattedMessage
|
||||
defaultMessage="Grader"
|
||||
description="Edit Modal Override Table Grader column header"
|
||||
id="gradebook.GradesView.EditModal.Overrides.graderHeader"
|
||||
/>,
|
||||
"accessor": "grader",
|
||||
},
|
||||
Object {
|
||||
"Header": <FormattedMessage
|
||||
defaultMessage="Reason"
|
||||
description="Edit Modal Override Table Reason column header"
|
||||
id="gradebook.GradesView.EditModal.Overrides.reasonHeader"
|
||||
/>,
|
||||
"accessor": "reason",
|
||||
},
|
||||
Object {
|
||||
"Header": <FormattedMessage
|
||||
defaultMessage="Adjusted grade"
|
||||
description="Edit Modal Override Table Adjusted grade column header"
|
||||
id="gradebook.GradesView.EditModal.Overrides.adjustedGradeHeader"
|
||||
/>,
|
||||
"accessor": "adjustedGrade",
|
||||
},
|
||||
]
|
||||
}
|
||||
data={
|
||||
Array [
|
||||
Object {
|
||||
"adjustedGrade": 0,
|
||||
"date": "yesterday",
|
||||
"grader": "me",
|
||||
"reason": "you ate my sandwich",
|
||||
},
|
||||
Object {
|
||||
"adjustedGrade": 20,
|
||||
"date": "today",
|
||||
"grader": "me",
|
||||
"reason": "you brought me a new sandwich",
|
||||
},
|
||||
Object {
|
||||
"adjustedGrade": <AdjustedGradeInput />,
|
||||
"date": "todaaaaaay",
|
||||
"reason": <ReasonInput />,
|
||||
},
|
||||
]
|
||||
}
|
||||
itemCount={2}
|
||||
/>
|
||||
`;
|
||||
26
src/components/GradesView/EditModal/OverrideTable/hooks.js
Normal file
26
src/components/GradesView/EditModal/OverrideTable/hooks.js
Normal file
@@ -0,0 +1,26 @@
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { gradeOverrideHistoryColumns as columns } from 'data/constants/app';
|
||||
import { selectors } from 'data/redux/hooks';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
const useOverrideTableData = () => {
|
||||
const { formatMessage } = useIntl();
|
||||
|
||||
const hide = selectors.grades.useHasOverrideErrors();
|
||||
const gradeOverrides = selectors.grades.useGradeData().gradeOverrideHistoryResults;
|
||||
const tableProps = {};
|
||||
if (!hide) {
|
||||
tableProps.columns = [
|
||||
{ Header: formatMessage(messages.dateHeader), accessor: columns.date },
|
||||
{ Header: formatMessage(messages.graderHeader), accessor: columns.grader },
|
||||
{ Header: formatMessage(messages.reasonHeader), accessor: columns.reason },
|
||||
{ Header: formatMessage(messages.adjustedGradeHeader), accessor: columns.adjustedGrade },
|
||||
];
|
||||
tableProps.data = gradeOverrides;
|
||||
}
|
||||
return { hide, ...tableProps };
|
||||
};
|
||||
|
||||
export default useOverrideTableData;
|
||||
@@ -0,0 +1,78 @@
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { formatMessage } from 'testUtils';
|
||||
|
||||
import { gradeOverrideHistoryColumns as columns } from 'data/constants/app';
|
||||
import { selectors } from 'data/redux/hooks';
|
||||
|
||||
import useOverrideTableData from './hooks';
|
||||
import messages from './messages';
|
||||
|
||||
jest.mock('data/redux/hooks', () => ({
|
||||
selectors: {
|
||||
grades: {
|
||||
useHasOverrideErrors: jest.fn(),
|
||||
useGradeData: jest.fn(),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
selectors.grades.useHasOverrideErrors.mockReturnValue(false);
|
||||
const gradeOverrides = ['some', 'override', 'data'];
|
||||
const gradeData = { gradeOverrideHistoryResults: gradeOverrides };
|
||||
selectors.grades.useGradeData.mockReturnValue(gradeData);
|
||||
|
||||
let out;
|
||||
describe('useOverrideTableData', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
out = useOverrideTableData();
|
||||
});
|
||||
describe('behavior', () => {
|
||||
it('initializes intl hook', () => {
|
||||
expect(useIntl).toHaveBeenCalled();
|
||||
});
|
||||
it('initializes redux hooks', () => {
|
||||
expect(selectors.grades.useHasOverrideErrors).toHaveBeenCalled();
|
||||
expect(selectors.grades.useGradeData).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
describe('output', () => {
|
||||
describe('no errors', () => {
|
||||
test('hide is false', () => {
|
||||
expect(out.hide).toEqual(false);
|
||||
});
|
||||
describe('columns', () => {
|
||||
test('date column', () => {
|
||||
const { Header, accessor } = out.columns[0];
|
||||
expect(Header).toEqual(formatMessage(messages.dateHeader));
|
||||
expect(accessor).toEqual(columns.date);
|
||||
});
|
||||
test('grader column', () => {
|
||||
const { Header, accessor } = out.columns[1];
|
||||
expect(Header).toEqual(formatMessage(messages.graderHeader));
|
||||
expect(accessor).toEqual(columns.grader);
|
||||
});
|
||||
test('reason column', () => {
|
||||
const { Header, accessor } = out.columns[2];
|
||||
expect(Header).toEqual(formatMessage(messages.reasonHeader));
|
||||
expect(accessor).toEqual(columns.reason);
|
||||
});
|
||||
test('adjustedGrade column', () => {
|
||||
const { Header, accessor } = out.columns[3];
|
||||
expect(Header).toEqual(formatMessage(messages.adjustedGradeHeader));
|
||||
expect(accessor).toEqual(columns.adjustedGrade);
|
||||
});
|
||||
});
|
||||
test('data passed from grade data', () => {
|
||||
expect(out.data).toEqual(gradeOverrides);
|
||||
});
|
||||
});
|
||||
describe('with errors', () => {
|
||||
it('returns hide true and no other fields', () => {
|
||||
selectors.grades.useHasOverrideErrors.mockReturnValue(true);
|
||||
out = useOverrideTableData();
|
||||
expect(out).toEqual({ hide: true });
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,73 +1,40 @@
|
||||
/* eslint-disable react/sort-comp, react/button-has-type, import/no-named-as-default */
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { DataTable } from '@edx/paragon';
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { gradeOverrideHistoryColumns as columns } from 'data/constants/app';
|
||||
import selectors from 'data/selectors';
|
||||
import { formatDateForDisplay } from 'utils';
|
||||
|
||||
import messages from './messages';
|
||||
import ReasonInput from './ReasonInput';
|
||||
import AdjustedGradeInput from './AdjustedGradeInput';
|
||||
import useOverrideTableData from './hooks';
|
||||
|
||||
/**
|
||||
* <OverrideTable />
|
||||
* Table containing previous grade override entries, and an "edit" row
|
||||
* with todays date, an AdjustedGradeInput and a ReasonInput
|
||||
*/
|
||||
export const OverrideTable = ({
|
||||
hide,
|
||||
gradeOverrides,
|
||||
todaysDate,
|
||||
}) => {
|
||||
if (hide) {
|
||||
return null;
|
||||
}
|
||||
|
||||
export const OverrideTable = () => {
|
||||
const { hide, columns, data } = useOverrideTableData();
|
||||
|
||||
if (hide) { return null; }
|
||||
|
||||
return (
|
||||
<DataTable
|
||||
columns={[
|
||||
{ Header: <FormattedMessage {...messages.dateHeader} />, accessor: columns.date },
|
||||
{ Header: <FormattedMessage {...messages.graderHeader} />, accessor: columns.grader },
|
||||
{ Header: <FormattedMessage {...messages.reasonHeader} />, accessor: columns.reason },
|
||||
{
|
||||
Header: <FormattedMessage {...messages.adjustedGradeHeader} />,
|
||||
accessor: columns.adjustedGrade,
|
||||
},
|
||||
]}
|
||||
columns={columns}
|
||||
data={[
|
||||
...gradeOverrides,
|
||||
...data,
|
||||
{
|
||||
adjustedGrade: <AdjustedGradeInput />,
|
||||
date: todaysDate,
|
||||
date: formatDateForDisplay(new Date()),
|
||||
reason: <ReasonInput />,
|
||||
},
|
||||
]}
|
||||
itemCount={gradeOverrides.length}
|
||||
itemCount={data.length}
|
||||
/>
|
||||
);
|
||||
};
|
||||
OverrideTable.defaultProps = {
|
||||
gradeOverrides: [],
|
||||
};
|
||||
OverrideTable.propTypes = {
|
||||
// redux
|
||||
gradeOverrides: PropTypes.arrayOf(PropTypes.shape({
|
||||
date: PropTypes.string,
|
||||
grader: PropTypes.string,
|
||||
reason: PropTypes.string,
|
||||
adjustedGrade: PropTypes.number,
|
||||
})),
|
||||
hide: PropTypes.bool.isRequired,
|
||||
todaysDate: PropTypes.string.isRequired,
|
||||
};
|
||||
OverrideTable.propTypes = {};
|
||||
|
||||
export const mapStateToProps = (state) => ({
|
||||
hide: selectors.grades.hasOverrideErrors(state),
|
||||
gradeOverrides: selectors.grades.gradeOverrides(state),
|
||||
todaysDate: selectors.app.modalState.todaysDate(state),
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps)(OverrideTable);
|
||||
export default OverrideTable;
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { DataTable } from '@edx/paragon';
|
||||
|
||||
import { formatDateForDisplay } from 'utils';
|
||||
|
||||
import AdjustedGradeInput from './AdjustedGradeInput';
|
||||
import ReasonInput from './ReasonInput';
|
||||
import useOverrideTableData from './hooks';
|
||||
import OverrideTable from '.';
|
||||
|
||||
jest.mock('utils', () => ({
|
||||
formatDateForDisplay: (date) => ({ formatted: date }),
|
||||
}));
|
||||
jest.mock('./hooks', () => jest.fn());
|
||||
jest.mock('./AdjustedGradeInput', () => 'AdjustedGradeInput');
|
||||
jest.mock('./ReasonInput', () => 'ReasonInput');
|
||||
|
||||
const hookProps = {
|
||||
hide: false,
|
||||
data: [
|
||||
{ test: 'data' },
|
||||
{ andOther: 'test-data' },
|
||||
],
|
||||
columns: 'test-columns',
|
||||
};
|
||||
useOverrideTableData.mockReturnValue(hookProps);
|
||||
|
||||
let el;
|
||||
describe('OverrideTable component', () => {
|
||||
beforeEach(() => {
|
||||
jest
|
||||
.clearAllMocks()
|
||||
.useFakeTimers('modern')
|
||||
.setSystemTime(new Date('2000-01-01').getTime());
|
||||
el = shallow(<OverrideTable />);
|
||||
});
|
||||
describe('behavior', () => {
|
||||
it('initializes hook data', () => {
|
||||
expect(useOverrideTableData).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
describe('render', () => {
|
||||
test('null render if hide', () => {
|
||||
useOverrideTableData.mockReturnValueOnce({ ...hookProps, hide: true });
|
||||
el = shallow(<OverrideTable />);
|
||||
expect(el.isEmptyRender()).toEqual(true);
|
||||
});
|
||||
test('snapshot', () => {
|
||||
expect(el).toMatchSnapshot();
|
||||
const table = el.find(DataTable);
|
||||
expect(table.props().columns).toEqual(hookProps.columns);
|
||||
const data = [...table.props().data];
|
||||
const inputRow = data.pop();
|
||||
const formattedDate = formatDateForDisplay(new Date());
|
||||
expect(data).toEqual(hookProps.data);
|
||||
expect(inputRow).toMatchObject({
|
||||
adjustedGrade: <AdjustedGradeInput />,
|
||||
date: formattedDate,
|
||||
reason: <ReasonInput />,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,81 +0,0 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import selectors from 'data/selectors';
|
||||
|
||||
import {
|
||||
OverrideTable,
|
||||
mapStateToProps,
|
||||
} from '.';
|
||||
|
||||
jest.mock('@edx/paragon', () => ({ DataTable: () => 'DataTable' }));
|
||||
jest.mock('./ReasonInput', () => 'ReasonInput');
|
||||
jest.mock('./AdjustedGradeInput', () => 'AdjustedGradeInput');
|
||||
|
||||
jest.mock('data/selectors', () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
app: {
|
||||
modalState: {
|
||||
todaysDate: jest.fn(state => ({ todaysDate: state })),
|
||||
},
|
||||
},
|
||||
grades: {
|
||||
hasOverrideErrors: jest.fn(state => ({ hasOverrideErrors: state })),
|
||||
gradeOverrides: jest.fn(state => ({ gradeOverrides: state })),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
describe('OverrideTable', () => {
|
||||
const props = {
|
||||
gradeOverrides: [
|
||||
{
|
||||
date: 'yesterday',
|
||||
grader: 'me',
|
||||
reason: 'you ate my sandwich',
|
||||
adjustedGrade: 0,
|
||||
},
|
||||
{
|
||||
date: 'today',
|
||||
grader: 'me',
|
||||
reason: 'you brought me a new sandwich',
|
||||
adjustedGrade: 20,
|
||||
},
|
||||
],
|
||||
hide: false,
|
||||
todaysDate: 'todaaaaaay',
|
||||
};
|
||||
|
||||
describe('Component', () => {
|
||||
describe('snapshots', () => {
|
||||
it('returns null if hide is true', () => {
|
||||
expect(shallow(<OverrideTable {...props} hide />)).toEqual({});
|
||||
});
|
||||
describe('basic snapshot', () => {
|
||||
test('shows a row for each entry and one editable row', () => {
|
||||
expect(shallow(<OverrideTable {...props} />)).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('mapStateToProps', () => {
|
||||
const testState = { I: 'wanna', be: 'the', very: 'best' };
|
||||
let mapped;
|
||||
beforeEach(() => {
|
||||
mapped = mapStateToProps(testState);
|
||||
});
|
||||
describe('modalState', () => {
|
||||
test('hide from grades.hasOverrideErrors', () => {
|
||||
expect(mapped.hide).toEqual(selectors.grades.hasOverrideErrors(testState));
|
||||
});
|
||||
test('gradeOverrides from grades.gradeOverrides', () => {
|
||||
expect(mapped.gradeOverrides).toEqual(selectors.grades.gradeOverrides(testState));
|
||||
});
|
||||
test('todaysData from app.modalState.todaysDate', () => {
|
||||
expect(mapped.todaysDate).toEqual(selectors.app.modalState.todaysDate(testState));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,99 +1,26 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`ModalHeaders Component snapshots gradeOverrideHistoryError is and empty and open is true modal open and StatusAlert showing 1`] = `
|
||||
exports[`ModalHeaders render snapshot 1`] = `
|
||||
<div>
|
||||
<HistoryHeader
|
||||
id="assignment"
|
||||
label={
|
||||
<FormattedMessage
|
||||
defaultMessage="Assignment"
|
||||
description="Edit Modal Assignment header"
|
||||
id="gradebook.GradesView.EditModal.headers.assignment"
|
||||
/>
|
||||
}
|
||||
value="Qwerty"
|
||||
label="Assignment"
|
||||
value="test-assignment-name"
|
||||
/>
|
||||
<HistoryHeader
|
||||
id="student"
|
||||
label={
|
||||
<FormattedMessage
|
||||
defaultMessage="Student"
|
||||
description="Edit Modal Student header"
|
||||
id="gradebook.GradesView.EditModal.headers.student"
|
||||
/>
|
||||
}
|
||||
value="Uiop"
|
||||
label="Student"
|
||||
value="test-user-name"
|
||||
/>
|
||||
<HistoryHeader
|
||||
id="original-grade"
|
||||
label={
|
||||
<FormattedMessage
|
||||
defaultMessage="Original Grade"
|
||||
description="Edit Modal Original Grade header"
|
||||
id="gradebook.GradesView.EditModal.headers.originalGrade"
|
||||
/>
|
||||
}
|
||||
value={20}
|
||||
label="Original Grade"
|
||||
value="test-original-grade"
|
||||
/>
|
||||
<HistoryHeader
|
||||
id="current-grade"
|
||||
label={
|
||||
<FormattedMessage
|
||||
defaultMessage="Current Grade"
|
||||
description="Edit Modal Current Grade header"
|
||||
id="gradebook.GradesView.EditModal.headers.currentGrade"
|
||||
/>
|
||||
}
|
||||
value={2}
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`ModalHeaders Component snapshots gradeOverrideHistoryError is empty and open is false modal closed and StatusAlert closed 1`] = `
|
||||
<div>
|
||||
<HistoryHeader
|
||||
id="assignment"
|
||||
label={
|
||||
<FormattedMessage
|
||||
defaultMessage="Assignment"
|
||||
description="Edit Modal Assignment header"
|
||||
id="gradebook.GradesView.EditModal.headers.assignment"
|
||||
/>
|
||||
}
|
||||
value="Qwerty"
|
||||
/>
|
||||
<HistoryHeader
|
||||
id="student"
|
||||
label={
|
||||
<FormattedMessage
|
||||
defaultMessage="Student"
|
||||
description="Edit Modal Student header"
|
||||
id="gradebook.GradesView.EditModal.headers.student"
|
||||
/>
|
||||
}
|
||||
value="Uiop"
|
||||
/>
|
||||
<HistoryHeader
|
||||
id="original-grade"
|
||||
label={
|
||||
<FormattedMessage
|
||||
defaultMessage="Original Grade"
|
||||
description="Edit Modal Original Grade header"
|
||||
id="gradebook.GradesView.EditModal.headers.originalGrade"
|
||||
/>
|
||||
}
|
||||
value={20}
|
||||
/>
|
||||
<HistoryHeader
|
||||
id="current-grade"
|
||||
label={
|
||||
<FormattedMessage
|
||||
defaultMessage="Current Grade"
|
||||
description="Edit Modal Current Grade header"
|
||||
id="gradebook.GradesView.EditModal.headers.currentGrade"
|
||||
/>
|
||||
}
|
||||
value={2}
|
||||
label="Current Grade"
|
||||
value="test-current-grade"
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`EditModal component render with error snapshot 1`] = `
|
||||
<ModalDialog
|
||||
hasCloseButton={true}
|
||||
isFullscreenOnMobile={true}
|
||||
isOpen="test-is-open"
|
||||
onClose={[MockFunction hooks.onClose]}
|
||||
size="xl"
|
||||
title="Edit Grades"
|
||||
>
|
||||
<ModalDialog.Body>
|
||||
<div>
|
||||
<ModalHeaders />
|
||||
<Alert
|
||||
dismissible={false}
|
||||
show={true}
|
||||
variant="danger"
|
||||
>
|
||||
test-error
|
||||
</Alert>
|
||||
<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>
|
||||
</ModalDialog.Body>
|
||||
<ModalDialog.Footer>
|
||||
<ActionRow>
|
||||
<ModalDialog.CloseButton
|
||||
variant="tertiary"
|
||||
>
|
||||
Cancel
|
||||
</ModalDialog.CloseButton>
|
||||
<Button
|
||||
onClick={[MockFunction hooks.handleAdjustedGradeClick]}
|
||||
variant="primary"
|
||||
>
|
||||
Save Grades
|
||||
</Button>
|
||||
</ActionRow>
|
||||
</ModalDialog.Footer>
|
||||
</ModalDialog>
|
||||
`;
|
||||
|
||||
exports[`EditModal component render without error snapshot 1`] = `
|
||||
<ModalDialog
|
||||
hasCloseButton={true}
|
||||
isFullscreenOnMobile={true}
|
||||
isOpen="test-is-open"
|
||||
onClose={[MockFunction hooks.onClose]}
|
||||
size="xl"
|
||||
title="Edit Grades"
|
||||
>
|
||||
<ModalDialog.Body>
|
||||
<div>
|
||||
<ModalHeaders />
|
||||
<Alert
|
||||
dismissible={false}
|
||||
show={false}
|
||||
variant="danger"
|
||||
/>
|
||||
<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>
|
||||
</ModalDialog.Body>
|
||||
<ModalDialog.Footer>
|
||||
<ActionRow>
|
||||
<ModalDialog.CloseButton
|
||||
variant="tertiary"
|
||||
>
|
||||
Cancel
|
||||
</ModalDialog.CloseButton>
|
||||
<Button
|
||||
onClick={[MockFunction hooks.handleAdjustedGradeClick]}
|
||||
variant="primary"
|
||||
>
|
||||
Save Grades
|
||||
</Button>
|
||||
</ActionRow>
|
||||
</ModalDialog.Footer>
|
||||
</ModalDialog>
|
||||
`;
|
||||
@@ -1,125 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`EditModal Component snapshots gradeOverrideHistoryError is and empty and open is true modal open and StatusAlert showing 1`] = `
|
||||
<ModalDialog
|
||||
hasCloseButton={true}
|
||||
isFullscreenOnMobile={true}
|
||||
isOpen={true}
|
||||
onClose={[MockFunction this.closeAssignmentModal]}
|
||||
size="xl"
|
||||
title="Edit Grades"
|
||||
>
|
||||
<ModalDialog.Body>
|
||||
<div>
|
||||
<ModalHeaders />
|
||||
<Alert
|
||||
dismissible={false}
|
||||
show={true}
|
||||
variant="danger"
|
||||
>
|
||||
Weve been trying to contact you regarding...
|
||||
</Alert>
|
||||
<OverrideTable />
|
||||
<div>
|
||||
<FormattedMessage
|
||||
defaultMessage="Showing most recent actions (max 5). To see more, please contact support"
|
||||
description="Edit Modal visibility hint message"
|
||||
id="gradebook.GradesView.EditModal.contactSupport"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<FormattedMessage
|
||||
defaultMessage="Note: Once you save, your changes will be visible to students."
|
||||
description="Edit Modal saved changes effect hint"
|
||||
id="gradebook.GradesView.EditModal.saveVisibility"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</ModalDialog.Body>
|
||||
<ModalDialog.Footer>
|
||||
<ActionRow>
|
||||
<ModalDialog.CloseButton
|
||||
variant="tertiary"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Cancel"
|
||||
description="Edit Modal close button text"
|
||||
id="gradebook.GradesView.EditModal.closeText"
|
||||
/>
|
||||
</ModalDialog.CloseButton>
|
||||
<Button
|
||||
onClick={[MockFunction this.handleAdjustedGradeClick]}
|
||||
variant="primary"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Save Grades"
|
||||
description="Edit Modal Save button label"
|
||||
id="gradebook.GradesView.EditModal.saveGrade"
|
||||
/>
|
||||
</Button>
|
||||
</ActionRow>
|
||||
</ModalDialog.Footer>
|
||||
</ModalDialog>
|
||||
`;
|
||||
|
||||
exports[`EditModal Component snapshots gradeOverrideHistoryError is empty and open is false modal closed and StatusAlert closed 1`] = `
|
||||
<ModalDialog
|
||||
hasCloseButton={true}
|
||||
isFullscreenOnMobile={true}
|
||||
isOpen={false}
|
||||
onClose={[MockFunction this.closeAssignmentModal]}
|
||||
size="xl"
|
||||
title="Edit Grades"
|
||||
>
|
||||
<ModalDialog.Body>
|
||||
<div>
|
||||
<ModalHeaders />
|
||||
<Alert
|
||||
dismissible={false}
|
||||
show={false}
|
||||
variant="danger"
|
||||
>
|
||||
|
||||
</Alert>
|
||||
<OverrideTable />
|
||||
<div>
|
||||
<FormattedMessage
|
||||
defaultMessage="Showing most recent actions (max 5). To see more, please contact support"
|
||||
description="Edit Modal visibility hint message"
|
||||
id="gradebook.GradesView.EditModal.contactSupport"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<FormattedMessage
|
||||
defaultMessage="Note: Once you save, your changes will be visible to students."
|
||||
description="Edit Modal saved changes effect hint"
|
||||
id="gradebook.GradesView.EditModal.saveVisibility"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</ModalDialog.Body>
|
||||
<ModalDialog.Footer>
|
||||
<ActionRow>
|
||||
<ModalDialog.CloseButton
|
||||
variant="tertiary"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Cancel"
|
||||
description="Edit Modal close button text"
|
||||
id="gradebook.GradesView.EditModal.closeText"
|
||||
/>
|
||||
</ModalDialog.CloseButton>
|
||||
<Button
|
||||
onClick={[MockFunction this.handleAdjustedGradeClick]}
|
||||
variant="primary"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Save Grades"
|
||||
description="Edit Modal Save button label"
|
||||
id="gradebook.GradesView.EditModal.saveGrade"
|
||||
/>
|
||||
</Button>
|
||||
</ActionRow>
|
||||
</ModalDialog.Footer>
|
||||
</ModalDialog>
|
||||
`;
|
||||
29
src/components/GradesView/EditModal/hooks.js
Normal file
29
src/components/GradesView/EditModal/hooks.js
Normal file
@@ -0,0 +1,29 @@
|
||||
import { selectors, actions, thunkActions } from 'data/redux/hooks';
|
||||
|
||||
export const useEditModalData = () => {
|
||||
const error = selectors.grades.useGradeData().gradeOverrideHistoryError;
|
||||
const isOpen = selectors.app.useModalData().open;
|
||||
const closeModal = actions.app.useCloseModal();
|
||||
const doneViewingAssignment = actions.grades.useDoneViewingAssignment();
|
||||
const updateGrades = thunkActions.grades.useUpdateGrades();
|
||||
|
||||
const onClose = () => {
|
||||
doneViewingAssignment();
|
||||
closeModal();
|
||||
};
|
||||
|
||||
const handleAdjustedGradeClick = () => {
|
||||
updateGrades();
|
||||
doneViewingAssignment();
|
||||
closeModal();
|
||||
};
|
||||
|
||||
return {
|
||||
onClose,
|
||||
error,
|
||||
handleAdjustedGradeClick,
|
||||
isOpen,
|
||||
};
|
||||
};
|
||||
|
||||
export default useEditModalData;
|
||||
68
src/components/GradesView/EditModal/hooks.test.js
Normal file
68
src/components/GradesView/EditModal/hooks.test.js
Normal file
@@ -0,0 +1,68 @@
|
||||
import { selectors, actions, thunkActions } from 'data/redux/hooks';
|
||||
|
||||
import useEditModalData from './hooks';
|
||||
|
||||
jest.mock('data/redux/hooks', () => ({
|
||||
actions: {
|
||||
app: { useCloseModal: jest.fn() },
|
||||
grades: { useDoneViewingAssignment: jest.fn() },
|
||||
},
|
||||
selectors: {
|
||||
app: { useModalData: jest.fn() },
|
||||
grades: { useGradeData: jest.fn() },
|
||||
},
|
||||
thunkActions: {
|
||||
grades: { useUpdateGrades: jest.fn() },
|
||||
},
|
||||
}));
|
||||
|
||||
const closeModal = jest.fn();
|
||||
const doneViewingAssignment = jest.fn();
|
||||
const updateGrades = jest.fn();
|
||||
actions.app.useCloseModal.mockReturnValue(closeModal);
|
||||
actions.grades.useDoneViewingAssignment.mockReturnValue(doneViewingAssignment);
|
||||
thunkActions.grades.useUpdateGrades.mockReturnValue(updateGrades);
|
||||
|
||||
const gradeData = { gradeOverridHistoryError: 'test-error' };
|
||||
const modalData = { open: true };
|
||||
selectors.app.useModalData.mockReturnValue(modalData);
|
||||
selectors.grades.useGradeData.mockReturnValue(gradeData);
|
||||
|
||||
let out;
|
||||
describe('useEditModalData', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
out = useEditModalData();
|
||||
});
|
||||
describe('behavior', () => {
|
||||
it('initializes redux hooks', () => {
|
||||
expect(selectors.grades.useGradeData).toHaveBeenCalled();
|
||||
expect(selectors.app.useModalData).toHaveBeenCalled();
|
||||
expect(actions.app.useCloseModal).toHaveBeenCalled();
|
||||
expect(actions.grades.useDoneViewingAssignment).toHaveBeenCalled();
|
||||
expect(thunkActions.grades.useUpdateGrades).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
describe('output', () => {
|
||||
it('forwards error from gradeData.gradeOverrideHistoryError', () => {
|
||||
expect(out.error).toEqual(gradeData.gradeOverrideHistoryError);
|
||||
});
|
||||
it('forwards isOpen from modalData.open', () => {
|
||||
expect(out.isOpen).toEqual(modalData.open);
|
||||
});
|
||||
describe('handleAdjustedGradeClick', () => {
|
||||
it('updates grades, calls doneViewingAssignment and closeModal', () => {
|
||||
out.handleAdjustedGradeClick();
|
||||
expect(updateGrades).toHaveBeenCalled();
|
||||
expect(doneViewingAssignment).toHaveBeenCalled();
|
||||
expect(closeModal).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
test('onClose calls doneViewingAssignment and closeModal', () => {
|
||||
out.onClose();
|
||||
expect(doneViewingAssignment).toHaveBeenCalled();
|
||||
expect(closeModal).toHaveBeenCalled();
|
||||
expect(updateGrades).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,7 +1,4 @@
|
||||
/* eslint-disable react/sort-comp, react/button-has-type, import/no-named-as-default */
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import {
|
||||
Button,
|
||||
@@ -9,15 +6,12 @@ import {
|
||||
ModalDialog,
|
||||
ActionRow,
|
||||
} from '@edx/paragon';
|
||||
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { useIntl } 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';
|
||||
import useEditModalData from './hooks';
|
||||
import messages from './messages';
|
||||
|
||||
/**
|
||||
* <EditModal />
|
||||
@@ -28,87 +22,48 @@ import ModalHeaders from './ModalHeaders';
|
||||
* adjusting the grade.
|
||||
* (also provides a close button that clears the modal state)
|
||||
*/
|
||||
export class EditModal extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.closeAssignmentModal = this.closeAssignmentModal.bind(this);
|
||||
this.handleAdjustedGradeClick = this.handleAdjustedGradeClick.bind(this);
|
||||
}
|
||||
export const EditModal = () => {
|
||||
const { formatMessage } = useIntl();
|
||||
const {
|
||||
onClose,
|
||||
error,
|
||||
handleAdjustedGradeClick,
|
||||
isOpen,
|
||||
} = useEditModalData();
|
||||
|
||||
closeAssignmentModal() {
|
||||
this.props.doneViewingAssignment();
|
||||
this.props.closeModal();
|
||||
}
|
||||
return (
|
||||
<ModalDialog
|
||||
title={formatMessage(messages.title)}
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
size="xl"
|
||||
hasCloseButton
|
||||
isFullscreenOnMobile
|
||||
>
|
||||
<ModalDialog.Body>
|
||||
<div>
|
||||
<ModalHeaders />
|
||||
<Alert variant="danger" show={!!error} dismissible={false}>
|
||||
{error}
|
||||
</Alert>
|
||||
<OverrideTable />
|
||||
<div>{formatMessage(messages.visibility)}</div>
|
||||
<div>{formatMessage(messages.saveVisibility)}</div>
|
||||
</div>
|
||||
</ModalDialog.Body>
|
||||
|
||||
handleAdjustedGradeClick() {
|
||||
this.props.updateGrades();
|
||||
this.closeAssignmentModal();
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<ModalDialog
|
||||
title={this.props.intl.formatMessage(messages.title)}
|
||||
isOpen={this.props.open}
|
||||
onClose={this.closeAssignmentModal}
|
||||
size="xl"
|
||||
hasCloseButton
|
||||
isFullscreenOnMobile
|
||||
>
|
||||
<ModalDialog.Body>
|
||||
<div>
|
||||
<ModalHeaders />
|
||||
<Alert
|
||||
variant="danger"
|
||||
show={!!this.props.gradeOverrideHistoryError}
|
||||
dismissible={false}
|
||||
>
|
||||
{this.props.gradeOverrideHistoryError}
|
||||
</Alert>
|
||||
<OverrideTable />
|
||||
<div><FormattedMessage {...messages.visibility} /></div>
|
||||
<div><FormattedMessage {...messages.saveVisibility} /></div>
|
||||
</div>
|
||||
</ModalDialog.Body>
|
||||
<ModalDialog.Footer>
|
||||
<ActionRow>
|
||||
<ModalDialog.CloseButton variant="tertiary">
|
||||
<FormattedMessage {...messages.closeText} />
|
||||
</ModalDialog.CloseButton>
|
||||
<Button variant="primary" onClick={this.handleAdjustedGradeClick}>
|
||||
<FormattedMessage {...messages.saveGrade} />
|
||||
</Button>
|
||||
</ActionRow>
|
||||
</ModalDialog.Footer>
|
||||
</ModalDialog>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
EditModal.defaultProps = {
|
||||
gradeOverrideHistoryError: '',
|
||||
<ModalDialog.Footer>
|
||||
<ActionRow>
|
||||
<ModalDialog.CloseButton variant="tertiary">
|
||||
{formatMessage(messages.closeText)}
|
||||
</ModalDialog.CloseButton>
|
||||
<Button variant="primary" onClick={handleAdjustedGradeClick}>
|
||||
{formatMessage(messages.saveGrade)}
|
||||
</Button>
|
||||
</ActionRow>
|
||||
</ModalDialog.Footer>
|
||||
</ModalDialog>
|
||||
);
|
||||
};
|
||||
|
||||
EditModal.propTypes = {
|
||||
// redux
|
||||
gradeOverrideHistoryError: PropTypes.string,
|
||||
open: PropTypes.bool.isRequired,
|
||||
closeModal: PropTypes.func.isRequired,
|
||||
doneViewingAssignment: PropTypes.func.isRequired,
|
||||
updateGrades: PropTypes.func.isRequired,
|
||||
// injected
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export const mapStateToProps = (state) => ({
|
||||
gradeOverrideHistoryError: selectors.grades.gradeOverrideHistoryError(state),
|
||||
open: selectors.app.modalState.open(state),
|
||||
});
|
||||
|
||||
export const mapDispatchToProps = {
|
||||
closeModal: actions.app.closeModal,
|
||||
doneViewingAssignment: actions.grades.doneViewingAssignment,
|
||||
updateGrades: thunkActions.grades.updateGrades,
|
||||
};
|
||||
|
||||
export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(EditModal));
|
||||
export default EditModal;
|
||||
|
||||
132
src/components/GradesView/EditModal/index.test.jsx
Normal file
132
src/components/GradesView/EditModal/index.test.jsx
Normal file
@@ -0,0 +1,132 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import {
|
||||
ActionRow,
|
||||
ModalDialog,
|
||||
} from '@edx/paragon';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { formatMessage } from 'testUtils';
|
||||
|
||||
import ModalHeaders from './ModalHeaders';
|
||||
import OverrideTable from './OverrideTable';
|
||||
import useEditModalData from './hooks';
|
||||
import EditModal from '.';
|
||||
import messages from './messages';
|
||||
|
||||
jest.mock('./hooks', () => jest.fn());
|
||||
jest.mock('./ModalHeaders', () => 'ModalHeaders');
|
||||
jest.mock('./OverrideTable', () => 'OverrideTable');
|
||||
|
||||
const hookProps = {
|
||||
onClose: jest.fn().mockName('hooks.onClose'),
|
||||
error: 'test-error',
|
||||
handleAdjustedGradeClick: jest.fn().mockName('hooks.handleAdjustedGradeClick'),
|
||||
isOpen: 'test-is-open',
|
||||
};
|
||||
useEditModalData.mockReturnValue(hookProps);
|
||||
|
||||
let el;
|
||||
describe('EditModal component', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
el = shallow(<EditModal />);
|
||||
});
|
||||
describe('behavior', () => {
|
||||
it('initializes intl hook', () => {
|
||||
expect(useIntl).toHaveBeenCalled();
|
||||
});
|
||||
it('initializes component hooks', () => {
|
||||
expect(useEditModalData).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
describe('render', () => {
|
||||
test('modal props', () => {
|
||||
const modalProps = el.find(ModalDialog).props();
|
||||
expect(modalProps.title).toEqual(formatMessage(messages.title));
|
||||
expect(modalProps.isOpen).toEqual(hookProps.isOpen);
|
||||
expect(modalProps.onClose).toEqual(hookProps.onClose);
|
||||
});
|
||||
const loadBody = () => {
|
||||
const body = el.find(ModalDialog).children().at(0);
|
||||
const children = body.find('div').children();
|
||||
return { body, children };
|
||||
};
|
||||
const testBody = () => {
|
||||
test('type', () => {
|
||||
const { body } = loadBody();
|
||||
expect(body.type()).toEqual('ModalDialog.Body');
|
||||
});
|
||||
test('headers row', () => {
|
||||
const { children } = loadBody();
|
||||
expect(children.at(0)).toMatchObject(shallow(<ModalHeaders />));
|
||||
});
|
||||
test('table row', () => {
|
||||
const { children } = loadBody();
|
||||
expect(children.at(2)).toMatchObject(shallow(<OverrideTable />));
|
||||
});
|
||||
test('messages', () => {
|
||||
const { children } = loadBody();
|
||||
expect(
|
||||
children.at(3).contains(formatMessage(messages.visibility)),
|
||||
).toEqual(true);
|
||||
expect(
|
||||
children.at(4).contains(formatMessage(messages.saveVisibility)),
|
||||
).toEqual(true);
|
||||
});
|
||||
};
|
||||
const testFooter = () => {
|
||||
let footer;
|
||||
beforeEach(() => {
|
||||
footer = el.find(ModalDialog).children().at(1);
|
||||
});
|
||||
test('type', () => {
|
||||
expect(footer.type()).toEqual('ModalDialog.Footer');
|
||||
});
|
||||
test('contains action row', () => {
|
||||
expect(footer.children().at(0).type()).toEqual('ActionRow');
|
||||
});
|
||||
test('close button', () => {
|
||||
const button = footer.find(ActionRow).children().at(0);
|
||||
expect(button.contains(formatMessage(messages.closeText))).toEqual(true);
|
||||
expect(button.type()).toEqual('ModalDialog.CloseButton');
|
||||
});
|
||||
test('adjusted grade button', () => {
|
||||
const button = footer.find(ActionRow).children().at(1);
|
||||
expect(button.contains(formatMessage(messages.saveGrade))).toEqual(true);
|
||||
expect(button.type()).toEqual('Button');
|
||||
expect(button.props().onClick).toEqual(hookProps.handleAdjustedGradeClick);
|
||||
});
|
||||
};
|
||||
describe('without error', () => {
|
||||
beforeEach(() => {
|
||||
useEditModalData.mockReturnValueOnce({ ...hookProps, error: undefined });
|
||||
el = shallow(<EditModal />);
|
||||
});
|
||||
test('snapshot', () => {
|
||||
expect(el).toMatchSnapshot();
|
||||
});
|
||||
testBody();
|
||||
testFooter();
|
||||
test('alert row', () => {
|
||||
const alert = loadBody().children.at(1);
|
||||
expect(alert.type()).toEqual('Alert');
|
||||
expect(alert.props().show).toEqual(false);
|
||||
});
|
||||
});
|
||||
describe('with error', () => {
|
||||
test('snapshot', () => {
|
||||
expect(el).toMatchSnapshot();
|
||||
});
|
||||
testBody();
|
||||
test('alert row', () => {
|
||||
const alert = loadBody().children.at(1);
|
||||
expect(alert.type()).toEqual('Alert');
|
||||
expect(alert.props().show).toEqual(true);
|
||||
expect(alert.contains(hookProps.error)).toEqual(true);
|
||||
});
|
||||
testFooter();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,130 +0,0 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import actions from 'data/actions';
|
||||
import selectors from 'data/selectors';
|
||||
import thunkActions from 'data/thunkActions';
|
||||
|
||||
import {
|
||||
EditModal,
|
||||
mapDispatchToProps,
|
||||
mapStateToProps,
|
||||
}
|
||||
from '.';
|
||||
|
||||
jest.mock('./OverrideTable', () => 'OverrideTable');
|
||||
jest.mock('./ModalHeaders', () => 'ModalHeaders');
|
||||
jest.mock('data/actions', () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
app: { closeModal: jest.fn() },
|
||||
grades: { doneViewingAssignment: jest.fn() },
|
||||
},
|
||||
}));
|
||||
jest.mock('data/thunkActions', () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
grades: { updateGrades: jest.fn() },
|
||||
},
|
||||
}));
|
||||
jest.mock('data/selectors', () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
app: {
|
||||
modalState: {
|
||||
open: jest.fn(state => ({ isModalOpen: state })),
|
||||
},
|
||||
},
|
||||
grades: {
|
||||
gradeOverrideHistoryError: jest.fn(state => ({ overrideHistoryError: state })),
|
||||
},
|
||||
},
|
||||
}));
|
||||
describe('EditModal', () => {
|
||||
let props;
|
||||
beforeEach(() => {
|
||||
props = {
|
||||
gradeOverrideHistoryError: 'Weve been trying to contact you regarding...',
|
||||
open: true,
|
||||
closeModal: jest.fn(),
|
||||
doneViewingAssignment: jest.fn(),
|
||||
updateGrades: jest.fn(),
|
||||
|
||||
intl: { formatMessage: (msg) => msg.defaultMessage },
|
||||
};
|
||||
});
|
||||
|
||||
describe('Component', () => {
|
||||
describe('behavior', () => {
|
||||
let el;
|
||||
beforeEach(() => {
|
||||
el = shallow(<EditModal {...props} />);
|
||||
});
|
||||
describe('closeAssignmentModal', () => {
|
||||
it('calls props.doneViewingAssignment and props.closeModal', () => {
|
||||
el.instance().closeAssignmentModal();
|
||||
expect(props.doneViewingAssignment).toHaveBeenCalledWith();
|
||||
expect(props.closeModal).toHaveBeenCalledWith();
|
||||
});
|
||||
});
|
||||
describe('handleAdjustedGradeClick', () => {
|
||||
it('calls props.updateGardes and this.closeAssignmentModal', () => {
|
||||
el.instance().closeAssignmentModal = jest.fn();
|
||||
el.instance().handleAdjustedGradeClick();
|
||||
expect(props.updateGrades).toHaveBeenCalledWith();
|
||||
expect(el.instance().closeAssignmentModal).toHaveBeenCalledWith();
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('snapshots', () => {
|
||||
let el;
|
||||
beforeEach(() => {
|
||||
el = shallow(<EditModal {...props} />);
|
||||
el.instance().closeAssignmentModal = jest.fn().mockName('this.closeAssignmentModal');
|
||||
el.instance().handleAdjustedGradeClick = jest.fn().mockName(
|
||||
'this.handleAdjustedGradeClick',
|
||||
);
|
||||
});
|
||||
describe('gradeOverrideHistoryError is and empty and open is true', () => {
|
||||
test('modal open and StatusAlert showing', () => {
|
||||
expect(el.instance().render()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
describe('gradeOverrideHistoryError is empty and open is false', () => {
|
||||
test('modal closed and StatusAlert closed', () => {
|
||||
el.setProps({ open: false, gradeOverrideHistoryError: '' });
|
||||
expect(el.instance().render()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('mapStateToProps', () => {
|
||||
const testState = { martha: 'why did you say that name?!' };
|
||||
let mapped;
|
||||
beforeEach(() => {
|
||||
mapped = mapStateToProps(testState);
|
||||
});
|
||||
test('gradeOverrideHistoryError from grades.gradeOverrideHistoryError', () => {
|
||||
expect(
|
||||
mapped.gradeOverrideHistoryError,
|
||||
).toEqual(selectors.grades.gradeOverrideHistoryError(testState));
|
||||
});
|
||||
test('open from app.modalState.open', () => {
|
||||
expect(mapped.open).toEqual(selectors.app.modalState.open(testState));
|
||||
});
|
||||
});
|
||||
describe('mapDispatchToProps', () => {
|
||||
test('closeModal from actions.app.closeModal', () => {
|
||||
expect(mapDispatchToProps.closeModal).toEqual(actions.app.closeModal);
|
||||
});
|
||||
test('doneViewingAssignemtn from actions.grades.doneViewingAssignment', () => {
|
||||
expect(
|
||||
mapDispatchToProps.doneViewingAssignment,
|
||||
).toEqual(actions.grades.doneViewingAssignment);
|
||||
});
|
||||
test('updateGrades from thunkActions.grades.updateGrades', () => {
|
||||
expect(mapDispatchToProps.updateGrades).toEqual(thunkActions.grades.updateGrades);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,11 +1,10 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { Button } from '@edx/paragon';
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import selectors from 'data/selectors';
|
||||
import { selectors } from 'data/redux/hooks';
|
||||
|
||||
/**
|
||||
* FilterBadge
|
||||
@@ -16,56 +15,43 @@ import selectors from 'data/selectors';
|
||||
* @param {string} filterName - api filter name (for redux connector)
|
||||
*/
|
||||
export const FilterBadge = ({
|
||||
config: {
|
||||
filterName,
|
||||
handleClose,
|
||||
}) => {
|
||||
const { formatMessage } = useIntl();
|
||||
const {
|
||||
displayName,
|
||||
isDefault,
|
||||
hideValue,
|
||||
value,
|
||||
connectedFilters,
|
||||
},
|
||||
handleClose,
|
||||
}) => !isDefault && (
|
||||
<div>
|
||||
<span className="badge badge-info">
|
||||
<span>
|
||||
<FormattedMessage {...displayName} />
|
||||
} = selectors.root.useFilterBadgeConfig(filterName);
|
||||
if (isDefault) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<span className="badge badge-info">
|
||||
<span>{formatMessage(displayName)}</span>
|
||||
<span>
|
||||
{!hideValue ? `: ${value}` : ''}
|
||||
</span>
|
||||
<Button
|
||||
className="btn-info"
|
||||
aria-label="close"
|
||||
onClick={handleClose(connectedFilters)}
|
||||
>
|
||||
<span aria-hidden="true">×</span>
|
||||
</Button>
|
||||
</span>
|
||||
<span>
|
||||
{!hideValue ? `: ${value}` : ''}
|
||||
</span>
|
||||
<Button
|
||||
className="btn-info"
|
||||
aria-label="close"
|
||||
onClick={handleClose(connectedFilters)}
|
||||
>
|
||||
<span aria-hidden="true">×</span>
|
||||
</Button>
|
||||
</span>
|
||||
<br />
|
||||
</div>
|
||||
);
|
||||
<br />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
FilterBadge.propTypes = {
|
||||
handleClose: PropTypes.func.isRequired,
|
||||
// eslint-disable-next-line
|
||||
filterName: PropTypes.string.isRequired,
|
||||
// redux
|
||||
config: PropTypes.shape({
|
||||
connectedFilters: PropTypes.arrayOf(PropTypes.string),
|
||||
displayName: PropTypes.shape({
|
||||
defaultMessage: PropTypes.string,
|
||||
}).isRequired,
|
||||
isDefault: PropTypes.bool.isRequired,
|
||||
hideValue: PropTypes.bool,
|
||||
value: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.bool,
|
||||
]),
|
||||
}).isRequired,
|
||||
};
|
||||
|
||||
export const mapStateToProps = (state, ownProps) => ({
|
||||
config: selectors.root.filterBadgeConfig(state, ownProps.filterName),
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps)(FilterBadge);
|
||||
export default FilterBadge;
|
||||
|
||||
@@ -1,107 +1,95 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { formatMessage } from 'testUtils';
|
||||
import { Button } from '@edx/paragon';
|
||||
import selectors from 'data/selectors';
|
||||
import { FilterBadge, mapStateToProps } from './FilterBadge';
|
||||
import { selectors } from 'data/redux/hooks';
|
||||
import FilterBadge from './FilterBadge';
|
||||
|
||||
jest.mock('@edx/paragon', () => ({
|
||||
Button: () => 'Button',
|
||||
}));
|
||||
jest.mock('data/selectors', () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
jest.mock('data/redux/hooks', () => ({
|
||||
selectors: {
|
||||
root: {
|
||||
filterBadgeConfig: jest.fn(state => ({ filterBadgeConfig: state })),
|
||||
useFilterBadgeConfig: jest.fn(),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
const handleClose = jest.fn(filters => ({ handleClose: filters }));
|
||||
const filterName = 'test-filter-name';
|
||||
|
||||
const hookProps = {
|
||||
displayName: {
|
||||
defaultMessage: 'a common name',
|
||||
},
|
||||
isDefault: false,
|
||||
hideValue: false,
|
||||
value: 'a common value',
|
||||
connectedFilters: ['some', 'filters'],
|
||||
};
|
||||
selectors.root.useFilterBadgeConfig.mockReturnValue(hookProps);
|
||||
|
||||
let el;
|
||||
describe('FilterBadge', () => {
|
||||
describe('component', () => {
|
||||
const config = {
|
||||
displayName: {
|
||||
defaultMessage: 'a common name',
|
||||
},
|
||||
isDefault: false,
|
||||
hideValue: false,
|
||||
value: 'a common value',
|
||||
connectedFilters: ['some', 'filters'],
|
||||
};
|
||||
const filterName = 'api.filter.name';
|
||||
let handleClose;
|
||||
let el;
|
||||
let props;
|
||||
beforeEach(() => {
|
||||
handleClose = (filters) => ({ handleClose: filters });
|
||||
props = { filterName, handleClose, config };
|
||||
beforeEach(() => {
|
||||
el = shallow(<FilterBadge {...{ handleClose, filterName }} />);
|
||||
});
|
||||
describe('behavior', () => {
|
||||
it('initializes intl hook', () => {
|
||||
expect(useIntl).toHaveBeenCalled();
|
||||
});
|
||||
describe('with default value', () => {
|
||||
beforeEach(() => {
|
||||
el = shallow(
|
||||
<FilterBadge {...props} config={{ ...config, isDefault: true }} />,
|
||||
);
|
||||
});
|
||||
test('snapshot - empty', () => {
|
||||
expect(el).toMatchSnapshot();
|
||||
});
|
||||
it('does not display', () => {
|
||||
expect(el).toEqual({});
|
||||
});
|
||||
});
|
||||
describe('with non-default value (active)', () => {
|
||||
describe('if hideValue is true', () => {
|
||||
beforeEach(() => {
|
||||
el = shallow(
|
||||
<FilterBadge {...props} config={{ ...config, hideValue: true }} />,
|
||||
);
|
||||
});
|
||||
test('snapshot - shows displayName but not value in span', () => {
|
||||
expect(el).toMatchSnapshot();
|
||||
});
|
||||
it('shows displayName but not value in span', () => {
|
||||
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));
|
||||
});
|
||||
});
|
||||
describe('if hideValue is false (default)', () => {
|
||||
beforeEach(() => {
|
||||
el = shallow(<FilterBadge {...props} />);
|
||||
});
|
||||
test('snapshot', () => {
|
||||
expect(el).toMatchSnapshot();
|
||||
});
|
||||
it('shows displayName and value in span', () => {
|
||||
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', () => {
|
||||
expect(el.find(Button).props().onClick).toEqual(handleClose(config.connectedFilters));
|
||||
});
|
||||
});
|
||||
it('initializes redux hooks', () => {
|
||||
expect(selectors.root.useFilterBadgeConfig).toHaveBeenCalledWith(filterName);
|
||||
});
|
||||
});
|
||||
describe('mapStateToProps', () => {
|
||||
const testState = { some: 'kind', of: 'alien' };
|
||||
const filterName = 'Lilu Dallas Multipass';
|
||||
test('config loads config from root.filterBadgeConfig with ownProps.filterName', () => {
|
||||
const { config } = mapStateToProps(testState, { filterName });
|
||||
expect(config).toEqual(selectors.root.filterBadgeConfig(testState, filterName));
|
||||
describe('render', () => {
|
||||
const testDisplayName = () => {
|
||||
test('formatted display name appears on badge', () => {
|
||||
expect(el.contains(formatMessage(hookProps.displayName))).toEqual(true);
|
||||
});
|
||||
};
|
||||
const testCloseButton = () => {
|
||||
test('close button forwards close method', () => {
|
||||
expect(el.find(Button).props().onClick).toEqual(handleClose(hookProps.connectedFilters));
|
||||
});
|
||||
};
|
||||
test('empty render if isDefault', () => {
|
||||
selectors.root.useFilterBadgeConfig.mockReturnValueOnce({
|
||||
...hookProps,
|
||||
isDefault: true,
|
||||
});
|
||||
el = shallow(<FilterBadge {...{ handleClose, filterName }} />);
|
||||
expect(el.isEmptyRender()).toEqual(true);
|
||||
});
|
||||
describe('hide Value', () => {
|
||||
beforeEach(() => {
|
||||
selectors.root.useFilterBadgeConfig.mockReturnValueOnce({
|
||||
...hookProps,
|
||||
hideValue: true,
|
||||
});
|
||||
el = shallow(<FilterBadge {...{ handleClose, filterName }} />);
|
||||
});
|
||||
testDisplayName();
|
||||
testCloseButton();
|
||||
test('snapshot', () => {
|
||||
expect(el).toMatchSnapshot();
|
||||
});
|
||||
test('value is note present in the badge', () => {
|
||||
expect(el.contains(hookProps.value)).toEqual(false);
|
||||
});
|
||||
});
|
||||
describe('do not hide value', () => {
|
||||
testDisplayName();
|
||||
testCloseButton();
|
||||
test('snapshot', () => {
|
||||
expect(el).toMatchSnapshot();
|
||||
});
|
||||
test('value is note present in the badge', () => {
|
||||
expect(el.text().includes(hookProps.value)).toEqual(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,16 +1,12 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`FilterBadge component with default value snapshot - empty 1`] = `""`;
|
||||
|
||||
exports[`FilterBadge component with non-default value (active) if hideValue is false (default) snapshot 1`] = `
|
||||
exports[`FilterBadge render do not hide value snapshot 1`] = `
|
||||
<div>
|
||||
<span
|
||||
className="badge badge-info"
|
||||
>
|
||||
<span>
|
||||
<FormattedMessage
|
||||
defaultMessage="a common name"
|
||||
/>
|
||||
a common name
|
||||
</span>
|
||||
<span>
|
||||
: a common value
|
||||
@@ -38,15 +34,13 @@ exports[`FilterBadge component with non-default value (active) if hideValue is f
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`FilterBadge component with non-default value (active) if hideValue is true snapshot - shows displayName but not value in span 1`] = `
|
||||
exports[`FilterBadge render hide Value snapshot 1`] = `
|
||||
<div>
|
||||
<span
|
||||
className="badge badge-info"
|
||||
>
|
||||
<span>
|
||||
<FormattedMessage
|
||||
defaultMessage="a common name"
|
||||
/>
|
||||
a common name
|
||||
</span>
|
||||
<span />
|
||||
<Button
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { Button, Icon } from '@edx/paragon';
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import thunkActions from 'data/thunkActions';
|
||||
|
||||
import messages from './FilterMenuToggle.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.
|
||||
*/
|
||||
export const FilterMenuToggle = ({ toggleFilterDrawer }) => (
|
||||
<Button
|
||||
id="edit-filters-btn"
|
||||
className="btn-primary align-self-start"
|
||||
onClick={toggleFilterDrawer}
|
||||
>
|
||||
<Icon className="fa fa-filter" /> <FormattedMessage {...messages.editFilters} />
|
||||
</Button>
|
||||
);
|
||||
|
||||
FilterMenuToggle.propTypes = {
|
||||
// From Redux
|
||||
toggleFilterDrawer: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export const mapStateToProps = () => ({});
|
||||
|
||||
export const mapDispatchToProps = {
|
||||
toggleFilterDrawer: thunkActions.app.filterMenu.toggle,
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(FilterMenuToggle);
|
||||
@@ -1,42 +0,0 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import thunkActions from 'data/thunkActions';
|
||||
|
||||
import { FilterMenuToggle, mapDispatchToProps, mapStateToProps } from './FilterMenuToggle';
|
||||
|
||||
jest.mock('@edx/paragon', () => ({
|
||||
Button: () => 'Button',
|
||||
Icon: () => 'Icon',
|
||||
}));
|
||||
jest.mock('data/thunkActions', () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
app: {
|
||||
filterMenu: { toggle: jest.fn() },
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
describe('FilterMenuToggle component', () => {
|
||||
describe('snapshots', () => {
|
||||
test('basic snapshot', () => {
|
||||
const toggleFilterDrawer = jest.fn().mockName('this.props.toggleFilterDrawer');
|
||||
expect(shallow((
|
||||
<FilterMenuToggle {...{ toggleFilterDrawer }} />
|
||||
))).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
describe('mapStateToProps', () => {
|
||||
test('does not connect any selectors', () => {
|
||||
expect(mapStateToProps({ test: 'state' })).toEqual({});
|
||||
});
|
||||
});
|
||||
describe('mapDispatchToProps', () => {
|
||||
test('toggleFilterDrawer from thunkActions.app.filterMenu.toggle', () => {
|
||||
expect(mapDispatchToProps.toggleFilterDrawer).toEqual(
|
||||
thunkActions.app.filterMenu.toggle,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,15 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`FilterMenuToggle component render snapshot 1`] = `
|
||||
<Button
|
||||
className="btn-primary align-self-start"
|
||||
id="edit-filters-btn"
|
||||
onClick={[MockFunction hooks.toggleFilterMenu]}
|
||||
>
|
||||
<Icon
|
||||
src="FilterAlt"
|
||||
/>
|
||||
|
||||
Edit Filters
|
||||
</Button>
|
||||
`;
|
||||
31
src/components/GradesView/FilterMenuToggle/index.jsx
Normal file
31
src/components/GradesView/FilterMenuToggle/index.jsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import React from 'react';
|
||||
|
||||
import { Button, Icon } from '@edx/paragon';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { FilterAlt } from '@edx/paragon/icons';
|
||||
|
||||
import { thunkActions } from 'data/redux/hooks';
|
||||
|
||||
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.
|
||||
*/
|
||||
export const FilterMenuToggle = () => {
|
||||
const toggleFilterMenu = thunkActions.app.filterMenu.useToggleMenu();
|
||||
const { formatMessage } = useIntl();
|
||||
return (
|
||||
<Button
|
||||
id="edit-filters-btn"
|
||||
className="btn-primary align-self-start"
|
||||
onClick={toggleFilterMenu}
|
||||
>
|
||||
<Icon src={FilterAlt} /> {formatMessage(messages.editFilters)}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
FilterMenuToggle.propTypes = {};
|
||||
|
||||
export default FilterMenuToggle;
|
||||
47
src/components/GradesView/FilterMenuToggle/index.test.jsx
Normal file
47
src/components/GradesView/FilterMenuToggle/index.test.jsx
Normal file
@@ -0,0 +1,47 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { formatMessage } from 'testUtils';
|
||||
import { thunkActions } from 'data/redux/hooks';
|
||||
|
||||
import FilterMenuToggle from '.';
|
||||
import messages from './messages';
|
||||
|
||||
jest.mock('data/redux/hooks', () => ({
|
||||
thunkActions: {
|
||||
app: {
|
||||
filterMenu: {
|
||||
useToggleMenu: jest.fn(),
|
||||
},
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
const toggleFilterMenu = jest.fn().mockName('hooks.toggleFilterMenu');
|
||||
thunkActions.app.filterMenu.useToggleMenu.mockReturnValue(toggleFilterMenu);
|
||||
|
||||
let el;
|
||||
describe('FilterMenuToggle component', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
el = shallow(<FilterMenuToggle />);
|
||||
});
|
||||
describe('behavior', () => {
|
||||
it('initializes intl hook', () => {
|
||||
expect(useIntl).toHaveBeenCalled();
|
||||
});
|
||||
it('initializes redux hooks', () => {
|
||||
expect(thunkActions.app.filterMenu.useToggleMenu).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
describe('render', () => {
|
||||
test('snapshot', () => {
|
||||
expect(el).toMatchSnapshot();
|
||||
expect(el.type()).toEqual('Button');
|
||||
expect(el.props().onClick).toEqual(toggleFilterMenu);
|
||||
expect(el.text().includes(formatMessage(messages.editFilters)));
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,44 +0,0 @@
|
||||
/* eslint-disable react/sort-comp, react/button-has-type, import/no-named-as-default */
|
||||
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';
|
||||
|
||||
/**
|
||||
* <FilteredUsersLabel />
|
||||
* Simple label component displaying the filtered and total users shown
|
||||
*/
|
||||
export const FilteredUsersLabel = ({
|
||||
filteredUsersCount,
|
||||
totalUsersCount,
|
||||
}) => {
|
||||
if (!totalUsersCount) {
|
||||
return null;
|
||||
}
|
||||
const bold = (val) => (<span className="font-weight-bold">{val}</span>);
|
||||
return (
|
||||
<FormattedMessage
|
||||
id="gradebook.GradesTab.usersVisibilityLabel'"
|
||||
defaultMessage="Showing {filteredUsers} of {totalUsers} total learners"
|
||||
description="Users visibility label"
|
||||
values={{
|
||||
filteredUsers: bold(filteredUsersCount),
|
||||
totalUsers: bold(totalUsersCount),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
FilteredUsersLabel.propTypes = {
|
||||
filteredUsersCount: PropTypes.number.isRequired,
|
||||
totalUsersCount: PropTypes.number.isRequired,
|
||||
};
|
||||
|
||||
export const mapStateToProps = (state) => ({
|
||||
totalUsersCount: selectors.grades.totalUsersCount(state),
|
||||
filteredUsersCount: selectors.grades.filteredUsersCount(state),
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps)(FilteredUsersLabel);
|
||||
@@ -1,46 +0,0 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import selectors from 'data/selectors';
|
||||
import { FilteredUsersLabel, mapStateToProps } from './FilteredUsersLabel';
|
||||
|
||||
jest.mock('@edx/paragon', () => ({
|
||||
Icon: () => 'Icon',
|
||||
}));
|
||||
jest.mock('data/selectors', () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
grades: {
|
||||
filteredUsersCount: state => ({ filteredUsersCount: state }),
|
||||
totalUsersCount: state => ({ totalUsersCount: state }),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
describe('FilteredUsersLabel', () => {
|
||||
describe('component', () => {
|
||||
const props = {
|
||||
filteredUsersCount: 23,
|
||||
totalUsersCount: 140,
|
||||
};
|
||||
it('does not render if totalUsersCount is falsey', () => {
|
||||
expect(shallow(<FilteredUsersLabel {...props} totalUsersCount={0} />)).toEqual({});
|
||||
});
|
||||
test('snapshot - displays label with number of filtered users out of total', () => {
|
||||
expect(shallow(<FilteredUsersLabel {...props} />)).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
describe('mapStateToProps', () => {
|
||||
const testState = { a: 'nice', day: 'for', some: 'rain' };
|
||||
let mapped;
|
||||
beforeEach(() => {
|
||||
mapped = mapStateToProps(testState);
|
||||
});
|
||||
test('filteredUsersCount from grades.filteredUsersCount', () => {
|
||||
expect(mapped.filteredUsersCount).toEqual(selectors.grades.filteredUsersCount(testState));
|
||||
});
|
||||
test('totalUsersCount from grades.totalUsersCount', () => {
|
||||
expect(mapped.totalUsersCount).toEqual(selectors.grades.totalUsersCount(testState));
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,23 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`FilteredUsersLabel component render snapshot 1`] = `
|
||||
<format-message-function
|
||||
message={
|
||||
Object {
|
||||
"defaultMessage": "Showing {filteredUsers} of {totalUsers} total learners",
|
||||
"description": "Users visibility label",
|
||||
"id": "gradebook.GradesTab.usersVisibilityLabel",
|
||||
}
|
||||
}
|
||||
values={
|
||||
Object {
|
||||
"filteredUsers": <BoldText
|
||||
text={100}
|
||||
/>,
|
||||
"totalUsers": <BoldText
|
||||
text={123}
|
||||
/>,
|
||||
}
|
||||
}
|
||||
/>
|
||||
`;
|
||||
37
src/components/GradesView/FilteredUsersLabel/index.jsx
Normal file
37
src/components/GradesView/FilteredUsersLabel/index.jsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { selectors } from 'data/redux/hooks';
|
||||
import messages from './messages';
|
||||
|
||||
export const BoldText = ({ text }) => (
|
||||
<span className="font-weight-bold">{text}</span>
|
||||
);
|
||||
BoldText.propTypes = {
|
||||
text: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
||||
};
|
||||
|
||||
/**
|
||||
* <FilteredUsersLabel />
|
||||
* Simple label component displaying the filtered and total users shown
|
||||
*/
|
||||
export const FilteredUsersLabel = () => {
|
||||
const { filteredUsersCount, totalUsersCount } = selectors.grades.useUserCounts();
|
||||
const { formatMessage } = useIntl();
|
||||
|
||||
if (!totalUsersCount) {
|
||||
return null;
|
||||
}
|
||||
return formatMessage(
|
||||
messages.visibilityLabel,
|
||||
{
|
||||
filteredUsers: <BoldText text={filteredUsersCount} />,
|
||||
totalUsers: <BoldText text={totalUsersCount} />,
|
||||
},
|
||||
);
|
||||
};
|
||||
FilteredUsersLabel.propTypes = {};
|
||||
|
||||
export default FilteredUsersLabel;
|
||||
56
src/components/GradesView/FilteredUsersLabel/index.test.jsx
Normal file
56
src/components/GradesView/FilteredUsersLabel/index.test.jsx
Normal file
@@ -0,0 +1,56 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { formatMessage } from 'testUtils';
|
||||
import { selectors } from 'data/redux/hooks';
|
||||
|
||||
import FilteredUsersLabel, { BoldText } from '.';
|
||||
import messages from './messages';
|
||||
|
||||
jest.mock('data/redux/hooks', () => ({
|
||||
selectors: {
|
||||
grades: {
|
||||
useUserCounts: jest.fn(),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
const userCounts = {
|
||||
filteredUsersCount: 100,
|
||||
totalUsersCount: 123,
|
||||
};
|
||||
selectors.grades.useUserCounts.mockReturnValue(userCounts);
|
||||
|
||||
let el;
|
||||
describe('FilteredUsersLabel component', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
el = shallow(<FilteredUsersLabel />);
|
||||
});
|
||||
describe('behavior', () => {
|
||||
it('initializes intl hook', () => {
|
||||
expect(useIntl).toHaveBeenCalled();
|
||||
});
|
||||
it('initializes redux hooks', () => {
|
||||
expect(selectors.grades.useUserCounts).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
describe('render', () => {
|
||||
test('null render if totalUsersCount is 0', () => {
|
||||
selectors.grades.useUserCounts.mockReturnValueOnce({
|
||||
...userCounts,
|
||||
totalUsersCount: 0,
|
||||
});
|
||||
expect(shallow(<FilteredUsersLabel />).isEmptyRender()).toEqual(true);
|
||||
});
|
||||
test('snapshot', () => {
|
||||
expect(el).toMatchSnapshot();
|
||||
expect(el).toMatchObject(shallow(formatMessage(messages.visibilityLabel, {
|
||||
filteredUsers: <BoldText text={userCounts.filteredUsersCount} />,
|
||||
totalUsers: <BoldText text={userCounts.totalUsersCount} />,
|
||||
})));
|
||||
});
|
||||
});
|
||||
});
|
||||
11
src/components/GradesView/FilteredUsersLabel/messages.js
Normal file
11
src/components/GradesView/FilteredUsersLabel/messages.js
Normal file
@@ -0,0 +1,11 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
visibilityLabel: {
|
||||
id: 'gradebook.GradesTab.usersVisibilityLabel',
|
||||
defaultMessage: 'Showing {filteredUsers} of {totalUsers} total learners',
|
||||
description: 'Users visibility label',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
@@ -29,16 +29,16 @@ Username.propTypes = {
|
||||
};
|
||||
|
||||
/**
|
||||
* Fields.Email
|
||||
* Simple label field for email value.
|
||||
* @param {string} email - email for display
|
||||
* Fields.Text
|
||||
* Simple label field for text value.
|
||||
* @param {string} value - value for display
|
||||
*/
|
||||
const Email = ({ email }) => <span className="wrap-text-in-cell">{email}</span>;
|
||||
Email.propTypes = {
|
||||
email: PropTypes.string.isRequired,
|
||||
const Text = ({ value }) => (<span className="wrap-text-in-cell">{value}</span>);
|
||||
Text.propTypes = {
|
||||
value: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default StrictDict({
|
||||
Email,
|
||||
Text,
|
||||
Username,
|
||||
});
|
||||
|
||||
@@ -41,13 +41,13 @@ describe('Gradebook Table Fields', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('Email', () => {
|
||||
const email = 'myTag@place.com';
|
||||
describe('Text', () => {
|
||||
const value = 'myTag@place.com';
|
||||
test('snapshot', () => {
|
||||
expect(shallow(<Fields.Email email={email} />)).toMatchSnapshot();
|
||||
expect(shallow(<Fields.Text value={value} />)).toMatchSnapshot();
|
||||
});
|
||||
test('wraps entry email', () => {
|
||||
expect(shallow(<Fields.Email email={email} />).text()).toEqual(email);
|
||||
test('wraps entry value', () => {
|
||||
expect(shallow(<Fields.Text value={value} />).text()).toEqual(value);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,14 +1,31 @@
|
||||
/* eslint-disable react/sort-comp, react/button-has-type */
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { Button } from '@edx/paragon';
|
||||
|
||||
import selectors from 'data/selectors';
|
||||
import thunkActions from 'data/thunkActions';
|
||||
import { selectors, thunkActions } from 'data/redux/hooks';
|
||||
import transforms from 'data/redux/transforms';
|
||||
import * as module from './GradeButton';
|
||||
|
||||
const { subsectionGrade } = selectors.grades;
|
||||
export const useGradeButtonData = ({ entry, subsection }) => {
|
||||
const areGradesFrozen = selectors.assignmentTypes.useAreGradesFrozen();
|
||||
const { gradeFormat } = selectors.grades.useGradeData();
|
||||
const setModalState = thunkActions.app.useSetModalStateFromTable();
|
||||
const label = transforms.grades.subsectionGrade({ gradeFormat, subsection });
|
||||
|
||||
const onClick = () => {
|
||||
setModalState({
|
||||
userEntry: entry,
|
||||
subsection,
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
areGradesFrozen,
|
||||
label,
|
||||
onClick,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* GradeButton
|
||||
@@ -18,38 +35,24 @@ const { subsectionGrade } = selectors.grades;
|
||||
* @param {object} entry - user's grade entry
|
||||
* @param {object} subsection - user's subsection grade from subsection_breakdown
|
||||
*/
|
||||
export class GradeButton extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.onClick = this.onClick.bind(this);
|
||||
}
|
||||
|
||||
get label() {
|
||||
return subsectionGrade[this.props.format](this.props.subsection);
|
||||
}
|
||||
|
||||
onClick() {
|
||||
this.props.setModalState({
|
||||
userEntry: this.props.entry,
|
||||
subsection: this.props.subsection,
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return this.props.areGradesFrozen
|
||||
? this.label
|
||||
: (
|
||||
<Button
|
||||
variant="link"
|
||||
className="btn-header grade-button"
|
||||
onClick={this.onClick}
|
||||
>
|
||||
{this.label}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const GradeButton = ({ entry, subsection }) => {
|
||||
const {
|
||||
areGradesFrozen,
|
||||
label,
|
||||
onClick,
|
||||
} = module.useGradeButtonData({ entry, subsection });
|
||||
return areGradesFrozen
|
||||
? label
|
||||
: (
|
||||
<Button
|
||||
variant="link"
|
||||
className="btn-header grade-button"
|
||||
onClick={onClick}
|
||||
>
|
||||
{label}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
GradeButton.propTypes = {
|
||||
subsection: PropTypes.shape({
|
||||
attempted: PropTypes.bool,
|
||||
@@ -62,19 +65,6 @@ GradeButton.propTypes = {
|
||||
user_id: PropTypes.number,
|
||||
username: PropTypes.string,
|
||||
}).isRequired,
|
||||
// redux
|
||||
areGradesFrozen: PropTypes.bool.isRequired,
|
||||
format: PropTypes.string.isRequired,
|
||||
setModalState: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export const mapStateToProps = (state) => ({
|
||||
areGradesFrozen: selectors.assignmentTypes.areGradesFrozen(state),
|
||||
format: selectors.grades.gradeFormat(state),
|
||||
});
|
||||
|
||||
export const mapDispatchToProps = {
|
||||
setModalState: thunkActions.app.setModalStateFromTable,
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(GradeButton);
|
||||
export default GradeButton;
|
||||
|
||||
@@ -1,118 +1,121 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { Button } from '@edx/paragon';
|
||||
import selectors from 'data/selectors';
|
||||
import thunkActions from 'data/thunkActions';
|
||||
import { selectors, thunkActions } from 'data/redux/hooks';
|
||||
import transforms from 'data/redux/transforms';
|
||||
import { keyStore } from 'utils';
|
||||
|
||||
import {
|
||||
GradeButton,
|
||||
mapStateToProps,
|
||||
mapDispatchToProps,
|
||||
} from './GradeButton';
|
||||
import * as module from './GradeButton';
|
||||
|
||||
jest.mock('@edx/paragon', () => ({
|
||||
Button: () => 'Button',
|
||||
}));
|
||||
const { useGradeButtonData, default: GradeButton } = module;
|
||||
|
||||
jest.mock('data/selectors', () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
assignmentTypes: {
|
||||
areGradesFrozen: jest.fn(state => ({ areGradesFrozen: state })),
|
||||
},
|
||||
jest.mock('data/redux/hooks', () => ({
|
||||
selectors: {
|
||||
assignmentTypes: { useAreGradesFrozen: jest.fn() },
|
||||
grades: {
|
||||
subsectionGrade: {
|
||||
percent: jest.fn(subsection => ({ percent: subsection })),
|
||||
},
|
||||
gradeFormat: jest.fn(state => ({ gradeFormat: state })),
|
||||
useGradeData: jest.fn(),
|
||||
},
|
||||
},
|
||||
thunkActions: {
|
||||
app: { useSetModalStateFromTable: jest.fn() },
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('data/thunkActions', () => ({
|
||||
app: {
|
||||
setModalStateFromTable: jest.fn(),
|
||||
jest.mock('data/redux/transforms', () => ({
|
||||
grades: {
|
||||
subsectionGrade: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
const props = {
|
||||
subsection: {
|
||||
attempted: false,
|
||||
percent: 23,
|
||||
score_possible: 32,
|
||||
subsection_name: 'the things we do',
|
||||
module_id: 'in potions',
|
||||
},
|
||||
entry: {
|
||||
user_id: 2,
|
||||
username: 'Jessie',
|
||||
},
|
||||
};
|
||||
const gradeFormat = 'percent';
|
||||
const setModalState = jest.fn();
|
||||
const subsectionGrade = 'test-subsection-grade';
|
||||
selectors.assignmentTypes.useAreGradesFrozen.mockReturnValue(false);
|
||||
selectors.grades.useGradeData.mockReturnValue({ gradeFormat });
|
||||
thunkActions.app.useSetModalStateFromTable.mockReturnValue(setModalState);
|
||||
transforms.grades.subsectionGrade.mockReturnValue(subsectionGrade);
|
||||
|
||||
let el;
|
||||
let out;
|
||||
describe('GradeButton', () => {
|
||||
let el;
|
||||
let props = {
|
||||
subsection: {
|
||||
attempted: false,
|
||||
percent: 23,
|
||||
score_possible: 32,
|
||||
subsection_name: 'the things we do',
|
||||
module_id: 'in potions',
|
||||
},
|
||||
entry: {
|
||||
user_id: 2,
|
||||
username: 'Jessie',
|
||||
},
|
||||
areGradesFrozen: false,
|
||||
format: 'percent',
|
||||
};
|
||||
beforeEach(() => {
|
||||
props = { ...props, setModalState: jest.fn() };
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
describe('component', () => {
|
||||
describe('snapshots', () => {
|
||||
test('grades are frozen', () => {
|
||||
el = shallow(<GradeButton {...{ ...props, areGradesFrozen: true }} />);
|
||||
const label = 'why you gotta label people?';
|
||||
jest.spyOn(el.instance(), 'label', 'get').mockReturnValue(label);
|
||||
el.instance().onClick = jest.fn().mockName('this.onClick');
|
||||
expect(el.instance().render()).toMatchSnapshot();
|
||||
expect(el.instance().render()).toEqual(label);
|
||||
});
|
||||
test('grades are not frozen', () => {
|
||||
el = shallow(<GradeButton {...props} />);
|
||||
const label = 'why you gotta label people?';
|
||||
jest.spyOn(el.instance(), 'label', 'get').mockReturnValue(label);
|
||||
el.instance().onClick = jest.fn().mockName('this.onClick');
|
||||
expect(el.instance().render()).toMatchSnapshot();
|
||||
expect(el.instance().render().props.children).toEqual(label);
|
||||
expect(el.render().is(Button)).toEqual(true);
|
||||
describe('useGradeButton hook', () => {
|
||||
beforeEach(() => {
|
||||
out = useGradeButtonData(props);
|
||||
});
|
||||
describe('behavior', () => {
|
||||
it('initializes redux hooks', () => {
|
||||
expect(selectors.assignmentTypes.useAreGradesFrozen).toHaveBeenCalled();
|
||||
expect(selectors.grades.useGradeData).toHaveBeenCalled();
|
||||
expect(transforms.grades.subsectionGrade).toHaveBeenCalledWith({
|
||||
gradeFormat,
|
||||
subsection: props.subsection,
|
||||
});
|
||||
expect(thunkActions.app.useSetModalStateFromTable).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
describe('label', () => {
|
||||
it('calls the appropriate formatter with the subsection prop', () => {
|
||||
el = shallow(<GradeButton {...props} />);
|
||||
expect(
|
||||
el.instance().label,
|
||||
).toEqual(selectors.grades.subsectionGrade[props.format](props.subsection));
|
||||
describe('output', () => {
|
||||
test('forwards areGradesFrozen from redux hook', () => {
|
||||
expect(out.areGradesFrozen).toEqual(false);
|
||||
});
|
||||
});
|
||||
describe('onClick', () => {
|
||||
it('calls props.setModalState with userEntry and subsection', () => {
|
||||
el = shallow(<GradeButton {...props} />);
|
||||
el.instance().onClick();
|
||||
expect(props.setModalState).toHaveBeenCalledWith({
|
||||
test('label passed from subsection grade redux hook', () => {
|
||||
expect(out.label).toEqual(subsectionGrade);
|
||||
});
|
||||
test('onClick sets modal state with user entry and subsection', () => {
|
||||
out.onClick();
|
||||
expect(setModalState).toHaveBeenCalledWith({
|
||||
userEntry: props.entry,
|
||||
subsection: props.subsection,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('mapStateToProps', () => {
|
||||
let mapped;
|
||||
const testState = { teams: { rocket: ['jesse', 'james'] } };
|
||||
describe('component', () => {
|
||||
let hookSpy;
|
||||
const moduleKeys = keyStore(module);
|
||||
const hookProps = {
|
||||
areGradesFrozen: false,
|
||||
label: 'test-label',
|
||||
onClick: jest.fn().mockName('hooks.onClick'),
|
||||
};
|
||||
beforeEach(() => {
|
||||
mapped = mapStateToProps(testState);
|
||||
hookSpy = jest.spyOn(module, moduleKeys.useGradeButtonData);
|
||||
});
|
||||
test('areGradesFrozen form assignmentTypes.areGradesFrozen', () => {
|
||||
expect(
|
||||
mapped.areGradesFrozen,
|
||||
).toEqual(selectors.assignmentTypes.areGradesFrozen(testState));
|
||||
describe('frozen grades', () => {
|
||||
beforeEach(() => {
|
||||
hookSpy.mockReturnValue({ ...hookProps, areGradesFrozen: true });
|
||||
el = shallow(<GradeButton {...props} />);
|
||||
});
|
||||
test('snapshot', () => {
|
||||
expect(el).toMatchSnapshot();
|
||||
expect(el.text()).toEqual(hookProps.label);
|
||||
});
|
||||
});
|
||||
test('format form grades.format', () => {
|
||||
expect(mapped.format).toEqual(selectors.grades.gradeFormat(testState));
|
||||
});
|
||||
});
|
||||
describe('mapDispatchToProps', () => {
|
||||
test('setModalState from thunkActions.app.setModalStateFromTable', () => {
|
||||
expect(mapDispatchToProps.setModalState).toEqual(thunkActions.app.setModalStateFromTable);
|
||||
describe('not frozen grades', () => {
|
||||
beforeEach(() => {
|
||||
hookSpy.mockReturnValue(hookProps);
|
||||
el = shallow(<GradeButton {...props} />);
|
||||
});
|
||||
test('snapshot', () => {
|
||||
expect(el).toMatchSnapshot();
|
||||
expect(el.type()).toEqual('Button');
|
||||
expect(el.props().onClick).toEqual(hookProps.onClick);
|
||||
expect(el.contains(hookProps.label)).toEqual(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import React from 'react';
|
||||
|
||||
import { StrictDict } from 'utils';
|
||||
|
||||
import { useIntl, getLocale, isRtl } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
Icon,
|
||||
OverlayTrigger,
|
||||
Tooltip,
|
||||
} from '@edx/paragon';
|
||||
import { FormattedMessage, getLocale, isRtl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { StrictDict } from 'utils';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
@@ -18,49 +18,76 @@ export const totalGradePercentageMessage = 'Total Grade values are always displa
|
||||
* Total Grade column header.
|
||||
* displays an overlay tooltip with screen-reader text to indicate total grade percentage
|
||||
*/
|
||||
const TotalGradeLabelReplacement = () => (
|
||||
<div>
|
||||
<OverlayTrigger
|
||||
trigger={['hover', 'focus']}
|
||||
key="left-basic"
|
||||
placement={isRtl(getLocale()) ? 'right' : 'left'}
|
||||
overlay={(
|
||||
<Tooltip id="course-grade-tooltip">
|
||||
<FormattedMessage {...messages.totalGradePercentage} />
|
||||
</Tooltip>
|
||||
)}
|
||||
>
|
||||
<div>
|
||||
<FormattedMessage {...messages.totalGradeHeading} />
|
||||
<div id="courseGradeTooltipIcon">
|
||||
<Icon
|
||||
className="fa fa-info-circle"
|
||||
screenReaderText={(
|
||||
<FormattedMessage {...messages.totalGradePercentage} />
|
||||
)}
|
||||
/>
|
||||
const TotalGradeLabelReplacement = () => {
|
||||
const { formatMessage } = useIntl();
|
||||
return (
|
||||
<div>
|
||||
<OverlayTrigger
|
||||
trigger={['hover', 'focus']}
|
||||
key="left-basic"
|
||||
placement={isRtl(getLocale()) ? 'right' : 'left'}
|
||||
overlay={(
|
||||
<Tooltip id="course-grade-tooltip">
|
||||
{formatMessage(messages.totalGradePercentage)}
|
||||
</Tooltip>
|
||||
)}
|
||||
>
|
||||
<div>
|
||||
{formatMessage(messages.totalGradeHeading)}
|
||||
<div id="courseGradeTooltipIcon">
|
||||
<Icon
|
||||
className="fa fa-info-circle"
|
||||
screenReaderText={formatMessage(messages.totalGradePercentage)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</OverlayTrigger>
|
||||
</div>
|
||||
</OverlayTrigger>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Asterisk to display next to heading labels that are only used for masters students
|
||||
*/
|
||||
const mastersOnlyFieldAsterisk = (
|
||||
<span className="font-weight-normal">*</span>
|
||||
);
|
||||
|
||||
/**
|
||||
* <UsernameLabelReplacement />
|
||||
* Username column header. Lists that Student Key is possibly available
|
||||
*/
|
||||
const UsernameLabelReplacement = () => (
|
||||
<div>
|
||||
const UsernameLabelReplacement = () => {
|
||||
const { formatMessage } = useIntl();
|
||||
return (
|
||||
<div>
|
||||
<FormattedMessage {...messages.usernameHeading} />
|
||||
<div>
|
||||
{formatMessage(messages.usernameHeading)}
|
||||
</div>
|
||||
<div className="font-weight-normal student-key">
|
||||
{formatMessage(messages.studentKeyLabel)}
|
||||
{ mastersOnlyFieldAsterisk }
|
||||
</div>
|
||||
</div>
|
||||
<div className="font-weight-normal student-key">
|
||||
<FormattedMessage {...messages.studentKeyLabel} />
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* <MastersOnlyLabelReplacement {message}>
|
||||
* Column header for fields that are only available for masters students
|
||||
*/
|
||||
const MastersOnlyLabelReplacement = (message) => {
|
||||
const { formatMessage } = useIntl();
|
||||
return (
|
||||
<div>
|
||||
{formatMessage(message)}
|
||||
{ mastersOnlyFieldAsterisk }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
);
|
||||
};
|
||||
|
||||
export default StrictDict({
|
||||
TotalGradeLabelReplacement,
|
||||
UsernameLabelReplacement,
|
||||
MastersOnlyLabelReplacement,
|
||||
});
|
||||
|
||||
@@ -9,6 +9,7 @@ import LabelReplacements from './LabelReplacements';
|
||||
const {
|
||||
TotalGradeLabelReplacement,
|
||||
UsernameLabelReplacement,
|
||||
MastersOnlyLabelReplacement,
|
||||
} = LabelReplacements;
|
||||
|
||||
jest.mock('@edx/paragon', () => ({
|
||||
@@ -35,6 +36,16 @@ describe('LabelReplacements', () => {
|
||||
expect(shallow(<UsernameLabelReplacement />)).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
describe('MastersOnlyLabelReplacement', () => {
|
||||
test('snapshot', () => {
|
||||
const message = {
|
||||
id: 'id',
|
||||
defaultMessage: 'defaultMessAge',
|
||||
description: 'desCripTion',
|
||||
};
|
||||
expect(shallow(<MastersOnlyLabelReplacement {...message} />)).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('snapshot', () => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Gradebook Table Fields Email snapshot 1`] = `
|
||||
exports[`Gradebook Table Fields Text snapshot 1`] = `
|
||||
<span
|
||||
className="wrap-text-in-cell"
|
||||
>
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`GradeButton component snapshots grades are frozen 1`] = `"why you gotta label people?"`;
|
||||
exports[`GradeButton component frozen grades snapshot 1`] = `"test-label"`;
|
||||
|
||||
exports[`GradeButton component snapshots grades are not frozen 1`] = `
|
||||
exports[`GradeButton component not frozen grades snapshot 1`] = `
|
||||
<Button
|
||||
className="btn-header grade-button"
|
||||
onClick={[MockFunction this.onClick]}
|
||||
onClick={[MockFunction hooks.onClick]}
|
||||
variant="link"
|
||||
>
|
||||
why you gotta label people?
|
||||
test-label
|
||||
</Button>
|
||||
`;
|
||||
|
||||
@@ -1,14 +1,21 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`LabelReplacements MastersOnlyLabelReplacement snapshot 1`] = `
|
||||
<div>
|
||||
defaultMessAge
|
||||
<span
|
||||
className="font-weight-normal"
|
||||
>
|
||||
*
|
||||
</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`LabelReplacements TotalGradeLabelReplacement displays overlay tooltip 1`] = `
|
||||
<Tooltip
|
||||
id="course-grade-tooltip"
|
||||
>
|
||||
<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.GradesView.table.totalGradePercentage"
|
||||
/>
|
||||
Total Grade values are always displayed as a percentage
|
||||
</Tooltip>
|
||||
`;
|
||||
|
||||
@@ -20,11 +27,7 @@ exports[`LabelReplacements TotalGradeLabelReplacement snapshot 1`] = `
|
||||
<Tooltip
|
||||
id="course-grade-tooltip"
|
||||
>
|
||||
<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.GradesView.table.totalGradePercentage"
|
||||
/>
|
||||
Total Grade values are always displayed as a percentage
|
||||
</Tooltip>
|
||||
}
|
||||
placement="left"
|
||||
@@ -36,23 +39,13 @@ exports[`LabelReplacements TotalGradeLabelReplacement snapshot 1`] = `
|
||||
}
|
||||
>
|
||||
<div>
|
||||
<FormattedMessage
|
||||
defaultMessage="Total Grade (%)"
|
||||
description="Gradebook table total grade column header"
|
||||
id="gradebook.GradesView.table.headings.totalGrade"
|
||||
/>
|
||||
Total Grade (%)
|
||||
<div
|
||||
id="courseGradeTooltipIcon"
|
||||
>
|
||||
<Icon
|
||||
className="fa fa-info-circle"
|
||||
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.GradesView.table.totalGradePercentage"
|
||||
/>
|
||||
}
|
||||
screenReaderText="Total Grade values are always displayed as a percentage"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -63,20 +56,17 @@ exports[`LabelReplacements TotalGradeLabelReplacement snapshot 1`] = `
|
||||
exports[`LabelReplacements UsernameLabelReplacement snapshot 1`] = `
|
||||
<div>
|
||||
<div>
|
||||
<FormattedMessage
|
||||
defaultMessage="Username"
|
||||
description="Gradebook table username column header"
|
||||
id="gradebook.GradesView.table.headings.username"
|
||||
/>
|
||||
Username
|
||||
</div>
|
||||
<div
|
||||
className="font-weight-normal student-key"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Student Key*"
|
||||
description="Gradebook table Student Key label"
|
||||
id="gradebook.GradesView.table.labels.studentKey"
|
||||
/>
|
||||
Student Key
|
||||
<span
|
||||
className="font-weight-normal"
|
||||
>
|
||||
*
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -89,11 +79,7 @@ exports[`snapshot left to right overlay placement 1`] = `
|
||||
<Tooltip
|
||||
id="course-grade-tooltip"
|
||||
>
|
||||
<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.GradesView.table.totalGradePercentage"
|
||||
/>
|
||||
Total Grade values are always displayed as a percentage
|
||||
</Tooltip>
|
||||
}
|
||||
placement="right"
|
||||
@@ -105,23 +91,13 @@ exports[`snapshot left to right overlay placement 1`] = `
|
||||
}
|
||||
>
|
||||
<div>
|
||||
<FormattedMessage
|
||||
defaultMessage="Total Grade (%)"
|
||||
description="Gradebook table total grade column header"
|
||||
id="gradebook.GradesView.table.headings.totalGrade"
|
||||
/>
|
||||
Total Grade (%)
|
||||
<div
|
||||
id="courseGradeTooltipIcon"
|
||||
>
|
||||
<Icon
|
||||
className="fa fa-info-circle"
|
||||
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.GradesView.table.totalGradePercentage"
|
||||
/>
|
||||
}
|
||||
screenReaderText="Total Grade values are always displayed as a percentage"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -137,11 +113,7 @@ exports[`snapshot right to left overlay placement 1`] = `
|
||||
<Tooltip
|
||||
id="course-grade-tooltip"
|
||||
>
|
||||
<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.GradesView.table.totalGradePercentage"
|
||||
/>
|
||||
Total Grade values are always displayed as a percentage
|
||||
</Tooltip>
|
||||
}
|
||||
placement="left"
|
||||
@@ -153,23 +125,13 @@ exports[`snapshot right to left overlay placement 1`] = `
|
||||
}
|
||||
>
|
||||
<div>
|
||||
<FormattedMessage
|
||||
defaultMessage="Total Grade (%)"
|
||||
description="Gradebook table total grade column header"
|
||||
id="gradebook.GradesView.table.headings.totalGrade"
|
||||
/>
|
||||
Total Grade (%)
|
||||
<div
|
||||
id="courseGradeTooltipIcon"
|
||||
>
|
||||
<Icon
|
||||
className="fa fa-info-circle"
|
||||
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.GradesView.table.totalGradePercentage"
|
||||
/>
|
||||
}
|
||||
screenReaderText="Total Grade values are always displayed as a percentage"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`GradebookTable snapshot 1`] = `
|
||||
<div
|
||||
className="gradebook-container"
|
||||
>
|
||||
<DataTable
|
||||
RowStatusComponent={[MockFunction hooks.nullMethod]}
|
||||
columns={
|
||||
Array [
|
||||
"some",
|
||||
"columns",
|
||||
]
|
||||
}
|
||||
data={
|
||||
Array [
|
||||
"some",
|
||||
"data",
|
||||
]
|
||||
}
|
||||
hasFixedColumnWidths={true}
|
||||
itemCount={3}
|
||||
rowHeaderColumnKey="username"
|
||||
>
|
||||
<DataTable.TableControlBar />
|
||||
<DataTable.Table />
|
||||
<DataTable.EmptyTable
|
||||
content="empty-table-content"
|
||||
/>
|
||||
</DataTable>
|
||||
</div>
|
||||
`;
|
||||
@@ -1,55 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`GradebookTable component snapshot - fields1 and 2 between email and totalGrade, mocked rows 1`] = `
|
||||
<div
|
||||
className="gradebook-container"
|
||||
>
|
||||
<DataTable
|
||||
RowStatusComponent={[MockFunction this.nullMethod]}
|
||||
columns={
|
||||
Array [
|
||||
Object {
|
||||
"Header": <UsernameLabelReplacement />,
|
||||
"accessor": "Username",
|
||||
},
|
||||
Object {
|
||||
"Header": <FormattedMessage
|
||||
defaultMessage="Email*"
|
||||
description="Gradebook table email column header"
|
||||
id="gradebook.GradesView.table.headings.email"
|
||||
/>,
|
||||
"accessor": "Email",
|
||||
},
|
||||
Object {
|
||||
"Header": "field1",
|
||||
"accessor": "field1",
|
||||
},
|
||||
Object {
|
||||
"Header": "field2",
|
||||
"accessor": "field2",
|
||||
},
|
||||
Object {
|
||||
"Header": <TotalGradeLabelReplacement />,
|
||||
"accessor": "Total Grade (%)",
|
||||
},
|
||||
]
|
||||
}
|
||||
data={
|
||||
Array [
|
||||
"mappedRow: 1",
|
||||
"mappedRow: 2",
|
||||
"mappedRow: 3",
|
||||
]
|
||||
}
|
||||
hasFixedColumnWidths={true}
|
||||
itemCount={3}
|
||||
rowHeaderColumnKey="username"
|
||||
>
|
||||
<DataTable.TableControlBar />
|
||||
<DataTable.Table />
|
||||
<DataTable.EmptyTable
|
||||
content="No results found"
|
||||
/>
|
||||
</DataTable>
|
||||
</div>
|
||||
`;
|
||||
61
src/components/GradesView/GradebookTable/hooks.jsx
Normal file
61
src/components/GradesView/GradebookTable/hooks.jsx
Normal file
@@ -0,0 +1,61 @@
|
||||
import React from 'react';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { selectors } from 'data/redux/hooks';
|
||||
import transforms from 'data/redux/transforms';
|
||||
import { Headings } from 'data/constants/grades';
|
||||
import { getLocalizedPercentSign } from 'i18n/utils';
|
||||
|
||||
import messages from './messages';
|
||||
import Fields from './Fields';
|
||||
import LabelReplacements from './LabelReplacements';
|
||||
import GradeButton from './GradeButton';
|
||||
|
||||
const { roundGrade } = transforms.grades;
|
||||
|
||||
export const useGradebookTableData = () => {
|
||||
const { formatMessage } = useIntl();
|
||||
const grades = selectors.grades.useAllGrades();
|
||||
const headings = selectors.root.useGetHeadings();
|
||||
|
||||
const mapHeaders = (heading) => {
|
||||
let label;
|
||||
if (heading === Headings.totalGrade) {
|
||||
label = <LabelReplacements.TotalGradeLabelReplacement />;
|
||||
} else if (heading === Headings.username) {
|
||||
label = <LabelReplacements.UsernameLabelReplacement />;
|
||||
} else if (heading === Headings.email) {
|
||||
label = <LabelReplacements.MastersOnlyLabelReplacement {...messages.emailHeading} />;
|
||||
} else if (heading === Headings.fullName) {
|
||||
label = <LabelReplacements.MastersOnlyLabelReplacement {...messages.fullNameHeading} />;
|
||||
} else {
|
||||
label = heading;
|
||||
}
|
||||
return { Header: label, accessor: heading };
|
||||
};
|
||||
|
||||
const mapRows = entry => ({
|
||||
[Headings.username]: (
|
||||
<Fields.Username username={entry.username} userKey={entry.external_user_key} />
|
||||
),
|
||||
[Headings.email]: (<Fields.Email email={entry.email} />),
|
||||
[Headings.totalGrade]: `${roundGrade(entry.percent * 100)}${getLocalizedPercentSign()}`,
|
||||
...entry.section_breakdown.reduce((acc, subsection) => ({
|
||||
...acc,
|
||||
[subsection.label]: <GradeButton {...{ entry, subsection }} />,
|
||||
}), {}),
|
||||
});
|
||||
|
||||
const nullMethod = () => null;
|
||||
|
||||
return {
|
||||
columns: headings.map(mapHeaders),
|
||||
data: grades.map(mapRows),
|
||||
grades,
|
||||
nullMethod,
|
||||
emptyContent: formatMessage(messages.noResultsFound),
|
||||
};
|
||||
};
|
||||
|
||||
export default useGradebookTableData;
|
||||
192
src/components/GradesView/GradebookTable/hooks.test.jsx
Normal file
192
src/components/GradesView/GradebookTable/hooks.test.jsx
Normal file
@@ -0,0 +1,192 @@
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { formatMessage } from 'testUtils';
|
||||
|
||||
import { getLocalizedPercentSign } from 'i18n/utils';
|
||||
import { selectors } from 'data/redux/hooks';
|
||||
import transforms from 'data/redux/transforms';
|
||||
import { Headings } from 'data/constants/grades';
|
||||
import LabelReplacements from './LabelReplacements';
|
||||
import Fields from './Fields';
|
||||
import GradeButton from './GradeButton';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
import useGradebookTableData from './hooks';
|
||||
|
||||
jest.mock('i18n/utils', () => ({
|
||||
getLocalizedPercentSign: () => '%',
|
||||
}));
|
||||
jest.mock('./GradeButton', () => 'GradeButton');
|
||||
jest.mock('./Fields', () => jest.requireActual('testUtils').mockNestedComponents({
|
||||
Username: 'Fields.Username',
|
||||
Email: 'Fields.Email',
|
||||
}));
|
||||
jest.mock('./LabelReplacements', () => jest.requireActual('testUtils').mockNestedComponents({
|
||||
TotalGradeLabelReplacement: 'LabelReplacements.TotalGradeLabelReplacement',
|
||||
UsernameLabelReplacement: 'LabelReplacements.UsernameLabelReplacement',
|
||||
MastersOnlyLabelReplacement: 'LabelReplacements.MastersOnlyLabelReplacement',
|
||||
}));
|
||||
|
||||
jest.mock('data/redux/hooks', () => ({
|
||||
selectors: {
|
||||
grades: { useAllGrades: jest.fn() },
|
||||
root: { useGetHeadings: jest.fn() },
|
||||
},
|
||||
}));
|
||||
jest.mock('data/redux/transforms', () => ({
|
||||
grades: { roundGrade: jest.fn() },
|
||||
}));
|
||||
|
||||
const roundGrade = grade => grade * 20;
|
||||
transforms.grades.roundGrade.mockImplementation(roundGrade);
|
||||
|
||||
const subsectionLabels = [
|
||||
'subsectionLabel1',
|
||||
'subsectionLabel2',
|
||||
'subsectionLabel3',
|
||||
];
|
||||
|
||||
const allGrades = [
|
||||
{
|
||||
username: 'test-username-1',
|
||||
external_user_key: 'EKey1',
|
||||
email: 'email-1',
|
||||
fullName: 'test-fullNAME',
|
||||
percent: 0.9,
|
||||
section_breakdown: [
|
||||
{ label: subsectionLabels[0] },
|
||||
{ label: subsectionLabels[1] },
|
||||
{ label: subsectionLabels[2] },
|
||||
],
|
||||
},
|
||||
{
|
||||
username: 'test-username-2',
|
||||
external_user_key: 'EKey2',
|
||||
email: 'email-2',
|
||||
percent: 0.8,
|
||||
section_breakdown: [
|
||||
{ label: subsectionLabels[0] },
|
||||
{ label: subsectionLabels[1] },
|
||||
{ label: subsectionLabels[2] },
|
||||
],
|
||||
},
|
||||
{
|
||||
username: 'test-username-3',
|
||||
external_user_key: 'EKey3',
|
||||
email: 'email-3',
|
||||
percent: 0.6,
|
||||
section_breakdown: [
|
||||
{ label: subsectionLabels[0] },
|
||||
{ label: subsectionLabels[1] },
|
||||
{ label: subsectionLabels[2] },
|
||||
],
|
||||
},
|
||||
];
|
||||
const testHeading = 'test-heading-value';
|
||||
const headings = [
|
||||
Headings.totalGrade,
|
||||
Headings.username,
|
||||
Headings.email,
|
||||
Headings.fullName,
|
||||
testHeading,
|
||||
];
|
||||
selectors.grades.useAllGrades.mockReturnValue(allGrades);
|
||||
selectors.root.useGetHeadings.mockReturnValue(headings);
|
||||
|
||||
let out;
|
||||
describe('useGradebookTableData', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
out = useGradebookTableData();
|
||||
});
|
||||
describe('behavior', () => {
|
||||
it('initializes intl hook', () => {
|
||||
expect(useIntl).toHaveBeenCalled();
|
||||
});
|
||||
it('initializes redux hooks', () => {
|
||||
expect(selectors.grades.useAllGrades).toHaveBeenCalled();
|
||||
expect(selectors.root.useGetHeadings).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
describe('output', () => {
|
||||
describe('columns', () => {
|
||||
test('total grade heading produces TotalGradeLabelReplacement label', () => {
|
||||
const { Header, accessor } = out.columns[0];
|
||||
expect(accessor).toEqual(headings[0]);
|
||||
expect(shallow(Header)).toMatchObject(
|
||||
shallow(<LabelReplacements.TotalGradeLabelReplacement />),
|
||||
);
|
||||
});
|
||||
test('username heading produces UsernameLabelReplacement', () => {
|
||||
const { Header, accessor } = out.columns[1];
|
||||
expect(accessor).toEqual(headings[1]);
|
||||
expect(shallow(Header)).toMatchObject(
|
||||
shallow(<LabelReplacements.UsernameLabelReplacement />),
|
||||
);
|
||||
});
|
||||
test('email heading replaces with email heading message', () => {
|
||||
const { Header, accessor } = out.columns[2];
|
||||
expect(accessor).toEqual(headings[2]);
|
||||
expect(shallow(Header)).toMatchObject(
|
||||
shallow(<LabelReplacements.MastersOnlyLabelReplacement {...messages.emailHeading} />),
|
||||
);
|
||||
});
|
||||
test('fullName heading replaces with fullName heading message', () => {
|
||||
const { Header, accessor } = out.columns[3];
|
||||
expect(accessor).toEqual(headings[3]);
|
||||
expect(shallow(Header)).toMatchObject(
|
||||
shallow(<LabelReplacements.MastersOnlyLabelReplacement {...messages.fullNameHeading} />),
|
||||
);
|
||||
});
|
||||
test('other headings are passed through', () => {
|
||||
const { Header, accessor } = out.columns[4];
|
||||
expect(accessor).toEqual(headings[4]);
|
||||
expect(Header).toEqual(headings[4]);
|
||||
});
|
||||
});
|
||||
describe('data', () => {
|
||||
test('username field', () => {
|
||||
allGrades.forEach((entry, index) => {
|
||||
expect(out.data[index][Headings.username]).toMatchObject(
|
||||
<Fields.Username username={entry.username} userKey={entry.external_user_key} />,
|
||||
);
|
||||
});
|
||||
});
|
||||
test('email field', () => {
|
||||
allGrades.forEach((entry, index) => {
|
||||
expect(out.data[index][Headings.email]).toMatchObject(
|
||||
<Fields.Email email={entry.email} />,
|
||||
);
|
||||
});
|
||||
});
|
||||
test('totalGrade field', () => {
|
||||
allGrades.forEach((entry, index) => {
|
||||
expect(out.data[index][Headings.totalGrade]).toEqual(
|
||||
`${roundGrade(entry.percent * 100)}${getLocalizedPercentSign()}`,
|
||||
);
|
||||
});
|
||||
});
|
||||
test('section breakdown', () => {
|
||||
allGrades.forEach((entry, gradeIndex) => {
|
||||
subsectionLabels.forEach((label, labelIndex) => {
|
||||
expect(out.data[gradeIndex][label]).toMatchObject(
|
||||
<GradeButton entry={entry} subsection={entry.section_breakdown[labelIndex]} />,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
it('forwards grades from redux', () => {
|
||||
expect(out.grades).toEqual(allGrades);
|
||||
});
|
||||
test('nullMethod returns null', () => {
|
||||
expect(out.nullMethod()).toEqual(null);
|
||||
});
|
||||
test('emptyContent', () => {
|
||||
expect(out.emptyContent).toEqual(formatMessage(messages.noResultsFound));
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,21 +1,8 @@
|
||||
/* eslint-disable react/sort-comp, react/button-has-type, import/no-named-as-default */
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { DataTable } from '@edx/paragon';
|
||||
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import selectors from 'data/selectors';
|
||||
import { Headings } from 'data/constants/grades';
|
||||
import { getLocalizedPercentSign } from 'i18n';
|
||||
|
||||
import messages from './messages';
|
||||
import Fields from './Fields';
|
||||
import LabelReplacements from './LabelReplacements';
|
||||
import GradeButton from './GradeButton';
|
||||
|
||||
const { roundGrade } = selectors.grades;
|
||||
import useGradebookTableData from './hooks';
|
||||
|
||||
/**
|
||||
* <GraebookTable />
|
||||
@@ -23,93 +10,33 @@ const { roundGrade } = selectors.grades;
|
||||
* a row for each user, with a column for their username, email, and total grade,
|
||||
* along with one for each subsection in their grade entry.
|
||||
*/
|
||||
export class GradebookTable extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.mapHeaders = this.mapHeaders.bind(this);
|
||||
this.mapRows = this.mapRows.bind(this);
|
||||
this.nullMethod = this.nullMethod.bind(this);
|
||||
}
|
||||
export const GradebookTable = () => {
|
||||
const {
|
||||
columns,
|
||||
data,
|
||||
grades,
|
||||
nullMethod,
|
||||
emptyContent,
|
||||
} = useGradebookTableData();
|
||||
|
||||
mapHeaders(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 { Header: label, accessor: heading };
|
||||
}
|
||||
|
||||
mapRows = entry => ({
|
||||
[Headings.username]: (
|
||||
<Fields.Username username={entry.username} userKey={entry.external_user_key} />
|
||||
),
|
||||
[Headings.email]: (<Fields.Email email={entry.email} />),
|
||||
[Headings.totalGrade]: `${roundGrade(entry.percent * 100)}${getLocalizedPercentSign()}`,
|
||||
...entry.section_breakdown.reduce((acc, subsection) => ({
|
||||
...acc,
|
||||
[subsection.label]: <GradeButton {...{ entry, subsection }} />,
|
||||
}), {}),
|
||||
});
|
||||
|
||||
nullMethod() {
|
||||
return null;
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="gradebook-container">
|
||||
<DataTable
|
||||
columns={this.props.headings.map(this.mapHeaders)}
|
||||
data={this.props.grades.map(this.mapRows)}
|
||||
rowHeaderColumnKey="username"
|
||||
hasFixedColumnWidths
|
||||
itemCount={this.props.grades.length}
|
||||
RowStatusComponent={this.nullMethod}
|
||||
>
|
||||
<DataTable.TableControlBar />
|
||||
<DataTable.Table />
|
||||
<DataTable.EmptyTable content={this.props.intl.formatMessage(messages.noResultsFound)} />
|
||||
</DataTable>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
GradebookTable.defaultProps = {
|
||||
grades: [],
|
||||
return (
|
||||
<div className="gradebook-container">
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={data}
|
||||
rowHeaderColumnKey="username"
|
||||
hasFixedColumnWidths
|
||||
itemCount={grades.length}
|
||||
RowStatusComponent={nullMethod}
|
||||
>
|
||||
<DataTable.TableControlBar />
|
||||
<DataTable.Table />
|
||||
<DataTable.EmptyTable content={emptyContent} />
|
||||
</DataTable>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
GradebookTable.propTypes = {
|
||||
// redux
|
||||
grades: PropTypes.arrayOf(PropTypes.shape({
|
||||
percent: PropTypes.number,
|
||||
section_breakdown: PropTypes.arrayOf(PropTypes.shape({
|
||||
attempted: PropTypes.bool,
|
||||
category: PropTypes.string,
|
||||
label: PropTypes.string,
|
||||
module_id: PropTypes.string,
|
||||
percent: PropTypes.number,
|
||||
scoreEarned: PropTypes.number,
|
||||
scorePossible: PropTypes.number,
|
||||
subsection_name: PropTypes.string,
|
||||
})),
|
||||
user_id: PropTypes.number,
|
||||
user_name: PropTypes.string,
|
||||
})),
|
||||
headings: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
// injected
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
GradebookTable.propTypes = {};
|
||||
|
||||
export const mapStateToProps = (state) => ({
|
||||
grades: selectors.grades.allGrades(state),
|
||||
headings: selectors.root.getHeadings(state),
|
||||
});
|
||||
|
||||
export default injectIntl(connect(mapStateToProps)(GradebookTable));
|
||||
export default GradebookTable;
|
||||
|
||||
39
src/components/GradesView/GradebookTable/index.test.jsx
Normal file
39
src/components/GradesView/GradebookTable/index.test.jsx
Normal file
@@ -0,0 +1,39 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { DataTable } from '@edx/paragon';
|
||||
|
||||
import useGradebookTableData from './hooks';
|
||||
import GradebookTable from '.';
|
||||
|
||||
jest.mock('./hooks', () => jest.fn());
|
||||
|
||||
const hookProps = {
|
||||
columns: ['some', 'columns'],
|
||||
data: ['some', 'data'],
|
||||
grades: ['a', 'few', 'grades'],
|
||||
nullMethod: jest.fn().mockName('hooks.nullMethod'),
|
||||
emptyContent: 'empty-table-content',
|
||||
};
|
||||
useGradebookTableData.mockReturnValue(hookProps);
|
||||
|
||||
let el;
|
||||
describe('GradebookTable', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
el = shallow(<GradebookTable />);
|
||||
});
|
||||
test('snapshot', () => {
|
||||
expect(el).toMatchSnapshot();
|
||||
});
|
||||
test('Datatable props', () => {
|
||||
const datatable = el.find(DataTable);
|
||||
const props = datatable.props();
|
||||
expect(props.columns).toEqual(hookProps.columns);
|
||||
expect(props.data).toEqual(hookProps.data);
|
||||
expect(props.itemCount).toEqual(hookProps.grades.length);
|
||||
expect(props.RowStatusComponent).toEqual(hookProps.nullMethod);
|
||||
expect(datatable.children().at(2).type()).toEqual('DataTable.EmptyTable');
|
||||
expect(datatable.children().at(2).props().content).toEqual(hookProps.emptyContent);
|
||||
});
|
||||
});
|
||||
@@ -1,9 +1,14 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
fullNameHeading: {
|
||||
id: 'gradebook.GradesView.table.headings.fullName',
|
||||
defaultMessage: 'Full Name',
|
||||
description: 'Gradebook table full name column header',
|
||||
},
|
||||
emailHeading: {
|
||||
id: 'gradebook.GradesView.table.headings.email',
|
||||
defaultMessage: 'Email*',
|
||||
defaultMessage: 'Email',
|
||||
description: 'Gradebook table email column header',
|
||||
},
|
||||
totalGradeHeading: {
|
||||
@@ -18,7 +23,7 @@ const messages = defineMessages({
|
||||
},
|
||||
studentKeyLabel: {
|
||||
id: 'gradebook.GradesView.table.labels.studentKey',
|
||||
defaultMessage: 'Student Key*',
|
||||
defaultMessage: 'Student Key',
|
||||
description: 'Gradebook table Student Key label',
|
||||
},
|
||||
usernameLabel: {
|
||||
|
||||
@@ -1,175 +0,0 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { DataTable } 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', () => jest.requireActual('testUtils').mockNestedComponents({
|
||||
DataTable: {
|
||||
Table: 'DataTable.Table',
|
||||
TableControlBar: 'DataTable.TableControlBar',
|
||||
EmptyTable: 'DataTable.EmptyTable',
|
||||
},
|
||||
}));
|
||||
jest.mock('./Fields', () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
Username: () => 'Fields.Username',
|
||||
Email: () => 'Fields.Email',
|
||||
},
|
||||
}));
|
||||
jest.mock('./LabelReplacements', () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
TotalGradeLabelReplacement: () => 'TotalGradeLabelReplacement',
|
||||
UsernameLabelReplacement: () => 'UsernameLabelReplacement',
|
||||
},
|
||||
}));
|
||||
jest.mock('./GradeButton', () => 'GradeButton');
|
||||
jest.mock('data/selectors', () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
grades: {
|
||||
roundGrade: jest.fn(grade => `roundedGrade: ${grade}`),
|
||||
allGrades: jest.fn(state => ({ allGrades: state })),
|
||||
},
|
||||
root: {
|
||||
getHeadings: jest.fn(state => ({ getHeadings: state })),
|
||||
},
|
||||
},
|
||||
}));
|
||||
describe('GradebookTable', () => {
|
||||
describe('component', () => {
|
||||
let el;
|
||||
const fields = { field1: 'field1', field2: 'field2' };
|
||||
const props = {
|
||||
grades: [
|
||||
{
|
||||
percent: 1,
|
||||
section_breakdown: [
|
||||
{ label: fields.field1, percent: 1.2 },
|
||||
{ label: fields.field2, percent: 2.3 },
|
||||
],
|
||||
},
|
||||
{
|
||||
percent: 2,
|
||||
section_breakdown: [
|
||||
{ label: fields.field1, percent: 1.2 },
|
||||
{ label: fields.field2, percent: 2.3 },
|
||||
],
|
||||
},
|
||||
{
|
||||
percent: 3,
|
||||
section_breakdown: [
|
||||
{ label: fields.field1, percent: 1.2 },
|
||||
{ label: fields.field2, percent: 2.3 },
|
||||
],
|
||||
},
|
||||
],
|
||||
headings: [
|
||||
Headings.username,
|
||||
Headings.email,
|
||||
fields.field1,
|
||||
fields.field2,
|
||||
Headings.totalGrade,
|
||||
],
|
||||
|
||||
intl: { formatMessage: (msg) => msg.defaultMessage },
|
||||
};
|
||||
test('snapshot - fields1 and 2 between email and totalGrade, mocked rows', () => {
|
||||
el = shallow(<GradebookTable {...props} />);
|
||||
el.instance().nullMethod = jest.fn().mockName('this.nullMethod');
|
||||
el.instance().mapRows = (entry) => `mappedRow: ${entry.percent}`;
|
||||
expect(el.instance().render()).toMatchSnapshot();
|
||||
});
|
||||
test('null method returns null for stub component', () => {
|
||||
el = shallow(<GradebookTable {...props} />);
|
||||
expect(el.instance().nullMethod()).toEqual(null);
|
||||
});
|
||||
describe('table columns (mapHeaders)', () => {
|
||||
let headings;
|
||||
beforeEach(() => {
|
||||
el = shallow(<GradebookTable {...props} />);
|
||||
headings = el.find(DataTable).props().columns;
|
||||
});
|
||||
test('username sets key and replaces Header with component', () => {
|
||||
const heading = headings[0];
|
||||
expect(heading.accessor).toEqual(Headings.username);
|
||||
expect(heading.Header.type).toEqual(LabelReplacements.UsernameLabelReplacement);
|
||||
});
|
||||
test('email sets key and Header from header', () => {
|
||||
const heading = headings[1];
|
||||
expect(heading.accessor).toEqual(Headings.email);
|
||||
expect(heading.Header).toEqual(<FormattedMessage {...messages.emailHeading} />);
|
||||
});
|
||||
test('subsections set key and Header from header', () => {
|
||||
expect(headings[2]).toEqual({ accessor: fields.field1, Header: fields.field1 });
|
||||
expect(headings[3]).toEqual({ accessor: fields.field2, Header: fields.field2 });
|
||||
});
|
||||
test('totalGrade sets key and replaces Header with component', () => {
|
||||
const heading = headings[4];
|
||||
expect(heading.accessor).toEqual(Headings.totalGrade);
|
||||
expect(heading.Header.type).toEqual(LabelReplacements.TotalGradeLabelReplacement);
|
||||
});
|
||||
});
|
||||
describe('table data (mapRows)', () => {
|
||||
let rows;
|
||||
beforeEach(() => {
|
||||
el = shallow(<GradebookTable {...props} />);
|
||||
rows = el.find(DataTable).props().data;
|
||||
});
|
||||
describe.each([0, 1, 2])('gradeEntry($percent)', (gradeIndex) => {
|
||||
let row;
|
||||
const entry = props.grades[gradeIndex];
|
||||
beforeEach(() => {
|
||||
row = rows[gradeIndex];
|
||||
});
|
||||
test('username set to Username Field', () => {
|
||||
const field = row[Headings.username];
|
||||
expect(field.type).toEqual(Fields.Username);
|
||||
expect(field.props).toEqual({
|
||||
username: entry.username,
|
||||
userKey: entry.external_user_key,
|
||||
});
|
||||
});
|
||||
test('email set to Email Field', () => {
|
||||
const field = row[Headings.email];
|
||||
expect(field.type).toEqual(Fields.Email);
|
||||
expect(field.props).toEqual({ email: entry.email });
|
||||
});
|
||||
test('totalGrade set to rounded percent grade * 100', () => {
|
||||
expect(
|
||||
row[Headings.totalGrade],
|
||||
).toEqual(`${selectors.grades.roundGrade(entry.percent * 100)}%`);
|
||||
});
|
||||
test('subsections loaded as GradeButtons', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('mapStateToProps', () => {
|
||||
let mapped;
|
||||
const testState = {
|
||||
where: 'did',
|
||||
all: 'of',
|
||||
these: 'bananas',
|
||||
come: 'from?',
|
||||
};
|
||||
beforeEach(() => {
|
||||
mapped = mapStateToProps(testState);
|
||||
});
|
||||
test('grades from grades.allGrades', () => {
|
||||
expect(mapped.grades).toEqual(selectors.grades.allGrades(testState));
|
||||
});
|
||||
test('headings from root.getHeadings', () => {
|
||||
expect(mapped.headings).toEqual(selectors.root.getHeadings(testState));
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -6,11 +6,10 @@ export const useImportButtonData = () => {
|
||||
const submitImportGradesButtonData = thunkActions.grades.useSubmitImportGradesButtonData();
|
||||
|
||||
const fileInputRef = useRef();
|
||||
const hasFile = fileInputRef.current && fileInputRef.current.files[0];
|
||||
|
||||
const handleClickImportGrades = () => hasFile && fileInputRef.current.click();
|
||||
const handleClickImportGrades = () => fileInputRef.current?.click();
|
||||
const handleFileInputChange = () => {
|
||||
if (hasFile) {
|
||||
if (fileInputRef.current?.files[0]) {
|
||||
const clearInput = () => {
|
||||
fileInputRef.current.value = null;
|
||||
};
|
||||
|
||||
@@ -29,7 +29,7 @@ testFormData.append('csv', testFile);
|
||||
const ref = {
|
||||
current: { click: jest.fn(), files: [testFile], value: 'test-value' },
|
||||
};
|
||||
describe('useAssignmentFilterData hook', () => {
|
||||
describe('useImportButtonData hook', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
React.useRef.mockReturnValue(ref);
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
/* eslint-disable react/sort-comp, react/button-has-type */
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { Toast } from '@edx/paragon';
|
||||
import {
|
||||
injectIntl,
|
||||
intlShape,
|
||||
} from '@edx/frontend-platform/i18n';
|
||||
|
||||
import selectors from 'data/selectors';
|
||||
import actions from 'data/actions';
|
||||
import { views } from 'data/constants/app';
|
||||
import messages from './ImportSuccessToast.messages';
|
||||
|
||||
/**
|
||||
* <ImportSuccessToast />
|
||||
* Toast component triggered by successful grade upload.
|
||||
* Provides a link to view the Bulk Management History tab.
|
||||
*/
|
||||
export class ImportSuccessToast extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.onClose = this.onClose.bind(this);
|
||||
this.handleShowHistoryView = this.handleShowHistoryView.bind(this);
|
||||
}
|
||||
|
||||
onClose() {
|
||||
this.props.setShow(false);
|
||||
}
|
||||
|
||||
handleShowHistoryView() {
|
||||
this.props.setAppView(views.bulkManagementHistory);
|
||||
this.onClose();
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Toast
|
||||
action={{
|
||||
label: this.props.intl.formatMessage(messages.showHistoryViewBtn),
|
||||
onClick: this.handleShowHistoryView,
|
||||
}}
|
||||
onClose={this.onClose}
|
||||
show={this.props.show}
|
||||
>
|
||||
{this.props.intl.formatMessage(messages.description)}
|
||||
</Toast>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ImportSuccessToast.propTypes = {
|
||||
// injected
|
||||
intl: intlShape.isRequired,
|
||||
// redux
|
||||
show: PropTypes.bool.isRequired,
|
||||
setAppView: PropTypes.func.isRequired,
|
||||
setShow: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export const mapStateToProps = (state) => ({
|
||||
show: selectors.app.showImportSuccessToast(state),
|
||||
});
|
||||
|
||||
export const mapDispatchToProps = {
|
||||
setAppView: actions.app.setView,
|
||||
setShow: actions.app.setShowImportSuccessToast,
|
||||
};
|
||||
|
||||
export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(ImportSuccessToast));
|
||||
@@ -1,110 +0,0 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import selectors from 'data/selectors';
|
||||
import actions from 'data/actions';
|
||||
import { views } from 'data/constants/app';
|
||||
|
||||
import {
|
||||
ImportSuccessToast,
|
||||
mapStateToProps,
|
||||
mapDispatchToProps,
|
||||
} from './ImportSuccessToast';
|
||||
import messages from './ImportSuccessToast.messages';
|
||||
|
||||
jest.mock('@edx/paragon', () => ({
|
||||
Toast: () => 'Toast',
|
||||
}));
|
||||
jest.mock('data/selectors', () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
app: {
|
||||
showImportSuccessToast: (state) => ({ showImportSuccessToast: state }),
|
||||
},
|
||||
},
|
||||
}));
|
||||
jest.mock('data/actions', () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
app: {
|
||||
setView: jest.fn(),
|
||||
setShow: jest.fn(),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
describe('ImportSuccessToast component', () => {
|
||||
describe('snapshots', () => {
|
||||
let el;
|
||||
let props = {
|
||||
show: true,
|
||||
};
|
||||
beforeEach(() => {
|
||||
props = {
|
||||
...props,
|
||||
intl: { formatMessage: (msg) => msg.defaultMessage },
|
||||
setAppView: jest.fn(),
|
||||
setShow: jest.fn(),
|
||||
};
|
||||
el = shallow(<ImportSuccessToast {...props} />);
|
||||
});
|
||||
test('snapshot', () => {
|
||||
el.instance().handleShowHistoryView = jest.fn().mockName('handleShowHistoryView');
|
||||
el.instance().onClose = jest.fn().mockName('onClose');
|
||||
expect(el).toMatchSnapshot();
|
||||
});
|
||||
describe('Toast props', () => {
|
||||
let toastProps;
|
||||
beforeEach(() => {
|
||||
toastProps = el.props();
|
||||
});
|
||||
test('action has translated label and onClick from this.handleShowHistoryView', () => {
|
||||
expect(toastProps.action).toEqual({
|
||||
label: props.intl.formatMessage(messages.showHistoryViewBtn),
|
||||
onClick: el.instance().handleShowHistoryView,
|
||||
});
|
||||
});
|
||||
test('onClose from this.onClose method', () => {
|
||||
expect(toastProps.onClose).toEqual(el.instance().onClose);
|
||||
});
|
||||
test('show from show prop', () => {
|
||||
expect(toastProps.show).toEqual(props.show);
|
||||
el.setProps({ show: false });
|
||||
expect(el.props().show).toEqual(false);
|
||||
});
|
||||
});
|
||||
describe('onClose', () => {
|
||||
it('calls props.setShow(false)', () => {
|
||||
el.instance().onClose();
|
||||
expect(props.setShow).toHaveBeenCalledWith(false);
|
||||
});
|
||||
});
|
||||
describe('handleShowHistoryView', () => {
|
||||
it('calls setAppView with views.bulkManagementHistory and this.onClose', () => {
|
||||
el.instance().onClose = jest.fn();
|
||||
el.instance().handleShowHistoryView();
|
||||
expect(props.setAppView).toHaveBeenCalledWith(views.bulkManagementHistory);
|
||||
expect(el.instance().onClose).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('behavior', () => {
|
||||
});
|
||||
describe('mapStateToProps', () => {
|
||||
const testState = { somewhere: 'over', the: 'rainbow' };
|
||||
const mapped = mapStateToProps(testState);
|
||||
test('show from app showImportSuccessToast selector', () => {
|
||||
expect(mapped.show).toEqual(
|
||||
selectors.app.showImportSuccessToast(testState),
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('mapDispatchToProps', () => {
|
||||
test('setAppView from actions.app.setView', () => {
|
||||
expect(mapDispatchToProps.setAppView).toEqual(actions.app.setView);
|
||||
});
|
||||
test('setShow from actions.setShowImportSuccessToast', () => {
|
||||
expect(mapDispatchToProps.setShow).toEqual(actions.app.setShowImportSuccessToast);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`ImportSuccessToast component render snapshot 1`] = `
|
||||
<Toast
|
||||
action="test-action"
|
||||
onClose={[MockFunction hooks.onClose]}
|
||||
show="test-show"
|
||||
>
|
||||
test-description
|
||||
</Toast>
|
||||
`;
|
||||
39
src/components/GradesView/ImportSuccessToast/hooks.js
Normal file
39
src/components/GradesView/ImportSuccessToast/hooks.js
Normal file
@@ -0,0 +1,39 @@
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { actions, selectors } from 'data/redux/hooks';
|
||||
import { views } from 'data/constants/app';
|
||||
import messages from './messages';
|
||||
|
||||
/**
|
||||
* <ImportSuccessToast />
|
||||
* Toast component triggered by successful grade upload.
|
||||
* Provides a link to view the Bulk Management History tab.
|
||||
*/
|
||||
export const useImportSuccessToastData = () => {
|
||||
const { formatMessage } = useIntl();
|
||||
|
||||
const show = selectors.app.useShowImportSuccessToast();
|
||||
const setAppView = actions.app.useSetView();
|
||||
const setShow = actions.app.useSetShowImportSuccessToast();
|
||||
|
||||
const onClose = () => {
|
||||
setShow(false);
|
||||
};
|
||||
|
||||
const handleShowHistoryView = () => {
|
||||
setAppView(views.bulkManagementHistory);
|
||||
setShow(false);
|
||||
};
|
||||
|
||||
return {
|
||||
action: {
|
||||
label: formatMessage(messages.showHistoryViewBtn),
|
||||
onClick: handleShowHistoryView,
|
||||
},
|
||||
onClose,
|
||||
show,
|
||||
description: formatMessage(messages.description),
|
||||
};
|
||||
};
|
||||
|
||||
export default useImportSuccessToastData;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user