Compare commits
82 Commits
open-relea
...
jkantor/ma
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
32be24cfe6 | ||
|
|
67789481fb | ||
|
|
543cd623e1 | ||
|
|
ba31b713e2 | ||
|
|
84fe2c6628 | ||
|
|
b87447b543 | ||
|
|
541a661dcc | ||
|
|
abc68f4224 | ||
|
|
14a5d4f849 | ||
|
|
b0e173dbba | ||
|
|
e1c8b01531 | ||
|
|
182dc396d5 | ||
|
|
e191aa9717 | ||
|
|
401916471b | ||
|
|
cee43bddcb | ||
|
|
6be4aac16e | ||
|
|
85607d7e97 | ||
|
|
af1b82bc1a | ||
|
|
7923f77d8b | ||
|
|
b84186ab0c | ||
|
|
0c4675cfa2 | ||
|
|
607b47be24 | ||
|
|
f6b2902914 | ||
|
|
b682b91f0a | ||
|
|
fcb4248521 | ||
|
|
23dfed82d0 | ||
|
|
d0ab0eca8f | ||
|
|
c690dde838 | ||
|
|
011737b492 | ||
|
|
524116a601 | ||
|
|
db5414e97f | ||
|
|
456edd453e | ||
|
|
ecceda2343 | ||
|
|
f5706635e0 | ||
|
|
933f6c0a6f | ||
|
|
229033b742 | ||
|
|
da447b12ed | ||
|
|
c7b5979067 | ||
|
|
8bf130b099 | ||
|
|
9d442b0edb | ||
|
|
84cdacd4e8 | ||
|
|
4fcc3f863f | ||
|
|
79679c23f2 | ||
|
|
9b2436991b | ||
|
|
c95f2d6b22 | ||
|
|
4f43e65f03 | ||
|
|
50bf7d236a | ||
|
|
d2723e5bc1 | ||
|
|
03fa143fc1 | ||
|
|
075846f869 | ||
|
|
1208d27d92 | ||
|
|
e345716bd4 | ||
|
|
2121a63c83 | ||
|
|
47cab71b3c | ||
|
|
2d8af2ec00 | ||
|
|
d55abbe91e | ||
|
|
a75f365bdd | ||
|
|
bbb7e895a5 | ||
|
|
bf70fd1450 | ||
|
|
af2ece8290 | ||
|
|
620827d772 | ||
|
|
c6a4685bf5 | ||
|
|
8dd2237f9c | ||
|
|
97c58157f8 | ||
|
|
ce093efba4 | ||
|
|
799ef5b8a1 | ||
|
|
f956351cf7 | ||
|
|
7772e21c6a | ||
|
|
f07a96ce58 | ||
|
|
f64bc8d4a6 | ||
|
|
134dabb710 | ||
|
|
65c25f00b6 | ||
|
|
31748e246e | ||
|
|
650be29ef9 | ||
|
|
b713ab5748 | ||
|
|
5fe80b4a52 | ||
|
|
9e04813d06 | ||
|
|
a0e1a60d23 | ||
|
|
68c7944dd5 | ||
|
|
f4f6e5551f | ||
|
|
ee99bfdaa4 | ||
|
|
318ce349fc |
2
.env
2
.env
@@ -30,3 +30,5 @@ ENTERPRISE_MARKETING_URL=''
|
||||
ENTERPRISE_MARKETING_UTM_SOURCE=''
|
||||
ENTERPRISE_MARKETING_UTM_CAMPAIGN=''
|
||||
ENTERPRISE_MARKETING_FOOTER_UTM_MEDIUM=''
|
||||
APP_ID=''
|
||||
MFE_CONFIG_API_URL=''
|
||||
|
||||
@@ -23,6 +23,7 @@ MARKETING_SITE_BASE_URL='http://localhost:18000'
|
||||
SUPPORT_URL='http://localhost:18000/support'
|
||||
CONTACT_URL='http://localhost:18000/contact'
|
||||
OPEN_SOURCE_URL='http://localhost:18000/openedx'
|
||||
ORDER_HISTORY_URL='http://localhost:1996/orders'
|
||||
TERMS_OF_SERVICE_URL='http://localhost:18000/terms-of-service'
|
||||
PRIVACY_POLICY_URL='http://localhost:18000/privacy-policy'
|
||||
FACEBOOK_URL='https://www.facebook.com'
|
||||
@@ -36,3 +37,5 @@ ENTERPRISE_MARKETING_URL='http://example.com'
|
||||
ENTERPRISE_MARKETING_UTM_SOURCE='example.com'
|
||||
ENTERPRISE_MARKETING_UTM_CAMPAIGN='example.com Referral'
|
||||
ENTERPRISE_MARKETING_FOOTER_UTM_MEDIUM='Footer'
|
||||
APP_ID=''
|
||||
MFE_CONFIG_API_URL=''
|
||||
|
||||
15
.eslintrc.js
15
.eslintrc.js
@@ -1,3 +1,4 @@
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
const { createConfig } = require('@edx/frontend-build');
|
||||
|
||||
const config = createConfig('eslint', {
|
||||
@@ -6,14 +7,22 @@ const config = createConfig('eslint', {
|
||||
'import/no-named-as-default-member': 'off',
|
||||
'import/no-self-import': 'off',
|
||||
'spaced-comment': ['error', 'always', { 'block': { 'exceptions': ['*'] } }],
|
||||
|
||||
// TOD: Remove this rule once we have a better way to handle this.
|
||||
'import/no-import-module-exports': 'off',
|
||||
'no-import-assign': 'off',
|
||||
'default-param-last': 'off',
|
||||
},
|
||||
overrides: [{
|
||||
files: ['*.test.js'], rules: { 'no-import-assign': 'off' },
|
||||
}],
|
||||
});
|
||||
|
||||
config.settings = {
|
||||
"import/resolver": {
|
||||
'import/resolver': {
|
||||
node: {
|
||||
paths: ["src", "node_modules"],
|
||||
extensions: [".js", ".jsx"],
|
||||
paths: ['src', 'node_modules'],
|
||||
extensions: ['.js', '.jsx'],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
2
.github/CODEOWNERS
vendored
2
.github/CODEOWNERS
vendored
@@ -3,4 +3,4 @@
|
||||
# These owners will be the default owners for everything in
|
||||
# the repo. Unless a later match takes precedence, they will
|
||||
# be requested for review when someone opens a pull request.
|
||||
* @edx/masters-devs-gta
|
||||
* @openedx/content-aurora
|
||||
|
||||
2
.github/pull_request_template.md
vendored
2
.github/pull_request_template.md
vendored
@@ -26,4 +26,4 @@ Collectively, these should be completed by reviewers of this PR:
|
||||
- [ ] I've tested the new functionality
|
||||
|
||||
|
||||
FYI: @edx/masters-devs-gta
|
||||
FYI: @openedx/content-aurora
|
||||
|
||||
19
.github/workflows/add-depr-ticket-to-depr-board.yml
vendored
Normal file
19
.github/workflows/add-depr-ticket-to-depr-board.yml
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
# Run the workflow that adds new tickets that are either:
|
||||
# - labelled "DEPR"
|
||||
# - title starts with "[DEPR]"
|
||||
# - body starts with "Proposal Date" (this is the first template field)
|
||||
# to the org-wide DEPR project board
|
||||
|
||||
name: Add newly created DEPR issues to the DEPR project board
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened]
|
||||
|
||||
jobs:
|
||||
routeissue:
|
||||
uses: openedx/.github/.github/workflows/add-depr-ticket-to-depr-board.yml@master
|
||||
secrets:
|
||||
GITHUB_APP_ID: ${{ secrets.GRAPHQL_AUTH_APP_ID }}
|
||||
GITHUB_APP_PRIVATE_KEY: ${{ secrets.GRAPHQL_AUTH_APP_PEM }}
|
||||
SLACK_BOT_TOKEN: ${{ secrets.SLACK_ISSUE_BOT_TOKEN }}
|
||||
20
.github/workflows/add-remove-label-on-comment.yml
vendored
Normal file
20
.github/workflows/add-remove-label-on-comment.yml
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
# This workflow runs when a comment is made on the ticket
|
||||
# If the comment starts with "label: " it tries to apply
|
||||
# the label indicated in rest of comment.
|
||||
# If the comment starts with "remove label: ", it tries
|
||||
# to remove the indicated label.
|
||||
# Note: Labels are allowed to have spaces and this script does
|
||||
# not parse spaces (as often a space is legitimate), so the command
|
||||
# "label: really long lots of words label" will apply the
|
||||
# label "really long lots of words label"
|
||||
|
||||
name: Allows for the adding and removing of labels via comment
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
|
||||
jobs:
|
||||
add_remove_labels:
|
||||
uses: openedx/.github/.github/workflows/add-remove-label-on-comment.yml@master
|
||||
|
||||
64
.github/workflows/ci.yml
vendored
Normal file
64
.github/workflows/ci.yml
vendored
Normal file
@@ -0,0 +1,64 @@
|
||||
name: node_js CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
branches:
|
||||
- '**'
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-20.04
|
||||
strategy:
|
||||
matrix:
|
||||
node: [16]
|
||||
npm: [8.5.x]
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Setup Nodejs
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: ${{ matrix.node }}
|
||||
|
||||
- name: Install npm 8.5.x
|
||||
run: npm install -g npm@${{ matrix.npm }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Unit Tests
|
||||
run: npm run test
|
||||
|
||||
- name: Validate Package Lock
|
||||
run: make validate-no-uncommitted-package-lock-changes
|
||||
|
||||
- name: Run Lint
|
||||
run: npm run lint
|
||||
|
||||
- name: Run Test
|
||||
run: npm run test
|
||||
|
||||
- name: Run Build
|
||||
run: npm run build
|
||||
|
||||
- name: Run Coverage
|
||||
uses: codecov/codecov-action@v2
|
||||
|
||||
- name: Send failure notification
|
||||
if: ${{ failure() }}
|
||||
uses: dawidd6/action-send-mail@v3
|
||||
with:
|
||||
server_address: email-smtp.us-east-1.amazonaws.com
|
||||
server_port: 465
|
||||
username: ${{secrets.EDX_SMTP_USERNAME}}
|
||||
password: ${{secrets.EDX_SMTP_PASSWORD}}
|
||||
subject: CI workflow failed in ${{github.repository}}
|
||||
to: masters-grades@edx.org
|
||||
from: github-actions <github-actions@edx.org>
|
||||
body: CI workflow in ${{github.repository}} failed! For details see "github.com/${{
|
||||
github.repository }}/actions/runs/${{ github.run_id }}"
|
||||
2
.github/workflows/commitlint.yml
vendored
2
.github/workflows/commitlint.yml
vendored
@@ -7,4 +7,4 @@ on:
|
||||
|
||||
jobs:
|
||||
commitlint:
|
||||
uses: edx/.github/.github/workflows/commitlint.yml@master
|
||||
uses: openedx/.github/.github/workflows/commitlint.yml@master
|
||||
|
||||
13
.github/workflows/lockfileversion-check.yml
vendored
Normal file
13
.github/workflows/lockfileversion-check.yml
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
#check package-lock file version
|
||||
|
||||
name: Lockfile Version check
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
version-check:
|
||||
uses: openedx/.github/.github/workflows/lockfileversion-check.yml@master
|
||||
33
.github/workflows/npm-publish.yml
vendored
Normal file
33
.github/workflows/npm-publish.yml
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
name: Release CI
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '*'
|
||||
|
||||
jobs:
|
||||
release:
|
||||
name: Release
|
||||
runs-on: ubuntu-20.04
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 12
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Create Build
|
||||
run: npm run build
|
||||
|
||||
- name: Release Package
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.SEMANTIC_RELEASE_GITHUB_TOKEN }}
|
||||
NPM_TOKEN: ${{ secrets.SEMANTIC_RELEASE_NPM_TOKEN }}
|
||||
run: npm semantic-release
|
||||
12
.github/workflows/self-assign-issue.yml
vendored
Normal file
12
.github/workflows/self-assign-issue.yml
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
# This workflow runs when a comment is made on the ticket
|
||||
# If the comment starts with "assign me" it assigns the author to the
|
||||
# ticket (case insensitive)
|
||||
|
||||
name: Assign comment author to ticket if they say "assign me"
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
|
||||
jobs:
|
||||
self_assign_by_comment:
|
||||
uses: openedx/.github/.github/workflows/self-assign-issue.yml@master
|
||||
12
.github/workflows/update-browserslist-db.yml
vendored
Normal file
12
.github/workflows/update-browserslist-db.yml
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
name: Update Browserslist DB
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 0 * * 1'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
update-browserslist:
|
||||
uses: openedx/.github/.github/workflows/update-browserslist-db.yml@master
|
||||
|
||||
secrets:
|
||||
requirements_bot_github_token: ${{ secrets.requirements_bot_github_token }}
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -5,6 +5,8 @@ npm-debug.log
|
||||
coverage
|
||||
|
||||
dist/
|
||||
src/i18n/transifex_input.json
|
||||
temp/babel-plugin-react-intl
|
||||
|
||||
### pyenv ###
|
||||
.python-version
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
.eslintignore
|
||||
.eslintrc.json
|
||||
.gitignore
|
||||
.travis.yml
|
||||
docker-compose.yml
|
||||
Dockerfile
|
||||
Makefile
|
||||
|
||||
28
.travis.yml
28
.travis.yml
@@ -1,28 +0,0 @@
|
||||
language: node_js
|
||||
node_js: 12
|
||||
notifications:
|
||||
email:
|
||||
recipients:
|
||||
- masters-grades@edx.org
|
||||
on_success: never
|
||||
on_failure: always
|
||||
webhooks: https://www.travisbuddy.com/
|
||||
on_success: never
|
||||
before_install:
|
||||
- npm install -g greenkeeper-lockfile@1.14.0
|
||||
install:
|
||||
- npm ci
|
||||
before_script: greenkeeper-lockfile-update
|
||||
after_script: greenkeeper-lockfile-upload
|
||||
script:
|
||||
- make validate-no-uncommitted-package-lock-changes
|
||||
- npm run lint
|
||||
- npm run test
|
||||
- npm run build
|
||||
after_success:
|
||||
- npm run travis-deploy-once "npm run semantic-release"
|
||||
- npm run coveralls
|
||||
env:
|
||||
global:
|
||||
- secure: bBLQZVw1aVUxB7GFNXGrdKeztyFrCCJusVgFcSuej9S4qmj9/jrVsEc9dEcH+BMS+b49+SvILoxzd6ZYLaRygQLzevnO1/dX596DeCKVK48PTTZRsNyafaSMCkxNKqEmRcA9hYL52xJJ5GpKo7ViWsFy8VFgUfZEJxQi8/lYbfQ1vlXRpo2LJfJh09v85roSXdQmajyGJ1Dz6elcwUX5B+BgXmIHizJXUMfFci61xTEZmgKtfeCiwFQA5pCvVMHBQhgySqT2N3eRESzRt2jAfAdcRKBYXS0rwKymdlL1ZF349Jm8xwtqm19Fwsut21181Lnn6FmccMWhQ7man3WH1xfT0ahmHNs1KJMyZcwRJd/gDfbd6iD3LB9Pt9hEQ00Qh/m7MYeahMxTEL9bp2TyILi8cTP91jeBUHCExCdv2jRrUQEnUS5vZUYRdM8CR2DLoLmNh3APndKzwgr5U8rh6RdhbQBJp97Hb/YYVrBiP2atLJAaYPY/xEQHK/YoXelQgiZ6wHBMV+tF/L0ZRn7KyVWdkbBKWfbEjRKbEJD9WD+V7HayMR81tm5CSqlrG8mTvSy2boIGiX14GV11ZEfMj5bjb6W41BW+QGqQerZvmwk/4ywe304X85PD0OBhIYPRzeLIi0Gt6lD1aOpVxgm4M03tdgYQzCPWRPq32CB+1IA=
|
||||
- secure: w1d/E+cc4+Bf017Jpp9YsKBzLSZw9sqKZGeM2tNrO6eJZbMJqfKTmfUrRw8BoLh1Z8YRkHF7RADDy3ln7XEdeAX3j9OoC3Cz0zN6iDX6TPcI461NuOIscJYb4tyFcuWm6FhgVlBAlo/BI3q+zqKwjfWuDaORpk6+haacCmvTe5V0vWhY+MYT7M+LfnKeKVzhI4magGt8jPTE21oziIFwCqCCjJc4+AmsWoWTzU0Q7Db0DZiJnLXFfXybLbkedAgJmcSgEGZCSpaZIOkX0/Lbazsz1Ky4KASfkrYT1Z5iKQ8TE3skmx1IIu+1egN8iBbdrY+NhvV24RkT+rpUvD7TBIHTrjQ5JYLe0kGjN70vG7YlKgjNSyTjkrEd7fCKpuIol3DVjBRz3tV5aCl0t/A8mIPqKyNI94MamWsExpqsxgcb9vBVno5caZvD8ZXNrGNqanB3MSoLGxZTLKif9u+AZfLnB3xtjaiJg3/BNoWaOBPlp/M6BvGIGHElwvLrAhUvl8wzrwJcQQWpmRMh0b6enr6Y7ox/mGGs7NBCT+CNKEsWeCfY4thZzgi6/GocXyqdTpXMkNSI1PDoPmi+vKafBd+7aAYbcUlJBTU6TAxyncln0tF2JF+ghTZ0v8nNzEQ9VmV4ddyoOHx6YnHvEcenWZGMROQnMCVifyDbaHpPbPI=
|
||||
@@ -1,8 +1,9 @@
|
||||
[main]
|
||||
host = https://www.transifex.com
|
||||
|
||||
[edx-platform.frontend-app-gradebook]
|
||||
[o:open-edx:p:edx-platform:r:frontend-app-gradebook]
|
||||
file_filter = src/i18n/messages/<lang>.json
|
||||
source_file = src/i18n/transifex_input.json
|
||||
source_lang = en
|
||||
type = KEYVALUEJSON
|
||||
type = KEYVALUEJSON
|
||||
|
||||
|
||||
15
Makefile
Executable file → Normal file
15
Makefile
Executable file → Normal file
@@ -1,9 +1,8 @@
|
||||
npm-install-%: ## install specified % npm package
|
||||
npm install $* --save-dev
|
||||
git add package.json
|
||||
|
||||
transifex_resource = frontend-app-gradebook
|
||||
transifex_langs = "ar,fr,es_419,zh_CN"
|
||||
export TRANSIFEX_RESOURCE = frontend-app-gradebook
|
||||
transifex_langs = "ar,de,es_419,fa_IR,fr,fr_CA,hi,it,pt,ru,uk,zh_CN"
|
||||
|
||||
transifex_utils = ./node_modules/.bin/transifex-utils.js
|
||||
i18n = ./src/i18n
|
||||
@@ -49,17 +48,17 @@ push_translations:
|
||||
# Pushing strings to Transifex...
|
||||
tx push -s
|
||||
# Fetching hashes from Transifex...
|
||||
./node_modules/reactifex/bash_scripts/get_hashed_strings.sh $(tx_url1)
|
||||
./node_modules/@edx/reactifex/bash_scripts/get_hashed_strings_v3.sh
|
||||
# Writing out comments to file...
|
||||
$(transifex_utils) $(transifex_temp) --comments
|
||||
$(transifex_utils) $(transifex_temp) --comments --v3-scripts-path
|
||||
# Pushing comments to Transifex...
|
||||
./node_modules/reactifex/bash_scripts/put_comments.sh $(tx_url2)
|
||||
./node_modules/@edx/reactifex/bash_scripts/put_comments_v3.sh
|
||||
|
||||
# Pulls translations from Transifex.
|
||||
pull_translations:
|
||||
tx pull -f --mode reviewed --language=$(transifex_langs)
|
||||
tx pull -t -f --mode reviewed --languages=$(transifex_langs)
|
||||
|
||||
# This target is used by Travis.
|
||||
# This target is used by CI.
|
||||
validate-no-uncommitted-package-lock-changes:
|
||||
# Checking for package-lock.json changes...
|
||||
git diff --exit-code package-lock.json
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
[](https://travis-ci.com/edx/frontend-app-gradebook) [](https://coveralls.io/github/edx/frontend-app-gradebook)
|
||||
[](https://travis-ci.com/edx/frontend-app-gradebook)
|
||||
[](https://app.codecov.io/gh/openedx/frontend-app-gradebook)
|
||||
[](@edx/frontend-app-gradebook)
|
||||
[](@edx/frontend-app-gradebook)
|
||||
[](@edx/frontend-app-gradebook)
|
||||
@@ -57,7 +58,7 @@ npm i --save @edx/frontend-app-gradebook
|
||||
|
||||
## Running the UI Standalone
|
||||
|
||||
To install the project please refer to the [`edX Developer Stack`](https://github.com/edx/devstack) instructions.
|
||||
To install the project please refer to the [`edX Developer Stack`](https://github.com/openedx/devstack) instructions.
|
||||
|
||||
The web application runs on port **1994**, so when you go to `http://localhost:1994/course-v1:edX+DemoX+Demo_Course` you should see the UI (assuming you have such a Demo Course in your devstack). Note that you always have to provide a course id to actually see a gradebook.
|
||||
|
||||
@@ -117,4 +118,4 @@ running gradebook container.
|
||||
|
||||
## Authentication with backend API services
|
||||
|
||||
See the [`@edx/frontend-auth`](https://github.com/edx/frontend-auth) repo for information about securing routes in your application that require user authentication.
|
||||
See the [`@edx/frontend-auth`](https://github.com/edx-unsupported/frontend-auth) repo for information about securing routes in your application that require user authentication.
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
# Travis Configuration
|
||||
|
||||
Your project might have different build requirements - however, this project's `.travis.yml` configuration is supposed to represent a good starting point.
|
||||
|
||||
## Node JS Version
|
||||
|
||||
The minimum `Node` and `npm` versions that edX supports is `8.9.3` and `5.5.1`, respectively.
|
||||
|
||||
## Caching node_modules
|
||||
|
||||
While [the `Travis` blog](https://blog.travis-ci.com/2016-11-21-travis-ci-now-supports-yarn) recommends
|
||||
|
||||
```yaml
|
||||
cache:
|
||||
directories:
|
||||
- node_modules
|
||||
```
|
||||
|
||||
this causes issues when testing different versions of `Node` because [`node_modules` will store the compiled native modules](https://stackoverflow.com/a/42523517/5225575).
|
||||
|
||||
Caching the `~/.npm` directory avoids storing these native modules.
|
||||
|
||||
## Notifications
|
||||
|
||||
This project uses a service called [`TravisBuddy`](https://www.travisbuddy.com/), which provides Travis build context within a PR via webhooks (configured only to add feedback for build failures).
|
||||
|
||||

|
||||
|
||||
## Installing `greenkeeper-lockfile`
|
||||
|
||||
As explained in [the `Greenkeeper` documentation](https://greenkeeper.io/docs.html#greenkeeper-step-by-step), `Greenkeeper` is a service that keeps track of your project's dependencies, and will, for example, automatically open PRs with an updated `package.json` file when the latest version of a dependency is a major version ahead of the existing dependency version in your `package.json` file.
|
||||
|
||||
This automated updating is great, but `Greenkeeper` does not update your `package-lock.json` file, just your `package.json` file. This makes sense, as the only way to update the `package-lock.json` file would be to run `npm install` when building your project, using the latest `package.json`, and then committing the updated `package-lock.json` file.
|
||||
|
||||
This is essentially what you have to do manually when `Greenkeeper` opens a PR - `git checkout` the branch, `npm install` locally, `git commit` the `package-lock.json` changes, and then `git push` those changes to the `Greenkeeper` branch on `origin`. It's fun probably only the first time, and even then it gets old, fast.
|
||||
|
||||
What [`greenkeeper-lockfile`](https://github.com/greenkeeperio/greenkeeper-lockfile) does is that it automates the previous steps as part of the build process.
|
||||
|
||||
It will
|
||||
* Check that the branch is a `Greenkeeper` branch
|
||||
* Update the lockfile
|
||||
* Push a commit with the updated lockfile back to the Greenkeeper branch
|
||||
|
||||
This is why it's important to install `greenkeeper-lockfile` in the `before_install` step, and since it's used exclusively only in the Travis Build, why it's not part of the package's dependencies.
|
||||
|
||||
## Scripts
|
||||
|
||||
Most of the `script`s are self-explanatory - you probably want to fail a build if there are linting violations, or if any tests don't pass, or if it cannot compile your files.
|
||||
|
||||
However, there are a couple additional `script`s that might seem less self-explanatory.
|
||||
|
||||
### What the heck is `make validate-no-uncommitted-package-lock-changes`?
|
||||
|
||||
There are only two requirements for a good `make target` name
|
||||
|
||||
1. Definitely make it really verbose so people can't remember what it's called
|
||||
2. Definitely don't not use a double-negative
|
||||
|
||||
What `make validate-no-uncommitted-package-lock-changes` does is `git diff`s for any `package-lock.json` file changes in your project. It's important to remember that all build `script`s are executed in Travis *after* the `install` step (aka post-`npm install`).
|
||||
|
||||
This is important because `npm` uses the pinned dependencies in your `package-lock.json` file to build the `node_modules` directory. However, the dependencies defined within the `package.json` file can be modified manually, for example, to become misaligned with the dependencies defined within the `package-lock.json`. So when `npm install` executes, the `package-lock.json` file will be updated to mirror the modified `package.json` changes.
|
||||
|
||||
However, when these changes surface within a Travis build, this indicates differing dependency expectations between the committed `package.json` file and the `package-lock.json` file, which is a good reason to fail a build.
|
||||
|
||||
### What is this `npm run is-es5` check?
|
||||
|
||||
This project outputs production files to the `dist` folder. The `npm script`, `npm run is-es5`, checks the JavaScript files in the `dist` folder to make sure that they are `ES5`-compliant.
|
||||
|
||||
This check is important because `ES5` JavaScript has [greater browser compatibility](http://kangax.github.io/compat-table/es5/) than [`ES2015+`](http://kangax.github.io/compat-table/es6/) - particularly for `IE11`.
|
||||
|
||||
### `deploy` step
|
||||
|
||||
How your project deploys will probably differ between the cookie cutter and your own application.
|
||||
|
||||
For demonstrational purposes, the cookie cutter deploys to GitHub pages using [`Travis`'s GitHub pages configuration](https://docs.travis-ci.com/user/deployment/pages/).
|
||||
|
||||
Your application might deploy to an `S3` bucket or to `npm`.
|
||||
40
documentation/CI.md
Executable file
40
documentation/CI.md
Executable file
@@ -0,0 +1,40 @@
|
||||
# CI Configuration
|
||||
|
||||
Your project might have different build requirements - however, this project's `.github/ci.yml` configuration is supposed to represent a good starting point.
|
||||
|
||||
## Node JS Version
|
||||
|
||||
The minimum `Node` and `npm` versions that edX supports is `8.9.3` and `5.5.1`, respectively.
|
||||
|
||||
## Scripts
|
||||
|
||||
Most of the `script`s are self-explanatory - you probably want to fail a build if there are linting violations, or if any tests don't pass, or if it cannot compile your files.
|
||||
|
||||
However, there are a couple additional `script`s that might seem less self-explanatory.
|
||||
|
||||
### What the heck is `make validate-no-uncommitted-package-lock-changes`?
|
||||
|
||||
There are only two requirements for a good `make target` name
|
||||
|
||||
1. Definitely make it really verbose so people can't remember what it's called
|
||||
2. Definitely don't not use a double-negative
|
||||
|
||||
What `make validate-no-uncommitted-package-lock-changes` does is `git diff`s for any `package-lock.json` file changes in your project. It's important to remember that all build `script`s are executed in CI *after* the `install` step (aka post-`npm install`).
|
||||
|
||||
This is important because `npm` uses the pinned dependencies in your `package-lock.json` file to build the `node_modules` directory. However, the dependencies defined within the `package.json` file can be modified manually, for example, to become misaligned with the dependencies defined within the `package-lock.json`. So when `npm install` executes, the `package-lock.json` file will be updated to mirror the modified `package.json` changes.
|
||||
|
||||
However, when these changes surface within a CI build, this indicates differing dependency expectations between the committed `package.json` file and the `package-lock.json` file, which is a good reason to fail a build.
|
||||
|
||||
### What is this `npm run is-es5` check?
|
||||
|
||||
This project outputs production files to the `dist` folder. The `npm script`, `npm run is-es5`, checks the JavaScript files in the `dist` folder to make sure that they are `ES5`-compliant.
|
||||
|
||||
This check is important because `ES5` JavaScript has [greater browser compatibility](http://kangax.github.io/compat-table/es5/) than [`ES2015+`](http://kangax.github.io/compat-table/es6/) - particularly for `IE11`.
|
||||
|
||||
### `deploy` step
|
||||
|
||||
How your project deploys will probably differ between the cookie cutter and your own application.
|
||||
|
||||
For demonstrational purposes, the cookie cutter deploys to GitHub pages using [ GitHUb CI ].
|
||||
|
||||
Your application might deploy to an `S3` bucket or to `npm`.
|
||||
@@ -5,7 +5,7 @@ Context
|
||||
=======
|
||||
|
||||
The LMS Grades API exposes a set of Gradebook-related endpoints:
|
||||
https://github.com/edx/edx-platform/blob/master/lms/djangoapps/grades/api/v1/gradebook_views.py
|
||||
https://github.com/openedx/edx-platform/blob/master/lms/djangoapps/grades/api/v1/gradebook_views.py
|
||||
The ``bulk-update`` endpoint defined therein allows for the creation/modification of subsection
|
||||
grades for multiple users and sections in a single request. This allows clients of the API to limit
|
||||
the number of network requests made and to more easily manage client-side data. Moreover,
|
||||
@@ -13,7 +13,7 @@ the course grade updates that occur during calls to this API are synchronous - t
|
||||
is completed before a response is given to the client.
|
||||
|
||||
For decisions made about the implementation of this API, see:
|
||||
https://github.com/edx/edx-platform/blob/master/lms/djangoapps/grades/docs/decisions/0001-gradebook-api.rst
|
||||
https://github.com/openedx/edx-platform/blob/master/lms/djangoapps/grades/docs/decisions/0001-gradebook-api.rst
|
||||
|
||||
Decision
|
||||
========
|
||||
|
||||
@@ -11,5 +11,6 @@ module.exports = createConfig('jest', {
|
||||
coveragePathIgnorePatterns: [
|
||||
'src/segment.js',
|
||||
'src/postcss.config.js',
|
||||
'testUtils', // don't unit test jest mocking tools
|
||||
],
|
||||
});
|
||||
|
||||
68194
package-lock.json
generated
68194
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
47
package.json
Executable file → Normal file
47
package.json
Executable file → Normal file
@@ -1,14 +1,13 @@
|
||||
{
|
||||
"name": "@edx/frontend-app-gradebook",
|
||||
"version": "1.4.47",
|
||||
"version": "1.6.1",
|
||||
"description": "edx editable gradebook-ui to manipulate grade overrides on subsections",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/edx/frontend-app-gradebook.git"
|
||||
"url": "git+https://github.com/openedx/frontend-app-gradebook.git"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "fedx-scripts webpack",
|
||||
"coveralls": "cat ./coverage/lcov.info | coveralls",
|
||||
"is-es5": "es-check es5 ./dist/*.js",
|
||||
"i18n_extract": "BABEL_ENV=i18n fedx-scripts babel src --quiet > /dev/null",
|
||||
"lint": "fedx-scripts eslint --ext .jsx,.js src/",
|
||||
@@ -17,20 +16,24 @@
|
||||
"semantic-release": "semantic-release",
|
||||
"start": "fedx-scripts webpack-dev-server --progress",
|
||||
"test": "TZ=GMT fedx-scripts jest --coverage --passWithNoTests",
|
||||
"watch-tests": "jest --watch",
|
||||
"travis-deploy-once": "travis-deploy-once"
|
||||
"watch-tests": "jest --watch"
|
||||
},
|
||||
"author": "edX",
|
||||
"license": "AGPL-3.0",
|
||||
"homepage": "https://github.com/edx/frontend-app-gradebook#readme",
|
||||
"homepage": "https://github.com/openedx/frontend-app-gradebook#readme",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"browserslist": [
|
||||
"extends @edx/browserslist-config"
|
||||
],
|
||||
"dependencies": {
|
||||
"@edx/brand": "npm:@edx/brand-edx.org@^1.3.2",
|
||||
"@edx/frontend-component-footer": "10.1.1",
|
||||
"@edx/frontend-platform": "1.9.5",
|
||||
"@edx/paragon": "14.16.4",
|
||||
"@edx/brand": "npm:@edx/brand-openedx@^1.2.0",
|
||||
"@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",
|
||||
"@fortawesome/free-brands-svg-icons": "^5.11.2",
|
||||
"@fortawesome/free-solid-svg-icons": "^5.11.2",
|
||||
@@ -44,13 +47,13 @@
|
||||
"enzyme-to-json": "^3.6.2",
|
||||
"font-awesome": "4.7.0",
|
||||
"history": "4.10.1",
|
||||
"node-sass": "^4.14.1",
|
||||
"prop-types": "15.7.2",
|
||||
"query-string": "6.13.0",
|
||||
"react": "16.13.1",
|
||||
"react-dom": "16.13.1",
|
||||
"react": "16.14.0",
|
||||
"react-dom": "16.14.0",
|
||||
"react-helmet": "^6.1.0",
|
||||
"react-intl": "^2.9.0",
|
||||
"react-redux": "^5.1.1",
|
||||
"react-redux": "^7.1.1",
|
||||
"react-router": "5.2.0",
|
||||
"react-router-dom": "5.2.0",
|
||||
"react-router-redux": "^5.0.0-alpha.9",
|
||||
@@ -60,25 +63,25 @@
|
||||
"redux-logger": "3.0.6",
|
||||
"redux-thunk": "2.3.0",
|
||||
"regenerator-runtime": "^0.13.7",
|
||||
"util": "^0.12.3",
|
||||
"sass": "^1.49.0",
|
||||
"whatwg-fetch": "^2.0.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@edx/frontend-build": "5.5.2",
|
||||
"axios": "0.21.1",
|
||||
"@edx/browserslist-config": "^1.1.1",
|
||||
"@edx/frontend-build": "^12.4.15",
|
||||
"@testing-library/react": "^12.1.0",
|
||||
"axios": "0.21.2",
|
||||
"axios-mock-adapter": "^1.17.0",
|
||||
"codecov": "^3.6.1",
|
||||
"enzyme-adapter-react-16": "^1.14.0",
|
||||
"es-check": "^2.3.0",
|
||||
"fetch-mock": "^6.5.2",
|
||||
"husky": "2.7.0",
|
||||
"identity-obj-proxy": "^3.0.0",
|
||||
"jest": "24.9.0",
|
||||
"react-dev-utils": "^5.0.3",
|
||||
"jest": "29.3.1",
|
||||
"react-dev-utils": "^12.0.1",
|
||||
"react-test-renderer": "^16.10.1",
|
||||
"reactifex": "1.1.1",
|
||||
"redux-mock-store": "^1.5.3",
|
||||
"semantic-release": "^17.2.3",
|
||||
"travis-deploy-once": "^5.0.11"
|
||||
"semantic-release": "^19.0.3"
|
||||
}
|
||||
}
|
||||
|
||||
44
src/App.jsx
44
src/App.jsx
@@ -1,36 +1,36 @@
|
||||
import React from 'react';
|
||||
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
|
||||
import { Provider } from 'react-redux';
|
||||
import { IntlProvider } from 'react-intl';
|
||||
|
||||
import { AppProvider } from '@edx/frontend-platform/react';
|
||||
|
||||
import Footer from '@edx/frontend-component-footer';
|
||||
import Header from '@edx/frontend-component-header';
|
||||
|
||||
import { routePath } from 'data/constants/app';
|
||||
import store from 'data/store';
|
||||
import GradebookPage from 'containers/GradebookPage';
|
||||
import EdxHeader from 'components/EdxHeader';
|
||||
import './App.scss';
|
||||
import Head from './head/Head';
|
||||
|
||||
const App = () => (
|
||||
<IntlProvider locale="en">
|
||||
<Provider store={store}>
|
||||
<Router>
|
||||
<div>
|
||||
<EdxHeader />
|
||||
<main>
|
||||
<Switch>
|
||||
<Route
|
||||
exact
|
||||
path={routePath}
|
||||
component={GradebookPage}
|
||||
/>
|
||||
</Switch>
|
||||
</main>
|
||||
<Footer logo={process.env.LOGO_POWERED_BY_OPEN_EDX_URL_SVG} />
|
||||
</div>
|
||||
</Router>
|
||||
</Provider>
|
||||
</IntlProvider>
|
||||
<AppProvider store={store}>
|
||||
<Head />
|
||||
<Router>
|
||||
<div>
|
||||
<Header />
|
||||
<main>
|
||||
<Switch>
|
||||
<Route
|
||||
exact
|
||||
path={routePath}
|
||||
component={GradebookPage}
|
||||
/>
|
||||
</Switch>
|
||||
</main>
|
||||
<Footer logo={process.env.LOGO_POWERED_BY_OPEN_EDX_URL_SVG} />
|
||||
</div>
|
||||
</Router>
|
||||
</AppProvider>
|
||||
);
|
||||
|
||||
export default App;
|
||||
|
||||
@@ -9,6 +9,7 @@ $fa-font-path: "~font-awesome/fonts";
|
||||
|
||||
$input-focus-box-shadow: $input-box-shadow; // hack to get upgrade to paragon 4.0.0 to work
|
||||
|
||||
@import "~@edx/frontend-component-header/dist/index";
|
||||
@import "~@edx/frontend-component-footer/dist/_footer";
|
||||
|
||||
@import "./components/GradesView/GradesView";
|
||||
|
||||
@@ -2,28 +2,25 @@ import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
|
||||
import { Provider } from 'react-redux';
|
||||
import { IntlProvider } from 'react-intl';
|
||||
import { AppProvider } from '@edx/frontend-platform/react';
|
||||
|
||||
import Footer from '@edx/frontend-component-footer';
|
||||
import Header from '@edx/frontend-component-header';
|
||||
|
||||
import { routePath } from 'data/constants/app';
|
||||
import store from 'data/store';
|
||||
import GradebookPage from 'containers/GradebookPage';
|
||||
import EdxHeader from 'components/EdxHeader';
|
||||
|
||||
import App from './App';
|
||||
import Head from './head/Head';
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
BrowserRouter: () => 'BrowserRouter',
|
||||
Route: () => 'Route',
|
||||
Switch: () => 'Switch',
|
||||
}));
|
||||
jest.mock('react-redux', () => ({
|
||||
Provider: () => 'Provider',
|
||||
}));
|
||||
jest.mock('react-intl', () => ({
|
||||
IntlProvider: () => 'IntlProvider',
|
||||
jest.mock('@edx/frontend-platform/react', () => ({
|
||||
AppProvider: () => 'AppProvider',
|
||||
}));
|
||||
jest.mock('data/constants/app', () => ({
|
||||
routePath: '/:courseId',
|
||||
@@ -31,7 +28,7 @@ jest.mock('data/constants/app', () => ({
|
||||
jest.mock('@edx/frontend-component-footer', () => 'Footer');
|
||||
jest.mock('data/store', () => 'testStore');
|
||||
jest.mock('containers/GradebookPage', () => 'GradebookPage');
|
||||
jest.mock('components/EdxHeader', () => 'EdxHeader');
|
||||
jest.mock('@edx/frontend-component-header', () => 'Header');
|
||||
|
||||
const logo = 'fakeLogo.png';
|
||||
let el;
|
||||
@@ -45,28 +42,25 @@ describe('App router component', () => {
|
||||
beforeEach(() => {
|
||||
process.env.LOGO_POWERED_BY_OPEN_EDX_URL_SVG = logo;
|
||||
el = shallow(<App />);
|
||||
router = el.childAt(0).childAt(0);
|
||||
router = el.childAt(1);
|
||||
});
|
||||
describe('IntlProvider', () => {
|
||||
test('outer-wrapper component', () => {
|
||||
expect(el.type()).toBe(IntlProvider);
|
||||
});
|
||||
test('"en" locale', () => {
|
||||
expect(el.props().locale).toEqual('en');
|
||||
describe('AppProvider', () => {
|
||||
test('AppProvider is the parent component, passed the redux store props', () => {
|
||||
expect(el.type()).toBe(AppProvider);
|
||||
expect(el.props().store).toEqual(store);
|
||||
});
|
||||
});
|
||||
describe('Provider, inside IntlProvider', () => {
|
||||
test('first child, passed the redux store props', () => {
|
||||
expect(el.childAt(0).type()).toBe(Provider);
|
||||
expect(el.childAt(0).props().store).toEqual(store);
|
||||
describe('Head', () => {
|
||||
test('first child of AppProvider', () => {
|
||||
expect(el.childAt(0).type()).toBe(Head);
|
||||
});
|
||||
});
|
||||
describe('Router', () => {
|
||||
test('first child of Provider', () => {
|
||||
test('second child of AppProvider', () => {
|
||||
expect(router.type()).toBe(Router);
|
||||
});
|
||||
test('EdxHeader is above/outside-of the routing', () => {
|
||||
expect(router.childAt(0).childAt(0).type()).toBe(EdxHeader);
|
||||
test('Header is above/outside-of the routing', () => {
|
||||
expect(router.childAt(0).childAt(0).type()).toBe(Header);
|
||||
expect(router.childAt(0).childAt(1).type()).toBe('main');
|
||||
});
|
||||
test('Routing - GradebookPage is only route', () => {
|
||||
|
||||
@@ -1,27 +1,24 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`App router component snapshot 1`] = `
|
||||
<IntlProvider
|
||||
locale="en"
|
||||
<AppProvider
|
||||
store="testStore"
|
||||
>
|
||||
<Provider
|
||||
store="testStore"
|
||||
>
|
||||
<BrowserRouter>
|
||||
<div>
|
||||
<EdxHeader />
|
||||
<main>
|
||||
<Switch>
|
||||
<Route
|
||||
component="GradebookPage"
|
||||
exact={true}
|
||||
path="/:courseId"
|
||||
/>
|
||||
</Switch>
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
</BrowserRouter>
|
||||
</Provider>
|
||||
</IntlProvider>
|
||||
<Head />
|
||||
<BrowserRouter>
|
||||
<div>
|
||||
<Header />
|
||||
<main>
|
||||
<Switch>
|
||||
<Route
|
||||
component="GradebookPage"
|
||||
exact={true}
|
||||
path="/:courseId"
|
||||
/>
|
||||
</Switch>
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
</BrowserRouter>
|
||||
</AppProvider>
|
||||
`;
|
||||
|
||||
@@ -3,7 +3,7 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { Table } from '@edx/paragon';
|
||||
import { DataTable } from '@edx/paragon';
|
||||
|
||||
import { bulkManagementColumns } from 'data/constants/app';
|
||||
import selectors from 'data/selectors';
|
||||
@@ -30,14 +30,13 @@ export const mapHistoryRows = ({
|
||||
export const HistoryTable = ({
|
||||
bulkManagementHistory,
|
||||
}) => (
|
||||
<>
|
||||
<Table
|
||||
data={bulkManagementHistory.map(mapHistoryRows)}
|
||||
hasFixedColumnWidths
|
||||
columns={bulkManagementColumns}
|
||||
className="table-striped"
|
||||
/>
|
||||
</>
|
||||
<DataTable
|
||||
data={bulkManagementHistory.map(mapHistoryRows)}
|
||||
hasFixedColumnWidths
|
||||
columns={bulkManagementColumns}
|
||||
className="table-striped"
|
||||
itemCount={bulkManagementHistory.length}
|
||||
/>
|
||||
);
|
||||
HistoryTable.defaultProps = {
|
||||
bulkManagementHistory: [],
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/* eslint-disable import/no-named-as-default */
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { Table } from '@edx/paragon';
|
||||
import { DataTable } from '@edx/paragon';
|
||||
|
||||
import selectors from 'data/selectors';
|
||||
import { bulkManagementColumns } from 'data/constants/app';
|
||||
@@ -9,13 +9,12 @@ import { bulkManagementColumns } from 'data/constants/app';
|
||||
import ResultsSummary from './ResultsSummary';
|
||||
import { HistoryTable, mapStateToProps } from './HistoryTable';
|
||||
|
||||
jest.mock('@edx/paragon', () => ({ DataTable: () => 'DataTable' }));
|
||||
|
||||
jest.mock('@edx/frontend-platform/i18n', () => ({
|
||||
defineMessages: m => m,
|
||||
FormattedMessage: () => 'FormattedMessage',
|
||||
}));
|
||||
jest.mock('@edx/paragon', () => ({
|
||||
Table: () => 'Table',
|
||||
}));
|
||||
jest.mock('data/selectors', () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
@@ -62,7 +61,7 @@ describe('HistoryTable', () => {
|
||||
describe('history table', () => {
|
||||
let table;
|
||||
beforeEach(() => {
|
||||
table = el.find(Table);
|
||||
table = el.find(DataTable);
|
||||
});
|
||||
describe('data (from bulkManagementHistory.map(this.formatHistoryRow)', () => {
|
||||
const fieldAssertions = [
|
||||
|
||||
@@ -42,78 +42,77 @@ Array [
|
||||
`;
|
||||
|
||||
exports[`HistoryTable component snapshot snapshot - loads formatted table 1`] = `
|
||||
<Fragment>
|
||||
<Table
|
||||
className="table-striped"
|
||||
columns={
|
||||
Array [
|
||||
Object {
|
||||
"columnSortable": false,
|
||||
"key": "filename",
|
||||
"label": "Gradebook",
|
||||
"width": "col-5",
|
||||
},
|
||||
Object {
|
||||
"columnSortable": false,
|
||||
"key": "resultsSummary",
|
||||
"label": "Download Summary",
|
||||
"width": "col",
|
||||
},
|
||||
Object {
|
||||
"columnSortable": false,
|
||||
"key": "user",
|
||||
"label": "Who",
|
||||
"width": "col-1",
|
||||
},
|
||||
Object {
|
||||
"columnSortable": false,
|
||||
"key": "timeUploaded",
|
||||
"label": "When",
|
||||
"width": "col",
|
||||
},
|
||||
]
|
||||
}
|
||||
data={
|
||||
Array [
|
||||
Object {
|
||||
"filename": <span
|
||||
className="wrap-text-in-cell"
|
||||
>
|
||||
blue.png
|
||||
</span>,
|
||||
"resultsSummary": <ResultsSummary
|
||||
courseId="Da Bu Dee"
|
||||
rowId={12}
|
||||
text="Da ba daa"
|
||||
/>,
|
||||
"timeUploaded": "65",
|
||||
"user": <span
|
||||
className="wrap-text-in-cell"
|
||||
>
|
||||
Eifel
|
||||
</span>,
|
||||
},
|
||||
Object {
|
||||
"filename": <span
|
||||
className="wrap-text-in-cell"
|
||||
>
|
||||
allStar.jpg
|
||||
</span>,
|
||||
"resultsSummary": <ResultsSummary
|
||||
courseId="rockstar"
|
||||
rowId={2}
|
||||
text="all that glitters is gold"
|
||||
/>,
|
||||
"timeUploaded": "2000s?",
|
||||
"user": <span
|
||||
className="wrap-text-in-cell"
|
||||
>
|
||||
Smashmouth
|
||||
</span>,
|
||||
},
|
||||
]
|
||||
}
|
||||
hasFixedColumnWidths={true}
|
||||
/>
|
||||
</Fragment>
|
||||
<DataTable
|
||||
className="table-striped"
|
||||
columns={
|
||||
Array [
|
||||
Object {
|
||||
"Header": "Gradebook",
|
||||
"accessor": "filename",
|
||||
"columnSortable": false,
|
||||
"width": "col-5",
|
||||
},
|
||||
Object {
|
||||
"Header": "Download Summary",
|
||||
"accessor": "resultsSummary",
|
||||
"columnSortable": false,
|
||||
"width": "col",
|
||||
},
|
||||
Object {
|
||||
"Header": "Who",
|
||||
"accessor": "user",
|
||||
"columnSortable": false,
|
||||
"width": "col-1",
|
||||
},
|
||||
Object {
|
||||
"Header": "When",
|
||||
"accessor": "timeUploaded",
|
||||
"columnSortable": false,
|
||||
"width": "col",
|
||||
},
|
||||
]
|
||||
}
|
||||
data={
|
||||
Array [
|
||||
Object {
|
||||
"filename": <span
|
||||
className="wrap-text-in-cell"
|
||||
>
|
||||
blue.png
|
||||
</span>,
|
||||
"resultsSummary": <ResultsSummary
|
||||
courseId="Da Bu Dee"
|
||||
rowId={12}
|
||||
text="Da ba daa"
|
||||
/>,
|
||||
"timeUploaded": "65",
|
||||
"user": <span
|
||||
className="wrap-text-in-cell"
|
||||
>
|
||||
Eifel
|
||||
</span>,
|
||||
},
|
||||
Object {
|
||||
"filename": <span
|
||||
className="wrap-text-in-cell"
|
||||
>
|
||||
allStar.jpg
|
||||
</span>,
|
||||
"resultsSummary": <ResultsSummary
|
||||
courseId="rockstar"
|
||||
rowId={2}
|
||||
text="all that glitters is gold"
|
||||
/>,
|
||||
"timeUploaded": "2000s?",
|
||||
"user": <span
|
||||
className="wrap-text-in-cell"
|
||||
>
|
||||
Smashmouth
|
||||
</span>,
|
||||
},
|
||||
]
|
||||
}
|
||||
hasFixedColumnWidths={true}
|
||||
itemCount={2}
|
||||
/>
|
||||
`;
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`AssignmentFilter component render snapshot 1`] = `
|
||||
<div
|
||||
className="student-filters"
|
||||
>
|
||||
<SelectGroup
|
||||
disabled={false}
|
||||
id="assignment"
|
||||
label="Assignment"
|
||||
onChange={[MockFunction]}
|
||||
options={
|
||||
Array [
|
||||
<option
|
||||
value=""
|
||||
>
|
||||
All
|
||||
</option>,
|
||||
<option
|
||||
value="label1"
|
||||
>
|
||||
label1
|
||||
:
|
||||
sLabel1
|
||||
</option>,
|
||||
<option
|
||||
value="label2"
|
||||
>
|
||||
label2
|
||||
:
|
||||
sLabel2
|
||||
</option>,
|
||||
<option
|
||||
value="label3"
|
||||
>
|
||||
label3
|
||||
:
|
||||
sLabel3
|
||||
</option>,
|
||||
<option
|
||||
value="label4"
|
||||
>
|
||||
label4
|
||||
:
|
||||
sLabel4
|
||||
</option>,
|
||||
]
|
||||
}
|
||||
value="test-label"
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
@@ -1,44 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`AssignmentFilter Component snapshots basic snapshot 1`] = `
|
||||
<div
|
||||
className="student-filters"
|
||||
>
|
||||
<SelectGroup
|
||||
disabled={false}
|
||||
id="assignment"
|
||||
label={
|
||||
<FormattedMessage
|
||||
defaultMessage="Assignment"
|
||||
description="Assignment filter select label in Gradebook Filters"
|
||||
id="gradebook.GradebookFilters.assignmentFilterLabel"
|
||||
/>
|
||||
}
|
||||
onChange={[MockFunction handleChange]}
|
||||
options={
|
||||
Array [
|
||||
<option
|
||||
value=""
|
||||
>
|
||||
All
|
||||
</option>,
|
||||
<option
|
||||
value="assgN1"
|
||||
>
|
||||
assgN1
|
||||
:
|
||||
subLabel1
|
||||
</option>,
|
||||
<option
|
||||
value="assgN2"
|
||||
>
|
||||
assgN2
|
||||
:
|
||||
subLabel2
|
||||
</option>,
|
||||
]
|
||||
}
|
||||
value="assgN1"
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
33
src/components/GradebookFilters/AssignmentFilter/hooks.js
Normal file
33
src/components/GradebookFilters/AssignmentFilter/hooks.js
Normal file
@@ -0,0 +1,33 @@
|
||||
import {
|
||||
selectors,
|
||||
actions,
|
||||
thunkActions,
|
||||
} from 'data/redux/hooks';
|
||||
|
||||
export const useAssignmentFilterData = ({
|
||||
updateQueryParams,
|
||||
}) => {
|
||||
const assignmentFilterOptions = selectors.filters.useSelectableAssignmentLabels();
|
||||
const selectedAssignmentLabel = selectors.filters.useSelectedAssignmentLabel() || '';
|
||||
|
||||
const updateAssignmentFilter = actions.filters.useUpdateAssignment();
|
||||
const conditionalFetch = thunkActions.grades.useFetchGradesIfAssignmentGradeFiltersSet();
|
||||
|
||||
const handleChange = ({ target: { value: assignment } }) => {
|
||||
const selectedFilterOption = assignmentFilterOptions.find(
|
||||
({ label }) => label === assignment,
|
||||
);
|
||||
const { type, id } = selectedFilterOption || {};
|
||||
updateAssignmentFilter({ label: assignment, type, id });
|
||||
updateQueryParams({ assignment: id });
|
||||
conditionalFetch();
|
||||
};
|
||||
|
||||
return {
|
||||
handleChange,
|
||||
selectedAssignmentLabel,
|
||||
assignmentFilterOptions,
|
||||
};
|
||||
};
|
||||
|
||||
export default useAssignmentFilterData;
|
||||
@@ -0,0 +1,88 @@
|
||||
import { selectors, actions, thunkActions } from 'data/redux/hooks';
|
||||
|
||||
import useAssignmentFilterData from './hooks';
|
||||
|
||||
jest.mock('data/redux/hooks', () => ({
|
||||
selectors: {
|
||||
filters: {
|
||||
useSelectableAssignmentLabels: jest.fn(),
|
||||
useSelectedAssignmentLabel: jest.fn(),
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
filters: { useUpdateAssignment: jest.fn() },
|
||||
},
|
||||
thunkActions: {
|
||||
grades: { useFetchGradesIfAssignmentGradeFiltersSet: jest.fn() },
|
||||
},
|
||||
}));
|
||||
|
||||
let out;
|
||||
const testKey = 'test-key';
|
||||
const event = { target: { value: testKey } };
|
||||
const testId = 'test-id';
|
||||
const testType = 'test-type';
|
||||
|
||||
const testLabel = { label: testKey, id: testId, type: testType };
|
||||
const selectableAssignmentLabels = [
|
||||
{ label: 'some' },
|
||||
{ label: 'test' },
|
||||
{ label: 'labels' },
|
||||
testLabel,
|
||||
];
|
||||
const selectedAssignmentLabel = 'test-assignment-label';
|
||||
selectors.filters.useSelectableAssignmentLabels.mockReturnValue(selectableAssignmentLabels);
|
||||
selectors.filters.useSelectedAssignmentLabel.mockReturnValue(selectedAssignmentLabel);
|
||||
|
||||
const updateAssignment = jest.fn();
|
||||
const fetch = jest.fn();
|
||||
actions.filters.useUpdateAssignment.mockReturnValue(updateAssignment);
|
||||
thunkActions.grades.useFetchGradesIfAssignmentGradeFiltersSet.mockReturnValue(fetch);
|
||||
|
||||
const updateQueryParams = jest.fn();
|
||||
|
||||
describe('useAssignmentFilterData hook', () => {
|
||||
beforeEach(() => {
|
||||
out = useAssignmentFilterData({ updateQueryParams });
|
||||
});
|
||||
describe('behavior', () => {
|
||||
it('initializes redux hooks', () => {
|
||||
expect(selectors.filters.useSelectableAssignmentLabels).toHaveBeenCalledWith();
|
||||
expect(selectors.filters.useSelectedAssignmentLabel).toHaveBeenCalledWith();
|
||||
expect(actions.filters.useUpdateAssignment).toHaveBeenCalledWith();
|
||||
expect(thunkActions.grades.useFetchGradesIfAssignmentGradeFiltersSet)
|
||||
.toHaveBeenCalledWith();
|
||||
});
|
||||
});
|
||||
describe('output', () => {
|
||||
describe('handleEvent', () => {
|
||||
beforeEach(() => {
|
||||
out.handleChange(event);
|
||||
});
|
||||
it('updates assignment filter with selected filter', () => {
|
||||
expect(updateAssignment).toHaveBeenCalledWith(testLabel);
|
||||
});
|
||||
it('updates queryParams', () => {
|
||||
expect(updateQueryParams).toHaveBeenCalledWith({ assignment: testId });
|
||||
});
|
||||
it('updates assignment filter with only label if no match', () => {
|
||||
out.handleChange({ target: { value: 'no-match' } });
|
||||
expect(updateAssignment).toHaveBeenCalledWith({ label: 'no-match' });
|
||||
});
|
||||
it('calls conditional fetch', () => {
|
||||
expect(fetch).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
it('passes selectedAssignmentLabel from hook', () => {
|
||||
expect(out.selectedAssignmentLabel).toEqual(selectedAssignmentLabel);
|
||||
});
|
||||
test('selectedAssignmentLabel is empty string if not set', () => {
|
||||
selectors.filters.useSelectedAssignmentLabel.mockReturnValue(undefined);
|
||||
out = useAssignmentFilterData({ updateQueryParams });
|
||||
expect(out.selectedAssignmentLabel).toEqual('');
|
||||
});
|
||||
it('passes assignmentFilterOptions from hook', () => {
|
||||
expect(out.assignmentFilterOptions).toEqual(selectableAssignmentLabels);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,98 +1,44 @@
|
||||
/* eslint-disable react/sort-comp, react/button-has-type */
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import selectors from 'data/selectors';
|
||||
import actions from 'data/actions';
|
||||
import thunkActions from 'data/thunkActions';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import messages from '../messages';
|
||||
import SelectGroup from '../SelectGroup';
|
||||
import useAssignmentFilterData from './hooks';
|
||||
|
||||
const { fetchGradesIfAssignmentGradeFiltersSet } = thunkActions.grades;
|
||||
|
||||
export class AssignmentFilter extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.handleChange = this.handleChange.bind(this);
|
||||
}
|
||||
|
||||
handleChange(event) {
|
||||
const assignment = event.target.value;
|
||||
const selectedFilterOption = this.props.assignmentFilterOptions.find(
|
||||
({ label }) => label === assignment,
|
||||
);
|
||||
const { type, id } = selectedFilterOption || {};
|
||||
const typedValue = { label: assignment, type, id };
|
||||
this.props.updateAssignmentFilter(typedValue);
|
||||
this.props.updateQueryParams({ assignment: id });
|
||||
this.props.fetchGradesIfAssignmentGradeFiltersSet();
|
||||
}
|
||||
|
||||
get options() {
|
||||
const mapper = ({ label, subsectionLabel }) => (
|
||||
<option key={label} value={label}>
|
||||
{label}: {subsectionLabel}
|
||||
</option>
|
||||
);
|
||||
return ([
|
||||
<option key="0" value="">All</option>,
|
||||
...this.props.assignmentFilterOptions.map(mapper),
|
||||
]);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="student-filters">
|
||||
<SelectGroup
|
||||
id="assignment"
|
||||
label={<FormattedMessage {...messages.assignment} />}
|
||||
value={this.props.selectedAssignment}
|
||||
onChange={this.handleChange}
|
||||
disabled={this.props.assignmentFilterOptions.length === 0}
|
||||
options={this.options}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
AssignmentFilter.defaultProps = {
|
||||
assignmentFilterOptions: [],
|
||||
selectedAssignment: '',
|
||||
const AssignmentFilter = ({ updateQueryParams }) => {
|
||||
const {
|
||||
handleChange,
|
||||
selectedAssignmentLabel,
|
||||
assignmentFilterOptions,
|
||||
} = useAssignmentFilterData({ updateQueryParams });
|
||||
const { formatMessage } = useIntl();
|
||||
const filterOptions = assignmentFilterOptions.map(({ label, subsectionLabel }) => (
|
||||
<option key={label} value={label}>
|
||||
{label}: {subsectionLabel}
|
||||
</option>
|
||||
));
|
||||
return (
|
||||
<div className="student-filters">
|
||||
<SelectGroup
|
||||
id="assignment"
|
||||
label={formatMessage(messages.assignment)}
|
||||
value={selectedAssignmentLabel}
|
||||
onChange={handleChange}
|
||||
disabled={assignmentFilterOptions.length === 0}
|
||||
options={[
|
||||
<option key="0" value="">All</option>,
|
||||
...filterOptions,
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
AssignmentFilter.propTypes = {
|
||||
updateQueryParams: PropTypes.func.isRequired,
|
||||
// redux
|
||||
assignmentFilterOptions: PropTypes.arrayOf(PropTypes.shape({
|
||||
label: PropTypes.string,
|
||||
subsectionLabel: PropTypes.string,
|
||||
type: PropTypes.string,
|
||||
id: PropTypes.string,
|
||||
})),
|
||||
selectedAssignment: PropTypes.string,
|
||||
fetchGradesIfAssignmentGradeFiltersSet: PropTypes.func.isRequired,
|
||||
updateAssignmentFilter: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export const mapStateToProps = (state) => {
|
||||
const { filters } = selectors;
|
||||
return {
|
||||
assignmentFilterOptions: filters.selectableAssignmentLabels(state),
|
||||
selectedAssignment: filters.selectedAssignmentLabel(state),
|
||||
selectedAssignmentType: filters.assignmentType(state),
|
||||
selectedCohort: filters.cohort(state),
|
||||
selectedTrack: filters.track(state),
|
||||
};
|
||||
};
|
||||
|
||||
export const mapDispatchToProps = {
|
||||
updateAssignmentFilter: actions.filters.update.assignment,
|
||||
fetchGradesIfAssignmentGradeFiltersSet,
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(AssignmentFilter);
|
||||
export default AssignmentFilter;
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import SelectGroup from '../SelectGroup';
|
||||
import useAssignmentFilterData from './hooks';
|
||||
import AssignmentFilter from '.';
|
||||
|
||||
jest.mock('../SelectGroup', () => 'SelectGroup');
|
||||
jest.mock('./hooks', () => ({ __esModule: true, default: jest.fn() }));
|
||||
|
||||
const handleChange = jest.fn();
|
||||
const selectedAssignmentLabel = 'test-label';
|
||||
const assignmentFilterOptions = [
|
||||
{ label: 'label1', subsectionLabel: 'sLabel1' },
|
||||
{ label: 'label2', subsectionLabel: 'sLabel2' },
|
||||
{ label: 'label3', subsectionLabel: 'sLabel3' },
|
||||
{ label: 'label4', subsectionLabel: 'sLabel4' },
|
||||
];
|
||||
useAssignmentFilterData.mockReturnValue({
|
||||
handleChange,
|
||||
selectedAssignmentLabel,
|
||||
assignmentFilterOptions,
|
||||
});
|
||||
|
||||
const updateQueryParams = jest.fn();
|
||||
|
||||
let el;
|
||||
describe('AssignmentFilter component', () => {
|
||||
beforeAll(() => {
|
||||
el = shallow(<AssignmentFilter updateQueryParams={updateQueryParams} />);
|
||||
});
|
||||
describe('behavior', () => {
|
||||
it('initializes hooks', () => {
|
||||
expect(useAssignmentFilterData).toHaveBeenCalledWith({ updateQueryParams });
|
||||
expect(useIntl).toHaveBeenCalledWith();
|
||||
});
|
||||
});
|
||||
describe('render', () => {
|
||||
test('snapshot', () => {
|
||||
expect(el).toMatchSnapshot();
|
||||
});
|
||||
test('filter options', () => {
|
||||
const { options } = el.find(SelectGroup).props();
|
||||
expect(options.length).toEqual(5);
|
||||
const testOption = assignmentFilterOptions[0];
|
||||
const optionProps = options[1].props;
|
||||
expect(optionProps.value).toEqual(testOption.label);
|
||||
expect(optionProps.children.join(''))
|
||||
.toEqual(`${testOption.label}: ${testOption.subsectionLabel}`);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,162 +0,0 @@
|
||||
import React from 'react';
|
||||
import { mount, shallow } from 'enzyme';
|
||||
|
||||
import selectors from 'data/selectors';
|
||||
import actions from 'data/actions';
|
||||
import { fetchGradesIfAssignmentGradeFiltersSet } from 'data/thunkActions/grades';
|
||||
import {
|
||||
AssignmentFilter,
|
||||
mapStateToProps,
|
||||
mapDispatchToProps,
|
||||
} from '.';
|
||||
|
||||
jest.mock('data/thunkActions/grades', () => ({
|
||||
updateGradesIfAssignmentGradeFiltersSet: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('data/selectors', () => ({
|
||||
/** Mocking to use passed state for validation purposes */
|
||||
filters: {
|
||||
selectableAssignmentLabels: jest.fn(() => ([{
|
||||
label: 'assigNment',
|
||||
subsectionLabel: 'subsection',
|
||||
type: 'assignMentType',
|
||||
id: 'subsectionId',
|
||||
}])),
|
||||
selectedAssignmentLabel: jest.fn(() => 'assigNment'),
|
||||
assignmentType: jest.fn(() => 'assignMentType'),
|
||||
cohort: jest.fn(() => 'COhort'),
|
||||
track: jest.fn(() => 'traCK'),
|
||||
},
|
||||
}));
|
||||
|
||||
describe('AssignmentFilter', () => {
|
||||
let props = {
|
||||
assignmentFilterOptions: [
|
||||
{
|
||||
label: 'assgN1',
|
||||
subsectionLabel: 'subLabel1',
|
||||
type: 'assgn_Type1',
|
||||
id: 'assgn_iD1',
|
||||
},
|
||||
{
|
||||
label: 'assgN2',
|
||||
subsectionLabel: 'subLabel2',
|
||||
type: 'assgn_Type2',
|
||||
id: 'assgn_iD2',
|
||||
},
|
||||
],
|
||||
selectedAssignment: 'assgN1',
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
props = {
|
||||
...props,
|
||||
updateQueryParams: jest.fn(),
|
||||
fetchGradesIfAssignmentGradeFiltersSet: jest.fn(),
|
||||
updateAssignmentFilter: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
describe('Component', () => {
|
||||
describe('behavior', () => {
|
||||
describe('handleChange', () => {
|
||||
let el;
|
||||
const newAssgn = 'assgN1';
|
||||
const event = { target: { value: newAssgn } };
|
||||
const selected = props.assignmentFilterOptions[0];
|
||||
beforeEach(() => {
|
||||
el = mount(<AssignmentFilter {...props} />);
|
||||
el.instance().handleChange(event);
|
||||
});
|
||||
it('calls props.updateAssignmentFilter with selection', () => {
|
||||
expect(props.updateAssignmentFilter).toHaveBeenCalledWith({
|
||||
label: newAssgn,
|
||||
type: selected.type,
|
||||
id: selected.id,
|
||||
});
|
||||
});
|
||||
it('calls props.updateQueryParams with selected assignment id',
|
||||
() => {
|
||||
expect(props.updateQueryParams).toHaveBeenCalledWith({
|
||||
assignment: selected.id,
|
||||
});
|
||||
});
|
||||
it('calls props.fetchGradesIfAssignmentGradeFiltersSet', () => {
|
||||
const method = props.fetchGradesIfAssignmentGradeFiltersSet;
|
||||
expect(method).toHaveBeenCalledWith();
|
||||
});
|
||||
describe('no selected option', () => {
|
||||
const value = 'fake';
|
||||
beforeEach(() => {
|
||||
el = mount(<AssignmentFilter {...props} />);
|
||||
el.instance().handleChange({ target: { value } });
|
||||
});
|
||||
it('calls props.updateAssignmentFilter with selection', () => {
|
||||
expect(props.updateAssignmentFilter).toHaveBeenCalledWith({
|
||||
label: value,
|
||||
type: undefined,
|
||||
id: undefined,
|
||||
});
|
||||
});
|
||||
it('calls props.updateQueryParams with selected assignment id',
|
||||
() => {
|
||||
expect(props.updateQueryParams).toHaveBeenCalledWith({
|
||||
assignment: undefined,
|
||||
});
|
||||
});
|
||||
it('calls props.fetchGradesIfAssignmentGradeFiltersSet', () => {
|
||||
const method = props.fetchGradesIfAssignmentGradeFiltersSet;
|
||||
expect(method).toHaveBeenCalledWith();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('snapshots', () => {
|
||||
test('basic snapshot', () => {
|
||||
const el = shallow(<AssignmentFilter {...props} />);
|
||||
el.instance().handleChange = jest.fn().mockName('handleChange');
|
||||
expect(el.instance().render()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('mapStateToProps', () => {
|
||||
const state = {
|
||||
filters: {
|
||||
assignment: { label: 'assigNment' },
|
||||
assignmentType: 'assignMentType',
|
||||
cohort: 'COhort',
|
||||
track: 'traCK',
|
||||
},
|
||||
};
|
||||
describe('assignmentFilterOptions', () => {
|
||||
it('is selected from filters.selectableAssignmentLabels', () => {
|
||||
expect(
|
||||
mapStateToProps(state).assignmentFilterOptions,
|
||||
).toEqual(
|
||||
selectors.filters.selectableAssignmentLabels(state),
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('selectedAssignment', () => {
|
||||
it('is selected from filters.selectedAssignmentLabel', () => {
|
||||
expect(
|
||||
mapStateToProps(state).selectedAssignment,
|
||||
).toEqual(
|
||||
selectors.filters.selectedAssignmentLabel(state),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('mapDispatchToProps', () => {
|
||||
test('updateAssignmentFilter', () => {
|
||||
expect(mapDispatchToProps.updateAssignmentFilter).toEqual(
|
||||
actions.filters.update.assignment,
|
||||
);
|
||||
});
|
||||
test('fetchGradesIfAsssignmentGradeFiltersSet', () => {
|
||||
const prop = mapDispatchToProps.fetchGradesIfAssignmentGradeFiltersSet;
|
||||
expect(prop).toEqual(fetchGradesIfAssignmentGradeFiltersSet);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,67 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`AssignmentFilter component render with selected assignment snapshot 1`] = `
|
||||
<div
|
||||
className="grade-filter-inputs"
|
||||
>
|
||||
<PercentGroup
|
||||
disabled={false}
|
||||
id="assignmentGradeMin"
|
||||
label="Min Grade"
|
||||
onChange={[MockFunction]}
|
||||
value={23}
|
||||
/>
|
||||
<PercentGroup
|
||||
disabled={false}
|
||||
id="assignmentGradeMax"
|
||||
label="Max Grade"
|
||||
onChange={[MockFunction]}
|
||||
value={300}
|
||||
/>
|
||||
<div
|
||||
className="grade-filter-action"
|
||||
>
|
||||
<Button
|
||||
disabled={false}
|
||||
name="assignmentGradeMinMax"
|
||||
type="submit"
|
||||
variant="outline-secondary"
|
||||
>
|
||||
Apply
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`AssignmentFilter component render without selected assignment snapshot 1`] = `
|
||||
<div
|
||||
className="grade-filter-inputs"
|
||||
>
|
||||
<PercentGroup
|
||||
disabled={true}
|
||||
id="assignmentGradeMin"
|
||||
label="Min Grade"
|
||||
onChange={[MockFunction]}
|
||||
value={23}
|
||||
/>
|
||||
<PercentGroup
|
||||
disabled={true}
|
||||
id="assignmentGradeMax"
|
||||
label="Max Grade"
|
||||
onChange={[MockFunction]}
|
||||
value={300}
|
||||
/>
|
||||
<div
|
||||
className="grade-filter-action"
|
||||
>
|
||||
<Button
|
||||
disabled={true}
|
||||
name="assignmentGradeMinMax"
|
||||
type="submit"
|
||||
variant="outline-secondary"
|
||||
>
|
||||
Apply
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -1,95 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`AssignmentGradeFilter Component snapshots buttons and groups disabled if no selected assignment 1`] = `
|
||||
<div
|
||||
className="grade-filter-inputs"
|
||||
>
|
||||
<PercentGroup
|
||||
disabled={true}
|
||||
id="assignmentGradeMin"
|
||||
label={
|
||||
<FormattedMessage
|
||||
defaultMessage="Min Grade"
|
||||
description="Min-grade filter select label in Gradebook Filters"
|
||||
id="gradebook.GradebookFilters.minGradeFilterLabel"
|
||||
/>
|
||||
}
|
||||
onChange={[MockFunction handleSetMin]}
|
||||
value="2"
|
||||
/>
|
||||
<PercentGroup
|
||||
disabled={true}
|
||||
id="assignmentGradeMax"
|
||||
label={
|
||||
<FormattedMessage
|
||||
defaultMessage="Max Grade"
|
||||
description="Max-grade filter select label in Gradebook Filters"
|
||||
id="gradebook.GradebookFilters.maxGradeFilterLabel"
|
||||
/>
|
||||
}
|
||||
onChange={[MockFunction handleSetMax]}
|
||||
value="98"
|
||||
/>
|
||||
<div
|
||||
className="grade-filter-action"
|
||||
>
|
||||
<ForwardRef
|
||||
active={false}
|
||||
disabled={true}
|
||||
name="assignmentGradeMinMax"
|
||||
onClick={[MockFunction handleSubmit]}
|
||||
type="submit"
|
||||
variant="outline-secondary"
|
||||
>
|
||||
Apply
|
||||
</ForwardRef>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`AssignmentGradeFilter Component snapshots smoke test 1`] = `
|
||||
<div
|
||||
className="grade-filter-inputs"
|
||||
>
|
||||
<PercentGroup
|
||||
disabled={false}
|
||||
id="assignmentGradeMin"
|
||||
label={
|
||||
<FormattedMessage
|
||||
defaultMessage="Min Grade"
|
||||
description="Min-grade filter select label in Gradebook Filters"
|
||||
id="gradebook.GradebookFilters.minGradeFilterLabel"
|
||||
/>
|
||||
}
|
||||
onChange={[MockFunction handleSetMin]}
|
||||
value="2"
|
||||
/>
|
||||
<PercentGroup
|
||||
disabled={false}
|
||||
id="assignmentGradeMax"
|
||||
label={
|
||||
<FormattedMessage
|
||||
defaultMessage="Max Grade"
|
||||
description="Max-grade filter select label in Gradebook Filters"
|
||||
id="gradebook.GradebookFilters.maxGradeFilterLabel"
|
||||
/>
|
||||
}
|
||||
onChange={[MockFunction handleSetMax]}
|
||||
value="98"
|
||||
/>
|
||||
<div
|
||||
className="grade-filter-action"
|
||||
>
|
||||
<ForwardRef
|
||||
active={false}
|
||||
disabled={false}
|
||||
name="assignmentGradeMinMax"
|
||||
onClick={[MockFunction handleSubmit]}
|
||||
type="submit"
|
||||
variant="outline-secondary"
|
||||
>
|
||||
Apply
|
||||
</ForwardRef>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -0,0 +1,36 @@
|
||||
/* eslint-disable react/sort-comp, react/button-has-type */
|
||||
import { selectors, actions, thunkActions } from 'data/redux/hooks';
|
||||
|
||||
const useAssignmentGradeFilterData = ({ updateQueryParams }) => {
|
||||
const localAssignmentLimits = selectors.app.useAssignmentGradeLimits();
|
||||
const selectedAssignment = selectors.filters.useSelectedAssignmentLabel();
|
||||
const fetchGrades = thunkActions.grades.useFetchGrades();
|
||||
const setFilter = actions.app.useSetLocalFilter();
|
||||
const updateAssignmentLimits = actions.filters.useUpdateAssignmentLimits();
|
||||
|
||||
const handleSubmit = () => {
|
||||
updateAssignmentLimits(localAssignmentLimits);
|
||||
fetchGrades();
|
||||
updateQueryParams(localAssignmentLimits);
|
||||
};
|
||||
|
||||
const handleSetMax = ({ target: { value } }) => {
|
||||
setFilter({ assignmentGradeMax: value });
|
||||
};
|
||||
|
||||
const handleSetMin = ({ target: { value } }) => {
|
||||
setFilter({ assignmentGradeMin: value });
|
||||
};
|
||||
|
||||
const { assignmentGradeMax, assignmentGradeMin } = localAssignmentLimits;
|
||||
return {
|
||||
assignmentGradeMin,
|
||||
assignmentGradeMax,
|
||||
selectedAssignment,
|
||||
handleSetMax,
|
||||
handleSetMin,
|
||||
handleSubmit,
|
||||
};
|
||||
};
|
||||
|
||||
export default useAssignmentGradeFilterData;
|
||||
@@ -0,0 +1,81 @@
|
||||
import { selectors, actions, thunkActions } from 'data/redux/hooks';
|
||||
|
||||
import useAssignmentGradeFilterData from './hooks';
|
||||
|
||||
jest.mock('data/redux/hooks', () => ({
|
||||
selectors: {
|
||||
app: { useAssignmentGradeLimits: jest.fn() },
|
||||
filters: { useSelectedAssignmentLabel: jest.fn() },
|
||||
},
|
||||
actions: {
|
||||
app: { useSetLocalFilter: jest.fn() },
|
||||
filters: { useUpdateAssignmentLimits: jest.fn() },
|
||||
},
|
||||
thunkActions: {
|
||||
grades: { useFetchGrades: jest.fn() },
|
||||
},
|
||||
}));
|
||||
|
||||
let out;
|
||||
|
||||
const assignmentGradeLimits = { assignmentGradeMax: 200, assignmentGradeMin: 3 };
|
||||
const selectedAssignmentLabel = 'test-assignment-label';
|
||||
selectors.app.useAssignmentGradeLimits.mockReturnValue(assignmentGradeLimits);
|
||||
selectors.filters.useSelectedAssignmentLabel.mockReturnValue(selectedAssignmentLabel);
|
||||
|
||||
const setLocalFilter = jest.fn();
|
||||
const updateAssignmentLimits = jest.fn();
|
||||
const fetch = jest.fn();
|
||||
actions.app.useSetLocalFilter.mockReturnValue(setLocalFilter);
|
||||
actions.filters.useUpdateAssignmentLimits.mockReturnValue(updateAssignmentLimits);
|
||||
thunkActions.grades.useFetchGrades.mockReturnValue(fetch);
|
||||
|
||||
const testValue = 42;
|
||||
|
||||
const updateQueryParams = jest.fn();
|
||||
|
||||
describe('useAssignmentFilterData hook', () => {
|
||||
beforeEach(() => {
|
||||
out = useAssignmentGradeFilterData({ updateQueryParams });
|
||||
});
|
||||
describe('behavior', () => {
|
||||
it('initializes redux hooks', () => {
|
||||
expect(selectors.app.useAssignmentGradeLimits).toHaveBeenCalledWith();
|
||||
expect(selectors.filters.useSelectedAssignmentLabel).toHaveBeenCalledWith();
|
||||
expect(actions.app.useSetLocalFilter).toHaveBeenCalledWith();
|
||||
expect(actions.filters.useUpdateAssignmentLimits).toHaveBeenCalledWith();
|
||||
expect(thunkActions.grades.useFetchGrades).toHaveBeenCalledWith();
|
||||
});
|
||||
});
|
||||
describe('output', () => {
|
||||
describe('handleSubmit', () => {
|
||||
beforeEach(() => {
|
||||
out.handleSubmit();
|
||||
});
|
||||
it('updates assignment limits filter', () => {
|
||||
expect(updateAssignmentLimits).toHaveBeenCalledWith(assignmentGradeLimits);
|
||||
});
|
||||
it('updates queryParams', () => {
|
||||
expect(updateQueryParams).toHaveBeenCalledWith(assignmentGradeLimits);
|
||||
});
|
||||
it('calls conditional fetch', () => {
|
||||
expect(fetch).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
test('handleSetMax sets assignmentGradeMax', () => {
|
||||
out.handleSetMax({ target: { value: testValue } });
|
||||
expect(setLocalFilter).toHaveBeenCalledWith({ assignmentGradeMax: testValue });
|
||||
});
|
||||
test('handleSetMin sets assignmentGradeMin', () => {
|
||||
out.handleSetMin({ target: { value: testValue } });
|
||||
expect(setLocalFilter).toHaveBeenCalledWith({ assignmentGradeMin: testValue });
|
||||
});
|
||||
it('passes selectedAssignment from hook', () => {
|
||||
expect(out.selectedAssignment).toEqual(selectedAssignmentLabel);
|
||||
});
|
||||
it('passes assignmentGradeMin and assignmentGradeMax from hook', () => {
|
||||
expect(out.assignmentGradeMax).toEqual(assignmentGradeLimits.assignmentGradeMax);
|
||||
expect(out.assignmentGradeMin).toEqual(assignmentGradeLimits.assignmentGradeMin);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,103 +1,56 @@
|
||||
/* eslint-disable react/sort-comp, react/button-has-type */
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Button } from '@edx/paragon';
|
||||
|
||||
import selectors from 'data/selectors';
|
||||
import actions from 'data/actions';
|
||||
import thunkActions from 'data/thunkActions';
|
||||
|
||||
import useAssignmentGradeFilterData from './hooks';
|
||||
import messages from '../messages';
|
||||
import PercentGroup from '../PercentGroup';
|
||||
|
||||
export class AssignmentGradeFilter extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.handleSubmit = this.handleSubmit.bind(this);
|
||||
this.handleSetMax = this.handleSetMax.bind(this);
|
||||
this.handleSetMin = this.handleSetMin.bind(this);
|
||||
}
|
||||
|
||||
handleSubmit() {
|
||||
this.props.updateAssignmentLimits(this.props.localAssignmentLimits);
|
||||
this.props.fetchGrades();
|
||||
this.props.updateQueryParams(this.props.localAssignmentLimits);
|
||||
}
|
||||
|
||||
handleSetMax({ target: { value } }) {
|
||||
this.props.setFilter({ assignmentGradeMax: value });
|
||||
}
|
||||
|
||||
handleSetMin({ target: { value } }) {
|
||||
this.props.setFilter({ assignmentGradeMin: value });
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
localAssignmentLimits: { assignmentGradeMax, assignmentGradeMin },
|
||||
} = this.props;
|
||||
return (
|
||||
<div className="grade-filter-inputs">
|
||||
<PercentGroup
|
||||
id="assignmentGradeMin"
|
||||
label={<FormattedMessage {...messages.minGrade} />}
|
||||
value={assignmentGradeMin}
|
||||
disabled={!this.props.selectedAssignment}
|
||||
onChange={this.handleSetMin}
|
||||
/>
|
||||
<PercentGroup
|
||||
id="assignmentGradeMax"
|
||||
label={<FormattedMessage {...messages.maxGrade} />}
|
||||
value={assignmentGradeMax}
|
||||
disabled={!this.props.selectedAssignment}
|
||||
onChange={this.handleSetMax}
|
||||
/>
|
||||
<div className="grade-filter-action">
|
||||
<Button
|
||||
type="submit"
|
||||
variant="outline-secondary"
|
||||
name="assignmentGradeMinMax"
|
||||
disabled={!this.props.selectedAssignment}
|
||||
onClick={this.handleSubmit}
|
||||
>
|
||||
Apply
|
||||
</Button>
|
||||
</div>
|
||||
export const AssignmentGradeFilter = ({ updateQueryParams }) => {
|
||||
const {
|
||||
assignmentGradeMin,
|
||||
assignmentGradeMax,
|
||||
selectedAssignment,
|
||||
handleSetMax,
|
||||
handleSetMin,
|
||||
handleSubmit,
|
||||
} = useAssignmentGradeFilterData({ updateQueryParams });
|
||||
const { formatMessage } = useIntl();
|
||||
return (
|
||||
<div className="grade-filter-inputs">
|
||||
<PercentGroup
|
||||
id="assignmentGradeMin"
|
||||
label={formatMessage(messages.minGrade)}
|
||||
value={assignmentGradeMin}
|
||||
disabled={!selectedAssignment}
|
||||
onChange={handleSetMin}
|
||||
/>
|
||||
<PercentGroup
|
||||
id="assignmentGradeMax"
|
||||
label={formatMessage(messages.maxGrade)}
|
||||
value={assignmentGradeMax}
|
||||
disabled={!selectedAssignment}
|
||||
onChange={handleSetMax}
|
||||
/>
|
||||
<div className="grade-filter-action">
|
||||
<Button
|
||||
type="submit"
|
||||
variant="outline-secondary"
|
||||
name="assignmentGradeMinMax"
|
||||
disabled={!selectedAssignment}
|
||||
onClick={handleSubmit}
|
||||
>
|
||||
{formatMessage(messages.apply)}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
AssignmentGradeFilter.defaultProps = {
|
||||
selectedAssignment: '',
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
AssignmentGradeFilter.propTypes = {
|
||||
updateQueryParams: PropTypes.func.isRequired,
|
||||
|
||||
// redux
|
||||
fetchGrades: PropTypes.func.isRequired,
|
||||
localAssignmentLimits: PropTypes.shape({
|
||||
assignmentGradeMax: PropTypes.string,
|
||||
assignmentGradeMin: PropTypes.string,
|
||||
}).isRequired,
|
||||
selectedAssignment: PropTypes.string,
|
||||
setFilter: PropTypes.func.isRequired,
|
||||
updateAssignmentLimits: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export const mapStateToProps = (state) => ({
|
||||
localAssignmentLimits: selectors.app.assignmentGradeLimits(state),
|
||||
selectedAssignment: selectors.filters.selectedAssignmentLabel(state),
|
||||
});
|
||||
|
||||
export const mapDispatchToProps = {
|
||||
fetchGrades: thunkActions.grades.fetchGrades,
|
||||
setFilter: actions.app.setLocalFilter,
|
||||
updateAssignmentLimits: actions.filters.update.assignmentLimits,
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(AssignmentGradeFilter);
|
||||
export default AssignmentGradeFilter;
|
||||
|
||||
@@ -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 PercentGroup from '../PercentGroup';
|
||||
import useAssignmentGradeFilterData from './hooks';
|
||||
import AssignmentFilter from '.';
|
||||
|
||||
jest.mock('../PercentGroup', () => 'PercentGroup');
|
||||
jest.mock('./hooks', () => ({ __esModule: true, default: jest.fn() }));
|
||||
|
||||
const hookData = {
|
||||
handleChange: jest.fn(),
|
||||
handleSetMax: jest.fn(),
|
||||
handleSetMin: jest.fn(),
|
||||
selectedAssignment: 'test-assignment',
|
||||
assignmentGradeMax: 300,
|
||||
assignmentGradeMin: 23,
|
||||
};
|
||||
useAssignmentGradeFilterData.mockReturnValue(hookData);
|
||||
|
||||
const updateQueryParams = jest.fn();
|
||||
|
||||
let el;
|
||||
describe('AssignmentFilter component', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
el = shallow(<AssignmentFilter updateQueryParams={updateQueryParams} />);
|
||||
});
|
||||
describe('behavior', () => {
|
||||
it('initializes hooks', () => {
|
||||
expect(useAssignmentGradeFilterData).toHaveBeenCalledWith({ updateQueryParams });
|
||||
expect(useIntl).toHaveBeenCalledWith();
|
||||
});
|
||||
});
|
||||
describe('render', () => {
|
||||
describe('with selected assignment', () => {
|
||||
test('snapshot', () => {
|
||||
expect(el).toMatchSnapshot();
|
||||
});
|
||||
it('renders a PercentGroup for both Max and Min filters', () => {
|
||||
let props = el.find(PercentGroup).at(0).props();
|
||||
expect(props.value).toEqual(hookData.assignmentGradeMin);
|
||||
expect(props.disabled).toEqual(false);
|
||||
expect(props.onChange).toEqual(hookData.handleSetMin);
|
||||
props = el.find(PercentGroup).at(1).props();
|
||||
expect(props.value).toEqual(hookData.assignmentGradeMax);
|
||||
expect(props.disabled).toEqual(false);
|
||||
expect(props.onChange).toEqual(hookData.handleSetMax);
|
||||
});
|
||||
it('renders a submit button', () => {
|
||||
const props = el.find(Button).props();
|
||||
expect(props.disabled).toEqual(false);
|
||||
expect(props.onClick).toEqual(hookData.handleSubmit);
|
||||
});
|
||||
});
|
||||
describe('without selected assignment', () => {
|
||||
beforeEach(() => {
|
||||
useAssignmentGradeFilterData.mockReturnValueOnce({
|
||||
...hookData,
|
||||
selectedAssignment: null,
|
||||
});
|
||||
el = shallow(<AssignmentFilter updateQueryParams={updateQueryParams} />);
|
||||
});
|
||||
test('snapshot', () => {
|
||||
expect(el).toMatchSnapshot();
|
||||
});
|
||||
it('disables controls', () => {
|
||||
let props = el.find(PercentGroup).at(0).props();
|
||||
expect(props.disabled).toEqual(true);
|
||||
props = el.find(PercentGroup).at(1).props();
|
||||
expect(props.disabled).toEqual(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,143 +0,0 @@
|
||||
import React from 'react';
|
||||
import { mount, shallow } from 'enzyme';
|
||||
|
||||
import actions from 'data/actions';
|
||||
import selectors from 'data/selectors';
|
||||
import { fetchGrades } from 'data/thunkActions/grades';
|
||||
|
||||
import {
|
||||
AssignmentGradeFilter,
|
||||
mapStateToProps,
|
||||
mapDispatchToProps,
|
||||
} from '.';
|
||||
|
||||
jest.mock('data/selectors', () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
app: {},
|
||||
filters: {},
|
||||
grades: {},
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('data/thunkActions/grades', () => ({
|
||||
fetchGrades: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('AssignmentGradeFilter', () => {
|
||||
let props = {};
|
||||
beforeEach(() => {
|
||||
props = {
|
||||
...props,
|
||||
updateQueryParams: jest.fn(),
|
||||
fetchGrades: jest.fn(),
|
||||
localAssignmentLimits: {
|
||||
assignmentGradeMax: '98',
|
||||
assignmentGradeMin: '2',
|
||||
},
|
||||
selectedAssignment: 'Potions 101.5',
|
||||
setFilter: jest.fn(),
|
||||
updateAssignmentLimits: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
describe('Component', () => {
|
||||
describe('behavior', () => {
|
||||
let el;
|
||||
beforeEach(() => {
|
||||
el = mount(<AssignmentGradeFilter {...props} />);
|
||||
});
|
||||
describe('handleSubmit', () => {
|
||||
beforeEach(() => {
|
||||
el.instance().handleSubmit();
|
||||
});
|
||||
it('calls props.updateAssignmentLimits with local assignment limits', () => {
|
||||
expect(props.updateAssignmentLimits).toHaveBeenCalledWith(props.localAssignmentLimits);
|
||||
});
|
||||
it('calls fetchUserGrades', () => {
|
||||
expect(props.fetchGrades).toHaveBeenCalledWith();
|
||||
});
|
||||
it('updates queryParams with assignment grade min and max', () => {
|
||||
expect(props.updateQueryParams).toHaveBeenCalledWith(props.localAssignmentLimits);
|
||||
});
|
||||
});
|
||||
describe('handleSetMin', () => {
|
||||
it('calls setFilters for assignmentGradeMin', () => {
|
||||
const testVal = 23;
|
||||
el.instance().handleSetMin({ target: { value: testVal } });
|
||||
expect(props.setFilter).toHaveBeenCalledWith({
|
||||
assignmentGradeMin: testVal,
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('handleSetMax', () => {
|
||||
it('calls setFilters for assignmentGradeMax', () => {
|
||||
const testVal = 92;
|
||||
el.instance().handleSetMax({ target: { value: testVal } });
|
||||
expect(props.setFilter).toHaveBeenCalledWith({
|
||||
assignmentGradeMax: testVal,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('snapshots', () => {
|
||||
let el;
|
||||
const mockMethods = () => {
|
||||
el.instance().handleSubmit = jest.fn().mockName('handleSubmit');
|
||||
el.instance().handleSetMax = jest.fn().mockName('handleSetMax');
|
||||
el.instance().handleSetMin = jest.fn().mockName('handleSetMin');
|
||||
};
|
||||
test('smoke test', () => {
|
||||
el = shallow(<AssignmentGradeFilter {...props} />);
|
||||
mockMethods(el);
|
||||
expect(el.instance().render()).toMatchSnapshot();
|
||||
});
|
||||
test('buttons and groups disabled if no selected assignment', () => {
|
||||
el = shallow(<AssignmentGradeFilter
|
||||
{...props}
|
||||
selectedAssignment={undefined}
|
||||
/>);
|
||||
mockMethods(el);
|
||||
expect(el.instance().render()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('mapStateToProps', () => {
|
||||
const testState = { belle: 'in', the: 'castle' };
|
||||
let mappedProps;
|
||||
beforeEach(() => {
|
||||
selectors.app.assignmentGradeLimits = jest.fn((state) => ({ gradeLimits: state }));
|
||||
selectors.filters.selectedAssignmentLabel = jest.fn((state) => ({ assignmentLabel: state }));
|
||||
mappedProps = mapStateToProps(testState);
|
||||
});
|
||||
describe('localAssignmentLimits', () => {
|
||||
it('returns selectors.app.assignmentGradeLimits', () => {
|
||||
expect(
|
||||
mappedProps.localAssignmentLimits,
|
||||
).toEqual(selectors.app.assignmentGradeLimits(testState));
|
||||
});
|
||||
});
|
||||
describe('selectedAsssignment', () => {
|
||||
it('returns selectors.filters.selectedAssignmentLabel', () => {
|
||||
expect(
|
||||
mappedProps.selectedAssignment,
|
||||
).toEqual(selectors.filters.selectedAssignmentLabel(testState));
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('mapDispatchToProps', () => {
|
||||
test('fetchGrades', () => {
|
||||
expect(mapDispatchToProps.fetchGrades).toEqual(fetchGrades);
|
||||
});
|
||||
test('setFilters', () => {
|
||||
expect(mapDispatchToProps.setFilter).toEqual(actions.app.setLocalFilter);
|
||||
});
|
||||
test('updateAssignmentLimits', () => {
|
||||
expect(
|
||||
mapDispatchToProps.updateAssignmentLimits,
|
||||
).toEqual(
|
||||
actions.filters.update.assignmentLimits,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,44 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`AssignmentFilterType component render snapshot 1`] = `
|
||||
<div
|
||||
className="student-filters"
|
||||
>
|
||||
<SelectGroup
|
||||
disabled={true}
|
||||
id="assignment-types"
|
||||
label="Assignment Types"
|
||||
onChange={[MockFunction]}
|
||||
options={
|
||||
Array [
|
||||
<option
|
||||
value=""
|
||||
>
|
||||
All
|
||||
</option>,
|
||||
<option
|
||||
value="test-type"
|
||||
>
|
||||
test-type
|
||||
</option>,
|
||||
<option
|
||||
value="type1"
|
||||
>
|
||||
type1
|
||||
</option>,
|
||||
<option
|
||||
value="type2"
|
||||
>
|
||||
type2
|
||||
</option>,
|
||||
<option
|
||||
value="type3"
|
||||
>
|
||||
type3
|
||||
</option>,
|
||||
]
|
||||
}
|
||||
value="test-type"
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
@@ -1,79 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`AssignmentTypeFilter Component snapshots SelectGroup disabled if no assignmentFilterOptions 1`] = `
|
||||
<div
|
||||
className="student-filters"
|
||||
>
|
||||
<SelectGroup
|
||||
disabled={true}
|
||||
id="assignment-types"
|
||||
label={
|
||||
<FormattedMessage
|
||||
defaultMessage="Assignment Types"
|
||||
description="Assignment Types filter select label in Gradebook Filters"
|
||||
id="gradebook.GradebookFilters.assignmentTypesLabel"
|
||||
/>
|
||||
}
|
||||
onChange={[MockFunction handleChange]}
|
||||
options={
|
||||
Array [
|
||||
<option
|
||||
value=""
|
||||
>
|
||||
All
|
||||
</option>,
|
||||
<option
|
||||
value="assignMentType1"
|
||||
>
|
||||
assignMentType1
|
||||
</option>,
|
||||
<option
|
||||
value="AssigNmentType2"
|
||||
>
|
||||
AssigNmentType2
|
||||
</option>,
|
||||
]
|
||||
}
|
||||
value="assigNmentType2"
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`AssignmentTypeFilter Component snapshots smoke test 1`] = `
|
||||
<div
|
||||
className="student-filters"
|
||||
>
|
||||
<SelectGroup
|
||||
disabled={false}
|
||||
id="assignment-types"
|
||||
label={
|
||||
<FormattedMessage
|
||||
defaultMessage="Assignment Types"
|
||||
description="Assignment Types filter select label in Gradebook Filters"
|
||||
id="gradebook.GradebookFilters.assignmentTypesLabel"
|
||||
/>
|
||||
}
|
||||
onChange={[MockFunction handleChange]}
|
||||
options={
|
||||
Array [
|
||||
<option
|
||||
value=""
|
||||
>
|
||||
All
|
||||
</option>,
|
||||
<option
|
||||
value="assignMentType1"
|
||||
>
|
||||
assignMentType1
|
||||
</option>,
|
||||
<option
|
||||
value="AssigNmentType2"
|
||||
>
|
||||
AssigNmentType2
|
||||
</option>,
|
||||
]
|
||||
}
|
||||
value="assigNmentType2"
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
@@ -0,0 +1,22 @@
|
||||
import { selectors, actions } from 'data/redux/hooks';
|
||||
|
||||
export const useAssignmentTypeFilterData = ({ updateQueryParams }) => {
|
||||
const assignmentTypes = selectors.assignmentTypes.useAllAssignmentTypes() || {};
|
||||
const assignmentFilterOptions = selectors.filters.useSelectableAssignmentLabels();
|
||||
const selectedAssignmentType = selectors.filters.useAssignmentType() || '';
|
||||
const filterAssignmentType = actions.filters.useUpdateAssignmentType();
|
||||
|
||||
const handleChange = (event) => {
|
||||
const assignmentType = event.target.value;
|
||||
filterAssignmentType(assignmentType);
|
||||
updateQueryParams({ assignmentType });
|
||||
};
|
||||
|
||||
return {
|
||||
assignmentTypes,
|
||||
handleChange,
|
||||
isDisabled: assignmentFilterOptions.length === 0,
|
||||
selectedAssignmentType,
|
||||
};
|
||||
};
|
||||
export default useAssignmentTypeFilterData;
|
||||
@@ -0,0 +1,92 @@
|
||||
import { selectors, actions } from 'data/redux/hooks';
|
||||
|
||||
import useAssignmentTypeFilterData from './hooks';
|
||||
|
||||
jest.mock('data/redux/hooks', () => ({
|
||||
selectors: {
|
||||
assignmentTypes: {
|
||||
useAllAssignmentTypes: jest.fn(),
|
||||
},
|
||||
filters: {
|
||||
useSelectableAssignmentLabels: jest.fn(),
|
||||
useAssignmentType: jest.fn(),
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
filters: { useUpdateAssignmentType: jest.fn() },
|
||||
},
|
||||
}));
|
||||
|
||||
let out;
|
||||
const testId = 'test-id';
|
||||
const testKey = 'test-key';
|
||||
|
||||
const testType = 'test-type';
|
||||
const allTypes = [testType, 'and', 'some', 'other', 'types'];
|
||||
selectors.assignmentTypes.useAllAssignmentTypes.mockReturnValue(allTypes);
|
||||
const event = { target: { value: testType } };
|
||||
|
||||
const testLabel = { label: testKey, id: testId, type: testType };
|
||||
const selectableAssignmentLabels = [
|
||||
{ label: 'some' },
|
||||
{ label: 'test' },
|
||||
{ label: 'labels' },
|
||||
testLabel,
|
||||
];
|
||||
selectors.filters.useSelectableAssignmentLabels.mockReturnValue(selectableAssignmentLabels);
|
||||
selectors.filters.useAssignmentType.mockReturnValue(testType);
|
||||
|
||||
const updateAssignmentType = jest.fn();
|
||||
actions.filters.useUpdateAssignmentType.mockReturnValue(updateAssignmentType);
|
||||
|
||||
const updateQueryParams = jest.fn();
|
||||
|
||||
describe('useAssignmentTypeFilterData hook', () => {
|
||||
beforeEach(() => {
|
||||
out = useAssignmentTypeFilterData({ updateQueryParams });
|
||||
});
|
||||
describe('behavior', () => {
|
||||
it('initializes redux hooks', () => {
|
||||
expect(selectors.assignmentTypes.useAllAssignmentTypes).toHaveBeenCalledWith();
|
||||
expect(selectors.filters.useSelectableAssignmentLabels).toHaveBeenCalledWith();
|
||||
expect(selectors.filters.useAssignmentType).toHaveBeenCalledWith();
|
||||
expect(actions.filters.useUpdateAssignmentType).toHaveBeenCalledWith();
|
||||
});
|
||||
});
|
||||
describe('output', () => {
|
||||
describe('handleEvent', () => {
|
||||
beforeEach(() => {
|
||||
out.handleChange(event);
|
||||
});
|
||||
it('updates assignmentType filter with selected filter', () => {
|
||||
expect(updateAssignmentType).toHaveBeenCalledWith(testType);
|
||||
});
|
||||
it('updates queryParams', () => {
|
||||
expect(updateQueryParams).toHaveBeenCalledWith({ assignmentType: testType });
|
||||
});
|
||||
});
|
||||
describe('selectedAssignmentType', () => {
|
||||
it('returns selected assignmentType', () => {
|
||||
expect(out.selectedAssignmentType).toEqual(testType);
|
||||
});
|
||||
it('returns empty string if no assignmentType is selected', () => {
|
||||
selectors.filters.useAssignmentType.mockReturnValue(undefined);
|
||||
out = useAssignmentTypeFilterData({ updateQueryParams });
|
||||
expect(out.selectedAssignmentType).toEqual('');
|
||||
});
|
||||
});
|
||||
it('passes assignmentTypes from hook', () => {
|
||||
expect(out.assignmentTypes).toEqual(allTypes);
|
||||
});
|
||||
test('assignmentTypes is empty object if hook returns undefined', () => {
|
||||
selectors.assignmentTypes.useAllAssignmentTypes.mockReturnValue(undefined);
|
||||
out = useAssignmentTypeFilterData({ updateQueryParams });
|
||||
expect(out.assignmentTypes).toEqual({});
|
||||
});
|
||||
it('returns isDisabled if assigmentFilterOptions is empty', () => {
|
||||
expect(out.isDisabled).toEqual(false);
|
||||
selectors.assignmentTypes.useAllAssignmentTypes.mockReturnValue([]);
|
||||
out = useAssignmentTypeFilterData({ updateQueryParams });
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,81 +1,42 @@
|
||||
/* eslint-disable react/sort-comp, react/button-has-type */
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import selectors from 'data/selectors';
|
||||
import actions from 'data/actions';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import SelectGroup from '../SelectGroup';
|
||||
import messages from '../messages';
|
||||
import useAssignmentTypeFilterData from './hooks';
|
||||
|
||||
export class AssignmentTypeFilter extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.handleChange = this.handleChange.bind(this);
|
||||
}
|
||||
|
||||
handleChange(event) {
|
||||
const assignmentType = event.target.value;
|
||||
this.props.filterAssignmentType(assignmentType);
|
||||
this.props.updateQueryParams({ assignmentType });
|
||||
}
|
||||
|
||||
get options() {
|
||||
const mapper = (entry) => (
|
||||
<option key={entry} value={entry}>{entry}</option>
|
||||
);
|
||||
return [
|
||||
<option key="0" value="">All</option>,
|
||||
...this.props.assignmentTypes.map(mapper),
|
||||
];
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="student-filters">
|
||||
<SelectGroup
|
||||
id="assignment-types"
|
||||
label={<FormattedMessage {...messages.assignmentTypes} />}
|
||||
value={this.props.selectedAssignmentType}
|
||||
onChange={this.handleChange}
|
||||
disabled={this.props.assignmentFilterOptions.length === 0}
|
||||
options={this.options}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
AssignmentTypeFilter.defaultProps = {
|
||||
assignmentTypes: [],
|
||||
assignmentFilterOptions: [],
|
||||
selectedAssignmentType: '',
|
||||
export const AssignmentTypeFilter = ({ updateQueryParams }) => {
|
||||
const {
|
||||
assignmentTypes,
|
||||
handleChange,
|
||||
isDisabled,
|
||||
selectedAssignmentType,
|
||||
} = useAssignmentTypeFilterData({ updateQueryParams });
|
||||
const { formatMessage } = useIntl();
|
||||
return (
|
||||
<div className="student-filters">
|
||||
<SelectGroup
|
||||
id="assignment-types"
|
||||
label={formatMessage(messages.assignmentTypes)}
|
||||
value={selectedAssignmentType}
|
||||
onChange={handleChange}
|
||||
disabled={isDisabled}
|
||||
options={[
|
||||
<option key="0" value="">All</option>,
|
||||
...assignmentTypes.map(entry => (
|
||||
<option key={entry} value={entry}>{entry}</option>
|
||||
)),
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
AssignmentTypeFilter.propTypes = {
|
||||
updateQueryParams: PropTypes.func.isRequired,
|
||||
|
||||
// redux
|
||||
assignmentTypes: PropTypes.arrayOf(PropTypes.string),
|
||||
assignmentFilterOptions: PropTypes.arrayOf(PropTypes.shape({
|
||||
label: PropTypes.string,
|
||||
subsectionLabel: PropTypes.string,
|
||||
})),
|
||||
filterAssignmentType: PropTypes.func.isRequired,
|
||||
selectedAssignmentType: PropTypes.string,
|
||||
};
|
||||
|
||||
export const mapStateToProps = (state) => ({
|
||||
assignmentTypes: selectors.assignmentTypes.allAssignmentTypes(state),
|
||||
assignmentFilterOptions: selectors.filters.selectableAssignmentLabels(state),
|
||||
selectedAssignmentType: selectors.filters.assignmentType(state),
|
||||
});
|
||||
|
||||
export const mapDispatchToProps = {
|
||||
filterAssignmentType: actions.filters.update.assignmentType,
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(AssignmentTypeFilter);
|
||||
export default AssignmentTypeFilter;
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import SelectGroup from '../SelectGroup';
|
||||
import useAssignmentFilterTypeData from './hooks';
|
||||
import AssignmentFilterType from '.';
|
||||
|
||||
jest.mock('../SelectGroup', () => 'SelectGroup');
|
||||
jest.mock('./hooks', () => ({ __esModule: true, default: jest.fn() }));
|
||||
|
||||
const handleChange = jest.fn();
|
||||
const testType = 'test-type';
|
||||
const assignmentTypes = [testType, 'type1', 'type2', 'type3'];
|
||||
useAssignmentFilterTypeData.mockReturnValue({
|
||||
handleChange,
|
||||
selectedAssignmentType: testType,
|
||||
assignmentTypes,
|
||||
isDisabled: true,
|
||||
});
|
||||
|
||||
const updateQueryParams = jest.fn();
|
||||
|
||||
let el;
|
||||
describe('AssignmentFilterType component', () => {
|
||||
beforeAll(() => {
|
||||
el = shallow(<AssignmentFilterType updateQueryParams={updateQueryParams} />);
|
||||
});
|
||||
describe('behavior', () => {
|
||||
it('initializes hooks', () => {
|
||||
expect(useAssignmentFilterTypeData).toHaveBeenCalledWith({ updateQueryParams });
|
||||
expect(useIntl).toHaveBeenCalledWith();
|
||||
});
|
||||
});
|
||||
describe('render', () => {
|
||||
test('snapshot', () => {
|
||||
expect(el).toMatchSnapshot();
|
||||
});
|
||||
test('filter options', () => {
|
||||
const { options } = el.find(SelectGroup).props();
|
||||
expect(options.length).toEqual(5);
|
||||
const optionProps = options[1].props;
|
||||
expect(optionProps.value).toEqual(assignmentTypes[0]);
|
||||
expect(optionProps.children).toEqual(testType);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,135 +0,0 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import selectors from 'data/selectors';
|
||||
import actions from 'data/actions';
|
||||
|
||||
import {
|
||||
AssignmentTypeFilter,
|
||||
mapStateToProps,
|
||||
mapDispatchToProps,
|
||||
} from '.';
|
||||
|
||||
jest.mock('data/selectors', () => ({
|
||||
/** Mocking to use passed state for validation purposes */
|
||||
assignmentTypes: {
|
||||
allAssignmentTypes: jest.fn(() => (['assignment', 'labs'])),
|
||||
},
|
||||
filters: {
|
||||
selectableAssignmentLabels: jest.fn(() => ([{
|
||||
label: 'assigNment',
|
||||
subsectionLabel: 'subsection',
|
||||
type: 'assignMentType',
|
||||
id: 'subsectionId',
|
||||
}])),
|
||||
assignmentType: jest.fn(() => 'assignMentType'),
|
||||
},
|
||||
}));
|
||||
|
||||
describe('AssignmentTypeFilter', () => {
|
||||
let props = {
|
||||
assignmentTypes: ['assignMentType1', 'AssigNmentType2'],
|
||||
assignmentFilterOptions: [
|
||||
{ label: 'filterLabel1', subsectionLabel: 'filterSubLabel2' },
|
||||
{ label: 'filterLabel2', subsectionLabel: 'filterSubLabel1' },
|
||||
],
|
||||
selectedAssignmentType: 'assigNmentType2',
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
props = {
|
||||
...props,
|
||||
filterAssignmentType: jest.fn(),
|
||||
updateQueryParams: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
describe('Component', () => {
|
||||
describe('behavior', () => {
|
||||
describe('handleChange', () => {
|
||||
let el;
|
||||
const newType = 'new Type';
|
||||
const event = { target: { value: newType } };
|
||||
beforeEach(() => {
|
||||
el = shallow(<AssignmentTypeFilter {...props} />);
|
||||
el.instance().handleChange(event);
|
||||
});
|
||||
it('calls props.filterAssignmentType with new type', () => {
|
||||
expect(props.filterAssignmentType).toHaveBeenCalledWith(
|
||||
newType,
|
||||
);
|
||||
});
|
||||
it('updates queryParams with assignmentType', () => {
|
||||
expect(props.updateQueryParams).toHaveBeenCalledWith({
|
||||
assignmentType: newType,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('snapshots', () => {
|
||||
let el;
|
||||
const mockMethods = () => {
|
||||
el.instance().handleChange = jest.fn().mockName('handleChange');
|
||||
};
|
||||
test('smoke test', () => {
|
||||
el = shallow(<AssignmentTypeFilter {...props} />);
|
||||
mockMethods(el);
|
||||
expect(el.instance().render()).toMatchSnapshot();
|
||||
});
|
||||
test('SelectGroup disabled if no assignmentFilterOptions', () => {
|
||||
el = shallow(<AssignmentTypeFilter
|
||||
{...props}
|
||||
assignmentFilterOptions={[]}
|
||||
/>);
|
||||
mockMethods(el);
|
||||
expect(el.instance().render()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('mapStateToProps', () => {
|
||||
const state = {
|
||||
assignmentTypes: {
|
||||
results: ['assignMentType1', 'assignMentType2'],
|
||||
},
|
||||
filters: {
|
||||
assignmentType: 'selectedAssignMent',
|
||||
cohort: 'selectedCOHOrt',
|
||||
track: 'SELectedTrack',
|
||||
},
|
||||
};
|
||||
describe('assignmentTypes', () => {
|
||||
it('is selected from assignmentTypes.allAssignmentTypes', () => {
|
||||
expect(
|
||||
mapStateToProps(state).assignmentTypes,
|
||||
).toEqual(
|
||||
selectors.assignmentTypes.allAssignmentTypes(state),
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('assignmentFilterOptions', () => {
|
||||
it('is selected from filters.selectableAssignmentLabels', () => {
|
||||
expect(
|
||||
mapStateToProps(state).assignmentFilterOptions,
|
||||
).toEqual(
|
||||
selectors.filters.selectableAssignmentLabels(state),
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('selectedAssignmentType', () => {
|
||||
it('is selected from filters.assignmentType', () => {
|
||||
expect(
|
||||
mapStateToProps(state).selectedAssignmentType,
|
||||
).toEqual(
|
||||
selectors.filters.assignmentType(state),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('mapDispatchToProps', () => {
|
||||
test('filterAssignmentType', () => {
|
||||
expect(mapDispatchToProps.filterAssignmentType).toEqual(
|
||||
actions.filters.update.assignmentType,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,63 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`CourseFilter component render if disabled snapshot 1`] = `
|
||||
<Fragment>
|
||||
<div
|
||||
className="grade-filter-inputs"
|
||||
>
|
||||
<PercentGroup
|
||||
id="minimum-grade"
|
||||
label="Min Grade"
|
||||
onChange={[MockFunction]}
|
||||
value={23}
|
||||
/>
|
||||
<PercentGroup
|
||||
id="maximum-grade"
|
||||
label="Max Grade"
|
||||
onChange={[MockFunction]}
|
||||
value={300}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="grade-filter-action"
|
||||
>
|
||||
<Button
|
||||
disabled={true}
|
||||
variant="outline-secondary"
|
||||
>
|
||||
Apply
|
||||
</Button>
|
||||
</div>
|
||||
</Fragment>
|
||||
`;
|
||||
|
||||
exports[`CourseFilter component render with selected assignment snapshot 1`] = `
|
||||
<Fragment>
|
||||
<div
|
||||
className="grade-filter-inputs"
|
||||
>
|
||||
<PercentGroup
|
||||
id="minimum-grade"
|
||||
label="Min Grade"
|
||||
onChange={[MockFunction]}
|
||||
value={23}
|
||||
/>
|
||||
<PercentGroup
|
||||
id="maximum-grade"
|
||||
label="Max Grade"
|
||||
onChange={[MockFunction]}
|
||||
value={300}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="grade-filter-action"
|
||||
>
|
||||
<Button
|
||||
disabled={false}
|
||||
variant="outline-secondary"
|
||||
>
|
||||
Apply
|
||||
</Button>
|
||||
</div>
|
||||
</Fragment>
|
||||
`;
|
||||
@@ -1,44 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`CourseGradeFilter Component snapshots basic snapshot 1`] = `
|
||||
<React.Fragment>
|
||||
<div
|
||||
className="grade-filter-inputs"
|
||||
>
|
||||
<PercentGroup
|
||||
id="minimum-grade"
|
||||
label={
|
||||
<FormattedMessage
|
||||
defaultMessage="Min Grade"
|
||||
description="Min-grade filter select label in Gradebook Filters"
|
||||
id="gradebook.GradebookFilters.minGradeFilterLabel"
|
||||
/>
|
||||
}
|
||||
onChange={[MockFunction handleUpdateMin]}
|
||||
value="5"
|
||||
/>
|
||||
<PercentGroup
|
||||
id="maximum-grade"
|
||||
label={
|
||||
<FormattedMessage
|
||||
defaultMessage="Max Grade"
|
||||
description="Max-grade filter select label in Gradebook Filters"
|
||||
id="gradebook.GradebookFilters.maxGradeFilterLabel"
|
||||
/>
|
||||
}
|
||||
onChange={[MockFunction handleUpdateMax]}
|
||||
value="92"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="grade-filter-action"
|
||||
>
|
||||
<Button
|
||||
onClick={[MockFunction handleApplyClick]}
|
||||
variant="outline-secondary"
|
||||
>
|
||||
Apply
|
||||
</Button>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
`;
|
||||
33
src/components/GradebookFilters/CourseGradeFilter/hooks.js
Normal file
33
src/components/GradebookFilters/CourseGradeFilter/hooks.js
Normal file
@@ -0,0 +1,33 @@
|
||||
import { actions, selectors, thunkActions } from 'data/redux/hooks';
|
||||
|
||||
export const useCourseGradeFilterData = ({
|
||||
updateQueryParams,
|
||||
}) => {
|
||||
const isDisabled = !selectors.app.useAreCourseGradeFiltersValid();
|
||||
const localCourseLimits = selectors.app.useCourseGradeLimits();
|
||||
const fetchGrades = thunkActions.grades.useFetchGrades();
|
||||
const setLocalFilter = actions.app.useSetLocalFilter();
|
||||
const updateFilter = actions.filters.useUpdateCourseGradeLimits();
|
||||
|
||||
const handleApplyClick = () => {
|
||||
updateFilter(localCourseLimits);
|
||||
fetchGrades();
|
||||
updateQueryParams(localCourseLimits);
|
||||
};
|
||||
|
||||
const { courseGradeMin, courseGradeMax } = localCourseLimits;
|
||||
return {
|
||||
max: {
|
||||
value: courseGradeMax,
|
||||
onChange: (e) => setLocalFilter({ courseGradeMax: e.target.value }),
|
||||
},
|
||||
min: {
|
||||
value: courseGradeMin,
|
||||
onChange: (e) => setLocalFilter({ courseGradeMin: e.target.value }),
|
||||
},
|
||||
handleApplyClick,
|
||||
isDisabled,
|
||||
};
|
||||
};
|
||||
|
||||
export default useCourseGradeFilterData;
|
||||
@@ -0,0 +1,78 @@
|
||||
import { selectors, actions, thunkActions } from 'data/redux/hooks';
|
||||
|
||||
import useCourseTypeFilterData from './hooks';
|
||||
|
||||
jest.mock('data/redux/hooks', () => ({
|
||||
selectors: {
|
||||
app: {
|
||||
useAreCourseGradeFiltersValid: jest.fn(),
|
||||
useCourseGradeLimits: jest.fn(),
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
app: { useSetLocalFilter: jest.fn() },
|
||||
filters: { useUpdateCourseGradeLimits: jest.fn() },
|
||||
},
|
||||
thunkActions: {
|
||||
grades: { useFetchGrades: jest.fn() },
|
||||
},
|
||||
}));
|
||||
|
||||
let out;
|
||||
|
||||
const courseGradeLimits = { courseGradeMax: 120, courseGradeMin: 32 };
|
||||
selectors.app.useAreCourseGradeFiltersValid.mockReturnValue(true);
|
||||
selectors.app.useCourseGradeLimits.mockReturnValue(courseGradeLimits);
|
||||
|
||||
const setLocalFilter = jest.fn();
|
||||
actions.app.useSetLocalFilter.mockReturnValue(setLocalFilter);
|
||||
const updateCourseGradeLimits = jest.fn();
|
||||
actions.filters.useUpdateCourseGradeLimits.mockReturnValue(updateCourseGradeLimits);
|
||||
const fetch = jest.fn();
|
||||
thunkActions.grades.useFetchGrades.mockReturnValue(fetch);
|
||||
|
||||
const testValue = 55;
|
||||
|
||||
const updateQueryParams = jest.fn();
|
||||
|
||||
describe('useCourseTypeFilterData hook', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
out = useCourseTypeFilterData({ updateQueryParams });
|
||||
});
|
||||
describe('behavior', () => {
|
||||
it('initializes redux hooks', () => {
|
||||
expect(selectors.app.useAreCourseGradeFiltersValid).toHaveBeenCalledWith();
|
||||
expect(selectors.app.useCourseGradeLimits).toHaveBeenCalledWith();
|
||||
expect(actions.app.useSetLocalFilter).toHaveBeenCalledWith();
|
||||
expect(actions.filters.useUpdateCourseGradeLimits).toHaveBeenCalledWith();
|
||||
expect(thunkActions.grades.useFetchGrades).toHaveBeenCalledWith();
|
||||
});
|
||||
});
|
||||
describe('output', () => {
|
||||
it('returns isDisabled if assigmentFilterOptions is empty', () => {
|
||||
expect(out.isDisabled).toEqual(false);
|
||||
selectors.app.useAreCourseGradeFiltersValid.mockReturnValue(false);
|
||||
out = useCourseTypeFilterData({ updateQueryParams });
|
||||
expect(out.isDisabled).toEqual(true);
|
||||
});
|
||||
test('min value and onChange', () => {
|
||||
const { courseGradeMin } = courseGradeLimits;
|
||||
expect(out.min.value).toEqual(courseGradeMin);
|
||||
out.min.onChange({ target: { value: testValue } });
|
||||
expect(setLocalFilter).toHaveBeenCalledWith({ courseGradeMin: testValue });
|
||||
});
|
||||
test('max value and onChange', () => {
|
||||
const { courseGradeMax } = courseGradeLimits;
|
||||
expect(out.max.value).toEqual(courseGradeMax);
|
||||
out.max.onChange({ target: { value: testValue } });
|
||||
expect(setLocalFilter).toHaveBeenCalledWith({ courseGradeMax: testValue });
|
||||
});
|
||||
it('updates filter, fetches grades, and updates query params on apply click', () => {
|
||||
out.handleApplyClick();
|
||||
expect(updateCourseGradeLimits).toHaveBeenCalledWith(courseGradeLimits);
|
||||
expect(fetch).toHaveBeenCalledWith();
|
||||
expect(updateQueryParams).toHaveBeenCalledWith(courseGradeLimits);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,103 +1,52 @@
|
||||
/* eslint-disable react/sort-comp, react/button-has-type, import/no-named-as-default */
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { Button } from '@edx/paragon';
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import selectors from 'data/selectors';
|
||||
import actions from 'data/actions';
|
||||
import thunkActions from 'data/thunkActions';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import messages from '../messages';
|
||||
import PercentGroup from '../PercentGroup';
|
||||
import useCourseGradeFilterData from './hooks';
|
||||
|
||||
export class CourseGradeFilter extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.handleApplyClick = this.handleApplyClick.bind(this);
|
||||
this.handleUpdateMin = this.handleUpdateMin.bind(this);
|
||||
this.handleUpdateMax = this.handleUpdateMax.bind(this);
|
||||
this.updateCourseGradeFilters = this.updateCourseGradeFilters.bind(this);
|
||||
}
|
||||
export const CourseGradeFilter = ({ updateQueryParams }) => {
|
||||
const {
|
||||
max,
|
||||
min,
|
||||
isDisabled,
|
||||
handleApplyClick,
|
||||
} = useCourseGradeFilterData({ updateQueryParams });
|
||||
const { formatMessage } = useIntl();
|
||||
|
||||
handleApplyClick() {
|
||||
if (this.props.areLimitsValid) {
|
||||
this.updateCourseGradeFilters();
|
||||
}
|
||||
}
|
||||
|
||||
handleUpdateMin({ target: { value } }) {
|
||||
this.props.setLocalFilter({ courseGradeMin: value });
|
||||
}
|
||||
|
||||
handleUpdateMax({ target: { value } }) {
|
||||
this.props.setLocalFilter({ courseGradeMax: value });
|
||||
}
|
||||
|
||||
updateCourseGradeFilters() {
|
||||
this.props.updateFilter(this.props.localCourseLimits);
|
||||
this.props.fetchGrades();
|
||||
this.props.updateQueryParams(this.props.localCourseLimits);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
localCourseLimits: { courseGradeMin, courseGradeMax },
|
||||
} = this.props;
|
||||
return (
|
||||
<>
|
||||
<div className="grade-filter-inputs">
|
||||
<PercentGroup
|
||||
id="minimum-grade"
|
||||
label={<FormattedMessage {...messages.minGrade} />}
|
||||
value={courseGradeMin}
|
||||
onChange={this.handleUpdateMin}
|
||||
/>
|
||||
<PercentGroup
|
||||
id="maximum-grade"
|
||||
label={<FormattedMessage {...messages.maxGrade} />}
|
||||
value={courseGradeMax}
|
||||
onChange={this.handleUpdateMax}
|
||||
/>
|
||||
</div>
|
||||
<div className="grade-filter-action">
|
||||
<Button
|
||||
variant="outline-secondary"
|
||||
onClick={this.handleApplyClick}
|
||||
>
|
||||
Apply
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<div className="grade-filter-inputs">
|
||||
<PercentGroup
|
||||
id="minimum-grade"
|
||||
label={formatMessage(messages.minGrade)}
|
||||
value={min.value}
|
||||
onChange={min.onChange}
|
||||
/>
|
||||
<PercentGroup
|
||||
id="maximum-grade"
|
||||
label={formatMessage(messages.maxGrade)}
|
||||
value={max.value}
|
||||
onChange={max.onChange}
|
||||
/>
|
||||
</div>
|
||||
<div className="grade-filter-action">
|
||||
<Button
|
||||
variant="outline-secondary"
|
||||
onClick={handleApplyClick}
|
||||
disabled={isDisabled}
|
||||
>
|
||||
{formatMessage(messages.apply)}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
CourseGradeFilter.propTypes = {
|
||||
updateQueryParams: PropTypes.func.isRequired,
|
||||
|
||||
// Redux
|
||||
areLimitsValid: PropTypes.bool.isRequired,
|
||||
fetchGrades: PropTypes.func.isRequired,
|
||||
localCourseLimits: PropTypes.shape({
|
||||
courseGradeMin: PropTypes.string.isRequired,
|
||||
courseGradeMax: PropTypes.string.isRequired,
|
||||
}).isRequired,
|
||||
setLocalFilter: PropTypes.func.isRequired,
|
||||
updateFilter: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export const mapStateToProps = (state) => ({
|
||||
areLimitsValid: selectors.app.areCourseGradeFiltersValid(state),
|
||||
localCourseLimits: selectors.app.courseGradeLimits(state),
|
||||
});
|
||||
|
||||
export const mapDispatchToProps = {
|
||||
fetchGrades: thunkActions.grades.fetchGrades,
|
||||
setLocalFilter: actions.app.setLocalFilter,
|
||||
updateFilter: actions.filters.update.courseGradeLimits,
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(CourseGradeFilter);
|
||||
export default CourseGradeFilter;
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Button } from '@edx/paragon';
|
||||
|
||||
import PercentGroup from '../PercentGroup';
|
||||
import useCourseGradeFilterData from './hooks';
|
||||
import CourseFilter from '.';
|
||||
|
||||
jest.mock('../PercentGroup', () => 'PercentGroup');
|
||||
jest.mock('./hooks', () => ({ __esModule: true, default: jest.fn() }));
|
||||
|
||||
const hookData = {
|
||||
handleChange: jest.fn(),
|
||||
max: {
|
||||
value: 300,
|
||||
onChange: jest.fn(),
|
||||
},
|
||||
min: {
|
||||
value: 23,
|
||||
onChange: jest.fn(),
|
||||
},
|
||||
selectedCourse: 'test-assignment',
|
||||
isDisabled: false,
|
||||
};
|
||||
useCourseGradeFilterData.mockReturnValue(hookData);
|
||||
|
||||
const updateQueryParams = jest.fn();
|
||||
|
||||
let el;
|
||||
describe('CourseFilter component', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
el = shallow(<CourseFilter updateQueryParams={updateQueryParams} />);
|
||||
});
|
||||
describe('behavior', () => {
|
||||
it('initializes hooks', () => {
|
||||
expect(useCourseGradeFilterData).toHaveBeenCalledWith({ updateQueryParams });
|
||||
expect(useIntl).toHaveBeenCalledWith();
|
||||
});
|
||||
});
|
||||
describe('render', () => {
|
||||
describe('with selected assignment', () => {
|
||||
test('snapshot', () => {
|
||||
expect(el).toMatchSnapshot();
|
||||
});
|
||||
it('renders a PercentGroup for both Max and Min filters', () => {
|
||||
let props = el.find(PercentGroup).at(0).props();
|
||||
expect(props.value).toEqual(hookData.min.value);
|
||||
expect(props.onChange).toEqual(hookData.min.onChange);
|
||||
props = el.find(PercentGroup).at(1).props();
|
||||
expect(props.value).toEqual(hookData.max.value);
|
||||
expect(props.onChange).toEqual(hookData.max.onChange);
|
||||
});
|
||||
it('renders a submit button', () => {
|
||||
const props = el.find(Button).props();
|
||||
expect(props.disabled).toEqual(false);
|
||||
expect(props.onClick).toEqual(hookData.handleApplyClick);
|
||||
});
|
||||
});
|
||||
describe('if disabled', () => {
|
||||
beforeEach(() => {
|
||||
useCourseGradeFilterData.mockReturnValueOnce({ ...hookData, isDisabled: true });
|
||||
el = shallow(<CourseFilter updateQueryParams={updateQueryParams} />);
|
||||
});
|
||||
test('snapshot', () => {
|
||||
expect(el).toMatchSnapshot();
|
||||
});
|
||||
it('disables submit', () => {
|
||||
const props = el.find(Button).props();
|
||||
expect(props.disabled).toEqual(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,150 +0,0 @@
|
||||
/* eslint-disable import/no-named-as-default */
|
||||
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import actions from 'data/actions';
|
||||
import selectors from 'data/selectors';
|
||||
import { fetchGrades } from 'data/thunkActions/grades';
|
||||
import {
|
||||
CourseGradeFilter,
|
||||
mapStateToProps,
|
||||
mapDispatchToProps,
|
||||
} from '.';
|
||||
|
||||
jest.mock('@edx/paragon', () => ({
|
||||
Button: () => 'Button',
|
||||
}));
|
||||
jest.mock('../PercentGroup', () => 'PercentGroup');
|
||||
|
||||
jest.mock('data/thunkActions/grades', () => ({
|
||||
fetchGrades: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('data/selectors', () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
app: {
|
||||
areCourseGradeFiltersValid: jest.fn(state => ({ areCourseGradeFiltersValid: state })),
|
||||
courseGradeLimits: jest.fn(state => ({ courseGradeLimits: state })),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
describe('CourseGradeFilter', () => {
|
||||
let props = {
|
||||
localCourseLimits: {
|
||||
courseGradeMin: '5',
|
||||
courseGradeMax: '92',
|
||||
},
|
||||
areLimitsValid: true,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
props = {
|
||||
...props,
|
||||
fetchGrades: jest.fn(),
|
||||
setLocalFilter: jest.fn(),
|
||||
updateQueryParams: jest.fn(),
|
||||
updateFilter: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
describe('Component', () => {
|
||||
describe('snapshots', () => {
|
||||
test('basic snapshot', () => {
|
||||
const el = shallow(<CourseGradeFilter {...props} />);
|
||||
el.instance().handleUpdateMin = jest.fn().mockName(
|
||||
'handleUpdateMin',
|
||||
);
|
||||
el.instance().handleUpdateMax = jest.fn().mockName(
|
||||
'handleUpdateMax',
|
||||
);
|
||||
el.instance().handleApplyClick = jest.fn().mockName(
|
||||
'handleApplyClick',
|
||||
);
|
||||
expect(el.instance().render()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe('behavior', () => {
|
||||
let el;
|
||||
const testVal = 'TESTvalue';
|
||||
beforeEach(() => {
|
||||
el = shallow(<CourseGradeFilter {...props} />);
|
||||
});
|
||||
describe('handleApplyClick', () => {
|
||||
beforeEach(() => {
|
||||
el.instance().updateCourseGradeFilters = jest.fn();
|
||||
});
|
||||
it('calls updateCourseGradeFilters is limits are valid', () => {
|
||||
el.instance().handleApplyClick();
|
||||
expect(el.instance().updateCourseGradeFilters).toHaveBeenCalledWith();
|
||||
});
|
||||
it('does not call updateCourseGradeFilters if limits are not valid', () => {
|
||||
el.setProps({ areLimitsValid: false });
|
||||
el.instance().handleApplyClick();
|
||||
expect(el.instance().updateCourseGradeFilters).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
describe('updateCourseGradeFilters', () => {
|
||||
beforeEach(() => {
|
||||
el.instance().updateCourseGradeFilters();
|
||||
});
|
||||
it('calls props.updateFilter with selection', () => {
|
||||
expect(props.updateFilter).toHaveBeenCalledWith(props.localCourseLimits);
|
||||
});
|
||||
it('calls props.getUserGrades with selection', () => {
|
||||
expect(props.fetchGrades).toHaveBeenCalledWith();
|
||||
});
|
||||
it('updates query params with courseGradeMin and courseGradeMax', () => {
|
||||
expect(props.updateQueryParams).toHaveBeenCalledWith(props.localCourseLimits);
|
||||
});
|
||||
});
|
||||
describe('handleUpdateMin', () => {
|
||||
it('calls props.setCourseGradeMin with event value', () => {
|
||||
el.instance().handleUpdateMin(
|
||||
{ target: { value: testVal } },
|
||||
);
|
||||
expect(props.setLocalFilter).toHaveBeenCalledWith({
|
||||
courseGradeMin: testVal,
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('handleUpdateMax', () => {
|
||||
it('calls props.setCourseGradeMax with event value', () => {
|
||||
el.instance().handleUpdateMax(
|
||||
{ target: { value: testVal } },
|
||||
);
|
||||
expect(props.setLocalFilter).toHaveBeenCalledWith({
|
||||
courseGradeMax: testVal,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('mapStateToProps', () => {
|
||||
const testState = { peanut: 'butter', jelly: 'time' };
|
||||
let mapped;
|
||||
beforeEach(() => {
|
||||
mapped = mapStateToProps(testState);
|
||||
});
|
||||
test('areLimitsValid from app.areCourseGradeFiltersValid', () => {
|
||||
expect(mapped.areLimitsValid).toEqual(selectors.app.areCourseGradeFiltersValid(testState));
|
||||
});
|
||||
test('localCourseLimits from app.courseGradeLimits', () => {
|
||||
expect(mapped.localCourseLimits).toEqual(selectors.app.courseGradeLimits(testState));
|
||||
});
|
||||
});
|
||||
describe('mapDispatchToProps', () => {
|
||||
test('fetchGrades from thunkActions.grades.fetchGrades', () => {
|
||||
expect(mapDispatchToProps.fetchGrades).toEqual(fetchGrades);
|
||||
});
|
||||
test('setLocalFilter from actions.app.setLocalFilter', () => {
|
||||
expect(mapDispatchToProps.setLocalFilter).toEqual(actions.app.setLocalFilter);
|
||||
});
|
||||
test('updateFilter from actions.filters.update.courseGradeLimits', () => {
|
||||
expect(mapDispatchToProps.updateFilter).toEqual(actions.filters.update.courseGradeLimits);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,72 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`StudentGroupsFilter component render snapshot 1`] = `
|
||||
<Fragment>
|
||||
<SelectGroup
|
||||
id="Tracks"
|
||||
label="Tracks"
|
||||
onChange={[MockFunction]}
|
||||
options={
|
||||
Array [
|
||||
<option
|
||||
value="Track-All"
|
||||
>
|
||||
Track-All
|
||||
</option>,
|
||||
<option
|
||||
value="v1"
|
||||
>
|
||||
n1
|
||||
</option>,
|
||||
<option
|
||||
value="v2"
|
||||
>
|
||||
n2
|
||||
</option>,
|
||||
<option
|
||||
value="v3"
|
||||
>
|
||||
n3
|
||||
</option>,
|
||||
<option
|
||||
value="v4"
|
||||
>
|
||||
n4
|
||||
</option>,
|
||||
]
|
||||
}
|
||||
value="test-track"
|
||||
/>
|
||||
<SelectGroup
|
||||
disabled={false}
|
||||
id="Cohorts"
|
||||
label="Cohorts"
|
||||
onChange={[MockFunction]}
|
||||
options={
|
||||
Array [
|
||||
<option
|
||||
value="Cohort-All"
|
||||
>
|
||||
Cohort-All
|
||||
</option>,
|
||||
<option
|
||||
value="v1"
|
||||
>
|
||||
n1
|
||||
</option>,
|
||||
<option
|
||||
value="v2"
|
||||
>
|
||||
n2
|
||||
</option>,
|
||||
<option
|
||||
value="v3"
|
||||
>
|
||||
n3
|
||||
</option>,
|
||||
]
|
||||
}
|
||||
value="test-cohort"
|
||||
/>
|
||||
</Fragment>
|
||||
`;
|
||||
@@ -1,190 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`StudentGroupsFilter Component snapshots Cohorts group disabled if no cohorts 1`] = `
|
||||
<React.Fragment>
|
||||
<SelectGroup
|
||||
disabled={false}
|
||||
id="Tracks"
|
||||
label="Tracks"
|
||||
onChange={[Function]}
|
||||
options={
|
||||
Array [
|
||||
<option
|
||||
value="Track-All"
|
||||
>
|
||||
Track-All
|
||||
</option>,
|
||||
<option
|
||||
value="TracK1"
|
||||
>
|
||||
TracK1
|
||||
</option>,
|
||||
<option
|
||||
value="TracK2"
|
||||
>
|
||||
TracK2
|
||||
</option>,
|
||||
<option
|
||||
value="TRACK3"
|
||||
>
|
||||
TRACK3
|
||||
</option>,
|
||||
]
|
||||
}
|
||||
value="TracK2"
|
||||
/>
|
||||
<SelectGroup
|
||||
disabled={true}
|
||||
id="Cohorts"
|
||||
label="Cohorts"
|
||||
onChange={[Function]}
|
||||
options={
|
||||
Array [
|
||||
<option
|
||||
value="Cohort-All"
|
||||
>
|
||||
Cohort-All
|
||||
</option>,
|
||||
]
|
||||
}
|
||||
value="cohorT3"
|
||||
/>
|
||||
</React.Fragment>
|
||||
`;
|
||||
|
||||
exports[`StudentGroupsFilter Component snapshots basic snapshot 1`] = `
|
||||
<React.Fragment>
|
||||
<SelectGroup
|
||||
disabled={false}
|
||||
id="Tracks"
|
||||
label="Tracks"
|
||||
onChange={[MockFunction updateTracks]}
|
||||
options={
|
||||
Array [
|
||||
<option
|
||||
value="Track-All"
|
||||
>
|
||||
Track-All
|
||||
</option>,
|
||||
<option
|
||||
value="TracK1"
|
||||
>
|
||||
TracK1
|
||||
</option>,
|
||||
<option
|
||||
value="TracK2"
|
||||
>
|
||||
TracK2
|
||||
</option>,
|
||||
<option
|
||||
value="TRACK3"
|
||||
>
|
||||
TRACK3
|
||||
</option>,
|
||||
]
|
||||
}
|
||||
value="TracK2"
|
||||
/>
|
||||
<SelectGroup
|
||||
disabled={false}
|
||||
id="Cohorts"
|
||||
label="Cohorts"
|
||||
onChange={[MockFunction updateCohorts]}
|
||||
options={
|
||||
Array [
|
||||
<option
|
||||
value="Cohort-All"
|
||||
>
|
||||
Cohort-All
|
||||
</option>,
|
||||
<option
|
||||
value="cohorT1"
|
||||
>
|
||||
cohorT1
|
||||
</option>,
|
||||
<option
|
||||
value="cohorT2"
|
||||
>
|
||||
cohorT2
|
||||
</option>,
|
||||
<option
|
||||
value="cohorT3"
|
||||
>
|
||||
cohorT3
|
||||
</option>,
|
||||
]
|
||||
}
|
||||
value="cohorT3"
|
||||
/>
|
||||
</React.Fragment>
|
||||
`;
|
||||
|
||||
exports[`StudentGroupsFilter Component snapshots mapCohortsEntries cohort options: [Cohort-All, <{slug, name}...>] 1`] = `
|
||||
Array [
|
||||
<option
|
||||
value="Cohort-All"
|
||||
>
|
||||
Cohort-All
|
||||
</option>,
|
||||
<option
|
||||
value="cohorT1"
|
||||
>
|
||||
cohorT1
|
||||
</option>,
|
||||
<option
|
||||
value="cohorT2"
|
||||
>
|
||||
cohorT2
|
||||
</option>,
|
||||
<option
|
||||
value="cohorT3"
|
||||
>
|
||||
cohorT3
|
||||
</option>,
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`StudentGroupsFilter Component snapshots mapTracksEntries cohort options: [Track-All, <{id, name}...>] 1`] = `
|
||||
Array [
|
||||
<option
|
||||
value="Track-All"
|
||||
>
|
||||
Track-All
|
||||
</option>,
|
||||
<option
|
||||
value="TracK1"
|
||||
>
|
||||
TracK1
|
||||
</option>,
|
||||
<option
|
||||
value="TracK2"
|
||||
>
|
||||
TracK2
|
||||
</option>,
|
||||
<option
|
||||
value="TRACK3"
|
||||
>
|
||||
TRACK3
|
||||
</option>,
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`StudentGroupsFilter optionFactory returns a list of options with a default first entry 1`] = `
|
||||
Array [
|
||||
<option
|
||||
value="All-Ponies"
|
||||
>
|
||||
All-Ponies
|
||||
</option>,
|
||||
<option
|
||||
value="RDash"
|
||||
>
|
||||
RDash
|
||||
</option>,
|
||||
<option
|
||||
value="PPie"
|
||||
>
|
||||
PPie
|
||||
</option>,
|
||||
]
|
||||
`;
|
||||
46
src/components/GradebookFilters/StudentGroupsFilter/hooks.js
Normal file
46
src/components/GradebookFilters/StudentGroupsFilter/hooks.js
Normal file
@@ -0,0 +1,46 @@
|
||||
import { actions, selectors, thunkActions } from 'data/redux/hooks';
|
||||
|
||||
export const useStudentGroupsFilterData = ({ updateQueryParams }) => {
|
||||
const selectedCohortEntry = selectors.root.useSelectedCohortEntry();
|
||||
const selectedTrackEntry = selectors.root.useSelectedTrackEntry();
|
||||
|
||||
const cohorts = selectors.cohorts.useAllCohorts();
|
||||
const tracks = selectors.tracks.useAllTracks();
|
||||
|
||||
const updateCohort = actions.filters.useUpdateCohort();
|
||||
const updateTrack = actions.filters.useUpdateTrack();
|
||||
|
||||
const fetchGrades = thunkActions.grades.useFetchGrades();
|
||||
|
||||
const handleUpdateTrack = (event) => {
|
||||
const selectedTrackItem = tracks.find(track => track.slug === event.target.value);
|
||||
const track = selectedTrackItem ? selectedTrackItem.slug.toString() : null;
|
||||
updateQueryParams({ track });
|
||||
updateTrack(track);
|
||||
fetchGrades();
|
||||
};
|
||||
|
||||
const handleUpdateCohort = (event) => {
|
||||
const selectedCohortItem = cohorts.find(cohort => cohort.id === parseInt(event.target.value, 10));
|
||||
const cohort = selectedCohortItem ? selectedCohortItem.id.toString() : null;
|
||||
// the param expected to be cohort_id
|
||||
updateQueryParams({ cohort });
|
||||
updateCohort(cohort);
|
||||
fetchGrades();
|
||||
};
|
||||
return {
|
||||
cohorts: {
|
||||
value: selectedCohortEntry?.id || '',
|
||||
isDisabled: cohorts.length === 0,
|
||||
handleChange: handleUpdateCohort,
|
||||
entries: cohorts.map(({ id: value, name }) => ({ value, name })),
|
||||
},
|
||||
tracks: {
|
||||
value: selectedTrackEntry?.slug || '',
|
||||
handleChange: handleUpdateTrack,
|
||||
entries: tracks.map(({ slug: value, name }) => ({ value, name })),
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export default useStudentGroupsFilterData;
|
||||
@@ -0,0 +1,141 @@
|
||||
import { selectors, actions, thunkActions } from 'data/redux/hooks';
|
||||
|
||||
import useAssignmentFilterData from './hooks';
|
||||
|
||||
jest.mock('data/redux/hooks', () => ({
|
||||
selectors: {
|
||||
root: {
|
||||
useSelectedCohortEntry: jest.fn(),
|
||||
useSelectedTrackEntry: jest.fn(),
|
||||
},
|
||||
cohorts: { useAllCohorts: jest.fn() },
|
||||
tracks: { useAllTracks: jest.fn() },
|
||||
},
|
||||
actions: {
|
||||
filters: {
|
||||
useUpdateCohort: jest.fn(),
|
||||
useUpdateTrack: jest.fn(),
|
||||
},
|
||||
},
|
||||
thunkActions: {
|
||||
grades: { useFetchGrades: jest.fn() },
|
||||
},
|
||||
}));
|
||||
|
||||
let out;
|
||||
|
||||
const testCohort = { name: 'cohort-name', id: 999 };
|
||||
selectors.root.useSelectedCohortEntry.mockReturnValue(testCohort);
|
||||
const testTrack = { name: 'track-name', slug: 8080 };
|
||||
selectors.root.useSelectedTrackEntry.mockReturnValue(testTrack);
|
||||
const allCohorts = [
|
||||
testCohort,
|
||||
{ name: 'cohort1', id: 11 },
|
||||
{ name: 'cohort2', id: 22 },
|
||||
{ name: 'cohort3', id: 33 },
|
||||
];
|
||||
selectors.cohorts.useAllCohorts.mockReturnValue(allCohorts);
|
||||
const allTracks = [
|
||||
testTrack,
|
||||
{ name: 'track1', slug: 111 },
|
||||
{ name: 'track2', slug: 222 },
|
||||
{ name: 'track3', slug: 333 },
|
||||
];
|
||||
selectors.tracks.useAllTracks.mockReturnValue(allTracks);
|
||||
|
||||
const updateCohort = jest.fn();
|
||||
actions.filters.useUpdateCohort.mockReturnValue(updateCohort);
|
||||
const updateTrack = jest.fn();
|
||||
actions.filters.useUpdateTrack.mockReturnValue(updateTrack);
|
||||
const fetch = jest.fn();
|
||||
thunkActions.grades.useFetchGrades.mockReturnValue(fetch);
|
||||
|
||||
const updateQueryParams = jest.fn();
|
||||
|
||||
describe('useAssignmentFilterData hook', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
out = useAssignmentFilterData({ updateQueryParams });
|
||||
});
|
||||
describe('behavior', () => {
|
||||
it('initializes redux hooks', () => {
|
||||
expect(selectors.root.useSelectedCohortEntry).toHaveBeenCalledWith();
|
||||
expect(selectors.root.useSelectedTrackEntry).toHaveBeenCalledWith();
|
||||
expect(selectors.cohorts.useAllCohorts).toHaveBeenCalledWith();
|
||||
expect(selectors.tracks.useAllTracks).toHaveBeenCalledWith();
|
||||
expect(actions.filters.useUpdateCohort).toHaveBeenCalledWith();
|
||||
expect(actions.filters.useUpdateTrack).toHaveBeenCalledWith();
|
||||
expect(thunkActions.grades.useFetchGrades).toHaveBeenCalledWith();
|
||||
});
|
||||
});
|
||||
describe('output', () => {
|
||||
describe('cohorts', () => {
|
||||
test('value from hook', () => {
|
||||
expect(out.cohorts.value).toEqual(testCohort.id);
|
||||
});
|
||||
test('disabled iff no cohorts found', () => {
|
||||
expect(out.cohorts.isDisabled).toEqual(false);
|
||||
selectors.cohorts.useAllCohorts.mockReturnValueOnce([]);
|
||||
out = useAssignmentFilterData({ updateQueryParams });
|
||||
expect(out.cohorts.isDisabled).toEqual(true);
|
||||
});
|
||||
test('entries map id to value', () => {
|
||||
const { entries } = out.cohorts;
|
||||
expect(entries[0]).toEqual({ value: testCohort.id, name: testCohort.name });
|
||||
expect(entries[1]).toEqual({ value: allCohorts[1].id, name: allCohorts[1].name });
|
||||
expect(entries[2]).toEqual({ value: allCohorts[2].id, name: allCohorts[2].name });
|
||||
expect(entries[3]).toEqual({ value: allCohorts[3].id, name: allCohorts[3].name });
|
||||
});
|
||||
test('value defaults to empty string', () => {
|
||||
selectors.root.useSelectedCohortEntry.mockReturnValueOnce(null);
|
||||
out = useAssignmentFilterData({ updateQueryParams });
|
||||
expect(out.cohorts.value).toEqual('');
|
||||
});
|
||||
describe('handleEvent', () => {
|
||||
it('updates filter and query params and fetches grades', () => {
|
||||
out.cohorts.handleChange({ target: { value: testCohort.id } });
|
||||
expect(updateCohort).toHaveBeenCalledWith(testCohort.id.toString());
|
||||
expect(updateQueryParams).toHaveBeenCalledWith({ cohort: testCohort.id.toString() });
|
||||
expect(fetch).toHaveBeenCalled();
|
||||
});
|
||||
it('passes null if no matching track is found', () => {
|
||||
out.cohorts.handleChange({ target: { value: 'fake-name' } });
|
||||
expect(updateCohort).toHaveBeenCalledWith(null);
|
||||
expect(updateQueryParams).toHaveBeenCalledWith({ cohort: null });
|
||||
expect(fetch).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('tracks', () => {
|
||||
test('value from hook', () => {
|
||||
expect(out.tracks.value).toEqual(testTrack.slug);
|
||||
});
|
||||
test('entries map slug to value', () => {
|
||||
const { entries } = out.tracks;
|
||||
expect(entries[0]).toEqual({ value: testTrack.slug, name: testTrack.name });
|
||||
expect(entries[1]).toEqual({ value: allTracks[1].slug, name: allTracks[1].name });
|
||||
expect(entries[2]).toEqual({ value: allTracks[2].slug, name: allTracks[2].name });
|
||||
expect(entries[3]).toEqual({ value: allTracks[3].slug, name: allTracks[3].name });
|
||||
});
|
||||
test('value defaults to empty string', () => {
|
||||
selectors.root.useSelectedTrackEntry.mockReturnValueOnce(null);
|
||||
out = useAssignmentFilterData({ updateQueryParams });
|
||||
expect(out.tracks.value).toEqual('');
|
||||
});
|
||||
describe('handleEvent', () => {
|
||||
it('updates filter and query params and fetches grades', () => {
|
||||
out.tracks.handleChange({ target: { value: testTrack.slug } });
|
||||
expect(updateTrack).toHaveBeenCalledWith(testTrack.slug.toString());
|
||||
expect(updateQueryParams).toHaveBeenCalledWith({ track: testTrack.slug.toString() });
|
||||
expect(fetch).toHaveBeenCalled();
|
||||
});
|
||||
it('passes null if no matching track is found', () => {
|
||||
out.tracks.handleChange({ target: { value: 'fake-name' } });
|
||||
expect(updateTrack).toHaveBeenCalledWith(null);
|
||||
expect(updateQueryParams).toHaveBeenCalledWith({ track: null });
|
||||
expect(fetch).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,152 +1,53 @@
|
||||
/* eslint-disable react/sort-comp, react/button-has-type, import/no-named-as-default */
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import actions from 'data/actions';
|
||||
import selectors from 'data/selectors';
|
||||
import thunkActions from 'data/thunkActions';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import messages from '../messages';
|
||||
import SelectGroup from '../SelectGroup';
|
||||
import useStudentGroupsFilterData from './hooks';
|
||||
|
||||
export const optionFactory = ({ data, defaultOption, key }) => [
|
||||
<option value={defaultOption} key="0">{defaultOption}</option>,
|
||||
...data.map(
|
||||
entry => (<option key={entry[key]} value={entry.name}>{entry.name}</option>),
|
||||
),
|
||||
];
|
||||
const mapOptions = ({ value, name }) => (
|
||||
<option key={name} value={value}>{name}</option>
|
||||
);
|
||||
|
||||
export class StudentGroupsFilter extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.mapCohortsEntries = this.mapCohortsEntries.bind(this);
|
||||
this.mapTracksEntries = this.mapTracksEntries.bind(this);
|
||||
this.updateCohorts = this.updateCohorts.bind(this);
|
||||
this.updateTracks = this.updateTracks.bind(this);
|
||||
}
|
||||
|
||||
mapCohortsEntries() {
|
||||
return optionFactory({
|
||||
data: this.props.cohorts,
|
||||
defaultOption: this.translate(messages.cohortAll),
|
||||
key: 'id',
|
||||
});
|
||||
}
|
||||
|
||||
mapTracksEntries() {
|
||||
return optionFactory({
|
||||
data: this.props.tracks,
|
||||
defaultOption: this.translate(messages.trackAll),
|
||||
key: 'slug',
|
||||
});
|
||||
}
|
||||
|
||||
selectedTrackSlugFromEvent({ target: { value } }) {
|
||||
const selectedTrackItem = this.props.tracksByName[value];
|
||||
return selectedTrackItem ? selectedTrackItem.slug : null;
|
||||
}
|
||||
|
||||
selectedCohortIdFromEvent({ target: { value } }) {
|
||||
const selectedCohortItem = this.props.cohortsByName[value];
|
||||
return selectedCohortItem ? selectedCohortItem.id.toString() : null;
|
||||
}
|
||||
|
||||
updateTracks(event) {
|
||||
const track = this.selectedTrackSlugFromEvent(event);
|
||||
this.props.updateQueryParams({ track });
|
||||
this.props.updateTrack(track);
|
||||
this.props.fetchGrades();
|
||||
}
|
||||
|
||||
updateCohorts(event) {
|
||||
const cohort = this.selectedCohortIdFromEvent(event);
|
||||
this.props.updateQueryParams({ cohort });
|
||||
this.props.updateCohort(cohort);
|
||||
this.props.fetchGrades();
|
||||
}
|
||||
|
||||
translate(message) {
|
||||
return this.props.intl.formatMessage(message);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<>
|
||||
<SelectGroup
|
||||
id="Tracks"
|
||||
label={this.translate(messages.tracks)}
|
||||
value={this.props.selectedTrackEntry.name}
|
||||
onChange={this.updateTracks}
|
||||
options={this.mapTracksEntries()}
|
||||
/>
|
||||
<SelectGroup
|
||||
id="Cohorts"
|
||||
label={this.translate(messages.cohorts)}
|
||||
value={this.props.selectedCohortEntry.name}
|
||||
disabled={this.props.cohorts.length === 0}
|
||||
onChange={this.updateCohorts}
|
||||
options={this.mapCohortsEntries()}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
StudentGroupsFilter.defaultProps = {
|
||||
cohorts: [],
|
||||
cohortsByName: {},
|
||||
selectedCohortEntry: { name: '' },
|
||||
selectedTrackEntry: { name: '' },
|
||||
tracks: [],
|
||||
tracksByName: {},
|
||||
export const StudentGroupsFilter = ({ updateQueryParams }) => {
|
||||
const { tracks, cohorts } = useStudentGroupsFilterData({ updateQueryParams });
|
||||
const { formatMessage } = useIntl();
|
||||
return (
|
||||
<>
|
||||
<SelectGroup
|
||||
id="Tracks"
|
||||
label={formatMessage(messages.tracks)}
|
||||
value={tracks.value}
|
||||
onChange={tracks.handleChange}
|
||||
options={[
|
||||
<option value={formatMessage(messages.trackAll)} key="0">
|
||||
{formatMessage(messages.trackAll)}
|
||||
</option>,
|
||||
...tracks.entries.map(mapOptions),
|
||||
]}
|
||||
/>
|
||||
<SelectGroup
|
||||
id="Cohorts"
|
||||
label={formatMessage(messages.cohorts)}
|
||||
value={cohorts.value}
|
||||
disabled={cohorts.isDisabled}
|
||||
onChange={cohorts.handleChange}
|
||||
options={[
|
||||
<option value={formatMessage(messages.cohortAll)} key="0">
|
||||
{formatMessage(messages.cohortAll)}
|
||||
</option>,
|
||||
...cohorts.entries.map(mapOptions),
|
||||
]}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
StudentGroupsFilter.propTypes = {
|
||||
updateQueryParams: PropTypes.func.isRequired,
|
||||
|
||||
// injected
|
||||
intl: intlShape.isRequired,
|
||||
|
||||
// redux
|
||||
cohorts: PropTypes.arrayOf(PropTypes.shape({
|
||||
name: PropTypes.string,
|
||||
id: PropTypes.number,
|
||||
})),
|
||||
cohortsByName: PropTypes.objectOf(PropTypes.shape({
|
||||
name: PropTypes.string,
|
||||
id: PropTypes.number,
|
||||
})),
|
||||
fetchGrades: PropTypes.func.isRequired,
|
||||
selectedTrackEntry: PropTypes.shape({ name: PropTypes.string }),
|
||||
selectedCohortEntry: PropTypes.shape({ name: PropTypes.string }),
|
||||
tracks: PropTypes.arrayOf(PropTypes.shape({
|
||||
name: PropTypes.string,
|
||||
slug: PropTypes.string,
|
||||
})),
|
||||
tracksByName: PropTypes.objectOf(PropTypes.shape({
|
||||
name: PropTypes.string,
|
||||
slug: PropTypes.string,
|
||||
})),
|
||||
updateCohort: PropTypes.func.isRequired,
|
||||
updateTrack: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export const mapStateToProps = (state) => ({
|
||||
cohorts: selectors.cohorts.allCohorts(state),
|
||||
cohortsByName: selectors.cohorts.cohortsByName(state),
|
||||
selectedCohortEntry: selectors.root.selectedCohortEntry(state),
|
||||
selectedTrackEntry: selectors.root.selectedTrackEntry(state),
|
||||
tracks: selectors.tracks.allTracks(state),
|
||||
tracksByName: selectors.tracks.tracksByName(state),
|
||||
});
|
||||
|
||||
export const mapDispatchToProps = {
|
||||
fetchGrades: thunkActions.grades.fetchGrades,
|
||||
updateCohort: actions.filters.update.cohort,
|
||||
updateTrack: actions.filters.update.track,
|
||||
};
|
||||
|
||||
export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(StudentGroupsFilter));
|
||||
export default StudentGroupsFilter;
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import SelectGroup from '../SelectGroup';
|
||||
import useStudentGroupsFilterData from './hooks';
|
||||
import StudentGroupsFilter from '.';
|
||||
|
||||
jest.mock('../SelectGroup', () => 'SelectGroup');
|
||||
jest.mock('./hooks', () => ({ __esModule: true, default: jest.fn() }));
|
||||
|
||||
const props = {
|
||||
cohorts: {
|
||||
value: 'test-cohort',
|
||||
entries: [
|
||||
{ value: 'v1', name: 'n1' },
|
||||
{ value: 'v2', name: 'n2' },
|
||||
{ value: 'v3', name: 'n3' },
|
||||
],
|
||||
handleChange: jest.fn(),
|
||||
isDisabled: false,
|
||||
},
|
||||
tracks: {
|
||||
value: 'test-track',
|
||||
entries: [
|
||||
{ value: 'v1', name: 'n1' },
|
||||
{ value: 'v2', name: 'n2' },
|
||||
{ value: 'v3', name: 'n3' },
|
||||
{ value: 'v4', name: 'n4' },
|
||||
],
|
||||
handleChange: jest.fn(),
|
||||
},
|
||||
};
|
||||
useStudentGroupsFilterData.mockReturnValue(props);
|
||||
const updateQueryParams = jest.fn();
|
||||
|
||||
let el;
|
||||
describe('StudentGroupsFilter component', () => {
|
||||
beforeAll(() => {
|
||||
jest.clearAllMocks();
|
||||
el = shallow(<StudentGroupsFilter updateQueryParams={updateQueryParams} />);
|
||||
});
|
||||
describe('behavior', () => {
|
||||
it('initializes hooks', () => {
|
||||
expect(useStudentGroupsFilterData).toHaveBeenCalledWith({ updateQueryParams });
|
||||
expect(useIntl).toHaveBeenCalledWith();
|
||||
});
|
||||
});
|
||||
describe('render', () => {
|
||||
test('snapshot', () => {
|
||||
expect(el).toMatchSnapshot();
|
||||
});
|
||||
test('track options', () => {
|
||||
const {
|
||||
options,
|
||||
onChange,
|
||||
value,
|
||||
} = el.find(SelectGroup).at(0).props();
|
||||
expect(value).toEqual(props.tracks.value);
|
||||
expect(onChange).toEqual(props.tracks.handleChange);
|
||||
expect(options.length).toEqual(5);
|
||||
const testEntry = props.tracks.entries[0];
|
||||
const optionProps = options[1].props;
|
||||
expect(optionProps.value).toEqual(testEntry.value);
|
||||
expect(optionProps.children).toEqual(testEntry.name);
|
||||
});
|
||||
test('cohort options', () => {
|
||||
const {
|
||||
options,
|
||||
onChange,
|
||||
disabled,
|
||||
value,
|
||||
} = el.find(SelectGroup).at(1).props();
|
||||
expect(value).toEqual(props.cohorts.value);
|
||||
expect(disabled).toEqual(false);
|
||||
expect(onChange).toEqual(props.cohorts.handleChange);
|
||||
expect(options.length).toEqual(4);
|
||||
const testEntry = props.cohorts.entries[0];
|
||||
const optionProps = options[1].props;
|
||||
expect(optionProps.value).toEqual(testEntry.value);
|
||||
expect(optionProps.children).toEqual(testEntry.name);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,239 +0,0 @@
|
||||
/* eslint-disable import/no-named-as-default */
|
||||
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { fetchGrades } from 'data/thunkActions/grades';
|
||||
import actions from 'data/actions';
|
||||
import selectors from 'data/selectors';
|
||||
import {
|
||||
optionFactory,
|
||||
StudentGroupsFilter,
|
||||
mapStateToProps,
|
||||
mapDispatchToProps,
|
||||
} from '.';
|
||||
|
||||
jest.mock('data/selectors', () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
root: {
|
||||
selectedCohortEntry: jest.fn(state => ({ selectedCohortEntry: state })),
|
||||
selectedTrackEntry: jest.fn(state => ({ selectedTrackEntry: state })),
|
||||
},
|
||||
cohorts: {
|
||||
allCohorts: jest.fn(state => ({ allCohorts: state })),
|
||||
cohortsByName: jest.fn(state => ({ cohortsByName: state })),
|
||||
},
|
||||
tracks: {
|
||||
allTracks: jest.fn(state => ({ allTracks: state })),
|
||||
tracksByName: jest.fn(state => ({ tracksByName: state })),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('data/thunkActions/grades', () => ({
|
||||
fetchGrades: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('StudentGroupsFilter', () => {
|
||||
let props = {
|
||||
cohorts: [
|
||||
{ name: 'cohorT1', id: 8001 },
|
||||
{ name: 'cohorT2', id: 8002 },
|
||||
{ name: 'cohorT3', id: 8003 },
|
||||
],
|
||||
tracks: [
|
||||
{ name: 'TracK1', slug: 'TracK1_slug' },
|
||||
{ name: 'TracK2', slug: 'TracK2_slug' },
|
||||
{ name: 'TRACK3', slug: 'TRACK3_slug' },
|
||||
],
|
||||
};
|
||||
|
||||
describe('optionFactory', () => {
|
||||
it('returns a list of options with a default first entry', () => {
|
||||
const data = [{ cMark: 'rainbow', name: 'RDash' }, { cMark: 'balloons', name: 'PPie' }];
|
||||
const defaultOption = 'All-Ponies';
|
||||
const key = 'cMark';
|
||||
const options = optionFactory({ data, defaultOption, key });
|
||||
expect(options).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Component', () => {
|
||||
beforeEach(() => {
|
||||
props = {
|
||||
...props,
|
||||
intl: { formatMessage: (msg) => msg.defaultMessage },
|
||||
cohortsByName: {
|
||||
[props.cohorts[0].name]: props.cohorts[0],
|
||||
[props.cohorts[1].name]: props.cohorts[1],
|
||||
[props.cohorts[2].name]: props.cohorts[2],
|
||||
},
|
||||
tracksByName: {
|
||||
[props.tracks[0].name]: props.tracks[0],
|
||||
[props.tracks[1].name]: props.tracks[1],
|
||||
[props.tracks[2].name]: props.tracks[2],
|
||||
},
|
||||
fetchGrades: jest.fn(),
|
||||
selectedCohortEntry: props.cohorts[2],
|
||||
selectedTrackEntry: props.tracks[1],
|
||||
updateQueryParams: jest.fn(),
|
||||
updateCohort: jest.fn().mockName('updateCohort'),
|
||||
updateTrack: jest.fn().mockName('updateTrack'),
|
||||
};
|
||||
});
|
||||
|
||||
describe('snapshots', () => {
|
||||
let el;
|
||||
beforeEach(() => {
|
||||
el = shallow(<StudentGroupsFilter {...props} />);
|
||||
});
|
||||
test('basic snapshot', () => {
|
||||
el.instance().updateTracks = jest.fn().mockName(
|
||||
'updateTracks',
|
||||
);
|
||||
el.instance().updateCohorts = jest.fn().mockName(
|
||||
'updateCohorts',
|
||||
);
|
||||
expect(el.instance().render()).toMatchSnapshot();
|
||||
});
|
||||
test('Cohorts group disabled if no cohorts', () => {
|
||||
el.setProps({ cohorts: [] });
|
||||
expect(el.instance().render()).toMatchSnapshot();
|
||||
});
|
||||
describe('mapCohortsEntries', () => {
|
||||
test('cohort options: [Cohort-All, <{slug, name}...>]', () => {
|
||||
expect(el.instance().mapCohortsEntries()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
describe('mapTracksEntries', () => {
|
||||
test('cohort options: [Track-All, <{id, name}...>]', () => {
|
||||
expect(el.instance().mapTracksEntries()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('behavior', () => {
|
||||
let el;
|
||||
beforeEach(() => {
|
||||
el = shallow(<StudentGroupsFilter {...props} />);
|
||||
});
|
||||
describe('selectedCohortIdFromEvent', () => {
|
||||
it('returns the id of the cohort with the name matching the event', () => {
|
||||
expect(
|
||||
el.instance().selectedCohortIdFromEvent(
|
||||
{ target: { value: props.cohorts[1].name } },
|
||||
),
|
||||
).toEqual(props.cohorts[1].id.toString());
|
||||
});
|
||||
it('returns null if no matching cohort is found', () => {
|
||||
expect(
|
||||
el.instance().selectedCohortIdFromEvent(
|
||||
{ target: { value: 'FAKE' } },
|
||||
),
|
||||
).toEqual(null);
|
||||
});
|
||||
});
|
||||
describe('selectedTrackSlugFromEvent', () => {
|
||||
it('returns the slug of the track with the name matching the event', () => {
|
||||
expect(
|
||||
el.instance().selectedTrackSlugFromEvent(
|
||||
{ target: { value: props.tracks[1].name } },
|
||||
),
|
||||
).toEqual(props.tracks[1].slug);
|
||||
});
|
||||
it('returns null if no matching track is found', () => {
|
||||
expect(
|
||||
el.instance().selectedTrackSlugFromEvent(
|
||||
{ target: { value: 'FAKE' } },
|
||||
),
|
||||
).toEqual(null);
|
||||
});
|
||||
});
|
||||
describe('updateTracks', () => {
|
||||
const selectedSlug = 'SLUG';
|
||||
beforeEach(() => {
|
||||
el = shallow(<StudentGroupsFilter {...props} />);
|
||||
jest.spyOn(
|
||||
el.instance(),
|
||||
'selectedTrackSlugFromEvent',
|
||||
).mockReturnValue(selectedSlug);
|
||||
el.instance().updateTracks({ target: {} });
|
||||
});
|
||||
it('calls updateTrack with new value', () => {
|
||||
expect(props.updateTrack).toHaveBeenCalledWith(selectedSlug);
|
||||
});
|
||||
it('calls fetchGrades', () => {
|
||||
expect(props.fetchGrades).toHaveBeenCalledWith();
|
||||
});
|
||||
it('updates queryParams with track value', () => {
|
||||
expect(props.updateQueryParams).toHaveBeenCalledWith({
|
||||
track: selectedSlug,
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('updateCohorts', () => {
|
||||
const selectedId = 23;
|
||||
beforeEach(() => {
|
||||
el = shallow(<StudentGroupsFilter {...props} />);
|
||||
jest.spyOn(
|
||||
el.instance(),
|
||||
'selectedCohortIdFromEvent',
|
||||
).mockReturnValue(selectedId);
|
||||
el.instance().updateCohorts({ target: {} });
|
||||
});
|
||||
it('calls updateCohort with new value', () => {
|
||||
expect(props.updateCohort).toHaveBeenCalledWith(selectedId);
|
||||
});
|
||||
it('calls fetchGrades', () => {
|
||||
expect(props.fetchGrades).toHaveBeenCalledWith();
|
||||
});
|
||||
it('updates queryParams with cohort value', () => {
|
||||
expect(props.updateQueryParams).toHaveBeenCalledWith({
|
||||
cohort: selectedId,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('mapStateToProps', () => {
|
||||
const testState = { h: 'e', l: 'l', o: 'oooooooooo' };
|
||||
let mappedProps;
|
||||
beforeAll(() => {
|
||||
mappedProps = mapStateToProps(testState);
|
||||
});
|
||||
test('cohorts from selectors.cohorts.allCohorts', () => {
|
||||
expect(mappedProps.cohorts).toEqual(selectors.cohorts.allCohorts(testState));
|
||||
});
|
||||
test('cohortsByName from selectors.cohorts.cohortsByName', () => {
|
||||
expect(mappedProps.cohortsByName).toEqual(selectors.cohorts.cohortsByName(testState));
|
||||
});
|
||||
test('selectedCohortEntry from selectors.root.selectedCohortEntry', () => {
|
||||
expect(
|
||||
mappedProps.selectedCohortEntry,
|
||||
).toEqual(selectors.root.selectedCohortEntry(testState));
|
||||
});
|
||||
test('selectedTrackEntry from selectors.root.selectedTrackEntry', () => {
|
||||
expect(
|
||||
mappedProps.selectedTrackEntry,
|
||||
).toEqual(selectors.root.selectedTrackEntry(testState));
|
||||
});
|
||||
test('tracks from selectors.tracks.allTracks', () => {
|
||||
expect(mappedProps.tracks).toEqual(selectors.tracks.allTracks(testState));
|
||||
});
|
||||
test('tracksByName from selectors.tracks.tracksByName', () => {
|
||||
expect(mappedProps.tracksByName).toEqual(selectors.tracks.tracksByName(testState));
|
||||
});
|
||||
});
|
||||
describe('mapDispatchToProps', () => {
|
||||
test('fetchGrades from thunkActions.grades.fetchGrades', () => {
|
||||
expect(mapDispatchToProps.fetchGrades).toEqual(fetchGrades);
|
||||
});
|
||||
test('updateCohort from actions.filters.update.cohort', () => {
|
||||
expect(mapDispatchToProps.updateCohort).toEqual(actions.filters.update.cohort);
|
||||
});
|
||||
test('updateTrack from actions.filters.update.track', () => {
|
||||
expect(mapDispatchToProps.updateTrack).toEqual(actions.filters.update.track);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -4,29 +4,22 @@ exports[`PercentGroup Component snapshots basic snapshot 1`] = `
|
||||
<div
|
||||
className="percent-group"
|
||||
>
|
||||
<FormGroup
|
||||
as="div"
|
||||
<Form.Group
|
||||
controlId="group id"
|
||||
isInvalid={false}
|
||||
isValid={false}
|
||||
>
|
||||
<FormLabel
|
||||
isInline={false}
|
||||
>
|
||||
<Form.Label>
|
||||
Group Label
|
||||
</FormLabel>
|
||||
<ForwardRef
|
||||
as="input"
|
||||
</Form.Label>
|
||||
<Form.Control
|
||||
disabled={false}
|
||||
max={100}
|
||||
min={0}
|
||||
onChange={[MockFunction props.onChange]}
|
||||
plaintext={false}
|
||||
step={1}
|
||||
type="number"
|
||||
value="group VALUE"
|
||||
/>
|
||||
</FormGroup>
|
||||
</Form.Group>
|
||||
<span
|
||||
className="input-percent-label"
|
||||
>
|
||||
@@ -39,29 +32,22 @@ exports[`PercentGroup Component snapshots disabled 1`] = `
|
||||
<div
|
||||
className="percent-group"
|
||||
>
|
||||
<FormGroup
|
||||
as="div"
|
||||
<Form.Group
|
||||
controlId="group id"
|
||||
isInvalid={false}
|
||||
isValid={false}
|
||||
>
|
||||
<FormLabel
|
||||
isInline={false}
|
||||
>
|
||||
<Form.Label>
|
||||
Group Label
|
||||
</FormLabel>
|
||||
<ForwardRef
|
||||
as="input"
|
||||
</Form.Label>
|
||||
<Form.Control
|
||||
disabled={true}
|
||||
max={100}
|
||||
min={0}
|
||||
onChange={[MockFunction props.onChange]}
|
||||
plaintext={false}
|
||||
step={1}
|
||||
type="number"
|
||||
value="group VALUE"
|
||||
/>
|
||||
</FormGroup>
|
||||
</Form.Group>
|
||||
<span
|
||||
className="input-percent-label"
|
||||
>
|
||||
|
||||
@@ -4,22 +4,16 @@ exports[`SelectGroup Component snapshots basic snapshot 1`] = `
|
||||
<div
|
||||
className="student-filters"
|
||||
>
|
||||
<FormGroup
|
||||
as="div"
|
||||
<Form.Group
|
||||
controlId="group id"
|
||||
isInvalid={false}
|
||||
isValid={false}
|
||||
>
|
||||
<FormLabel
|
||||
isInline={false}
|
||||
>
|
||||
<Form.Label>
|
||||
Group Label
|
||||
</FormLabel>
|
||||
<ForwardRef
|
||||
</Form.Label>
|
||||
<Form.Control
|
||||
as="select"
|
||||
disabled={false}
|
||||
onChange={[MockFunction props.onChange]}
|
||||
plaintext={false}
|
||||
value="group VALUE"
|
||||
>
|
||||
<option
|
||||
@@ -40,8 +34,8 @@ exports[`SelectGroup Component snapshots basic snapshot 1`] = `
|
||||
>
|
||||
Option 3
|
||||
</option>
|
||||
</ForwardRef>
|
||||
</FormGroup>
|
||||
</Form.Control>
|
||||
</Form.Group>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -49,22 +43,16 @@ exports[`SelectGroup Component snapshots disabled 1`] = `
|
||||
<div
|
||||
className="student-filters"
|
||||
>
|
||||
<FormGroup
|
||||
as="div"
|
||||
<Form.Group
|
||||
controlId="group id"
|
||||
isInvalid={false}
|
||||
isValid={false}
|
||||
>
|
||||
<FormLabel
|
||||
isInline={false}
|
||||
>
|
||||
<Form.Label>
|
||||
Group Label
|
||||
</FormLabel>
|
||||
<ForwardRef
|
||||
</Form.Label>
|
||||
<Form.Control
|
||||
as="select"
|
||||
disabled={true}
|
||||
onChange={[MockFunction props.onChange]}
|
||||
plaintext={false}
|
||||
value="group VALUE"
|
||||
>
|
||||
<option
|
||||
@@ -85,7 +73,7 @@ exports[`SelectGroup Component snapshots disabled 1`] = `
|
||||
>
|
||||
Option 3
|
||||
</option>
|
||||
</ForwardRef>
|
||||
</FormGroup>
|
||||
</Form.Control>
|
||||
</Form.Group>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`GradebookFilters render snapshot 1`] = `
|
||||
<Fragment>
|
||||
<div
|
||||
className="filter-sidebar-header"
|
||||
>
|
||||
<h2>
|
||||
<Icon
|
||||
className="fa fa-filter"
|
||||
/>
|
||||
</h2>
|
||||
<IconButton
|
||||
alt="Close Filters"
|
||||
aria-label="Close Filters"
|
||||
className="p-1"
|
||||
iconAs="Icon"
|
||||
onClick={[MockFunction hook.closeMenu]}
|
||||
src={[Function]}
|
||||
/>
|
||||
</div>
|
||||
<Collapsible
|
||||
className="filter-group mb-3"
|
||||
defaultOpen={true}
|
||||
title="Assignments"
|
||||
>
|
||||
<div>
|
||||
<AssignmentTypeFilter
|
||||
updateQueryParams={[MockFunction]}
|
||||
/>
|
||||
<AssignmentFilter
|
||||
updateQueryParams={[MockFunction]}
|
||||
/>
|
||||
<AssignmentGradeFilter
|
||||
updateQueryParams={[MockFunction]}
|
||||
/>
|
||||
</div>
|
||||
</Collapsible>
|
||||
<Collapsible
|
||||
className="filter-group mb-3"
|
||||
defaultOpen={true}
|
||||
title="Overall Grade"
|
||||
>
|
||||
<CourseGradeFilter
|
||||
updateQueryParams={[MockFunction]}
|
||||
/>
|
||||
</Collapsible>
|
||||
<Collapsible
|
||||
className="filter-group mb-3"
|
||||
defaultOpen={true}
|
||||
title="Student Groups"
|
||||
>
|
||||
<StudentGroupsFilter
|
||||
updateQueryParams={[MockFunction]}
|
||||
/>
|
||||
</Collapsible>
|
||||
<Collapsible
|
||||
className="filter-group mb-3"
|
||||
defaultOpen={true}
|
||||
title="Include Course Team Members"
|
||||
>
|
||||
<Form.Checkbox
|
||||
checked={true}
|
||||
onChange={[MockFunction hook.handleChange]}
|
||||
>
|
||||
Include Course Team Members
|
||||
</Form.Checkbox>
|
||||
</Collapsible>
|
||||
</Fragment>
|
||||
`;
|
||||
@@ -1,98 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`GradebookFilters Component snapshots basic snapshot 1`] = `
|
||||
<React.Fragment>
|
||||
<div
|
||||
className="filter-sidebar-header"
|
||||
>
|
||||
<h2>
|
||||
<Icon
|
||||
className="fa fa-filter"
|
||||
/>
|
||||
</h2>
|
||||
<IconButton
|
||||
alt="Close Filters"
|
||||
aria-label="Close Filters"
|
||||
className="p-1"
|
||||
iconAs="Icon"
|
||||
onClick={[MockFunction this.props.closeMenu]}
|
||||
src="paragon.icons.Close"
|
||||
/>
|
||||
</div>
|
||||
<Collapsible
|
||||
className="filter-group mb-3"
|
||||
defaultOpen={true}
|
||||
title={
|
||||
<FormattedMessage
|
||||
defaultMessage="Assignments"
|
||||
description="Assignment filter group label in Gradebook Filters"
|
||||
id="gradebook.GradebookFilters.assignmentsFilterLabel"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<div>
|
||||
<Connect(AssignmentTypeFilter)
|
||||
updateQueryParams={[MockFunction]}
|
||||
/>
|
||||
<Connect(AssignmentFilter)
|
||||
updateQueryParams={[MockFunction]}
|
||||
/>
|
||||
<Connect(AssignmentGradeFilter)
|
||||
updateQueryParams={[MockFunction]}
|
||||
/>
|
||||
</div>
|
||||
</Collapsible>
|
||||
<Collapsible
|
||||
className="filter-group mb-3"
|
||||
defaultOpen={true}
|
||||
title={
|
||||
<FormattedMessage
|
||||
defaultMessage="Overall Grade"
|
||||
description="Overall Grade filter group label in Gradebook Filters"
|
||||
id="gradebook.GradebookFilters.overallGradeFilterLabel"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Connect(CourseGradeFilter)
|
||||
updateQueryParams={[MockFunction]}
|
||||
/>
|
||||
</Collapsible>
|
||||
<Collapsible
|
||||
className="filter-group mb-3"
|
||||
defaultOpen={true}
|
||||
title={
|
||||
<FormattedMessage
|
||||
defaultMessage="Student Groups"
|
||||
description="Student Groups filter group label in Gradebook Filters"
|
||||
id="gradebook.GradebookFilters.studentGroupsFilterLabel"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<InjectIntl(ShimmedIntlComponent)
|
||||
updateQueryParams={[MockFunction]}
|
||||
/>
|
||||
</Collapsible>
|
||||
<Collapsible
|
||||
className="filter-group mb-3"
|
||||
defaultOpen={true}
|
||||
title={
|
||||
<FormattedMessage
|
||||
defaultMessage="Include Course Team Members"
|
||||
description="Include Course Team Members filter label in Gradebook Filters"
|
||||
id="gradebook.GradebookFilters.includeCourseTeamMembersFilterLabel"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Checkbox
|
||||
checked={true}
|
||||
onChange={[MockFunction handleIncludeTeamMembersChange]}
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Include Course Team Members"
|
||||
description="Include Course Team Members filter label in Gradebook Filters"
|
||||
id="gradebook.GradebookFilters.includeCourseTeamMembersFilterLabel"
|
||||
/>
|
||||
</Checkbox>
|
||||
</Collapsible>
|
||||
</React.Fragment>
|
||||
`;
|
||||
23
src/components/GradebookFilters/hooks.js
Normal file
23
src/components/GradebookFilters/hooks.js
Normal file
@@ -0,0 +1,23 @@
|
||||
import { actions, selectors, thunkActions } from 'data/redux/hooks';
|
||||
|
||||
export const useGradebookFiltersData = ({ updateQueryParams }) => {
|
||||
const includeCourseRoleMembers = selectors.filters.useIncludeCourseRoleMembers();
|
||||
const updateIncludeCourseRoleMembers = actions.filters.useUpdateIncludeCourseRoleMembers();
|
||||
const closeMenu = thunkActions.app.useCloseFilterMenu();
|
||||
const fetchGrades = thunkActions.grades.useFetchGrades();
|
||||
|
||||
const handleIncludeTeamMembersChange = ({ target: { checked } }) => {
|
||||
updateIncludeCourseRoleMembers(checked);
|
||||
fetchGrades();
|
||||
updateQueryParams({ includeCourseRoleMembers: checked });
|
||||
};
|
||||
return {
|
||||
closeMenu,
|
||||
includeCourseTeamMembers: {
|
||||
handleChange: handleIncludeTeamMembersChange,
|
||||
value: includeCourseRoleMembers,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export default useGradebookFiltersData;
|
||||
57
src/components/GradebookFilters/hooks.test.jsx
Normal file
57
src/components/GradebookFilters/hooks.test.jsx
Normal file
@@ -0,0 +1,57 @@
|
||||
import { actions, selectors, thunkActions } from 'data/redux/hooks';
|
||||
import * as hooks from './hooks';
|
||||
|
||||
jest.mock('data/redux/hooks', () => ({
|
||||
actions: {
|
||||
filters: { useUpdateIncludeCourseRoleMembers: jest.fn() },
|
||||
},
|
||||
selectors: {
|
||||
filters: { useIncludeCourseRoleMembers: jest.fn() },
|
||||
},
|
||||
thunkActions: {
|
||||
app: { useCloseFilterMenu: jest.fn() },
|
||||
grades: { useFetchGrades: jest.fn() },
|
||||
},
|
||||
}));
|
||||
|
||||
selectors.filters.useIncludeCourseRoleMembers.mockReturnValue(true);
|
||||
const updateIncludeCourseRoleMembers = jest.fn();
|
||||
actions.filters.useUpdateIncludeCourseRoleMembers.mockReturnValue(updateIncludeCourseRoleMembers);
|
||||
const closeFilterMenu = jest.fn();
|
||||
thunkActions.app.useCloseFilterMenu.mockReturnValue(closeFilterMenu);
|
||||
const fetchGrades = jest.fn();
|
||||
thunkActions.grades.useFetchGrades.mockReturnValue(fetchGrades);
|
||||
|
||||
const updateQueryParams = jest.fn();
|
||||
|
||||
let out;
|
||||
describe('GradebookFiltersData component hooks', () => {
|
||||
describe('useGradebookFiltersData', () => {
|
||||
beforeEach(() => {
|
||||
out = hooks.useGradebookFiltersData({ updateQueryParams });
|
||||
});
|
||||
describe('behavior', () => {
|
||||
it('initializes hooks', () => {
|
||||
expect(actions.filters.useUpdateIncludeCourseRoleMembers).toHaveBeenCalledWith();
|
||||
expect(selectors.filters.useIncludeCourseRoleMembers).toHaveBeenCalledWith();
|
||||
expect(thunkActions.app.useCloseFilterMenu).toHaveBeenCalledWith();
|
||||
expect(thunkActions.grades.useFetchGrades).toHaveBeenCalledWith();
|
||||
});
|
||||
});
|
||||
describe('output', () => {
|
||||
test('closeMenu', () => {
|
||||
expect(out.closeMenu).toEqual(closeFilterMenu);
|
||||
});
|
||||
test('includeCourseTeamMembers value', () => {
|
||||
expect(out.includeCourseTeamMembers.value).toEqual(true);
|
||||
});
|
||||
test('includeCourseTeamMembers handleChange', () => {
|
||||
const event = { target: { checked: false } };
|
||||
out.includeCourseTeamMembers.handleChange(event);
|
||||
expect(updateIncludeCourseRoleMembers).toHaveBeenCalledWith(false);
|
||||
expect(fetchGrades).toHaveBeenCalledWith();
|
||||
expect(updateQueryParams).toHaveBeenCalledWith({ includeCourseRoleMembers: false });
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,7 +1,5 @@
|
||||
/* eslint-disable react/sort-comp, import/no-named-as-default */
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import {
|
||||
Collapsible,
|
||||
@@ -10,11 +8,7 @@ import {
|
||||
Form,
|
||||
} from '@edx/paragon';
|
||||
import { Close } from '@edx/paragon/icons';
|
||||
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import actions from 'data/actions';
|
||||
import selectors from 'data/selectors';
|
||||
import thunkActions from 'data/thunkActions';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import messages from './messages';
|
||||
import AssignmentTypeFilter from './AssignmentTypeFilter';
|
||||
@@ -22,103 +16,74 @@ import AssignmentFilter from './AssignmentFilter';
|
||||
import AssignmentGradeFilter from './AssignmentGradeFilter';
|
||||
import CourseGradeFilter from './CourseGradeFilter';
|
||||
import StudentGroupsFilter from './StudentGroupsFilter';
|
||||
import useGradebookFiltersData from './hooks';
|
||||
|
||||
export class GradebookFilters extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
includeCourseRoleMembers: this.props.includeCourseRoleMembers,
|
||||
};
|
||||
this.handleIncludeTeamMembersChange = this.handleIncludeTeamMembersChange.bind(this);
|
||||
}
|
||||
export const GradebookFilters = ({ updateQueryParams }) => {
|
||||
const {
|
||||
closeMenu,
|
||||
includeCourseTeamMembers,
|
||||
} = useGradebookFiltersData({ updateQueryParams });
|
||||
const { formatMessage } = useIntl();
|
||||
const collapsibleClassName = 'filter-group mb-3';
|
||||
return (
|
||||
<>
|
||||
<div className="filter-sidebar-header">
|
||||
<h2><Icon className="fa fa-filter" /></h2>
|
||||
<IconButton
|
||||
className="p-1"
|
||||
onClick={closeMenu}
|
||||
iconAs={Icon}
|
||||
src={Close}
|
||||
alt={formatMessage(messages.closeFilters)}
|
||||
aria-label={formatMessage(messages.closeFilters)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
handleIncludeTeamMembersChange(event) {
|
||||
const includeCourseRoleMembers = event.target.checked;
|
||||
this.setState({ includeCourseRoleMembers });
|
||||
this.props.updateIncludeCourseRoleMembers(includeCourseRoleMembers);
|
||||
this.props.fetchGrades();
|
||||
this.props.updateQueryParams({ includeCourseRoleMembers });
|
||||
}
|
||||
|
||||
collapsibleGroup = (title, content) => (
|
||||
<Collapsible
|
||||
title={<FormattedMessage {...title} />}
|
||||
defaultOpen
|
||||
className="filter-group mb-3"
|
||||
>
|
||||
{content}
|
||||
</Collapsible>
|
||||
);
|
||||
|
||||
render() {
|
||||
const {
|
||||
intl,
|
||||
updateQueryParams,
|
||||
} = this.props;
|
||||
return (
|
||||
<>
|
||||
<div className="filter-sidebar-header">
|
||||
<h2><Icon className="fa fa-filter" /></h2>
|
||||
<IconButton
|
||||
className="p-1"
|
||||
onClick={this.props.closeMenu}
|
||||
iconAs={Icon}
|
||||
src={Close}
|
||||
alt={intl.formatMessage(messages.closeFilters)}
|
||||
aria-label={intl.formatMessage(messages.closeFilters)}
|
||||
/>
|
||||
<Collapsible
|
||||
title={formatMessage(messages.assignments)}
|
||||
defaultOpen
|
||||
className={collapsibleClassName}
|
||||
>
|
||||
<div>
|
||||
<AssignmentTypeFilter updateQueryParams={updateQueryParams} />
|
||||
<AssignmentFilter updateQueryParams={updateQueryParams} />
|
||||
<AssignmentGradeFilter updateQueryParams={updateQueryParams} />
|
||||
</div>
|
||||
</Collapsible>
|
||||
|
||||
{this.collapsibleGroup(messages.assignments, (
|
||||
<div>
|
||||
<AssignmentTypeFilter updateQueryParams={updateQueryParams} />
|
||||
<AssignmentFilter updateQueryParams={updateQueryParams} />
|
||||
<AssignmentGradeFilter updateQueryParams={updateQueryParams} />
|
||||
</div>
|
||||
))}
|
||||
<Collapsible
|
||||
title={formatMessage(messages.overallGrade)}
|
||||
defaultOpen
|
||||
className={collapsibleClassName}
|
||||
>
|
||||
<CourseGradeFilter updateQueryParams={updateQueryParams} />
|
||||
</Collapsible>
|
||||
|
||||
{this.collapsibleGroup(messages.overallGrade, (
|
||||
<CourseGradeFilter updateQueryParams={updateQueryParams} />
|
||||
))}
|
||||
<Collapsible
|
||||
title={formatMessage(messages.studentGroups)}
|
||||
defaultOpen
|
||||
className={collapsibleClassName}
|
||||
>
|
||||
<StudentGroupsFilter updateQueryParams={updateQueryParams} />
|
||||
</Collapsible>
|
||||
|
||||
{this.collapsibleGroup(messages.studentGroups, (
|
||||
<StudentGroupsFilter updateQueryParams={updateQueryParams} />
|
||||
))}
|
||||
|
||||
{this.collapsibleGroup(messages.includeCourseTeamMembers, (
|
||||
<Form.Checkbox
|
||||
checked={this.state.includeCourseRoleMembers}
|
||||
onChange={this.handleIncludeTeamMembersChange}
|
||||
>
|
||||
<FormattedMessage {...messages.includeCourseTeamMembers} />
|
||||
</Form.Checkbox>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
GradebookFilters.defaultProps = {
|
||||
includeCourseRoleMembers: false,
|
||||
<Collapsible
|
||||
title={formatMessage(messages.includeCourseTeamMembers)}
|
||||
defaultOpen
|
||||
className={collapsibleClassName}
|
||||
>
|
||||
<Form.Checkbox
|
||||
checked={includeCourseTeamMembers.value}
|
||||
onChange={includeCourseTeamMembers.handleChange}
|
||||
>
|
||||
{formatMessage(messages.includeCourseTeamMembers)}
|
||||
</Form.Checkbox>
|
||||
</Collapsible>
|
||||
</>
|
||||
);
|
||||
};
|
||||
GradebookFilters.propTypes = {
|
||||
updateQueryParams: PropTypes.func.isRequired,
|
||||
// injected
|
||||
intl: intlShape.isRequired,
|
||||
// redux
|
||||
closeMenu: PropTypes.func.isRequired,
|
||||
fetchGrades: PropTypes.func.isRequired,
|
||||
includeCourseRoleMembers: PropTypes.bool,
|
||||
updateIncludeCourseRoleMembers: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export const mapStateToProps = (state) => ({
|
||||
includeCourseRoleMembers: selectors.filters.includeCourseRoleMembers(state),
|
||||
});
|
||||
|
||||
export const mapDispatchToProps = {
|
||||
closeMenu: thunkActions.app.filterMenu.close,
|
||||
fetchGrades: thunkActions.grades.fetchGrades,
|
||||
updateIncludeCourseRoleMembers: actions.filters.update.includeCourseRoleMembers,
|
||||
};
|
||||
|
||||
export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(GradebookFilters));
|
||||
export default GradebookFilters;
|
||||
|
||||
82
src/components/GradebookFilters/index.test.jsx
Normal file
82
src/components/GradebookFilters/index.test.jsx
Normal file
@@ -0,0 +1,82 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Collapsible } from '@edx/paragon';
|
||||
|
||||
import { formatMessage } from 'testUtils';
|
||||
|
||||
import AssignmentTypeFilter from './AssignmentTypeFilter';
|
||||
import AssignmentFilter from './AssignmentFilter';
|
||||
import AssignmentGradeFilter from './AssignmentGradeFilter';
|
||||
import CourseGradeFilter from './CourseGradeFilter';
|
||||
import StudentGroupsFilter from './StudentGroupsFilter';
|
||||
import messages from './messages';
|
||||
|
||||
import useGradebookFiltersData from './hooks';
|
||||
import GradebookFilters from '.';
|
||||
|
||||
jest.mock('./AssignmentTypeFilter', () => 'AssignmentTypeFilter');
|
||||
jest.mock('./AssignmentFilter', () => 'AssignmentFilter');
|
||||
jest.mock('./AssignmentGradeFilter', () => 'AssignmentGradeFilter');
|
||||
jest.mock('./CourseGradeFilter', () => 'CourseGradeFilter');
|
||||
jest.mock('./StudentGroupsFilter', () => 'StudentGroupsFilter');
|
||||
|
||||
jest.mock('./hooks', () => ({ __esModule: true, default: jest.fn() }));
|
||||
|
||||
const hookProps = {
|
||||
closeMenu: jest.fn().mockName('hook.closeMenu'),
|
||||
includeCourseTeamMembers: {
|
||||
value: true,
|
||||
handleChange: jest.fn().mockName('hook.handleChange'),
|
||||
},
|
||||
};
|
||||
useGradebookFiltersData.mockReturnValue(hookProps);
|
||||
|
||||
let el;
|
||||
const updateQueryParams = jest.fn();
|
||||
|
||||
describe('GradebookFilters', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
el = shallow(<GradebookFilters updateQueryParams={updateQueryParams} />);
|
||||
});
|
||||
describe('behavior', () => {
|
||||
it('initializes hooks', () => {
|
||||
expect(useGradebookFiltersData).toHaveBeenCalledWith({ updateQueryParams });
|
||||
expect(useIntl).toHaveBeenCalledWith();
|
||||
});
|
||||
});
|
||||
describe('render', () => {
|
||||
test('snapshot', () => {
|
||||
expect(el).toMatchSnapshot();
|
||||
});
|
||||
test('Assignment filters', () => {
|
||||
expect(el.find(Collapsible).at(0).children()).toMatchObject(shallow(
|
||||
<div>
|
||||
<AssignmentTypeFilter updateQueryParams={updateQueryParams} />
|
||||
<AssignmentFilter updateQueryParams={updateQueryParams} />
|
||||
<AssignmentGradeFilter updateQueryParams={updateQueryParams} />
|
||||
</div>,
|
||||
));
|
||||
});
|
||||
test('CourseGrade filters', () => {
|
||||
expect(el.find(Collapsible).at(1).children()).toMatchObject(shallow(
|
||||
<CourseGradeFilter updateQueryParams={updateQueryParams} />,
|
||||
));
|
||||
});
|
||||
test('StudentGroups filters', () => {
|
||||
expect(el.find(Collapsible).at(2).children()).toMatchObject(shallow(
|
||||
<StudentGroupsFilter updateQueryParams={updateQueryParams} />,
|
||||
));
|
||||
});
|
||||
test('includeCourseTeamMembers', () => {
|
||||
const checkbox = el.find(Collapsible).at(3).children();
|
||||
expect(checkbox.props()).toEqual({
|
||||
checked: true,
|
||||
onChange: hookProps.includeCourseTeamMembers.handleChange,
|
||||
children: formatMessage(messages.includeCourseTeamMembers),
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -66,6 +66,11 @@ const messages = defineMessages({
|
||||
defaultMessage: 'Close Filters',
|
||||
description: 'Button label for Close button in Gradebook Filters',
|
||||
},
|
||||
apply: {
|
||||
id: 'gradebook.GradebookFilters.apply',
|
||||
defaultMessage: 'Apply',
|
||||
description: 'Apply filter button text',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
|
||||
@@ -1,121 +0,0 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import actions from 'data/actions';
|
||||
import selectors from 'data/selectors';
|
||||
import thunkActions from 'data/thunkActions';
|
||||
|
||||
import {
|
||||
GradebookFilters,
|
||||
mapStateToProps,
|
||||
mapDispatchToProps,
|
||||
} from '.';
|
||||
|
||||
jest.mock('@edx/paragon', () => ({
|
||||
Collapsible: 'Collapsible',
|
||||
Form: {
|
||||
Checkbox: 'Checkbox',
|
||||
},
|
||||
Icon: 'Icon',
|
||||
IconButton: 'IconButton',
|
||||
}));
|
||||
jest.mock('@edx/paragon/icons', () => ({
|
||||
Close: 'paragon.icons.Close',
|
||||
}));
|
||||
jest.mock('data/selectors', () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
filters: {
|
||||
includeCourseRoleMembers: jest.fn((state) => ({ includeCourseRoleMembers: state })),
|
||||
},
|
||||
},
|
||||
}));
|
||||
jest.mock('data/thunkActions', () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
app: { filterMenu: { close: jest.fn() } },
|
||||
grades: { fetchGrades: jest.fn() },
|
||||
},
|
||||
}));
|
||||
|
||||
describe('GradebookFilters', () => {
|
||||
let props = {
|
||||
includeCourseRoleMembers: true,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
props = {
|
||||
...props,
|
||||
intl: { formatMessage: (msg) => msg.defaultMessage },
|
||||
closeMenu: jest.fn().mockName('this.props.closeMenu'),
|
||||
fetchGrades: jest.fn(),
|
||||
updateIncludeCourseRoleMembers: jest.fn(),
|
||||
updateQueryParams: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
describe('Component', () => {
|
||||
describe('behavior', () => {
|
||||
describe('handleIncludeTeamMembersChange', () => {
|
||||
let el;
|
||||
beforeEach(() => {
|
||||
el = shallow(<GradebookFilters {...props} />);
|
||||
el.instance().setState = jest.fn();
|
||||
});
|
||||
it('calls setState with newVal', () => {
|
||||
el.instance().handleIncludeTeamMembersChange(
|
||||
{ target: { checked: true } },
|
||||
);
|
||||
expect(
|
||||
el.instance().setState,
|
||||
).toHaveBeenCalledWith({ includeCourseRoleMembers: true });
|
||||
});
|
||||
it('calls props.updateIncludeCourseRoleMembers with newVal', () => {
|
||||
el.instance().handleIncludeTeamMembersChange(
|
||||
{ target: { checked: false } },
|
||||
);
|
||||
expect(
|
||||
props.updateIncludeCourseRoleMembers,
|
||||
).toHaveBeenCalledWith(false);
|
||||
});
|
||||
it('calls props.updateQueryParams with newVal', () => {
|
||||
el.instance().handleIncludeTeamMembersChange(
|
||||
{ target: { checked: true } },
|
||||
);
|
||||
expect(
|
||||
props.updateQueryParams,
|
||||
).toHaveBeenCalledWith({ includeCourseRoleMembers: true });
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('snapshots', () => {
|
||||
test('basic snapshot', () => {
|
||||
const el = shallow(<GradebookFilters {...props} />);
|
||||
el.instance().handleIncludeTeamMembersChange = jest.fn().mockName(
|
||||
'handleIncludeTeamMembersChange',
|
||||
);
|
||||
expect(el.instance().render()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('mapStateToProps', () => {
|
||||
const testState = { A: 'laska' };
|
||||
test('includeCourseRoleMembers from filters.includeCourseRoleMembers', () => {
|
||||
expect(
|
||||
mapStateToProps(testState).includeCourseRoleMembers,
|
||||
).toEqual(selectors.filters.includeCourseRoleMembers(testState));
|
||||
});
|
||||
});
|
||||
describe('mapDispatchToProps', () => {
|
||||
test('fetchGrades from thunkActions.grades.fetchGrades', () => {
|
||||
expect(mapDispatchToProps.fetchGrades).toEqual(thunkActions.grades.fetchGrades);
|
||||
});
|
||||
describe('updateIncludeCourseRoleMembers', () => {
|
||||
test('from actions.filters.update.includeCourseRoleMembers', () => {
|
||||
expect(mapDispatchToProps.updateIncludeCourseRoleMembers).toEqual(
|
||||
actions.filters.update.includeCourseRoleMembers,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -29,9 +29,9 @@ exports[`GradebookHeader component snapshots default values (grades frozen, cann
|
||||
<div
|
||||
className="subtitle-row d-flex justify-content-between align-items-center"
|
||||
>
|
||||
<h3>
|
||||
<h2>
|
||||
fakeID
|
||||
</h3>
|
||||
</h2>
|
||||
</div>
|
||||
<div
|
||||
className="alert alert-warning"
|
||||
@@ -75,9 +75,9 @@ exports[`GradebookHeader component snapshots grades frozen, can view. grades fro
|
||||
<div
|
||||
className="subtitle-row d-flex justify-content-between align-items-center"
|
||||
>
|
||||
<h3>
|
||||
<h2>
|
||||
fakeID
|
||||
</h3>
|
||||
</h2>
|
||||
</div>
|
||||
<div
|
||||
className="alert alert-warning"
|
||||
@@ -121,9 +121,9 @@ exports[`GradebookHeader component snapshots grades frozen, cannot view unauthor
|
||||
<div
|
||||
className="subtitle-row d-flex justify-content-between align-items-center"
|
||||
>
|
||||
<h3>
|
||||
<h2>
|
||||
fakeID
|
||||
</h3>
|
||||
</h2>
|
||||
</div>
|
||||
<div
|
||||
className="alert alert-warning"
|
||||
@@ -177,9 +177,9 @@ exports[`GradebookHeader component snapshots show bulk management, active view i
|
||||
<div
|
||||
className="subtitle-row d-flex justify-content-between align-items-center"
|
||||
>
|
||||
<h3>
|
||||
<h2>
|
||||
fakeID
|
||||
</h3>
|
||||
</h2>
|
||||
<Button
|
||||
onClick={[MockFunction this.handleToggleViewClick]}
|
||||
variant="tertiary"
|
||||
@@ -233,9 +233,9 @@ exports[`GradebookHeader component snapshots show bulk management, active view i
|
||||
<div
|
||||
className="subtitle-row d-flex justify-content-between align-items-center"
|
||||
>
|
||||
<h3>
|
||||
<h2>
|
||||
fakeID
|
||||
</h3>
|
||||
</h2>
|
||||
<Button
|
||||
onClick={[MockFunction this.handleToggleViewClick]}
|
||||
variant="tertiary"
|
||||
|
||||
@@ -2,10 +2,10 @@ 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 { Button } from '@edx/paragon';
|
||||
|
||||
import { configuration } from 'config';
|
||||
import { views } from 'data/constants/app';
|
||||
import actions from 'data/actions';
|
||||
import selectors from 'data/selectors';
|
||||
@@ -18,6 +18,11 @@ export class GradebookHeader extends React.Component {
|
||||
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
|
||||
@@ -25,14 +30,9 @@ export class GradebookHeader extends React.Component {
|
||||
}
|
||||
|
||||
lmsInstructorDashboardUrl = courseId => (
|
||||
`${configuration.LMS_BASE_URL}/courses/${courseId}/instructor`
|
||||
`${getConfig().LMS_BASE_URL}/courses/${courseId}/instructor`
|
||||
);
|
||||
|
||||
handleToggleViewClick() {
|
||||
const newView = this.props.activeView === views.grades ? views.bulkManagementHistory : views.grades;
|
||||
this.props.setView(newView);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="gradebook-header">
|
||||
@@ -47,7 +47,7 @@ export class GradebookHeader extends React.Component {
|
||||
<FormattedMessage {...messages.gradebook} />
|
||||
</h1>
|
||||
<div className="subtitle-row d-flex justify-content-between align-items-center">
|
||||
<h3>{this.props.courseId}</h3>
|
||||
<h2>{this.props.courseId}</h2>
|
||||
{ this.props.showBulkManagement && (
|
||||
<Button
|
||||
variant="tertiary"
|
||||
|
||||
@@ -4,7 +4,7 @@ const messages = defineMessages({
|
||||
downloadGradesBtn: {
|
||||
id: 'gradebook.GradesView.BulkManagementControls.bulkManagementLabel',
|
||||
defaultMessage: 'Download Grades',
|
||||
description: 'Button text for bulk grades download control in GradesView',
|
||||
description: 'A labeled button that allows an admin user to download course grades all at once (in bulk).',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import { Form } from '@edx/paragon';
|
||||
|
||||
import selectors from 'data/selectors';
|
||||
import actions from 'data/actions';
|
||||
import { getLocalizedSlash } from 'i18n';
|
||||
|
||||
/**
|
||||
* <AdjustedGradeInput />
|
||||
@@ -32,7 +33,7 @@ export class AdjustedGradeInput extends React.Component {
|
||||
value={this.props.value}
|
||||
onChange={this.onChange}
|
||||
/>
|
||||
{this.props.possibleGrade && ` / ${this.props.possibleGrade}`}
|
||||
{this.props.possibleGrade && ` ${getLocalizedSlash()} ${this.props.possibleGrade}`}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,40 +1,40 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`OverrideTable Component snapshots basic snapshot shows a row for each entry and one editable row 1`] = `
|
||||
<Table
|
||||
<DataTable
|
||||
columns={
|
||||
Array [
|
||||
Object {
|
||||
"key": "date",
|
||||
"label": <FormattedMessage
|
||||
"Header": <FormattedMessage
|
||||
defaultMessage="Date"
|
||||
description="Edit Modal Override Table Date column header"
|
||||
id="gradebook.GradesView.EditModal.Overrides.dateHeader"
|
||||
/>,
|
||||
"accessor": "date",
|
||||
},
|
||||
Object {
|
||||
"key": "grader",
|
||||
"label": <FormattedMessage
|
||||
"Header": <FormattedMessage
|
||||
defaultMessage="Grader"
|
||||
description="Edit Modal Override Table Grader column header"
|
||||
id="gradebook.GradesView.EditModal.Overrides.graderHeader"
|
||||
/>,
|
||||
"accessor": "grader",
|
||||
},
|
||||
Object {
|
||||
"key": "reason",
|
||||
"label": <FormattedMessage
|
||||
"Header": <FormattedMessage
|
||||
defaultMessage="Reason"
|
||||
description="Edit Modal Override Table Reason column header"
|
||||
id="gradebook.GradesView.EditModal.Overrides.reasonHeader"
|
||||
/>,
|
||||
"accessor": "reason",
|
||||
},
|
||||
Object {
|
||||
"key": "adjustedGrade",
|
||||
"label": <FormattedMessage
|
||||
"Header": <FormattedMessage
|
||||
defaultMessage="Adjusted grade"
|
||||
description="Edit Modal Override Table Adjusted grade column header"
|
||||
id="gradebook.GradesView.EditModal.Overrides.adjustedGradeHeader"
|
||||
/>,
|
||||
"accessor": "adjustedGrade",
|
||||
},
|
||||
]
|
||||
}
|
||||
@@ -59,5 +59,6 @@ exports[`OverrideTable Component snapshots basic snapshot shows a row for each e
|
||||
},
|
||||
]
|
||||
}
|
||||
itemCount={2}
|
||||
/>
|
||||
`;
|
||||
|
||||
@@ -3,7 +3,7 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { Table } from '@edx/paragon';
|
||||
import { DataTable } from '@edx/paragon';
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { gradeOverrideHistoryColumns as columns } from 'data/constants/app';
|
||||
@@ -27,14 +27,14 @@ export const OverrideTable = ({
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<Table
|
||||
<DataTable
|
||||
columns={[
|
||||
{ label: <FormattedMessage {...messages.dateHeader} />, key: columns.date },
|
||||
{ label: <FormattedMessage {...messages.graderHeader} />, key: columns.grader },
|
||||
{ label: <FormattedMessage {...messages.reasonHeader} />, key: columns.reason },
|
||||
{ Header: <FormattedMessage {...messages.dateHeader} />, accessor: columns.date },
|
||||
{ Header: <FormattedMessage {...messages.graderHeader} />, accessor: columns.grader },
|
||||
{ Header: <FormattedMessage {...messages.reasonHeader} />, accessor: columns.reason },
|
||||
{
|
||||
label: <FormattedMessage {...messages.adjustedGradeHeader} />,
|
||||
key: columns.adjustedGrade,
|
||||
Header: <FormattedMessage {...messages.adjustedGradeHeader} />,
|
||||
accessor: columns.adjustedGrade,
|
||||
},
|
||||
]}
|
||||
data={[
|
||||
@@ -45,6 +45,7 @@ export const OverrideTable = ({
|
||||
reason: <ReasonInput />,
|
||||
},
|
||||
]}
|
||||
itemCount={gradeOverrides.length}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
mapStateToProps,
|
||||
} from '.';
|
||||
|
||||
jest.mock('@edx/paragon', () => ({ Table: () => 'Table' }));
|
||||
jest.mock('@edx/paragon', () => ({ DataTable: () => 'DataTable' }));
|
||||
jest.mock('./ReasonInput', () => 'ReasonInput');
|
||||
jest.mock('./AdjustedGradeInput', () => 'AdjustedGradeInput');
|
||||
|
||||
|
||||
@@ -1,16 +1,24 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`EditMoal Component snapshots gradeOverrideHistoryError is and empty and open is true modal open and StatusAlert showing 1`] = `
|
||||
<Modal
|
||||
body={
|
||||
exports[`EditModal Component snapshots gradeOverrideHistoryError is and empty and open is true modal open and StatusAlert showing 1`] = `
|
||||
<ModalDialog
|
||||
hasCloseButton={true}
|
||||
isFullscreenOnMobile={true}
|
||||
isOpen={true}
|
||||
onClose={[MockFunction this.closeAssignmentModal]}
|
||||
size="xl"
|
||||
title="Edit Grades"
|
||||
>
|
||||
<ModalDialog.Body>
|
||||
<div>
|
||||
<ModalHeaders />
|
||||
<StatusAlert
|
||||
alertType="danger"
|
||||
dialog="Weve been trying to contact you regarding..."
|
||||
<Alert
|
||||
dismissible={false}
|
||||
open={true}
|
||||
/>
|
||||
show={true}
|
||||
variant="danger"
|
||||
>
|
||||
Weve been trying to contact you regarding...
|
||||
</Alert>
|
||||
<OverrideTable />
|
||||
<div>
|
||||
<FormattedMessage
|
||||
@@ -27,9 +35,18 @@ exports[`EditMoal Component snapshots gradeOverrideHistoryError is and empty and
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
buttons={
|
||||
Array [
|
||||
</ModalDialog.Body>
|
||||
<ModalDialog.Footer>
|
||||
<ActionRow>
|
||||
<ModalDialog.CloseButton
|
||||
variant="tertiary"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Cancel"
|
||||
description="Edit Modal close button text"
|
||||
id="gradebook.GradesView.EditModal.closeText"
|
||||
/>
|
||||
</ModalDialog.CloseButton>
|
||||
<Button
|
||||
onClick={[MockFunction this.handleAdjustedGradeClick]}
|
||||
variant="primary"
|
||||
@@ -39,39 +56,31 @@ exports[`EditMoal Component snapshots gradeOverrideHistoryError is and empty and
|
||||
description="Edit Modal Save button label"
|
||||
id="gradebook.GradesView.EditModal.saveGrade"
|
||||
/>
|
||||
</Button>,
|
||||
]
|
||||
}
|
||||
closeText={
|
||||
<FormattedMessage
|
||||
defaultMessage="Cancel"
|
||||
description="Edit Modal close button text"
|
||||
id="gradebook.GradesView.EditModal.closeText"
|
||||
/>
|
||||
}
|
||||
onClose={[MockFunction this.closeAssignmentModal]}
|
||||
open={true}
|
||||
title={
|
||||
<FormattedMessage
|
||||
defaultMessage="Edit Grades"
|
||||
description="Edit Modal title"
|
||||
id="gradebook.GradesView.EditModal.title"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</Button>
|
||||
</ActionRow>
|
||||
</ModalDialog.Footer>
|
||||
</ModalDialog>
|
||||
`;
|
||||
|
||||
exports[`EditMoal Component snapshots gradeOverrideHistoryError is empty and open is false modal closed and StatusAlert closed 1`] = `
|
||||
<Modal
|
||||
body={
|
||||
exports[`EditModal Component snapshots gradeOverrideHistoryError is empty and open is false modal closed and StatusAlert closed 1`] = `
|
||||
<ModalDialog
|
||||
hasCloseButton={true}
|
||||
isFullscreenOnMobile={true}
|
||||
isOpen={false}
|
||||
onClose={[MockFunction this.closeAssignmentModal]}
|
||||
size="xl"
|
||||
title="Edit Grades"
|
||||
>
|
||||
<ModalDialog.Body>
|
||||
<div>
|
||||
<ModalHeaders />
|
||||
<StatusAlert
|
||||
alertType="danger"
|
||||
dialog=""
|
||||
<Alert
|
||||
dismissible={false}
|
||||
open={false}
|
||||
/>
|
||||
show={false}
|
||||
variant="danger"
|
||||
>
|
||||
|
||||
</Alert>
|
||||
<OverrideTable />
|
||||
<div>
|
||||
<FormattedMessage
|
||||
@@ -88,9 +97,18 @@ exports[`EditMoal Component snapshots gradeOverrideHistoryError is empty and ope
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
buttons={
|
||||
Array [
|
||||
</ModalDialog.Body>
|
||||
<ModalDialog.Footer>
|
||||
<ActionRow>
|
||||
<ModalDialog.CloseButton
|
||||
variant="tertiary"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Cancel"
|
||||
description="Edit Modal close button text"
|
||||
id="gradebook.GradesView.EditModal.closeText"
|
||||
/>
|
||||
</ModalDialog.CloseButton>
|
||||
<Button
|
||||
onClick={[MockFunction this.handleAdjustedGradeClick]}
|
||||
variant="primary"
|
||||
@@ -100,24 +118,8 @@ exports[`EditMoal Component snapshots gradeOverrideHistoryError is empty and ope
|
||||
description="Edit Modal Save button label"
|
||||
id="gradebook.GradesView.EditModal.saveGrade"
|
||||
/>
|
||||
</Button>,
|
||||
]
|
||||
}
|
||||
closeText={
|
||||
<FormattedMessage
|
||||
defaultMessage="Cancel"
|
||||
description="Edit Modal close button text"
|
||||
id="gradebook.GradesView.EditModal.closeText"
|
||||
/>
|
||||
}
|
||||
onClose={[MockFunction this.closeAssignmentModal]}
|
||||
open={false}
|
||||
title={
|
||||
<FormattedMessage
|
||||
defaultMessage="Edit Grades"
|
||||
description="Edit Modal title"
|
||||
id="gradebook.GradesView.EditModal.title"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</Button>
|
||||
</ActionRow>
|
||||
</ModalDialog.Footer>
|
||||
</ModalDialog>
|
||||
`;
|
||||
|
||||
@@ -5,10 +5,11 @@ import { connect } from 'react-redux';
|
||||
|
||||
import {
|
||||
Button,
|
||||
Modal,
|
||||
StatusAlert,
|
||||
Alert,
|
||||
ModalDialog,
|
||||
ActionRow,
|
||||
} from '@edx/paragon';
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import selectors from 'data/selectors';
|
||||
import actions from 'data/actions';
|
||||
@@ -46,31 +47,40 @@ export class EditModal extends React.Component {
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Modal
|
||||
open={this.props.open}
|
||||
title={<FormattedMessage {...messages.title} />}
|
||||
closeText={<FormattedMessage {...messages.closeText} />}
|
||||
body={(
|
||||
<ModalDialog
|
||||
title={this.props.intl.formatMessage(messages.title)}
|
||||
isOpen={this.props.open}
|
||||
onClose={this.closeAssignmentModal}
|
||||
size="xl"
|
||||
hasCloseButton
|
||||
isFullscreenOnMobile
|
||||
>
|
||||
<ModalDialog.Body>
|
||||
<div>
|
||||
<ModalHeaders />
|
||||
<StatusAlert
|
||||
alertType="danger"
|
||||
dialog={this.props.gradeOverrideHistoryError}
|
||||
open={!!this.props.gradeOverrideHistoryError}
|
||||
<Alert
|
||||
variant="danger"
|
||||
show={!!this.props.gradeOverrideHistoryError}
|
||||
dismissible={false}
|
||||
/>
|
||||
>
|
||||
{this.props.gradeOverrideHistoryError}
|
||||
</Alert>
|
||||
<OverrideTable />
|
||||
<div><FormattedMessage {...messages.visibility} /></div>
|
||||
<div><FormattedMessage {...messages.saveVisibility} /></div>
|
||||
</div>
|
||||
)}
|
||||
buttons={[
|
||||
<Button variant="primary" onClick={this.handleAdjustedGradeClick}>
|
||||
<FormattedMessage {...messages.saveGrade} />
|
||||
</Button>,
|
||||
]}
|
||||
onClose={this.closeAssignmentModal}
|
||||
/>
|
||||
</ModalDialog.Body>
|
||||
<ModalDialog.Footer>
|
||||
<ActionRow>
|
||||
<ModalDialog.CloseButton variant="tertiary">
|
||||
<FormattedMessage {...messages.closeText} />
|
||||
</ModalDialog.CloseButton>
|
||||
<Button variant="primary" onClick={this.handleAdjustedGradeClick}>
|
||||
<FormattedMessage {...messages.saveGrade} />
|
||||
</Button>
|
||||
</ActionRow>
|
||||
</ModalDialog.Footer>
|
||||
</ModalDialog>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -86,6 +96,8 @@ EditModal.propTypes = {
|
||||
closeModal: PropTypes.func.isRequired,
|
||||
doneViewingAssignment: PropTypes.func.isRequired,
|
||||
updateGrades: PropTypes.func.isRequired,
|
||||
// injected
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export const mapStateToProps = (state) => ({
|
||||
@@ -99,4 +111,4 @@ export const mapDispatchToProps = {
|
||||
updateGrades: thunkActions.grades.updateGrades,
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(EditModal);
|
||||
export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(EditModal));
|
||||
|
||||
@@ -9,15 +9,11 @@ import {
|
||||
EditModal,
|
||||
mapDispatchToProps,
|
||||
mapStateToProps,
|
||||
} from '.';
|
||||
}
|
||||
from '.';
|
||||
|
||||
jest.mock('./OverrideTable', () => 'OverrideTable');
|
||||
jest.mock('./ModalHeaders', () => 'ModalHeaders');
|
||||
jest.mock('@edx/paragon', () => ({
|
||||
Button: () => 'Button',
|
||||
Modal: () => 'Modal',
|
||||
StatusAlert: () => 'StatusAlert',
|
||||
}));
|
||||
jest.mock('data/actions', () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
@@ -44,7 +40,7 @@ jest.mock('data/selectors', () => ({
|
||||
},
|
||||
},
|
||||
}));
|
||||
describe('EditMoal', () => {
|
||||
describe('EditModal', () => {
|
||||
let props;
|
||||
beforeEach(() => {
|
||||
props = {
|
||||
@@ -53,6 +49,8 @@ describe('EditMoal', () => {
|
||||
closeModal: jest.fn(),
|
||||
doneViewingAssignment: jest.fn(),
|
||||
updateGrades: jest.fn(),
|
||||
|
||||
intl: { formatMessage: (msg) => msg.defaultMessage },
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -2,20 +2,23 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import * as constants from 'data/constants/filters';
|
||||
import FilterBadges from '.';
|
||||
import FilterBadge from './FilterBadge';
|
||||
|
||||
jest.mock('./FilterBadge', () => 'FilterBadge');
|
||||
|
||||
const order = ['filter1', 'filter2', 'filter3'];
|
||||
jest.mock('data/constants/filters', () => ({
|
||||
...jest.requireActual('data/constants/filters'),
|
||||
badgeOrder: order,
|
||||
}));
|
||||
|
||||
describe('FilterBadges', () => {
|
||||
describe('component', () => {
|
||||
let el;
|
||||
let handleClose;
|
||||
const order = ['filter1', 'filter2', 'filter3'];
|
||||
beforeEach(() => {
|
||||
handleClose = jest.fn().mockName('this.props.handleClose');
|
||||
constants.badgeOrder = order;
|
||||
el = shallow(<FilterBadges handleClose={handleClose} />);
|
||||
});
|
||||
test('snapshot - has a filterbadge with handleClose for each filter in badgeOrder', () => {
|
||||
|
||||
@@ -4,7 +4,7 @@ const messages = defineMessages({
|
||||
editFilters: {
|
||||
id: 'gradebook.GradesView.editFilterLabel',
|
||||
defaultMessage: 'Edit Filters',
|
||||
description: 'Button text on Grades tab to open/close the Filters tab',
|
||||
description: 'A labeled button in the Grades tab that opens/closes the Filters tab, allowing the grades to be filtered',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -29,18 +29,16 @@ Username.propTypes = {
|
||||
};
|
||||
|
||||
/**
|
||||
* Fields.Email
|
||||
* Simple label field for email value.
|
||||
* @param {string} email - email for display
|
||||
* Fields.Text
|
||||
* Simple label field for text value.
|
||||
* @param {string} value - value for display
|
||||
*/
|
||||
const Email = ({ email }) => (
|
||||
<span className="wrap-text-in-cell">{email}</span>
|
||||
);
|
||||
Email.propTypes = {
|
||||
email: PropTypes.string.isRequired,
|
||||
const Text = ({ value }) => (<span className="wrap-text-in-cell">{value}</span>);
|
||||
Text.propTypes = {
|
||||
value: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default StrictDict({
|
||||
Email,
|
||||
Text,
|
||||
Username,
|
||||
});
|
||||
|
||||
@@ -41,13 +41,13 @@ describe('Gradebook Table Fields', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('Email', () => {
|
||||
const email = 'myTag@place.com';
|
||||
describe('Text', () => {
|
||||
const value = 'myTag@place.com';
|
||||
test('snapshot', () => {
|
||||
expect(shallow(<Fields.Email email={email} />)).toMatchSnapshot();
|
||||
expect(shallow(<Fields.Text value={value} />)).toMatchSnapshot();
|
||||
});
|
||||
test('wraps entry email', () => {
|
||||
expect(shallow(<Fields.Email email={email} />).text()).toEqual(email);
|
||||
test('wraps entry value', () => {
|
||||
expect(shallow(<Fields.Text value={value} />).text()).toEqual(value);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
OverlayTrigger,
|
||||
Tooltip,
|
||||
} from '@edx/paragon';
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
import { FormattedMessage, getLocale, isRtl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
@@ -23,7 +23,7 @@ const TotalGradeLabelReplacement = () => (
|
||||
<OverlayTrigger
|
||||
trigger={['hover', 'focus']}
|
||||
key="left-basic"
|
||||
placement="left"
|
||||
placement={isRtl(getLocale()) ? 'right' : 'left'}
|
||||
overlay={(
|
||||
<Tooltip id="course-grade-tooltip">
|
||||
<FormattedMessage {...messages.totalGradePercentage} />
|
||||
@@ -45,6 +45,13 @@ 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
|
||||
@@ -56,11 +63,24 @@ 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,
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { getLocale } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { OverlayTrigger } from '@edx/paragon';
|
||||
|
||||
@@ -8,6 +9,7 @@ import LabelReplacements from './LabelReplacements';
|
||||
const {
|
||||
TotalGradeLabelReplacement,
|
||||
UsernameLabelReplacement,
|
||||
MastersOnlyLabelReplacement,
|
||||
} = LabelReplacements;
|
||||
|
||||
jest.mock('@edx/paragon', () => ({
|
||||
@@ -34,4 +36,28 @@ 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', () => {
|
||||
let el;
|
||||
test('right to left overlay placement', () => {
|
||||
getLocale.mockImplementation(() => 'en');
|
||||
el = shallow(<TotalGradeLabelReplacement />);
|
||||
expect(el).toMatchSnapshot();
|
||||
});
|
||||
test('left to right overlay placement', () => {
|
||||
getLocale.mockImplementation(() => 'ar');
|
||||
el = shallow(<TotalGradeLabelReplacement />);
|
||||
expect(el).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Gradebook Table Fields Email snapshot 1`] = `
|
||||
exports[`Gradebook Table Fields Text snapshot 1`] = `
|
||||
<span
|
||||
className="wrap-text-in-cell"
|
||||
>
|
||||
|
||||
@@ -1,5 +1,20 @@
|
||||
// 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"
|
||||
@@ -73,10 +88,111 @@ 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>
|
||||
`;
|
||||
|
||||
exports[`snapshot left to right overlay placement 1`] = `
|
||||
<div>
|
||||
<OverlayTrigger
|
||||
key="left-basic"
|
||||
overlay={
|
||||
<Tooltip
|
||||
id="course-grade-tooltip"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Total Grade values are always displayed as a percentage"
|
||||
description="Gradebook table message that total grades are displayed in percent format"
|
||||
id="gradebook.GradesView.table.totalGradePercentage"
|
||||
/>
|
||||
</Tooltip>
|
||||
}
|
||||
placement="right"
|
||||
trigger={
|
||||
Array [
|
||||
"hover",
|
||||
"focus",
|
||||
]
|
||||
}
|
||||
>
|
||||
<div>
|
||||
<FormattedMessage
|
||||
defaultMessage="Total Grade (%)"
|
||||
description="Gradebook table total grade column header"
|
||||
id="gradebook.GradesView.table.headings.totalGrade"
|
||||
/>
|
||||
<div
|
||||
id="courseGradeTooltipIcon"
|
||||
>
|
||||
<Icon
|
||||
className="fa fa-info-circle"
|
||||
screenReaderText={
|
||||
<FormattedMessage
|
||||
defaultMessage="Total Grade values are always displayed as a percentage"
|
||||
description="Gradebook table message that total grades are displayed in percent format"
|
||||
id="gradebook.GradesView.table.totalGradePercentage"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</OverlayTrigger>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`snapshot right to left overlay placement 1`] = `
|
||||
<div>
|
||||
<OverlayTrigger
|
||||
key="left-basic"
|
||||
overlay={
|
||||
<Tooltip
|
||||
id="course-grade-tooltip"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Total Grade values are always displayed as a percentage"
|
||||
description="Gradebook table message that total grades are displayed in percent format"
|
||||
id="gradebook.GradesView.table.totalGradePercentage"
|
||||
/>
|
||||
</Tooltip>
|
||||
}
|
||||
placement="left"
|
||||
trigger={
|
||||
Array [
|
||||
"hover",
|
||||
"focus",
|
||||
]
|
||||
}
|
||||
>
|
||||
<div>
|
||||
<FormattedMessage
|
||||
defaultMessage="Total Grade (%)"
|
||||
description="Gradebook table total grade column header"
|
||||
id="gradebook.GradesView.table.headings.totalGrade"
|
||||
/>
|
||||
<div
|
||||
id="courseGradeTooltipIcon"
|
||||
>
|
||||
<Icon
|
||||
className="fa fa-info-circle"
|
||||
screenReaderText={
|
||||
<FormattedMessage
|
||||
defaultMessage="Total Grade values are always displayed as a percentage"
|
||||
description="Gradebook table message that total grades are displayed in percent format"
|
||||
id="gradebook.GradesView.table.totalGradePercentage"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</OverlayTrigger>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -4,48 +4,60 @@ exports[`GradebookTable component snapshot - fields1 and 2 between email and tot
|
||||
<div
|
||||
className="gradebook-container"
|
||||
>
|
||||
<div
|
||||
className="gbook"
|
||||
<DataTable
|
||||
RowStatusComponent={[MockFunction this.nullMethod]}
|
||||
columns={
|
||||
Array [
|
||||
Object {
|
||||
"Header": <UsernameLabelReplacement />,
|
||||
"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"
|
||||
description="Gradebook table email column header"
|
||||
id="gradebook.GradesView.table.headings.email"
|
||||
/>,
|
||||
"accessor": "Email",
|
||||
},
|
||||
Object {
|
||||
"Header": "field1",
|
||||
"accessor": "field1",
|
||||
},
|
||||
Object {
|
||||
"Header": "field2",
|
||||
"accessor": "field2",
|
||||
},
|
||||
Object {
|
||||
"Header": <TotalGradeLabelReplacement />,
|
||||
"accessor": "Total Grade (%)",
|
||||
},
|
||||
]
|
||||
}
|
||||
data={
|
||||
Array [
|
||||
"mappedRow: 1",
|
||||
"mappedRow: 2",
|
||||
"mappedRow: 3",
|
||||
]
|
||||
}
|
||||
hasFixedColumnWidths={true}
|
||||
itemCount={3}
|
||||
rowHeaderColumnKey="username"
|
||||
>
|
||||
<Table
|
||||
columns={
|
||||
Array [
|
||||
Object {
|
||||
"key": "Username",
|
||||
"label": <UsernameLabelReplacement />,
|
||||
},
|
||||
Object {
|
||||
"key": "Email",
|
||||
"label": <FormattedMessage
|
||||
defaultMessage="Email"
|
||||
description="Gradebook table email column header"
|
||||
id="gradebook.GradesView.table.headings.email"
|
||||
/>,
|
||||
},
|
||||
Object {
|
||||
"key": "field1",
|
||||
"label": "field1",
|
||||
},
|
||||
Object {
|
||||
"key": "field2",
|
||||
"label": "field2",
|
||||
},
|
||||
Object {
|
||||
"key": "Total Grade (%)",
|
||||
"label": <TotalGradeLabelReplacement />,
|
||||
},
|
||||
]
|
||||
}
|
||||
data={
|
||||
Array [
|
||||
"mappedRow: 1",
|
||||
"mappedRow: 2",
|
||||
"mappedRow: 3",
|
||||
]
|
||||
}
|
||||
hasFixedColumnWidths={true}
|
||||
rowHeaderColumnKey="username"
|
||||
<DataTable.TableControlBar />
|
||||
<DataTable.Table />
|
||||
<DataTable.EmptyTable
|
||||
content="No results found"
|
||||
/>
|
||||
</div>
|
||||
</DataTable>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -3,11 +3,12 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { Table } from '@edx/paragon';
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
import { DataTable } from '@edx/paragon';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import selectors from 'data/selectors';
|
||||
import { Headings } from 'data/constants/grades';
|
||||
import { getLocalizedPercentSign } from 'i18n';
|
||||
|
||||
import messages from './messages';
|
||||
import Fields from './Fields';
|
||||
@@ -27,6 +28,7 @@ export class GradebookTable extends React.Component {
|
||||
super(props);
|
||||
this.mapHeaders = this.mapHeaders.bind(this);
|
||||
this.mapRows = this.mapRows.bind(this);
|
||||
this.nullMethod = this.nullMethod.bind(this);
|
||||
}
|
||||
|
||||
mapHeaders(heading) {
|
||||
@@ -36,40 +38,47 @@ export class GradebookTable extends React.Component {
|
||||
} else if (heading === Headings.username) {
|
||||
label = <LabelReplacements.UsernameLabelReplacement />;
|
||||
} else if (heading === Headings.email) {
|
||||
label = <FormattedMessage {...messages.emailHeading} />;
|
||||
label = <LabelReplacements.MastersOnlyLabelReplacement {...messages.emailHeading} />;
|
||||
} else if (heading === Headings.fullName) {
|
||||
label = <LabelReplacements.MastersOnlyLabelReplacement {...messages.fullNameHeading} />;
|
||||
} else {
|
||||
label = heading;
|
||||
}
|
||||
return { label, key: heading };
|
||||
return { Header: label, accessor: heading };
|
||||
}
|
||||
|
||||
mapRows(entry) {
|
||||
const dataRow = {
|
||||
[Headings.username]: (
|
||||
<Fields.Username username={entry.username} userKey={entry.external_user_key} />
|
||||
),
|
||||
[Headings.email]: (<Fields.Email email={entry.email} />),
|
||||
[Headings.totalGrade]: `${roundGrade(entry.percent * 100)}%`,
|
||||
};
|
||||
entry.section_breakdown.forEach(subsection => {
|
||||
dataRow[subsection.label] = (
|
||||
<GradeButton {...{ entry, subsection }} />
|
||||
);
|
||||
});
|
||||
return dataRow;
|
||||
mapRows = entry => ({
|
||||
[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.totalGrade]: `${roundGrade(entry.percent * 100)}${getLocalizedPercentSign()}`,
|
||||
...entry.section_breakdown.reduce((acc, subsection) => ({
|
||||
...acc,
|
||||
[subsection.label]: <GradeButton {...{ entry, subsection }} />,
|
||||
}), {}),
|
||||
});
|
||||
|
||||
nullMethod() {
|
||||
return null;
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="gradebook-container">
|
||||
<div className="gbook">
|
||||
<Table
|
||||
columns={this.props.headings.map(this.mapHeaders)}
|
||||
data={this.props.grades.map(this.mapRows)}
|
||||
rowHeaderColumnKey="username"
|
||||
hasFixedColumnWidths
|
||||
/>
|
||||
</div>
|
||||
<DataTable
|
||||
columns={this.props.headings.map(this.mapHeaders)}
|
||||
data={this.props.grades.map(this.mapRows)}
|
||||
rowHeaderColumnKey="username"
|
||||
hasFixedColumnWidths
|
||||
itemCount={this.props.grades.length}
|
||||
RowStatusComponent={this.nullMethod}
|
||||
>
|
||||
<DataTable.TableControlBar />
|
||||
<DataTable.Table />
|
||||
<DataTable.EmptyTable content={this.props.intl.formatMessage(messages.noResultsFound)} />
|
||||
</DataTable>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -97,6 +106,8 @@ GradebookTable.propTypes = {
|
||||
user_name: PropTypes.string,
|
||||
})),
|
||||
headings: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
// injected
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export const mapStateToProps = (state) => ({
|
||||
@@ -104,4 +115,4 @@ export const mapStateToProps = (state) => ({
|
||||
headings: selectors.root.getHeadings(state),
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps)(GradebookTable);
|
||||
export default injectIntl(connect(mapStateToProps)(GradebookTable));
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
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',
|
||||
@@ -18,7 +23,7 @@ const messages = defineMessages({
|
||||
},
|
||||
studentKeyLabel: {
|
||||
id: 'gradebook.GradesView.table.labels.studentKey',
|
||||
defaultMessage: 'Student Key*',
|
||||
defaultMessage: 'Student Key',
|
||||
description: 'Gradebook table Student Key label',
|
||||
},
|
||||
usernameLabel: {
|
||||
@@ -31,6 +36,11 @@ const messages = defineMessages({
|
||||
defaultMessage: 'Total Grade values are always displayed as a percentage',
|
||||
description: 'Gradebook table message that total grades are displayed in percent format',
|
||||
},
|
||||
noResultsFound: {
|
||||
id: 'gradebook.GradesView.table.noResultsFound',
|
||||
defaultMessage: 'No results found',
|
||||
description: 'Gradebook table message when no learner results were found',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { Table } from '@edx/paragon';
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
import { DataTable } from '@edx/paragon';
|
||||
|
||||
import selectors from 'data/selectors';
|
||||
import { Headings } from 'data/constants/grades';
|
||||
@@ -11,14 +10,18 @@ import Fields from './Fields';
|
||||
import messages from './messages';
|
||||
import { GradebookTable, mapStateToProps } from '.';
|
||||
|
||||
jest.mock('@edx/paragon', () => ({
|
||||
Table: () => 'Table',
|
||||
jest.mock('@edx/paragon', () => jest.requireActual('testUtils').mockNestedComponents({
|
||||
DataTable: {
|
||||
Table: 'DataTable.Table',
|
||||
TableControlBar: 'DataTable.TableControlBar',
|
||||
EmptyTable: 'DataTable.EmptyTable',
|
||||
},
|
||||
}));
|
||||
jest.mock('./Fields', () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
Username: () => 'Fields.Username',
|
||||
Email: () => 'Fields.Email',
|
||||
Text: () => 'Fields.Text',
|
||||
},
|
||||
}));
|
||||
jest.mock('./LabelReplacements', () => ({
|
||||
@@ -26,6 +29,7 @@ jest.mock('./LabelReplacements', () => ({
|
||||
default: {
|
||||
TotalGradeLabelReplacement: () => 'TotalGradeLabelReplacement',
|
||||
UsernameLabelReplacement: () => 'UsernameLabelReplacement',
|
||||
MastersOnlyLabelReplacement: () => 'MastersOnlyLabelReplacement',
|
||||
},
|
||||
}));
|
||||
jest.mock('./GradeButton', () => 'GradeButton');
|
||||
@@ -71,48 +75,61 @@ describe('GradebookTable', () => {
|
||||
],
|
||||
headings: [
|
||||
Headings.username,
|
||||
Headings.fullName,
|
||||
Headings.email,
|
||||
fields.field1,
|
||||
fields.field2,
|
||||
Headings.totalGrade,
|
||||
],
|
||||
|
||||
intl: { formatMessage: (msg) => msg.defaultMessage },
|
||||
};
|
||||
test('snapshot - fields1 and 2 between email and totalGrade, mocked rows', () => {
|
||||
el = shallow(<GradebookTable {...props} />);
|
||||
el.instance().nullMethod = jest.fn().mockName('this.nullMethod');
|
||||
el.instance().mapRows = (entry) => `mappedRow: ${entry.percent}`;
|
||||
expect(el.instance().render()).toMatchSnapshot();
|
||||
});
|
||||
test('null method returns null for stub component', () => {
|
||||
el = shallow(<GradebookTable {...props} />);
|
||||
expect(el.instance().nullMethod()).toEqual(null);
|
||||
});
|
||||
describe('table columns (mapHeaders)', () => {
|
||||
let headings;
|
||||
beforeEach(() => {
|
||||
el = shallow(<GradebookTable {...props} />);
|
||||
headings = el.find(Table).props().columns;
|
||||
headings = el.find(DataTable).props().columns;
|
||||
});
|
||||
test('username sets key and replaces label with component', () => {
|
||||
test('username sets key and replaces Header with component', () => {
|
||||
const heading = headings[0];
|
||||
expect(heading.key).toEqual(Headings.username);
|
||||
expect(heading.label.type).toEqual(LabelReplacements.UsernameLabelReplacement);
|
||||
expect(heading.accessor).toEqual(Headings.username);
|
||||
expect(heading.Header.type).toEqual(LabelReplacements.UsernameLabelReplacement);
|
||||
});
|
||||
test('email sets key and label from header', () => {
|
||||
test('full name sets key and Header from header', () => {
|
||||
const heading = headings[1];
|
||||
expect(heading.key).toEqual(Headings.email);
|
||||
expect(heading.label).toEqual(<FormattedMessage {...messages.emailHeading} />);
|
||||
expect(heading.accessor).toEqual(Headings.fullName);
|
||||
expect(heading.Header).toEqual(<LabelReplacements.MastersOnlyLabelReplacement {...messages.fullNameHeading} />);
|
||||
});
|
||||
test('subsections set key and label from header', () => {
|
||||
expect(headings[2]).toEqual({ key: fields.field1, label: fields.field1 });
|
||||
expect(headings[3]).toEqual({ key: fields.field2, label: fields.field2 });
|
||||
test('email sets key and Header from header', () => {
|
||||
const heading = headings[2];
|
||||
expect(heading.accessor).toEqual(Headings.email);
|
||||
expect(heading.Header).toEqual(<LabelReplacements.MastersOnlyLabelReplacement {...messages.emailHeading} />);
|
||||
});
|
||||
test('totalGrade sets key and replaces label with component', () => {
|
||||
const heading = headings[4];
|
||||
expect(heading.key).toEqual(Headings.totalGrade);
|
||||
expect(heading.label.type).toEqual(LabelReplacements.TotalGradeLabelReplacement);
|
||||
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 });
|
||||
});
|
||||
test('totalGrade sets key and replaces Header with component', () => {
|
||||
const heading = headings[5];
|
||||
expect(heading.accessor).toEqual(Headings.totalGrade);
|
||||
expect(heading.Header.type).toEqual(LabelReplacements.TotalGradeLabelReplacement);
|
||||
});
|
||||
});
|
||||
describe('table data (mapRows)', () => {
|
||||
let rows;
|
||||
beforeEach(() => {
|
||||
el = shallow(<GradebookTable {...props} />);
|
||||
rows = el.find(Table).props().data;
|
||||
rows = el.find(DataTable).props().data;
|
||||
});
|
||||
describe.each([0, 1, 2])('gradeEntry($percent)', (gradeIndex) => {
|
||||
let row;
|
||||
@@ -128,10 +145,15 @@ describe('GradebookTable', () => {
|
||||
userKey: entry.external_user_key,
|
||||
});
|
||||
});
|
||||
test('email set to Email Field', () => {
|
||||
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', () => {
|
||||
const field = row[Headings.email];
|
||||
expect(field.type).toEqual(Fields.Email);
|
||||
expect(field.props).toEqual({ email: entry.email });
|
||||
expect(field.type).toEqual(Fields.Text);
|
||||
expect(field.props).toEqual({ value: entry.email });
|
||||
});
|
||||
test('totalGrade set to rounded percent grade * 100', () => {
|
||||
expect(
|
||||
|
||||
@@ -24,14 +24,14 @@
|
||||
.search-help-text {
|
||||
margin-left: 20px;
|
||||
}
|
||||
h4 {
|
||||
h3 {
|
||||
font-weight: bold;
|
||||
margin-top: 0rem;
|
||||
}
|
||||
.import-grades-btn {
|
||||
margin-left: 20px;
|
||||
}
|
||||
.intervention-report-description: {
|
||||
.intervention-report-description {
|
||||
margin-right: 40px;
|
||||
}
|
||||
h4.step-message-1 {
|
||||
@@ -67,104 +67,9 @@
|
||||
overflow-x: auto;
|
||||
height: 600px;
|
||||
overflow-y: auto;
|
||||
word-break: break-word;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.gbook {
|
||||
width: 100%;
|
||||
|
||||
.grade-button {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.student-key {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
#courseGradeTooltipIcon {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.table thead tr {
|
||||
min-height: 60px;
|
||||
&:nth-child(1) {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
background-color: white;
|
||||
th {
|
||||
background-color: white;
|
||||
border-bottom: 1px solid $gray_200;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
thead, tbody, tr, td, th {
|
||||
display: block;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.table tr th:first-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
.table tr th:first-child,
|
||||
.table tr td:first-child {
|
||||
position: sticky;
|
||||
left: 0;
|
||||
z-index: 1; // to float over the following children in the side-scrolling case
|
||||
background: white;
|
||||
}
|
||||
|
||||
.table tr {
|
||||
th:nth-child(1),
|
||||
td:nth-child(1),
|
||||
th:nth-child(2),
|
||||
td:nth-child(2) {
|
||||
width: 240px;
|
||||
}
|
||||
th:nth-last-of-type(1) {
|
||||
width: 150px;
|
||||
}
|
||||
th, td {
|
||||
width: 120px;
|
||||
}
|
||||
}
|
||||
.table tbody th {
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.table {
|
||||
overflow-x: hidden;
|
||||
|
||||
height: 100%;
|
||||
|
||||
tbody {
|
||||
overflow-y: auto;
|
||||
display: block;
|
||||
}
|
||||
|
||||
thead, tbody tr {
|
||||
display: table;
|
||||
table-layout: fixed;
|
||||
}
|
||||
|
||||
th {
|
||||
vertical-align: top;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.link-style {
|
||||
color: #0075b4;
|
||||
&:hover, &:focus {
|
||||
color: #004368;
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.form-group, .pgn__form-group {
|
||||
label {
|
||||
font-weight: bold;
|
||||
@@ -207,3 +112,13 @@
|
||||
select#ScoreView.form-control {
|
||||
padding-right: 26px;
|
||||
}
|
||||
|
||||
[dir=rtl] #course-grade-tooltip .arrow {
|
||||
right: initial;
|
||||
left: 0;
|
||||
|
||||
&:before {
|
||||
border-width: 0.4rem 0.4rem 0.4rem 0;
|
||||
border-right-color: $black;
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user