Compare commits
2 Commits
bw/cm1_red
...
bw/compone
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
096ec432fe | ||
|
|
18648eb8c6 |
1
.env
1
.env
@@ -32,4 +32,3 @@ ENTERPRISE_MARKETING_UTM_CAMPAIGN=''
|
||||
ENTERPRISE_MARKETING_FOOTER_UTM_MEDIUM=''
|
||||
APP_ID=''
|
||||
MFE_CONFIG_API_URL=''
|
||||
DISPLAY_FEEDBACK_WIDGET='true'
|
||||
|
||||
@@ -39,4 +39,3 @@ ENTERPRISE_MARKETING_UTM_CAMPAIGN='example.com Referral'
|
||||
ENTERPRISE_MARKETING_FOOTER_UTM_MEDIUM='Footer'
|
||||
APP_ID=''
|
||||
MFE_CONFIG_API_URL=''
|
||||
DISPLAY_FEEDBACK_WIDGET='false'
|
||||
|
||||
@@ -3,4 +3,3 @@ 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,7 +4,6 @@ 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
|
||||
@@ -55,23 +54,9 @@ 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:
|
||||
|
||||
33574
package-lock.json
generated
33574
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.2",
|
||||
"version": "1.6.1",
|
||||
"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-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/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/paragon": "^19.25.4",
|
||||
"@edx/reactifex": "^2.1.1",
|
||||
"@fortawesome/fontawesome-svg-core": "^1.2.25",
|
||||
@@ -47,11 +47,12 @@
|
||||
"enzyme-to-json": "^3.6.2",
|
||||
"font-awesome": "4.7.0",
|
||||
"history": "4.10.1",
|
||||
"prop-types": "15.8.1",
|
||||
"prop-types": "15.7.2",
|
||||
"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",
|
||||
@@ -67,7 +68,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",
|
||||
@@ -76,7 +77,7 @@
|
||||
"fetch-mock": "^6.5.2",
|
||||
"husky": "2.7.0",
|
||||
"identity-obj-proxy": "^3.0.0",
|
||||
"jest": "^26.6.3",
|
||||
"jest": "29.3.1",
|
||||
"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="Close"
|
||||
src={[Function]}
|
||||
/>
|
||||
</div>
|
||||
<Collapsible
|
||||
|
||||
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>
|
||||
`;
|
||||
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.downloadReport.useBulkGrades();
|
||||
|
||||
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: {
|
||||
downloadReport: { useBulkGrades: jest.fn() },
|
||||
},
|
||||
},
|
||||
selectors: {
|
||||
root: {
|
||||
useGradeExportUrl: jest.fn(),
|
||||
useShowBulkManagement: jest.fn(),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
const downloadBulkGrades = jest.fn();
|
||||
actions.grades.downloadReport.useBulkGrades.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.downloadReport.useBulkGrades).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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
32
src/components/GradesView/BulkManagementControls/index.jsx
Normal file
32
src/components/GradesView/BulkManagementControls/index.jsx
Normal file
@@ -0,0 +1,32 @@
|
||||
/* 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();
|
||||
|
||||
return show && (
|
||||
<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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -7,7 +7,7 @@ import { Form } from '@edx/paragon';
|
||||
|
||||
import selectors from 'data/selectors';
|
||||
import actions from 'data/actions';
|
||||
import { getLocalizedSlash } from 'i18n/utils';
|
||||
import { getLocalizedSlash } from 'i18n';
|
||||
|
||||
/**
|
||||
* <AdjustedGradeInput />
|
||||
|
||||
@@ -29,16 +29,16 @@ Username.propTypes = {
|
||||
};
|
||||
|
||||
/**
|
||||
* Fields.Text
|
||||
* Simple label field for text value.
|
||||
* @param {string} value - value for display
|
||||
* Fields.Email
|
||||
* Simple label field for email value.
|
||||
* @param {string} email - email for display
|
||||
*/
|
||||
const Text = ({ value }) => (<span className="wrap-text-in-cell">{value}</span>);
|
||||
Text.propTypes = {
|
||||
value: PropTypes.string.isRequired,
|
||||
const Email = ({ email }) => <span className="wrap-text-in-cell">{email}</span>;
|
||||
Email.propTypes = {
|
||||
email: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default StrictDict({
|
||||
Text,
|
||||
Email,
|
||||
Username,
|
||||
});
|
||||
|
||||
@@ -41,13 +41,13 @@ describe('Gradebook Table Fields', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('Text', () => {
|
||||
const value = 'myTag@place.com';
|
||||
describe('Email', () => {
|
||||
const email = 'myTag@place.com';
|
||||
test('snapshot', () => {
|
||||
expect(shallow(<Fields.Text value={value} />)).toMatchSnapshot();
|
||||
expect(shallow(<Fields.Email email={email} />)).toMatchSnapshot();
|
||||
});
|
||||
test('wraps entry value', () => {
|
||||
expect(shallow(<Fields.Text value={value} />).text()).toEqual(value);
|
||||
test('wraps entry email', () => {
|
||||
expect(shallow(<Fields.Email email={email} />).text()).toEqual(email);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -45,13 +45,6 @@ const TotalGradeLabelReplacement = () => (
|
||||
</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
|
||||
@@ -63,24 +56,11 @@ const UsernameLabelReplacement = () => (
|
||||
</div>
|
||||
<div className="font-weight-normal student-key">
|
||||
<FormattedMessage {...messages.studentKeyLabel} />
|
||||
{ mastersOnlyFieldAsterisk }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
/**
|
||||
* <MastersOnlyLabelReplacement {message}>
|
||||
* Column header for fields that are only available for masters students
|
||||
*/
|
||||
const MastersOnlyLabelReplacement = (message) => (
|
||||
<div>
|
||||
<FormattedMessage {...message} />
|
||||
{ mastersOnlyFieldAsterisk }
|
||||
</div>
|
||||
);
|
||||
|
||||
export default StrictDict({
|
||||
TotalGradeLabelReplacement,
|
||||
UsernameLabelReplacement,
|
||||
MastersOnlyLabelReplacement,
|
||||
});
|
||||
|
||||
@@ -9,7 +9,6 @@ import LabelReplacements from './LabelReplacements';
|
||||
const {
|
||||
TotalGradeLabelReplacement,
|
||||
UsernameLabelReplacement,
|
||||
MastersOnlyLabelReplacement,
|
||||
} = LabelReplacements;
|
||||
|
||||
jest.mock('@edx/paragon', () => ({
|
||||
@@ -36,16 +35,6 @@ 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 Text snapshot 1`] = `
|
||||
exports[`Gradebook Table Fields Email snapshot 1`] = `
|
||||
<span
|
||||
className="wrap-text-in-cell"
|
||||
>
|
||||
|
||||
@@ -1,20 +1,5 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`LabelReplacements MastersOnlyLabelReplacement snapshot 1`] = `
|
||||
<div>
|
||||
<FormattedMessage
|
||||
defaultMessage="defaultMessAge"
|
||||
description="desCripTion"
|
||||
id="id"
|
||||
/>
|
||||
<span
|
||||
className="font-weight-normal"
|
||||
>
|
||||
*
|
||||
</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`LabelReplacements TotalGradeLabelReplacement displays overlay tooltip 1`] = `
|
||||
<Tooltip
|
||||
id="course-grade-tooltip"
|
||||
@@ -88,15 +73,10 @@ exports[`LabelReplacements UsernameLabelReplacement snapshot 1`] = `
|
||||
className="font-weight-normal student-key"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Student Key"
|
||||
defaultMessage="Student Key*"
|
||||
description="Gradebook table Student Key label"
|
||||
id="gradebook.GradesView.table.labels.studentKey"
|
||||
/>
|
||||
<span
|
||||
className="font-weight-normal"
|
||||
>
|
||||
*
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -13,16 +13,8 @@ exports[`GradebookTable component snapshot - fields1 and 2 between email and tot
|
||||
"accessor": "Username",
|
||||
},
|
||||
Object {
|
||||
"Header": <MastersOnlyLabelReplacement
|
||||
defaultMessage="Full Name"
|
||||
description="Gradebook table full name column header"
|
||||
id="gradebook.GradesView.table.headings.fullName"
|
||||
/>,
|
||||
"accessor": "Full Name",
|
||||
},
|
||||
Object {
|
||||
"Header": <MastersOnlyLabelReplacement
|
||||
defaultMessage="Email"
|
||||
"Header": <FormattedMessage
|
||||
defaultMessage="Email*"
|
||||
description="Gradebook table email column header"
|
||||
id="gradebook.GradesView.table.headings.email"
|
||||
/>,
|
||||
|
||||
@@ -4,11 +4,11 @@ import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { DataTable } from '@edx/paragon';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import selectors from 'data/selectors';
|
||||
import { Headings } from 'data/constants/grades';
|
||||
import { getLocalizedPercentSign } from 'i18n/utils';
|
||||
import { getLocalizedPercentSign } from 'i18n';
|
||||
|
||||
import messages from './messages';
|
||||
import Fields from './Fields';
|
||||
@@ -38,9 +38,7 @@ export class GradebookTable extends React.Component {
|
||||
} 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} />;
|
||||
label = <FormattedMessage {...messages.emailHeading} />;
|
||||
} else {
|
||||
label = heading;
|
||||
}
|
||||
@@ -51,8 +49,7 @@ export class GradebookTable extends React.Component {
|
||||
[Headings.username]: (
|
||||
<Fields.Username username={entry.username} userKey={entry.external_user_key} />
|
||||
),
|
||||
[Headings.fullName]: (<Fields.Text value={entry.full_name} />),
|
||||
[Headings.email]: (<Fields.Text value={entry.email} />),
|
||||
[Headings.email]: (<Fields.Email email={entry.email} />),
|
||||
[Headings.totalGrade]: `${roundGrade(entry.percent * 100)}${getLocalizedPercentSign()}`,
|
||||
...entry.section_breakdown.reduce((acc, subsection) => ({
|
||||
...acc,
|
||||
|
||||
@@ -1,14 +1,9 @@
|
||||
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: {
|
||||
@@ -23,7 +18,7 @@ const messages = defineMessages({
|
||||
},
|
||||
studentKeyLabel: {
|
||||
id: 'gradebook.GradesView.table.labels.studentKey',
|
||||
defaultMessage: 'Student Key',
|
||||
defaultMessage: 'Student Key*',
|
||||
description: 'Gradebook table Student Key label',
|
||||
},
|
||||
usernameLabel: {
|
||||
|
||||
@@ -2,6 +2,7 @@ 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';
|
||||
@@ -21,7 +22,7 @@ jest.mock('./Fields', () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
Username: () => 'Fields.Username',
|
||||
Text: () => 'Fields.Text',
|
||||
Email: () => 'Fields.Email',
|
||||
},
|
||||
}));
|
||||
jest.mock('./LabelReplacements', () => ({
|
||||
@@ -29,7 +30,6 @@ jest.mock('./LabelReplacements', () => ({
|
||||
default: {
|
||||
TotalGradeLabelReplacement: () => 'TotalGradeLabelReplacement',
|
||||
UsernameLabelReplacement: () => 'UsernameLabelReplacement',
|
||||
MastersOnlyLabelReplacement: () => 'MastersOnlyLabelReplacement',
|
||||
},
|
||||
}));
|
||||
jest.mock('./GradeButton', () => 'GradeButton');
|
||||
@@ -75,7 +75,6 @@ describe('GradebookTable', () => {
|
||||
],
|
||||
headings: [
|
||||
Headings.username,
|
||||
Headings.fullName,
|
||||
Headings.email,
|
||||
fields.field1,
|
||||
fields.field2,
|
||||
@@ -105,22 +104,17 @@ describe('GradebookTable', () => {
|
||||
expect(heading.accessor).toEqual(Headings.username);
|
||||
expect(heading.Header.type).toEqual(LabelReplacements.UsernameLabelReplacement);
|
||||
});
|
||||
test('full name sets key and Header from header', () => {
|
||||
const heading = headings[1];
|
||||
expect(heading.accessor).toEqual(Headings.fullName);
|
||||
expect(heading.Header).toEqual(<LabelReplacements.MastersOnlyLabelReplacement {...messages.fullNameHeading} />);
|
||||
});
|
||||
test('email sets key and Header from header', () => {
|
||||
const heading = headings[2];
|
||||
const heading = headings[1];
|
||||
expect(heading.accessor).toEqual(Headings.email);
|
||||
expect(heading.Header).toEqual(<LabelReplacements.MastersOnlyLabelReplacement {...messages.emailHeading} />);
|
||||
expect(heading.Header).toEqual(<FormattedMessage {...messages.emailHeading} />);
|
||||
});
|
||||
test('subsections set key and Header from header', () => {
|
||||
expect(headings[3]).toEqual({ accessor: fields.field1, Header: fields.field1 });
|
||||
expect(headings[4]).toEqual({ accessor: fields.field2, Header: fields.field2 });
|
||||
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[5];
|
||||
const heading = headings[4];
|
||||
expect(heading.accessor).toEqual(Headings.totalGrade);
|
||||
expect(heading.Header.type).toEqual(LabelReplacements.TotalGradeLabelReplacement);
|
||||
});
|
||||
@@ -145,15 +139,10 @@ describe('GradebookTable', () => {
|
||||
userKey: entry.external_user_key,
|
||||
});
|
||||
});
|
||||
test('fullName set to Text Field', () => {
|
||||
const field = row[Headings.fullName];
|
||||
expect(field.type).toEqual(Fields.Text);
|
||||
expect(field.props).toEqual({ value: entry.full_name });
|
||||
});
|
||||
test('email set to Text Field', () => {
|
||||
test('email set to Email Field', () => {
|
||||
const field = row[Headings.email];
|
||||
expect(field.type).toEqual(Fields.Text);
|
||||
expect(field.props).toEqual({ value: entry.email });
|
||||
expect(field.type).toEqual(Fields.Email);
|
||||
expect(field.props).toEqual({ email: entry.email });
|
||||
});
|
||||
test('totalGrade set to rounded percent grade * 100', () => {
|
||||
expect(
|
||||
|
||||
@@ -6,10 +6,11 @@ export const useImportButtonData = () => {
|
||||
const submitImportGradesButtonData = thunkActions.grades.useSubmitImportGradesButtonData();
|
||||
|
||||
const fileInputRef = useRef();
|
||||
const hasFile = fileInputRef.current && fileInputRef.current.files[0];
|
||||
|
||||
const handleClickImportGrades = () => fileInputRef.current?.click();
|
||||
const handleClickImportGrades = () => hasFile && fileInputRef.current.click();
|
||||
const handleFileInputChange = () => {
|
||||
if (fileInputRef.current?.files[0]) {
|
||||
if (hasFile) {
|
||||
const clearInput = () => {
|
||||
fileInputRef.current.value = null;
|
||||
};
|
||||
|
||||
@@ -3,7 +3,6 @@ import { StrictDict } from 'utils';
|
||||
const EMAIL_HEADING = 'Email';
|
||||
const TOTAL_COURSE_GRADE_HEADING = 'Total Grade (%)';
|
||||
const USERNAME_HEADING = 'Username';
|
||||
const FULL_NAME_HEADING = 'Full Name';
|
||||
|
||||
const GradeFormats = StrictDict({
|
||||
absolute: 'absolute',
|
||||
@@ -11,17 +10,15 @@ const GradeFormats = StrictDict({
|
||||
});
|
||||
|
||||
const Headings = StrictDict({
|
||||
email: EMAIL_HEADING,
|
||||
totalGrade: TOTAL_COURSE_GRADE_HEADING,
|
||||
username: USERNAME_HEADING,
|
||||
fullName: FULL_NAME_HEADING,
|
||||
email: 'Email',
|
||||
totalGrade: 'Total Grade (%)',
|
||||
username: 'Username',
|
||||
});
|
||||
|
||||
export {
|
||||
EMAIL_HEADING,
|
||||
TOTAL_COURSE_GRADE_HEADING,
|
||||
USERNAME_HEADING,
|
||||
FULL_NAME_HEADING,
|
||||
GradeFormats,
|
||||
Headings,
|
||||
};
|
||||
|
||||
@@ -4,11 +4,7 @@ import { actionHook } from './utils';
|
||||
|
||||
const app = StrictDict({
|
||||
useSetLocalFilter: actionHook(actions.app.setLocalFilter),
|
||||
useSetSearchValue: actionHook(actions.app.setSearchValue),
|
||||
useSetShowImportSuccessToast: actionHook(actions.app.setShowImportSuccessToast),
|
||||
useSetView: actionHook(actions.app.setView),
|
||||
useCloseModal: actionHook(actions.app.closeModal),
|
||||
useSetModalState: actionHook(actions.app.setModalState),
|
||||
});
|
||||
|
||||
const filters = StrictDict({
|
||||
@@ -19,15 +15,12 @@ const filters = StrictDict({
|
||||
useUpdateCourseGradeLimits: actionHook(actions.filters.update.courseGradeLimits),
|
||||
useUpdateIncludeCourseRoleMembers: actionHook(actions.filters.update.includeCourseRoleMembers),
|
||||
useUpdateTrack: actionHook(actions.filters.update.track),
|
||||
useResetFilters: actionHook(actions.filters.reset),
|
||||
});
|
||||
|
||||
const grades = StrictDict({
|
||||
useDoneViewingAssignment: actionHook(actions.grades.doneViewingAssignment),
|
||||
useDownloadBulkGradesReport: actionHook(actions.grades.downloadReport.bulkGrades),
|
||||
useDownloadInterventionReport: actionHook(actions.grades.downloadReport.intervention),
|
||||
useToggleGradeFormat: actionHook(actions.grades.toggleGradeFormat),
|
||||
useCloseBanner: actionHook(actions.grades.banner.close),
|
||||
downloadReport: {
|
||||
useBulkGrades: actionHook(actions.grades.downloadReport.bulkGrades),
|
||||
},
|
||||
});
|
||||
|
||||
export default StrictDict({
|
||||
|
||||
@@ -4,6 +4,23 @@ import actions from 'data/actions';
|
||||
import { actionHook } from './utils';
|
||||
import actionHooks from './actions';
|
||||
|
||||
jest.mock('data/actions', () => ({
|
||||
app: {
|
||||
setLocalFilter: jest.fn(),
|
||||
setView: jest.fn(),
|
||||
},
|
||||
filters: {
|
||||
update: {
|
||||
assignment: jest.fn(),
|
||||
assignmentLimits: jest.fn(),
|
||||
},
|
||||
},
|
||||
grades: {
|
||||
downloadReport: {
|
||||
bulkGrades: jest.fn(),
|
||||
},
|
||||
},
|
||||
}));
|
||||
jest.mock('./utils', () => ({
|
||||
actionHook: (action) => ({ actionHook: action }),
|
||||
}));
|
||||
@@ -13,7 +30,6 @@ let hooks;
|
||||
const testActionHook = (hookKey, action) => {
|
||||
test(hookKey, () => {
|
||||
expect(hooks[hookKey]).toEqual(actionHook(action));
|
||||
expect(hooks[hookKey]).not.toEqual(undefined);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -22,11 +38,7 @@ describe('action hooks', () => {
|
||||
const hookKeys = keyStore(actionHooks.app);
|
||||
beforeEach(() => { hooks = actionHooks.app; });
|
||||
testActionHook(hookKeys.useSetLocalFilter, actions.app.setLocalFilter);
|
||||
testActionHook(hookKeys.useSetSearchValue, actions.app.setSearchValue);
|
||||
testActionHook(hookKeys.useSetShowImportSuccessToast, actions.app.setShowImportSuccessToast);
|
||||
testActionHook(hookKeys.useSetView, actions.app.setView);
|
||||
testActionHook(hookKeys.useCloseModal, actions.app.closeModal);
|
||||
testActionHook(hookKeys.useSetModalState, actions.app.setModalState);
|
||||
});
|
||||
describe('filters', () => {
|
||||
const hookKeys = keyStore(actionHooks.filters);
|
||||
@@ -34,23 +46,19 @@ describe('action hooks', () => {
|
||||
beforeEach(() => { hooks = actionHooks.filters; });
|
||||
testActionHook(hookKeys.useUpdateAssignment, actionGroup.assignment);
|
||||
testActionHook(hookKeys.useUpdateAssignmentLimits, actionGroup.assignmentLimits);
|
||||
testActionHook(hookKeys.useUpdateAssignmentType, actionGroup.assignmentType);
|
||||
testActionHook(hookKeys.useUpdateCohort, actionGroup.cohort);
|
||||
testActionHook(hookKeys.useUpdateCohort, actionGroup.updateCohort);
|
||||
testActionHook(hookKeys.useUpdateCourseGradeLimits, actionGroup.courseGradeLimits);
|
||||
testActionHook(
|
||||
hookKeys.useUpdateIncludeCourseRoleMembers,
|
||||
actionGroup.includeCourseRoleMembers,
|
||||
actionGroup.updateIncludeCourseRoleMembers,
|
||||
);
|
||||
testActionHook(hookKeys.useResetFilters, actions.filters.reset);
|
||||
testActionHook(hookKeys.useUpdateTrack, actionGroup.updateTrack);
|
||||
});
|
||||
describe('grades', () => {
|
||||
const hookKeys = keyStore(actionHooks.grades);
|
||||
const actionGroup = actions.grades;
|
||||
beforeEach(() => { hooks = actionHooks.grades; });
|
||||
testActionHook(hookKeys.useDoneViewingAssignment, actionGroup.doneViewingAssignment);
|
||||
testActionHook(hookKeys.useDownloadBulkGradesReport, actionGroup.downloadReport.bulkGrades);
|
||||
testActionHook(hookKeys.useDownloadInterventionReport, actionGroup.downloadReport.intervention);
|
||||
testActionHook(hookKeys.useToggleGradeFormat, actionGroup.toggleGradeFormat);
|
||||
testActionHook(hookKeys.useCloseBanner, actionGroup.banner.close);
|
||||
test('downloadReport.useBulkGrades', () => {
|
||||
expect(hooks.downloadReport.useBulkGrades)
|
||||
.toEqual(actionHook(actions.grades.downloadReport.bulkGrades));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,75 +3,48 @@ import { useSelector } from 'react-redux';
|
||||
import { StrictDict } from 'utils';
|
||||
import selectors from 'data/selectors';
|
||||
|
||||
const selectorHook = (selector) => () => useSelector(selector);
|
||||
|
||||
export const root = StrictDict({
|
||||
useEditModalPossibleGrade: selectorHook(selectors.root.editModalPossibleGrade),
|
||||
useGetHeadings: selectorHook(selectors.root.getHeadings),
|
||||
useGradeExportUrl: selectorHook(selectors.root.gradeExportUrl),
|
||||
useInterventionExportUrl: selectorHook(selectors.root.interventionExportUrl),
|
||||
useSelectedCohortEntry: selectorHook(selectors.root.selectedCohortEntry),
|
||||
useSelectedTrackEntry: selectorHook(selectors.root.selectedTrackEntry),
|
||||
useShouldShowSpinner: selectorHook(selectors.root.shouldShowSpinner),
|
||||
useShowBulkManagement: selectorHook(selectors.root.showBulkManagement),
|
||||
useFilterBadgeConfig: (filterName) => useSelector(
|
||||
(state) => selectors.root.filterBadgeConfig(state, filterName),
|
||||
),
|
||||
useGradeExportUrl: () => useSelector(selectors.root.gradeExportUrl),
|
||||
useSelectedCohortEntry: () => useSelector(selectors.root.selectedCohortEntry),
|
||||
useSelectedTrackEntry: () => useSelector(selectors.root.selectedTrackEntry),
|
||||
useShowBulkManagement: () => useSelector(selectors.root.showBulkManagement),
|
||||
});
|
||||
|
||||
export const app = StrictDict({
|
||||
useActiveView: selectorHook(selectors.app.activeView),
|
||||
useAssignmentGradeLimits: selectorHook(selectors.app.assignmentGradeLimits),
|
||||
useAreCourseGradeFiltersValid: selectorHook(selectors.app.areCourseGradeFiltersValid),
|
||||
useCourseGradeLimits: selectorHook(selectors.app.courseGradeLimits),
|
||||
useCourseGradeFilterValidity: selectorHook(selectors.app.courseGradeFilterValidity),
|
||||
useCourseId: selectorHook(selectors.app.courseId),
|
||||
useModalData: selectorHook(selectors.app.modalData),
|
||||
useSearchValue: selectorHook(selectors.app.searchValue),
|
||||
useShowImportSuccessToast: selectorHook(selectors.app.showImportSuccessToast),
|
||||
useActiveView: () => useSelector(selectors.app.activeView),
|
||||
useAssignmentGradeLimits: () => useSelector(selectors.app.assignmentGradeLimits),
|
||||
useAreCourseGradeFiltersValid: () => useSelector(selectors.app.areCourseGradeFiltersValid),
|
||||
useCourseGradeLimits: () => useSelector(selectors.app.courseGradeLimits),
|
||||
useCourseId: () => useSelector(selectors.app.courseId),
|
||||
});
|
||||
|
||||
export const assignmentTypes = StrictDict({
|
||||
useAllAssignmentTypes: selectorHook(selectors.assignmentTypes.allAssignmentTypes),
|
||||
useAreGradesFrozen: selectorHook(selectors.assignmentTypes.areGradesFrozen),
|
||||
useAllAssignmentTypes: () => useSelector(selectors.assignmentTypes.allAssignmentTypes),
|
||||
useAreGradesFrozen: () => useSelector(selectors.assignmentTypes.areGradesFrozen),
|
||||
});
|
||||
|
||||
export const cohorts = StrictDict({
|
||||
useAllCohorts: selectorHook(selectors.cohorts.allCohorts),
|
||||
useAllCohorts: () => useSelector(selectors.cohorts.allCohorts),
|
||||
// maybe not needed?
|
||||
useCohortsByName: selectorHook(selectors.cohorts.cohortsByName),
|
||||
useCohortsByName: () => useSelector(selectors.cohorts.cohortsByName),
|
||||
});
|
||||
|
||||
export const filters = StrictDict({
|
||||
useData: selectorHook(selectors.filters.allFilters),
|
||||
useIncludeCourseRoleMembers: selectorHook(selectors.filters.includeCourseRoleMembers),
|
||||
useSelectableAssignmentLabels: selectorHook(selectors.filters.selectableAssignmentLabels),
|
||||
useSelectedAssignmentLabel: selectorHook(selectors.filters.selectedAssignmentLabel),
|
||||
useAssignmentType: selectorHook(selectors.filters.assignmentType),
|
||||
});
|
||||
|
||||
export const grades = StrictDict({
|
||||
useAllGrades: selectorHook(selectors.grades.allGrades),
|
||||
useUserCounts: () => ({
|
||||
filteredUsersCount: useSelector(selectors.grades.filteredUsersCount),
|
||||
totalUsersCount: useSelector(selectors.grades.totalUsersCount),
|
||||
}),
|
||||
useGradeData: selectorHook(selectors.grades.gradeData),
|
||||
useHasOverrideErrors: selectorHook(selectors.grades.hasOverrideErrors),
|
||||
useShowSuccess: selectorHook(selectors.grades.showSuccess),
|
||||
useSubsectionGrade: ({ gradeFormat, subsection }) => () => (
|
||||
selectors.grades.subsectionGrade[gradeFormat](subsection)
|
||||
),
|
||||
useData: () => useSelector(selectors.filters.allFilters),
|
||||
useIncludeCourseRoleMembers: () => useSelector(selectors.filters.includeCourseRoleMembers),
|
||||
useSelectableAssignmentLabels: () => useSelector(selectors.filters.selectableAssignmentLabels),
|
||||
useSelectedAssignmentLabel: () => useSelector(selectors.filters.selectedAssignmentLabel),
|
||||
useAssignmentType: () => useSelector(selectors.filters.assignmentType),
|
||||
});
|
||||
|
||||
export const roles = StrictDict({
|
||||
useCanUserViewGradebook: selectorHook(selectors.roles.canUserViewGradebook),
|
||||
useCanUserViewGradebook: () => useSelector(selectors.roles.canUserViewGradebook),
|
||||
});
|
||||
|
||||
export const tracks = StrictDict({
|
||||
useAllTracks: selectorHook(selectors.tracks.allTracks),
|
||||
useAllTracks: () => useSelector(selectors.tracks.allTracks),
|
||||
// maybe not needed?
|
||||
useTracksByName: selectorHook(selectors.tracks.tracksByName),
|
||||
useTracksByName: () => useSelector(selectors.tracks.tracksByName),
|
||||
});
|
||||
|
||||
export default StrictDict({
|
||||
@@ -79,7 +52,6 @@ export default StrictDict({
|
||||
assignmentTypes,
|
||||
cohorts,
|
||||
filters,
|
||||
grades,
|
||||
roles,
|
||||
tracks,
|
||||
root,
|
||||
|
||||
@@ -7,102 +7,104 @@ jest.mock('react-redux', () => ({
|
||||
useSelector: (selector) => ({ useSelector: selector }),
|
||||
}));
|
||||
|
||||
const testValue = 'test-value';
|
||||
const testState = { test: 'state value' };
|
||||
jest.mock('data/selectors', () => ({
|
||||
app: {
|
||||
activeView: jest.fn(),
|
||||
assignmentGradeLimits: jest.fn(),
|
||||
areCourseGradeFiltersValid: jest.fn(),
|
||||
courseGradelimits: jest.fn(),
|
||||
courseId: jest.fn(),
|
||||
},
|
||||
assignmentTypes: {
|
||||
allAssignmentTypes: jest.fn(),
|
||||
areGradesFrozen: jest.fn(),
|
||||
},
|
||||
cohorts: {
|
||||
allCohorts: jest.fn(),
|
||||
cohortsByName: jest.fn(),
|
||||
},
|
||||
filters: {
|
||||
allFilters: jest.fn(),
|
||||
includeCourseRoleMembers: jest.fn(),
|
||||
selectableAssignmentLabels: jest.fn(),
|
||||
selectedAssignmentLabel: jest.fn(),
|
||||
assignmentType: jest.fn(),
|
||||
},
|
||||
roles: {
|
||||
canUserViewGradebook: jest.fn(),
|
||||
},
|
||||
tracks: {
|
||||
allTracks: jest.fn(),
|
||||
tracksByName: jest.fn(),
|
||||
},
|
||||
root: {
|
||||
gradeExportUrl: jest.fn(),
|
||||
selectedCohortEntry: jest.fn(),
|
||||
selectedTrackEntry: jest.fn(),
|
||||
showBulkManagement: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
let hookKeys;
|
||||
let hooks;
|
||||
let selKeys;
|
||||
let selectorGroup;
|
||||
|
||||
const loadSelectorGroup = (hookGroup, selGroup) => {
|
||||
hookKeys = keyStore(hookGroup);
|
||||
selKeys = keyStore(selGroup);
|
||||
beforeEach(() => {
|
||||
hooks = hookGroup;
|
||||
selectorGroup = selGroup;
|
||||
});
|
||||
};
|
||||
|
||||
const testHook = (hookKey, selectorKey) => {
|
||||
const testHook = (hookKey, selector) => {
|
||||
test(hookKey, () => {
|
||||
expect(hooks[hookKey]()).toEqual(useSelector(selectorGroup[selectorKey]));
|
||||
expect(hooks[hookKey]()).toEqual(useSelector(selector));
|
||||
});
|
||||
};
|
||||
|
||||
describe('selector hooks', () => {
|
||||
describe('root selectors', () => {
|
||||
loadSelectorGroup(selectorHooks.root, selectors.root);
|
||||
testHook(hookKeys.useEditModalPossibleGrade, selKeys.editModalPossibleGrade);
|
||||
testHook(hookKeys.useGetHeadings, selKeys.getHeadings);
|
||||
testHook(hookKeys.useGradeExportUrl, selKeys.gradeExportUrl);
|
||||
testHook(hookKeys.useInterventionExportUrl, selKeys.interventionExportUrl);
|
||||
testHook(hookKeys.useSelectedCohortEntry, selKeys.selectedCohortEntry);
|
||||
testHook(hookKeys.useSelectedTrackEntry, selKeys.selectedTrackEntry);
|
||||
testHook(hookKeys.useShouldShowSpinner, selKeys.shouldShowSpinner);
|
||||
testHook(hookKeys.useShowBulkManagement, selKeys.showBulkManagement);
|
||||
describe(hookKeys.useFilterBadgeConfig, () => {
|
||||
test('calls filterBadgeConfig selector with passed filterName', () => {
|
||||
const filterBadgeConfig = (state, filterName) => ({
|
||||
filterBadgeConfig: { state, filterName },
|
||||
});
|
||||
const rootKeys = keyStore(selectors.root);
|
||||
jest.spyOn(selectors.root, rootKeys.filterBadgeConfig)
|
||||
.mockImplementation(filterBadgeConfig);
|
||||
const out = hooks.useFilterBadgeConfig(testValue);
|
||||
expect(out.useSelector(testState)).toEqual(filterBadgeConfig(testState, testValue));
|
||||
});
|
||||
});
|
||||
const hookKeys = keyStore(selectorHooks.root);
|
||||
beforeEach(() => { hooks = selectorHooks.root; });
|
||||
testHook(hookKeys.useGradeExportUrl, selectors.root.gradeExportUrl);
|
||||
testHook(hookKeys.useSelectedCohortEntry, selectors.root.selectedCohortEntry);
|
||||
testHook(hookKeys.useSelectedTrackEntry, selectors.root.selectedTrackEntry);
|
||||
testHook(hookKeys.useShowBulkManagement, selectors.root.showBulkManagement);
|
||||
});
|
||||
describe('app', () => {
|
||||
loadSelectorGroup(selectorHooks.app, selectors.app);
|
||||
testHook(hookKeys.useActiveView, selKeys.activeView);
|
||||
testHook(hookKeys.useAssignmentGradeLimits, selKeys.assignmentGradeLimits);
|
||||
testHook(hookKeys.useAreCourseGradeFiltersValid, selKeys.areCourseGradeFiltersValid);
|
||||
testHook(hookKeys.useCourseGradeLimits, selKeys.courseGradeLimits);
|
||||
testHook(hookKeys.useCourseId, selKeys.courseId);
|
||||
testHook(hookKeys.useModalData, selKeys.modalData);
|
||||
testHook(hookKeys.useSearchValue, selKeys.searchValue);
|
||||
testHook(hookKeys.useShowImportSuccessToast, selKeys.showImportSuccessToast);
|
||||
const hookKeys = keyStore(selectorHooks.app);
|
||||
const selGroup = selectors.app;
|
||||
beforeEach(() => { hooks = selectorHooks.app; });
|
||||
testHook(hookKeys.useActiveView, selGroup.activeView);
|
||||
testHook(hookKeys.useAssignmentGradeLimits, selGroup.assignmentGradeLimits);
|
||||
testHook(hookKeys.useAreCourseGradeFiltersValid, selGroup.areCourseGradeFiltersValid);
|
||||
testHook(hookKeys.useCourseGradeLimits, selGroup.courseGradeLimits);
|
||||
testHook(hookKeys.useCourseId, selGroup.courseId);
|
||||
});
|
||||
describe('assignmentTypes', () => {
|
||||
loadSelectorGroup(selectorHooks.assignmentTypes, selectors.assignmentTypes);
|
||||
testHook(hookKeys.useAllAssignmentTypes, selKeys.allAssignmentTypes);
|
||||
testHook(hookKeys.useAreGradesFrozen, selKeys.areGradesFrozen);
|
||||
const hookKeys = keyStore(selectorHooks.assignmentTypes);
|
||||
const selGroup = selectors.assignmentTypes;
|
||||
beforeEach(() => { hooks = selectorHooks.assignmentTypes; });
|
||||
testHook(hookKeys.useAllAssignmentTypes, selGroup.allAssignmentTypes);
|
||||
testHook(hookKeys.useAreGradesFrozen, selGroup.areGradesFrozen);
|
||||
});
|
||||
describe('cohorts', () => {
|
||||
loadSelectorGroup(selectorHooks.cohorts, selectors.cohorts);
|
||||
testHook(hookKeys.useAllCohorts, selKeys.allCohorts);
|
||||
testHook(hookKeys.useCohortsByName, selKeys.cohortsByName);
|
||||
const hookKeys = keyStore(selectorHooks.cohorts);
|
||||
const selGroup = selectors.cohorts;
|
||||
beforeEach(() => { hooks = selectorHooks.cohorts; });
|
||||
testHook(hookKeys.useAllCohorts, selGroup.allCohorts);
|
||||
testHook(hookKeys.useCohortsByName, selGroup.cohortsByName);
|
||||
});
|
||||
describe('filters', () => {
|
||||
loadSelectorGroup(selectorHooks.filters, selectors.filters);
|
||||
testHook(hookKeys.useData, selKeys.allFilters);
|
||||
testHook(hookKeys.useIncludeCourseRoleMembers, selKeys.includeCourseRoleMembers);
|
||||
testHook(hookKeys.useSelectableAssignmentLabels, selKeys.selectableAssignmentLabels);
|
||||
testHook(hookKeys.useSelectedAssignmentLabel, selKeys.selectedAssignmentLabel);
|
||||
testHook(hookKeys.useAssignmentType, selKeys.assignmentType);
|
||||
});
|
||||
describe('grades', () => {
|
||||
loadSelectorGroup(selectorHooks.grades, selectors.grades);
|
||||
testHook(hookKeys.useAllGrades, selKeys.allGrades);
|
||||
testHook(hookKeys.useGradeData, selKeys.gradeData);
|
||||
testHook(hookKeys.useHasOverrideErrors, selKeys.hasOverrideErrors);
|
||||
testHook(hookKeys.useShowSuccess, selKeys.showSuccess);
|
||||
test(hookKeys.useUserCounts, () => {
|
||||
expect(hooks.useUserCounts()).toEqual({
|
||||
filteredUsersCount: useSelector(selectors.grades.filteredUsersCount),
|
||||
totalUsersCount: useSelector(selectors.grades.totalUsersCount),
|
||||
});
|
||||
});
|
||||
const hookKeys = keyStore(selectorHooks.filters);
|
||||
const selGroup = selectors.filters;
|
||||
beforeEach(() => { hooks = selectorHooks.filters; });
|
||||
testHook(hookKeys.useData, selGroup.allFilters);
|
||||
testHook(hookKeys.useIncludeCourseRoleMembers, selGroup.includeCourseRoleMembers);
|
||||
testHook(hookKeys.useSelectableAssignmentLabels, selGroup.selectableAssignmentLabels);
|
||||
testHook(hookKeys.useSelectedAssignmentLabel, selGroup.selectedAssignmentLabel);
|
||||
testHook(hookKeys.useAssignmentType, selGroup.assignmentType);
|
||||
});
|
||||
describe('roles', () => {
|
||||
loadSelectorGroup(selectorHooks.roles, selectors.roles);
|
||||
testHook(hookKeys.useCanUserViewGradebook, selKeys.canUserViewGradebook);
|
||||
const hookKeys = keyStore(selectorHooks.roles);
|
||||
const selGroup = selectors.roles;
|
||||
beforeEach(() => { hooks = selectorHooks.roles; });
|
||||
testHook(hookKeys.useCanUserViewGradebook, selGroup.canUserViewGradebook);
|
||||
});
|
||||
describe('tracks', () => {
|
||||
loadSelectorGroup(selectorHooks.tracks, selectors.tracks);
|
||||
testHook(hookKeys.useAllTracks, selKeys.allTracks);
|
||||
testHook(hookKeys.useTracksByName, selKeys.tracksByName);
|
||||
const hookKeys = keyStore(selectorHooks.tracks);
|
||||
const selGroup = selectors.tracks;
|
||||
beforeEach(() => { hooks = selectorHooks.tracks; });
|
||||
testHook(hookKeys.useAllTracks, selGroup.allTracks);
|
||||
testHook(hookKeys.useTracksByName, selGroup.tracksByName);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,22 +3,15 @@ import thunkActions from 'data/thunkActions';
|
||||
import { actionHook } from './utils';
|
||||
|
||||
const app = StrictDict({
|
||||
filterMenu: {
|
||||
useCloseMenu: actionHook(thunkActions.app.filterMenu.close),
|
||||
useHandleTransitionEnd: actionHook(thunkActions.app.filterMenu.handleTransitionEnd),
|
||||
useToggleMenu: actionHook(thunkActions.app.filterMenu.toggle),
|
||||
},
|
||||
useSetModalStateFromTable: actionHook(thunkActions.app.setModalStateFromTable),
|
||||
useCloseFilterMenu: actionHook(thunkActions.app.filterMenu.close),
|
||||
});
|
||||
|
||||
const grades = StrictDict({
|
||||
useFetchGradesIfAssignmentGradeFiltersSet: actionHook(
|
||||
thunkActions.grades.fetchGradesIfAssignmentGradeFiltersSet,
|
||||
),
|
||||
useFetchPrevNextGrades: actionHook(thunkActions.grades.fetchPrevNextGrades),
|
||||
useFetchGrades: actionHook(thunkActions.grades.fetchGrades),
|
||||
useSubmitImportGradesButtonData: actionHook(thunkActions.grades.submitImportGradesButtonData),
|
||||
useUpdateGrades: actionHook(thunkActions.grades.updateGrades),
|
||||
});
|
||||
|
||||
export default StrictDict({
|
||||
|
||||
@@ -5,11 +5,7 @@ import thunkActionHooks from './thunkActions';
|
||||
|
||||
jest.mock('data/thunkActions', () => ({
|
||||
app: {
|
||||
filterMenu: {
|
||||
close: jest.fn(),
|
||||
handleTransitionEnd: jest.fn(),
|
||||
toggle: jest.fn(),
|
||||
},
|
||||
filterMenu: { close: jest.fn() },
|
||||
},
|
||||
grades: {
|
||||
fetchGrades: jest.fn(),
|
||||
@@ -29,38 +25,24 @@ const testActionHook = (hookKey, action) => {
|
||||
expect(hooks[hookKey]).toEqual(actionHook(action));
|
||||
});
|
||||
};
|
||||
let hookKeys;
|
||||
describe('thunkAction hooks', () => {
|
||||
describe('app', () => {
|
||||
hookKeys = keyStore(thunkActionHooks.app);
|
||||
const hookKeys = keyStore(thunkActionHooks.app);
|
||||
beforeEach(() => { hooks = thunkActionHooks.app; });
|
||||
testActionHook(hookKeys.useSetModalStateFromTable, thunkActions.app.setModalStateFromTable);
|
||||
|
||||
describe('filterMenu', () => {
|
||||
hookKeys = keyStore(thunkActionHooks.app.filterMenu);
|
||||
beforeEach(() => { hooks = thunkActionHooks.app.filterMenu; });
|
||||
testActionHook(hookKeys.useCloseMenu, thunkActions.app.filterMenu.close);
|
||||
testActionHook(
|
||||
hookKeys.useHandleTransitionEnd,
|
||||
thunkActions.app.filterMenu.handleTransitionEnd,
|
||||
);
|
||||
testActionHook(hookKeys.useToggleMenu, thunkActions.app.filterMenu.toggle);
|
||||
});
|
||||
testActionHook(hookKeys.useCloseFilterMenu, thunkActions.app.filterMenu.close);
|
||||
});
|
||||
describe('grades', () => {
|
||||
hookKeys = keyStore(thunkActionHooks.grades);
|
||||
const hookKeys = keyStore(thunkActionHooks.grades);
|
||||
const actionGroup = thunkActions.grades;
|
||||
beforeEach(() => { hooks = thunkActionHooks.grades; });
|
||||
testActionHook(hookKeys.useFetchGrades, actionGroup.fetchGrades);
|
||||
testActionHook(
|
||||
hookKeys.useFetchGradesIfAssignmentGradeFiltersSet,
|
||||
actionGroup.fetchGradesIfAssignmentGradeFiltersSet,
|
||||
);
|
||||
testActionHook(hookKeys.useFetchPrevNextGrades, actionGroup.fetchPrevNextGrades);
|
||||
testActionHook(hookKeys.useFetchGrades, actionGroup.fetchGrades);
|
||||
testActionHook(
|
||||
hookKeys.useSubmitImportGradesButtonData,
|
||||
actionGroup.submitImportGradesButtonData,
|
||||
);
|
||||
testActionHook(hookKeys.useUpdateGrades, actionGroup.updateGrades);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
import { StrictDict } from 'utils';
|
||||
import selectors from 'data/selectors';
|
||||
|
||||
export const grades = StrictDict({
|
||||
subsectionGrade: ({ gradeFormat, subsection }) => () => (
|
||||
selectors.grades.subsectionGrade[gradeFormat](subsection)
|
||||
),
|
||||
roundGrade: selectors.grades.roundGrade,
|
||||
});
|
||||
|
||||
export default StrictDict({
|
||||
grades,
|
||||
});
|
||||
@@ -1,38 +0,0 @@
|
||||
import selectors from 'data/selectors';
|
||||
|
||||
import { GradeFormats } from 'data/constants/grades';
|
||||
import transforms from './transforms';
|
||||
|
||||
jest.mock('data/selectors', () => {
|
||||
const {
|
||||
GradeFormats: { absolute, percent },
|
||||
} = jest.requireActual('data/constants/grades');
|
||||
return {
|
||||
grades: {
|
||||
subsectionGrade: {
|
||||
[absolute]: jest.fn(v => ({ absolute: v })),
|
||||
[percent]: jest.fn(v => ({ percent: v })),
|
||||
},
|
||||
roundGrade: jest.fn(),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
describe('redux transforms', () => {
|
||||
describe('grades transforms', () => {
|
||||
test('subsectionGrade', () => {
|
||||
const subsection = 'test-subsection';
|
||||
expect(transforms.grades.subsectionGrade({
|
||||
gradeFormat: GradeFormats.absolute,
|
||||
subsection,
|
||||
})()).toEqual(selectors.grades.subsectionGrade.absolute(subsection));
|
||||
expect(transforms.grades.subsectionGrade({
|
||||
gradeFormat: GradeFormats.percent,
|
||||
subsection,
|
||||
})()).toEqual(selectors.grades.subsectionGrade.percent(subsection));
|
||||
});
|
||||
test('roundGrade', () => {
|
||||
expect(transforms.grades.roundGrade).toEqual(selectors.grades.roundGrade);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -89,16 +89,6 @@ const modalSelectors = simpleSelectorFactory(
|
||||
],
|
||||
);
|
||||
|
||||
const modalData = ({ app: { modalState } }) => ({
|
||||
assignmentName: modalState.assignmentName,
|
||||
adjustedGradePossible: modalState.adjustedGradePossible,
|
||||
adjustedGradeValue: modalState.adjustedGradeValue,
|
||||
open: modalState.open,
|
||||
reasonForChange: modalState.reasonForChange,
|
||||
todaysDate: modalState.todaysDate,
|
||||
updateUserName: modalState.updateUserName,
|
||||
});
|
||||
|
||||
const filterMenuSelectors = simpleSelectorFactory(
|
||||
({ app: { filterMenu } }) => filterMenu,
|
||||
['open', 'transitioning'],
|
||||
@@ -125,7 +115,6 @@ export default StrictDict({
|
||||
isFilterMenuOpening,
|
||||
...simpleSelectors,
|
||||
modalState: StrictDict(modalSelectors),
|
||||
modalData,
|
||||
filterMenu: StrictDict({
|
||||
...filterMenuSelectors,
|
||||
isClosed: isFilterMenuClosed,
|
||||
|
||||
@@ -3,7 +3,7 @@ import { StrictDict } from 'utils';
|
||||
|
||||
import { Headings, GradeFormats } from 'data/constants/grades';
|
||||
import { formatDateForDisplay } from 'data/actions/utils';
|
||||
import { getLocalizedSlash } from 'i18n/utils';
|
||||
import { getLocalizedSlash } from 'i18n';
|
||||
import simpleSelectorFactory from '../utils';
|
||||
import * as module from './grades';
|
||||
|
||||
@@ -105,17 +105,12 @@ export const headingMapper = (category, label = 'All') => {
|
||||
} else {
|
||||
filter = filters.byLabel;
|
||||
}
|
||||
const {
|
||||
username,
|
||||
fullName,
|
||||
email,
|
||||
totalGrade,
|
||||
} = Headings;
|
||||
const { username, email, totalGrade } = Headings;
|
||||
const filteredLabels = (entry) => entry.filter(filter).map(s => s.label);
|
||||
|
||||
return (entry) => (
|
||||
entry
|
||||
? [username, fullName, email, ...filteredLabels(entry), totalGrade]
|
||||
? [username, email, ...filteredLabels(entry), totalGrade]
|
||||
: []
|
||||
);
|
||||
};
|
||||
@@ -266,26 +261,12 @@ const simpleSelectors = simpleSelectorFactory(
|
||||
'gradeOverrideHistoryError',
|
||||
'gradeOriginalEarnedGraded',
|
||||
'gradeOriginalPossibleGraded',
|
||||
'nextPage',
|
||||
'prevPage',
|
||||
'showSuccess',
|
||||
],
|
||||
);
|
||||
|
||||
const gradeData = ({ grades }) => ({
|
||||
courseId: grades.courseId,
|
||||
filteredUsersCount: grades.filteredUsersCount,
|
||||
totalUsersCount: grades.totalUsersCount,
|
||||
gradeFormat: grades.gradeFormat,
|
||||
showSpinner: grades.showSpinner,
|
||||
gradeOverrideCurrentEarnedGradedOverride: grades.gradeOverrideCurrentEarnedGradedOverride,
|
||||
gradeOverrideHistoryError: grades.gradeOverrideHistoryError,
|
||||
gradeOverrideHistoryResults: grades.gradeOverrideHistoryResults,
|
||||
gradeOriginalEarnedGraded: grades.gradeOriginalEarnedGraded,
|
||||
gradeOriginalPossibleGraded: grades.gradeOriginalPossibleGraded,
|
||||
nextPage: grades.nextPage,
|
||||
prevPage: grades.prevPage,
|
||||
showSuccess: grades.showSuccess,
|
||||
});
|
||||
|
||||
export default StrictDict({
|
||||
bulkImportError,
|
||||
formatGradeOverrideForDisplay,
|
||||
@@ -300,7 +281,6 @@ export default StrictDict({
|
||||
subsectionGrade,
|
||||
|
||||
...simpleSelectors,
|
||||
gradeData,
|
||||
allGrades,
|
||||
bulkManagementHistoryEntries,
|
||||
getExampleSectionBreakdown,
|
||||
|
||||
@@ -1,9 +1,4 @@
|
||||
import {
|
||||
EMAIL_HEADING,
|
||||
FULL_NAME_HEADING,
|
||||
TOTAL_COURSE_GRADE_HEADING,
|
||||
USERNAME_HEADING,
|
||||
} from '../constants/grades';
|
||||
import { EMAIL_HEADING, TOTAL_COURSE_GRADE_HEADING, USERNAME_HEADING } from '../constants/grades';
|
||||
import { formatDateForDisplay } from '../actions/utils';
|
||||
import * as selectors from './grades';
|
||||
import exportedSelectors from './grades';
|
||||
@@ -184,7 +179,6 @@ describe('grades selectors', () => {
|
||||
describe('headingMapper', () => {
|
||||
const expectedHeaders = (subsectionLabels) => ([
|
||||
USERNAME_HEADING,
|
||||
FULL_NAME_HEADING,
|
||||
EMAIL_HEADING,
|
||||
...subsectionLabels,
|
||||
TOTAL_COURSE_GRADE_HEADING,
|
||||
|
||||
@@ -36,6 +36,10 @@ export const sectionOverrideHistoryUrl = (subsectionId, userId) => stringifyUrl(
|
||||
{ user_id: userId, history_record_limit: historyRecordLimit },
|
||||
);
|
||||
|
||||
export const instructorDashboardUrl = () => (
|
||||
`${getConfig().LMS_BASE_URL}/courses/${courseId}/instructor`
|
||||
);
|
||||
|
||||
export default StrictDict({
|
||||
getUrlPrefix,
|
||||
getBulkGradesUrl,
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import { messages as footerMessages } from '@edx/frontend-component-footer';
|
||||
import { messages as headerMessages } from '@edx/frontend-component-header';
|
||||
|
||||
import { getLocale, isRtl } from '@edx/frontend-platform/i18n';
|
||||
import arMessages from './messages/ar.json';
|
||||
import deMessages from './messages/de.json';
|
||||
import es419Messages from './messages/es_419.json';
|
||||
@@ -15,7 +13,7 @@ import ukMessages from './messages/uk.json';
|
||||
import zhcnMessages from './messages/zh_CN.json';
|
||||
// no need to import en messages-- they are in the defaultMessage field
|
||||
|
||||
const appMessages = {
|
||||
const messages = {
|
||||
ar: arMessages,
|
||||
'es-419': es419Messages,
|
||||
'fa-ir': faIRMessages,
|
||||
@@ -30,8 +28,25 @@ const appMessages = {
|
||||
uk: ukMessages,
|
||||
};
|
||||
|
||||
export default [
|
||||
footerMessages,
|
||||
headerMessages,
|
||||
appMessages,
|
||||
];
|
||||
export const getLocalizedSlash = () => {
|
||||
// For fractional grades
|
||||
// if we are in a LTR language, we want to use a forward slash.
|
||||
// If we are in a RTL language, we want to use a backslash instead
|
||||
if (isRtl(getLocale())) {
|
||||
return '\\';
|
||||
}
|
||||
return '/';
|
||||
};
|
||||
|
||||
export const getLocalizedPercentSign = () => {
|
||||
// LTR languages put the percent to the right of a number.
|
||||
// RTL languages put the percent sign to the left of the number.
|
||||
// We can place a non-printing unicode right-to-left marker next to the percent
|
||||
// sign to make it print to the left of the number if we are currently in a LTR language
|
||||
if (isRtl(getLocale())) {
|
||||
return '\u200f%';
|
||||
}
|
||||
return '%';
|
||||
};
|
||||
|
||||
export default messages;
|
||||
@@ -1,5 +1,5 @@
|
||||
import { isRtl } from '@edx/frontend-platform/i18n';
|
||||
import { getLocalizedSlash, getLocalizedPercentSign } from './utils';
|
||||
import { getLocalizedSlash, getLocalizedPercentSign } from './index';
|
||||
|
||||
jest.mock('@edx/frontend-platform/i18n', () => ({
|
||||
isRtl: jest.fn(),
|
||||
@@ -1,22 +0,0 @@
|
||||
import { getLocale, isRtl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
export const getLocalizedSlash = () => {
|
||||
// For fractional grades
|
||||
// if we are in a LTR language, we want to use a forward slash.
|
||||
// If we are in a RTL language, we want to use a backslash instead
|
||||
if (isRtl(getLocale())) {
|
||||
return '\\';
|
||||
}
|
||||
return '/';
|
||||
};
|
||||
|
||||
export const getLocalizedPercentSign = () => {
|
||||
// LTR languages put the percent to the right of a number.
|
||||
// RTL languages put the percent sign to the left of the number.
|
||||
// We can place a non-printing unicode right-to-left marker next to the percent
|
||||
// sign to make it print to the left of the number if we are currently in a LTR language
|
||||
if (isRtl(getLocale())) {
|
||||
return '\u200f%';
|
||||
}
|
||||
return '%';
|
||||
};
|
||||
@@ -10,14 +10,13 @@ import {
|
||||
mergeConfig,
|
||||
subscribe,
|
||||
} from '@edx/frontend-platform';
|
||||
import { messages as headerMessages } from '@edx/frontend-component-header';
|
||||
import { messages as footerMessages } from '@edx/frontend-component-footer';
|
||||
|
||||
import lightning from './lightning';
|
||||
|
||||
import messages from './i18n';
|
||||
import appMessages from './i18n';
|
||||
import App from './App';
|
||||
|
||||
subscribe(APP_READY, () => {
|
||||
lightning();
|
||||
ReactDOM.render(<App />, document.getElementById('root'));
|
||||
});
|
||||
|
||||
@@ -38,6 +37,10 @@ initialize({
|
||||
});
|
||||
},
|
||||
},
|
||||
messages,
|
||||
messages: [
|
||||
appMessages,
|
||||
headerMessages,
|
||||
footerMessages,
|
||||
],
|
||||
requireAuthenticatedUser: true,
|
||||
});
|
||||
|
||||
@@ -7,8 +7,10 @@ import {
|
||||
mergeConfig,
|
||||
subscribe,
|
||||
} from '@edx/frontend-platform';
|
||||
import { messages as headerMessages } from '@edx/frontend-component-header';
|
||||
import { messages as footerMessages } from '@edx/frontend-component-footer';
|
||||
|
||||
import messages from './i18n';
|
||||
import appMessages from './i18n';
|
||||
import App from './App';
|
||||
import '.';
|
||||
|
||||
@@ -21,6 +23,12 @@ jest.mock('@edx/frontend-platform', () => ({
|
||||
mergeConfig: jest.fn(),
|
||||
subscribe: jest.fn(),
|
||||
}));
|
||||
jest.mock('@edx/frontend-component-header', () => ({
|
||||
messages: ['some', 'messages'],
|
||||
}));
|
||||
jest.mock('@edx/frontend-component-footer', () => ({
|
||||
messages: ['some', 'messages'],
|
||||
}));
|
||||
jest.mock('./App', () => 'App');
|
||||
|
||||
describe('app registry', () => {
|
||||
@@ -42,7 +50,7 @@ describe('app registry', () => {
|
||||
});
|
||||
test('initialize is called with requireAuthenticatedUser, messages, and a config handler', () => {
|
||||
expect(initialize).toHaveBeenCalledWith({
|
||||
messages,
|
||||
messages: [appMessages, headerMessages, footerMessages],
|
||||
requireAuthenticatedUser: true,
|
||||
handlers: {
|
||||
config: expect.any(Function),
|
||||
|
||||
131
src/lightning.js
131
src/lightning.js
@@ -1,131 +0,0 @@
|
||||
module.exports = () => {
|
||||
if (process.env.DISPLAY_FEEDBACK_WIDGET === "true") {
|
||||
window.lightningjs ||
|
||||
(function (n) {
|
||||
var e = "lightningjs";
|
||||
function t(e, t) {
|
||||
var r, i, a, o, d, c;
|
||||
return (
|
||||
t && (t += (/\?/.test(t) ? "&" : "?") + "lv=1"),
|
||||
n[e] ||
|
||||
((r = window),
|
||||
(i = document),
|
||||
(a = e),
|
||||
(o = i.location.protocol),
|
||||
(d = "load"),
|
||||
(c = 0),
|
||||
(function () {
|
||||
n[a] = function () {
|
||||
var t = arguments,
|
||||
i = this,
|
||||
o = ++c,
|
||||
d = (i && i != r && i.id) || 0;
|
||||
function s() {
|
||||
return (s.id = o), n[a].apply(s, arguments);
|
||||
}
|
||||
return (
|
||||
(e.s = e.s || []).push([o, d, t]),
|
||||
(s.then = function (n, t, r) {
|
||||
var i = (e.fh[o] = e.fh[o] || []),
|
||||
a = (e.eh[o] = e.eh[o] || []),
|
||||
d = (e.ph[o] = e.ph[o] || []);
|
||||
return n && i.push(n), t && a.push(t), r && d.push(r), s;
|
||||
}),
|
||||
s
|
||||
);
|
||||
};
|
||||
var e = (n[a]._ = {});
|
||||
function s() {
|
||||
e.P(d), (e.w = 1), n[a]("_load");
|
||||
}
|
||||
(e.fh = {}),
|
||||
(e.eh = {}),
|
||||
(e.ph = {}),
|
||||
(e.l = t
|
||||
? t.replace(/^\/\//, ("https:" == o ? o : "http:") + "//")
|
||||
: t),
|
||||
(e.p = { 0: +new Date() }),
|
||||
(e.P = function (n) {
|
||||
e.p[n] = new Date() - e.p[0];
|
||||
}),
|
||||
e.w && s(),
|
||||
r.addEventListener
|
||||
? r.addEventListener(d, s, !1)
|
||||
: r.attachEvent("onload", s);
|
||||
var l = function () {
|
||||
function n() {
|
||||
return [
|
||||
"<!DOCTYPE ",
|
||||
o,
|
||||
"><",
|
||||
o,
|
||||
"><head></head><",
|
||||
t,
|
||||
"><",
|
||||
r,
|
||||
' src="',
|
||||
e.l,
|
||||
'"></',
|
||||
r,
|
||||
"></",
|
||||
t,
|
||||
"></",
|
||||
o,
|
||||
">",
|
||||
].join("");
|
||||
}
|
||||
var t = "body",
|
||||
r = "script",
|
||||
o = "html",
|
||||
d = i[t];
|
||||
if (!d) return setTimeout(l, 100);
|
||||
e.P(1);
|
||||
var c,
|
||||
s = i.createElement("div"),
|
||||
h = s.appendChild(i.createElement("div")),
|
||||
u = i.createElement("iframe");
|
||||
(s.style.display = "none"),
|
||||
(d.insertBefore(s, d.firstChild).id = "lightningjs-" + a),
|
||||
(u.frameBorder = "0"),
|
||||
(u.id = "lightningjs-frame-" + a),
|
||||
/MSIE[ ]+6/.test(navigator.userAgent) &&
|
||||
(u.src = "javascript:false"),
|
||||
(u.allowTransparency = "true"),
|
||||
h.appendChild(u);
|
||||
try {
|
||||
u.contentWindow.document.open();
|
||||
} catch (n) {
|
||||
(e.domain = i.domain),
|
||||
(c =
|
||||
"javascript:var d=document.open();d.domain='" +
|
||||
i.domain +
|
||||
"';"),
|
||||
(u.src = c + "void(0);");
|
||||
}
|
||||
try {
|
||||
var p = u.contentWindow.document;
|
||||
p.write(n()), p.close();
|
||||
} catch (e) {
|
||||
u.src =
|
||||
c +
|
||||
'd.write("' +
|
||||
n().replace(/"/g, String.fromCharCode(92) + '"') +
|
||||
'");d.close();';
|
||||
}
|
||||
e.P(2);
|
||||
};
|
||||
e.l && l();
|
||||
})()),
|
||||
(n[e].lv = "1"),
|
||||
n[e]
|
||||
);
|
||||
}
|
||||
var r = (window.lightningjs = t(e));
|
||||
(r.require = t), (r.modules = n);
|
||||
})({});
|
||||
window.usabilla_live = lightningjs.require(
|
||||
"usabilla_live",
|
||||
"//w.usabilla.com/a13a22d8ea20.js"
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -32,18 +32,6 @@ jest.mock('@edx/frontend-platform/i18n', () => {
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('@edx/frontend-component-header', () => ({
|
||||
messages: ['some', 'messages'],
|
||||
}));
|
||||
jest.mock('@edx/frontend-component-footer', () => ({
|
||||
messages: ['some', 'messages'],
|
||||
}));
|
||||
|
||||
jest.mock('@edx/paragon/icons', () => ({
|
||||
FilterAlt: 'FilterAlt',
|
||||
Close: 'Close',
|
||||
}));
|
||||
|
||||
jest.mock('@edx/paragon', () => jest.requireActual('testUtils').mockNestedComponents({
|
||||
Alert: 'Alert',
|
||||
ActionRow: 'ActionRow',
|
||||
@@ -80,7 +68,6 @@ jest.mock('@edx/paragon', () => jest.requireActual('testUtils').mockNestedCompon
|
||||
Hyperlink: 'Hyperlink',
|
||||
Icon: 'Icon',
|
||||
IconButton: 'IconButton',
|
||||
Input: 'Input',
|
||||
ModalDialog: {
|
||||
Body: 'ModalDialog.Body',
|
||||
CloseButton: 'ModalDialog.CloseButton',
|
||||
@@ -90,10 +77,8 @@ jest.mock('@edx/paragon', () => jest.requireActual('testUtils').mockNestedCompon
|
||||
},
|
||||
OverlayTrigger: 'OverlayTrigger',
|
||||
Row: 'Row',
|
||||
SearchField: 'SearchField',
|
||||
Spinner: 'Spinner',
|
||||
StatefulButton: 'StatefulButton',
|
||||
Toast: 'Toast',
|
||||
Spinner: 'Spinner',
|
||||
|
||||
useCheckboxSetValues: () => jest.fn().mockImplementation((values) => ([values, {
|
||||
add: jest.fn().mockName('useCheckboxSetValues.add'),
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
export const options = {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
timeZone: 'UTC',
|
||||
};
|
||||
export const timeOptions = {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
timeZone: 'UTC',
|
||||
timeZoneName: 'short',
|
||||
};
|
||||
|
||||
const formatDateForDisplay = (inputDate) => {
|
||||
const date = inputDate.toLocaleDateString('en-US', options);
|
||||
const time = inputDate.toLocaleTimeString('en-US', timeOptions);
|
||||
return `${date} at ${time}`;
|
||||
};
|
||||
|
||||
export default formatDateForDisplay;
|
||||
@@ -1,4 +1,3 @@
|
||||
/* eslint-disable import/prefer-default-export */
|
||||
export { default as StrictDict } from './StrictDict';
|
||||
export { default as keyStore } from './keyStore';
|
||||
export { default as formatDateForDisplay } from './formatDate';
|
||||
|
||||
Reference in New Issue
Block a user