Compare commits
49 Commits
master
...
jkantor/pt
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7f369caf2e | ||
|
|
21aecb9634 | ||
|
|
aefa116816 | ||
|
|
31b319b141 | ||
|
|
1f952fc454 | ||
|
|
a0f01cb38a | ||
|
|
f4e88ce9ea | ||
|
|
fb6ad622e2 | ||
|
|
2365dbdd06 | ||
|
|
9f4d82fb5d | ||
|
|
47d491099f | ||
|
|
1832786a5d | ||
|
|
a61bb7c382 | ||
|
|
7123ab7bb1 | ||
|
|
bfefacb940 | ||
|
|
85b0571335 | ||
|
|
c13f118ac2 | ||
|
|
3f41d5a10c | ||
|
|
1b44ee222e | ||
|
|
2728d5d4e9 | ||
|
|
6106b65714 | ||
|
|
8ca5513af4 | ||
|
|
3245198877 | ||
|
|
74257bc1f4 | ||
|
|
7656e602b6 | ||
|
|
69a443a571 | ||
|
|
2bfea2823b | ||
|
|
35a0a6456c | ||
|
|
24a9a6a761 | ||
|
|
0caa243a2e | ||
|
|
724039c629 | ||
|
|
e82132df5f | ||
|
|
3846f1eae5 | ||
|
|
11698e055f | ||
|
|
7817ac751c | ||
|
|
0dfbca7cd8 | ||
|
|
5e922a1643 | ||
|
|
60f9abbe2b | ||
|
|
118d5aac31 | ||
|
|
a8e2c080dc | ||
|
|
f0f482cc32 | ||
|
|
dbe917f692 | ||
|
|
69f1ca5a99 | ||
|
|
d4de38a8e7 | ||
|
|
6736e6cd26 | ||
|
|
b6ab78c244 | ||
|
|
662783dbd4 | ||
|
|
b315c0b1e6 | ||
|
|
241e188465 |
3
.env
3
.env
@@ -12,6 +12,7 @@ CREDIT_HELP_LINK_URL=''
|
|||||||
CSRF_TOKEN_API_PATH=''
|
CSRF_TOKEN_API_PATH=''
|
||||||
DISCOVERY_API_BASE_URL=''
|
DISCOVERY_API_BASE_URL=''
|
||||||
DISCUSSIONS_MFE_BASE_URL=''
|
DISCUSSIONS_MFE_BASE_URL=''
|
||||||
|
DISCOUNT_CODE_INFO_URL=''
|
||||||
ECOMMERCE_BASE_URL=''
|
ECOMMERCE_BASE_URL=''
|
||||||
ENABLE_JUMPNAV='true'
|
ENABLE_JUMPNAV='true'
|
||||||
ENABLE_NOTICES=''
|
ENABLE_NOTICES=''
|
||||||
@@ -50,3 +51,5 @@ TWITTER_URL=''
|
|||||||
USER_INFO_COOKIE_NAME=''
|
USER_INFO_COOKIE_NAME=''
|
||||||
OPTIMIZELY_FULL_STACK_SDK_KEY=''
|
OPTIMIZELY_FULL_STACK_SDK_KEY=''
|
||||||
SHOW_UNGRADED_ASSIGNMENT_PROGRESS=''
|
SHOW_UNGRADED_ASSIGNMENT_PROGRESS=''
|
||||||
|
# Fallback in local style files
|
||||||
|
PARAGON_THEME_URLS={}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ CREDIT_HELP_LINK_URL='https://help.edx.org/edxlearner/s/article/Can-I-receive-co
|
|||||||
CSRF_TOKEN_API_PATH='/csrf/api/v1/token'
|
CSRF_TOKEN_API_PATH='/csrf/api/v1/token'
|
||||||
DISCOVERY_API_BASE_URL='http://localhost:18381'
|
DISCOVERY_API_BASE_URL='http://localhost:18381'
|
||||||
DISCUSSIONS_MFE_BASE_URL='http://localhost:2002'
|
DISCUSSIONS_MFE_BASE_URL='http://localhost:2002'
|
||||||
|
DISCOUNT_CODE_INFO_URL=''
|
||||||
ECOMMERCE_BASE_URL='http://localhost:18130'
|
ECOMMERCE_BASE_URL='http://localhost:18130'
|
||||||
ENABLE_JUMPNAV='true'
|
ENABLE_JUMPNAV='true'
|
||||||
ENABLE_NOTICES=''
|
ENABLE_NOTICES=''
|
||||||
@@ -52,3 +53,5 @@ CHAT_RESPONSE_URL='http://localhost:18000/api/learning_assistant/v1/course_id'
|
|||||||
PRIVACY_POLICY_URL='http://localhost:18000/privacy'
|
PRIVACY_POLICY_URL='http://localhost:18000/privacy'
|
||||||
OPTIMIZELY_FULL_STACK_SDK_KEY=''
|
OPTIMIZELY_FULL_STACK_SDK_KEY=''
|
||||||
SHOW_UNGRADED_ASSIGNMENT_PROGRESS=''
|
SHOW_UNGRADED_ASSIGNMENT_PROGRESS=''
|
||||||
|
# Fallback in local style files
|
||||||
|
PARAGON_THEME_URLS={}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ CREDIT_HELP_LINK_URL='https://help.edx.org/edxlearner/s/article/Can-I-receive-co
|
|||||||
CSRF_TOKEN_API_PATH='/csrf/api/v1/token'
|
CSRF_TOKEN_API_PATH='/csrf/api/v1/token'
|
||||||
DISCOVERY_API_BASE_URL='http://localhost:18381'
|
DISCOVERY_API_BASE_URL='http://localhost:18381'
|
||||||
DISCUSSIONS_MFE_BASE_URL='http://localhost:2002'
|
DISCUSSIONS_MFE_BASE_URL='http://localhost:2002'
|
||||||
|
DISCOUNT_CODE_INFO_URL=''
|
||||||
ECOMMERCE_BASE_URL='http://localhost:18130'
|
ECOMMERCE_BASE_URL='http://localhost:18130'
|
||||||
ENABLE_JUMPNAV='true'
|
ENABLE_JUMPNAV='true'
|
||||||
ENABLE_NOTICES=''
|
ENABLE_NOTICES=''
|
||||||
@@ -50,3 +51,4 @@ USER_INFO_COOKIE_NAME='edx-user-info'
|
|||||||
PRIVACY_POLICY_URL='http://localhost:18000/privacy'
|
PRIVACY_POLICY_URL='http://localhost:18000/privacy'
|
||||||
SHOW_UNGRADED_ASSIGNMENT_PROGRESS=''
|
SHOW_UNGRADED_ASSIGNMENT_PROGRESS=''
|
||||||
ENTERPRISE_LEARNER_PORTAL_URL='http://localhost:Enterprise'
|
ENTERPRISE_LEARNER_PORTAL_URL='http://localhost:Enterprise'
|
||||||
|
FEATURE_ENABLE_CHAT_V2_ENDPOINT='false'
|
||||||
|
|||||||
5
Makefile
5
Makefile
@@ -40,9 +40,10 @@ pull_translations:
|
|||||||
translations/frontend-component-header/src/i18n/messages:frontend-component-header \
|
translations/frontend-component-header/src/i18n/messages:frontend-component-header \
|
||||||
translations/frontend-component-footer/src/i18n/messages:frontend-component-footer \
|
translations/frontend-component-footer/src/i18n/messages:frontend-component-footer \
|
||||||
translations/frontend-lib-special-exams/src/i18n/messages:frontend-lib-special-exams \
|
translations/frontend-lib-special-exams/src/i18n/messages:frontend-lib-special-exams \
|
||||||
translations/frontend-app-learning/src/i18n/messages:frontend-app-learning
|
translations/frontend-app-learning/src/i18n/messages:frontend-app-learning \
|
||||||
|
$(ATLAS_EXTRA_SOURCES)
|
||||||
|
|
||||||
$(intl_imports) frontend-platform paragon frontend-component-header frontend-component-footer frontend-lib-special-exams frontend-app-learning
|
$(intl_imports) frontend-platform paragon frontend-component-header frontend-component-footer frontend-lib-special-exams frontend-app-learning $(ATLAS_EXTRA_INTL_IMPORTS)
|
||||||
|
|
||||||
|
|
||||||
# This target is used by Travis.
|
# This target is used by Travis.
|
||||||
|
|||||||
1196
package-lock.json
generated
1196
package-lock.json
generated
@@ -13,7 +13,7 @@
|
|||||||
"@edx/browserslist-config": "1.5.0",
|
"@edx/browserslist-config": "1.5.0",
|
||||||
"@edx/frontend-component-footer": "^14.6.0",
|
"@edx/frontend-component-footer": "^14.6.0",
|
||||||
"@edx/frontend-component-header": "^6.2.0",
|
"@edx/frontend-component-header": "^6.2.0",
|
||||||
"@edx/frontend-lib-learning-assistant": "^2.20.0",
|
"@edx/frontend-lib-learning-assistant": "^2.24.0",
|
||||||
"@edx/frontend-lib-special-exams": "^4.0.0",
|
"@edx/frontend-lib-special-exams": "^4.0.0",
|
||||||
"@edx/frontend-platform": "^8.3.1",
|
"@edx/frontend-platform": "^8.3.1",
|
||||||
"@edx/openedx-atlas": "^0.7.0",
|
"@edx/openedx-atlas": "^0.7.0",
|
||||||
@@ -24,7 +24,7 @@
|
|||||||
"@fortawesome/react-fontawesome": "^0.1.4",
|
"@fortawesome/react-fontawesome": "^0.1.4",
|
||||||
"@openedx/frontend-build": "^14.5.0",
|
"@openedx/frontend-build": "^14.5.0",
|
||||||
"@openedx/frontend-plugin-framework": "^1.7.0",
|
"@openedx/frontend-plugin-framework": "^1.7.0",
|
||||||
"@openedx/paragon": "^22.16.0",
|
"@openedx/paragon": "^23.4.5",
|
||||||
"@popperjs/core": "2.11.8",
|
"@popperjs/core": "2.11.8",
|
||||||
"@reduxjs/toolkit": "1.9.7",
|
"@reduxjs/toolkit": "1.9.7",
|
||||||
"buffer": "^6.0.3",
|
"buffer": "^6.0.3",
|
||||||
@@ -1940,6 +1940,158 @@
|
|||||||
"integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==",
|
"integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@bundled-es-modules/deepmerge": {
|
||||||
|
"version": "4.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@bundled-es-modules/deepmerge/-/deepmerge-4.3.1.tgz",
|
||||||
|
"integrity": "sha512-Rk453EklPUPC3NRWc3VUNI/SSUjdBaFoaQvFRmNBNtMHVtOFD5AntiWg5kEE1hqcPqedYFDzxE3ZcMYPcA195w==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"deepmerge": "^4.3.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@bundled-es-modules/glob": {
|
||||||
|
"version": "10.4.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@bundled-es-modules/glob/-/glob-10.4.2.tgz",
|
||||||
|
"integrity": "sha512-740y5ofkzydsFao5EXJrGilcIL6EFEw/cmPf2uhTw9J6G1YOhiIFjNFCHdpgEiiH5VlU3G0SARSjlFlimRRSMA==",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"buffer": "^6.0.3",
|
||||||
|
"events": "^3.3.0",
|
||||||
|
"glob": "^10.4.2",
|
||||||
|
"patch-package": "^8.0.0",
|
||||||
|
"path": "^0.12.7",
|
||||||
|
"stream": "^0.0.3",
|
||||||
|
"string_decoder": "^1.3.0",
|
||||||
|
"url": "^0.11.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@bundled-es-modules/glob/node_modules/brace-expansion": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"balanced-match": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@bundled-es-modules/glob/node_modules/glob": {
|
||||||
|
"version": "10.4.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
|
||||||
|
"integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"foreground-child": "^3.1.0",
|
||||||
|
"jackspeak": "^3.1.2",
|
||||||
|
"minimatch": "^9.0.4",
|
||||||
|
"minipass": "^7.1.2",
|
||||||
|
"package-json-from-dist": "^1.0.0",
|
||||||
|
"path-scurry": "^1.11.1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"glob": "dist/esm/bin.mjs"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@bundled-es-modules/glob/node_modules/minimatch": {
|
||||||
|
"version": "9.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
|
||||||
|
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"brace-expansion": "^2.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16 || 14 >=14.17"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@bundled-es-modules/memfs": {
|
||||||
|
"version": "4.17.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@bundled-es-modules/memfs/-/memfs-4.17.0.tgz",
|
||||||
|
"integrity": "sha512-ykdrkEmQr9BV804yd37ikXfNnvxrwYfY9Z2/EtMHFEFadEjsQXJ1zL9bVZrKNLDtm91UdUOEHso6Aweg93K6xQ==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"assert": "^2.1.0",
|
||||||
|
"buffer": "^6.0.3",
|
||||||
|
"events": "^3.3.0",
|
||||||
|
"memfs": "^4.17.0",
|
||||||
|
"path": "^0.12.7",
|
||||||
|
"stream": "^0.0.3",
|
||||||
|
"util": "^0.12.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@bundled-es-modules/memfs/node_modules/memfs": {
|
||||||
|
"version": "4.17.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/memfs/-/memfs-4.17.1.tgz",
|
||||||
|
"integrity": "sha512-thuTRd7F4m4dReCIy7vv4eNYnU6XI/tHMLSMMHLiortw/Y0QxqKtinG523U2aerzwYWGi606oBP4oMPy4+edag==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@jsonjoy.com/json-pack": "^1.0.3",
|
||||||
|
"@jsonjoy.com/util": "^1.3.0",
|
||||||
|
"tree-dump": "^1.0.1",
|
||||||
|
"tslib": "^2.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 4.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/streamich"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@bundled-es-modules/postcss-calc-ast-parser": {
|
||||||
|
"version": "0.1.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@bundled-es-modules/postcss-calc-ast-parser/-/postcss-calc-ast-parser-0.1.6.tgz",
|
||||||
|
"integrity": "sha512-y65TM5zF+uaxo9OeekJ3rxwTINlQvrkbZLogYvQYVoLtxm4xEiHfZ7e/MyiWbStYyWZVZkVqsaVU6F4SUK5XUA==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"postcss-calc-ast-parser": "^0.1.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@chevrotain/cst-dts-gen": {
|
||||||
|
"version": "11.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@chevrotain/cst-dts-gen/-/cst-dts-gen-11.0.3.tgz",
|
||||||
|
"integrity": "sha512-BvIKpRLeS/8UbfxXxgC33xOumsacaeCKAjAeLyOn7Pcp95HiRbrpl14S+9vaZLolnbssPIUuiUd8IvgkRyt6NQ==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@chevrotain/gast": "11.0.3",
|
||||||
|
"@chevrotain/types": "11.0.3",
|
||||||
|
"lodash-es": "4.17.21"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@chevrotain/gast": {
|
||||||
|
"version": "11.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@chevrotain/gast/-/gast-11.0.3.tgz",
|
||||||
|
"integrity": "sha512-+qNfcoNk70PyS/uxmj3li5NiECO+2YKZZQMbmjTqRI3Qchu8Hig/Q9vgkHpI3alNjr7M+a2St5pw5w5F6NL5/Q==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@chevrotain/types": "11.0.3",
|
||||||
|
"lodash-es": "4.17.21"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@chevrotain/regexp-to-ast": {
|
||||||
|
"version": "11.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@chevrotain/regexp-to-ast/-/regexp-to-ast-11.0.3.tgz",
|
||||||
|
"integrity": "sha512-1fMHaBZxLFvWI067AVbGJav1eRY7N8DDvYCTwGBiE/ytKBgP8azTdgyrKyWZ9Mfh09eHWb5PgTSO8wi7U824RA==",
|
||||||
|
"license": "Apache-2.0"
|
||||||
|
},
|
||||||
|
"node_modules/@chevrotain/types": {
|
||||||
|
"version": "11.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-11.0.3.tgz",
|
||||||
|
"integrity": "sha512-gsiM3G8b58kZC2HaWR50gu6Y1440cHiJ+i3JUvcp/35JchYejb2+5MVeJK0iKThYpAa/P2PYFV4hoi44HD+aHQ==",
|
||||||
|
"license": "Apache-2.0"
|
||||||
|
},
|
||||||
|
"node_modules/@chevrotain/utils": {
|
||||||
|
"version": "11.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@chevrotain/utils/-/utils-11.0.3.tgz",
|
||||||
|
"integrity": "sha512-YslZMgtJUyuMbZ+aKvfF3x1f5liK4mWNxghFRv7jqRR9C3R3fAOGTTKvxXDa2Y1s9zSbcpuO0cAxDYsc9SrXoQ==",
|
||||||
|
"license": "Apache-2.0"
|
||||||
|
},
|
||||||
"node_modules/@cospired/i18n-iso-languages": {
|
"node_modules/@cospired/i18n-iso-languages": {
|
||||||
"version": "4.2.0",
|
"version": "4.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/@cospired/i18n-iso-languages/-/i18n-iso-languages-4.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/@cospired/i18n-iso-languages/-/i18n-iso-languages-4.2.0.tgz",
|
||||||
@@ -2261,9 +2413,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@edx/frontend-lib-learning-assistant": {
|
"node_modules/@edx/frontend-lib-learning-assistant": {
|
||||||
"version": "2.21.0",
|
"version": "2.24.0",
|
||||||
"resolved": "https://registry.npmjs.org/@edx/frontend-lib-learning-assistant/-/frontend-lib-learning-assistant-2.21.0.tgz",
|
"resolved": "https://registry.npmjs.org/@edx/frontend-lib-learning-assistant/-/frontend-lib-learning-assistant-2.24.0.tgz",
|
||||||
"integrity": "sha512-CUzPCQaBgXi6E1kvY0nyBSVFu8RUGpwKH4V0p8ZuysyHyRHpA+339b+gEi9FvVBMP/X4IxZHsZhi7nphlr43Iw==",
|
"integrity": "sha512-+RwmKbYxsJ6Ct9scBX3jnxSUuoiW5ed1vbCz9PQiQ8fobuiMM3fokLynIreB5ZVYWvrjSa5OaMwBq1bUXsprZw==",
|
||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@edx/brand": "npm:@openedx/brand-openedx@^1.2.3",
|
"@edx/brand": "npm:@openedx/brand-openedx@^1.2.3",
|
||||||
@@ -3154,6 +3306,102 @@
|
|||||||
"deprecated": "Use @eslint/object-schema instead",
|
"deprecated": "Use @eslint/object-schema instead",
|
||||||
"license": "BSD-3-Clause"
|
"license": "BSD-3-Clause"
|
||||||
},
|
},
|
||||||
|
"node_modules/@isaacs/cliui": {
|
||||||
|
"version": "8.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
|
||||||
|
"integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"string-width": "^5.1.2",
|
||||||
|
"string-width-cjs": "npm:string-width@^4.2.0",
|
||||||
|
"strip-ansi": "^7.0.1",
|
||||||
|
"strip-ansi-cjs": "npm:strip-ansi@^6.0.1",
|
||||||
|
"wrap-ansi": "^8.1.0",
|
||||||
|
"wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@isaacs/cliui/node_modules/ansi-regex": {
|
||||||
|
"version": "6.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz",
|
||||||
|
"integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/chalk/ansi-regex?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@isaacs/cliui/node_modules/ansi-styles": {
|
||||||
|
"version": "6.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
|
||||||
|
"integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@isaacs/cliui/node_modules/emoji-regex": {
|
||||||
|
"version": "9.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
|
||||||
|
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@isaacs/cliui/node_modules/string-width": {
|
||||||
|
"version": "5.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
|
||||||
|
"integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"eastasianwidth": "^0.2.0",
|
||||||
|
"emoji-regex": "^9.2.2",
|
||||||
|
"strip-ansi": "^7.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@isaacs/cliui/node_modules/strip-ansi": {
|
||||||
|
"version": "7.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
|
||||||
|
"integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ansi-regex": "^6.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@isaacs/cliui/node_modules/wrap-ansi": {
|
||||||
|
"version": "8.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
|
||||||
|
"integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ansi-styles": "^6.1.0",
|
||||||
|
"string-width": "^5.0.1",
|
||||||
|
"strip-ansi": "^7.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@istanbuljs/load-nyc-config": {
|
"node_modules/@istanbuljs/load-nyc-config": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz",
|
||||||
@@ -3556,6 +3804,60 @@
|
|||||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@jsonjoy.com/base64": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@jsonjoy.com/base64/-/base64-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-q6XAnWQDIMA3+FTiOYajoYqySkO+JSat0ytXGSuRdq9uXE7o92gzuQwQM14xaCRlBLGq3v5miDGC4vkVTn54xA==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/streamich"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"tslib": "2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@jsonjoy.com/json-pack": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pack/-/json-pack-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-io1zEbbYcElht3tdlqEOFxZ0dMTYrHz9iMf0gqn1pPjZFTCgM5R4R5IMA20Chb2UPYYsxjzs8CgZ7Nb5n2K2rA==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@jsonjoy.com/base64": "^1.1.1",
|
||||||
|
"@jsonjoy.com/util": "^1.1.2",
|
||||||
|
"hyperdyperid": "^1.2.0",
|
||||||
|
"thingies": "^1.20.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/streamich"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"tslib": "2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@jsonjoy.com/util": {
|
||||||
|
"version": "1.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@jsonjoy.com/util/-/util-1.6.0.tgz",
|
||||||
|
"integrity": "sha512-sw/RMbehRhN68WRtcKCpQOPfnH6lLP4GJfqzi3iYej8tnzpZUDr6UkZYJjcjjC0FWEJOJbyM3PTIwxucUmDG2A==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/streamich"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"tslib": "2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@leichtgewicht/ip-codec": {
|
"node_modules/@leichtgewicht/ip-codec": {
|
||||||
"version": "2.0.5",
|
"version": "2.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz",
|
||||||
@@ -4137,9 +4439,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@openedx/paragon": {
|
"node_modules/@openedx/paragon": {
|
||||||
"version": "22.17.0",
|
"version": "23.4.5",
|
||||||
"resolved": "https://registry.npmjs.org/@openedx/paragon/-/paragon-22.17.0.tgz",
|
"resolved": "https://registry.npmjs.org/@openedx/paragon/-/paragon-23.4.5.tgz",
|
||||||
"integrity": "sha512-MzOLQ0myaOErwumPJwxVZXTw7zJKrARtu4YMSaISF5Sz6pE1/dYz9qfRcqaraYRcJGNdbPRzOG0v3iqbZo1uHQ==",
|
"integrity": "sha512-baBTZDO6hdCjI+Jj3oSQaz5GkFTR2TEs94pPMOls7bc/Fsv4zQIN8xDPo4NzAVo/2+3eSuEzUz3xzBeb+94rtw==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"example",
|
"example",
|
||||||
@@ -4149,20 +4451,32 @@
|
|||||||
"dependent-usage-analyzer"
|
"dependent-usage-analyzer"
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fortawesome/fontawesome-svg-core": "^6.1.1",
|
|
||||||
"@fortawesome/react-fontawesome": "^0.1.18",
|
|
||||||
"@popperjs/core": "^2.11.4",
|
"@popperjs/core": "^2.11.4",
|
||||||
|
"@tokens-studio/sd-transforms": "^1.2.4",
|
||||||
|
"axios": "^0.27.2",
|
||||||
"bootstrap": "^4.6.2",
|
"bootstrap": "^4.6.2",
|
||||||
"chalk": "^4.1.2",
|
"chalk": "^4.1.2",
|
||||||
"child_process": "^1.0.2",
|
"child_process": "^1.0.2",
|
||||||
|
"chroma-js": "^2.4.2",
|
||||||
"classnames": "^2.3.1",
|
"classnames": "^2.3.1",
|
||||||
|
"cli-progress": "^3.12.0",
|
||||||
|
"commander": "^9.4.1",
|
||||||
"email-prop-type": "^3.0.0",
|
"email-prop-type": "^3.0.0",
|
||||||
"file-selector": "^0.6.0",
|
"file-selector": "^0.6.0",
|
||||||
"font-awesome": "^4.7.0",
|
|
||||||
"glob": "^8.0.3",
|
"glob": "^8.0.3",
|
||||||
"inquirer": "^8.2.5",
|
"inquirer": "^8.2.5",
|
||||||
|
"js-toml": "^1.0.0",
|
||||||
"lodash.uniqby": "^4.7.0",
|
"lodash.uniqby": "^4.7.0",
|
||||||
|
"log-update": "^4.0.0",
|
||||||
"mailto-link": "^2.0.0",
|
"mailto-link": "^2.0.0",
|
||||||
|
"minimist": "^1.2.8",
|
||||||
|
"ora": "^5.4.1",
|
||||||
|
"postcss": "^8.4.21",
|
||||||
|
"postcss-combine-duplicated-selectors": "^10.0.3",
|
||||||
|
"postcss-custom-media": "^9.1.2",
|
||||||
|
"postcss-import": "^15.1.0",
|
||||||
|
"postcss-map": "^0.11.0",
|
||||||
|
"postcss-minify": "^1.1.0",
|
||||||
"prop-types": "^15.8.1",
|
"prop-types": "^15.8.1",
|
||||||
"react-bootstrap": "^1.6.5",
|
"react-bootstrap": "^1.6.5",
|
||||||
"react-colorful": "^5.6.1",
|
"react-colorful": "^5.6.1",
|
||||||
@@ -4175,6 +4489,8 @@
|
|||||||
"react-responsive": "^8.2.0",
|
"react-responsive": "^8.2.0",
|
||||||
"react-table": "^7.7.0",
|
"react-table": "^7.7.0",
|
||||||
"react-transition-group": "^4.4.2",
|
"react-transition-group": "^4.4.2",
|
||||||
|
"sass": "^1.58.3",
|
||||||
|
"style-dictionary": "^4.3.2",
|
||||||
"tabbable": "^5.3.3",
|
"tabbable": "^5.3.3",
|
||||||
"uncontrollable": "^7.2.1",
|
"uncontrollable": "^7.2.1",
|
||||||
"uuid": "^9.0.0"
|
"uuid": "^9.0.0"
|
||||||
@@ -4188,6 +4504,16 @@
|
|||||||
"react-intl": "^5.25.1 || ^6.4.0"
|
"react-intl": "^5.25.1 || ^6.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@openedx/paragon/node_modules/axios": {
|
||||||
|
"version": "0.27.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz",
|
||||||
|
"integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"follow-redirects": "^1.14.9",
|
||||||
|
"form-data": "^4.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@openedx/paragon/node_modules/brace-expansion": {
|
"node_modules/@openedx/paragon/node_modules/brace-expansion": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
|
||||||
@@ -4197,6 +4523,15 @@
|
|||||||
"balanced-match": "^1.0.0"
|
"balanced-match": "^1.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@openedx/paragon/node_modules/commander": {
|
||||||
|
"version": "9.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz",
|
||||||
|
"integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": "^12.20.0 || >=14"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@openedx/paragon/node_modules/glob": {
|
"node_modules/@openedx/paragon/node_modules/glob": {
|
||||||
"version": "8.1.0",
|
"version": "8.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz",
|
||||||
@@ -4229,6 +4564,34 @@
|
|||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@openedx/paragon/node_modules/postcss-custom-media": {
|
||||||
|
"version": "9.1.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/postcss-custom-media/-/postcss-custom-media-9.1.5.tgz",
|
||||||
|
"integrity": "sha512-GStyWMz7Qbo/Gtw1xVspzVSX8eipgNg4lpsO3CAeY4/A1mzok+RV6MCv3fg62trWijh/lYEj6vps4o8JcBBpDA==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/csstools"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/csstools"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@csstools/cascade-layer-name-parser": "^1.0.2",
|
||||||
|
"@csstools/css-parser-algorithms": "^2.2.0",
|
||||||
|
"@csstools/css-tokenizer": "^2.1.1",
|
||||||
|
"@csstools/media-query-list-parser": "^2.1.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^14 || ^16 || >=18"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"postcss": "^8.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@optimizely/js-sdk-logging": {
|
"node_modules/@optimizely/js-sdk-logging": {
|
||||||
"version": "0.3.1",
|
"version": "0.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/@optimizely/js-sdk-logging/-/js-sdk-logging-0.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/@optimizely/js-sdk-logging/-/js-sdk-logging-0.3.1.tgz",
|
||||||
@@ -4731,6 +5094,16 @@
|
|||||||
"url": "https://opencollective.com/parcel"
|
"url": "https://opencollective.com/parcel"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@pkgjs/parseargs": {
|
||||||
|
"version": "0.11.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
|
||||||
|
"integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@pmmmwh/react-refresh-webpack-plugin": {
|
"node_modules/@pmmmwh/react-refresh-webpack-plugin": {
|
||||||
"version": "0.5.15",
|
"version": "0.5.15",
|
||||||
"resolved": "https://registry.npmjs.org/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.5.15.tgz",
|
"resolved": "https://registry.npmjs.org/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.5.15.tgz",
|
||||||
@@ -5268,6 +5641,32 @@
|
|||||||
"@testing-library/dom": ">=7.21.4"
|
"@testing-library/dom": ">=7.21.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@tokens-studio/sd-transforms": {
|
||||||
|
"version": "1.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tokens-studio/sd-transforms/-/sd-transforms-1.3.0.tgz",
|
||||||
|
"integrity": "sha512-zVbiYjTGWpSuwzZwiuvcWf79CQEcTMKSxrOaQJ0zHXFxEmrpETWeIRxv2IO8rtMos/cS8mvnDwPngoHQOMs1SA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@bundled-es-modules/deepmerge": "^4.3.1",
|
||||||
|
"@bundled-es-modules/postcss-calc-ast-parser": "^0.1.6",
|
||||||
|
"@tokens-studio/types": "^0.5.1",
|
||||||
|
"colorjs.io": "^0.5.2",
|
||||||
|
"expr-eval-fork": "^2.0.2",
|
||||||
|
"is-mergeable-object": "^1.1.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"style-dictionary": "^4.3.0 || ^5.0.0-rc.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tokens-studio/types": {
|
||||||
|
"version": "0.5.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tokens-studio/types/-/types-0.5.2.tgz",
|
||||||
|
"integrity": "sha512-rzMcZP0bj2E5jaa7Fj0LGgYHysoCrbrxILVbT0ohsCUH5uCHY/u6J7Qw/TE0n6gR9Js/c9ZO9T8mOoz0HdLMbA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@tootallnate/once": {
|
"node_modules/@tootallnate/once": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz",
|
||||||
@@ -6504,6 +6903,23 @@
|
|||||||
"integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==",
|
"integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==",
|
||||||
"license": "Apache-2.0"
|
"license": "Apache-2.0"
|
||||||
},
|
},
|
||||||
|
"node_modules/@yarnpkg/lockfile": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==",
|
||||||
|
"license": "BSD-2-Clause"
|
||||||
|
},
|
||||||
|
"node_modules/@zip.js/zip.js": {
|
||||||
|
"version": "2.7.60",
|
||||||
|
"resolved": "https://registry.npmjs.org/@zip.js/zip.js/-/zip.js-2.7.60.tgz",
|
||||||
|
"integrity": "sha512-vA3rLyqdxBrVo1FWSsbyoecaqWTV+vgPRf0QKeM7kVDG0r+lHUqd7zQDv1TO9k4BcAoNzNDSNrrel24Mk6addA==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"engines": {
|
||||||
|
"bun": ">=0.7.0",
|
||||||
|
"deno": ">=1.0.0",
|
||||||
|
"node": ">=16.5.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/abab": {
|
"node_modules/abab": {
|
||||||
"version": "2.0.6",
|
"version": "2.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz",
|
||||||
@@ -6955,6 +7371,19 @@
|
|||||||
"integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==",
|
"integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/assert": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/assert/-/assert-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-eLHpSK/Y4nhMJ07gDaAzoX/XAKS8PSaojml3M0DM4JpV1LAi5JOJ/p6H/XWrl8L+DzVEvVCW1z3vWAaB9oTsQw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"call-bind": "^1.0.2",
|
||||||
|
"is-nan": "^1.3.2",
|
||||||
|
"object-is": "^1.1.5",
|
||||||
|
"object.assign": "^4.1.4",
|
||||||
|
"util": "^0.12.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/assert-ok": {
|
"node_modules/assert-ok": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/assert-ok/-/assert-ok-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/assert-ok/-/assert-ok-1.0.0.tgz",
|
||||||
@@ -6967,6 +7396,15 @@
|
|||||||
"integrity": "sha512-eBvWn1lvIApYMhzQMsu9ciLfkBY499mFZlNqG+/9WR7PVlroQw0vG30cOQQbaKz3sCEc44TAOu2ykzqXSNnwag==",
|
"integrity": "sha512-eBvWn1lvIApYMhzQMsu9ciLfkBY499mFZlNqG+/9WR7PVlroQw0vG30cOQQbaKz3sCEc44TAOu2ykzqXSNnwag==",
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/astral-regex": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/async": {
|
"node_modules/async": {
|
||||||
"version": "3.2.6",
|
"version": "3.2.6",
|
||||||
"resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz",
|
"resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz",
|
||||||
@@ -7934,6 +8372,12 @@
|
|||||||
"url": "https://github.com/chalk/chalk?sponsor=1"
|
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/change-case": {
|
||||||
|
"version": "5.4.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/change-case/-/change-case-5.4.4.tgz",
|
||||||
|
"integrity": "sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/char-regex": {
|
"node_modules/char-regex": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz",
|
||||||
@@ -8077,6 +8521,20 @@
|
|||||||
"boolbase": "~1.0.0"
|
"boolbase": "~1.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/chevrotain": {
|
||||||
|
"version": "11.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-11.0.3.tgz",
|
||||||
|
"integrity": "sha512-ci2iJH6LeIkvP9eJW6gpueU8cnZhv85ELY8w8WiFtNjMHA5ad6pQLaJo9mEly/9qUyCpvqX8/POVUTf18/HFdw==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@chevrotain/cst-dts-gen": "11.0.3",
|
||||||
|
"@chevrotain/gast": "11.0.3",
|
||||||
|
"@chevrotain/regexp-to-ast": "11.0.3",
|
||||||
|
"@chevrotain/types": "11.0.3",
|
||||||
|
"@chevrotain/utils": "11.0.3",
|
||||||
|
"lodash-es": "4.17.21"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/child_process": {
|
"node_modules/child_process": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/child_process/-/child_process-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/child_process/-/child_process-1.0.2.tgz",
|
||||||
@@ -8113,6 +8571,12 @@
|
|||||||
"integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",
|
"integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/chroma-js": {
|
||||||
|
"version": "2.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/chroma-js/-/chroma-js-2.6.0.tgz",
|
||||||
|
"integrity": "sha512-BLHvCB9s8Z1EV4ethr6xnkl/P2YRFOGqfgvuMG/MyCbZPrTA+NeiByY6XvgF0zP4/2deU2CXnWyMa3zu1LqQ3A==",
|
||||||
|
"license": "(BSD-3-Clause AND Apache-2.0)"
|
||||||
|
},
|
||||||
"node_modules/chrome-trace-event": {
|
"node_modules/chrome-trace-event": {
|
||||||
"version": "1.0.4",
|
"version": "1.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz",
|
||||||
@@ -8204,6 +8668,18 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/cli-progress": {
|
||||||
|
"version": "3.12.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/cli-progress/-/cli-progress-3.12.0.tgz",
|
||||||
|
"integrity": "sha512-tRkV3HJ1ASwm19THiiLIXLO7Im7wlTuKnvkYaTkyoAPefqjNg7W7DHKUlGRxy9vxDvbyCYQkQozvptuMkGCg8A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"string-width": "^4.2.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/cli-spinners": {
|
"node_modules/cli-spinners": {
|
||||||
"version": "2.9.2",
|
"version": "2.9.2",
|
||||||
"resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz",
|
"resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz",
|
||||||
@@ -8348,6 +8824,12 @@
|
|||||||
"integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==",
|
"integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/colorjs.io": {
|
||||||
|
"version": "0.5.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/colorjs.io/-/colorjs.io-0.5.2.tgz",
|
||||||
|
"integrity": "sha512-twmVoizEW7ylZSN32OgKdXRmo1qg+wT5/6C3xu5b9QsWzSFAhHLn2xd8ro0diCsKfCj1RdaTP/nrcW+vAoQPIw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/combined-stream": {
|
"node_modules/combined-stream": {
|
||||||
"version": "1.0.8",
|
"version": "1.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||||
@@ -9656,6 +10138,12 @@
|
|||||||
"integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==",
|
"integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/eastasianwidth": {
|
||||||
|
"version": "0.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
|
||||||
|
"integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/ee-first": {
|
"node_modules/ee-first": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
|
||||||
@@ -11059,6 +11547,12 @@
|
|||||||
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
|
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/expr-eval-fork": {
|
||||||
|
"version": "2.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/expr-eval-fork/-/expr-eval-fork-2.0.2.tgz",
|
||||||
|
"integrity": "sha512-NaAnObPVwHEYrODd7Jzp3zzT9pgTAlUUL4MZiZu9XAYPDpx89cPsfyEImFb2XY0vQNbrqg2CG7CLiI+Rs3seaQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/express": {
|
"node_modules/express": {
|
||||||
"version": "4.21.2",
|
"version": "4.21.2",
|
||||||
"resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz",
|
"resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz",
|
||||||
@@ -11479,6 +11973,15 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/find-yarn-workspace-root": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/find-yarn-workspace-root/-/find-yarn-workspace-root-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-1IMnbjt4KzsQfnhnzNd8wUEgXZ44IzZaZmnLYx7D5FZlaHt2gW20Cri8Q+E/t5tIj4+epTBub+2Zxu/vNILzqQ==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"micromatch": "^4.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/flat": {
|
"node_modules/flat": {
|
||||||
"version": "5.0.2",
|
"version": "5.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz",
|
||||||
@@ -11556,15 +12059,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/font-awesome": {
|
|
||||||
"version": "4.7.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/font-awesome/-/font-awesome-4.7.0.tgz",
|
|
||||||
"integrity": "sha512-U6kGnykA/6bFmg1M/oT9EkFeIYv7JlX3bozwQJWiiLz6L0w3F5vBVPxHlwyX/vtNq1ckcpRKOB9f2Qal/VtFpg==",
|
|
||||||
"license": "(OFL-1.1 AND MIT)",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=0.10.3"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/for-each": {
|
"node_modules/for-each": {
|
||||||
"version": "0.3.5",
|
"version": "0.3.5",
|
||||||
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz",
|
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz",
|
||||||
@@ -11580,6 +12074,34 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/foreground-child": {
|
||||||
|
"version": "3.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
|
||||||
|
"integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"cross-spawn": "^7.0.6",
|
||||||
|
"signal-exit": "^4.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/foreground-child/node_modules/signal-exit": {
|
||||||
|
"version": "4.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
|
||||||
|
"integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/fork-ts-checker-webpack-plugin": {
|
"node_modules/fork-ts-checker-webpack-plugin": {
|
||||||
"version": "6.5.3",
|
"version": "6.5.3",
|
||||||
"resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-6.5.3.tgz",
|
"resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-6.5.3.tgz",
|
||||||
@@ -12664,6 +13186,15 @@
|
|||||||
"node": ">=10.17.0"
|
"node": ">=10.17.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/hyperdyperid": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/hyperdyperid/-/hyperdyperid-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-Y93lCzHYgGWdrJ66yIktxiaGULYc6oGiABxhcO5AufBeOyoIdZF7bIfLaOrbM0iGIOXQQgxxRrFEnb+Y6w1n4A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/hyphenate-style-name": {
|
"node_modules/hyphenate-style-name": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.1.0.tgz",
|
||||||
@@ -13039,6 +13570,22 @@
|
|||||||
"node": ">= 0.10"
|
"node": ">= 0.10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/is-arguments": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"call-bound": "^1.0.2",
|
||||||
|
"has-tostringtag": "^1.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/is-array-buffer": {
|
"node_modules/is-array-buffer": {
|
||||||
"version": "3.0.5",
|
"version": "3.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz",
|
||||||
@@ -13369,6 +13916,28 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/is-mergeable-object": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-mergeable-object/-/is-mergeable-object-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-CPduJfuGg8h8vW74WOxHtHmtQutyQBzR+3MjQ6iDHIYdbOnm1YC7jv43SqCoU8OPGTJD4nibmiryA4kmogbGrA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/is-nan": {
|
||||||
|
"version": "1.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-nan/-/is-nan-1.3.2.tgz",
|
||||||
|
"integrity": "sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"call-bind": "^1.0.0",
|
||||||
|
"define-properties": "^1.1.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/is-number": {
|
"node_modules/is-number": {
|
||||||
"version": "7.0.0",
|
"version": "7.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
|
||||||
@@ -13818,6 +14387,21 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/jackspeak": {
|
||||||
|
"version": "3.4.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz",
|
||||||
|
"integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==",
|
||||||
|
"license": "BlueOak-1.0.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@isaacs/cliui": "^8.0.2"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@pkgjs/parseargs": "^0.11.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/jake": {
|
"node_modules/jake": {
|
||||||
"version": "10.9.2",
|
"version": "10.9.2",
|
||||||
"resolved": "https://registry.npmjs.org/jake/-/jake-10.9.2.tgz",
|
"resolved": "https://registry.npmjs.org/jake/-/jake-10.9.2.tgz",
|
||||||
@@ -14792,6 +15376,16 @@
|
|||||||
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
|
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/js-toml": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/js-toml/-/js-toml-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-rHd/IolpFm2V5BmHCEY8CckHs8NDsYZZ64H5RNgA6Opsr9vX4QyTiQPplgtqg7b3ztqYShZC38nl6CUg7QuhXg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"chevrotain": "^11.0.3",
|
||||||
|
"xregexp": "^5.1.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/js-yaml": {
|
"node_modules/js-yaml": {
|
||||||
"version": "3.14.1",
|
"version": "3.14.1",
|
||||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz",
|
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz",
|
||||||
@@ -15037,6 +15631,15 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/klaw-sync": {
|
||||||
|
"version": "6.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/klaw-sync/-/klaw-sync-6.0.0.tgz",
|
||||||
|
"integrity": "sha512-nIeuVSzdCCs6TDPTqI8w1Yre34sSq7AkZ4B3sfOBbI2CgVSB4Du4aLQijFU2+lhAFCwt9+42Hel6lQNIv6AntQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"graceful-fs": "^4.1.11"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/kleur": {
|
"node_modules/kleur": {
|
||||||
"version": "3.0.3",
|
"version": "3.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz",
|
||||||
@@ -15178,6 +15781,12 @@
|
|||||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/lodash-es": {
|
||||||
|
"version": "4.17.21",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz",
|
||||||
|
"integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/lodash.assignin": {
|
"node_modules/lodash.assignin": {
|
||||||
"version": "4.2.0",
|
"version": "4.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/lodash.assignin/-/lodash.assignin-4.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.assignin/-/lodash.assignin-4.2.0.tgz",
|
||||||
@@ -15332,6 +15941,24 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/log-update": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/log-update/-/log-update-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ansi-escapes": "^4.3.0",
|
||||||
|
"cli-cursor": "^3.1.0",
|
||||||
|
"slice-ansi": "^4.0.0",
|
||||||
|
"wrap-ansi": "^6.2.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/loose-envify": {
|
"node_modules/loose-envify": {
|
||||||
"version": "1.4.0",
|
"version": "1.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
|
||||||
@@ -16191,6 +16818,15 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/minipass": {
|
||||||
|
"version": "7.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
|
||||||
|
"integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16 || 14 >=14.17"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/mkdirp-classic": {
|
"node_modules/mkdirp-classic": {
|
||||||
"version": "0.5.3",
|
"version": "0.5.3",
|
||||||
"resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
|
"resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
|
||||||
@@ -16492,6 +17128,22 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/object-is": {
|
||||||
|
"version": "1.1.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz",
|
||||||
|
"integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"call-bind": "^1.0.7",
|
||||||
|
"define-properties": "^1.2.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/object-keys": {
|
"node_modules/object-keys": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
|
||||||
@@ -16829,6 +17481,12 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/package-json-from-dist": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
|
||||||
|
"license": "BlueOak-1.0.0"
|
||||||
|
},
|
||||||
"node_modules/param-case": {
|
"node_modules/param-case": {
|
||||||
"version": "3.0.4",
|
"version": "3.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz",
|
||||||
@@ -16900,6 +17558,95 @@
|
|||||||
"tslib": "^2.0.3"
|
"tslib": "^2.0.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/patch-package": {
|
||||||
|
"version": "8.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/patch-package/-/patch-package-8.0.0.tgz",
|
||||||
|
"integrity": "sha512-da8BVIhzjtgScwDJ2TtKsfT5JFWz1hYoBl9rUQ1f38MC2HwnEIkK8VN3dKMKcP7P7bvvgzNDbfNHtx3MsQb5vA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@yarnpkg/lockfile": "^1.1.0",
|
||||||
|
"chalk": "^4.1.2",
|
||||||
|
"ci-info": "^3.7.0",
|
||||||
|
"cross-spawn": "^7.0.3",
|
||||||
|
"find-yarn-workspace-root": "^2.0.0",
|
||||||
|
"fs-extra": "^9.0.0",
|
||||||
|
"json-stable-stringify": "^1.0.2",
|
||||||
|
"klaw-sync": "^6.0.0",
|
||||||
|
"minimist": "^1.2.6",
|
||||||
|
"open": "^7.4.2",
|
||||||
|
"rimraf": "^2.6.3",
|
||||||
|
"semver": "^7.5.3",
|
||||||
|
"slash": "^2.0.0",
|
||||||
|
"tmp": "^0.0.33",
|
||||||
|
"yaml": "^2.2.2"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"patch-package": "index.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14",
|
||||||
|
"npm": ">5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/patch-package/node_modules/open": {
|
||||||
|
"version": "7.4.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/open/-/open-7.4.2.tgz",
|
||||||
|
"integrity": "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"is-docker": "^2.0.0",
|
||||||
|
"is-wsl": "^2.1.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/patch-package/node_modules/semver": {
|
||||||
|
"version": "7.7.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz",
|
||||||
|
"integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==",
|
||||||
|
"license": "ISC",
|
||||||
|
"bin": {
|
||||||
|
"semver": "bin/semver.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/patch-package/node_modules/slash": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/patch-package/node_modules/yaml": {
|
||||||
|
"version": "2.7.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.1.tgz",
|
||||||
|
"integrity": "sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ==",
|
||||||
|
"license": "ISC",
|
||||||
|
"bin": {
|
||||||
|
"yaml": "bin.mjs"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 14"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/path": {
|
||||||
|
"version": "0.12.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/path/-/path-0.12.7.tgz",
|
||||||
|
"integrity": "sha512-aXXC6s+1w7otVF9UletFkFcDsJeO7lSZBPUQhtb5O0xJe8LtYhj/GxldoL09bBj9+ZmE2hNoHqQSFMN5fikh4Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"process": "^0.11.1",
|
||||||
|
"util": "^0.10.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/path-exists": {
|
"node_modules/path-exists": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
|
||||||
@@ -16939,6 +17686,28 @@
|
|||||||
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
|
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/path-scurry": {
|
||||||
|
"version": "1.11.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
|
||||||
|
"integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
|
||||||
|
"license": "BlueOak-1.0.0",
|
||||||
|
"dependencies": {
|
||||||
|
"lru-cache": "^10.2.0",
|
||||||
|
"minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16 || 14 >=14.18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/path-scurry/node_modules/lru-cache": {
|
||||||
|
"version": "10.4.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
|
||||||
|
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/path-to-regexp": {
|
"node_modules/path-to-regexp": {
|
||||||
"version": "0.1.12",
|
"version": "0.1.12",
|
||||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
|
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
|
||||||
@@ -16954,6 +17723,27 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/path-unified": {
|
||||||
|
"version": "0.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/path-unified/-/path-unified-0.2.0.tgz",
|
||||||
|
"integrity": "sha512-MNKqvrKbbbb5p7XHXV6ZAsf/1f/yJQa13S/fcX0uua8ew58Tgc6jXV+16JyAbnR/clgCH+euKDxrF2STxMHdrg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/path/node_modules/inherits": {
|
||||||
|
"version": "2.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
|
||||||
|
"integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
|
"node_modules/path/node_modules/util": {
|
||||||
|
"version": "0.10.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/util/-/util-0.10.4.tgz",
|
||||||
|
"integrity": "sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"inherits": "2.0.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/picocolors": {
|
"node_modules/picocolors": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||||
@@ -17357,6 +18147,24 @@
|
|||||||
"postcss": "^8.2.2"
|
"postcss": "^8.2.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/postcss-calc-ast-parser": {
|
||||||
|
"version": "0.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/postcss-calc-ast-parser/-/postcss-calc-ast-parser-0.1.4.tgz",
|
||||||
|
"integrity": "sha512-CebpbHc96zgFjGgdQ6BqBy6XIUgRx1xXWCAAk6oke02RZ5nxwo9KQejTg8y7uYEeI9kv8jKQPYjoe6REsY23vw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"postcss-value-parser": "^3.3.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/postcss-calc-ast-parser/node_modules/postcss-value-parser": {
|
||||||
|
"version": "3.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz",
|
||||||
|
"integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/postcss-colormin": {
|
"node_modules/postcss-colormin": {
|
||||||
"version": "6.1.0",
|
"version": "6.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-6.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-6.1.0.tgz",
|
||||||
@@ -17375,6 +18183,21 @@
|
|||||||
"postcss": "^8.4.31"
|
"postcss": "^8.4.31"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/postcss-combine-duplicated-selectors": {
|
||||||
|
"version": "10.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/postcss-combine-duplicated-selectors/-/postcss-combine-duplicated-selectors-10.0.3.tgz",
|
||||||
|
"integrity": "sha512-IP0BmwFloCskv7DV7xqvzDXqMHpwdczJa6ZvIW8abgHdcIHs9mCJX2ltFhu3EwA51ozp13DByng30+Ke+eIExA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"postcss-selector-parser": "^6.0.4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^10.0.0 || ^12.0.0 || >=14.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"postcss": "^8.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/postcss-convert-values": {
|
"node_modules/postcss-convert-values": {
|
||||||
"version": "6.1.0",
|
"version": "6.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-6.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-6.1.0.tgz",
|
||||||
@@ -17467,6 +18290,23 @@
|
|||||||
"postcss": "^8.4.31"
|
"postcss": "^8.4.31"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/postcss-import": {
|
||||||
|
"version": "15.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz",
|
||||||
|
"integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"postcss-value-parser": "^4.0.0",
|
||||||
|
"read-cache": "^1.0.0",
|
||||||
|
"resolve": "^1.1.7"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"postcss": "^8.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/postcss-loader": {
|
"node_modules/postcss-loader": {
|
||||||
"version": "8.1.1",
|
"version": "8.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-8.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-8.1.1.tgz",
|
||||||
@@ -17554,6 +18394,52 @@
|
|||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/postcss-map": {
|
||||||
|
"version": "0.11.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/postcss-map/-/postcss-map-0.11.0.tgz",
|
||||||
|
"integrity": "sha512-cgHYZrH9aAMds90upYUPhYz8xnAcRD45SwuNns/nQHONIrPQDhpwk3JLsAQGOndQxnRVXfB6nB+3WqSMy8fqlA==",
|
||||||
|
"license": "Unlicense",
|
||||||
|
"dependencies": {
|
||||||
|
"js-yaml": "^3.12.0",
|
||||||
|
"postcss": "^7.0.2",
|
||||||
|
"reduce-function-call": "^1.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/postcss-map/node_modules/picocolors": {
|
||||||
|
"version": "0.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz",
|
||||||
|
"integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
|
"node_modules/postcss-map/node_modules/postcss": {
|
||||||
|
"version": "7.0.39",
|
||||||
|
"resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz",
|
||||||
|
"integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"picocolors": "^0.2.1",
|
||||||
|
"source-map": "^0.6.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/postcss/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/postcss-map/node_modules/source-map": {
|
||||||
|
"version": "0.6.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||||
|
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/postcss-merge-longhand": {
|
"node_modules/postcss-merge-longhand": {
|
||||||
"version": "6.0.5",
|
"version": "6.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-6.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-6.0.5.tgz",
|
||||||
@@ -17588,6 +18474,19 @@
|
|||||||
"postcss": "^8.4.31"
|
"postcss": "^8.4.31"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/postcss-minify": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/postcss-minify/-/postcss-minify-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-9D64ueIW0DL2FdLajQTlXrnTN8Ox9NjuXqigKMmB819RhdClNPYx5Zp3i5x0ghjjy3vGrLBBYEYvJjY/1eMNbw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"postcss-selector-parser": "^6.0",
|
||||||
|
"postcss-value-parser": "^4.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"postcss": "^8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/postcss-minify-font-values": {
|
"node_modules/postcss-minify-font-values": {
|
||||||
"version": "6.1.0",
|
"version": "6.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/postcss-minify-font-values/-/postcss-minify-font-values-6.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/postcss-minify-font-values/-/postcss-minify-font-values-6.1.0.tgz",
|
||||||
@@ -18054,6 +18953,21 @@
|
|||||||
"node": ">= 0.8.0"
|
"node": ">= 0.8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/prettier": {
|
||||||
|
"version": "3.5.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz",
|
||||||
|
"integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"prettier": "bin/prettier.cjs"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/prettier/prettier?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/pretty-error": {
|
"node_modules/pretty-error": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/pretty-error/-/pretty-error-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/pretty-error/-/pretty-error-4.0.0.tgz",
|
||||||
@@ -18100,7 +19014,6 @@
|
|||||||
"version": "0.11.10",
|
"version": "0.11.10",
|
||||||
"resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
|
"resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
|
||||||
"integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==",
|
"integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.6.0"
|
"node": ">= 0.6.0"
|
||||||
@@ -19165,6 +20078,24 @@
|
|||||||
"react-dom": ">=16.6.0"
|
"react-dom": ">=16.6.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/read-cache": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"pify": "^2.3.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/read-cache/node_modules/pify": {
|
||||||
|
"version": "2.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
|
||||||
|
"integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/read-pkg": {
|
"node_modules/read-pkg": {
|
||||||
"version": "5.2.0",
|
"version": "5.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz",
|
||||||
@@ -19292,6 +20223,15 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/reduce-function-call": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/reduce-function-call/-/reduce-function-call-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-Hl/tuV2VDgWgCSEeWMLwxLZqX7OK59eU1guxXsRKTAyeYimivsKdtcV4fu3r710tpG5GmDKDhQ0HSZLExnNmyQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"balanced-match": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/redux": {
|
"node_modules/redux": {
|
||||||
"version": "4.2.1",
|
"version": "4.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz",
|
||||||
@@ -20508,6 +21448,23 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/slice-ansi": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ansi-styles": "^4.0.0",
|
||||||
|
"astral-regex": "^2.0.0",
|
||||||
|
"is-fullwidth-code-point": "^3.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/chalk/slice-ansi?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/snake-case": {
|
"node_modules/snake-case": {
|
||||||
"version": "3.0.4",
|
"version": "3.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/snake-case/-/snake-case-3.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/snake-case/-/snake-case-3.0.4.tgz",
|
||||||
@@ -20766,6 +21723,27 @@
|
|||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/stream": {
|
||||||
|
"version": "0.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/stream/-/stream-0.0.3.tgz",
|
||||||
|
"integrity": "sha512-aMsbn7VKrl4A2T7QAQQbzgN7NVc70vgF5INQrBXqn4dCXN1zy3L9HGgLO5s7PExmdrzTJ8uR/27aviW8or8/+A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"component-emitter": "^2.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/stream/node_modules/component-emitter": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-4m5s3Me2xxlVKG9PkZpQqHQR7bgpnN7joDMJ4yvVkVXngjoITG76IaZmzmywSeRTeTpc6N6r3H3+KyUurV8OYw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/streamx": {
|
"node_modules/streamx": {
|
||||||
"version": "2.22.0",
|
"version": "2.22.0",
|
||||||
"resolved": "https://registry.npmjs.org/streamx/-/streamx-2.22.0.tgz",
|
"resolved": "https://registry.npmjs.org/streamx/-/streamx-2.22.0.tgz",
|
||||||
@@ -20824,6 +21802,27 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/string-width-cjs": {
|
||||||
|
"name": "string-width",
|
||||||
|
"version": "4.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||||
|
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"emoji-regex": "^8.0.0",
|
||||||
|
"is-fullwidth-code-point": "^3.0.0",
|
||||||
|
"strip-ansi": "^6.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/string-width-cjs/node_modules/emoji-regex": {
|
||||||
|
"version": "8.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||||
|
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/string-width/node_modules/emoji-regex": {
|
"node_modules/string-width/node_modules/emoji-regex": {
|
||||||
"version": "8.0.0",
|
"version": "8.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||||
@@ -20925,6 +21924,19 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/strip-ansi-cjs": {
|
||||||
|
"name": "strip-ansi",
|
||||||
|
"version": "6.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||||
|
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ansi-regex": "^5.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/strip-bom": {
|
"node_modules/strip-bom": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz",
|
||||||
@@ -20967,6 +21979,55 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/style-dictionary": {
|
||||||
|
"version": "4.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/style-dictionary/-/style-dictionary-4.4.0.tgz",
|
||||||
|
"integrity": "sha512-+xU0IA1StzqAqFs/QtXkK+XJa7wpS4X5H+JQccRKsRCElgeLGocFU1U/UMvMUylKFw6vwGV+Y/a2wb2pm5rFFQ==",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@bundled-es-modules/deepmerge": "^4.3.1",
|
||||||
|
"@bundled-es-modules/glob": "^10.4.2",
|
||||||
|
"@bundled-es-modules/memfs": "^4.9.4",
|
||||||
|
"@zip.js/zip.js": "^2.7.44",
|
||||||
|
"chalk": "^5.3.0",
|
||||||
|
"change-case": "^5.3.0",
|
||||||
|
"commander": "^12.1.0",
|
||||||
|
"is-plain-obj": "^4.1.0",
|
||||||
|
"json5": "^2.2.2",
|
||||||
|
"patch-package": "^8.0.0",
|
||||||
|
"path-unified": "^0.2.0",
|
||||||
|
"prettier": "^3.3.3",
|
||||||
|
"tinycolor2": "^1.6.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"style-dictionary": "bin/style-dictionary.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/style-dictionary/node_modules/chalk": {
|
||||||
|
"version": "5.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz",
|
||||||
|
"integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": "^12.17.0 || ^14.13 || >=16.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/style-dictionary/node_modules/commander": {
|
||||||
|
"version": "12.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz",
|
||||||
|
"integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/style-loader": {
|
"node_modules/style-loader": {
|
||||||
"version": "3.3.4",
|
"version": "3.3.4",
|
||||||
"resolved": "https://registry.npmjs.org/style-loader/-/style-loader-3.3.4.tgz",
|
"resolved": "https://registry.npmjs.org/style-loader/-/style-loader-3.3.4.tgz",
|
||||||
@@ -21345,6 +22406,18 @@
|
|||||||
"integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==",
|
"integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/thingies": {
|
||||||
|
"version": "1.21.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/thingies/-/thingies-1.21.0.tgz",
|
||||||
|
"integrity": "sha512-hsqsJsFMsV+aD4s3CWKk85ep/3I9XzYV/IXaSouJMYIoDlgyi11cBhsqYe9/geRfB0YIikBQg6raRaM+nIMP9g==",
|
||||||
|
"license": "Unlicense",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.18"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"tslib": "^2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/thread-stream": {
|
"node_modules/thread-stream": {
|
||||||
"version": "2.7.0",
|
"version": "2.7.0",
|
||||||
"resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-2.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-2.7.0.tgz",
|
||||||
@@ -21379,6 +22452,12 @@
|
|||||||
"integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==",
|
"integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/tinycolor2": {
|
||||||
|
"version": "1.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.6.0.tgz",
|
||||||
|
"integrity": "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/tinyglobby": {
|
"node_modules/tinyglobby": {
|
||||||
"version": "0.2.13",
|
"version": "0.2.13",
|
||||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.13.tgz",
|
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.13.tgz",
|
||||||
@@ -21505,6 +22584,22 @@
|
|||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/tree-dump": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/tree-dump/-/tree-dump-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-dpev9ABuLWdEubk+cIaI9cHwRNNDjkBBLXTwI4UCUFdQ5xXKqNXoK4FEciw/vxf+NQ7Cb7sGUyeUtORvHIdRXQ==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/streamich"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"tslib": "2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/trim-lines": {
|
"node_modules/trim-lines": {
|
||||||
"version": "3.0.1",
|
"version": "3.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz",
|
||||||
@@ -22197,6 +23292,19 @@
|
|||||||
"punycode": "^2.1.0"
|
"punycode": "^2.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/url": {
|
||||||
|
"version": "0.11.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/url/-/url-0.11.4.tgz",
|
||||||
|
"integrity": "sha512-oCwdVC7mTuWiPyjLUz/COz5TLk6wgp0RCsN+wHZ2Ekneac9w8uuV0njcbbie2ME+Vs+d6duwmYuR3HgQXs1fOg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"punycode": "^1.4.1",
|
||||||
|
"qs": "^6.12.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/url-loader": {
|
"node_modules/url-loader": {
|
||||||
"version": "4.1.1",
|
"version": "4.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/url-loader/-/url-loader-4.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/url-loader/-/url-loader-4.1.1.tgz",
|
||||||
@@ -22252,6 +23360,12 @@
|
|||||||
"requires-port": "^1.0.0"
|
"requires-port": "^1.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/url/node_modules/punycode": {
|
||||||
|
"version": "1.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz",
|
||||||
|
"integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/use-callback-ref": {
|
"node_modules/use-callback-ref": {
|
||||||
"version": "1.3.3",
|
"version": "1.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz",
|
||||||
@@ -22304,6 +23418,19 @@
|
|||||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/util": {
|
||||||
|
"version": "0.12.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz",
|
||||||
|
"integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"inherits": "^2.0.3",
|
||||||
|
"is-arguments": "^1.0.4",
|
||||||
|
"is-generator-function": "^1.0.7",
|
||||||
|
"is-typed-array": "^1.1.3",
|
||||||
|
"which-typed-array": "^1.1.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/util-deprecate": {
|
"node_modules/util-deprecate": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||||
@@ -23063,6 +24190,24 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/wrap-ansi-cjs": {
|
||||||
|
"name": "wrap-ansi",
|
||||||
|
"version": "7.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
|
||||||
|
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ansi-styles": "^4.0.0",
|
||||||
|
"string-width": "^4.1.0",
|
||||||
|
"strip-ansi": "^6.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/wrappy": {
|
"node_modules/wrappy": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||||
@@ -23118,6 +24263,15 @@
|
|||||||
"integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==",
|
"integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/xregexp": {
|
||||||
|
"version": "5.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/xregexp/-/xregexp-5.1.2.tgz",
|
||||||
|
"integrity": "sha512-6hGgEMCGhqCTFEJbqmWrNIPqfpdirdGWkqshu7fFZddmTSfgv5Sn9D2SaKloR79s5VUiUlpwzg3CM3G6D3VIlw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime-corejs3": "^7.26.9"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/y18n": {
|
"node_modules/y18n": {
|
||||||
"version": "5.0.8",
|
"version": "5.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
|
||||||
|
|||||||
@@ -37,7 +37,7 @@
|
|||||||
"@edx/browserslist-config": "1.5.0",
|
"@edx/browserslist-config": "1.5.0",
|
||||||
"@edx/frontend-component-footer": "^14.6.0",
|
"@edx/frontend-component-footer": "^14.6.0",
|
||||||
"@edx/frontend-component-header": "^6.2.0",
|
"@edx/frontend-component-header": "^6.2.0",
|
||||||
"@edx/frontend-lib-learning-assistant": "^2.20.0",
|
"@edx/frontend-lib-learning-assistant": "^2.24.0",
|
||||||
"@edx/frontend-lib-special-exams": "^4.0.0",
|
"@edx/frontend-lib-special-exams": "^4.0.0",
|
||||||
"@edx/frontend-platform": "^8.3.1",
|
"@edx/frontend-platform": "^8.3.1",
|
||||||
"@edx/openedx-atlas": "^0.7.0",
|
"@edx/openedx-atlas": "^0.7.0",
|
||||||
@@ -48,7 +48,7 @@
|
|||||||
"@fortawesome/react-fontawesome": "^0.1.4",
|
"@fortawesome/react-fontawesome": "^0.1.4",
|
||||||
"@openedx/frontend-build": "^14.5.0",
|
"@openedx/frontend-build": "^14.5.0",
|
||||||
"@openedx/frontend-plugin-framework": "^1.7.0",
|
"@openedx/frontend-plugin-framework": "^1.7.0",
|
||||||
"@openedx/paragon": "^22.16.0",
|
"@openedx/paragon": "^23.4.5",
|
||||||
"@popperjs/core": "2.11.8",
|
"@popperjs/core": "2.11.8",
|
||||||
"@reduxjs/toolkit": "1.9.7",
|
"@reduxjs/toolkit": "1.9.7",
|
||||||
"buffer": "^6.0.3",
|
"buffer": "^6.0.3",
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ exports[`app registry subscribe: APP_READY. links App to root element 1`] = `
|
|||||||
<Preferences Unsubscribe />
|
<Preferences Unsubscribe />
|
||||||
</PageWrap>
|
</PageWrap>
|
||||||
}
|
}
|
||||||
path="/preferences-unsubscribe/:userToken/:updatePatch"
|
path="/preferences-unsubscribe/:userToken/:updatePatch?"
|
||||||
/>
|
/>
|
||||||
<Route
|
<Route
|
||||||
element={
|
element={
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ export const DECODE_ROUTES = {
|
|||||||
|
|
||||||
export const ROUTES = {
|
export const ROUTES = {
|
||||||
UNSUBSCRIBE: '/goal-unsubscribe/:token',
|
UNSUBSCRIBE: '/goal-unsubscribe/:token',
|
||||||
PREFERENCES_UNSUBSCRIBE: '/preferences-unsubscribe/:userToken/:updatePatch',
|
PREFERENCES_UNSUBSCRIBE: '/preferences-unsubscribe/:userToken/:updatePatch?',
|
||||||
REDIRECT: '/redirect/*',
|
REDIRECT: '/redirect/*',
|
||||||
DASHBOARD: 'dashboard',
|
DASHBOARD: 'dashboard',
|
||||||
ENTERPRISE_LEARNER_DASHBOARD: 'enterprise-learner-dashboard',
|
ENTERPRISE_LEARNER_DASHBOARD: 'enterprise-learner-dashboard',
|
||||||
|
|||||||
@@ -9,8 +9,8 @@
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
max-width: none;
|
max-width: none;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
border-top: 1px solid $light-300;
|
border-top: 1px solid var(--pgn-color-light-300);
|
||||||
z-index: $zindex-modal; // Bootstrap's z-index layer for Modals.
|
z-index: var(--pgn-elevation-modal-zindex); // Bootstrap's z-index layer for Modals.
|
||||||
|
|
||||||
&__form {
|
&__form {
|
||||||
position: relative;
|
position: relative;
|
||||||
@@ -47,7 +47,7 @@
|
|||||||
|
|
||||||
&__results-summary {
|
&__results-summary {
|
||||||
font-size: .9rem;
|
font-size: .9rem;
|
||||||
color: $gray-500;
|
color: var(--pgn-color-gray-500);
|
||||||
padding: 1rem 0 .5rem;
|
padding: 1rem 0 .5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,7 +62,7 @@
|
|||||||
margin-top: 1.5rem;
|
margin-top: 1.5rem;
|
||||||
|
|
||||||
&__empty {
|
&__empty {
|
||||||
color: $gray-500;
|
color: var(--pgn-color-gray-500);
|
||||||
padding: 6rem 0;
|
padding: 6rem 0;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
@@ -76,17 +76,17 @@
|
|||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
background: $light-300;
|
background: var(--pgn-color-light-300);
|
||||||
}
|
}
|
||||||
|
|
||||||
&:not(:first-child) {
|
&:not(:first-child) {
|
||||||
border-top: 1px solid $light-300;
|
border-top: 1px solid var(--pgn-color-light-300);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__icon {
|
&__icon {
|
||||||
padding: 0.375rem 0 0 0.375rem;
|
padding: 0.375rem 0 0 0.375rem;
|
||||||
color: $gray-300;
|
color: var(--pgn-color-gray-300);
|
||||||
}
|
}
|
||||||
|
|
||||||
&__info {
|
&__info {
|
||||||
@@ -99,7 +99,7 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
line-height: 2.5;
|
line-height: 2.5;
|
||||||
font-size: 0.875rem;
|
font-size: 0.875rem;
|
||||||
color: $black;
|
color: var(--pgn-color-black);
|
||||||
|
|
||||||
> span {
|
> span {
|
||||||
display: block;
|
display: block;
|
||||||
@@ -113,7 +113,7 @@
|
|||||||
font-variant-numeric: lining-nums tabular-nums;
|
font-variant-numeric: lining-nums tabular-nums;
|
||||||
min-width: 1.25rem;
|
min-width: 1.25rem;
|
||||||
line-height: 1rem;
|
line-height: 1rem;
|
||||||
background: $light-300;
|
background: var(--pgn-color-light-300);
|
||||||
border-radius: 99rem;
|
border-radius: 99rem;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
margin-left: 0.375rem;
|
margin-left: 0.375rem;
|
||||||
@@ -125,7 +125,7 @@
|
|||||||
&__breadcrumbs {
|
&__breadcrumbs {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 1.25rem;
|
gap: 1.25rem;
|
||||||
color: $gray-500;
|
color: var(--pgn-color-gray-500);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
list-style: none;
|
list-style: none;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
@@ -156,14 +156,14 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.courseware-search-results-tabs {
|
.courseware-search-results-tabs {
|
||||||
border-bottom-color: $gray-400 !important;
|
border-bottom-color: var(--pgn-color-gray-400) !important;
|
||||||
|
|
||||||
&.nav-tabs .nav-link.active {
|
&.nav-tabs .nav-link.active {
|
||||||
border-bottom-width: 4px !important;
|
border-bottom-width: 4px !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: map-get($grid-breakpoints, 'md')) {
|
@media (--pgn-size-breakpoint-min-width-md) {
|
||||||
.courseware-search {
|
.courseware-search {
|
||||||
&__close {
|
&__close {
|
||||||
right: -2.5rem;
|
right: -2.5rem;
|
||||||
|
|||||||
@@ -17,7 +17,21 @@ Factory.define('progressTabData')
|
|||||||
percent: 1,
|
percent: 1,
|
||||||
is_passing: true,
|
is_passing: true,
|
||||||
},
|
},
|
||||||
|
final_grades: 0.5,
|
||||||
credit_course_requirements: null,
|
credit_course_requirements: null,
|
||||||
|
assignment_type_grade_summary: [
|
||||||
|
{
|
||||||
|
type: 'Homework',
|
||||||
|
short_label: 'HW',
|
||||||
|
weight: 1,
|
||||||
|
average_grade: 1,
|
||||||
|
weighted_grade: 1,
|
||||||
|
num_droppable: 1,
|
||||||
|
num_total: 2,
|
||||||
|
has_hidden_contribution: 'none',
|
||||||
|
last_grade_publish_date: null,
|
||||||
|
},
|
||||||
|
],
|
||||||
section_scores: [
|
section_scores: [
|
||||||
{
|
{
|
||||||
display_name: 'First section',
|
display_name: 'First section',
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ exports[`Data layer integration tests Test fetchDatesTab Should fetch, normalize
|
|||||||
"courseHome": {
|
"courseHome": {
|
||||||
"courseId": "course-v1:edX+DemoX+Demo_Course",
|
"courseId": "course-v1:edX+DemoX+Demo_Course",
|
||||||
"courseStatus": "loaded",
|
"courseStatus": "loaded",
|
||||||
|
"examsData": null,
|
||||||
"proctoringPanelStatus": "loading",
|
"proctoringPanelStatus": "loading",
|
||||||
"showSearch": false,
|
"showSearch": false,
|
||||||
"targetUserId": undefined,
|
"targetUserId": undefined,
|
||||||
@@ -397,6 +398,7 @@ exports[`Data layer integration tests Test fetchOutlineTab Should fetch, normali
|
|||||||
"courseHome": {
|
"courseHome": {
|
||||||
"courseId": "course-v1:edX+DemoX+Demo_Course",
|
"courseId": "course-v1:edX+DemoX+Demo_Course",
|
||||||
"courseStatus": "loaded",
|
"courseStatus": "loaded",
|
||||||
|
"examsData": null,
|
||||||
"proctoringPanelStatus": "loading",
|
"proctoringPanelStatus": "loading",
|
||||||
"showSearch": false,
|
"showSearch": false,
|
||||||
"targetUserId": undefined,
|
"targetUserId": undefined,
|
||||||
@@ -528,6 +530,7 @@ exports[`Data layer integration tests Test fetchOutlineTab Should fetch, normali
|
|||||||
"hideFromTOC": undefined,
|
"hideFromTOC": undefined,
|
||||||
"icon": null,
|
"icon": null,
|
||||||
"id": "block-v1:edX+DemoX+Demo_Course+type@sequential+block@bcdabcdabcdabcdabcdabcdabcdabcd1",
|
"id": "block-v1:edX+DemoX+Demo_Course+type@sequential+block@bcdabcdabcdabcdabcdabcdabcdabcd1",
|
||||||
|
"isPreview": false,
|
||||||
"navigationDisabled": undefined,
|
"navigationDisabled": undefined,
|
||||||
"sectionId": "block-v1:edX+DemoX+Demo_Course+type@chapter+block@bcdabcdabcdabcdabcdabcdabcdabcd2",
|
"sectionId": "block-v1:edX+DemoX+Demo_Course+type@chapter+block@bcdabcdabcdabcdabcdabcdabcdabcd2",
|
||||||
"showLink": true,
|
"showLink": true,
|
||||||
@@ -669,6 +672,7 @@ exports[`Data layer integration tests Test fetchProgressTab Should fetch, normal
|
|||||||
"courseHome": {
|
"courseHome": {
|
||||||
"courseId": "course-v1:edX+DemoX+Demo_Course",
|
"courseId": "course-v1:edX+DemoX+Demo_Course",
|
||||||
"courseStatus": "loaded",
|
"courseStatus": "loaded",
|
||||||
|
"examsData": null,
|
||||||
"proctoringPanelStatus": "loading",
|
"proctoringPanelStatus": "loading",
|
||||||
"showSearch": false,
|
"showSearch": false,
|
||||||
"targetUserId": undefined,
|
"targetUserId": undefined,
|
||||||
@@ -761,6 +765,19 @@ exports[`Data layer integration tests Test fetchProgressTab Should fetch, normal
|
|||||||
"progress": {
|
"progress": {
|
||||||
"course-v1:edX+DemoX+Demo_Course": {
|
"course-v1:edX+DemoX+Demo_Course": {
|
||||||
"accessExpiration": null,
|
"accessExpiration": null,
|
||||||
|
"assignmentTypeGradeSummary": [
|
||||||
|
{
|
||||||
|
"averageGrade": 1,
|
||||||
|
"hasHiddenContribution": "none",
|
||||||
|
"lastGradePublishDate": null,
|
||||||
|
"numDroppable": 1,
|
||||||
|
"numTotal": 2,
|
||||||
|
"shortLabel": "HW",
|
||||||
|
"type": "Homework",
|
||||||
|
"weight": 1,
|
||||||
|
"weightedGrade": 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
"certificateData": {},
|
"certificateData": {},
|
||||||
"completionSummary": {
|
"completionSummary": {
|
||||||
"completeCount": 1,
|
"completeCount": 1,
|
||||||
@@ -776,17 +793,17 @@ exports[`Data layer integration tests Test fetchProgressTab Should fetch, normal
|
|||||||
"creditCourseRequirements": null,
|
"creditCourseRequirements": null,
|
||||||
"end": "3027-03-31T00:00:00Z",
|
"end": "3027-03-31T00:00:00Z",
|
||||||
"enrollmentMode": "audit",
|
"enrollmentMode": "audit",
|
||||||
|
"finalGrades": 0.5,
|
||||||
"gradesFeatureIsFullyLocked": false,
|
"gradesFeatureIsFullyLocked": false,
|
||||||
"gradesFeatureIsPartiallyLocked": false,
|
"gradesFeatureIsPartiallyLocked": false,
|
||||||
"gradingPolicy": {
|
"gradingPolicy": {
|
||||||
"assignmentPolicies": [
|
"assignmentPolicies": [
|
||||||
{
|
{
|
||||||
"averageGrade": "1.0000",
|
|
||||||
"numDroppable": 1,
|
"numDroppable": 1,
|
||||||
|
"numTotal": 2,
|
||||||
"shortLabel": "HW",
|
"shortLabel": "HW",
|
||||||
"type": "Homework",
|
"type": "Homework",
|
||||||
"weight": 1,
|
"weight": 1,
|
||||||
"weightedGrade": 1,
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"gradeRange": {
|
"gradeRange": {
|
||||||
|
|||||||
@@ -3,93 +3,6 @@ import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
|||||||
import { logInfo } from '@edx/frontend-platform/logging';
|
import { logInfo } from '@edx/frontend-platform/logging';
|
||||||
import { appendBrowserTimezoneToUrl } from '../../utils';
|
import { appendBrowserTimezoneToUrl } from '../../utils';
|
||||||
|
|
||||||
const calculateAssignmentTypeGrades = (points, assignmentWeight, numDroppable) => {
|
|
||||||
let dropCount = numDroppable;
|
|
||||||
// Drop the lowest grades
|
|
||||||
while (dropCount && points.length >= dropCount) {
|
|
||||||
const lowestScore = Math.min(...points);
|
|
||||||
const lowestScoreIndex = points.indexOf(lowestScore);
|
|
||||||
points.splice(lowestScoreIndex, 1);
|
|
||||||
dropCount--;
|
|
||||||
}
|
|
||||||
let averageGrade = 0;
|
|
||||||
let weightedGrade = 0;
|
|
||||||
if (points.length) {
|
|
||||||
// Calculate the average grade for the assignment and round it. This rounding is not ideal and does not accurately
|
|
||||||
// reflect what a learner's grade would be, however, we must have parity with the current grading behavior that
|
|
||||||
// exists in edx-platform.
|
|
||||||
averageGrade = (points.reduce((a, b) => a + b, 0) / points.length).toFixed(4);
|
|
||||||
weightedGrade = averageGrade * assignmentWeight;
|
|
||||||
}
|
|
||||||
return { averageGrade, weightedGrade };
|
|
||||||
};
|
|
||||||
|
|
||||||
function normalizeAssignmentPolicies(assignmentPolicies, sectionScores) {
|
|
||||||
const gradeByAssignmentType = {};
|
|
||||||
assignmentPolicies.forEach(assignment => {
|
|
||||||
// Create an array with the number of total assignments and set the scores to 0
|
|
||||||
// as placeholders for assignments that have not yet been released
|
|
||||||
gradeByAssignmentType[assignment.type] = {
|
|
||||||
grades: Array(assignment.numTotal).fill(0),
|
|
||||||
numAssignmentsCreated: 0,
|
|
||||||
numTotalExpectedAssignments: assignment.numTotal,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
sectionScores.forEach((chapter) => {
|
|
||||||
chapter.subsections.forEach((subsection) => {
|
|
||||||
if (!(subsection.hasGradedAssignment && subsection.showGrades && subsection.numPointsPossible)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const {
|
|
||||||
assignmentType,
|
|
||||||
numPointsEarned,
|
|
||||||
numPointsPossible,
|
|
||||||
} = subsection;
|
|
||||||
|
|
||||||
// If a subsection's assignment type does not match an assignment policy in Studio,
|
|
||||||
// we won't be able to include it in this accumulation of grades by assignment type.
|
|
||||||
// This may happen if a course author has removed/renamed an assignment policy in Studio and
|
|
||||||
// neglected to update the subsection's of that assignment type
|
|
||||||
if (!gradeByAssignmentType[assignmentType]) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let {
|
|
||||||
numAssignmentsCreated,
|
|
||||||
} = gradeByAssignmentType[assignmentType];
|
|
||||||
|
|
||||||
numAssignmentsCreated++;
|
|
||||||
if (numAssignmentsCreated <= gradeByAssignmentType[assignmentType].numTotalExpectedAssignments) {
|
|
||||||
// Remove a placeholder grade so long as the number of recorded created assignments is less than the number
|
|
||||||
// of expected assignments
|
|
||||||
gradeByAssignmentType[assignmentType].grades.shift();
|
|
||||||
}
|
|
||||||
// Add the graded assignment to the list
|
|
||||||
gradeByAssignmentType[assignmentType].grades.push(numPointsEarned ? numPointsEarned / numPointsPossible : 0);
|
|
||||||
// Record the created assignment
|
|
||||||
gradeByAssignmentType[assignmentType].numAssignmentsCreated = numAssignmentsCreated;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return assignmentPolicies.map((assignment) => {
|
|
||||||
const { averageGrade, weightedGrade } = calculateAssignmentTypeGrades(
|
|
||||||
gradeByAssignmentType[assignment.type].grades,
|
|
||||||
assignment.weight,
|
|
||||||
assignment.numDroppable,
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
averageGrade,
|
|
||||||
numDroppable: assignment.numDroppable,
|
|
||||||
shortLabel: assignment.shortLabel,
|
|
||||||
type: assignment.type,
|
|
||||||
weight: assignment.weight,
|
|
||||||
weightedGrade,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tweak the metadata for consistency
|
* Tweak the metadata for consistency
|
||||||
* @param metadata the data to normalize
|
* @param metadata the data to normalize
|
||||||
@@ -155,6 +68,7 @@ export function normalizeOutlineBlocks(courseId, blocks) {
|
|||||||
title: block.display_name,
|
title: block.display_name,
|
||||||
hideFromTOC: block.hide_from_toc,
|
hideFromTOC: block.hide_from_toc,
|
||||||
navigationDisabled: block.navigation_disabled,
|
navigationDisabled: block.navigation_disabled,
|
||||||
|
isPreview: block.is_preview,
|
||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@@ -236,11 +150,6 @@ export async function getProgressTabData(courseId, targetUserId) {
|
|||||||
const { data } = await getAuthenticatedHttpClient().get(url);
|
const { data } = await getAuthenticatedHttpClient().get(url);
|
||||||
const camelCasedData = camelCaseObject(data);
|
const camelCasedData = camelCaseObject(data);
|
||||||
|
|
||||||
camelCasedData.gradingPolicy.assignmentPolicies = normalizeAssignmentPolicies(
|
|
||||||
camelCasedData.gradingPolicy.assignmentPolicies,
|
|
||||||
camelCasedData.sectionScores,
|
|
||||||
);
|
|
||||||
|
|
||||||
// We replace gradingPolicy.gradeRange with the original data to preserve the intended casing for the grade.
|
// We replace gradingPolicy.gradeRange with the original data to preserve the intended casing for the grade.
|
||||||
// For example, if a grade range key is "A", we do not want it to be camel cased (i.e. "A" would become "a")
|
// For example, if a grade range key is "A", we do not want it to be camel cased (i.e. "A" would become "a")
|
||||||
// in order to preserve a course team's desired grade formatting.
|
// in order to preserve a course team's desired grade formatting.
|
||||||
@@ -471,3 +380,24 @@ export async function searchCourseContentFromAPI(courseId, searchKeyword, option
|
|||||||
|
|
||||||
return camelCaseObject(response);
|
return camelCaseObject(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getExamsData(courseId, sequenceId) {
|
||||||
|
let url;
|
||||||
|
|
||||||
|
if (!getConfig().EXAMS_BASE_URL) {
|
||||||
|
url = `${getConfig().LMS_BASE_URL}/api/edx_proctoring/v1/proctored_exam/attempt/course_id/${encodeURIComponent(courseId)}?is_learning_mfe=true&content_id=${encodeURIComponent(sequenceId)}`;
|
||||||
|
} else {
|
||||||
|
url = `${getConfig().EXAMS_BASE_URL}/api/v1/student/exam/attempt/course_id/${encodeURIComponent(courseId)}/content_id/${encodeURIComponent(sequenceId)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { data } = await getAuthenticatedHttpClient().get(url);
|
||||||
|
return camelCaseObject(data);
|
||||||
|
} catch (error) {
|
||||||
|
const { httpErrorStatus } = error && error.customAttributes;
|
||||||
|
if (httpErrorStatus === 404) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,12 @@
|
|||||||
import { getTimeOffsetMillis } from './api';
|
import { getConfig, setConfig } from '@edx/frontend-platform';
|
||||||
|
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||||
|
import MockAdapter from 'axios-mock-adapter';
|
||||||
|
import { getTimeOffsetMillis, getExamsData } from './api';
|
||||||
|
import { initializeMockApp } from '../../setupTest';
|
||||||
|
|
||||||
|
initializeMockApp();
|
||||||
|
|
||||||
|
const axiosMock = new MockAdapter(getAuthenticatedHttpClient());
|
||||||
|
|
||||||
describe('Calculate the time offset properly', () => {
|
describe('Calculate the time offset properly', () => {
|
||||||
it('Should return 0 if the headerDate is not set', async () => {
|
it('Should return 0 if the headerDate is not set', async () => {
|
||||||
@@ -14,3 +22,156 @@ describe('Calculate the time offset properly', () => {
|
|||||||
expect(offset).toBe(86398750);
|
expect(offset).toBe(86398750);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('getExamsData', () => {
|
||||||
|
const courseId = 'course-v1:edX+DemoX+Demo_Course';
|
||||||
|
const sequenceId = 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@12345';
|
||||||
|
let originalConfig;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
axiosMock.reset();
|
||||||
|
originalConfig = getConfig();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
axiosMock.reset();
|
||||||
|
if (originalConfig) {
|
||||||
|
setConfig(originalConfig);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should use LMS URL when EXAMS_BASE_URL is not configured', async () => {
|
||||||
|
setConfig({
|
||||||
|
...originalConfig,
|
||||||
|
EXAMS_BASE_URL: undefined,
|
||||||
|
LMS_BASE_URL: 'http://localhost:18000',
|
||||||
|
});
|
||||||
|
|
||||||
|
const mockExamData = {
|
||||||
|
exam: {
|
||||||
|
id: 1,
|
||||||
|
course_id: courseId,
|
||||||
|
content_id: sequenceId,
|
||||||
|
exam_name: 'Test Exam',
|
||||||
|
attempt_status: 'created',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const expectedUrl = `http://localhost:18000/api/edx_proctoring/v1/proctored_exam/attempt/course_id/${encodeURIComponent(courseId)}?is_learning_mfe=true&content_id=${encodeURIComponent(sequenceId)}`;
|
||||||
|
axiosMock.onGet(expectedUrl).reply(200, mockExamData);
|
||||||
|
|
||||||
|
const result = await getExamsData(courseId, sequenceId);
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
exam: {
|
||||||
|
id: 1,
|
||||||
|
courseId,
|
||||||
|
contentId: sequenceId,
|
||||||
|
examName: 'Test Exam',
|
||||||
|
attemptStatus: 'created',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(axiosMock.history.get).toHaveLength(1);
|
||||||
|
expect(axiosMock.history.get[0].url).toBe(expectedUrl);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should use EXAMS_BASE_URL when configured', async () => {
|
||||||
|
setConfig({
|
||||||
|
...originalConfig,
|
||||||
|
EXAMS_BASE_URL: 'http://localhost:18740',
|
||||||
|
LMS_BASE_URL: 'http://localhost:18000',
|
||||||
|
});
|
||||||
|
|
||||||
|
const mockExamData = {
|
||||||
|
exam: {
|
||||||
|
id: 1,
|
||||||
|
course_id: courseId,
|
||||||
|
content_id: sequenceId,
|
||||||
|
exam_name: 'Test Exam',
|
||||||
|
attempt_status: 'submitted',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const expectedUrl = `http://localhost:18740/api/v1/student/exam/attempt/course_id/${encodeURIComponent(courseId)}/content_id/${encodeURIComponent(sequenceId)}`;
|
||||||
|
axiosMock.onGet(expectedUrl).reply(200, mockExamData);
|
||||||
|
|
||||||
|
const result = await getExamsData(courseId, sequenceId);
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
exam: {
|
||||||
|
id: 1,
|
||||||
|
courseId,
|
||||||
|
contentId: sequenceId,
|
||||||
|
examName: 'Test Exam',
|
||||||
|
attemptStatus: 'submitted',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(axiosMock.history.get).toHaveLength(1);
|
||||||
|
expect(axiosMock.history.get[0].url).toBe(expectedUrl);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return empty object when API returns 404', async () => {
|
||||||
|
setConfig({
|
||||||
|
...originalConfig,
|
||||||
|
EXAMS_BASE_URL: undefined,
|
||||||
|
LMS_BASE_URL: 'http://localhost:18000',
|
||||||
|
});
|
||||||
|
|
||||||
|
const expectedUrl = `http://localhost:18000/api/edx_proctoring/v1/proctored_exam/attempt/course_id/${encodeURIComponent(courseId)}?is_learning_mfe=true&content_id=${encodeURIComponent(sequenceId)}`;
|
||||||
|
|
||||||
|
// Mock a 404 error with the custom error response function to add customAttributes
|
||||||
|
axiosMock.onGet(expectedUrl).reply(() => {
|
||||||
|
const error = new Error('Request failed with status code 404');
|
||||||
|
error.response = { status: 404, data: {} };
|
||||||
|
error.customAttributes = { httpErrorStatus: 404 };
|
||||||
|
return Promise.reject(error);
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await getExamsData(courseId, sequenceId);
|
||||||
|
|
||||||
|
expect(result).toEqual({});
|
||||||
|
expect(axiosMock.history.get).toHaveLength(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw error for non-404 HTTP errors', async () => {
|
||||||
|
setConfig({
|
||||||
|
...originalConfig,
|
||||||
|
EXAMS_BASE_URL: undefined,
|
||||||
|
LMS_BASE_URL: 'http://localhost:18000',
|
||||||
|
});
|
||||||
|
|
||||||
|
const expectedUrl = `http://localhost:18000/api/edx_proctoring/v1/proctored_exam/attempt/course_id/${encodeURIComponent(courseId)}?is_learning_mfe=true&content_id=${encodeURIComponent(sequenceId)}`;
|
||||||
|
|
||||||
|
// Mock a 500 error with custom error response
|
||||||
|
axiosMock.onGet(expectedUrl).reply(() => {
|
||||||
|
const error = new Error('Request failed with status code 500');
|
||||||
|
error.response = { status: 500, data: { error: 'Server Error' } };
|
||||||
|
error.customAttributes = { httpErrorStatus: 500 };
|
||||||
|
return Promise.reject(error);
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(getExamsData(courseId, sequenceId)).rejects.toThrow();
|
||||||
|
expect(axiosMock.history.get).toHaveLength(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should properly encode URL parameters', async () => {
|
||||||
|
setConfig({
|
||||||
|
...originalConfig,
|
||||||
|
EXAMS_BASE_URL: 'http://localhost:18740',
|
||||||
|
LMS_BASE_URL: 'http://localhost:18000',
|
||||||
|
});
|
||||||
|
|
||||||
|
const specialCourseId = 'course-v1:edX+Demo X+Demo Course';
|
||||||
|
const specialSequenceId = 'block-v1:edX+Demo X+Demo Course+type@sequential+block@test sequence';
|
||||||
|
|
||||||
|
const mockExamData = { exam: { id: 1 } };
|
||||||
|
const expectedUrl = `http://localhost:18740/api/v1/student/exam/attempt/course_id/${encodeURIComponent(specialCourseId)}/content_id/${encodeURIComponent(specialSequenceId)}`;
|
||||||
|
axiosMock.onGet(expectedUrl).reply(200, mockExamData);
|
||||||
|
|
||||||
|
await getExamsData(specialCourseId, specialSequenceId);
|
||||||
|
|
||||||
|
expect(axiosMock.history.get[0].url).toBe(expectedUrl);
|
||||||
|
expect(axiosMock.history.get[0].url).toContain('course-v1%3AedX%2BDemo%20X%2BDemo%20Course');
|
||||||
|
expect(axiosMock.history.get[0].url).toContain('block-v1%3AedX%2BDemo%20X%2BDemo%20Course%2Btype%40sequential%2Bblock%40test%20sequence');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -297,4 +297,178 @@ describe('Data layer integration tests', () => {
|
|||||||
expect(enabled).toBe(false);
|
expect(enabled).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Test fetchExamAttemptsData', () => {
|
||||||
|
const sequenceIds = [
|
||||||
|
'block-v1:edX+DemoX+Demo_Course+type@sequential+block@12345',
|
||||||
|
'block-v1:edX+DemoX+Demo_Course+type@sequential+block@67890',
|
||||||
|
'block-v1:edX+DemoX+Demo_Course+type@sequential+block@abcde',
|
||||||
|
];
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
// Mock individual exam endpoints with different responses
|
||||||
|
sequenceIds.forEach((sequenceId, index) => {
|
||||||
|
// Handle both LMS and EXAMS service URL patterns
|
||||||
|
const lmsExamUrl = new RegExp(`.*edx_proctoring/v1/proctored_exam/attempt/course_id/${encodeURIComponent(courseId)}.*content_id=${encodeURIComponent(sequenceId)}.*`);
|
||||||
|
const examsServiceUrl = new RegExp(`.*/api/v1/student/exam/attempt/course_id/${encodeURIComponent(courseId)}/content_id/${encodeURIComponent(sequenceId)}.*`);
|
||||||
|
|
||||||
|
let attemptStatus = 'ready_to_start';
|
||||||
|
if (index === 0) {
|
||||||
|
attemptStatus = 'created';
|
||||||
|
} else if (index === 1) {
|
||||||
|
attemptStatus = 'submitted';
|
||||||
|
}
|
||||||
|
|
||||||
|
const mockExamData = {
|
||||||
|
exam: {
|
||||||
|
id: index + 1,
|
||||||
|
course_id: courseId,
|
||||||
|
content_id: sequenceId,
|
||||||
|
exam_name: `Test Exam ${index + 1}`,
|
||||||
|
attempt_status: attemptStatus,
|
||||||
|
time_remaining_seconds: 3600,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Mock both URL patterns
|
||||||
|
axiosMock.onGet(lmsExamUrl).reply(200, mockExamData);
|
||||||
|
axiosMock.onGet(examsServiceUrl).reply(200, mockExamData);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should fetch exam data for all sequence IDs and dispatch setExamsData', async () => {
|
||||||
|
await executeThunk(thunks.fetchExamAttemptsData(courseId, sequenceIds), store.dispatch);
|
||||||
|
|
||||||
|
const state = store.getState();
|
||||||
|
|
||||||
|
// Verify the examsData was set in the store
|
||||||
|
expect(state.courseHome.examsData).toHaveLength(3);
|
||||||
|
expect(state.courseHome.examsData).toEqual([
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
courseId,
|
||||||
|
contentId: sequenceIds[0],
|
||||||
|
examName: 'Test Exam 1',
|
||||||
|
attemptStatus: 'created',
|
||||||
|
timeRemainingSeconds: 3600,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
courseId,
|
||||||
|
contentId: sequenceIds[1],
|
||||||
|
examName: 'Test Exam 2',
|
||||||
|
attemptStatus: 'submitted',
|
||||||
|
timeRemainingSeconds: 3600,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
courseId,
|
||||||
|
contentId: sequenceIds[2],
|
||||||
|
examName: 'Test Exam 3',
|
||||||
|
attemptStatus: 'ready_to_start',
|
||||||
|
timeRemainingSeconds: 3600,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Verify all API calls were made
|
||||||
|
expect(axiosMock.history.get).toHaveLength(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle 404 responses and include empty objects in results', async () => {
|
||||||
|
// Override one endpoint to return 404 for both URL patterns
|
||||||
|
const examUrl404LMS = new RegExp(`.*edx_proctoring/v1/proctored_exam/attempt/course_id/${encodeURIComponent(courseId)}.*content_id=${encodeURIComponent(sequenceIds[1])}.*`);
|
||||||
|
const examUrl404Exams = new RegExp(`.*/api/v1/student/exam/attempt/course_id/${encodeURIComponent(courseId)}/content_id/${encodeURIComponent(sequenceIds[1])}.*`);
|
||||||
|
axiosMock.onGet(examUrl404LMS).reply(404);
|
||||||
|
axiosMock.onGet(examUrl404Exams).reply(404);
|
||||||
|
|
||||||
|
await executeThunk(thunks.fetchExamAttemptsData(courseId, sequenceIds), store.dispatch);
|
||||||
|
|
||||||
|
const state = store.getState();
|
||||||
|
|
||||||
|
// Verify the examsData includes empty object for 404 response
|
||||||
|
expect(state.courseHome.examsData).toHaveLength(3);
|
||||||
|
expect(state.courseHome.examsData[1]).toEqual({});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle API errors and log them while continuing with other requests', async () => {
|
||||||
|
// Override one endpoint to return 500 error for both URL patterns
|
||||||
|
const examUrl500LMS = new RegExp(`.*edx_proctoring/v1/proctored_exam/attempt/course_id/${encodeURIComponent(courseId)}.*content_id=${encodeURIComponent(sequenceIds[0])}.*`);
|
||||||
|
const examUrl500Exams = new RegExp(`.*/api/v1/student/exam/attempt/course_id/${encodeURIComponent(courseId)}/content_id/${encodeURIComponent(sequenceIds[0])}.*`);
|
||||||
|
axiosMock.onGet(examUrl500LMS).reply(500, { error: 'Server Error' });
|
||||||
|
axiosMock.onGet(examUrl500Exams).reply(500, { error: 'Server Error' });
|
||||||
|
|
||||||
|
await executeThunk(thunks.fetchExamAttemptsData(courseId, sequenceIds), store.dispatch);
|
||||||
|
|
||||||
|
const state = store.getState();
|
||||||
|
|
||||||
|
// Verify error was logged for the failed request
|
||||||
|
expect(loggingService.logError).toHaveBeenCalled();
|
||||||
|
|
||||||
|
// Verify the examsData still includes results for successful requests
|
||||||
|
expect(state.courseHome.examsData).toHaveLength(3);
|
||||||
|
// First item should be the error result (just empty object for API errors)
|
||||||
|
expect(state.courseHome.examsData[0]).toEqual({});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle empty sequence IDs array', async () => {
|
||||||
|
await executeThunk(thunks.fetchExamAttemptsData(courseId, []), store.dispatch);
|
||||||
|
|
||||||
|
const state = store.getState();
|
||||||
|
|
||||||
|
expect(state.courseHome.examsData).toEqual([]);
|
||||||
|
expect(axiosMock.history.get).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle mixed success and error responses', async () => {
|
||||||
|
// Setup mixed responses
|
||||||
|
const examUrl1LMS = new RegExp(`.*edx_proctoring/v1/proctored_exam/attempt/course_id/${encodeURIComponent(courseId)}.*content_id=${encodeURIComponent(sequenceIds[0])}.*`);
|
||||||
|
const examUrl1Exams = new RegExp(`.*/api/v1/student/exam/attempt/course_id/${encodeURIComponent(courseId)}/content_id/${encodeURIComponent(sequenceIds[0])}.*`);
|
||||||
|
const examUrl2LMS = new RegExp(`.*edx_proctoring/v1/proctored_exam/attempt/course_id/${encodeURIComponent(courseId)}.*content_id=${encodeURIComponent(sequenceIds[1])}.*`);
|
||||||
|
const examUrl2Exams = new RegExp(`.*/api/v1/student/exam/attempt/course_id/${encodeURIComponent(courseId)}/content_id/${encodeURIComponent(sequenceIds[1])}.*`);
|
||||||
|
const examUrl3LMS = new RegExp(`.*edx_proctoring/v1/proctored_exam/attempt/course_id/${encodeURIComponent(courseId)}.*content_id=${encodeURIComponent(sequenceIds[2])}.*`);
|
||||||
|
const examUrl3Exams = new RegExp(`.*/api/v1/student/exam/attempt/course_id/${encodeURIComponent(courseId)}/content_id/${encodeURIComponent(sequenceIds[2])}.*`);
|
||||||
|
|
||||||
|
axiosMock.onGet(examUrl1LMS).reply(200, {
|
||||||
|
exam: {
|
||||||
|
id: 1,
|
||||||
|
exam_name: 'Success Exam',
|
||||||
|
course_id: courseId,
|
||||||
|
content_id: sequenceIds[0],
|
||||||
|
attempt_status: 'created',
|
||||||
|
time_remaining_seconds: 3600,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
axiosMock.onGet(examUrl1Exams).reply(200, {
|
||||||
|
exam: {
|
||||||
|
id: 1,
|
||||||
|
exam_name: 'Success Exam',
|
||||||
|
course_id: courseId,
|
||||||
|
content_id: sequenceIds[0],
|
||||||
|
attempt_status: 'created',
|
||||||
|
time_remaining_seconds: 3600,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
axiosMock.onGet(examUrl2LMS).reply(404);
|
||||||
|
axiosMock.onGet(examUrl2Exams).reply(404);
|
||||||
|
axiosMock.onGet(examUrl3LMS).reply(500, { error: 'Server Error' });
|
||||||
|
axiosMock.onGet(examUrl3Exams).reply(500, { error: 'Server Error' });
|
||||||
|
|
||||||
|
await executeThunk(thunks.fetchExamAttemptsData(courseId, sequenceIds), store.dispatch);
|
||||||
|
|
||||||
|
const state = store.getState();
|
||||||
|
|
||||||
|
expect(state.courseHome.examsData).toHaveLength(3);
|
||||||
|
expect(state.courseHome.examsData[0]).toMatchObject({
|
||||||
|
id: 1,
|
||||||
|
examName: 'Success Exam',
|
||||||
|
courseId,
|
||||||
|
contentId: sequenceIds[0],
|
||||||
|
});
|
||||||
|
expect(state.courseHome.examsData[1]).toEqual({});
|
||||||
|
expect(state.courseHome.examsData[2]).toEqual({});
|
||||||
|
|
||||||
|
// Verify error was logged for the 500 error (may be called more than once due to multiple URL patterns)
|
||||||
|
expect(loggingService.logError).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ const slice = createSlice({
|
|||||||
toastBodyLink: null,
|
toastBodyLink: null,
|
||||||
toastHeader: '',
|
toastHeader: '',
|
||||||
showSearch: false,
|
showSearch: false,
|
||||||
|
examsData: null,
|
||||||
},
|
},
|
||||||
reducers: {
|
reducers: {
|
||||||
fetchProctoringInfoResolved: (state) => {
|
fetchProctoringInfoResolved: (state) => {
|
||||||
@@ -53,6 +54,9 @@ const slice = createSlice({
|
|||||||
setShowSearch: (state, { payload }) => {
|
setShowSearch: (state, { payload }) => {
|
||||||
state.showSearch = payload;
|
state.showSearch = payload;
|
||||||
},
|
},
|
||||||
|
setExamsData: (state, { payload }) => {
|
||||||
|
state.examsData = payload;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -64,6 +68,7 @@ export const {
|
|||||||
fetchTabSuccess,
|
fetchTabSuccess,
|
||||||
setCallToActionToast,
|
setCallToActionToast,
|
||||||
setShowSearch,
|
setShowSearch,
|
||||||
|
setExamsData,
|
||||||
} = slice.actions;
|
} = slice.actions;
|
||||||
|
|
||||||
export const {
|
export const {
|
||||||
|
|||||||
145
src/course-home/data/slice.test.js
Normal file
145
src/course-home/data/slice.test.js
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
import { reducer, setExamsData } from './slice';
|
||||||
|
|
||||||
|
describe('course home data slice', () => {
|
||||||
|
describe('setExamsData reducer', () => {
|
||||||
|
it('should set examsData in state', () => {
|
||||||
|
const initialState = {
|
||||||
|
courseStatus: 'loading',
|
||||||
|
courseId: null,
|
||||||
|
metadataModel: 'courseHomeCourseMetadata',
|
||||||
|
proctoringPanelStatus: 'loading',
|
||||||
|
tabFetchStates: {},
|
||||||
|
toastBodyText: '',
|
||||||
|
toastBodyLink: null,
|
||||||
|
toastHeader: '',
|
||||||
|
showSearch: false,
|
||||||
|
examsData: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockExamsData = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
courseId: 'course-v1:edX+DemoX+Demo_Course',
|
||||||
|
examName: 'Midterm Exam',
|
||||||
|
attemptStatus: 'created',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
courseId: 'course-v1:edX+DemoX+Demo_Course',
|
||||||
|
examName: 'Final Exam',
|
||||||
|
attemptStatus: 'submitted',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const action = setExamsData(mockExamsData);
|
||||||
|
const newState = reducer(initialState, action);
|
||||||
|
|
||||||
|
expect(newState.examsData).toEqual(mockExamsData);
|
||||||
|
expect(newState).toEqual({
|
||||||
|
...initialState,
|
||||||
|
examsData: mockExamsData,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update examsData when state already has data', () => {
|
||||||
|
const initialState = {
|
||||||
|
courseStatus: 'loaded',
|
||||||
|
courseId: 'test-course',
|
||||||
|
metadataModel: 'courseHomeCourseMetadata',
|
||||||
|
proctoringPanelStatus: 'loading',
|
||||||
|
tabFetchStates: {},
|
||||||
|
toastBodyText: '',
|
||||||
|
toastBodyLink: null,
|
||||||
|
toastHeader: '',
|
||||||
|
showSearch: false,
|
||||||
|
examsData: [{ id: 1, examName: 'Old Exam' }],
|
||||||
|
};
|
||||||
|
|
||||||
|
const newExamsData = [
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
courseId: 'course-v1:edX+DemoX+Demo_Course',
|
||||||
|
examName: 'New Exam',
|
||||||
|
attemptStatus: 'ready_to_start',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const action = setExamsData(newExamsData);
|
||||||
|
const newState = reducer(initialState, action);
|
||||||
|
|
||||||
|
expect(newState.examsData).toEqual(newExamsData);
|
||||||
|
expect(newState.examsData).not.toEqual(initialState.examsData);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set examsData to empty array', () => {
|
||||||
|
const initialState = {
|
||||||
|
courseStatus: 'loaded',
|
||||||
|
courseId: 'test-course',
|
||||||
|
metadataModel: 'courseHomeCourseMetadata',
|
||||||
|
proctoringPanelStatus: 'loading',
|
||||||
|
tabFetchStates: {},
|
||||||
|
toastBodyText: '',
|
||||||
|
toastBodyLink: null,
|
||||||
|
toastHeader: '',
|
||||||
|
showSearch: false,
|
||||||
|
examsData: [{ id: 1, examName: 'Some Exam' }],
|
||||||
|
};
|
||||||
|
|
||||||
|
const action = setExamsData([]);
|
||||||
|
const newState = reducer(initialState, action);
|
||||||
|
|
||||||
|
expect(newState.examsData).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set examsData to null', () => {
|
||||||
|
const initialState = {
|
||||||
|
courseStatus: 'loaded',
|
||||||
|
courseId: 'test-course',
|
||||||
|
metadataModel: 'courseHomeCourseMetadata',
|
||||||
|
proctoringPanelStatus: 'loading',
|
||||||
|
tabFetchStates: {},
|
||||||
|
toastBodyText: '',
|
||||||
|
toastBodyLink: null,
|
||||||
|
toastHeader: '',
|
||||||
|
showSearch: false,
|
||||||
|
examsData: [{ id: 1, examName: 'Some Exam' }],
|
||||||
|
};
|
||||||
|
|
||||||
|
const action = setExamsData(null);
|
||||||
|
const newState = reducer(initialState, action);
|
||||||
|
|
||||||
|
expect(newState.examsData).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not affect other state properties when setting examsData', () => {
|
||||||
|
const initialState = {
|
||||||
|
courseStatus: 'loaded',
|
||||||
|
courseId: 'test-course-id',
|
||||||
|
metadataModel: 'courseHomeCourseMetadata',
|
||||||
|
proctoringPanelStatus: 'complete',
|
||||||
|
tabFetchStates: { progress: 'loaded' },
|
||||||
|
toastBodyText: 'Toast message',
|
||||||
|
toastBodyLink: 'http://example.com',
|
||||||
|
toastHeader: 'Toast Header',
|
||||||
|
showSearch: true,
|
||||||
|
examsData: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockExamsData = [{ id: 1, examName: 'Test Exam' }];
|
||||||
|
const action = setExamsData(mockExamsData);
|
||||||
|
const newState = reducer(initialState, action);
|
||||||
|
|
||||||
|
// Verify that only examsData changed
|
||||||
|
expect(newState).toEqual({
|
||||||
|
...initialState,
|
||||||
|
examsData: mockExamsData,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Verify other properties remain unchanged
|
||||||
|
expect(newState.courseStatus).toBe(initialState.courseStatus);
|
||||||
|
expect(newState.courseId).toBe(initialState.courseId);
|
||||||
|
expect(newState.showSearch).toBe(initialState.showSearch);
|
||||||
|
expect(newState.toastBodyText).toBe(initialState.toastBodyText);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
executePostFromPostEvent,
|
executePostFromPostEvent,
|
||||||
getCourseHomeCourseMetadata,
|
getCourseHomeCourseMetadata,
|
||||||
getDatesTabData,
|
getDatesTabData,
|
||||||
|
getExamsData,
|
||||||
getOutlineTabData,
|
getOutlineTabData,
|
||||||
getProgressTabData,
|
getProgressTabData,
|
||||||
postCourseDeadlines,
|
postCourseDeadlines,
|
||||||
@@ -26,6 +27,7 @@ import {
|
|||||||
fetchTabRequest,
|
fetchTabRequest,
|
||||||
fetchTabSuccess,
|
fetchTabSuccess,
|
||||||
setCallToActionToast,
|
setCallToActionToast,
|
||||||
|
setExamsData,
|
||||||
} from './slice';
|
} from './slice';
|
||||||
|
|
||||||
import mapSearchResponse from '../courseware-search/map-search-response';
|
import mapSearchResponse from '../courseware-search/map-search-response';
|
||||||
@@ -223,3 +225,19 @@ export function searchCourseContent(courseId, searchKeyword) {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function fetchExamAttemptsData(courseId, sequenceIds) {
|
||||||
|
return async (dispatch) => {
|
||||||
|
const results = await Promise.all(sequenceIds.map(async (sequenceId) => {
|
||||||
|
try {
|
||||||
|
const response = await getExamsData(courseId, sequenceId);
|
||||||
|
return response.exam || {};
|
||||||
|
} catch (e) {
|
||||||
|
logError(e);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
dispatch(setExamsData(results));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@@ -171,6 +171,7 @@ const OutlineTab = () => {
|
|||||||
</div>
|
</div>
|
||||||
{rootCourseId && (
|
{rootCourseId && (
|
||||||
<div className="col col-12 col-md-4">
|
<div className="col col-12 col-md-4">
|
||||||
|
<CourseOutlineTabNotificationsSlot courseId={courseId} />
|
||||||
<ProctoringInfoPanel />
|
<ProctoringInfoPanel />
|
||||||
{ /** Defer showing the goal widget until the ProctoringInfoPanel has resolved or has been determined as
|
{ /** Defer showing the goal widget until the ProctoringInfoPanel has resolved or has been determined as
|
||||||
disabled to avoid components bouncing around too much as screen is rendered */ }
|
disabled to avoid components bouncing around too much as screen is rendered */ }
|
||||||
@@ -181,7 +182,6 @@ const OutlineTab = () => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<CourseTools />
|
<CourseTools />
|
||||||
<CourseOutlineTabNotificationsSlot courseId={courseId} />
|
|
||||||
<CourseDates />
|
<CourseDates />
|
||||||
<CourseHandouts />
|
<CourseHandouts />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,22 +1,18 @@
|
|||||||
@import "~@edx/brand/paragon/variables";
|
|
||||||
@import "~@openedx/paragon/scss/core/core";
|
|
||||||
@import "~@edx/brand/paragon/overrides";
|
|
||||||
|
|
||||||
.flag-button {
|
.flag-button {
|
||||||
background-color: $white;
|
background-color: var(--pgn-color-white);
|
||||||
border: 1px solid $light-400;
|
border: 1px solid var(--pgn-color-light-400);
|
||||||
border-radius: .2rem;
|
border-radius: .2rem;
|
||||||
box-shadow: 0 0 0 2px $light-400;
|
box-shadow: 0 0 0 2px var(--pgn-color-light-400);
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
border: 1px solid $primary-300;
|
border: 1px solid var(--pgn-color-primary-300);
|
||||||
box-shadow: 0 0 0 2px $white;
|
box-shadow: 0 0 0 2px var(--pgn-color-white);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.flag-button-selected {
|
.flag-button-selected {
|
||||||
border: 1px solid $primary-300;
|
border: 1px solid var(--pgn-color-primary-300);
|
||||||
box-shadow: 0 0 0 2px $primary-300;
|
box-shadow: 0 0 0 2px var(--pgn-color-primary-300);
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
.outline-sidebar-proctoring-panel {
|
.outline-sidebar-proctoring-panel {
|
||||||
border: 1px solid $dark-500;
|
border: 1px solid var(--pgn-color-dark-500);
|
||||||
border-top: 5px solid $brand-600;
|
border-top: 5px solid var(--pgn-color-brand-600);
|
||||||
}
|
}
|
||||||
.proctoring-onboarding-success {
|
.proctoring-onboarding-success {
|
||||||
border-top: 5px solid $primary-500;
|
border-top: 5px solid var(--pgn-color-primary-500);
|
||||||
}
|
}
|
||||||
.proctoring-onboarding-submitted {
|
.proctoring-onboarding-submitted {
|
||||||
border-top: 5px solid $dark-500;
|
border-top: 5px solid var(--pgn-color-dark-500);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import React from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import { useWindowSize } from '@openedx/paragon';
|
import { useWindowSize } from '@openedx/paragon';
|
||||||
import { useContextId } from '../../data/hooks';
|
import { useContextId } from '../../data/hooks';
|
||||||
|
import { useModel } from '../../generic/model-store';
|
||||||
import ProgressTabCertificateStatusSidePanelSlot from '../../plugin-slots/ProgressTabCertificateStatusSidePanelSlot';
|
import ProgressTabCertificateStatusSidePanelSlot from '../../plugin-slots/ProgressTabCertificateStatusSidePanelSlot';
|
||||||
|
|
||||||
import CourseCompletion from './course-completion/CourseCompletion';
|
import CourseCompletion from './course-completion/CourseCompletion';
|
||||||
@@ -10,11 +11,17 @@ import ProgressTabCertificateStatusMainBodySlot from '../../plugin-slots/Progres
|
|||||||
import ProgressTabCourseGradeSlot from '../../plugin-slots/ProgressTabCourseGradeSlot';
|
import ProgressTabCourseGradeSlot from '../../plugin-slots/ProgressTabCourseGradeSlot';
|
||||||
import ProgressTabGradeBreakdownSlot from '../../plugin-slots/ProgressTabGradeBreakdownSlot';
|
import ProgressTabGradeBreakdownSlot from '../../plugin-slots/ProgressTabGradeBreakdownSlot';
|
||||||
import ProgressTabRelatedLinksSlot from '../../plugin-slots/ProgressTabRelatedLinksSlot';
|
import ProgressTabRelatedLinksSlot from '../../plugin-slots/ProgressTabRelatedLinksSlot';
|
||||||
import { useModel } from '../../generic/model-store';
|
import { useGetExamsData } from './hooks';
|
||||||
|
|
||||||
const ProgressTab = () => {
|
const ProgressTab = () => {
|
||||||
const courseId = useContextId();
|
const courseId = useContextId();
|
||||||
const { disableProgressGraph } = useModel('progress', courseId);
|
const { disableProgressGraph, sectionScores } = useModel('progress', courseId);
|
||||||
|
|
||||||
|
const sequenceIds = useMemo(() => (
|
||||||
|
sectionScores.flatMap((section) => (section.subsections)).map((subsection) => subsection.blockKey)
|
||||||
|
), [sectionScores]);
|
||||||
|
|
||||||
|
useGetExamsData(courseId, sequenceIds);
|
||||||
|
|
||||||
const windowWidth = useWindowSize().width;
|
const windowWidth = useWindowSize().width;
|
||||||
if (windowWidth === undefined) {
|
if (windowWidth === undefined) {
|
||||||
|
|||||||
@@ -661,143 +661,133 @@ describe('Progress Tab', () => {
|
|||||||
expect(screen.getByText('Grade summary')).toBeInTheDocument();
|
expect(screen.getByText('Grade summary')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does not render Grade Summary when assignment policies are not populated', async () => {
|
it('does not render Grade Summary when assignment type grade summary is not populated', async () => {
|
||||||
setTabData({
|
setTabData({
|
||||||
grading_policy: {
|
assignment_type_grade_summary: [],
|
||||||
assignment_policies: [],
|
|
||||||
grade_range: {
|
|
||||||
pass: 0.75,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
section_scores: [],
|
|
||||||
});
|
});
|
||||||
await fetchAndRender();
|
await fetchAndRender();
|
||||||
expect(screen.queryByText('Grade summary')).not.toBeInTheDocument();
|
expect(screen.queryByText('Grade summary')).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('calculates grades correctly when number of droppable assignments equals total number of assignments', async () => {
|
it('shows lock icon when all subsections of assignment type are hidden', async () => {
|
||||||
setTabData({
|
setTabData({
|
||||||
grading_policy: {
|
grading_policy: {
|
||||||
assignment_policies: [
|
assignment_policies: [
|
||||||
{
|
|
||||||
num_droppable: 2,
|
|
||||||
num_total: 2,
|
|
||||||
short_label: 'HW',
|
|
||||||
type: 'Homework',
|
|
||||||
weight: 1,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
grade_range: {
|
|
||||||
pass: 0.75,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
await fetchAndRender();
|
|
||||||
expect(screen.getByText('Grade summary')).toBeInTheDocument();
|
|
||||||
// The row is comprised of "{Assignment type} {footnote - optional} {weight} {grade} {weighted grade}"
|
|
||||||
expect(screen.getByRole('row', { name: 'Homework 1 100% 0% 0%' })).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
it('calculates grades correctly when number of droppable assignments is less than total number of assignments', async () => {
|
|
||||||
await fetchAndRender();
|
|
||||||
expect(screen.getByText('Grade summary')).toBeInTheDocument();
|
|
||||||
// The row is comprised of "{Assignment type} {footnote - optional} {weight} {grade} {weighted grade}"
|
|
||||||
expect(screen.getByRole('row', { name: 'Homework 1 100% 100% 100%' })).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
it('calculates grades correctly when number of droppable assignments is zero', async () => {
|
|
||||||
setTabData({
|
|
||||||
grading_policy: {
|
|
||||||
assignment_policies: [
|
|
||||||
{
|
|
||||||
num_droppable: 0,
|
|
||||||
num_total: 2,
|
|
||||||
short_label: 'HW',
|
|
||||||
type: 'Homework',
|
|
||||||
weight: 1,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
grade_range: {
|
|
||||||
pass: 0.75,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
await fetchAndRender();
|
|
||||||
expect(screen.getByText('Grade summary')).toBeInTheDocument();
|
|
||||||
// The row is comprised of "{Assignment type} {weight} {grade} {weighted grade}"
|
|
||||||
expect(screen.getByRole('row', { name: 'Homework 100% 50% 50%' })).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
it('calculates grades correctly when number of total assignments is less than the number of assignments created', async () => {
|
|
||||||
setTabData({
|
|
||||||
grading_policy: {
|
|
||||||
assignment_policies: [
|
|
||||||
{
|
|
||||||
num_droppable: 1,
|
|
||||||
num_total: 1, // two assignments created in the factory, but 1 is expected per Studio settings
|
|
||||||
short_label: 'HW',
|
|
||||||
type: 'Homework',
|
|
||||||
weight: 1,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
grade_range: {
|
|
||||||
pass: 0.75,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
await fetchAndRender();
|
|
||||||
expect(screen.getByText('Grade summary')).toBeInTheDocument();
|
|
||||||
// The row is comprised of "{Assignment type} {footnote - optional} {weight} {grade} {weighted grade}"
|
|
||||||
expect(screen.getByRole('row', { name: 'Homework 1 100% 100% 100%' })).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
it('calculates grades correctly when number of total assignments is greater than the number of assignments created', async () => {
|
|
||||||
setTabData({
|
|
||||||
grading_policy: {
|
|
||||||
assignment_policies: [
|
|
||||||
{
|
|
||||||
num_droppable: 0,
|
|
||||||
num_total: 5, // two assignments created in the factory, but 5 are expected per Studio settings
|
|
||||||
short_label: 'HW',
|
|
||||||
type: 'Homework',
|
|
||||||
weight: 1,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
grade_range: {
|
|
||||||
pass: 0.75,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
await fetchAndRender();
|
|
||||||
expect(screen.getByText('Grade summary')).toBeInTheDocument();
|
|
||||||
// The row is comprised of "{Assignment type} {weight} {grade} {weighted grade}"
|
|
||||||
expect(screen.getByRole('row', { name: 'Homework 100% 20% 20%' })).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
it('calculates weighted grades correctly', async () => {
|
|
||||||
setTabData({
|
|
||||||
grading_policy: {
|
|
||||||
assignment_policies: [
|
|
||||||
{
|
|
||||||
num_droppable: 1,
|
|
||||||
num_total: 2,
|
|
||||||
short_label: 'HW',
|
|
||||||
type: 'Homework',
|
|
||||||
weight: 0.5,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
num_droppable: 0,
|
num_droppable: 0,
|
||||||
num_total: 1,
|
num_total: 1,
|
||||||
short_label: 'Ex',
|
short_label: 'Final',
|
||||||
type: 'Exam',
|
type: 'Final Exam',
|
||||||
weight: 0.5,
|
weight: 1,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
grade_range: {
|
grade_range: {
|
||||||
pass: 0.75,
|
pass: 0.75,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
assignment_type_grade_summary: [
|
||||||
|
{
|
||||||
|
type: 'Final Exam',
|
||||||
|
weight: 0.4,
|
||||||
|
average_grade: 0.0,
|
||||||
|
weighted_grade: 0.0,
|
||||||
|
last_grade_publish_date: '2025-10-15T14:17:04.368903Z',
|
||||||
|
has_hidden_contribution: 'all',
|
||||||
|
short_label: 'Final',
|
||||||
|
num_droppable: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
});
|
});
|
||||||
await fetchAndRender();
|
await fetchAndRender();
|
||||||
expect(screen.getByText('Grade summary')).toBeInTheDocument();
|
// Should show lock icon for grade and weighted grade
|
||||||
// The row is comprised of "{Assignment type} {footnote - optional} {weight} {grade} {weighted grade}"
|
expect(screen.getAllByTestId('lock-icon')).toHaveLength(2);
|
||||||
expect(screen.getByRole('row', { name: 'Homework 1 50% 100% 50%' })).toBeInTheDocument();
|
});
|
||||||
expect(screen.getByRole('row', { name: 'Exam 50% 0% 0%' })).toBeInTheDocument();
|
|
||||||
|
it('shows percent plus hidden grades when some subsections of assignment type are hidden', async () => {
|
||||||
|
setTabData({
|
||||||
|
grading_policy: {
|
||||||
|
assignment_policies: [
|
||||||
|
{
|
||||||
|
num_droppable: 0,
|
||||||
|
num_total: 2,
|
||||||
|
short_label: 'HW',
|
||||||
|
type: 'Homework',
|
||||||
|
weight: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
grade_range: {
|
||||||
|
pass: 0.75,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
assignment_type_grade_summary: [
|
||||||
|
{
|
||||||
|
type: 'Homework',
|
||||||
|
weight: 1,
|
||||||
|
average_grade: 0.25,
|
||||||
|
weighted_grade: 0.25,
|
||||||
|
last_grade_publish_date: '2025-10-15T14:17:04.368903Z',
|
||||||
|
has_hidden_contribution: 'some',
|
||||||
|
short_label: 'HW',
|
||||||
|
num_droppable: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
await fetchAndRender();
|
||||||
|
// Should show percent + hidden scores for grade and weighted grade
|
||||||
|
const hiddenScoresCells = screen.getAllByText(/% \+ Hidden Scores/);
|
||||||
|
expect(hiddenScoresCells).toHaveLength(2);
|
||||||
|
// Only correct visible scores should be shown (from subsection2)
|
||||||
|
// The correct visible score is 1/4 = 0.25 -> 25%
|
||||||
|
expect(hiddenScoresCells[0]).toHaveTextContent('25% + Hidden Scores');
|
||||||
|
expect(hiddenScoresCells[1]).toHaveTextContent('25% + Hidden Scores');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('displays a warning message with the latest due date when not all assignment scores are included in the total grade', async () => {
|
||||||
|
setTabData({
|
||||||
|
grading_policy: {
|
||||||
|
assignment_policies: [
|
||||||
|
{
|
||||||
|
num_droppable: 0,
|
||||||
|
num_total: 2,
|
||||||
|
short_label: 'HW',
|
||||||
|
type: 'Homework',
|
||||||
|
weight: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
grade_range: {
|
||||||
|
pass: 0.75,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
assignment_type_grade_summary: [
|
||||||
|
{
|
||||||
|
type: 'Homework',
|
||||||
|
weight: 1,
|
||||||
|
average_grade: 1,
|
||||||
|
weighted_grade: 1,
|
||||||
|
last_grade_publish_date: tomorrow.toISOString(),
|
||||||
|
has_hidden_contribution: 'none',
|
||||||
|
short_label: 'HW',
|
||||||
|
num_droppable: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
await fetchAndRender();
|
||||||
|
|
||||||
|
const formattedDateTime = new Intl.DateTimeFormat('en', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'long',
|
||||||
|
day: 'numeric',
|
||||||
|
hour: 'numeric',
|
||||||
|
minute: 'numeric',
|
||||||
|
timeZoneName: 'short',
|
||||||
|
}).format(tomorrow);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
screen.getByText(
|
||||||
|
`Some assignment scores are not yet included in your total grade. These grades will be released by ${formattedDateTime}.`,
|
||||||
|
),
|
||||||
|
).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders override notice', async () => {
|
it('renders override notice', async () => {
|
||||||
@@ -1500,4 +1490,287 @@ describe('Progress Tab', () => {
|
|||||||
expect(screen.getByText('Course progress for otherstudent')).toBeInTheDocument();
|
expect(screen.getByText('Course progress for otherstudent')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Exam data fetching integration', () => {
|
||||||
|
const mockSectionScores = [
|
||||||
|
{
|
||||||
|
display_name: 'Section 1',
|
||||||
|
subsections: [
|
||||||
|
{
|
||||||
|
assignment_type: 'Exam',
|
||||||
|
block_key: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@exam1',
|
||||||
|
display_name: 'Midterm Exam',
|
||||||
|
learner_has_access: true,
|
||||||
|
has_graded_assignment: true,
|
||||||
|
percent_graded: 0.8,
|
||||||
|
show_correctness: 'always',
|
||||||
|
show_grades: true,
|
||||||
|
url: '/mock-url',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
assignment_type: 'Homework',
|
||||||
|
block_key: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@homework1',
|
||||||
|
display_name: 'Homework 1',
|
||||||
|
learner_has_access: true,
|
||||||
|
has_graded_assignment: true,
|
||||||
|
percent_graded: 0.9,
|
||||||
|
show_correctness: 'always',
|
||||||
|
show_grades: true,
|
||||||
|
url: '/mock-url',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
display_name: 'Section 2',
|
||||||
|
subsections: [
|
||||||
|
{
|
||||||
|
assignment_type: 'Exam',
|
||||||
|
block_key: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@final_exam',
|
||||||
|
display_name: 'Final Exam',
|
||||||
|
learner_has_access: true,
|
||||||
|
has_graded_assignment: true,
|
||||||
|
percent_graded: 0.85,
|
||||||
|
show_correctness: 'always',
|
||||||
|
show_grades: true,
|
||||||
|
url: '/mock-url',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
// Reset any existing handlers to avoid conflicts
|
||||||
|
axiosMock.reset();
|
||||||
|
|
||||||
|
// Re-add the base mocks that other tests expect
|
||||||
|
axiosMock.onGet(courseMetadataUrl).reply(200, defaultMetadata);
|
||||||
|
axiosMock.onGet(progressUrl).reply(200, defaultTabData);
|
||||||
|
axiosMock.onGet(masqueradeUrl).reply(200, { success: true });
|
||||||
|
|
||||||
|
// Mock exam data endpoints using specific GET handlers
|
||||||
|
axiosMock.onGet(/.*exam1.*/).reply(200, {
|
||||||
|
exam: {
|
||||||
|
id: 1,
|
||||||
|
course_id: courseId,
|
||||||
|
content_id: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@exam1',
|
||||||
|
exam_name: 'Midterm Exam',
|
||||||
|
attempt_status: 'submitted',
|
||||||
|
time_remaining_seconds: 0,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
axiosMock.onGet(/.*homework1.*/).reply(404);
|
||||||
|
|
||||||
|
axiosMock.onGet(/.*final_exam.*/).reply(200, {
|
||||||
|
exam: {
|
||||||
|
id: 2,
|
||||||
|
course_id: courseId,
|
||||||
|
content_id: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@final_exam',
|
||||||
|
exam_name: 'Final Exam',
|
||||||
|
attempt_status: 'ready_to_start',
|
||||||
|
time_remaining_seconds: 7200,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should fetch exam data for all subsections when ProgressTab renders', async () => {
|
||||||
|
setTabData({ section_scores: mockSectionScores });
|
||||||
|
|
||||||
|
await fetchAndRender();
|
||||||
|
|
||||||
|
// Verify exam API calls were made for all subsections
|
||||||
|
expect(axiosMock.history.get.filter(req => req.url.includes('/api/v1/student/exam/attempt/'))).toHaveLength(3);
|
||||||
|
|
||||||
|
// Verify the exam data is in the Redux store
|
||||||
|
const state = store.getState();
|
||||||
|
expect(state.courseHome.examsData).toHaveLength(3);
|
||||||
|
|
||||||
|
// Check the exam data structure
|
||||||
|
expect(state.courseHome.examsData[0]).toEqual({
|
||||||
|
id: 1,
|
||||||
|
courseId,
|
||||||
|
contentId: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@exam1',
|
||||||
|
examName: 'Midterm Exam',
|
||||||
|
attemptStatus: 'submitted',
|
||||||
|
timeRemainingSeconds: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(state.courseHome.examsData[1]).toEqual({}); // 404 response for homework
|
||||||
|
|
||||||
|
expect(state.courseHome.examsData[2]).toEqual({
|
||||||
|
id: 2,
|
||||||
|
courseId,
|
||||||
|
contentId: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@final_exam',
|
||||||
|
examName: 'Final Exam',
|
||||||
|
attemptStatus: 'ready_to_start',
|
||||||
|
timeRemainingSeconds: 7200,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle empty section scores gracefully', async () => {
|
||||||
|
setTabData({ section_scores: [] });
|
||||||
|
|
||||||
|
await fetchAndRender();
|
||||||
|
|
||||||
|
// Verify no exam API calls were made
|
||||||
|
expect(axiosMock.history.get.filter(req => req.url.includes('/api/v1/student/exam/attempt/'))).toHaveLength(0);
|
||||||
|
|
||||||
|
// Verify empty exam data in Redux store
|
||||||
|
const state = store.getState();
|
||||||
|
expect(state.courseHome.examsData).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should re-fetch exam data when section scores change', async () => {
|
||||||
|
// Initial render with limited section scores
|
||||||
|
setTabData({
|
||||||
|
section_scores: [mockSectionScores[0]], // Only first section
|
||||||
|
});
|
||||||
|
|
||||||
|
await fetchAndRender();
|
||||||
|
|
||||||
|
// Verify initial API calls (2 subsections in first section)
|
||||||
|
expect(axiosMock.history.get.filter(req => req.url.includes('/api/v1/student/exam/attempt/'))).toHaveLength(2);
|
||||||
|
|
||||||
|
// Clear axios history to track new calls
|
||||||
|
axiosMock.resetHistory();
|
||||||
|
|
||||||
|
// Update with full section scores and re-render
|
||||||
|
setTabData({ section_scores: mockSectionScores });
|
||||||
|
await executeThunk(thunks.fetchProgressTab(courseId), store.dispatch);
|
||||||
|
|
||||||
|
// Verify additional API calls for all subsections
|
||||||
|
expect(axiosMock.history.get.filter(req => req.url.includes('/api/v1/student/exam/attempt/'))).toHaveLength(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle exam API errors gracefully without breaking ProgressTab', async () => {
|
||||||
|
// Clear existing mocks and setup specific error scenario
|
||||||
|
axiosMock.reset();
|
||||||
|
|
||||||
|
// Re-add base mocks
|
||||||
|
axiosMock.onGet(courseMetadataUrl).reply(200, defaultMetadata);
|
||||||
|
axiosMock.onGet(progressUrl).reply(200, defaultTabData);
|
||||||
|
axiosMock.onGet(masqueradeUrl).reply(200, { success: true });
|
||||||
|
|
||||||
|
// Mock first exam to return 500 error
|
||||||
|
axiosMock.onGet(/.*exam1.*/).reply(500, { error: 'Server Error' });
|
||||||
|
|
||||||
|
// Mock other exams to succeed
|
||||||
|
axiosMock.onGet(/.*homework1.*/).reply(404, { customAttributes: { httpErrorStatus: 404 } });
|
||||||
|
axiosMock.onGet(/.*final_exam.*/).reply(200, {
|
||||||
|
exam: {
|
||||||
|
id: 2,
|
||||||
|
course_id: courseId,
|
||||||
|
content_id: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@final_exam',
|
||||||
|
exam_name: 'Final Exam',
|
||||||
|
attempt_status: 'ready_to_start',
|
||||||
|
time_remaining_seconds: 7200,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
setTabData({ section_scores: mockSectionScores });
|
||||||
|
|
||||||
|
await fetchAndRender();
|
||||||
|
|
||||||
|
// Verify ProgressTab still renders successfully despite API error
|
||||||
|
expect(screen.getByText('Grades')).toBeInTheDocument();
|
||||||
|
|
||||||
|
// Verify the exam data includes error placeholder for failed request
|
||||||
|
const state = store.getState();
|
||||||
|
expect(state.courseHome.examsData).toHaveLength(3);
|
||||||
|
expect(state.courseHome.examsData[0]).toEqual({}); // Failed request returns empty object
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should use EXAMS_BASE_URL when configured for exam API calls', async () => {
|
||||||
|
// Configure EXAMS_BASE_URL
|
||||||
|
const originalConfig = getConfig();
|
||||||
|
setConfig({
|
||||||
|
...originalConfig,
|
||||||
|
EXAMS_BASE_URL: 'http://localhost:18740',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Override mock to use new base URL
|
||||||
|
const examUrlWithExamsBase = /http:\/\/localhost:18740\/api\/v1\/student\/exam\/attempt\/course_id.*/;
|
||||||
|
axiosMock.onGet(examUrlWithExamsBase).reply(200, {
|
||||||
|
exam: {
|
||||||
|
id: 1,
|
||||||
|
course_id: courseId,
|
||||||
|
exam_name: 'Test Exam',
|
||||||
|
attempt_status: 'created',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
setTabData({ section_scores: [mockSectionScores[0]] });
|
||||||
|
|
||||||
|
await fetchAndRender();
|
||||||
|
|
||||||
|
// Verify API calls use EXAMS_BASE_URL
|
||||||
|
const examApiCalls = axiosMock.history.get.filter(req => req.url.includes('localhost:18740'));
|
||||||
|
expect(examApiCalls.length).toBeGreaterThan(0);
|
||||||
|
|
||||||
|
// Restore original config
|
||||||
|
setConfig(originalConfig);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should extract sequence IDs correctly from nested section scores structure', async () => {
|
||||||
|
const complexSectionScores = [
|
||||||
|
{
|
||||||
|
display_name: 'Introduction',
|
||||||
|
subsections: [
|
||||||
|
{
|
||||||
|
assignment_type: 'Lecture',
|
||||||
|
block_key: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@intro',
|
||||||
|
display_name: 'Course Introduction',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
display_name: 'Assessments',
|
||||||
|
subsections: [
|
||||||
|
{
|
||||||
|
assignment_type: 'Exam',
|
||||||
|
block_key: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@quiz1',
|
||||||
|
display_name: 'Quiz 1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
assignment_type: 'Exam',
|
||||||
|
block_key: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@quiz2',
|
||||||
|
display_name: 'Quiz 2',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// Mock all the expected sequence IDs
|
||||||
|
const expectedSequenceIds = [
|
||||||
|
'block-v1:edX+DemoX+Demo_Course+type@sequential+block@intro',
|
||||||
|
'block-v1:edX+DemoX+Demo_Course+type@sequential+block@quiz1',
|
||||||
|
'block-v1:edX+DemoX+Demo_Course+type@sequential+block@quiz2',
|
||||||
|
];
|
||||||
|
|
||||||
|
expectedSequenceIds.forEach((sequenceId, index) => {
|
||||||
|
const examUrl = new RegExp(`.*/api/v1/student/exam/attempt/course_id/${encodeURIComponent(courseId)}/content_id/${encodeURIComponent(sequenceId)}.*`);
|
||||||
|
axiosMock.onGet(examUrl).reply(index === 0 ? 404 : 200, {
|
||||||
|
exam: {
|
||||||
|
id: index,
|
||||||
|
course_id: courseId,
|
||||||
|
content_id: sequenceId,
|
||||||
|
exam_name: `Test ${index}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
setTabData({ section_scores: complexSectionScores });
|
||||||
|
|
||||||
|
await fetchAndRender();
|
||||||
|
|
||||||
|
// Verify API calls were made for all extracted sequence IDs
|
||||||
|
expect(axiosMock.history.get.filter(req => req.url.includes('/api/v1/student/exam/attempt/'))).toHaveLength(3);
|
||||||
|
|
||||||
|
// Verify correct sequence IDs were used in API calls
|
||||||
|
const apiCalls = axiosMock.history.get.filter(req => req.url.includes('/api/v1/student/exam/attempt/'));
|
||||||
|
expectedSequenceIds.forEach(sequenceId => {
|
||||||
|
expect(apiCalls.some(call => call.url.includes(encodeURIComponent(sequenceId)))).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -7,18 +7,18 @@
|
|||||||
|
|
||||||
.donut-chart-label {
|
.donut-chart-label {
|
||||||
font: {
|
font: {
|
||||||
family: $font-family-sans-serif;
|
family: var(--pgn-typography-font-family-sans-serif);
|
||||||
size: .2rem;
|
size: .2rem;
|
||||||
weight: $font-weight-normal;
|
weight: var(--pgn-typography-font-weight-normal);
|
||||||
}
|
}
|
||||||
text-anchor: middle;
|
text-anchor: middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
.donut-chart-number {
|
.donut-chart-number {
|
||||||
font: {
|
font: {
|
||||||
family: $font-family-monospace;
|
family: var(--pgn-typography-font-family-monospace);
|
||||||
size: .5rem;
|
size: .5rem;
|
||||||
weight: $font-weight-bold;
|
weight: var(--pgn-typography-font-weight-bold);
|
||||||
}
|
}
|
||||||
line-height: 1rem;
|
line-height: 1rem;
|
||||||
text-anchor: middle;
|
text-anchor: middle;
|
||||||
@@ -29,7 +29,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.donut-chart-text {
|
.donut-chart-text {
|
||||||
fill: $primary-500;
|
fill: var(--pgn-color-primary-500);
|
||||||
-moz-transform: translateY(0.25em);
|
-moz-transform: translateY(0.25em);
|
||||||
-ms-transform: translateY(0.25em);
|
-ms-transform: translateY(0.25em);
|
||||||
-webkit-transform: translateY(0.25em);
|
-webkit-transform: translateY(0.25em);
|
||||||
@@ -56,7 +56,7 @@
|
|||||||
|
|
||||||
.donut-ring, .donut-segment, .donut-hole {
|
.donut-ring, .donut-segment, .donut-hole {
|
||||||
&.complete-stroke {
|
&.complete-stroke {
|
||||||
stroke: $info-500;
|
stroke: var(--pgn-color-info-500);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.divider-stroke {
|
&.divider-stroke {
|
||||||
@@ -65,10 +65,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&.incomplete-stroke {
|
&.incomplete-stroke {
|
||||||
stroke: $light-300;
|
stroke: var(--pgn-color-light-300);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.locked-stroke {
|
&.locked-stroke {
|
||||||
stroke: $primary-500;
|
stroke: var(--pgn-color-primary-500);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,26 +8,57 @@ import { useModel } from '../../../../generic/model-store';
|
|||||||
|
|
||||||
import GradeRangeTooltip from './GradeRangeTooltip';
|
import GradeRangeTooltip from './GradeRangeTooltip';
|
||||||
import messages from '../messages';
|
import messages from '../messages';
|
||||||
|
import { getLatestDueDateInFuture } from '../../utils';
|
||||||
|
|
||||||
|
const ResponsiveText = ({
|
||||||
|
wideScreen, children, hasLetterGrades, passingGrade,
|
||||||
|
}) => {
|
||||||
|
const className = wideScreen ? 'h4 m-0 align-bottom' : 'h5 align-bottom';
|
||||||
|
const iconSize = wideScreen ? 'h3' : 'h4';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span className={className}>
|
||||||
|
{children}
|
||||||
|
{hasLetterGrades && (
|
||||||
|
<span style={{ whiteSpace: 'nowrap' }}>
|
||||||
|
|
||||||
|
<GradeRangeTooltip iconButtonClassName={iconSize} passingGrade={passingGrade} />
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const NoticeRow = ({
|
||||||
|
wideScreen, icon, bgClass, message,
|
||||||
|
}) => {
|
||||||
|
const textClass = wideScreen ? 'h4 m-0 align-bottom' : 'h5 align-bottom';
|
||||||
|
return (
|
||||||
|
<div className={`row w-100 m-0 px-4 py-3 py-md-4 rounded-bottom ${bgClass}`}>
|
||||||
|
<div className="col-auto p-0">{icon}</div>
|
||||||
|
<div className="col-11 pl-2 px-0">
|
||||||
|
<span className={textClass}>{message}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const CourseGradeFooter = ({ passingGrade }) => {
|
const CourseGradeFooter = ({ passingGrade }) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const courseId = useContextId();
|
const courseId = useContextId();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
courseGrade: {
|
assignmentTypeGradeSummary,
|
||||||
isPassing,
|
courseGrade: { isPassing, letterGrade },
|
||||||
letterGrade,
|
gradingPolicy: { gradeRange },
|
||||||
},
|
|
||||||
gradingPolicy: {
|
|
||||||
gradeRange,
|
|
||||||
},
|
|
||||||
} = useModel('progress', courseId);
|
} = useModel('progress', courseId);
|
||||||
|
|
||||||
|
const latestDueDate = getLatestDueDateInFuture(assignmentTypeGradeSummary);
|
||||||
const wideScreen = useWindowSize().width >= breakpoints.medium.minWidth;
|
const wideScreen = useWindowSize().width >= breakpoints.medium.minWidth;
|
||||||
|
const hasLetterGrades = Object.keys(gradeRange).length > 1;
|
||||||
|
|
||||||
const hasLetterGrades = Object.keys(gradeRange).length > 1; // A pass/fail course will only have one key
|
// build footer text
|
||||||
let footerText = intl.formatMessage(messages.courseGradeFooterNonPassing, { passingGrade });
|
let footerText = intl.formatMessage(messages.courseGradeFooterNonPassing, { passingGrade });
|
||||||
|
|
||||||
if (isPassing) {
|
if (isPassing) {
|
||||||
if (hasLetterGrades) {
|
if (hasLetterGrades) {
|
||||||
const minGradeRangeCutoff = gradeRange[letterGrade] * 100;
|
const minGradeRangeCutoff = gradeRange[letterGrade] * 100;
|
||||||
@@ -47,42 +78,63 @@ const CourseGradeFooter = ({ passingGrade }) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const icon = isPassing ? <Icon src={CheckCircle} className="text-success-300 d-inline-flex align-bottom" />
|
const passingIcon = isPassing ? (
|
||||||
: <Icon src={WarningFilled} className="d-inline-flex align-bottom" />;
|
<Icon src={CheckCircle} className="text-success-300 d-inline-flex align-bottom" />
|
||||||
|
) : (
|
||||||
|
<Icon src={WarningFilled} className="d-inline-flex align-bottom" />
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`row w-100 m-0 px-4 py-3 py-md-4 rounded-bottom ${isPassing ? 'bg-success-100' : 'bg-warning-100'}`}>
|
<div>
|
||||||
<div className="col-auto p-0">
|
<NoticeRow
|
||||||
{icon}
|
wideScreen={wideScreen}
|
||||||
</div>
|
icon={passingIcon}
|
||||||
<div className="col-11 pl-2 px-0">
|
bgClass={isPassing ? 'bg-success-100' : 'bg-warning-100'}
|
||||||
{!wideScreen && (
|
message={(
|
||||||
<span className="h5 align-bottom">
|
<ResponsiveText
|
||||||
|
wideScreen={wideScreen}
|
||||||
|
hasLetterGrades={hasLetterGrades}
|
||||||
|
passingGrade={passingGrade}
|
||||||
|
>
|
||||||
{footerText}
|
{footerText}
|
||||||
{hasLetterGrades && (
|
</ResponsiveText>
|
||||||
<span style={{ whiteSpace: 'nowrap' }}>
|
|
||||||
|
|
||||||
<GradeRangeTooltip iconButtonClassName="h4" passingGrade={passingGrade} />
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
)}
|
)}
|
||||||
{wideScreen && (
|
/>
|
||||||
<span className="h4 m-0 align-bottom">
|
{latestDueDate && (
|
||||||
{footerText}
|
<NoticeRow
|
||||||
{hasLetterGrades && (
|
wideScreen={wideScreen}
|
||||||
<span style={{ whiteSpace: 'nowrap' }}>
|
icon={<Icon src={WarningFilled} className="d-inline-flex align-bottom" />}
|
||||||
|
bgClass="bg-warning-100"
|
||||||
<GradeRangeTooltip iconButtonClassName="h3" passingGrade={passingGrade} />
|
message={intl.formatMessage(messages.courseGradeFooterDueDateNotice, {
|
||||||
</span>
|
dueDate: intl.formatDate(latestDueDate, {
|
||||||
)}
|
year: 'numeric',
|
||||||
</span>
|
month: 'long',
|
||||||
)}
|
day: 'numeric',
|
||||||
</div>
|
hour: 'numeric',
|
||||||
|
minute: 'numeric',
|
||||||
|
timeZoneName: 'short',
|
||||||
|
}),
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
ResponsiveText.propTypes = {
|
||||||
|
wideScreen: PropTypes.bool.isRequired,
|
||||||
|
children: PropTypes.node.isRequired,
|
||||||
|
hasLetterGrades: PropTypes.bool.isRequired,
|
||||||
|
passingGrade: PropTypes.number.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
NoticeRow.propTypes = {
|
||||||
|
wideScreen: PropTypes.bool.isRequired,
|
||||||
|
icon: PropTypes.element.isRequired,
|
||||||
|
bgClass: PropTypes.string.isRequired,
|
||||||
|
message: PropTypes.string.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
CourseGradeFooter.propTypes = {
|
CourseGradeFooter.propTypes = {
|
||||||
passingGrade: PropTypes.number.isRequired,
|
passingGrade: PropTypes.number.isRequired,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ const CurrentGradeTooltip = ({ tooltipClassName }) => {
|
|||||||
const courseId = useContextId();
|
const courseId = useContextId();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
assignmentTypeGradeSummary,
|
||||||
courseGrade: {
|
courseGrade: {
|
||||||
isPassing,
|
isPassing,
|
||||||
percent,
|
percent,
|
||||||
@@ -25,6 +26,8 @@ const CurrentGradeTooltip = ({ tooltipClassName }) => {
|
|||||||
|
|
||||||
const isLocaleRtl = isRtl(getLocale());
|
const isLocaleRtl = isRtl(getLocale());
|
||||||
|
|
||||||
|
const hasHiddenGrades = assignmentTypeGradeSummary.some((assignmentType) => assignmentType.hasHiddenContribution !== 'none');
|
||||||
|
|
||||||
if (isLocaleRtl) {
|
if (isLocaleRtl) {
|
||||||
currentGradeDirection = currentGrade < 50 ? '-' : '';
|
currentGradeDirection = currentGrade < 50 ? '-' : '';
|
||||||
}
|
}
|
||||||
@@ -56,6 +59,15 @@ const CurrentGradeTooltip = ({ tooltipClassName }) => {
|
|||||||
>
|
>
|
||||||
{intl.formatMessage(messages.currentGradeLabel)}
|
{intl.formatMessage(messages.currentGradeLabel)}
|
||||||
</text>
|
</text>
|
||||||
|
<text
|
||||||
|
className="x-small"
|
||||||
|
textAnchor={currentGrade < 50 ? 'start' : 'end'}
|
||||||
|
x={`${Math.min(...[isLocaleRtl ? 100 - currentGrade : currentGrade, 100])}%`}
|
||||||
|
y="35px"
|
||||||
|
style={{ transform: `translateX(${currentGradeDirection}3.4em)` }}
|
||||||
|
>
|
||||||
|
{hasHiddenGrades ? ` + ${intl.formatMessage(messages.hiddenScoreLabel)}` : ''}
|
||||||
|
</text>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,24 +4,24 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.grade-bar__base {
|
.grade-bar__base {
|
||||||
fill: $light-300;
|
fill: var(--pgn-color-light-300);
|
||||||
}
|
}
|
||||||
|
|
||||||
.grade-bar__divider {
|
.grade-bar__divider {
|
||||||
fill: $primary-500;
|
fill: var(--pgn-color-primary-500);
|
||||||
width: 1px;
|
width: 1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.grade-bar--passing {
|
.grade-bar--passing {
|
||||||
fill: $primary-500;
|
fill: var(--pgn-color-primary-500);
|
||||||
}
|
}
|
||||||
|
|
||||||
.grade-bar--current-passing {
|
.grade-bar--current-passing {
|
||||||
fill: $success-500;
|
fill: var(--pgn-color-success-500);
|
||||||
}
|
}
|
||||||
|
|
||||||
.grade-bar--current-non-passing {
|
.grade-bar--current-non-passing {
|
||||||
fill: $accent-b;
|
fill: var(--pgn-color-accent-b);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,22 +31,22 @@
|
|||||||
|
|
||||||
#minimum-grade-tooltip {
|
#minimum-grade-tooltip {
|
||||||
.arrow::after {
|
.arrow::after {
|
||||||
border-bottom-color: $primary-500;
|
border-bottom-color: var(--pgn-color-primary-500);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#passing-grade-tooltip {
|
#passing-grade-tooltip {
|
||||||
background: $success-500;
|
background: var(--pgn-color-success-500);
|
||||||
|
|
||||||
.arrow::after {
|
.arrow::after {
|
||||||
border-top-color: $success-500;
|
border-top-color: var(--pgn-color-success-500);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#non-passing-grade-tooltip {
|
#non-passing-grade-tooltip {
|
||||||
background: $accent-b;
|
background: var(--pgn-color-accent-b);
|
||||||
|
|
||||||
.arrow::after {
|
.arrow::after {
|
||||||
border-top-color: $accent-b;
|
border-top-color: var(--pgn-color-accent-b);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,14 +10,12 @@ const GradeSummary = () => {
|
|||||||
const courseId = useContextId();
|
const courseId = useContextId();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
gradingPolicy: {
|
assignmentTypeGradeSummary,
|
||||||
assignmentPolicies,
|
|
||||||
},
|
|
||||||
} = useModel('progress', courseId);
|
} = useModel('progress', courseId);
|
||||||
|
|
||||||
const [allOfSomeAssignmentTypeIsLocked, setAllOfSomeAssignmentTypeIsLocked] = useState(false);
|
const [allOfSomeAssignmentTypeIsLocked, setAllOfSomeAssignmentTypeIsLocked] = useState(false);
|
||||||
|
|
||||||
if (assignmentPolicies.length === 0) {
|
if (assignmentTypeGradeSummary.length === 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import PropTypes from 'prop-types';
|
|||||||
|
|
||||||
import { getLocale, isRtl, useIntl } from '@edx/frontend-platform/i18n';
|
import { getLocale, isRtl, useIntl } from '@edx/frontend-platform/i18n';
|
||||||
import { DataTable } from '@openedx/paragon';
|
import { DataTable } from '@openedx/paragon';
|
||||||
|
import { Lock } from '@openedx/paragon/icons';
|
||||||
import { useContextId } from '../../../../data/hooks';
|
import { useContextId } from '../../../../data/hooks';
|
||||||
import { useModel } from '../../../../generic/model-store';
|
import { useModel } from '../../../../generic/model-store';
|
||||||
|
|
||||||
@@ -16,9 +17,7 @@ const GradeSummaryTable = ({ setAllOfSomeAssignmentTypeIsLocked }) => {
|
|||||||
const courseId = useContextId();
|
const courseId = useContextId();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
gradingPolicy: {
|
assignmentTypeGradeSummary,
|
||||||
assignmentPolicies,
|
|
||||||
},
|
|
||||||
gradesFeatureIsFullyLocked,
|
gradesFeatureIsFullyLocked,
|
||||||
sectionScores,
|
sectionScores,
|
||||||
} = useModel('progress', courseId);
|
} = useModel('progress', courseId);
|
||||||
@@ -55,7 +54,7 @@ const GradeSummaryTable = ({ setAllOfSomeAssignmentTypeIsLocked }) => {
|
|||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
const gradeSummaryData = assignmentPolicies.map((assignment) => {
|
const gradeSummaryData = assignmentTypeGradeSummary.map((assignment) => {
|
||||||
const {
|
const {
|
||||||
averageGrade,
|
averageGrade,
|
||||||
numDroppable,
|
numDroppable,
|
||||||
@@ -80,13 +79,24 @@ const GradeSummaryTable = ({ setAllOfSomeAssignmentTypeIsLocked }) => {
|
|||||||
const locked = !gradesFeatureIsFullyLocked && hasNoAccessToAssignmentsOfType(assignmentType);
|
const locked = !gradesFeatureIsFullyLocked && hasNoAccessToAssignmentsOfType(assignmentType);
|
||||||
const isLocaleRtl = isRtl(getLocale());
|
const isLocaleRtl = isRtl(getLocale());
|
||||||
|
|
||||||
|
let weightedGradeDisplay = `${getGradePercent(weightedGrade)}${isLocaleRtl ? '\u200f' : ''}%`;
|
||||||
|
let gradeDisplay = `${getGradePercent(averageGrade)}${isLocaleRtl ? '\u200f' : ''}%`;
|
||||||
|
|
||||||
|
if (assignment.hasHiddenContribution === 'all') {
|
||||||
|
gradeDisplay = <Lock data-testid="lock-icon" />;
|
||||||
|
weightedGradeDisplay = <Lock data-testid="lock-icon" />;
|
||||||
|
} else if (assignment.hasHiddenContribution === 'some') {
|
||||||
|
gradeDisplay = `${getGradePercent(averageGrade)}${isLocaleRtl ? '\u200f' : ''}% + ${intl.formatMessage(messages.hiddenScoreLabel)}`;
|
||||||
|
weightedGradeDisplay = `${getGradePercent(weightedGrade)}${isLocaleRtl ? '\u200f' : ''}% + ${intl.formatMessage(messages.hiddenScoreLabel)}`;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
type: {
|
type: {
|
||||||
footnoteId, footnoteMarker, type: assignmentType, locked,
|
footnoteId, footnoteMarker, type: assignmentType, locked,
|
||||||
},
|
},
|
||||||
weight: { weight: `${(weight * 100).toFixed(0)}${isLocaleRtl ? '\u200f' : ''}%`, locked },
|
weight: { weight: `${(weight * 100).toFixed(0)}${isLocaleRtl ? '\u200f' : ''}%`, locked },
|
||||||
grade: { grade: `${getGradePercent(averageGrade)}${isLocaleRtl ? '\u200f' : ''}%`, locked },
|
grade: { grade: gradeDisplay, locked },
|
||||||
weightedGrade: { weightedGrade: `${getGradePercent(weightedGrade)}${isLocaleRtl ? '\u200f' : ''}%`, locked },
|
weightedGrade: { weightedGrade: weightedGradeDisplay, locked },
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
const getAssignmentTypeCell = (value) => (
|
const getAssignmentTypeCell = (value) => (
|
||||||
@@ -102,6 +112,16 @@ const GradeSummaryTable = ({ setAllOfSomeAssignmentTypeIsLocked }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
<ul className="micro mb-3 pl-3 text-gray-700">
|
||||||
|
<li>
|
||||||
|
<b>{intl.formatMessage(messages.hiddenScoreLabel)}: </b>
|
||||||
|
{intl.formatMessage(messages.hiddenScoreInfoText)}
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<b><Lock style={{ height: '15px' }} />: </b>
|
||||||
|
{` ${intl.formatMessage(messages.hiddenScoreLockInfoText)}`}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
<DataTable
|
<DataTable
|
||||||
data={gradeSummaryData}
|
data={gradeSummaryData}
|
||||||
itemCount={gradeSummaryData.length}
|
itemCount={gradeSummaryData.length}
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
import { useContext } from 'react';
|
|
||||||
|
|
||||||
import { getLocale, isRtl, useIntl } from '@edx/frontend-platform/i18n';
|
import { getLocale, isRtl, useIntl } from '@edx/frontend-platform/i18n';
|
||||||
import {
|
import {
|
||||||
DataTable,
|
DataTable,
|
||||||
DataTableContext,
|
|
||||||
Icon,
|
Icon,
|
||||||
OverlayTrigger,
|
OverlayTrigger,
|
||||||
Stack,
|
Stack,
|
||||||
@@ -17,18 +14,6 @@ import messages from '../messages';
|
|||||||
|
|
||||||
const GradeSummaryTableFooter = () => {
|
const GradeSummaryTableFooter = () => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
|
||||||
const { data } = useContext(DataTableContext);
|
|
||||||
|
|
||||||
const rawGrade = data.reduce(
|
|
||||||
(grade, currentValue) => {
|
|
||||||
const { weightedGrade } = currentValue.weightedGrade;
|
|
||||||
const percent = weightedGrade.replace(/%/g, '').trim();
|
|
||||||
return grade + parseFloat(percent);
|
|
||||||
},
|
|
||||||
0,
|
|
||||||
).toFixed(2);
|
|
||||||
|
|
||||||
const courseId = useContextId();
|
const courseId = useContextId();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -36,8 +21,16 @@ const GradeSummaryTableFooter = () => {
|
|||||||
isPassing,
|
isPassing,
|
||||||
percent,
|
percent,
|
||||||
},
|
},
|
||||||
|
finalGrades,
|
||||||
} = useModel('progress', courseId);
|
} = useModel('progress', courseId);
|
||||||
|
|
||||||
|
const getGradePercent = (grade) => {
|
||||||
|
const percentage = grade * 100;
|
||||||
|
return Number.isInteger(percentage) ? percentage.toFixed(0) : percentage.toFixed(2);
|
||||||
|
};
|
||||||
|
|
||||||
|
const rawGrade = getGradePercent(finalGrades);
|
||||||
|
|
||||||
const bgColor = isPassing ? 'bg-success-100' : 'bg-warning-100';
|
const bgColor = isPassing ? 'bg-success-100' : 'bg-warning-100';
|
||||||
const totalGrade = (percent * 100).toFixed(0);
|
const totalGrade = (percent * 100).toFixed(0);
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,11 @@ const messages = defineMessages({
|
|||||||
defaultMessage: 'Your current grade is {currentGrade}%. A weighted grade of {passingGrade}% is required to pass in this course.',
|
defaultMessage: 'Your current grade is {currentGrade}%. A weighted grade of {passingGrade}% is required to pass in this course.',
|
||||||
description: 'Alt text for the grade chart bar',
|
description: 'Alt text for the grade chart bar',
|
||||||
},
|
},
|
||||||
|
courseGradeFooterDueDateNotice: {
|
||||||
|
id: 'progress.courseGrade.footer.dueDateNotice',
|
||||||
|
defaultMessage: 'Some assignment scores are not yet included in your total grade. These grades will be released by {dueDate}.',
|
||||||
|
description: 'This is shown when there are pending assignments with a due date in the future',
|
||||||
|
},
|
||||||
courseGradeFooterGenericPassing: {
|
courseGradeFooterGenericPassing: {
|
||||||
id: 'progress.courseGrade.footer.generic.passing',
|
id: 'progress.courseGrade.footer.generic.passing',
|
||||||
defaultMessage: 'You’re currently passing this course',
|
defaultMessage: 'You’re currently passing this course',
|
||||||
@@ -148,6 +153,21 @@ const messages = defineMessages({
|
|||||||
+ "Your weighted grade is what's used to determine if you pass the course.",
|
+ "Your weighted grade is what's used to determine if you pass the course.",
|
||||||
description: 'The content of (tip box) for the grade summary section',
|
description: 'The content of (tip box) for the grade summary section',
|
||||||
},
|
},
|
||||||
|
hiddenScoreLabel: {
|
||||||
|
id: 'progress.hiddenScoreLabel',
|
||||||
|
defaultMessage: 'Hidden Scores',
|
||||||
|
description: 'Text to indicate that some scores are hidden',
|
||||||
|
},
|
||||||
|
hiddenScoreInfoText: {
|
||||||
|
id: 'progress.hiddenScoreInfoText',
|
||||||
|
defaultMessage: 'Scores from assignments that count toward your final grade but some are not shown here.',
|
||||||
|
description: 'Information text about hidden score label',
|
||||||
|
},
|
||||||
|
hiddenScoreLockInfoText: {
|
||||||
|
id: 'progress.hiddenScoreLockInfoText',
|
||||||
|
defaultMessage: 'Scores for an assignment type are hidden but still counted toward the course grade.',
|
||||||
|
description: 'Information text about hidden score label when learners have limited access to grades feature',
|
||||||
|
},
|
||||||
noAccessToAssignmentType: {
|
noAccessToAssignmentType: {
|
||||||
id: 'progress.noAcessToAssignmentType',
|
id: 'progress.noAcessToAssignmentType',
|
||||||
defaultMessage: 'You do not have access to assignments of type {assignmentType}',
|
defaultMessage: 'You do not have access to assignments of type {assignmentType}',
|
||||||
|
|||||||
12
src/course-home/progress-tab/hooks.jsx
Normal file
12
src/course-home/progress-tab/hooks.jsx
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { useEffect } from 'react';
|
||||||
|
import { useDispatch } from 'react-redux';
|
||||||
|
|
||||||
|
import { fetchExamAttemptsData } from '../data/thunks';
|
||||||
|
|
||||||
|
export function useGetExamsData(courseId, sequenceIds) {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
dispatch(fetchExamAttemptsData(courseId, sequenceIds));
|
||||||
|
}, [dispatch, courseId, sequenceIds]);
|
||||||
|
}
|
||||||
168
src/course-home/progress-tab/hooks.test.jsx
Normal file
168
src/course-home/progress-tab/hooks.test.jsx
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
import { renderHook } from '@testing-library/react';
|
||||||
|
import { useDispatch } from 'react-redux';
|
||||||
|
import { useGetExamsData } from './hooks';
|
||||||
|
import { fetchExamAttemptsData } from '../data/thunks';
|
||||||
|
|
||||||
|
// Mock the dependencies
|
||||||
|
jest.mock('react-redux', () => ({
|
||||||
|
useDispatch: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('../data/thunks', () => ({
|
||||||
|
fetchExamAttemptsData: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('useGetExamsData hook', () => {
|
||||||
|
const mockDispatch = jest.fn();
|
||||||
|
const mockFetchExamAttemptsData = jest.fn();
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
useDispatch.mockReturnValue(mockDispatch);
|
||||||
|
fetchExamAttemptsData.mockReturnValue(mockFetchExamAttemptsData);
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should dispatch fetchExamAttemptsData on mount', () => {
|
||||||
|
const courseId = 'course-v1:edX+DemoX+Demo_Course';
|
||||||
|
const sequenceIds = [
|
||||||
|
'block-v1:edX+DemoX+Demo_Course+type@sequential+block@12345',
|
||||||
|
'block-v1:edX+DemoX+Demo_Course+type@sequential+block@67890',
|
||||||
|
];
|
||||||
|
|
||||||
|
renderHook(() => useGetExamsData(courseId, sequenceIds));
|
||||||
|
|
||||||
|
expect(fetchExamAttemptsData).toHaveBeenCalledWith(courseId, sequenceIds);
|
||||||
|
expect(mockDispatch).toHaveBeenCalledWith(mockFetchExamAttemptsData);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should re-dispatch when courseId changes', () => {
|
||||||
|
const initialCourseId = 'course-v1:edX+DemoX+Demo_Course';
|
||||||
|
const newCourseId = 'course-v1:edX+NewCourse+Demo';
|
||||||
|
const sequenceIds = ['block-v1:edX+DemoX+Demo_Course+type@sequential+block@12345'];
|
||||||
|
|
||||||
|
const { rerender } = renderHook(
|
||||||
|
({ courseId: cId, sequenceIds: sIds }) => useGetExamsData(cId, sIds),
|
||||||
|
{
|
||||||
|
initialProps: { courseId: initialCourseId, sequenceIds },
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Verify initial call
|
||||||
|
expect(fetchExamAttemptsData).toHaveBeenCalledWith(initialCourseId, sequenceIds);
|
||||||
|
expect(mockDispatch).toHaveBeenCalledWith(mockFetchExamAttemptsData);
|
||||||
|
|
||||||
|
// Clear mocks to isolate the re-render call
|
||||||
|
jest.clearAllMocks();
|
||||||
|
|
||||||
|
// Re-render with new courseId
|
||||||
|
rerender({ courseId: newCourseId, sequenceIds });
|
||||||
|
|
||||||
|
expect(fetchExamAttemptsData).toHaveBeenCalledWith(newCourseId, sequenceIds);
|
||||||
|
expect(mockDispatch).toHaveBeenCalledWith(mockFetchExamAttemptsData);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should re-dispatch when sequenceIds changes', () => {
|
||||||
|
const courseId = 'course-v1:edX+DemoX+Demo_Course';
|
||||||
|
const initialSequenceIds = ['block-v1:edX+DemoX+Demo_Course+type@sequential+block@12345'];
|
||||||
|
const newSequenceIds = [
|
||||||
|
'block-v1:edX+DemoX+Demo_Course+type@sequential+block@12345',
|
||||||
|
'block-v1:edX+DemoX+Demo_Course+type@sequential+block@67890',
|
||||||
|
];
|
||||||
|
|
||||||
|
const { rerender } = renderHook(
|
||||||
|
({ courseId: cId, sequenceIds: sIds }) => useGetExamsData(cId, sIds),
|
||||||
|
{
|
||||||
|
initialProps: { courseId, sequenceIds: initialSequenceIds },
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Verify initial call
|
||||||
|
expect(fetchExamAttemptsData).toHaveBeenCalledWith(courseId, initialSequenceIds);
|
||||||
|
expect(mockDispatch).toHaveBeenCalledWith(mockFetchExamAttemptsData);
|
||||||
|
|
||||||
|
// Clear mocks to isolate the re-render call
|
||||||
|
jest.clearAllMocks();
|
||||||
|
|
||||||
|
// Re-render with new sequenceIds
|
||||||
|
rerender({ courseId, sequenceIds: newSequenceIds });
|
||||||
|
|
||||||
|
expect(fetchExamAttemptsData).toHaveBeenCalledWith(courseId, newSequenceIds);
|
||||||
|
expect(mockDispatch).toHaveBeenCalledWith(mockFetchExamAttemptsData);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not re-dispatch when neither courseId nor sequenceIds changes', () => {
|
||||||
|
const courseId = 'course-v1:edX+DemoX+Demo_Course';
|
||||||
|
const sequenceIds = ['block-v1:edX+DemoX+Demo_Course+type@sequential+block@12345'];
|
||||||
|
|
||||||
|
const { rerender } = renderHook(
|
||||||
|
({ courseId: cId, sequenceIds: sIds }) => useGetExamsData(cId, sIds),
|
||||||
|
{
|
||||||
|
initialProps: { courseId, sequenceIds },
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Verify initial call
|
||||||
|
expect(fetchExamAttemptsData).toHaveBeenCalledTimes(1);
|
||||||
|
expect(mockDispatch).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
// Clear mocks to isolate the re-render call
|
||||||
|
jest.clearAllMocks();
|
||||||
|
|
||||||
|
// Re-render with same props
|
||||||
|
rerender({ courseId, sequenceIds });
|
||||||
|
|
||||||
|
// Should not dispatch again
|
||||||
|
expect(fetchExamAttemptsData).not.toHaveBeenCalled();
|
||||||
|
expect(mockDispatch).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle empty sequenceIds array', () => {
|
||||||
|
const courseId = 'course-v1:edX+DemoX+Demo_Course';
|
||||||
|
const sequenceIds = [];
|
||||||
|
|
||||||
|
renderHook(() => useGetExamsData(courseId, sequenceIds));
|
||||||
|
|
||||||
|
expect(fetchExamAttemptsData).toHaveBeenCalledWith(courseId, []);
|
||||||
|
expect(mockDispatch).toHaveBeenCalledWith(mockFetchExamAttemptsData);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle null/undefined courseId', () => {
|
||||||
|
const sequenceIds = ['block-v1:edX+DemoX+Demo_Course+type@sequential+block@12345'];
|
||||||
|
|
||||||
|
renderHook(() => useGetExamsData(null, sequenceIds));
|
||||||
|
|
||||||
|
expect(fetchExamAttemptsData).toHaveBeenCalledWith(null, sequenceIds);
|
||||||
|
expect(mockDispatch).toHaveBeenCalledWith(mockFetchExamAttemptsData);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle sequenceIds reference change but same content', () => {
|
||||||
|
const courseId = 'course-v1:edX+DemoX+Demo_Course';
|
||||||
|
const sequenceIds1 = ['block-v1:edX+DemoX+Demo_Course+type@sequential+block@12345'];
|
||||||
|
const sequenceIds2 = ['block-v1:edX+DemoX+Demo_Course+type@sequential+block@12345']; // Same content, different reference
|
||||||
|
|
||||||
|
const { rerender } = renderHook(
|
||||||
|
({ courseId: cId, sequenceIds: sIds }) => useGetExamsData(cId, sIds),
|
||||||
|
{
|
||||||
|
initialProps: { courseId, sequenceIds: sequenceIds1 },
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Verify initial call
|
||||||
|
expect(fetchExamAttemptsData).toHaveBeenCalledTimes(1);
|
||||||
|
expect(mockDispatch).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
// Clear mocks to isolate the re-render call
|
||||||
|
jest.clearAllMocks();
|
||||||
|
|
||||||
|
// Re-render with different reference but same content
|
||||||
|
rerender({ courseId, sequenceIds: sequenceIds2 });
|
||||||
|
|
||||||
|
// Should dispatch again because the reference changed (useEffect dependency)
|
||||||
|
expect(fetchExamAttemptsData).toHaveBeenCalledWith(courseId, sequenceIds2);
|
||||||
|
expect(mockDispatch).toHaveBeenCalledWith(mockFetchExamAttemptsData);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -5,3 +5,15 @@ export const showUngradedAssignments = () => (
|
|||||||
getConfig().SHOW_UNGRADED_ASSIGNMENT_PROGRESS === 'true'
|
getConfig().SHOW_UNGRADED_ASSIGNMENT_PROGRESS === 'true'
|
||||||
|| getConfig().SHOW_UNGRADED_ASSIGNMENT_PROGRESS === true
|
|| getConfig().SHOW_UNGRADED_ASSIGNMENT_PROGRESS === true
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const getLatestDueDateInFuture = (assignmentTypeGradeSummary) => {
|
||||||
|
let latest = null;
|
||||||
|
assignmentTypeGradeSummary.forEach((assignment) => {
|
||||||
|
const assignmentLastGradePublishDate = assignment.lastGradePublishDate;
|
||||||
|
if (assignmentLastGradePublishDate && (!latest || new Date(assignmentLastGradePublishDate) > new Date(latest))
|
||||||
|
&& new Date(assignmentLastGradePublishDate) > new Date()) {
|
||||||
|
latest = assignmentLastGradePublishDate;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return latest;
|
||||||
|
};
|
||||||
|
|||||||
@@ -5,13 +5,13 @@
|
|||||||
.nav a,
|
.nav a,
|
||||||
.nav button {
|
.nav button {
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: $light-400;
|
background-color: var(--pgn-color-light-400);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav a {
|
.nav a {
|
||||||
&:not(.active):hover {
|
&:not(.active):hover {
|
||||||
background-color: $light-400;
|
background-color: var(--pgn-color-light-400);
|
||||||
border-bottom: none;
|
border-bottom: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { breakpoints, useWindowSize } from '@openedx/paragon';
|
|||||||
import { AlertList } from '@src/generic/user-messages';
|
import { AlertList } from '@src/generic/user-messages';
|
||||||
import { useModel } from '@src/generic/model-store';
|
import { useModel } from '@src/generic/model-store';
|
||||||
import { getCoursewareOutlineSidebarSettings } from '../data/selectors';
|
import { getCoursewareOutlineSidebarSettings } from '../data/selectors';
|
||||||
import Chat from './chat/Chat';
|
import { LearnerToolsSlot } from '../../plugin-slots/LearnerToolsSlot';
|
||||||
import SidebarProvider from './sidebar/SidebarContextProvider';
|
import SidebarProvider from './sidebar/SidebarContextProvider';
|
||||||
import NewSidebarProvider from './new-sidebar/SidebarContextProvider';
|
import NewSidebarProvider from './new-sidebar/SidebarContextProvider';
|
||||||
import { NotificationsDiscussionsSidebarTriggerSlot } from '../../plugin-slots/NotificationsDiscussionsSidebarTriggerSlot';
|
import { NotificationsDiscussionsSidebarTriggerSlot } from '../../plugin-slots/NotificationsDiscussionsSidebarTriggerSlot';
|
||||||
@@ -62,7 +62,7 @@ const Course = ({
|
|||||||
const [weeklyGoalCelebrationOpen, setWeeklyGoalCelebrationOpen] = useState(
|
const [weeklyGoalCelebrationOpen, setWeeklyGoalCelebrationOpen] = useState(
|
||||||
celebrations && !celebrations.streakLengthToCelebrate && celebrations.weeklyGoal,
|
celebrations && !celebrations.streakLengthToCelebrate && celebrations.weeklyGoal,
|
||||||
);
|
);
|
||||||
const shouldDisplayChat = windowWidth >= breakpoints.medium.minWidth;
|
const shouldDisplayLearnerTools = windowWidth >= breakpoints.medium.minWidth;
|
||||||
const daysPerWeek = course?.courseGoals?.selectedGoal?.daysPerWeek;
|
const daysPerWeek = course?.courseGoals?.selectedGoal?.daysPerWeek;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -95,17 +95,13 @@ const Course = ({
|
|||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{shouldDisplayChat && (
|
{shouldDisplayLearnerTools && (
|
||||||
<>
|
<LearnerToolsSlot
|
||||||
<Chat
|
enrollmentMode={course.enrollmentMode}
|
||||||
enabled={course.learningAssistantEnabled}
|
isStaff={isStaff}
|
||||||
enrollmentMode={course.enrollmentMode}
|
courseId={courseId}
|
||||||
isStaff={isStaff}
|
unitId={unitId}
|
||||||
courseId={courseId}
|
/>
|
||||||
contentToolsEnabled={course.showCalculator || course.notes.enabled}
|
|
||||||
unitId={unitId}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
<div className="w-100 d-flex align-items-center">
|
<div className="w-100 d-flex align-items-center">
|
||||||
<CourseOutlineMobileSidebarTriggerSlot />
|
<CourseOutlineMobileSidebarTriggerSlot />
|
||||||
|
|||||||
@@ -13,17 +13,25 @@ import Course from './Course';
|
|||||||
import setupDiscussionSidebar from './test-utils';
|
import setupDiscussionSidebar from './test-utils';
|
||||||
|
|
||||||
jest.mock('@edx/frontend-platform/analytics');
|
jest.mock('@edx/frontend-platform/analytics');
|
||||||
jest.mock('@edx/frontend-lib-special-exams/dist/data/thunks.js', () => ({
|
jest.mock('@edx/frontend-lib-special-exams', () => {
|
||||||
...jest.requireActual('@edx/frontend-lib-special-exams/dist/data/thunks.js'),
|
const actual = jest.requireActual('@edx/frontend-lib-special-exams');
|
||||||
checkExamEntry: () => jest.fn(),
|
return {
|
||||||
}));
|
...actual,
|
||||||
const mockChatTestId = 'fake-chat';
|
__esModule: true,
|
||||||
|
// Mock the default export (SequenceExamWrapper) to just render children
|
||||||
|
// eslint-disable-next-line react/prop-types
|
||||||
|
default: ({ children }) => <div data-testid="sequence-exam-wrapper">{children}</div>,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
const mockLearnerToolsTestId = 'fake-learner-tools';
|
||||||
jest.mock(
|
jest.mock(
|
||||||
'./chat/Chat',
|
'../../plugin-slots/LearnerToolsSlot',
|
||||||
// eslint-disable-next-line react/prop-types
|
() => ({
|
||||||
() => function ({ courseId }) {
|
// eslint-disable-next-line react/prop-types
|
||||||
return <div className="fake-chat" data-testid={mockChatTestId}>Chat contents {courseId} </div>;
|
LearnerToolsSlot({ courseId }) {
|
||||||
},
|
return <div className="fake-learner-tools" data-testid={mockLearnerToolsTestId}>LearnerTools contents {courseId} </div>;
|
||||||
|
},
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
const recordFirstSectionCelebration = jest.fn();
|
const recordFirstSectionCelebration = jest.fn();
|
||||||
@@ -360,28 +368,27 @@ describe('Course', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('displays chat when screen is wide enough (browser)', async () => {
|
it('displays learner tools when screen is wide enough (browser)', async () => {
|
||||||
const courseMetadata = Factory.build('courseMetadata', {
|
const courseMetadata = Factory.build('courseMetadata', {
|
||||||
learning_assistant_enabled: true,
|
|
||||||
enrollment: { mode: 'verified' },
|
enrollment: { mode: 'verified' },
|
||||||
});
|
});
|
||||||
const testStore = await initializeTestStore({ courseMetadata }, false);
|
const testStore = await initializeTestStore({ courseMetadata }, false);
|
||||||
const { courseware } = testStore.getState();
|
const { courseware, models } = testStore.getState();
|
||||||
const { courseId, sequenceId } = courseware;
|
const { courseId, sequenceId } = courseware;
|
||||||
const testData = {
|
const testData = {
|
||||||
...mockData,
|
...mockData,
|
||||||
courseId,
|
courseId,
|
||||||
sequenceId,
|
sequenceId,
|
||||||
|
unitId: Object.values(models.units)[0].id,
|
||||||
};
|
};
|
||||||
render(<Course {...testData} />, { store: testStore, wrapWithRouter: true });
|
render(<Course {...testData} />, { store: testStore, wrapWithRouter: true });
|
||||||
const chat = screen.queryByTestId(mockChatTestId);
|
const learnerTools = screen.queryByTestId(mockLearnerToolsTestId);
|
||||||
waitFor(() => expect(chat).toBeInTheDocument());
|
await waitFor(() => expect(learnerTools).toBeInTheDocument());
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does not display chat when screen is too narrow (mobile)', async () => {
|
it('does not display learner tools when screen is too narrow (mobile)', async () => {
|
||||||
global.innerWidth = breakpoints.extraSmall.minWidth;
|
global.innerWidth = breakpoints.extraSmall.minWidth;
|
||||||
const courseMetadata = Factory.build('courseMetadata', {
|
const courseMetadata = Factory.build('courseMetadata', {
|
||||||
learning_assistant_enabled: true,
|
|
||||||
enrollment: { mode: 'verified' },
|
enrollment: { mode: 'verified' },
|
||||||
});
|
});
|
||||||
const testStore = await initializeTestStore({ courseMetadata }, false);
|
const testStore = await initializeTestStore({ courseMetadata }, false);
|
||||||
@@ -393,7 +400,7 @@ describe('Course', () => {
|
|||||||
sequenceId,
|
sequenceId,
|
||||||
};
|
};
|
||||||
render(<Course {...testData} />, { store: testStore, wrapWithRouter: true });
|
render(<Course {...testData} />, { store: testStore, wrapWithRouter: true });
|
||||||
const chat = screen.queryByTestId(mockChatTestId);
|
const learnerTools = screen.queryByTestId(mockLearnerToolsTestId);
|
||||||
await expect(chat).not.toBeInTheDocument();
|
await expect(learnerTools).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -22,7 +22,6 @@
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|
||||||
button {
|
button {
|
||||||
@extend .btn-primary;
|
|
||||||
font-size: 1.2rem;
|
font-size: 1.2rem;
|
||||||
width: 50%;
|
width: 50%;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,82 +0,0 @@
|
|||||||
import { createPortal } from 'react-dom';
|
|
||||||
import { useSelector } from 'react-redux';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
import { Xpert } from '@edx/frontend-lib-learning-assistant';
|
|
||||||
import { getConfig } from '@edx/frontend-platform';
|
|
||||||
|
|
||||||
import { ALLOW_UPSELL_MODES, VERIFIED_MODES } from '@src/constants';
|
|
||||||
import { useModel } from '../../../generic/model-store';
|
|
||||||
|
|
||||||
const Chat = ({
|
|
||||||
enabled,
|
|
||||||
enrollmentMode,
|
|
||||||
isStaff,
|
|
||||||
courseId,
|
|
||||||
contentToolsEnabled,
|
|
||||||
unitId,
|
|
||||||
}) => {
|
|
||||||
const {
|
|
||||||
activeAttempt, exam,
|
|
||||||
} = useSelector(state => state.specialExams);
|
|
||||||
const course = useModel('coursewareMeta', courseId);
|
|
||||||
|
|
||||||
// If is disabled or taking an exam, we don't show the chat.
|
|
||||||
if (!enabled || activeAttempt?.attempt_id || exam?.id) { return null; }
|
|
||||||
|
|
||||||
// If is not staff and doesn't have an enrollment, we don't show the chat.
|
|
||||||
if (!isStaff && !enrollmentMode) { return null; }
|
|
||||||
|
|
||||||
const verifiedMode = VERIFIED_MODES.includes(enrollmentMode); // Enrollment verified
|
|
||||||
const auditMode = (
|
|
||||||
!isStaff
|
|
||||||
&& !verifiedMode
|
|
||||||
&& ALLOW_UPSELL_MODES.includes(enrollmentMode) // Can upgrade course
|
|
||||||
&& getConfig().ENABLE_XPERT_AUDIT
|
|
||||||
);
|
|
||||||
// If user has no access, we don't show the chat.
|
|
||||||
if (!isStaff && !(verifiedMode || auditMode)) { return null; }
|
|
||||||
|
|
||||||
// Date validation
|
|
||||||
const {
|
|
||||||
accessExpiration,
|
|
||||||
start,
|
|
||||||
end,
|
|
||||||
} = course;
|
|
||||||
|
|
||||||
const utcDate = (new Date()).toISOString();
|
|
||||||
const expiration = accessExpiration?.expirationDate || utcDate;
|
|
||||||
const validDate = (
|
|
||||||
(start ? start <= utcDate : true)
|
|
||||||
&& (end ? end >= utcDate : true)
|
|
||||||
&& (auditMode ? expiration >= utcDate : true)
|
|
||||||
);
|
|
||||||
// If date is invalid, we don't show the chat.
|
|
||||||
if (!validDate) { return null; }
|
|
||||||
|
|
||||||
// Use a portal to ensure that component overlay does not compete with learning MFE styles.
|
|
||||||
return createPortal(
|
|
||||||
<Xpert
|
|
||||||
courseId={courseId}
|
|
||||||
contentToolsEnabled={contentToolsEnabled}
|
|
||||||
unitId={unitId}
|
|
||||||
isUpgradeEligible={auditMode}
|
|
||||||
/>,
|
|
||||||
document.body,
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
Chat.propTypes = {
|
|
||||||
isStaff: PropTypes.bool.isRequired,
|
|
||||||
enabled: PropTypes.bool.isRequired,
|
|
||||||
enrollmentMode: PropTypes.string,
|
|
||||||
courseId: PropTypes.string.isRequired,
|
|
||||||
contentToolsEnabled: PropTypes.bool.isRequired,
|
|
||||||
unitId: PropTypes.string.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
Chat.defaultProps = {
|
|
||||||
enrollmentMode: null,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Chat;
|
|
||||||
@@ -1,286 +0,0 @@
|
|||||||
import { BrowserRouter } from 'react-router-dom';
|
|
||||||
import React from 'react';
|
|
||||||
import { Factory } from 'rosie';
|
|
||||||
|
|
||||||
import { getConfig } from '@edx/frontend-platform';
|
|
||||||
|
|
||||||
import {
|
|
||||||
initializeMockApp,
|
|
||||||
initializeTestStore,
|
|
||||||
render,
|
|
||||||
screen,
|
|
||||||
} from '../../../setupTest';
|
|
||||||
|
|
||||||
import Chat from './Chat';
|
|
||||||
|
|
||||||
// We do a partial mock to avoid mocking out other exported values (e.g. the reducer).
|
|
||||||
// We mock out the Xpert component, because the Xpert component has its own rules for whether it renders
|
|
||||||
// or not, and this includes the results of API calls it makes. We don't want to test those rules here, just
|
|
||||||
// whether the Xpert is rendered by the Chat component in certain conditions. Instead of actually rendering
|
|
||||||
// Xpert, we render and assert on a mocked component.
|
|
||||||
const mockXpertTestId = 'xpert';
|
|
||||||
|
|
||||||
jest.mock('@edx/frontend-lib-learning-assistant', () => {
|
|
||||||
const originalModule = jest.requireActual('@edx/frontend-lib-learning-assistant');
|
|
||||||
|
|
||||||
return {
|
|
||||||
__esModule: true,
|
|
||||||
...originalModule,
|
|
||||||
Xpert: () => (<div data-testid={mockXpertTestId}>mocked Xpert</div>),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
jest.mock('@edx/frontend-platform', () => ({
|
|
||||||
getConfig: jest.fn().mockReturnValue({ ENABLE_XPERT_AUDIT: false }),
|
|
||||||
}));
|
|
||||||
|
|
||||||
initializeMockApp();
|
|
||||||
|
|
||||||
const courseId = 'course-v1:edX+DemoX+Demo_Course';
|
|
||||||
let testCases = [];
|
|
||||||
let enabledTestCases = [];
|
|
||||||
let disabledTestCases = [];
|
|
||||||
const enabledModes = [
|
|
||||||
'professional', 'verified', 'no-id-professional', 'credit', 'masters', 'executive-education',
|
|
||||||
'paid-executive-education', 'paid-bootcamp',
|
|
||||||
];
|
|
||||||
const disabledModes = [null, undefined, 'xyz', 'audit', 'honor', 'unpaid-executive-education', 'unpaid-bootcamp'];
|
|
||||||
|
|
||||||
describe('Chat', () => {
|
|
||||||
let store;
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
|
||||||
store = await initializeTestStore({
|
|
||||||
specialExams: {
|
|
||||||
activeAttempt: {
|
|
||||||
attempt_id: null,
|
|
||||||
},
|
|
||||||
exam: {
|
|
||||||
id: null,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Generate test cases.
|
|
||||||
enabledTestCases = enabledModes.map((mode) => ({ enrollmentMode: mode, isVisible: true }));
|
|
||||||
disabledTestCases = disabledModes.map((mode) => ({ enrollmentMode: mode, isVisible: false }));
|
|
||||||
testCases = enabledTestCases.concat(disabledTestCases);
|
|
||||||
|
|
||||||
testCases.forEach(test => {
|
|
||||||
it(
|
|
||||||
`visibility determined by ${test.enrollmentMode} enrollment mode when enabled and not isStaff`,
|
|
||||||
async () => {
|
|
||||||
render(
|
|
||||||
<BrowserRouter>
|
|
||||||
<Chat
|
|
||||||
enrollmentMode={test.enrollmentMode}
|
|
||||||
isStaff={false}
|
|
||||||
enabled
|
|
||||||
courseId={courseId}
|
|
||||||
contentToolsEnabled={false}
|
|
||||||
/>
|
|
||||||
</BrowserRouter>,
|
|
||||||
{ store },
|
|
||||||
);
|
|
||||||
|
|
||||||
const chat = screen.queryByTestId(mockXpertTestId);
|
|
||||||
if (test.isVisible) {
|
|
||||||
expect(chat).toBeInTheDocument();
|
|
||||||
} else {
|
|
||||||
expect(chat).not.toBeInTheDocument();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Generate test cases.
|
|
||||||
testCases = enabledModes.concat(disabledModes).map((mode) => ({ enrollmentMode: mode, isVisible: true }));
|
|
||||||
testCases.forEach(test => {
|
|
||||||
it('visibility determined by isStaff when enabled and any enrollment mode', async () => {
|
|
||||||
render(
|
|
||||||
<BrowserRouter>
|
|
||||||
<Chat
|
|
||||||
enrollmentMode={test.enrollmentMode}
|
|
||||||
isStaff
|
|
||||||
enabled
|
|
||||||
courseId={courseId}
|
|
||||||
contentToolsEnabled={false}
|
|
||||||
/>
|
|
||||||
</BrowserRouter>,
|
|
||||||
{ store },
|
|
||||||
);
|
|
||||||
|
|
||||||
const chat = screen.queryByTestId(mockXpertTestId);
|
|
||||||
if (test.isVisible) {
|
|
||||||
expect(chat).toBeInTheDocument();
|
|
||||||
} else {
|
|
||||||
expect(chat).not.toBeInTheDocument();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Generate the map function used for generating test cases by currying the map function.
|
|
||||||
// In this test suite, visibility depends on whether the enrollment mode is a valid or invalid
|
|
||||||
// enrollment mode for enabling the Chat when the user is not a staff member and the Chat is enabled. Instead of
|
|
||||||
// defining two separate map functions that differ in only one case, curry the function.
|
|
||||||
const generateMapFunction = (areEnabledModes) => (
|
|
||||||
(mode) => (
|
|
||||||
[
|
|
||||||
{
|
|
||||||
enrollmentMode: mode, isStaff: true, enabled: true, isVisible: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
enrollmentMode: mode, isStaff: true, enabled: false, isVisible: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
enrollmentMode: mode, isStaff: false, enabled: true, isVisible: areEnabledModes,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
enrollmentMode: mode, isStaff: false, enabled: false, isVisible: false,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
// Generate test cases.
|
|
||||||
enabledTestCases = enabledModes.map(generateMapFunction(true));
|
|
||||||
disabledTestCases = disabledModes.map(generateMapFunction(false));
|
|
||||||
testCases = enabledTestCases.concat(disabledTestCases);
|
|
||||||
testCases = testCases.flat();
|
|
||||||
testCases.forEach(test => {
|
|
||||||
it(
|
|
||||||
`visibility determined by ${test.enabled} enabled when ${test.isStaff} isStaff
|
|
||||||
and ${test.enrollmentMode} enrollment mode`,
|
|
||||||
async () => {
|
|
||||||
render(
|
|
||||||
<BrowserRouter>
|
|
||||||
<Chat
|
|
||||||
enrollmentMode={test.enrollmentMode}
|
|
||||||
isStaff={test.isStaff}
|
|
||||||
enabled={test.enabled}
|
|
||||||
courseId={courseId}
|
|
||||||
contentToolsEnabled={false}
|
|
||||||
/>
|
|
||||||
</BrowserRouter>,
|
|
||||||
{ store },
|
|
||||||
);
|
|
||||||
|
|
||||||
const chat = screen.queryByTestId(mockXpertTestId);
|
|
||||||
if (test.isVisible) {
|
|
||||||
expect(chat).toBeInTheDocument();
|
|
||||||
} else {
|
|
||||||
expect(chat).not.toBeInTheDocument();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('if course end date has passed, component should not be visible', async () => {
|
|
||||||
store = await initializeTestStore({
|
|
||||||
specialExams: {
|
|
||||||
activeAttempt: {
|
|
||||||
attempt_id: 1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
courseMetadata: Factory.build('courseMetadata', {
|
|
||||||
start: '2014-02-03T05:00:00Z',
|
|
||||||
end: '2014-02-05T05:00:00Z',
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
render(
|
|
||||||
<BrowserRouter>
|
|
||||||
<Chat
|
|
||||||
enrollmentMode="verified"
|
|
||||||
isStaff
|
|
||||||
enabled
|
|
||||||
courseId={courseId}
|
|
||||||
contentToolsEnabled={false}
|
|
||||||
/>
|
|
||||||
</BrowserRouter>,
|
|
||||||
{ store },
|
|
||||||
);
|
|
||||||
|
|
||||||
const chat = screen.queryByTestId(mockXpertTestId);
|
|
||||||
expect(chat).not.toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('if learner has active exam attempt, component should not be visible', async () => {
|
|
||||||
store = await initializeTestStore({
|
|
||||||
specialExams: {
|
|
||||||
activeAttempt: {
|
|
||||||
attempt_id: 1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
render(
|
|
||||||
<BrowserRouter>
|
|
||||||
<Chat
|
|
||||||
enrollmentMode="verified"
|
|
||||||
isStaff
|
|
||||||
enabled
|
|
||||||
courseId={courseId}
|
|
||||||
contentToolsEnabled={false}
|
|
||||||
/>
|
|
||||||
</BrowserRouter>,
|
|
||||||
{ store },
|
|
||||||
);
|
|
||||||
|
|
||||||
const chat = screen.queryByTestId(mockXpertTestId);
|
|
||||||
expect(chat).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('displays component for audit learner if explicitly enabled', async () => {
|
|
||||||
getConfig.mockImplementation(() => ({ ENABLE_XPERT_AUDIT: true }));
|
|
||||||
|
|
||||||
store = await initializeTestStore({
|
|
||||||
courseMetadata: Factory.build('courseMetadata', {
|
|
||||||
access_expiration: { expiration_date: '' },
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
render(
|
|
||||||
<BrowserRouter>
|
|
||||||
<Chat
|
|
||||||
enrollmentMode="audit"
|
|
||||||
isStaff={false}
|
|
||||||
enabled
|
|
||||||
courseId={courseId}
|
|
||||||
contentToolsEnabled={false}
|
|
||||||
/>
|
|
||||||
</BrowserRouter>,
|
|
||||||
{ store },
|
|
||||||
);
|
|
||||||
|
|
||||||
const chat = screen.queryByTestId(mockXpertTestId);
|
|
||||||
expect(chat).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('does not display component for audit learner if access deadline has passed', async () => {
|
|
||||||
getConfig.mockImplementation(() => ({ ENABLE_XPERT_AUDIT: true }));
|
|
||||||
|
|
||||||
store = await initializeTestStore({
|
|
||||||
courseMetadata: Factory.build('courseMetadata', {
|
|
||||||
access_expiration: { expiration_date: '2014-02-03T05:00:00Z' },
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
render(
|
|
||||||
<BrowserRouter>
|
|
||||||
<Chat
|
|
||||||
enrollmentMode="audit"
|
|
||||||
isStaff={false}
|
|
||||||
enabled
|
|
||||||
courseId={courseId}
|
|
||||||
contentToolsEnabled={false}
|
|
||||||
/>
|
|
||||||
</BrowserRouter>,
|
|
||||||
{ store },
|
|
||||||
);
|
|
||||||
|
|
||||||
const chat = screen.queryByTestId(mockXpertTestId);
|
|
||||||
expect(chat).not.toBeInTheDocument();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export { default } from './Chat';
|
|
||||||
@@ -149,7 +149,7 @@ const Calculator = () => {
|
|||||||
/>
|
/>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<table className="table small">
|
<table className="pgn__data-table small">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="col">
|
<th scope="col">
|
||||||
|
|||||||
@@ -4,4 +4,19 @@
|
|||||||
background-color: #f1f1f1;
|
background-color: #f1f1f1;
|
||||||
box-shadow: 0 -1px 0 0 #ddd;
|
box-shadow: 0 -1px 0 0 #ddd;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
tr {
|
||||||
|
border-bottom: var(--pgn-size-border-width) solid var(--pgn-color-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
thead tr {
|
||||||
|
border-bottom: calc(2 * var(--pgn-size-border-width)) solid var(--pgn-color-border);
|
||||||
|
border-top: var(--pgn-size-border-width) solid var(--pgn-color-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
tbody tr {
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,8 +8,8 @@
|
|||||||
display: inline-block;
|
display: inline-block;
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
background-color: #f1f1f1;
|
background-color: #f1f1f1 !important;
|
||||||
border: solid 1px #ddd;
|
border: solid 1px #ddd !important;
|
||||||
border-bottom: none;
|
border-bottom: none;
|
||||||
border-top-left-radius: .3rem;
|
border-top-left-radius: .3rem;
|
||||||
border-top-right-radius: .3rem;
|
border-top-right-radius: .3rem;
|
||||||
|
|||||||
@@ -18,8 +18,8 @@ import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
|
|||||||
|
|
||||||
import CelebrationMobile from './assets/celebration_456x328.gif';
|
import CelebrationMobile from './assets/celebration_456x328.gif';
|
||||||
import CelebrationDesktop from './assets/celebration_750x540.gif';
|
import CelebrationDesktop from './assets/celebration_750x540.gif';
|
||||||
import certificate from '../../../generic/assets/openedx_certificate.png';
|
import certificate from '../../../generic/assets/edX_certificate.png';
|
||||||
import certificateLocked from '../../../generic/assets/openedx_locked_certificate.png';
|
import certificateLocked from '../../../generic/assets/edX_locked_certificate.png';
|
||||||
import { FormattedPricing } from '../../../generic/upgrade-button';
|
import { FormattedPricing } from '../../../generic/upgrade-button';
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
import { useModel } from '../../../generic/model-store';
|
import { useModel } from '../../../generic/model-store';
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import PropTypes from 'prop-types';
|
|||||||
import { getConfig } from '@edx/frontend-platform';
|
import { getConfig } from '@edx/frontend-platform';
|
||||||
import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
|
import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
|
||||||
import { Alert, Button, Hyperlink } from '@openedx/paragon';
|
import { Alert, Button, Hyperlink } from '@openedx/paragon';
|
||||||
import certImage from '../../../generic/assets/openedx_certificate.png';
|
import certImage from '../../../generic/assets/edX_certificate.png';
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
import { ErrorPage } from '@edx/frontend-platform/react';
|
import { ModalDialog } from '@openedx/paragon';
|
||||||
import { StrictDict } from '@edx/react-unit-test-utils';
|
|
||||||
import { ModalDialog, Modal } from '@openedx/paragon';
|
|
||||||
import { ContentIFrameLoaderSlot } from '../../../../plugin-slots/ContentIFrameLoaderSlot';
|
import { ContentIFrameLoaderSlot } from '../../../../plugin-slots/ContentIFrameLoaderSlot';
|
||||||
|
import { ContentIFrameErrorSlot } from '../../../../plugin-slots/ContentIFrameErrorSlot';
|
||||||
|
|
||||||
import * as hooks from './hooks';
|
import * as hooks from './hooks';
|
||||||
|
|
||||||
@@ -22,10 +20,10 @@ export const IFRAME_FEATURE_POLICY = (
|
|||||||
'microphone *; camera *; midi *; geolocation *; encrypted-media *; clipboard-write *; autoplay *'
|
'microphone *; camera *; midi *; geolocation *; encrypted-media *; clipboard-write *; autoplay *'
|
||||||
);
|
);
|
||||||
|
|
||||||
export const testIDs = StrictDict({
|
export const testIDs = {
|
||||||
contentIFrame: 'content-iframe-test-id',
|
contentIFrame: 'content-iframe-test-id',
|
||||||
modalIFrame: 'modal-iframe-test-id',
|
modalIFrame: 'modal-iframe-test-id',
|
||||||
});
|
};
|
||||||
|
|
||||||
const ContentIFrame = ({
|
const ContentIFrame = ({
|
||||||
iframeUrl,
|
iframeUrl,
|
||||||
@@ -65,54 +63,44 @@ const ContentIFrame = ({
|
|||||||
onLoad: handleIFrameLoad,
|
onLoad: handleIFrameLoad,
|
||||||
};
|
};
|
||||||
|
|
||||||
let modalContent;
|
|
||||||
if (modalOptions.isOpen) {
|
|
||||||
modalContent = modalOptions.body
|
|
||||||
? <div className="unit-modal">{ modalOptions.body }</div>
|
|
||||||
: (
|
|
||||||
<iframe
|
|
||||||
title={modalOptions.title}
|
|
||||||
allow={IFRAME_FEATURE_POLICY}
|
|
||||||
frameBorder="0"
|
|
||||||
src={modalOptions.url}
|
|
||||||
style={{ width: '100%', height: modalOptions.height }}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{(shouldShowContent && !hasLoaded) && (
|
{(shouldShowContent && !hasLoaded) && (
|
||||||
showError ? <ErrorPage /> : <ContentIFrameLoaderSlot courseId={courseId} loadingMessage={loadingMessage} />
|
showError ? (
|
||||||
|
<ContentIFrameErrorSlot courseId={courseId} />
|
||||||
|
) : (
|
||||||
|
<ContentIFrameLoaderSlot courseId={courseId} loadingMessage={loadingMessage} />
|
||||||
|
)
|
||||||
)}
|
)}
|
||||||
{shouldShowContent && (
|
{shouldShowContent && (
|
||||||
<div className="unit-iframe-wrapper">
|
<div className="unit-iframe-wrapper">
|
||||||
<iframe title={title} {...contentIFrameProps} data-testid={testIDs.contentIFrame} />
|
<iframe title={title} {...contentIFrameProps} data-testid={testIDs.contentIFrame} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{modalOptions.isOpen && (modalOptions.isFullscreen
|
{modalOptions.isOpen
|
||||||
? (
|
&& (
|
||||||
<ModalDialog
|
<ModalDialog
|
||||||
dialogClassName="modal-lti"
|
className="modal-lti"
|
||||||
onClose={handleModalClose}
|
onClose={handleModalClose}
|
||||||
size="fullscreen"
|
size={modalOptions.isFullscreen ? 'fullscreen' : 'md'}
|
||||||
isOpen
|
isOpen
|
||||||
hasCloseButton={false}
|
hasCloseButton={false}
|
||||||
>
|
>
|
||||||
<ModalDialog.Body className={modalOptions.modalBodyClassName}>
|
<ModalDialog.Body className={modalOptions.modalBodyClassName}>
|
||||||
{modalContent}
|
{modalOptions.body
|
||||||
|
? <div className="unit-modal">{ modalOptions.body }</div>
|
||||||
|
: (
|
||||||
|
<iframe
|
||||||
|
title={modalOptions.title}
|
||||||
|
allow={IFRAME_FEATURE_POLICY}
|
||||||
|
frameBorder="0"
|
||||||
|
src={modalOptions.url}
|
||||||
|
style={{ width: '100%', height: modalOptions.height }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</ModalDialog.Body>
|
</ModalDialog.Body>
|
||||||
</ModalDialog>
|
</ModalDialog>
|
||||||
|
)}
|
||||||
) : (
|
|
||||||
<Modal
|
|
||||||
body={modalContent}
|
|
||||||
dialogClassName="modal-lti"
|
|
||||||
onClose={handleModalClose}
|
|
||||||
open
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,26 +1,11 @@
|
|||||||
import React from 'react';
|
import { render, screen } from '@testing-library/react';
|
||||||
|
|
||||||
import { ErrorPage } from '@edx/frontend-platform/react';
|
|
||||||
import { ModalDialog, Modal } from '@openedx/paragon';
|
|
||||||
import { shallow } from '@edx/react-unit-test-utils';
|
|
||||||
|
|
||||||
import PageLoading from '@src/generic/PageLoading';
|
|
||||||
|
|
||||||
import { ContentIFrameLoaderSlot } from '@src/plugin-slots/ContentIFrameLoaderSlot';
|
|
||||||
import * as hooks from './hooks';
|
import * as hooks from './hooks';
|
||||||
import ContentIFrame, { IFRAME_FEATURE_POLICY, testIDs } from './ContentIFrame';
|
import ContentIFrame, { IFRAME_FEATURE_POLICY } from './ContentIFrame';
|
||||||
|
|
||||||
jest.mock('@edx/frontend-platform/react', () => ({ ErrorPage: 'ErrorPage' }));
|
jest.mock('@edx/frontend-platform/react', () => ({ ErrorPage: () => <div>ErrorPage</div> }));
|
||||||
|
|
||||||
jest.mock('@openedx/paragon', () => jest.requireActual('@edx/react-unit-test-utils')
|
jest.mock('@src/generic/PageLoading', () => jest.fn(() => <div>PageLoading</div>));
|
||||||
.mockComponents({
|
|
||||||
Modal: 'Modal',
|
|
||||||
ModalDialog: {
|
|
||||||
Body: 'ModalDialog.Body',
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
jest.mock('@src/generic/PageLoading', () => 'PageLoading');
|
|
||||||
|
|
||||||
jest.mock('./hooks', () => ({
|
jest.mock('./hooks', () => ({
|
||||||
useIFrameBehavior: jest.fn(),
|
useIFrameBehavior: jest.fn(),
|
||||||
@@ -68,14 +53,13 @@ const props = {
|
|||||||
title: 'test-title',
|
title: 'test-title',
|
||||||
};
|
};
|
||||||
|
|
||||||
let el;
|
|
||||||
describe('ContentIFrame Component', () => {
|
describe('ContentIFrame Component', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
});
|
});
|
||||||
describe('behavior', () => {
|
describe('behavior', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
el = shallow(<ContentIFrame {...props} />);
|
render(<ContentIFrame {...props} />);
|
||||||
});
|
});
|
||||||
it('initializes iframe behavior hook', () => {
|
it('initializes iframe behavior hook', () => {
|
||||||
expect(hooks.useIFrameBehavior).toHaveBeenCalledWith({
|
expect(hooks.useIFrameBehavior).toHaveBeenCalledWith({
|
||||||
@@ -90,61 +74,61 @@ describe('ContentIFrame Component', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('output', () => {
|
describe('output', () => {
|
||||||
let component;
|
|
||||||
describe('if shouldShowContent', () => {
|
describe('if shouldShowContent', () => {
|
||||||
describe('if not hasLoaded', () => {
|
describe('if not hasLoaded', () => {
|
||||||
it('displays errorPage if showError', () => {
|
it('displays errorPage if showError', () => {
|
||||||
hooks.useIFrameBehavior.mockReturnValueOnce({ ...iframeBehavior, showError: true });
|
hooks.useIFrameBehavior.mockReturnValueOnce({ ...iframeBehavior, showError: true });
|
||||||
el = shallow(<ContentIFrame {...props} />);
|
render(<ContentIFrame {...props} />);
|
||||||
expect(el.instance.findByType(ErrorPage).length).toEqual(1);
|
const errorPage = screen.getByText('ErrorPage');
|
||||||
|
expect(errorPage).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
it('displays PageLoading component if not showError', () => {
|
it('displays PageLoading component if not showError', () => {
|
||||||
el = shallow(<ContentIFrame {...props} />);
|
render(<ContentIFrame {...props} />);
|
||||||
[component] = el.instance.findByType(ContentIFrameLoaderSlot);
|
const pageLoading = screen.getByText('PageLoading');
|
||||||
expect(component.props.loadingMessage).toEqual(props.loadingMessage);
|
expect(pageLoading).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('hasLoaded', () => {
|
describe('hasLoaded', () => {
|
||||||
it('does not display PageLoading or ErrorPage', () => {
|
it('does not display PageLoading or ErrorPage', () => {
|
||||||
hooks.useIFrameBehavior.mockReturnValueOnce({ ...iframeBehavior, hasLoaded: true });
|
hooks.useIFrameBehavior.mockReturnValueOnce({ ...iframeBehavior, hasLoaded: true });
|
||||||
el = shallow(<ContentIFrame {...props} />);
|
render(<ContentIFrame {...props} />);
|
||||||
expect(el.instance.findByType(PageLoading).length).toEqual(0);
|
const pageLoading = screen.queryByText('PageLoading');
|
||||||
expect(el.instance.findByType(ErrorPage).length).toEqual(0);
|
expect(pageLoading).toBeNull();
|
||||||
|
const errorPage = screen.queryByText('ErrorPage');
|
||||||
|
expect(errorPage).toBeNull();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
it('display iframe with props from hooks', () => {
|
it('display iframe with props from hooks', () => {
|
||||||
el = shallow(<ContentIFrame {...props} />);
|
render(<ContentIFrame {...props} />);
|
||||||
[component] = el.instance.findByTestId(testIDs.contentIFrame);
|
const iframe = screen.getByTitle(props.title);
|
||||||
expect(component.props).toEqual({
|
expect(iframe).toBeInTheDocument();
|
||||||
allow: IFRAME_FEATURE_POLICY,
|
expect(iframe).toHaveAttribute('id', props.elementId);
|
||||||
allowFullScreen: true,
|
expect(iframe).toHaveAttribute('src', props.iframeUrl);
|
||||||
scrolling: 'no',
|
expect(iframe).toHaveAttribute('allow', IFRAME_FEATURE_POLICY);
|
||||||
referrerPolicy: 'origin',
|
expect(iframe).toHaveAttribute('allowfullscreen', '');
|
||||||
title: props.title,
|
expect(iframe).toHaveAttribute('scrolling', 'no');
|
||||||
id: props.elementId,
|
expect(iframe).toHaveAttribute('referrerpolicy', 'origin');
|
||||||
src: props.iframeUrl,
|
|
||||||
height: iframeBehavior.iframeHeight,
|
|
||||||
onLoad: iframeBehavior.handleIFrameLoad,
|
|
||||||
'data-testid': testIDs.contentIFrame,
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('if not shouldShowContent', () => {
|
describe('if not shouldShowContent', () => {
|
||||||
it('does not show PageLoading, ErrorPage, or unit-iframe-wrapper', () => {
|
it('does not show PageLoading, ErrorPage, or unit-iframe-wrapper', () => {
|
||||||
el = shallow(<ContentIFrame {...{ ...props, shouldShowContent: false }} />);
|
render(<ContentIFrame {...{ ...props, shouldShowContent: false }} />);
|
||||||
expect(el.instance.findByType(PageLoading).length).toEqual(0);
|
expect(screen.queryByText('PageLoading')).toBeNull();
|
||||||
expect(el.instance.findByType(ErrorPage).length).toEqual(0);
|
expect(screen.queryByText('ErrorPage')).toBeNull();
|
||||||
expect(el.instance.findByTestId(testIDs.contentIFrame).length).toEqual(0);
|
expect(screen.queryByTitle(props.title)).toBeNull();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
it('does not display modal if modalOptions returns isOpen: false', () => {
|
it('does not display modal if modalOptions returns isOpen: false', () => {
|
||||||
el = shallow(<ContentIFrame {...props} />);
|
render(<ContentIFrame {...props} />);
|
||||||
expect(el.instance.findByType(Modal).length).toEqual(0);
|
const modal = screen.queryByRole('dialog');
|
||||||
|
expect(modal).toBeNull();
|
||||||
});
|
});
|
||||||
describe('if modalOptions.isOpen', () => {
|
describe('if modalOptions.isOpen', () => {
|
||||||
const testModalOpenAndHandleClose = () => {
|
const testModalOpenAndHandleClose = () => {
|
||||||
test('Modal component isOpen, with handleModalClose from hook', () => {
|
it('closes modal on close button click', () => {
|
||||||
expect(component.props.onClose).toEqual(modalIFrameData.handleModalClose);
|
const closeButton = screen.getByTestId('modal-backdrop');
|
||||||
|
closeButton.click();
|
||||||
|
expect(modalIFrameData.handleModalClose).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
describe('fullscreen modal', () => {
|
describe('fullscreen modal', () => {
|
||||||
@@ -154,14 +138,13 @@ describe('ContentIFrame Component', () => {
|
|||||||
...modalIFrameData,
|
...modalIFrameData,
|
||||||
modalOptions: { ...modalOptions.withBody, isFullscreen: true },
|
modalOptions: { ...modalOptions.withBody, isFullscreen: true },
|
||||||
});
|
});
|
||||||
el = shallow(<ContentIFrame {...props} />);
|
render(<ContentIFrame {...props} />);
|
||||||
[component] = el.instance.findByType(ModalDialog);
|
|
||||||
});
|
});
|
||||||
it('displays Modal with div wrapping provided body content if modal.body is provided', () => {
|
it('displays Modal with div wrapping provided body content if modal.body is provided', () => {
|
||||||
const content = component.findByType(ModalDialog.Body)[0].children[0];
|
const dialog = screen.getByRole('dialog');
|
||||||
expect(content.matches(shallow(
|
expect(dialog).toBeInTheDocument();
|
||||||
<div className="unit-modal">{modalOptions.withBody.body}</div>,
|
const modalBody = screen.getByText(modalOptions.withBody.body);
|
||||||
))).toEqual(true);
|
expect(modalBody).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
testModalOpenAndHandleClose();
|
testModalOpenAndHandleClose();
|
||||||
});
|
});
|
||||||
@@ -172,53 +155,42 @@ describe('ContentIFrame Component', () => {
|
|||||||
...modalIFrameData,
|
...modalIFrameData,
|
||||||
modalOptions: { ...modalOptions.withUrl, isFullscreen: true },
|
modalOptions: { ...modalOptions.withUrl, isFullscreen: true },
|
||||||
});
|
});
|
||||||
el = shallow(<ContentIFrame {...props} />);
|
render(<ContentIFrame {...props} />);
|
||||||
[component] = el.instance.findByType(ModalDialog);
|
});
|
||||||
|
it('displays Modal with iframe to provided url if modal.body is not provided', () => {
|
||||||
|
const iframe = screen.getByTitle(modalOptions.withUrl.title);
|
||||||
|
expect(iframe).toBeInTheDocument();
|
||||||
|
expect(iframe).toHaveAttribute('allow', IFRAME_FEATURE_POLICY);
|
||||||
|
expect(iframe).toHaveAttribute('src', modalOptions.withUrl.url);
|
||||||
});
|
});
|
||||||
testModalOpenAndHandleClose();
|
testModalOpenAndHandleClose();
|
||||||
it('displays Modal with iframe to provided url if modal.body is not provided', () => {
|
|
||||||
const content = component.findByType(ModalDialog.Body)[0].children[0];
|
|
||||||
expect(content.matches(shallow(
|
|
||||||
<iframe
|
|
||||||
title={modalOptions.withUrl.title}
|
|
||||||
allow={IFRAME_FEATURE_POLICY}
|
|
||||||
frameBorder="0"
|
|
||||||
src={modalOptions.withUrl.url}
|
|
||||||
style={{ width: '100%', height: modalOptions.withUrl.height }}
|
|
||||||
/>,
|
|
||||||
))).toEqual(true);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('body modal', () => {
|
describe('body modal', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
hooks.useModalIFrameData.mockReturnValueOnce({ ...modalIFrameData, modalOptions: modalOptions.withBody });
|
hooks.useModalIFrameData.mockReturnValueOnce({ ...modalIFrameData, modalOptions: modalOptions.withBody });
|
||||||
el = shallow(<ContentIFrame {...props} />);
|
render(<ContentIFrame {...props} />);
|
||||||
[component] = el.instance.findByType(Modal);
|
|
||||||
});
|
});
|
||||||
it('displays Modal with div wrapping provided body content if modal.body is provided', () => {
|
it('displays Modal with div wrapping provided body content if modal.body is provided', () => {
|
||||||
expect(component.props.body).toEqual(<div className="unit-modal">{modalOptions.withBody.body}</div>);
|
const dialog = screen.getByRole('dialog');
|
||||||
|
expect(dialog).toBeInTheDocument();
|
||||||
|
const modalBody = screen.getByText(modalOptions.withBody.body);
|
||||||
|
expect(modalBody).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
testModalOpenAndHandleClose();
|
testModalOpenAndHandleClose();
|
||||||
});
|
});
|
||||||
describe('url modal', () => {
|
describe('url modal', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
hooks.useModalIFrameData.mockReturnValueOnce({ ...modalIFrameData, modalOptions: modalOptions.withUrl });
|
hooks.useModalIFrameData.mockReturnValueOnce({ ...modalIFrameData, modalOptions: modalOptions.withUrl });
|
||||||
el = shallow(<ContentIFrame {...props} />);
|
render(<ContentIFrame {...props} />);
|
||||||
[component] = el.instance.findByType(Modal);
|
});
|
||||||
|
it('displays Modal with iframe to provided url if modal.body is not provided', () => {
|
||||||
|
const iframe = screen.getByTitle(modalOptions.withUrl.title);
|
||||||
|
expect(iframe).toBeInTheDocument();
|
||||||
|
expect(iframe).toHaveAttribute('allow', IFRAME_FEATURE_POLICY);
|
||||||
|
expect(iframe).toHaveAttribute('src', modalOptions.withUrl.url);
|
||||||
});
|
});
|
||||||
testModalOpenAndHandleClose();
|
testModalOpenAndHandleClose();
|
||||||
it('displays Modal with iframe to provided url if modal.body is not provided', () => {
|
|
||||||
expect(component.props.body).toEqual(
|
|
||||||
<iframe
|
|
||||||
title={modalOptions.withUrl.title}
|
|
||||||
allow={IFRAME_FEATURE_POLICY}
|
|
||||||
frameBorder="0"
|
|
||||||
src={modalOptions.withUrl.url}
|
|
||||||
style={{ width: '100%', height: modalOptions.withUrl.height }}
|
|
||||||
/>,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,22 +1,15 @@
|
|||||||
import React from 'react';
|
import { render, screen } from '@testing-library/react';
|
||||||
|
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||||
import { formatMessage, shallow } from '@edx/react-unit-test-utils';
|
|
||||||
|
|
||||||
import { useModel } from '@src/generic/model-store';
|
import { useModel } from '@src/generic/model-store';
|
||||||
import PageLoading from '@src/generic/PageLoading';
|
|
||||||
|
|
||||||
import { GatedUnitContentMessageSlot } from '@src/plugin-slots/GatedUnitContentMessageSlot';
|
|
||||||
import messages from '../messages';
|
|
||||||
import HonorCode from '../honor-code';
|
|
||||||
import LockPaywall from '../lock-paywall';
|
|
||||||
import hooks from './hooks';
|
import hooks from './hooks';
|
||||||
import { modelKeys } from './constants';
|
import { modelKeys } from './constants';
|
||||||
|
|
||||||
import UnitSuspense from './UnitSuspense';
|
import UnitSuspense from './UnitSuspense';
|
||||||
|
|
||||||
jest.mock('@edx/frontend-platform/i18n', () => ({
|
jest.mock('@edx/frontend-platform/i18n', () => ({
|
||||||
|
...jest.requireActual('@edx/frontend-platform/i18n'),
|
||||||
defineMessages: m => m,
|
defineMessages: m => m,
|
||||||
useIntl: () => ({ formatMessage: jest.requireActual('@edx/react-unit-test-utils').formatMessage }),
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
jest.mock('react', () => ({
|
jest.mock('react', () => ({
|
||||||
@@ -24,10 +17,9 @@ jest.mock('react', () => ({
|
|||||||
Suspense: 'Suspense',
|
Suspense: 'Suspense',
|
||||||
}));
|
}));
|
||||||
|
|
||||||
jest.mock('../honor-code', () => 'HonorCode');
|
jest.mock('../honor-code', () => jest.fn(() => <div>HonorCode</div>));
|
||||||
jest.mock('../lock-paywall', () => 'LockPaywall');
|
jest.mock('../lock-paywall', () => jest.fn(() => <div>LockPaywall</div>));
|
||||||
jest.mock('@src/generic/model-store', () => ({ useModel: jest.fn() }));
|
jest.mock('@src/generic/model-store', () => ({ useModel: jest.fn() }));
|
||||||
jest.mock('@src/generic/PageLoading', () => 'PageLoading');
|
|
||||||
|
|
||||||
jest.mock('./hooks', () => ({
|
jest.mock('./hooks', () => ({
|
||||||
useShouldDisplayHonorCode: jest.fn(() => false),
|
useShouldDisplayHonorCode: jest.fn(() => false),
|
||||||
@@ -46,7 +38,6 @@ const props = {
|
|||||||
id: 'test-id',
|
id: 'test-id',
|
||||||
};
|
};
|
||||||
|
|
||||||
let el;
|
|
||||||
describe('UnitSuspense component', () => {
|
describe('UnitSuspense component', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
@@ -54,7 +45,7 @@ describe('UnitSuspense component', () => {
|
|||||||
});
|
});
|
||||||
describe('behavior', () => {
|
describe('behavior', () => {
|
||||||
it('initializes models', () => {
|
it('initializes models', () => {
|
||||||
el = shallow(<UnitSuspense {...props} />);
|
render(<IntlProvider locale="en"><UnitSuspense {...props} /></IntlProvider>);
|
||||||
const { calls } = useModel.mock;
|
const { calls } = useModel.mock;
|
||||||
const [unitCall] = calls.filter(call => call[0] === modelKeys.units);
|
const [unitCall] = calls.filter(call => call[0] === modelKeys.units);
|
||||||
const [metaCall] = calls.filter(call => call[0] === modelKeys.coursewareMeta);
|
const [metaCall] = calls.filter(call => call[0] === modelKeys.coursewareMeta);
|
||||||
@@ -66,8 +57,9 @@ describe('UnitSuspense component', () => {
|
|||||||
describe('LockPaywall', () => {
|
describe('LockPaywall', () => {
|
||||||
const testNoPaywall = () => {
|
const testNoPaywall = () => {
|
||||||
it('does not display LockPaywall', () => {
|
it('does not display LockPaywall', () => {
|
||||||
el = shallow(<UnitSuspense {...props} />);
|
render(<IntlProvider locale="en"><UnitSuspense {...props} /></IntlProvider>);
|
||||||
expect(el.instance.findByType(LockPaywall).length).toEqual(0);
|
const lockPaywall = screen.queryByText('LockPaywall');
|
||||||
|
expect(lockPaywall).toBeNull();
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
describe('gating not enabled', () => { testNoPaywall(); });
|
describe('gating not enabled', () => { testNoPaywall(); });
|
||||||
@@ -78,29 +70,29 @@ describe('UnitSuspense component', () => {
|
|||||||
describe('gating enabled, gated content included', () => {
|
describe('gating enabled, gated content included', () => {
|
||||||
beforeEach(() => { mockModels(true, true); });
|
beforeEach(() => { mockModels(true, true); });
|
||||||
it('displays LockPaywall in Suspense wrapper with PageLoading fallback', () => {
|
it('displays LockPaywall in Suspense wrapper with PageLoading fallback', () => {
|
||||||
el = shallow(<UnitSuspense {...props} />);
|
hooks.useShouldDisplayHonorCode.mockReturnValueOnce(false);
|
||||||
const [component] = el.instance.findByType(GatedUnitContentMessageSlot);
|
render(<IntlProvider locale="en"><UnitSuspense {...props} /></IntlProvider>);
|
||||||
expect(component.parent.type).toEqual('Suspense');
|
const lockPaywall = screen.getByText('LockPaywall');
|
||||||
expect(component.parent.props.fallback)
|
expect(lockPaywall).toBeInTheDocument();
|
||||||
.toEqual(<PageLoading srMessage={formatMessage(messages.loadingLockedContent)} />);
|
const suspenseWrapper = lockPaywall.closest('suspense');
|
||||||
expect(component.props.courseId).toEqual(props.courseId);
|
expect(suspenseWrapper).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('HonorCode', () => {
|
describe('HonorCode', () => {
|
||||||
it('does not display HonorCode if useShouldDisplayHonorCode => false', () => {
|
it('does not display HonorCode if useShouldDisplayHonorCode => false', () => {
|
||||||
hooks.useShouldDisplayHonorCode.mockReturnValueOnce(false);
|
hooks.useShouldDisplayHonorCode.mockReturnValueOnce(false);
|
||||||
el = shallow(<UnitSuspense {...props} />);
|
render(<IntlProvider locale="en"><UnitSuspense {...props} /></IntlProvider>);
|
||||||
expect(el.instance.findByType(HonorCode).length).toEqual(0);
|
const honorCode = screen.queryByText('HonorCode');
|
||||||
|
expect(honorCode).toBeNull();
|
||||||
});
|
});
|
||||||
it('displays HonorCode component in Suspense wrapper with PageLoading fallback if shouldDisplayHonorCode', () => {
|
it('displays HonorCode component in Suspense wrapper with PageLoading fallback if shouldDisplayHonorCode', () => {
|
||||||
hooks.useShouldDisplayHonorCode.mockReturnValueOnce(true);
|
hooks.useShouldDisplayHonorCode.mockReturnValueOnce(true);
|
||||||
el = shallow(<UnitSuspense {...props} />);
|
render(<IntlProvider locale="en"><UnitSuspense {...props} /></IntlProvider>);
|
||||||
const [component] = el.instance.findByType(HonorCode);
|
const honorCode = screen.getByText('HonorCode');
|
||||||
expect(component.parent.type).toEqual('Suspense');
|
expect(honorCode).toBeInTheDocument();
|
||||||
expect(component.parent.props.fallback)
|
const suspenseWrapper = honorCode.closest('suspense');
|
||||||
.toEqual(<PageLoading srMessage={formatMessage(messages.loadingHonorCode)} />);
|
expect(suspenseWrapper).toBeInTheDocument();
|
||||||
expect(component.props.courseId).toEqual(props.courseId);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,26 +1,25 @@
|
|||||||
import { StrictDict } from '@edx/react-unit-test-utils/dist';
|
export const modelKeys = {
|
||||||
|
|
||||||
export const modelKeys = StrictDict({
|
|
||||||
units: 'units',
|
units: 'units',
|
||||||
coursewareMeta: 'coursewareMeta',
|
coursewareMeta: 'coursewareMeta',
|
||||||
});
|
} as const;
|
||||||
|
|
||||||
export const views = StrictDict({
|
export const views = {
|
||||||
student: 'student_view',
|
student: 'student_view',
|
||||||
public: 'public_view',
|
public: 'public_view',
|
||||||
});
|
} as const;
|
||||||
|
|
||||||
export const loadingState = 'loading';
|
export const loadingState = 'loading';
|
||||||
|
|
||||||
export const messageTypes = StrictDict({
|
export const messageTypes = {
|
||||||
modal: 'plugin.modal',
|
modal: 'plugin.modal',
|
||||||
resize: 'plugin.resize',
|
resize: 'plugin.resize',
|
||||||
videoFullScreen: 'plugin.videoFullScreen',
|
videoFullScreen: 'plugin.videoFullScreen',
|
||||||
});
|
autoAdvance: 'plugin.autoAdvance',
|
||||||
|
} as const;
|
||||||
|
|
||||||
export default StrictDict({
|
export default {
|
||||||
modelKeys,
|
modelKeys,
|
||||||
views,
|
views,
|
||||||
loadingState,
|
loadingState,
|
||||||
messageTypes,
|
messageTypes,
|
||||||
});
|
};
|
||||||
@@ -1,19 +1,13 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { logError } from '@edx/frontend-platform/logging';
|
import { logError } from '@edx/frontend-platform/logging';
|
||||||
import { StrictDict, useKeyedState } from '@edx/react-unit-test-utils';
|
|
||||||
import { useExamAccessToken, useFetchExamAccessToken, useIsExam } from '@edx/frontend-lib-special-exams';
|
import { useExamAccessToken, useFetchExamAccessToken, useIsExam } from '@edx/frontend-lib-special-exams';
|
||||||
|
|
||||||
export const stateKeys = StrictDict({
|
|
||||||
accessToken: 'accessToken',
|
|
||||||
blockAccess: 'blockAccess',
|
|
||||||
});
|
|
||||||
|
|
||||||
const useExamAccess = ({
|
const useExamAccess = ({
|
||||||
id,
|
id,
|
||||||
}) => {
|
}) => {
|
||||||
const isExam = useIsExam();
|
const isExam = useIsExam();
|
||||||
const [blockAccess, setBlockAccess] = useKeyedState(stateKeys.blockAccess, isExam);
|
const [blockAccess, setBlockAccess] = React.useState(isExam);
|
||||||
|
|
||||||
const fetchExamAccessToken = useFetchExamAccessToken();
|
const fetchExamAccessToken = useFetchExamAccessToken();
|
||||||
|
|
||||||
|
|||||||
@@ -84,6 +84,19 @@ describe('<Unit />', () => {
|
|||||||
|
|
||||||
expect(nextButton).toBeVisible();
|
expect(nextButton).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Test for accessibility compliance: unit title must be an h1 (heading level 1) as the page's primary heading
|
||||||
|
// for screen reader and accessibility compliance.
|
||||||
|
// See: https://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/latest/course_components/create_html_component.html#the-visual-editor
|
||||||
|
// JIRA: https://2u-internal.atlassian.net/browse/AU-2135
|
||||||
|
it('renders unit title as h1 heading for accessibility', () => {
|
||||||
|
renderComponent(defaultProps);
|
||||||
|
|
||||||
|
const unitTitle = screen.getByRole('heading', { level: 1 });
|
||||||
|
|
||||||
|
expect(unitTitle).toBeInTheDocument();
|
||||||
|
expect(unitTitle.tagName).toBe('H1');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('UnitSuspense', () => {
|
describe('UnitSuspense', () => {
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
import { Locked } from '@openedx/paragon/icons';
|
import { Locked } from '@openedx/paragon/icons';
|
||||||
import SidebarContext from '../../sidebar/SidebarContext';
|
import SidebarContext from '../../sidebar/SidebarContext';
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
import certificateLocked from '../../../../generic/assets/openedx_locked_certificate.png';
|
import certificateLocked from '../../../../generic/assets/edX_locked_certificate.png';
|
||||||
import { useModel } from '../../../../generic/model-store';
|
import { useModel } from '../../../../generic/model-store';
|
||||||
import { UpgradeButton } from '../../../../generic/upgrade-button';
|
import { UpgradeButton } from '../../../../generic/upgrade-button';
|
||||||
import {
|
import {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.lock-paywall-container svg {
|
.lock-paywall-container svg {
|
||||||
color: $primary-700;
|
color: var(--pgn-color-primary-700);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (min-width: 992px) and (max-width: 1100px) {
|
@media only screen and (min-width: 992px) and (max-width: 1100px) {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#course-sidebar {
|
#course-sidebar {
|
||||||
@media (max-width: map-get($grid-breakpoints, "lg")) {
|
@media (--pgn-size-breakpoint-max-width-lg) {
|
||||||
overflow-y: scroll;
|
overflow-y: scroll;
|
||||||
padding: 0 .625rem !important;
|
padding: 0 .625rem !important;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.outline-sidebar {
|
.outline-sidebar {
|
||||||
@media (min-width: map-get($grid-breakpoints, "xl")) {
|
@media (--pgn-size-breakpoint-min-width-xl) {
|
||||||
left: 0;
|
left: 0;
|
||||||
top: 0;
|
top: 0;
|
||||||
}
|
}
|
||||||
@@ -23,12 +23,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.outline-sidebar-heading {
|
.outline-sidebar-heading {
|
||||||
font-weight: $font-weight-bold;
|
font-weight: var(--pgn-typography-font-weight-bold);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.course-sidebar-section {
|
.course-sidebar-section {
|
||||||
background: $white;
|
background: var(--pgn-color-white);
|
||||||
border: 1px solid #d7d3d1;
|
border: 1px solid #d7d3d1;
|
||||||
|
|
||||||
button {
|
button {
|
||||||
@@ -52,7 +52,7 @@
|
|||||||
#outline-sidebar-outline {
|
#outline-sidebar-outline {
|
||||||
margin-top: -1px;
|
margin-top: -1px;
|
||||||
|
|
||||||
@media (min-width: map-get($grid-breakpoints, "xl")) {
|
@media (--pgn-size-breakpoint-min-width-xl) {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,14 +62,14 @@
|
|||||||
|
|
||||||
.collapsible-trigger {
|
.collapsible-trigger {
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
padding: map-get($spacers, 3\.5) map-get($spacers, 4) map-get($spacers, 3\.5) map-get($spacers, 5);
|
padding: var(--pgn-spacing-spacer-3-5) var(--pgn-spacing-spacer-4) var(--pgn-spacing-spacer-3-5) var(--pgn-spacing-spacer-5);
|
||||||
|
|
||||||
@media (max-width: map-get($grid-breakpoints, "sm")) {
|
@media (--pgn-size-breakpoint-max-width-sm) {
|
||||||
padding-left: map-get($spacers, 4);
|
padding-left: var(--pgn-spacing-spacer-4);
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: $light-500;
|
background-color: var(--pgn-color-light-500);
|
||||||
}
|
}
|
||||||
|
|
||||||
.collapsible-icon {
|
.collapsible-icon {
|
||||||
@@ -78,7 +78,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&:last-child .pgn_collapsible {
|
&:last-child .pgn_collapsible {
|
||||||
@extend .mb-0;
|
margin-bottom: 0px !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,15 +86,15 @@
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
|
|
||||||
ol li > a {
|
ol li > a {
|
||||||
padding: map-get($spacers, 3\.5) map-get($spacers, 4) map-get($spacers, 3\.5) map-get($spacers, 5\.5);
|
padding: var(--pgn-spacing-spacer-3-5) var(--pgn-spacing-spacer-4) var(--pgn-spacing-spacer-3-5) var(--pgn-spacing-spacer-5-5);
|
||||||
|
|
||||||
@media (max-width: map-get($grid-breakpoints, "sm")) {
|
@media (--pgn-size-breakpoint-max-width-sm) {
|
||||||
padding-left: map-get($spacers, 4\.5);
|
padding-left: var(--pgn-spacing-spacer-4-5);
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
background-color: $light-500;
|
background-color: var(--pgn-color-light-500);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
.discussions-sidebar-frame {
|
.discussions-sidebar-frame {
|
||||||
@media (max-width: map-get($grid-breakpoints, "xl")) {
|
@media (--pgn-size-breakpoint-max-width-xl) {
|
||||||
max-height: calc(100vh - 65px);
|
max-height: calc(100vh - 65px);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
src/generic/assets/edX_certificate.png
Normal file
BIN
src/generic/assets/edX_certificate.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.1 KiB |
BIN
src/generic/assets/edX_locked_certificate.png
Normal file
BIN
src/generic/assets/edX_locked_certificate.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.5 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 8.3 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 6.7 KiB |
@@ -8,6 +8,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.upsell-bullet a {
|
.upsell-bullet a {
|
||||||
color: $primary-500;
|
color: var(--pgn-color-primary-500);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -166,11 +166,13 @@ subscribe(APP_INIT_ERROR, (error) => {
|
|||||||
initialize({
|
initialize({
|
||||||
handlers: {
|
handlers: {
|
||||||
config: () => {
|
config: () => {
|
||||||
|
/* istanbul ignore next */
|
||||||
mergeConfig({
|
mergeConfig({
|
||||||
CONTACT_URL: process.env.CONTACT_URL || null,
|
CONTACT_URL: process.env.CONTACT_URL || null,
|
||||||
CREDENTIALS_BASE_URL: process.env.CREDENTIALS_BASE_URL || null,
|
CREDENTIALS_BASE_URL: process.env.CREDENTIALS_BASE_URL || null,
|
||||||
CREDIT_HELP_LINK_URL: process.env.CREDIT_HELP_LINK_URL || null,
|
CREDIT_HELP_LINK_URL: process.env.CREDIT_HELP_LINK_URL || null,
|
||||||
DISCUSSIONS_MFE_BASE_URL: process.env.DISCUSSIONS_MFE_BASE_URL || null,
|
DISCUSSIONS_MFE_BASE_URL: process.env.DISCUSSIONS_MFE_BASE_URL || null,
|
||||||
|
DISCOUNT_CODE_INFO_URL: process.env.DISCOUNT_CODE_INFO_URL || null,
|
||||||
ENTERPRISE_LEARNER_PORTAL_HOSTNAME: process.env.ENTERPRISE_LEARNER_PORTAL_HOSTNAME || null,
|
ENTERPRISE_LEARNER_PORTAL_HOSTNAME: process.env.ENTERPRISE_LEARNER_PORTAL_HOSTNAME || null,
|
||||||
ENTERPRISE_LEARNER_PORTAL_URL: process.env.ENTERPRISE_LEARNER_PORTAL_URL || null,
|
ENTERPRISE_LEARNER_PORTAL_URL: process.env.ENTERPRISE_LEARNER_PORTAL_URL || null,
|
||||||
ENABLE_JUMPNAV: process.env.ENABLE_JUMPNAV || null,
|
ENABLE_JUMPNAV: process.env.ENABLE_JUMPNAV || null,
|
||||||
@@ -194,6 +196,7 @@ initialize({
|
|||||||
PRIVACY_POLICY_URL: process.env.PRIVACY_POLICY_URL || null,
|
PRIVACY_POLICY_URL: process.env.PRIVACY_POLICY_URL || null,
|
||||||
SHOW_UNGRADED_ASSIGNMENT_PROGRESS: process.env.SHOW_UNGRADED_ASSIGNMENT_PROGRESS || false,
|
SHOW_UNGRADED_ASSIGNMENT_PROGRESS: process.env.SHOW_UNGRADED_ASSIGNMENT_PROGRESS || false,
|
||||||
ENABLE_XPERT_AUDIT: process.env.ENABLE_XPERT_AUDIT || false,
|
ENABLE_XPERT_AUDIT: process.env.ENABLE_XPERT_AUDIT || false,
|
||||||
|
FEATURE_ENABLE_CHAT_V2_ENDPOINT: process.env.FEATURE_ENABLE_CHAT_V2_ENDPOINT || false,
|
||||||
}, 'LearnerAppConfig');
|
}, 'LearnerAppConfig');
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,7 +1,4 @@
|
|||||||
@import "~@edx/brand/paragon/fonts";
|
@use "@openedx/paragon/styles/css/core/custom-media-breakpoints" as paragonCustomMediaBreakpoints;
|
||||||
@import "~@edx/brand/paragon/variables";
|
|
||||||
@import "~@openedx/paragon/scss/core/core";
|
|
||||||
@import "~@edx/brand/paragon/overrides";
|
|
||||||
|
|
||||||
@import "~@edx/frontend-component-footer/dist/footer";
|
@import "~@edx/frontend-component-footer/dist/footer";
|
||||||
@import "~@edx/frontend-component-header/dist/index";
|
@import "~@edx/frontend-component-header/dist/index";
|
||||||
@@ -51,7 +48,7 @@
|
|||||||
.nav-link {
|
.nav-link {
|
||||||
border-bottom: 4px solid transparent;
|
border-bottom: 4px solid transparent;
|
||||||
border-top: 4px solid transparent;
|
border-top: 4px solid transparent;
|
||||||
color: $gray-700;
|
color: var(--pgn-color-gray-700);
|
||||||
|
|
||||||
// temporary until we can remove .btn class from dropdowns
|
// temporary until we can remove .btn class from dropdowns
|
||||||
border-left: 0;
|
border-left: 0;
|
||||||
@@ -61,9 +58,9 @@
|
|||||||
&:hover,
|
&:hover,
|
||||||
&:focus,
|
&:focus,
|
||||||
&.active {
|
&.active {
|
||||||
font-weight: $font-weight-normal;
|
font-weight: var(--pgn-typography-font-weight-normal);
|
||||||
color: $primary-500;
|
color: var(--pgn-color-primary-500);
|
||||||
border-bottom-color: $primary-500;
|
border-bottom-color: var(--pgn-color-primary-500);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -82,7 +79,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.sequence {
|
.sequence {
|
||||||
@media (min-width: map-get($grid-breakpoints, "sm")) {
|
@media (--pgn-size-breakpoint-min-width-sm) {
|
||||||
border: solid 1px #eaeaea;
|
border: solid 1px #eaeaea;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
@@ -94,7 +91,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.notification-btn {
|
.notification-btn {
|
||||||
@media (max-width: map-get($grid-breakpoints, "sm")) {
|
@media (--pgn-size-breakpoint-max-width-xs) {
|
||||||
height: 3rem;
|
height: 3rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -103,15 +100,15 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
|
|
||||||
@media (max-width: map-get($grid-breakpoints, "sm")) {
|
@media (--pgn-size-breakpoint-max-width-xs) {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: map-get($grid-breakpoints, "sm")) {
|
@media (--pgn-size-breakpoint-min-width-sm) {
|
||||||
margin: -1px -1px 0;
|
margin: -1px -1px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: map-get($grid-breakpoints, "sm")) {
|
@media (--pgn-size-breakpoint-max-width-xs) {
|
||||||
width: 100% !important;
|
width: 100% !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,13 +124,13 @@
|
|||||||
height: 3rem;
|
height: 3rem;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
color: $gray-500;
|
color: var(--pgn-color-gray-500);
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
|
||||||
&:hover,
|
&:hover,
|
||||||
&:focus,
|
&:focus,
|
||||||
&.active {
|
&.active {
|
||||||
color: $gray-700;
|
color: var(--pgn-color-gray-700);
|
||||||
}
|
}
|
||||||
|
|
||||||
&:focus {
|
&:focus {
|
||||||
@@ -148,13 +145,13 @@
|
|||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
height: 2px;
|
height: 2px;
|
||||||
background: $primary;
|
background: var(--pgn-color-primary-base);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.complete {
|
&.complete {
|
||||||
background-color: #eef7e5;
|
background-color: #eef7e5;
|
||||||
color: $success;
|
color: var(--pgn-color-success-base);
|
||||||
}
|
}
|
||||||
|
|
||||||
&:first-child {
|
&:first-child {
|
||||||
@@ -218,12 +215,12 @@
|
|||||||
min-width: 0;
|
min-width: 0;
|
||||||
margin: 0 1rem;
|
margin: 0 1rem;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
color: $gray-700;
|
color: var(--pgn-color-gray-700);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.active {
|
&.active {
|
||||||
.unit-icon {
|
.unit-icon {
|
||||||
color: $primary-500;
|
color: var(--pgn-color-primary-500);
|
||||||
}
|
}
|
||||||
|
|
||||||
&:after {
|
&:after {
|
||||||
@@ -235,7 +232,7 @@
|
|||||||
right: auto;
|
right: auto;
|
||||||
width: 2px;
|
width: 2px;
|
||||||
height: auto;
|
height: auto;
|
||||||
background: $primary;
|
background: var(--pgn-color-primary-base);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -250,18 +247,18 @@
|
|||||||
|
|
||||||
.previous-btn,
|
.previous-btn,
|
||||||
.next-btn {
|
.next-btn {
|
||||||
border: 1px solid $light-400 !important;
|
border: 1px solid var(--pgn-color-light-400) !important;
|
||||||
color: $gray-700;
|
color: var(--pgn-color-gray-700);
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
@media (max-width: map-get($grid-breakpoints, "sm")) {
|
@media (--pgn-size-breakpoint-max-width-sm) {
|
||||||
padding-top: 1rem;
|
padding-top: 1rem;
|
||||||
padding-bottom: 1rem;
|
padding-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: map-get($grid-breakpoints, "sm")) {
|
@media (--pgn-size-breakpoint-min-width-sm) {
|
||||||
min-width: fit-content;
|
min-width: fit-content;
|
||||||
padding-left: 2rem;
|
padding-left: 2rem;
|
||||||
padding-right: 2rem;
|
padding-right: 2rem;
|
||||||
@@ -272,7 +269,7 @@
|
|||||||
border-left-width: 0;
|
border-left-width: 0;
|
||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
|
|
||||||
@media (min-width: map-get($grid-breakpoints, "sm")) {
|
@media (--pgn-size-breakpoint-min-width-sm) {
|
||||||
border-left-width: 1px;
|
border-left-width: 1px;
|
||||||
border-top-left-radius: 4px;
|
border-top-left-radius: 4px;
|
||||||
}
|
}
|
||||||
@@ -282,7 +279,7 @@
|
|||||||
border-left-width: 1px;
|
border-left-width: 1px;
|
||||||
border-right-width: 0;
|
border-right-width: 0;
|
||||||
|
|
||||||
@media (min-width: map-get($grid-breakpoints, "sm")) {
|
@media (--pgn-size-breakpoint-min-width-sm) {
|
||||||
border-top-right-radius: 4px;
|
border-top-right-radius: 4px;
|
||||||
border-right-width: 1px;
|
border-right-width: 1px;
|
||||||
}
|
}
|
||||||
@@ -296,15 +293,20 @@
|
|||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
margin-right: auto;
|
margin-right: auto;
|
||||||
|
|
||||||
@media (min-width: map-get($grid-breakpoints, "sm")) {
|
@media (--pgn-size-breakpoint-min-width-sm) {
|
||||||
padding-left: $grid-gutter-width;
|
padding-left: var(--pgn-spacing-grid-gutter-width);
|
||||||
padding-right: $grid-gutter-width;
|
padding-right: var(--pgn-spacing-grid-gutter-width);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 830px) {
|
@media (min-width: 830px) {
|
||||||
padding-left: 40px;
|
padding-left: 40px;
|
||||||
padding-right: 40px;
|
padding-right: 40px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Unit title is styled as an H3
|
||||||
|
.unit-title {
|
||||||
|
font-size: var(--pgn-typography-font-size-h3-base);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.unit-iframe-wrapper {
|
.unit-iframe-wrapper {
|
||||||
@@ -316,8 +318,8 @@
|
|||||||
// here we compensate for the padding of the parent div with "container-xl"
|
// here we compensate for the padding of the parent div with "container-xl"
|
||||||
// class to ensure that the viewport width is the same as the width of the
|
// class to ensure that the viewport width is the same as the width of the
|
||||||
// iframe.
|
// iframe.
|
||||||
margin-left: -$grid-gutter-width * .5;
|
margin-left: calc(var(--pgn-spacing-grid-gutter-width) * -0.5);
|
||||||
margin-right: -$grid-gutter-width * .5;
|
margin-right: calc(var(--pgn-spacing-grid-gutter-width) * -0.5);
|
||||||
|
|
||||||
margin-bottom: 2rem;
|
margin-bottom: 2rem;
|
||||||
|
|
||||||
@@ -339,9 +341,9 @@
|
|||||||
max-width: 640px;
|
max-width: 640px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
|
|
||||||
@media (max-width: map-get($grid-breakpoints, "sm")) {
|
@media (--pgn-size-breakpoint-max-width-xs) {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: $spacer;
|
gap: var(--pgn-spacing-spacer-base);
|
||||||
}
|
}
|
||||||
|
|
||||||
.previous-button,
|
.previous-button,
|
||||||
@@ -370,19 +372,22 @@
|
|||||||
// window (retaining padding around the edge). Bootstrap modals don't have a full-screen
|
// window (retaining padding around the edge). Bootstrap modals don't have a full-screen
|
||||||
// size like this. Because of the hack below around react-focus-on's div, it would be better long-term to pull this into Paragon and perhaps call it "modal-full" or something like that.
|
// size like this. Because of the hack below around react-focus-on's div, it would be better long-term to pull this into Paragon and perhaps call it "modal-full" or something like that.
|
||||||
.modal-lti {
|
.modal-lti {
|
||||||
height: 100%;
|
height: 80vh;
|
||||||
max-width: 100% !important;
|
max-width: 100% !important;
|
||||||
|
|
||||||
// I don't like this. We need to set a height of 100% on a div created by react-focus-on, a
|
// I don't like this. We need to set a height of 100% on a div created by react-focus-on, a
|
||||||
// package we use in our Modal. That div has no class name or ID, so instead we're uniquely
|
// package we use in our Modal. That div has no class name or ID, so instead we're uniquely
|
||||||
// identifying it by based on a unique attribute it has which its siblings don't share.
|
// identifying it by based on a unique attribute it has which its siblings don't share.
|
||||||
> div[data-focus-lock-disabled="false"] {
|
> div[data-focus-lock-disabled="false"] {
|
||||||
height: 100%;
|
height: 80vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Along with setting the height of modal-content's parent div from react-focus-on, we need to
|
// Along with setting the height of modal-content's parent div from react-focus-on, we need to
|
||||||
// set modal-content's height as well to get the modal to expand to full-screen height.
|
// set modal-content's height as well to get the modal to expand to full-screen height.
|
||||||
.modal-content {
|
.modal-content {
|
||||||
|
height: 80vh;
|
||||||
|
}
|
||||||
|
.pgn__modal-body-content {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -411,8 +416,8 @@
|
|||||||
|
|
||||||
.icon-hover {
|
.icon-hover {
|
||||||
&:hover {
|
&:hover {
|
||||||
color: $primary-500 !important;
|
color: var(--pgn-color-primary-500) !important;
|
||||||
background-color: $light-300 !important;
|
background-color: var(--pgn-color-light-300) !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -435,7 +440,7 @@
|
|||||||
height: 56px !important;
|
height: 56px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
@include media-breakpoint-down(xs) {
|
@media (--pgn-size-breakpoint-max-width-xs) {
|
||||||
.course-outline-tab .pgn__card {
|
.course-outline-tab .pgn__card {
|
||||||
.pgn__card-header {
|
.pgn__card-header {
|
||||||
display: block;
|
display: block;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||||
import { Input } from '@openedx/paragon';
|
import { Form } from '@openedx/paragon';
|
||||||
|
|
||||||
import { MasqueradeStatus, Payload } from './data/api';
|
import { MasqueradeStatus, Payload } from './data/api';
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
@@ -40,11 +40,10 @@ export const MasqueradeUserNameInput: React.FC<Props> = ({ onSubmit, onError, ..
|
|||||||
}, [handleSubmit]);
|
}, [handleSubmit]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Input
|
<Form.Control
|
||||||
aria-labelledby="masquerade-search-label"
|
aria-labelledby="masquerade-search-label"
|
||||||
label={intl.formatMessage(messages.userNameLabel)}
|
label={intl.formatMessage(messages.userNameLabel)}
|
||||||
onKeyPress={handleKeyPress}
|
onKeyPress={handleKeyPress}
|
||||||
type="text"
|
|
||||||
{...otherProps}
|
{...otherProps}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
39
src/plugin-slots/ContentIFrameErrorSlot/README.md
Normal file
39
src/plugin-slots/ContentIFrameErrorSlot/README.md
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
# Content iFrame Error Slot
|
||||||
|
|
||||||
|
### Slot ID: `org.openedx.frontend.learning.content_iframe_error.v1`
|
||||||
|
|
||||||
|
### Parameters: `courseId`
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
This slot is used to replace/modify the content iframe error page.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
The following `env.config.jsx` will replace the error page with emojis.
|
||||||
|
|
||||||
|
```js
|
||||||
|
import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
pluginSlots: {
|
||||||
|
'org.openedx.frontend.learning.content_iframe_error.v1': {
|
||||||
|
keepDefault: false,
|
||||||
|
plugins: [
|
||||||
|
{
|
||||||
|
op: PLUGIN_OPERATIONS.Insert,
|
||||||
|
widget: {
|
||||||
|
id: 'custom_error_page',
|
||||||
|
type: DIRECT_PLUGIN,
|
||||||
|
RenderWidget: ({courseId}) => (
|
||||||
|
<h1>🚨🤖💥</h1>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export default config;
|
||||||
|
```
|
||||||
17
src/plugin-slots/ContentIFrameErrorSlot/index.tsx
Normal file
17
src/plugin-slots/ContentIFrameErrorSlot/index.tsx
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { PluginSlot } from '@openedx/frontend-plugin-framework';
|
||||||
|
import { ErrorPage } from '@edx/frontend-platform/react';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
courseId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ContentIFrameErrorSlot : React.FC<Props> = ({ courseId }: Props) => (
|
||||||
|
<PluginSlot
|
||||||
|
id="org.openedx.frontend.learning.content_iframe_error.v1"
|
||||||
|
pluginProps={{ courseId }}
|
||||||
|
>
|
||||||
|
<ErrorPage />
|
||||||
|
</PluginSlot>
|
||||||
|
);
|
||||||
28
src/plugin-slots/LearnerToolsSlot/README.md
Normal file
28
src/plugin-slots/LearnerToolsSlot/README.md
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
# Learner Tools Slot
|
||||||
|
|
||||||
|
### Slot ID: `org.openedx.frontend.learning.learner_tools.v1`
|
||||||
|
|
||||||
|
### Slot ID Aliases
|
||||||
|
* `learner_tools_slot`
|
||||||
|
|
||||||
|
### Description
|
||||||
|
This plugin slot provides a location for learner-facing tools and features to be displayed during course content navigation. The slot is rendered via a React portal to `document.body` to ensure proper positioning and stacking context.
|
||||||
|
|
||||||
|
### Props:
|
||||||
|
* `courseId` - The unique identifier for the current course
|
||||||
|
* `unitId` - The unique identifier for the current unit/vertical being viewed
|
||||||
|
* `userId` - The authenticated user's ID (automatically retrieved from auth context)
|
||||||
|
* `isStaff` - Boolean indicating whether the user has staff/instructor privileges
|
||||||
|
* `enrollmentMode` - The user's enrollment mode (e.g., 'audit', 'verified', 'honor', etc.)
|
||||||
|
|
||||||
|
### Usage
|
||||||
|
Plugins registered to this slot can use the provided context to:
|
||||||
|
- Display course-specific tools based on courseId and unitId
|
||||||
|
- Show different features based on user's enrollment mode
|
||||||
|
- Provide staff-only functionality when isStaff is true
|
||||||
|
- Query additional data from Redux store or backend APIs as needed
|
||||||
|
|
||||||
|
### Notes
|
||||||
|
- Returns `null` if user is not authenticated
|
||||||
|
- Plugins should manage their own feature flag checks and requirements
|
||||||
|
- The slot uses a portal to render to `document.body` for flexible positioning
|
||||||
47
src/plugin-slots/LearnerToolsSlot/index.jsx
Normal file
47
src/plugin-slots/LearnerToolsSlot/index.jsx
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import { createPortal } from 'react-dom';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { PluginSlot } from '@openedx/frontend-plugin-framework';
|
||||||
|
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
|
||||||
|
|
||||||
|
export const LearnerToolsSlot = ({
|
||||||
|
enrollmentMode = null,
|
||||||
|
isStaff,
|
||||||
|
courseId,
|
||||||
|
unitId,
|
||||||
|
}) => {
|
||||||
|
const authenticatedUser = getAuthenticatedUser();
|
||||||
|
|
||||||
|
// Return null if user is not authenticated to avoid destructuring errors
|
||||||
|
if (!authenticatedUser) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { userId } = authenticatedUser;
|
||||||
|
|
||||||
|
// Provide minimal, generic context - no feature-specific flags
|
||||||
|
const pluginContext = {
|
||||||
|
courseId,
|
||||||
|
unitId,
|
||||||
|
userId,
|
||||||
|
isStaff,
|
||||||
|
enrollmentMode,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Use generic plugin slot ID (location-based, not feature-specific)
|
||||||
|
// Plugins will query their own requirements from Redux/config
|
||||||
|
return createPortal(
|
||||||
|
<PluginSlot
|
||||||
|
id="org.openedx.frontend.learning.learner_tools.v1"
|
||||||
|
idAliases={['learner_tools_slot']}
|
||||||
|
pluginProps={pluginContext}
|
||||||
|
/>,
|
||||||
|
document.body,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
LearnerToolsSlot.propTypes = {
|
||||||
|
isStaff: PropTypes.bool.isRequired,
|
||||||
|
enrollmentMode: PropTypes.string,
|
||||||
|
courseId: PropTypes.string.isRequired,
|
||||||
|
unitId: PropTypes.string.isRequired,
|
||||||
|
};
|
||||||
104
src/plugin-slots/LearnerToolsSlot/index.test.jsx
Normal file
104
src/plugin-slots/LearnerToolsSlot/index.test.jsx
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { render } from '@testing-library/react';
|
||||||
|
import { PluginSlot } from '@openedx/frontend-plugin-framework';
|
||||||
|
import * as auth from '@edx/frontend-platform/auth';
|
||||||
|
|
||||||
|
import { LearnerToolsSlot } from './index';
|
||||||
|
|
||||||
|
jest.mock('@openedx/frontend-plugin-framework', () => ({
|
||||||
|
PluginSlot: jest.fn(() => <div data-testid="plugin-slot">Plugin Slot</div>),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('@edx/frontend-platform/auth', () => ({
|
||||||
|
getAuthenticatedUser: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('LearnerToolsSlot', () => {
|
||||||
|
const defaultProps = {
|
||||||
|
courseId: 'course-v1:edX+DemoX+Demo_Course',
|
||||||
|
unitId: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@unit1',
|
||||||
|
isStaff: false,
|
||||||
|
enrollmentMode: 'verified',
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
// Mock document.body for createPortal
|
||||||
|
document.body.innerHTML = '<div id="root"></div>';
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders PluginSlot with correct props when user is authenticated', () => {
|
||||||
|
const mockUser = { userId: 123, username: 'testuser' };
|
||||||
|
auth.getAuthenticatedUser.mockReturnValue(mockUser);
|
||||||
|
|
||||||
|
render(<LearnerToolsSlot {...defaultProps} />);
|
||||||
|
|
||||||
|
expect(PluginSlot).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
id: 'org.openedx.frontend.learning.learner_tools.v1',
|
||||||
|
idAliases: ['learner_tools_slot'],
|
||||||
|
pluginProps: {
|
||||||
|
courseId: defaultProps.courseId,
|
||||||
|
unitId: defaultProps.unitId,
|
||||||
|
userId: mockUser.userId,
|
||||||
|
isStaff: defaultProps.isStaff,
|
||||||
|
enrollmentMode: defaultProps.enrollmentMode,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns null when user is not authenticated', () => {
|
||||||
|
auth.getAuthenticatedUser.mockReturnValue(null);
|
||||||
|
|
||||||
|
const { container } = render(<LearnerToolsSlot {...defaultProps} />);
|
||||||
|
|
||||||
|
expect(container.firstChild).toBeNull();
|
||||||
|
expect(PluginSlot).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('uses default null for enrollmentMode when not provided', () => {
|
||||||
|
const mockUser = { userId: 456, username: 'testuser2' };
|
||||||
|
auth.getAuthenticatedUser.mockReturnValue(mockUser);
|
||||||
|
|
||||||
|
const { enrollmentMode, ...propsWithoutEnrollmentMode } = defaultProps;
|
||||||
|
|
||||||
|
render(<LearnerToolsSlot {...propsWithoutEnrollmentMode} />);
|
||||||
|
|
||||||
|
expect(PluginSlot).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
pluginProps: expect.objectContaining({
|
||||||
|
enrollmentMode: null,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('passes isStaff=true correctly', () => {
|
||||||
|
const mockUser = { userId: 789, username: 'staffuser' };
|
||||||
|
auth.getAuthenticatedUser.mockReturnValue(mockUser);
|
||||||
|
|
||||||
|
render(<LearnerToolsSlot {...defaultProps} isStaff />);
|
||||||
|
|
||||||
|
expect(PluginSlot).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
pluginProps: expect.objectContaining({
|
||||||
|
isStaff: true,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders to document.body via portal', () => {
|
||||||
|
const mockUser = { userId: 999, username: 'portaluser' };
|
||||||
|
auth.getAuthenticatedUser.mockReturnValue(mockUser);
|
||||||
|
|
||||||
|
render(<LearnerToolsSlot {...defaultProps} />);
|
||||||
|
|
||||||
|
// The portal should render to document.body
|
||||||
|
expect(document.body.querySelector('[data-testid="plugin-slot"]')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -11,6 +11,7 @@
|
|||||||
* [`org.openedx.frontend.learning.course_outline_tab_notifications.v1`](./CourseOutlineTabNotificationsSlot/)
|
* [`org.openedx.frontend.learning.course_outline_tab_notifications.v1`](./CourseOutlineTabNotificationsSlot/)
|
||||||
* [`org.openedx.frontend.learning.course_recommendations.v1`](./CourseRecommendationsSlot/)
|
* [`org.openedx.frontend.learning.course_recommendations.v1`](./CourseRecommendationsSlot/)
|
||||||
* [`org.openedx.frontend.learning.gated_unit_content_message.v1`](./GatedUnitContentMessageSlot/)
|
* [`org.openedx.frontend.learning.gated_unit_content_message.v1`](./GatedUnitContentMessageSlot/)
|
||||||
|
* [`org.openedx.frontend.learning.learner_tools.v1`](./LearnerToolsSlot/)
|
||||||
* [`org.openedx.frontend.learning.next_unit_top_nav_trigger.v1`](./NextUnitTopNavTriggerSlot/)
|
* [`org.openedx.frontend.learning.next_unit_top_nav_trigger.v1`](./NextUnitTopNavTriggerSlot/)
|
||||||
* [`org.openedx.frontend.learning.notification_tray.v1`](./NotificationTraySlot/)
|
* [`org.openedx.frontend.learning.notification_tray.v1`](./NotificationTraySlot/)
|
||||||
* [`org.openedx.frontend.learning.notification_widget.v1`](./NotificationWidgetSlot/)
|
* [`org.openedx.frontend.learning.notification_widget.v1`](./NotificationWidgetSlot/)
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ const UnitTitleSlot = ({
|
|||||||
>
|
>
|
||||||
<div className="d-flex justify-content-between">
|
<div className="d-flex justify-content-between">
|
||||||
<div className="mb-0">
|
<div className="mb-0">
|
||||||
<h3 className="h3">{unit.title}</h3>
|
<h1 className="unit-title">{unit.title}</h1>
|
||||||
</div>
|
</div>
|
||||||
{isEnabledOutlineSidebar && renderUnitNavigation(true)}
|
{isEnabledOutlineSidebar && renderUnitNavigation(true)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { getConfig } from '@edx/frontend-platform';
|
import { getConfig } from '@edx/frontend-platform';
|
||||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||||
|
|
||||||
export const getUnsubscribeUrl = (userToken, updatePatch) => (
|
export const getUnsubscribeUrl = (userToken) => (
|
||||||
`${getConfig().LMS_BASE_URL}/api/notifications/preferences/update/${userToken}/${updatePatch}/`
|
`${getConfig().LMS_BASE_URL}/api/notifications/preferences/update/${userToken}/`
|
||||||
);
|
);
|
||||||
|
|
||||||
export async function unsubscribeNotificationPreferences(userToken, updatePatch) {
|
export async function unsubscribeNotificationPreferences(userToken) {
|
||||||
const url = getUnsubscribeUrl(userToken, updatePatch);
|
const url = getUnsubscribeUrl(userToken);
|
||||||
return getAuthenticatedHttpClient().get(url);
|
return getAuthenticatedHttpClient().get(url);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,18 +17,18 @@ import messages from './messages';
|
|||||||
|
|
||||||
const PreferencesUnsubscribe = () => {
|
const PreferencesUnsubscribe = () => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const { userToken, updatePatch } = useParams();
|
const { userToken } = useParams();
|
||||||
const [status, setStatus] = useState(LOADING);
|
const [status, setStatus] = useState(LOADING);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
unsubscribeNotificationPreferences(userToken, updatePatch).then(
|
unsubscribeNotificationPreferences(userToken).then(
|
||||||
() => setStatus(LOADED),
|
() => setStatus(LOADED),
|
||||||
(error) => {
|
(error) => {
|
||||||
setStatus(FAILED);
|
setStatus(FAILED);
|
||||||
logError(error);
|
logError(error);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
sendTrackEvent('edx.ui.lms.notifications.preferences.unsubscribe', { userToken, updatePatch });
|
sendTrackEvent('edx.ui.lms.notifications.preferences.unsubscribe', { userToken });
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const pageContent = {
|
const pageContent = {
|
||||||
|
|||||||
@@ -24,8 +24,7 @@ describe('Notification Preferences One Click Unsubscribe', () => {
|
|||||||
let component;
|
let component;
|
||||||
let store;
|
let store;
|
||||||
const userToken = '1234';
|
const userToken = '1234';
|
||||||
const updatePatch = 'abc123';
|
const url = getUnsubscribeUrl(userToken);
|
||||||
const url = getUnsubscribeUrl(userToken, updatePatch);
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
await initializeTestStore();
|
await initializeTestStore();
|
||||||
@@ -39,7 +38,7 @@ describe('Notification Preferences One Click Unsubscribe', () => {
|
|||||||
component = (
|
component = (
|
||||||
<AppProvider store={store} wrapWithRouter={false}>
|
<AppProvider store={store} wrapWithRouter={false}>
|
||||||
<UserMessagesProvider>
|
<UserMessagesProvider>
|
||||||
<MemoryRouter initialEntries={[`${`/preferences-unsubscribe/${userToken}/${updatePatch}/`}`]}>
|
<MemoryRouter initialEntries={[`${`/preferences-unsubscribe/${userToken}/`}`]}>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path={ROUTES.PREFERENCES_UNSUBSCRIBE} element={<PreferencesUnsubscribe />} />
|
<Route path={ROUTES.PREFERENCES_UNSUBSCRIBE} element={<PreferencesUnsubscribe />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
@@ -69,7 +68,6 @@ describe('Notification Preferences One Click Unsubscribe', () => {
|
|||||||
expect(screen.getByTestId('heading-text')).toHaveTextContent('Error unsubscribing from preference');
|
expect(screen.getByTestId('heading-text')).toHaveTextContent('Error unsubscribing from preference');
|
||||||
expect(sendTrackEvent).toHaveBeenCalledWith('edx.ui.lms.notifications.preferences.unsubscribe', {
|
expect(sendTrackEvent).toHaveBeenCalledWith('edx.ui.lms.notifications.preferences.unsubscribe', {
|
||||||
userToken,
|
userToken,
|
||||||
updatePatch,
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
endCourseHomeTour,
|
endCourseHomeTour,
|
||||||
endCoursewareTour,
|
endCoursewareTour,
|
||||||
fetchTourData,
|
fetchTourData,
|
||||||
|
openCourseHomeTour,
|
||||||
} from './data';
|
} from './data';
|
||||||
|
|
||||||
const ProductTours = ({
|
const ProductTours = ({
|
||||||
@@ -164,7 +165,7 @@ const ProductTours = ({
|
|||||||
is_staff: administrator,
|
is_staff: administrator,
|
||||||
});
|
});
|
||||||
dispatch(closeNewUserCourseHomeModal());
|
dispatch(closeNewUserCourseHomeModal());
|
||||||
setIsNewUserCourseHomeTourEnabled(true);
|
dispatch(openCourseHomeTour());
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ export {
|
|||||||
endCourseHomeTour,
|
endCourseHomeTour,
|
||||||
endCoursewareTour,
|
endCoursewareTour,
|
||||||
fetchTourData,
|
fetchTourData,
|
||||||
|
openCourseHomeTour,
|
||||||
} from './thunks';
|
} from './thunks';
|
||||||
|
|
||||||
export { reducer } from './slice';
|
export { reducer } from './slice';
|
||||||
|
|||||||
@@ -6,12 +6,17 @@ import {
|
|||||||
disableCoursewareTour,
|
disableCoursewareTour,
|
||||||
disableNewUserCourseHomeModal,
|
disableNewUserCourseHomeModal,
|
||||||
setTourData,
|
setTourData,
|
||||||
|
launchCourseHomeTour,
|
||||||
} from './slice';
|
} from './slice';
|
||||||
|
|
||||||
export function closeNewUserCourseHomeModal() {
|
export function closeNewUserCourseHomeModal() {
|
||||||
return async (dispatch) => dispatch(disableNewUserCourseHomeModal());
|
return async (dispatch) => dispatch(disableNewUserCourseHomeModal());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function openCourseHomeTour() {
|
||||||
|
return async (dispatch) => dispatch(launchCourseHomeTour());
|
||||||
|
}
|
||||||
|
|
||||||
export function endCourseHomeTour(username) {
|
export function endCourseHomeTour(username) {
|
||||||
return async (dispatch) => {
|
return async (dispatch) => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
[dir="rtl"] .new-user-tour-dialog .pgn__modal-hero .pgn__modal-hero-bg {
|
[dir="rtl"] .new-user-tour-dialog .pgn__modal-hero .pgn__modal-hero-bg {
|
||||||
transform: scaleX(-1);
|
transform: scaleX(-1);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -116,6 +116,7 @@ export function buildMinimalCourseBlocks(courseId, title, options = {}) {
|
|||||||
effort_activities: 2,
|
effort_activities: 2,
|
||||||
effort_time: 15,
|
effort_time: 15,
|
||||||
type: 'sequential',
|
type: 'sequential',
|
||||||
|
is_preview: false,
|
||||||
},
|
},
|
||||||
{ courseId },
|
{ courseId },
|
||||||
)];
|
)];
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
/* eslint-disable react/prop-types */
|
/* eslint-disable react/prop-types */
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { camelCaseObject, getConfig } from '@edx/frontend-platform';
|
import { getConfig } from '@edx/frontend-platform';
|
||||||
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
|
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
|
||||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
|
||||||
import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
|
import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
|
||||||
import { Lightbulb, MoneyFilled } from '@openedx/paragon/icons';
|
import { Lightbulb, MoneyFilled } from '@openedx/paragon/icons';
|
||||||
import {
|
import {
|
||||||
@@ -16,7 +15,12 @@ import { useModel } from '../../generic/model-store';
|
|||||||
import StreakMobileImage from './assets/Streak_mobile.png';
|
import StreakMobileImage from './assets/Streak_mobile.png';
|
||||||
import StreakDesktopImage from './assets/Streak_desktop.png';
|
import StreakDesktopImage from './assets/Streak_desktop.png';
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
import { recordModalClosing, recordStreakCelebration } from './utils';
|
import {
|
||||||
|
calculateVoucherDiscountPercentage,
|
||||||
|
getDiscountCodePercentage,
|
||||||
|
recordModalClosing,
|
||||||
|
recordStreakCelebration,
|
||||||
|
} from './utils';
|
||||||
|
|
||||||
function getRandomFactoid(intl, streakLength) {
|
function getRandomFactoid(intl, streakLength) {
|
||||||
const boldedSectionA = intl.formatMessage(messages.streakFactoidABoldedSection);
|
const boldedSectionA = intl.formatMessage(messages.streakFactoidABoldedSection);
|
||||||
@@ -42,13 +46,6 @@ function getRandomFactoid(intl, streakLength) {
|
|||||||
return factoids[Math.floor(Math.random() * (factoids.length))];
|
return factoids[Math.floor(Math.random() * (factoids.length))];
|
||||||
}
|
}
|
||||||
|
|
||||||
async function calculateVoucherDiscount(voucher, sku, username) {
|
|
||||||
const urlBase = `${getConfig().ECOMMERCE_BASE_URL}/api/v2/baskets/calculate`;
|
|
||||||
const url = `${urlBase}/?code=${voucher}&sku=${sku}&username=${username}`;
|
|
||||||
return getAuthenticatedHttpClient().get(url)
|
|
||||||
.then(res => camelCaseObject(res));
|
|
||||||
}
|
|
||||||
|
|
||||||
const CloseText = ({ intl }) => (
|
const CloseText = ({ intl }) => (
|
||||||
<span>
|
<span>
|
||||||
{intl.formatMessage(messages.streakButton)}
|
{intl.formatMessage(messages.streakButton)}
|
||||||
@@ -83,34 +80,38 @@ const StreakModal = ({
|
|||||||
|
|
||||||
// Ask ecommerce to calculate discount savings
|
// Ask ecommerce to calculate discount savings
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (streakDiscountCouponEnabled && verifiedMode && getConfig().ECOMMERCE_BASE_URL) {
|
(async () => {
|
||||||
calculateVoucherDiscount(discountCode, verifiedMode.sku, username)
|
let streakDiscountPercentage = 0;
|
||||||
.then(
|
try {
|
||||||
(result) => {
|
if (streakDiscountCouponEnabled && verifiedMode) {
|
||||||
const { totalInclTax, totalInclTaxExclDiscounts } = result.data;
|
// If the discount service is available, use it to get the discount percentage
|
||||||
if (totalInclTaxExclDiscounts && totalInclTax !== totalInclTaxExclDiscounts) {
|
if (getConfig().DISCOUNT_CODE_INFO_URL) {
|
||||||
// Just store the percent (rather than using these values directly), because ecommerce doesn't give us
|
streakDiscountPercentage = await getDiscountCodePercentage(
|
||||||
// the currency symbol to use, so we want to use the symbol that LMS gives us. And I don't want to assume
|
discountCode,
|
||||||
// ecommerce's currency is the same as the LMS. So we'll keep using the values in verifiedMode, just
|
courseId,
|
||||||
// multiplied by the calculated percentage.
|
);
|
||||||
setDiscountPercent(1 - totalInclTax / totalInclTaxExclDiscounts);
|
// If the discount service is not available, fall back to ecommerce to calculate the discount percentage
|
||||||
sendTrackEvent('edx.bi.course.streak_discount_enabled', {
|
} else if (getConfig().ECOMMERCE_BASE_URL) {
|
||||||
course_id: courseId,
|
streakDiscountPercentage = await calculateVoucherDiscountPercentage(
|
||||||
sku: verifiedMode.sku,
|
discountCode,
|
||||||
});
|
verifiedMode.sku,
|
||||||
} else {
|
username,
|
||||||
setDiscountPercent(0);
|
);
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
() => {
|
} catch {
|
||||||
// ignore any errors - we just won't show the discount to the user then
|
// ignore any errors - we just won't show the discount to the user then
|
||||||
setDiscountPercent(0);
|
} finally {
|
||||||
},
|
if (streakDiscountPercentage) {
|
||||||
);
|
sendTrackEvent('edx.bi.course.streak_discount_enabled', {
|
||||||
} else {
|
course_id: courseId,
|
||||||
setDiscountPercent(0);
|
sku: verifiedMode.sku,
|
||||||
}
|
});
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
}
|
||||||
|
setDiscountPercent(streakDiscountPercentage);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [streakDiscountCouponEnabled, username, verifiedMode]);
|
}, [streakDiscountCouponEnabled, username, verifiedMode]);
|
||||||
|
|
||||||
if (!isStreakCelebrationOpen) {
|
if (!isStreakCelebrationOpen) {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Factory } from 'rosie';
|
import { Factory } from 'rosie';
|
||||||
import { camelCaseObject, getConfig } from '@edx/frontend-platform';
|
import { camelCaseObject, getConfig, mergeConfig } from '@edx/frontend-platform';
|
||||||
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
|
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
|
||||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||||
import { breakpoints } from '@openedx/paragon';
|
import { breakpoints } from '@openedx/paragon';
|
||||||
@@ -34,6 +34,19 @@ describe('Loaded Tab Page', () => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setDiscountViaDiscountCodeInfo(percent) {
|
||||||
|
const discountURLParams = new URLSearchParams();
|
||||||
|
discountURLParams.append('code', 'ZGY11119949');
|
||||||
|
discountURLParams.append('course_run_key', courseMetadata.id);
|
||||||
|
const discountURL = `${getConfig().DISCOUNT_CODE_INFO_URL}?${discountURLParams.toString()}`;
|
||||||
|
|
||||||
|
mockData.streakDiscountCouponEnabled = true;
|
||||||
|
axiosMock.onGet(discountURL).reply(200, {
|
||||||
|
isApplicable: true,
|
||||||
|
discountPercentage: percent / 100,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function setDiscountError() {
|
function setDiscountError() {
|
||||||
mockData.streakDiscountCouponEnabled = true;
|
mockData.streakDiscountCouponEnabled = true;
|
||||||
axiosMock.onGet(calculateUrl).reply(500);
|
axiosMock.onGet(calculateUrl).reply(500);
|
||||||
@@ -105,4 +118,22 @@ describe('Loaded Tab Page', () => {
|
|||||||
sku: mockData.verifiedMode.sku,
|
sku: mockData.verifiedMode.sku,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('shows discount version of streak celebration modal when discount available and info fetched using DISCOUNT_CODE_INFO_URL', async () => {
|
||||||
|
mergeConfig({ DISCOUNT_CODE_INFO_URL: 'http://localhost:8140/lms/discount-code-info/' });
|
||||||
|
|
||||||
|
global.innerWidth = breakpoints.extraSmall.maxWidth;
|
||||||
|
setDiscountViaDiscountCodeInfo(14);
|
||||||
|
await renderModal();
|
||||||
|
|
||||||
|
const endDateText = `Ends ${new Date(Date.now() + 14 * 24 * 60 * 60 * 1000).toLocaleDateString({ timeZone: 'UTC' })}.`;
|
||||||
|
expect(screen.getByText('You’ve unlocked a 14% off discount when you upgrade this course for a limited time only.', { exact: false })).toBeInTheDocument();
|
||||||
|
expect(screen.getByText(endDateText, { exact: false })).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('Continue with course')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByText('Keep it up')).not.toBeInTheDocument();
|
||||||
|
expect(sendTrackEvent).toHaveBeenCalledWith('edx.bi.course.streak_discount_enabled', {
|
||||||
|
course_id: mockData.courseId,
|
||||||
|
sku: mockData.verifiedMode.sku,
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
|
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
|
||||||
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
|
import { camelCaseObject, getConfig } from '@edx/frontend-platform';
|
||||||
|
import {
|
||||||
|
getAuthenticatedHttpClient,
|
||||||
|
getAuthenticatedUser,
|
||||||
|
} from '@edx/frontend-platform/auth';
|
||||||
|
|
||||||
import { updateModel } from '../../generic/model-store';
|
import { updateModel } from '../../generic/model-store';
|
||||||
|
|
||||||
@@ -24,4 +28,39 @@ function recordModalClosing(celebrations, org, courseId, dispatch) {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
export { recordStreakCelebration, recordModalClosing };
|
async function calculateVoucherDiscountPercentage(voucher, sku, username) {
|
||||||
|
const urlBase = `${getConfig().ECOMMERCE_BASE_URL}/api/v2/baskets/calculate`;
|
||||||
|
const url = `${urlBase}/?code=${voucher}&sku=${sku}&username=${username}`;
|
||||||
|
|
||||||
|
const result = await getAuthenticatedHttpClient().get(url);
|
||||||
|
const { totalInclTax, totalInclTaxExclDiscounts } = camelCaseObject(result).data;
|
||||||
|
|
||||||
|
if (totalInclTaxExclDiscounts && totalInclTax !== totalInclTaxExclDiscounts) {
|
||||||
|
// Just store the percent (rather than using these values directly), because ecommerce doesn't give us
|
||||||
|
// the currency symbol to use, so we want to use the symbol that LMS gives us. And I don't want to assume
|
||||||
|
// ecommerce's currency is the same as the LMS. So we'll keep using the values in verifiedMode, just
|
||||||
|
// multiplied by the calculated percentage.
|
||||||
|
return 1 - totalInclTax / totalInclTaxExclDiscounts;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getDiscountCodePercentage(code, courseId) {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
params.append('code', code);
|
||||||
|
params.append('course_run_key', courseId);
|
||||||
|
const url = `${getConfig().DISCOUNT_CODE_INFO_URL}?${params.toString()}`;
|
||||||
|
|
||||||
|
const result = await getAuthenticatedHttpClient().get(url);
|
||||||
|
const { isApplicable, discountPercentage } = camelCaseObject(result).data;
|
||||||
|
|
||||||
|
return isApplicable ? +discountPercentage : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
calculateVoucherDiscountPercentage,
|
||||||
|
getDiscountCodePercentage,
|
||||||
|
recordModalClosing,
|
||||||
|
recordStreakCelebration,
|
||||||
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user