feat: added Advanced settings page (#521)
Co-authored-by: sendr <sendr84@gmail.com> Co-authored-by: ruzniaievdm <ruzniaievdm@gmail.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
NODE_ENV='development'
|
||||
ACCESS_TOKEN_COOKIE_NAME='edx-jwt-cookie-header-payload'
|
||||
BASE_URL='localhost:2001'
|
||||
BASE_URL='http://localhost:2001'
|
||||
CREDENTIALS_BASE_URL='http://localhost:18150'
|
||||
CSRF_TOKEN_API_PATH='/csrf/api/v1/token'
|
||||
DISCOVERY_API_BASE_URL=
|
||||
@@ -40,7 +40,7 @@ ENABLE_NEW_VIDEO_UPLOAD_PAGE = false
|
||||
ENABLE_NEW_SCHEDULE_DETAILS_PAGE = false
|
||||
ENABLE_NEW_GRADING_PAGE = false
|
||||
ENABLE_NEW_COURSE_TEAM_PAGE = false
|
||||
ENABLE_NEW_ADVANCED_SETTINGS_PAGE = false
|
||||
ENABLE_NEW_ADVANCED_SETTINGS_PAGE = true
|
||||
ENABLE_NEW_IMPORT_PAGE = false
|
||||
ENABLE_NEW_EXPORT_PAGE = false
|
||||
ENABLE_UNIT_PAGE = false
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
ACCESS_TOKEN_COOKIE_NAME='edx-jwt-cookie-header-payload'
|
||||
BASE_URL='localhost:2001'
|
||||
BASE_URL='http://localhost:2001'
|
||||
CREDENTIALS_BASE_URL='http://localhost:18150'
|
||||
CSRF_TOKEN_API_PATH='/csrf/api/v1/token'
|
||||
DISCOVERY_API_BASE_URL='http://localhost:18381'
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
const { createConfig } = require('@edx/frontend-build');
|
||||
|
||||
module.exports = createConfig(
|
||||
'eslint',
|
||||
'eslint',
|
||||
{
|
||||
rules: {
|
||||
'jsx-a11y/label-has-associated-control': [2, {
|
||||
@@ -10,7 +10,7 @@ module.exports = createConfig(
|
||||
}],
|
||||
'template-curly-spacing': 'off',
|
||||
'react-hooks/exhaustive-deps': 'off',
|
||||
indent: 'off',
|
||||
indent: ['error', 2],
|
||||
'no-restricted-exports': 'off',
|
||||
},
|
||||
},
|
||||
|
||||
34
.stylelintrc.json
Normal file
34
.stylelintrc.json
Normal file
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"extends": ["@edx/stylelint-config-edx"],
|
||||
"rules": {
|
||||
"selector-pseudo-class-no-unknown": [true, {
|
||||
"ignorePseudoClasses": ["export"]
|
||||
}],
|
||||
"unit-no-unknown": [true, {
|
||||
"ignoreUnits": ["\\.5"]
|
||||
}],
|
||||
"property-no-vendor-prefix": [true, {
|
||||
"ignoreProperties": ["animation", "filter"]
|
||||
}],
|
||||
"value-no-vendor-prefix": [true, {
|
||||
"ignoreValues": ["fill-available"]
|
||||
}],
|
||||
"function-no-unknown": null,
|
||||
"number-leading-zero": "never",
|
||||
"no-descending-specificity": null,
|
||||
"selector-class-pattern": null,
|
||||
"scss/no-global-function-names": null,
|
||||
"color-hex-case": "upper",
|
||||
"color-hex-length": "long",
|
||||
"scss/dollar-variable-empty-line-before": null,
|
||||
"scss/dollar-variable-colon-space-after": "at-least-one-space",
|
||||
"at-rule-no-unknown": null,
|
||||
"scss/at-rule-no-unknown": true,
|
||||
"scss/at-import-partial-extension": null,
|
||||
"scss/comment-no-empty": null,
|
||||
"property-no-unknown": [true, {
|
||||
"ignoreProperties": ["xs", "sm", "md", "lg", "xl", "xxl"]
|
||||
}],
|
||||
"alpha-value-notation": "number"
|
||||
}
|
||||
}
|
||||
722
package-lock.json
generated
722
package-lock.json
generated
@@ -35,6 +35,7 @@
|
||||
"react-responsive": "8.1.0",
|
||||
"react-router": "5.2.0",
|
||||
"react-router-dom": "5.2.0",
|
||||
"react-textarea-autosize": "^8.4.1",
|
||||
"react-transition-group": "4.4.1",
|
||||
"redux": "4.0.5",
|
||||
"regenerator-runtime": "0.13.7",
|
||||
@@ -45,6 +46,7 @@
|
||||
"@edx/browserslist-config": "1.0.0",
|
||||
"@edx/frontend-build": "12.8.6",
|
||||
"@edx/reactifex": "^1.0.3",
|
||||
"@edx/stylelint-config-edx": "^2.3.0",
|
||||
"@testing-library/jest-dom": "5.16.4",
|
||||
"@testing-library/react": "12.1.1",
|
||||
"@testing-library/user-event": "^13.2.1",
|
||||
@@ -2026,6 +2028,22 @@
|
||||
"@csstools/css-tokenizer": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@csstools/selector-specificity": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-2.2.0.tgz",
|
||||
"integrity": "sha512-+OJ9konv95ClSTOJCmMZqpd5+YGsB2S+x6w3E1oaM8UuR5j8nTNHYSz8c9BEPGDOCMQYIEEGlVPj/VY64iTbGw==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": "^14 || ^16 || >=18"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/csstools"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"postcss-selector-parser": "^6.0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/@discoveryjs/json-ext": {
|
||||
"version": "0.5.7",
|
||||
"dev": true,
|
||||
@@ -2671,6 +2689,18 @@
|
||||
"edx_reactifex": "main.js"
|
||||
}
|
||||
},
|
||||
"node_modules/@edx/stylelint-config-edx": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@edx/stylelint-config-edx/-/stylelint-config-edx-2.3.0.tgz",
|
||||
"integrity": "sha512-JWOIHJmTm7JWWln6+aT2v7XCLuFZJ2cBDJrT6CgBDVSLaQbBJUN67DP5QO2rP9Z7fVKXlgZ0iJnP6IlniXIU2A==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"stylelint": "^14.5.0",
|
||||
"stylelint-config-recommended-scss": "^5.0.2",
|
||||
"stylelint-config-standard": "^25.0.0",
|
||||
"stylelint-scss": "^4.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/eslintrc": {
|
||||
"version": "1.4.1",
|
||||
"dev": true,
|
||||
@@ -4834,6 +4864,12 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/minimist": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.2.tgz",
|
||||
"integrity": "sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "18.0.0",
|
||||
"dev": true,
|
||||
@@ -5750,6 +5786,15 @@
|
||||
"get-intrinsic": "^1.1.3"
|
||||
}
|
||||
},
|
||||
"node_modules/arrify": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz",
|
||||
"integrity": "sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/assert-ok": {
|
||||
"version": "1.0.0",
|
||||
"license": "MIT"
|
||||
@@ -5767,6 +5812,15 @@
|
||||
"dev": true,
|
||||
"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==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/asynckit": {
|
||||
"version": "0.4.0",
|
||||
"license": "MIT"
|
||||
@@ -6610,6 +6664,32 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/camelcase-keys": {
|
||||
"version": "6.2.2",
|
||||
"resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-6.2.2.tgz",
|
||||
"integrity": "sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"camelcase": "^5.3.1",
|
||||
"map-obj": "^4.0.0",
|
||||
"quick-lru": "^4.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/camelcase-keys/node_modules/camelcase": {
|
||||
"version": "5.3.1",
|
||||
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
|
||||
"integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/caniuse-api": {
|
||||
"version": "3.0.0",
|
||||
"dev": true,
|
||||
@@ -7315,6 +7395,15 @@
|
||||
"postcss": "^8.0.9"
|
||||
}
|
||||
},
|
||||
"node_modules/css-functions-list": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/css-functions-list/-/css-functions-list-3.1.0.tgz",
|
||||
"integrity": "sha512-/9lCvYZaUbBGvYUgYGFJ4dcYiyqdhSjG7IPVluoV8A1ILjkF7ilmhp1OGUz8n+nmBcu0RNrQAzgD8B6FJbrt2w==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=12.22"
|
||||
}
|
||||
},
|
||||
"node_modules/css-loader": {
|
||||
"version": "5.2.7",
|
||||
"dev": true,
|
||||
@@ -7597,6 +7686,31 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/decamelize-keys": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/decamelize-keys/-/decamelize-keys-1.1.1.tgz",
|
||||
"integrity": "sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"decamelize": "^1.1.0",
|
||||
"map-obj": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/decamelize-keys/node_modules/map-obj": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz",
|
||||
"integrity": "sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/decimal.js": {
|
||||
"version": "10.4.3",
|
||||
"dev": true,
|
||||
@@ -10519,6 +10633,12 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/globjoin": {
|
||||
"version": "0.1.4",
|
||||
"resolved": "https://registry.npmjs.org/globjoin/-/globjoin-0.1.4.tgz",
|
||||
"integrity": "sha512-xYfnw62CKG8nLkZBfWbhWwDw02CHty86jfPcc2cr3ZfeuK9ysoVPPEUxf21bAD/rWAgk52SuBrLJlefNy8mvFg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/gopd": {
|
||||
"version": "1.0.1",
|
||||
"dev": true,
|
||||
@@ -10565,6 +10685,15 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/hard-rejection": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/hard-rejection/-/hard-rejection-2.1.0.tgz",
|
||||
"integrity": "sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/harmony-reflect": {
|
||||
"version": "1.6.2",
|
||||
"dev": true,
|
||||
@@ -10818,6 +10947,18 @@
|
||||
"node": ">= 12"
|
||||
}
|
||||
},
|
||||
"node_modules/html-tags": {
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/html-tags/-/html-tags-3.3.1.tgz",
|
||||
"integrity": "sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/html-webpack-plugin": {
|
||||
"version": "5.5.0",
|
||||
"dev": true,
|
||||
@@ -11229,6 +11370,15 @@
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/import-lazy": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-4.0.0.tgz",
|
||||
"integrity": "sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/import-local": {
|
||||
"version": "3.1.0",
|
||||
"dev": true,
|
||||
@@ -14297,6 +14447,12 @@
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/known-css-properties": {
|
||||
"version": "0.26.0",
|
||||
"resolved": "https://registry.npmjs.org/known-css-properties/-/known-css-properties-0.26.0.tgz",
|
||||
"integrity": "sha512-5FZRzrZzNTBruuurWpvZnvP9pum+fe0HcK8z/ooo+U+Hmp4vtbyp1/QDsqmufirXy4egGzbaH/y2uCZf+6W5Kg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/language-subtag-registry": {
|
||||
"version": "0.3.22",
|
||||
"dev": true,
|
||||
@@ -14459,6 +14615,12 @@
|
||||
"version": "4.1.1",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.truncate": {
|
||||
"version": "4.4.2",
|
||||
"resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz",
|
||||
"integrity": "sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/lodash.uniq": {
|
||||
"version": "4.5.0",
|
||||
"dev": true,
|
||||
@@ -14560,6 +14722,18 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/map-obj": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.3.0.tgz",
|
||||
"integrity": "sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/map-visit": {
|
||||
"version": "1.0.0",
|
||||
"dev": true,
|
||||
@@ -14578,6 +14752,16 @@
|
||||
"css-mediaquery": "^0.1.2"
|
||||
}
|
||||
},
|
||||
"node_modules/mathml-tag-names": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/mathml-tag-names/-/mathml-tag-names-2.1.3.tgz",
|
||||
"integrity": "sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg==",
|
||||
"dev": true,
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/mdn-data": {
|
||||
"version": "2.0.14",
|
||||
"dev": true,
|
||||
@@ -14602,6 +14786,113 @@
|
||||
"node": ">= 4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/meow": {
|
||||
"version": "9.0.0",
|
||||
"resolved": "https://registry.npmjs.org/meow/-/meow-9.0.0.tgz",
|
||||
"integrity": "sha512-+obSblOQmRhcyBt62furQqRAQpNyWXo8BuQ5bN7dG8wmwQ+vwHKp/rCFD4CrTP8CsDQD1sjoZ94K417XEUk8IQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/minimist": "^1.2.0",
|
||||
"camelcase-keys": "^6.2.2",
|
||||
"decamelize": "^1.2.0",
|
||||
"decamelize-keys": "^1.1.0",
|
||||
"hard-rejection": "^2.1.0",
|
||||
"minimist-options": "4.1.0",
|
||||
"normalize-package-data": "^3.0.0",
|
||||
"read-pkg-up": "^7.0.1",
|
||||
"redent": "^3.0.0",
|
||||
"trim-newlines": "^3.0.0",
|
||||
"type-fest": "^0.18.0",
|
||||
"yargs-parser": "^20.2.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/meow/node_modules/hosted-git-info": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz",
|
||||
"integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"lru-cache": "^6.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/meow/node_modules/lru-cache": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
|
||||
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"yallist": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/meow/node_modules/normalize-package-data": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-3.0.3.tgz",
|
||||
"integrity": "sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"hosted-git-info": "^4.0.1",
|
||||
"is-core-module": "^2.5.0",
|
||||
"semver": "^7.3.4",
|
||||
"validate-npm-package-license": "^3.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/meow/node_modules/semver": {
|
||||
"version": "7.5.1",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.5.1.tgz",
|
||||
"integrity": "sha512-Wvss5ivl8TMRZXXESstBA4uR5iXgEN/VC5/sOcuXdVLzcdkz4HWetIoRfG5gb5X+ij/G9rw9YoGn3QoQ8OCSpw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"lru-cache": "^6.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/meow/node_modules/type-fest": {
|
||||
"version": "0.18.1",
|
||||
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.18.1.tgz",
|
||||
"integrity": "sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/meow/node_modules/yallist": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/meow/node_modules/yargs-parser": {
|
||||
"version": "20.2.9",
|
||||
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz",
|
||||
"integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/merge-descriptors": {
|
||||
"version": "1.0.1",
|
||||
"dev": true,
|
||||
@@ -14744,6 +15035,29 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/minimist-options": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/minimist-options/-/minimist-options-4.1.0.tgz",
|
||||
"integrity": "sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"arrify": "^1.0.1",
|
||||
"is-plain-obj": "^1.1.0",
|
||||
"kind-of": "^6.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/minimist-options/node_modules/is-plain-obj": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz",
|
||||
"integrity": "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/minipass": {
|
||||
"version": "3.3.4",
|
||||
"dev": true,
|
||||
@@ -16194,6 +16508,12 @@
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/postcss-media-query-parser": {
|
||||
"version": "0.2.3",
|
||||
"resolved": "https://registry.npmjs.org/postcss-media-query-parser/-/postcss-media-query-parser-0.2.3.tgz",
|
||||
"integrity": "sha512-3sOlxmbKcSHMjlUXQZKQ06jOswE7oVkXPxmZdoB1r5l0q6gTFTQSHxNxOrCccElbW7dxNytifNEo8qidX2Vsig==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/postcss-merge-longhand": {
|
||||
"version": "5.1.7",
|
||||
"dev": true,
|
||||
@@ -16510,6 +16830,12 @@
|
||||
"postcss": "^8.2.15"
|
||||
}
|
||||
},
|
||||
"node_modules/postcss-resolve-nested-selector": {
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/postcss-resolve-nested-selector/-/postcss-resolve-nested-selector-0.1.1.tgz",
|
||||
"integrity": "sha512-HvExULSwLqHLgUy1rl3ANIqCsvMS0WHss2UOsXhXnQaZ9VCc2oBvIpXrl00IUFT5ZDITME0o6oiXeiHr2SAIfw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/postcss-rtlcss": {
|
||||
"version": "3.7.2",
|
||||
"dev": true,
|
||||
@@ -16524,6 +16850,44 @@
|
||||
"postcss": "^8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/postcss-safe-parser": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/postcss-safe-parser/-/postcss-safe-parser-6.0.0.tgz",
|
||||
"integrity": "sha512-FARHN8pwH+WiS2OPCxJI8FuRJpTVnn6ZNFiqAM2aeW2LwTHWWmWgIyKC6cUo0L8aeKiF/14MNvnpls6R2PBeMQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=12.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/postcss/"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"postcss": "^8.3.3"
|
||||
}
|
||||
},
|
||||
"node_modules/postcss-scss": {
|
||||
"version": "4.0.6",
|
||||
"resolved": "https://registry.npmjs.org/postcss-scss/-/postcss-scss-4.0.6.tgz",
|
||||
"integrity": "sha512-rLDPhJY4z/i4nVFZ27j9GqLxj1pwxE80eAzUNRMXtcpipFYIeowerzBgG3yJhMtObGEXidtIgbUpQ3eLDsf5OQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/postcss/"
|
||||
},
|
||||
{
|
||||
"type": "tidelift",
|
||||
"url": "https://tidelift.com/funding/github/npm/postcss-scss"
|
||||
}
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"postcss": "^8.4.19"
|
||||
}
|
||||
},
|
||||
"node_modules/postcss-selector-parser": {
|
||||
"version": "6.0.11",
|
||||
"dev": true,
|
||||
@@ -16889,6 +17253,15 @@
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/quick-lru": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-4.0.1.tgz",
|
||||
"integrity": "sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/raf": {
|
||||
"version": "3.4.1",
|
||||
"dev": true,
|
||||
@@ -17651,6 +18024,22 @@
|
||||
"object-assign": "^4.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/react-textarea-autosize": {
|
||||
"version": "8.4.1",
|
||||
"resolved": "https://registry.npmjs.org/react-textarea-autosize/-/react-textarea-autosize-8.4.1.tgz",
|
||||
"integrity": "sha512-aD2C+qK6QypknC+lCMzteOdIjoMbNlgSFmJjCV+DrfTPwp59i/it9mMNf2HDzvRjQgKAyBDPyLJhcrzElf2U4Q==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.20.13",
|
||||
"use-composed-ref": "^1.3.0",
|
||||
"use-latest": "^1.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-transition-group": {
|
||||
"version": "4.4.1",
|
||||
"license": "BSD-3-Clause",
|
||||
@@ -19041,6 +19430,56 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"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==",
|
||||
"dev": true,
|
||||
"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/slice-ansi/node_modules/ansi-styles": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"color-convert": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/slice-ansi/node_modules/color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"color-name": "~1.1.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/slice-ansi/node_modules/color-name": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/snapdragon": {
|
||||
"version": "0.8.2",
|
||||
"dev": true,
|
||||
@@ -19733,6 +20172,12 @@
|
||||
"version": "4.0.0",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/style-search": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/style-search/-/style-search-0.1.0.tgz",
|
||||
"integrity": "sha512-Dj1Okke1C3uKKwQcetra4jSuk0DqbzbYtXipzFlFMZtowbF1x7BKJwB9AayVMyFARvU8EDrZdcax4At/452cAg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/stylehacks": {
|
||||
"version": "5.1.1",
|
||||
"dev": true,
|
||||
@@ -19748,6 +20193,187 @@
|
||||
"postcss": "^8.2.15"
|
||||
}
|
||||
},
|
||||
"node_modules/stylelint": {
|
||||
"version": "14.16.1",
|
||||
"resolved": "https://registry.npmjs.org/stylelint/-/stylelint-14.16.1.tgz",
|
||||
"integrity": "sha512-ErlzR/T3hhbV+a925/gbfc3f3Fep9/bnspMiJPorfGEmcBbXdS+oo6LrVtoUZ/w9fqD6o6k7PtUlCOsCRdjX/A==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@csstools/selector-specificity": "^2.0.2",
|
||||
"balanced-match": "^2.0.0",
|
||||
"colord": "^2.9.3",
|
||||
"cosmiconfig": "^7.1.0",
|
||||
"css-functions-list": "^3.1.0",
|
||||
"debug": "^4.3.4",
|
||||
"fast-glob": "^3.2.12",
|
||||
"fastest-levenshtein": "^1.0.16",
|
||||
"file-entry-cache": "^6.0.1",
|
||||
"global-modules": "^2.0.0",
|
||||
"globby": "^11.1.0",
|
||||
"globjoin": "^0.1.4",
|
||||
"html-tags": "^3.2.0",
|
||||
"ignore": "^5.2.1",
|
||||
"import-lazy": "^4.0.0",
|
||||
"imurmurhash": "^0.1.4",
|
||||
"is-plain-object": "^5.0.0",
|
||||
"known-css-properties": "^0.26.0",
|
||||
"mathml-tag-names": "^2.1.3",
|
||||
"meow": "^9.0.0",
|
||||
"micromatch": "^4.0.5",
|
||||
"normalize-path": "^3.0.0",
|
||||
"picocolors": "^1.0.0",
|
||||
"postcss": "^8.4.19",
|
||||
"postcss-media-query-parser": "^0.2.3",
|
||||
"postcss-resolve-nested-selector": "^0.1.1",
|
||||
"postcss-safe-parser": "^6.0.0",
|
||||
"postcss-selector-parser": "^6.0.11",
|
||||
"postcss-value-parser": "^4.2.0",
|
||||
"resolve-from": "^5.0.0",
|
||||
"string-width": "^4.2.3",
|
||||
"strip-ansi": "^6.0.1",
|
||||
"style-search": "^0.1.0",
|
||||
"supports-hyperlinks": "^2.3.0",
|
||||
"svg-tags": "^1.0.0",
|
||||
"table": "^6.8.1",
|
||||
"v8-compile-cache": "^2.3.0",
|
||||
"write-file-atomic": "^4.0.2"
|
||||
},
|
||||
"bin": {
|
||||
"stylelint": "bin/stylelint.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/stylelint"
|
||||
}
|
||||
},
|
||||
"node_modules/stylelint-config-recommended": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/stylelint-config-recommended/-/stylelint-config-recommended-6.0.0.tgz",
|
||||
"integrity": "sha512-ZorSSdyMcxWpROYUvLEMm0vSZud2uB7tX1hzBZwvVY9SV/uly4AvvJPPhCcymZL3fcQhEQG5AELmrxWqtmzacw==",
|
||||
"dev": true,
|
||||
"peerDependencies": {
|
||||
"stylelint": "^14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/stylelint-config-recommended-scss": {
|
||||
"version": "5.0.2",
|
||||
"resolved": "https://registry.npmjs.org/stylelint-config-recommended-scss/-/stylelint-config-recommended-scss-5.0.2.tgz",
|
||||
"integrity": "sha512-b14BSZjcwW0hqbzm9b0S/ScN2+3CO3O4vcMNOw2KGf8lfVSwJ4p5TbNEXKwKl1+0FMtgRXZj6DqVUe/7nGnuBg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"postcss-scss": "^4.0.2",
|
||||
"stylelint-config-recommended": "^6.0.0",
|
||||
"stylelint-scss": "^4.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"stylelint": "^14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/stylelint-config-standard": {
|
||||
"version": "25.0.0",
|
||||
"resolved": "https://registry.npmjs.org/stylelint-config-standard/-/stylelint-config-standard-25.0.0.tgz",
|
||||
"integrity": "sha512-21HnP3VSpaT1wFjFvv9VjvOGDtAviv47uTp3uFmzcN+3Lt+RYRv6oAplLaV51Kf792JSxJ6svCJh/G18E9VnCA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"stylelint-config-recommended": "^7.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"stylelint": "^14.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/stylelint-config-standard/node_modules/stylelint-config-recommended": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/stylelint-config-recommended/-/stylelint-config-recommended-7.0.0.tgz",
|
||||
"integrity": "sha512-yGn84Bf/q41J4luis1AZ95gj0EQwRX8lWmGmBwkwBNSkpGSpl66XcPTulxGa/Z91aPoNGuIGBmFkcM1MejMo9Q==",
|
||||
"dev": true,
|
||||
"peerDependencies": {
|
||||
"stylelint": "^14.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/stylelint-scss": {
|
||||
"version": "4.7.0",
|
||||
"resolved": "https://registry.npmjs.org/stylelint-scss/-/stylelint-scss-4.7.0.tgz",
|
||||
"integrity": "sha512-TSUgIeS0H3jqDZnby1UO1Qv3poi1N8wUYIJY6D1tuUq2MN3lwp/rITVo0wD+1SWTmRm0tNmGO0b7nKInnqF6Hg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"postcss-media-query-parser": "^0.2.3",
|
||||
"postcss-resolve-nested-selector": "^0.1.1",
|
||||
"postcss-selector-parser": "^6.0.11",
|
||||
"postcss-value-parser": "^4.2.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"stylelint": "^14.5.1 || ^15.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/stylelint/node_modules/array-union": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz",
|
||||
"integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/stylelint/node_modules/balanced-match": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-2.0.0.tgz",
|
||||
"integrity": "sha512-1ugUSr8BHXRnK23KfuYS+gVMC3LB8QGH9W1iGtDPsNWoQbgtXSExkBu2aDR4epiGWZOjZsj6lDl/N/AqqTC3UA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/stylelint/node_modules/globby": {
|
||||
"version": "11.1.0",
|
||||
"resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz",
|
||||
"integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"array-union": "^2.1.0",
|
||||
"dir-glob": "^3.0.1",
|
||||
"fast-glob": "^3.2.9",
|
||||
"ignore": "^5.2.0",
|
||||
"merge2": "^1.4.1",
|
||||
"slash": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/stylelint/node_modules/is-plain-object": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz",
|
||||
"integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/stylelint/node_modules/slash": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
|
||||
"integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/stylelint/node_modules/write-file-atomic": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz",
|
||||
"integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"imurmurhash": "^0.1.4",
|
||||
"signal-exit": "^3.0.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^12.13.0 || ^14.15.0 || >=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/superagent": {
|
||||
"version": "3.8.3",
|
||||
"dev": true,
|
||||
@@ -19847,6 +20473,12 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/svg-tags": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/svg-tags/-/svg-tags-1.0.0.tgz",
|
||||
"integrity": "sha512-ovssysQTa+luh7A5Weu3Rta6FJlFBBbInjOh722LIt6klpU2/HtdUbszju/G4devcvk8PGt7FCLv5wftu3THUA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/svgo": {
|
||||
"version": "2.8.0",
|
||||
"dev": true,
|
||||
@@ -19954,6 +20586,44 @@
|
||||
"version": "5.3.3",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/table": {
|
||||
"version": "6.8.1",
|
||||
"resolved": "https://registry.npmjs.org/table/-/table-6.8.1.tgz",
|
||||
"integrity": "sha512-Y4X9zqrCftUhMeH2EptSSERdVKt/nEdijTOacGD/97EKjhQ/Qs8RTlEGABSJNNN8lac9kheH+af7yAkEWlgneA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"ajv": "^8.0.1",
|
||||
"lodash.truncate": "^4.4.2",
|
||||
"slice-ansi": "^4.0.0",
|
||||
"string-width": "^4.2.3",
|
||||
"strip-ansi": "^6.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/table/node_modules/ajv": {
|
||||
"version": "8.12.0",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz",
|
||||
"integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.1",
|
||||
"json-schema-traverse": "^1.0.0",
|
||||
"require-from-string": "^2.0.2",
|
||||
"uri-js": "^4.2.2"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/epoberezkin"
|
||||
}
|
||||
},
|
||||
"node_modules/table/node_modules/json-schema-traverse": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
|
||||
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/tapable": {
|
||||
"version": "2.2.1",
|
||||
"dev": true,
|
||||
@@ -20307,6 +20977,15 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/trim-newlines": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-3.0.1.tgz",
|
||||
"integrity": "sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/tsconfig-paths": {
|
||||
"version": "3.14.2",
|
||||
"dev": true,
|
||||
@@ -20689,6 +21368,43 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/use-composed-ref": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/use-composed-ref/-/use-composed-ref-1.3.0.tgz",
|
||||
"integrity": "sha512-GLMG0Jc/jiKov/3Ulid1wbv3r54K9HlMW29IWcDFPEqFkSO2nS0MuefWgMJpeHQ9YJeXDL3ZUF+P3jdXlZX/cQ==",
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/use-isomorphic-layout-effect": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.2.tgz",
|
||||
"integrity": "sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA==",
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/use-latest": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/use-latest/-/use-latest-1.2.1.tgz",
|
||||
"integrity": "sha512-xA+AVm/Wlg3e2P/JiItTziwS7FK92LWrDB0p+hgXloIMuVCeJJ8v6f0eeHyPZaJrM+usM1FkFfbNCrJGs8A/zw==",
|
||||
"dependencies": {
|
||||
"use-isomorphic-layout-effect": "^1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/use-sidecar": {
|
||||
"version": "1.1.2",
|
||||
"license": "MIT",
|
||||
@@ -20734,6 +21450,12 @@
|
||||
"uuid": "bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/v8-compile-cache": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz",
|
||||
"integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/v8-to-istanbul": {
|
||||
"version": "7.1.2",
|
||||
"dev": true,
|
||||
|
||||
@@ -12,8 +12,9 @@
|
||||
"scripts": {
|
||||
"build": "fedx-scripts webpack",
|
||||
"i18n_extract": "BABEL_ENV=i18n fedx-scripts babel src --quiet > /dev/null",
|
||||
"lint": "fedx-scripts eslint --ext .js --ext .jsx .",
|
||||
"lint:fix": "fedx-scripts eslint --ext .js --ext .jsx . --fix",
|
||||
"stylelint": "stylelint \"src/**/*.scss\" \"scss/**/*.scss\" --config .stylelintrc.json",
|
||||
"lint": "npm run stylelint && fedx-scripts eslint --ext .js --ext .jsx .",
|
||||
"lint:fix": "npm run stylelint && fedx-scripts eslint --ext .js --ext .jsx . --fix",
|
||||
"snapshot": "fedx-scripts jest --updateSnapshot",
|
||||
"start": "fedx-scripts webpack-dev-server --progress",
|
||||
"test": "fedx-scripts jest --coverage --passWithNoTests"
|
||||
@@ -59,6 +60,7 @@
|
||||
"react-responsive": "8.1.0",
|
||||
"react-router": "5.2.0",
|
||||
"react-router-dom": "5.2.0",
|
||||
"react-textarea-autosize": "^8.4.1",
|
||||
"react-transition-group": "4.4.1",
|
||||
"redux": "4.0.5",
|
||||
"regenerator-runtime": "0.13.7",
|
||||
@@ -69,6 +71,7 @@
|
||||
"@edx/browserslist-config": "1.0.0",
|
||||
"@edx/frontend-build": "12.8.6",
|
||||
"@edx/reactifex": "^1.0.3",
|
||||
"@edx/stylelint-config-edx": "^2.3.0",
|
||||
"@testing-library/jest-dom": "5.16.4",
|
||||
"@testing-library/react": "12.1.1",
|
||||
"@testing-library/user-event": "^13.2.1",
|
||||
|
||||
@@ -76,7 +76,7 @@ const CourseAuthoringPage = ({ courseId, children }) => {
|
||||
}
|
||||
return (
|
||||
<div className={pathname.includes('/editor/') ? '' : 'bg-light-200'}>
|
||||
{/* While V2 Editors are tempoarily served from thier own pages
|
||||
{/* While V2 Editors are temporarily served from their own pages
|
||||
using url pattern containing /editor/,
|
||||
we shouldn't have the header and footer on these pages.
|
||||
This functionality will be removed in TNL-9591 */}
|
||||
@@ -89,7 +89,7 @@ const CourseAuthoringPage = ({ courseId, children }) => {
|
||||
courseId={courseId}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
)}
|
||||
{children}
|
||||
{!inProgress && showHeader && <AppFooter />}
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
|
||||
import { queryByTestId, render } from '@testing-library/react';
|
||||
import { render } from '@testing-library/react';
|
||||
|
||||
import { getConfig, initializeMockApp } from '@edx/frontend-platform';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
@@ -23,51 +23,6 @@ jest.mock('react-router-dom', () => ({
|
||||
}));
|
||||
let axiosMock;
|
||||
let store;
|
||||
let container;
|
||||
function renderComponent() {
|
||||
const wrapper = render(
|
||||
<AppProvider store={store}>
|
||||
<IntlProvider locale="en">
|
||||
<CourseAuthoringPage courseId={courseId}>
|
||||
<PagesAndResources courseId={courseId} />
|
||||
</CourseAuthoringPage>
|
||||
</IntlProvider>
|
||||
</AppProvider>
|
||||
,
|
||||
);
|
||||
container = wrapper.container;
|
||||
}
|
||||
|
||||
const mockStore = async () => {
|
||||
const apiBaseUrl = getConfig().STUDIO_BASE_URL;
|
||||
const courseAppsApiUrl = `${apiBaseUrl}/api/course_apps/v1/apps`;
|
||||
axiosMock.onGet(`${courseAppsApiUrl}/${courseId}`).reply(403, {
|
||||
response: { status: 403 },
|
||||
});
|
||||
|
||||
await executeThunk(fetchCourseApps(courseId), store.dispatch);
|
||||
};
|
||||
describe('DiscussionsSettings', () => {
|
||||
beforeEach(() => {
|
||||
initializeMockApp({
|
||||
authenticatedUser: {
|
||||
userId: 3,
|
||||
username: 'abc123',
|
||||
administrator: true,
|
||||
roles: [],
|
||||
},
|
||||
});
|
||||
|
||||
store = initializeStore();
|
||||
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
|
||||
});
|
||||
|
||||
test('renders permission error in case of 403', async () => {
|
||||
await mockStore();
|
||||
renderComponent();
|
||||
expect(queryByTestId(container, 'permissionDeniedAlert')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Editor Pages Load no header', () => {
|
||||
const mockStoreSuccess = async () => {
|
||||
|
||||
@@ -9,6 +9,7 @@ import ProctoredExamSettings from './proctored-exam-settings/ProctoredExamSettin
|
||||
import EditorContainer from './editors/EditorContainer';
|
||||
import VideoSelectorContainer from './selectors/VideoSelectorContainer';
|
||||
import CustomPages from './custom-pages';
|
||||
import { AdvancedSettings } from './advanced-settings';
|
||||
|
||||
/**
|
||||
* As of this writing, these routes are mounted at a path prefixed with the following:
|
||||
@@ -34,25 +35,25 @@ const CourseAuthoringRoutes = ({ courseId }) => {
|
||||
<PageRoute path={`${path}/outline`}>
|
||||
{process.env.ENABLE_NEW_COURSE_OUTLINE_PAGE === 'true'
|
||||
&& (
|
||||
<Placeholder />
|
||||
<Placeholder />
|
||||
)}
|
||||
</PageRoute>
|
||||
<PageRoute path={`${path}/course_info`}>
|
||||
{process.env.ENABLE_NEW_UPDATES_PAGE === 'true'
|
||||
&& (
|
||||
<Placeholder />
|
||||
<Placeholder />
|
||||
)}
|
||||
</PageRoute>
|
||||
<PageRoute path={`${path}/assets`}>
|
||||
{process.env.ENABLE_NEW_FILES_UPLOADS_PAGE === 'true'
|
||||
&& (
|
||||
<Placeholder />
|
||||
<Placeholder />
|
||||
)}
|
||||
</PageRoute>
|
||||
<PageRoute path={`${path}/videos`}>
|
||||
{process.env.ENABLE_NEW_VIDEO_UPLOAD_PAGE === 'true'
|
||||
&& (
|
||||
<Placeholder />
|
||||
<Placeholder />
|
||||
)}
|
||||
</PageRoute>
|
||||
<PageRoute path={`${path}/pages-and-resources`}>
|
||||
@@ -64,65 +65,65 @@ const CourseAuthoringRoutes = ({ courseId }) => {
|
||||
<PageRoute path={`${path}/custom-pages`}>
|
||||
{process.env.ENABLE_NEW_CUSTOM_PAGES === 'true'
|
||||
&& (
|
||||
<CustomPages courseId={courseId} />
|
||||
<CustomPages courseId={courseId} />
|
||||
)}
|
||||
</PageRoute>
|
||||
<PageRoute path={`${path}/container/:blockId`}>
|
||||
{process.env.ENABLE_UNIT_PAGE === 'true'
|
||||
&& (
|
||||
<Placeholder />
|
||||
<Placeholder />
|
||||
)}
|
||||
</PageRoute>
|
||||
<PageRoute path={`${path}/editor/course-videos/:blockId`}>
|
||||
{process.env.ENABLE_NEW_EDITOR_PAGES === 'true'
|
||||
&& (
|
||||
<VideoSelectorContainer
|
||||
courseId={courseId}
|
||||
/>
|
||||
<VideoSelectorContainer
|
||||
courseId={courseId}
|
||||
/>
|
||||
)}
|
||||
</PageRoute>
|
||||
<PageRoute path={`${path}/editor/:blockType/:blockId?`}>
|
||||
{process.env.ENABLE_NEW_EDITOR_PAGES === 'true'
|
||||
&& (
|
||||
<EditorContainer
|
||||
courseId={courseId}
|
||||
/>
|
||||
<EditorContainer
|
||||
courseId={courseId}
|
||||
/>
|
||||
)}
|
||||
</PageRoute>
|
||||
<PageRoute path={`${path}/settings/details`}>
|
||||
{process.env.ENABLE_NEW_SCHEDULE_DETAILS_PAGE === 'true'
|
||||
&& (
|
||||
<Placeholder />
|
||||
<Placeholder />
|
||||
)}
|
||||
</PageRoute>
|
||||
<PageRoute path={`${path}/settings/grading`}>
|
||||
{process.env.ENABLE_NEW_GRADING_PAGE === 'true'
|
||||
&& (
|
||||
<Placeholder />
|
||||
<Placeholder />
|
||||
)}
|
||||
</PageRoute>
|
||||
<PageRoute path={`${path}/course_team`}>
|
||||
{process.env.ENABLE_NEW_COURSE_TEAM_PAGE === 'true'
|
||||
&& (
|
||||
<Placeholder />
|
||||
<Placeholder />
|
||||
)}
|
||||
</PageRoute>
|
||||
<PageRoute path={`${path}/settings/advanced`}>
|
||||
{process.env.ENABLE_NEW_ADVANCED_SETTINGS_PAGE === 'true'
|
||||
&& (
|
||||
<Placeholder />
|
||||
<AdvancedSettings courseId={courseId} />
|
||||
)}
|
||||
</PageRoute>
|
||||
<PageRoute path={`${path}/import`}>
|
||||
{process.env.ENABLE_NEW_IMPORT_PAGE === 'true'
|
||||
&& (
|
||||
<Placeholder />
|
||||
<Placeholder />
|
||||
)}
|
||||
</PageRoute>
|
||||
<PageRoute path={`${path}/export`}>
|
||||
{process.env.ENABLE_NEW_EXPORT_PAGE === 'true'
|
||||
&& (
|
||||
<Placeholder />
|
||||
<Placeholder />
|
||||
)}
|
||||
</PageRoute>
|
||||
</Switch>
|
||||
|
||||
277
src/advanced-settings/AdvancedSettings.jsx
Normal file
277
src/advanced-settings/AdvancedSettings.jsx
Normal file
@@ -0,0 +1,277 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import {
|
||||
Container, Button, Layout, StatefulButton,
|
||||
} from '@edx/paragon';
|
||||
import { CheckCircle, Info, Warning } from '@edx/paragon/icons';
|
||||
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import AlertProctoringError from '../generic/AlertProctoringError';
|
||||
import InternetConnectionAlert from '../generic/internet-connection-alert';
|
||||
import { parseArrayOrObjectValues } from '../utils';
|
||||
import { RequestStatus } from '../data/constants';
|
||||
import SubHeader from '../generic/sub-header/SubHeader';
|
||||
import AlertMessage from '../generic/alert-message';
|
||||
import { fetchCourseAppSettings, updateCourseAppSetting, fetchProctoringExamErrors } from './data/thunks';
|
||||
import {
|
||||
getCourseAppSettings, getSavingStatus, getProctoringExamErrors, getSendRequestErrors, getLoadingStatus,
|
||||
} from './data/selectors';
|
||||
import SettingCard from './setting-card/SettingCard';
|
||||
import SettingsSidebar from './settings-sidebar/SettingsSidebar';
|
||||
import validateAdvancedSettingsData from './utils';
|
||||
import messages from './messages';
|
||||
import ModalError from './modal-error/ModalError';
|
||||
|
||||
const AdvancedSettings = ({ intl, courseId }) => {
|
||||
const advancedSettingsData = useSelector(getCourseAppSettings);
|
||||
const savingStatus = useSelector(getSavingStatus);
|
||||
const proctoringExamErrors = useSelector(getProctoringExamErrors);
|
||||
const settingsWithSendErrors = useSelector(getSendRequestErrors) || {};
|
||||
const dispatch = useDispatch();
|
||||
const [saveSettingsPrompt, showSaveSettingsPrompt] = useState(false);
|
||||
const [showDeprecated, setShowDeprecated] = useState(false);
|
||||
const [errorModal, showErrorModal] = useState(false);
|
||||
const [editedSettings, setEditedSettings] = useState({});
|
||||
const [errorFields, setErrorFields] = useState([]);
|
||||
const [showSuccessAlert, setShowSuccessAlert] = useState(false);
|
||||
const loadingSettingsStatus = useSelector(getLoadingStatus);
|
||||
const [isQueryPending, setIsQueryPending] = useState(false);
|
||||
const [isEditableState, setIsEditableState] = useState(false);
|
||||
const [hasInternetConnectionError, setInternetConnectionError] = useState(false);
|
||||
const isLoading = loadingSettingsStatus === RequestStatus.IN_PROGRESS;
|
||||
const updateSettingsButtonState = {
|
||||
labels: {
|
||||
default: intl.formatMessage(messages.buttonSaveText),
|
||||
pending: intl.formatMessage(messages.buttonSavingText),
|
||||
},
|
||||
disabledStates: ['pending'],
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchCourseAppSettings(courseId));
|
||||
dispatch(fetchProctoringExamErrors(courseId));
|
||||
}, [courseId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (savingStatus === RequestStatus.SUCCESSFUL) {
|
||||
setIsQueryPending(false);
|
||||
setShowSuccessAlert(true);
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
|
||||
if (!isEditableState) {
|
||||
showSaveSettingsPrompt(false);
|
||||
}
|
||||
} else if (savingStatus === RequestStatus.FAILED && !hasInternetConnectionError) {
|
||||
setErrorFields(settingsWithSendErrors);
|
||||
showErrorModal(true);
|
||||
}
|
||||
}, [savingStatus]);
|
||||
|
||||
if (isLoading) {
|
||||
// eslint-disable-next-line react/jsx-no-useless-fragment
|
||||
return <></>;
|
||||
}
|
||||
|
||||
const handleSettingChange = (e, settingName) => {
|
||||
const { value } = e.target;
|
||||
if (!saveSettingsPrompt) {
|
||||
showSaveSettingsPrompt(true);
|
||||
}
|
||||
setIsEditableState(true);
|
||||
setShowSuccessAlert(false);
|
||||
setEditedSettings((prevEditedSettings) => ({
|
||||
...prevEditedSettings,
|
||||
[settingName]: value,
|
||||
}));
|
||||
};
|
||||
|
||||
const handleResetSettingsValues = () => {
|
||||
setIsEditableState(false);
|
||||
showErrorModal(false);
|
||||
setEditedSettings({});
|
||||
showSaveSettingsPrompt(false);
|
||||
setInternetConnectionError(false);
|
||||
setIsQueryPending(false);
|
||||
};
|
||||
|
||||
const handleSettingBlur = () => {
|
||||
validateAdvancedSettingsData(editedSettings, setErrorFields, setEditedSettings);
|
||||
};
|
||||
|
||||
const handleUpdateAdvancedSettingsData = () => {
|
||||
const isValid = validateAdvancedSettingsData(editedSettings, setErrorFields, setEditedSettings);
|
||||
if (isValid) {
|
||||
setIsQueryPending(true);
|
||||
setIsEditableState(false);
|
||||
} else {
|
||||
setIsQueryPending(false);
|
||||
showSaveSettingsPrompt(false);
|
||||
showErrorModal(!errorModal);
|
||||
}
|
||||
};
|
||||
|
||||
const handleInternetConnectionFailed = () => {
|
||||
setInternetConnectionError(true);
|
||||
showSaveSettingsPrompt(false);
|
||||
setShowSuccessAlert(false);
|
||||
setIsQueryPending(false);
|
||||
};
|
||||
|
||||
const handleQueryProcessing = () => {
|
||||
setShowSuccessAlert(false);
|
||||
dispatch(updateCourseAppSetting(courseId, parseArrayOrObjectValues(editedSettings)));
|
||||
};
|
||||
|
||||
const handleManuallyChangeClick = (setToState) => {
|
||||
setIsEditableState(true);
|
||||
showErrorModal(setToState);
|
||||
showSaveSettingsPrompt(true);
|
||||
setIsQueryPending(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Container size="xl" className="m-4">
|
||||
<div className="setting-header mt-5">
|
||||
{(proctoringExamErrors?.length > 0) && (
|
||||
<AlertProctoringError
|
||||
icon={Info}
|
||||
proctoringErrorsData={proctoringExamErrors}
|
||||
aria-hidden="true"
|
||||
aria-labelledby={intl.formatMessage(messages.alertProctoringAriaLabelledby)}
|
||||
aria-describedby={intl.formatMessage(messages.alertProctoringDescribedby)}
|
||||
/>
|
||||
)}
|
||||
<AlertMessage
|
||||
show={showSuccessAlert}
|
||||
variant="success"
|
||||
icon={CheckCircle}
|
||||
title={intl.formatMessage(messages.alertSuccess)}
|
||||
description={intl.formatMessage(messages.alertSuccessDescriptions)}
|
||||
aria-hidden="true"
|
||||
aria-labelledby={intl.formatMessage(messages.alertSuccessAriaLabelledby)}
|
||||
aria-describedby={intl.formatMessage(messages.alertSuccessAriaDescribedby)}
|
||||
/>
|
||||
</div>
|
||||
<section className="setting-items mb-4">
|
||||
<Layout
|
||||
lg={[{ span: 9 }, { span: 3 }]}
|
||||
md={[{ span: 9 }, { span: 3 }]}
|
||||
sm={[{ span: 9 }, { span: 3 }]}
|
||||
xs={[{ span: 9 }, { span: 3 }]}
|
||||
xl={[{ span: 9 }, { span: 3 }]}
|
||||
>
|
||||
<Layout.Element>
|
||||
<article>
|
||||
<div>
|
||||
<section className="setting-items-policies">
|
||||
<SubHeader
|
||||
subtitle={intl.formatMessage(messages.headingSubtitle)}
|
||||
title={intl.formatMessage(messages.headingTitle)}
|
||||
contentTitle={intl.formatMessage(messages.policy)}
|
||||
instruction={(
|
||||
<FormattedMessage
|
||||
id="course-authoring.advanced-settings.policies.description"
|
||||
defaultMessage="{notice} Do not modify these policies unless you are familiar with their purpose."
|
||||
values={{ notice: <strong>Warning: </strong> }}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<div className="setting-items-deprecated-setting">
|
||||
<Button
|
||||
variant={showDeprecated ? 'outline-brand' : 'tertiary'}
|
||||
onClick={() => setShowDeprecated(!showDeprecated)}
|
||||
size="sm"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="course-authoring.advanced-settings.deprecated.button.text"
|
||||
defaultMessage="{visibility} deprecated settings"
|
||||
values={{
|
||||
visibility:
|
||||
showDeprecated ? intl.formatMessage(messages.deprecatedButtonHideText)
|
||||
: intl.formatMessage(messages.deprecatedButtonShowText),
|
||||
}}
|
||||
/>
|
||||
</Button>
|
||||
</div>
|
||||
<ul className="setting-items-list p-0">
|
||||
{Object.keys(advancedSettingsData).sort().map((settingName) => {
|
||||
const settingData = advancedSettingsData[settingName];
|
||||
const editedValue = editedSettings[settingName] !== undefined
|
||||
? editedSettings[settingName] : JSON.stringify(settingData.value, null, 4);
|
||||
|
||||
return (
|
||||
<SettingCard
|
||||
key={settingName}
|
||||
settingData={settingData}
|
||||
onChange={(e) => handleSettingChange(e, settingName)}
|
||||
showDeprecated={showDeprecated}
|
||||
name={settingName}
|
||||
value={editedValue}
|
||||
handleBlur={handleSettingBlur}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</section>
|
||||
</div>
|
||||
</article>
|
||||
</Layout.Element>
|
||||
<Layout.Element>
|
||||
<SettingsSidebar courseId={courseId} />
|
||||
</Layout.Element>
|
||||
</Layout>
|
||||
</section>
|
||||
</Container>
|
||||
<div className="alert-toast">
|
||||
{!isEditableState && (
|
||||
<InternetConnectionAlert
|
||||
isFailed={savingStatus === RequestStatus.FAILED}
|
||||
isQueryPending={isQueryPending}
|
||||
onQueryProcessing={handleQueryProcessing}
|
||||
onInternetConnectionFailed={handleInternetConnectionFailed}
|
||||
/>
|
||||
)}
|
||||
<AlertMessage
|
||||
show={saveSettingsPrompt}
|
||||
aria-hidden={saveSettingsPrompt}
|
||||
aria-labelledby={intl.formatMessage(messages.alertWarningAriaLabelledby)}
|
||||
aria-describedby={intl.formatMessage(messages.alertWarningAriaDescribedby)}
|
||||
role="dialog"
|
||||
actions={[
|
||||
!isQueryPending && (
|
||||
<Button variant="tertiary" onClick={handleResetSettingsValues}>
|
||||
{intl.formatMessage(messages.buttonCancelText)}
|
||||
</Button>
|
||||
),
|
||||
<StatefulButton
|
||||
key="statefulBtn"
|
||||
onClick={handleUpdateAdvancedSettingsData}
|
||||
state={isQueryPending ? RequestStatus.PENDING : 'default'}
|
||||
{...updateSettingsButtonState}
|
||||
/>,
|
||||
].filter(Boolean)}
|
||||
variant="warning"
|
||||
icon={Warning}
|
||||
title={intl.formatMessage(messages.alertWarning)}
|
||||
description={intl.formatMessage(messages.alertWarningDescriptions)}
|
||||
/>
|
||||
</div>
|
||||
<ModalError
|
||||
isError={errorModal}
|
||||
showErrorModal={(setToState) => handleManuallyChangeClick(setToState)}
|
||||
handleUndoChanges={handleResetSettingsValues}
|
||||
settingsData={advancedSettingsData}
|
||||
errorList={errorFields.length > 0 ? errorFields : []}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
AdvancedSettings.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
courseId: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(AdvancedSettings);
|
||||
135
src/advanced-settings/AdvancedSettings.test.jsx
Normal file
135
src/advanced-settings/AdvancedSettings.test.jsx
Normal file
@@ -0,0 +1,135 @@
|
||||
import React from 'react';
|
||||
import { initializeMockApp } from '@edx/frontend-platform';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
import { IntlProvider, injectIntl } from '@edx/frontend-platform/i18n';
|
||||
import { AppProvider } from '@edx/frontend-platform/react';
|
||||
import { render, fireEvent, waitFor } from '@testing-library/react';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
|
||||
import initializeStore from '../store';
|
||||
import { advancedSettingsMock } from './__mocks__';
|
||||
import { getCourseAdvancedSettingsApiUrl } from './data/api';
|
||||
import AdvancedSettings from './AdvancedSettings';
|
||||
import messages from './messages';
|
||||
|
||||
let axiosMock;
|
||||
let store;
|
||||
const mockPathname = '/foo-bar';
|
||||
const courseId = '123';
|
||||
|
||||
// Mock the TextareaAutosize component
|
||||
jest.mock('react-textarea-autosize', () => jest.fn((props) => (
|
||||
<textarea
|
||||
{...props}
|
||||
onFocus={() => {}}
|
||||
onBlur={() => {}}
|
||||
/>
|
||||
)));
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useLocation: () => ({
|
||||
pathname: mockPathname,
|
||||
}),
|
||||
}));
|
||||
|
||||
const RootWrapper = () => (
|
||||
<AppProvider store={store}>
|
||||
<IntlProvider locale="en" messages={{}}>
|
||||
<AdvancedSettings intl={injectIntl} courseId={courseId} />
|
||||
</IntlProvider>
|
||||
</AppProvider>
|
||||
);
|
||||
|
||||
describe('<AdvancedSettings />', () => {
|
||||
beforeEach(() => {
|
||||
initializeMockApp({
|
||||
authenticatedUser: {
|
||||
userId: 3,
|
||||
username: 'abc123',
|
||||
administrator: true,
|
||||
roles: [],
|
||||
},
|
||||
});
|
||||
store = initializeStore();
|
||||
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
|
||||
axiosMock
|
||||
.onGet(`${getCourseAdvancedSettingsApiUrl(courseId)}?fetch_all=0`)
|
||||
.reply(200, advancedSettingsMock);
|
||||
});
|
||||
it('should render without errors', async () => {
|
||||
const { getByText } = render(<RootWrapper />);
|
||||
await waitFor(() => {
|
||||
expect(getByText(messages.headingSubtitle.defaultMessage)).toBeInTheDocument();
|
||||
const advancedSettingsElement = getByText(messages.headingTitle.defaultMessage, {
|
||||
selector: 'h2.sub-header-title',
|
||||
});
|
||||
expect(advancedSettingsElement).toBeInTheDocument();
|
||||
expect(getByText(messages.policy.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(/Do not modify these policies unless you are familiar with their purpose./i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
it('should render setting element', async () => {
|
||||
const { getByText } = render(<RootWrapper />);
|
||||
await waitFor(() => {
|
||||
const advancedModuleListTitle = getByText(/Advanced Module List/i);
|
||||
expect(advancedModuleListTitle).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
it('should change to onСhange', async () => {
|
||||
const { getByLabelText } = render(<RootWrapper />);
|
||||
await waitFor(() => {
|
||||
const textarea = getByLabelText(/Advanced Module List/i);
|
||||
expect(textarea).toBeInTheDocument();
|
||||
fireEvent.change(textarea, { target: { value: '[1, 2, 3]' } });
|
||||
expect(textarea.value).toBe('[1, 2, 3]');
|
||||
});
|
||||
});
|
||||
it('should display a warning alert', async () => {
|
||||
const { getByLabelText, getByText } = render(<RootWrapper />);
|
||||
await waitFor(() => {
|
||||
const textarea = getByLabelText(/Advanced Module List/i);
|
||||
fireEvent.change(textarea, { target: { value: '[3, 2, 1]' } });
|
||||
expect(getByText(messages.buttonCancelText.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(messages.buttonSaveText.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(messages.alertWarning.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(messages.alertWarningDescriptions.defaultMessage)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
it('should display a tooltip on clicking on the icon', async () => {
|
||||
const { getByLabelText, getByText } = render(<RootWrapper />);
|
||||
await waitFor(() => {
|
||||
const button = getByLabelText(/Show help text/i);
|
||||
fireEvent.click(button);
|
||||
expect(getByText(/Enter the names of the advanced modules to use in your course./i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
it('should change deprecated button text ', async () => {
|
||||
const { getByText } = render(<RootWrapper />);
|
||||
await waitFor(() => {
|
||||
const showDeprecatedItemsBtn = getByText(/Show Deprecated Settings/i);
|
||||
expect(showDeprecatedItemsBtn).toBeInTheDocument();
|
||||
fireEvent.click(showDeprecatedItemsBtn);
|
||||
expect(getByText(/Hide Deprecated Settings/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
it('should reset to default value on click on Cancel button', async () => {
|
||||
const { getByLabelText, getByText } = render(<RootWrapper />);
|
||||
await waitFor(() => {
|
||||
const textarea = getByLabelText(/Advanced Module List/i);
|
||||
fireEvent.change(textarea, { target: { value: '[3, 2, 1]' } });
|
||||
fireEvent.click(getByText(messages.buttonCancelText.defaultMessage));
|
||||
expect(textarea.value).toBe('[]');
|
||||
});
|
||||
});
|
||||
it('should update the textarea value and display the updated value after clicking "Change manually"', async () => {
|
||||
const { getByLabelText, getByText } = render(<RootWrapper />);
|
||||
await waitFor(() => {
|
||||
const textarea = getByLabelText(/Advanced Module List/i);
|
||||
fireEvent.change(textarea, { target: { value: '[3, 2, 1' } });
|
||||
fireEvent.click(getByText(messages.buttonSaveText.defaultMessage));
|
||||
fireEvent.click(getByText(/Change manually/i));
|
||||
expect(textarea.value).toBe('[3, 2, 1');
|
||||
});
|
||||
});
|
||||
});
|
||||
9
src/advanced-settings/__mocks__/advancedSettings.js
Normal file
9
src/advanced-settings/__mocks__/advancedSettings.js
Normal file
@@ -0,0 +1,9 @@
|
||||
module.exports = {
|
||||
advancedModules: {
|
||||
deprecated: false,
|
||||
displayName: 'Advanced Module List',
|
||||
help: 'Enter the names of the advanced modules to use in your course.',
|
||||
hideOnEnabledPublisher: false,
|
||||
value: [],
|
||||
},
|
||||
};
|
||||
2
src/advanced-settings/__mocks__/index.js
Normal file
2
src/advanced-settings/__mocks__/index.js
Normal file
@@ -0,0 +1,2 @@
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export { default as advancedSettingsMock } from './advancedSettings';
|
||||
41
src/advanced-settings/data/api.js
Normal file
41
src/advanced-settings/data/api.js
Normal file
@@ -0,0 +1,41 @@
|
||||
/* eslint-disable import/prefer-default-export */
|
||||
import { camelCaseObject, getConfig } from '@edx/frontend-platform';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
import { convertObjectToSnakeCase } from '../../utils';
|
||||
|
||||
const getApiBaseUrl = () => getConfig().STUDIO_BASE_URL;
|
||||
export const getCourseAdvancedSettingsApiUrl = (courseId) => `${getApiBaseUrl()}/api/contentstore/v0/advanced_settings/${courseId}`;
|
||||
const getProctoringErrorsApiUrl = () => `${getApiBaseUrl()}/api/contentstore/v1/proctoring_errors/`;
|
||||
|
||||
/**
|
||||
* Get's advanced setting for a course.
|
||||
* @param {string} courseId
|
||||
* @returns {Promise<Object>}
|
||||
*/
|
||||
export async function getCourseAdvancedSettings(courseId) {
|
||||
const { data } = await getAuthenticatedHttpClient()
|
||||
.get(`${getCourseAdvancedSettingsApiUrl(courseId)}?fetch_all=0`);
|
||||
return camelCaseObject(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates advanced setting for a course.
|
||||
* @param {string} courseId
|
||||
* @param {object} settings
|
||||
* @returns {Promise<Object>}
|
||||
*/
|
||||
export async function updateCourseAdvancedSettings(courseId, settings) {
|
||||
const { data } = await getAuthenticatedHttpClient()
|
||||
.patch(`${getCourseAdvancedSettingsApiUrl(courseId)}`, convertObjectToSnakeCase(settings));
|
||||
return camelCaseObject(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets proctoring exam errors.
|
||||
* @param {string} courseId
|
||||
* @returns {Promise<Object>}
|
||||
*/
|
||||
export async function getProctoringExamErrors(courseId) {
|
||||
const { data } = await getAuthenticatedHttpClient().get(`${getProctoringErrorsApiUrl()}${courseId}`);
|
||||
return camelCaseObject(data);
|
||||
}
|
||||
5
src/advanced-settings/data/selectors.js
Normal file
5
src/advanced-settings/data/selectors.js
Normal file
@@ -0,0 +1,5 @@
|
||||
export const getLoadingStatus = (state) => state.advancedSettings.loadingStatus;
|
||||
export const getCourseAppSettings = state => state.advancedSettings.courseAppSettings;
|
||||
export const getSavingStatus = (state) => state.advancedSettings.savingStatus;
|
||||
export const getProctoringExamErrors = (state) => state.advancedSettings.proctoringErrors.proctoringErrors;
|
||||
export const getSendRequestErrors = (state) => state.advancedSettings.sendRequestErrors.developer_message;
|
||||
48
src/advanced-settings/data/slice.js
Normal file
48
src/advanced-settings/data/slice.js
Normal file
@@ -0,0 +1,48 @@
|
||||
/* eslint-disable no-param-reassign */
|
||||
import { createSlice } from '@reduxjs/toolkit';
|
||||
|
||||
import { RequestStatus } from '../../data/constants';
|
||||
|
||||
const slice = createSlice({
|
||||
name: 'advancedSettings',
|
||||
initialState: {
|
||||
loadingStatus: RequestStatus.IN_PROGRESS,
|
||||
savingStatus: '',
|
||||
courseAppSettings: {},
|
||||
proctoringErrors: {},
|
||||
sendRequestErrors: {},
|
||||
},
|
||||
reducers: {
|
||||
updateLoadingStatus: (state, { payload }) => {
|
||||
state.loadingStatus = payload.status;
|
||||
},
|
||||
updateSavingStatus: (state, { payload }) => {
|
||||
state.savingStatus = payload.status;
|
||||
},
|
||||
fetchCourseAppsSettingsSuccess: (state, { payload }) => {
|
||||
Object.assign(state.courseAppSettings, payload);
|
||||
},
|
||||
updateCourseAppsSettingsSuccess: (state, { payload }) => {
|
||||
Object.assign(state.courseAppSettings, payload);
|
||||
},
|
||||
getDataSendErrors: (state, { payload }) => {
|
||||
Object.assign(state.sendRequestErrors, payload);
|
||||
},
|
||||
fetchProctoringExamErrorsSuccess: (state, { payload }) => {
|
||||
Object.assign(state.proctoringErrors, payload);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const {
|
||||
updateLoadingStatus,
|
||||
updateSavingStatus,
|
||||
getDataSendErrors,
|
||||
fetchCourseAppsSettingsSuccess,
|
||||
updateCourseAppsSettingsSuccess,
|
||||
fetchProctoringExamErrorsSuccess,
|
||||
} = slice.actions;
|
||||
|
||||
export const {
|
||||
reducer,
|
||||
} = slice;
|
||||
68
src/advanced-settings/data/thunks.js
Normal file
68
src/advanced-settings/data/thunks.js
Normal file
@@ -0,0 +1,68 @@
|
||||
import { RequestStatus } from '../../data/constants';
|
||||
import {
|
||||
getCourseAdvancedSettings,
|
||||
updateCourseAdvancedSettings,
|
||||
getProctoringExamErrors,
|
||||
} from './api';
|
||||
import {
|
||||
fetchCourseAppsSettingsSuccess,
|
||||
updateCourseAppsSettingsSuccess,
|
||||
updateLoadingStatus,
|
||||
updateSavingStatus,
|
||||
fetchProctoringExamErrorsSuccess,
|
||||
getDataSendErrors,
|
||||
} from './slice';
|
||||
|
||||
export function fetchCourseAppSettings(courseId) {
|
||||
return async (dispatch) => {
|
||||
dispatch(updateLoadingStatus({ status: RequestStatus.IN_PROGRESS }));
|
||||
|
||||
try {
|
||||
const settingValues = await getCourseAdvancedSettings(courseId);
|
||||
dispatch(fetchCourseAppsSettingsSuccess(settingValues));
|
||||
dispatch(updateLoadingStatus({ status: RequestStatus.SUCCESSFUL }));
|
||||
} catch (error) {
|
||||
dispatch(updateLoadingStatus({ status: RequestStatus.FAILED }));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function updateCourseAppSetting(courseId, settings) {
|
||||
return async (dispatch) => {
|
||||
dispatch(updateSavingStatus({ status: RequestStatus.IN_PROGRESS }));
|
||||
|
||||
try {
|
||||
const settingValues = await updateCourseAdvancedSettings(courseId, settings);
|
||||
dispatch(updateCourseAppsSettingsSuccess(settingValues));
|
||||
dispatch(updateSavingStatus({ status: RequestStatus.SUCCESSFUL }));
|
||||
return true;
|
||||
} catch (error) {
|
||||
let errorData;
|
||||
try {
|
||||
const { customAttributes: { httpErrorResponseData } } = error;
|
||||
errorData = JSON.parse(httpErrorResponseData);
|
||||
} catch (err) {
|
||||
errorData = {};
|
||||
}
|
||||
|
||||
dispatch(getDataSendErrors(errorData));
|
||||
dispatch(updateSavingStatus({ status: RequestStatus.FAILED }));
|
||||
return false;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchProctoringExamErrors(courseId) {
|
||||
return async (dispatch) => {
|
||||
dispatch(updateSavingStatus({ status: RequestStatus.IN_PROGRESS }));
|
||||
|
||||
try {
|
||||
const settingValues = await getProctoringExamErrors(courseId);
|
||||
dispatch(fetchProctoringExamErrorsSuccess(settingValues));
|
||||
return true;
|
||||
} catch (error) {
|
||||
dispatch(updateSavingStatus({ status: RequestStatus.FAILED }));
|
||||
return false;
|
||||
}
|
||||
};
|
||||
}
|
||||
2
src/advanced-settings/index.js
Normal file
2
src/advanced-settings/index.js
Normal file
@@ -0,0 +1,2 @@
|
||||
/* eslint-disable import/prefer-default-export */
|
||||
export { default as AdvancedSettings } from './AdvancedSettings';
|
||||
86
src/advanced-settings/messages.js
Normal file
86
src/advanced-settings/messages.js
Normal file
@@ -0,0 +1,86 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
headingTitle: {
|
||||
id: 'course-authoring.advanced-settings.heading.title',
|
||||
defaultMessage: 'Advanced settings',
|
||||
},
|
||||
headingSubtitle: {
|
||||
id: 'course-authoring.advanced-settings.heading.subtitle',
|
||||
defaultMessage: 'Settings',
|
||||
},
|
||||
policy: {
|
||||
id: 'course-authoring.advanced-settings.policies.title',
|
||||
defaultMessage: 'Manual policy definition',
|
||||
},
|
||||
alertWarning: {
|
||||
id: 'course-authoring.advanced-settings.alert.warning',
|
||||
defaultMessage: 'You`ve made some changes',
|
||||
},
|
||||
alertWarningDescriptions: {
|
||||
id: 'course-authoring.advanced-settings.alert.warning.descriptions',
|
||||
defaultMessage: 'Your changes will not take effect until you save your progress. Take care with key and value formatting, as validation is not implemented.',
|
||||
},
|
||||
alertSuccess: {
|
||||
id: 'course-authoring.advanced-settings.alert.success',
|
||||
defaultMessage: 'Your policy changes have been saved.',
|
||||
},
|
||||
alertSuccessDescriptions: {
|
||||
id: 'course-authoring.advanced-settings.alert.success.descriptions',
|
||||
defaultMessage: 'No validation is performed on policy keys or value pairs. If you are having difficulties, check your formatting.',
|
||||
},
|
||||
alertProctoringError: {
|
||||
id: 'course-authoring.advanced-settings.alert.proctoring.error',
|
||||
defaultMessage: 'This course has protected exam setting that are incomplete or invalid.',
|
||||
},
|
||||
alertProctoringErrorDescriptions: {
|
||||
id: 'course-authoring.advanced-settings.alert.proctoring.error.descriptions',
|
||||
defaultMessage: 'You will be unable to make changes until the following setting are updated on the page below.',
|
||||
},
|
||||
buttonSaveText: {
|
||||
id: 'course-authoring.advanced-settings.alert.button.save',
|
||||
defaultMessage: 'Save changes',
|
||||
},
|
||||
buttonSavingText: {
|
||||
id: 'course-authoring.advanced-settings.alert.button.saving',
|
||||
defaultMessage: 'Saving',
|
||||
},
|
||||
buttonCancelText: {
|
||||
id: 'course-authoring.advanced-settings.alert.button.cancel',
|
||||
defaultMessage: 'Cancel',
|
||||
},
|
||||
deprecatedButtonShowText: {
|
||||
id: 'course-authoring.advanced-settings.deprecated.button.show',
|
||||
defaultMessage: 'Show',
|
||||
},
|
||||
deprecatedButtonHideText: {
|
||||
id: 'course-authoring.advanced-settings.deprecated.button.hide',
|
||||
defaultMessage: 'Hide',
|
||||
},
|
||||
alertWarningAriaLabelledby: {
|
||||
id: 'course-authoring.advanced-settings.alert.warning.aria.labelledby',
|
||||
defaultMessage: 'notification-warning-title',
|
||||
},
|
||||
alertWarningAriaDescribedby: {
|
||||
id: 'course-authoring.advanced-settings.alert.warning.aria.describedby',
|
||||
defaultMessage: 'notification-warning-description',
|
||||
},
|
||||
alertSuccessAriaLabelledby: {
|
||||
id: 'course-authoring.advanced-settings.alert.success.aria.labelledby',
|
||||
defaultMessage: 'alert-confirmation-title',
|
||||
},
|
||||
alertSuccessAriaDescribedby: {
|
||||
id: 'course-authoring.advanced-settings.alert.success.aria.describedby',
|
||||
defaultMessage: 'alert-confirmation-description',
|
||||
},
|
||||
alertProctoringAriaLabelledby: {
|
||||
id: 'course-authoring.advanced-settings.alert.proctoring.error.aria.labelledby',
|
||||
defaultMessage: 'alert-danger-title',
|
||||
},
|
||||
alertProctoringDescribedby: {
|
||||
id: 'course-authoring.advanced-settings.alert.proctoring.error.aria.describedby',
|
||||
defaultMessage: 'alert-danger-description',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
63
src/advanced-settings/modal-error/ModalError.jsx
Normal file
63
src/advanced-settings/modal-error/ModalError.jsx
Normal file
@@ -0,0 +1,63 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { ActionRow, AlertModal, Button } from '@edx/paragon';
|
||||
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import ModalErrorListItem from './ModalErrorListItem';
|
||||
import messages from './messages';
|
||||
|
||||
const ModalError = ({
|
||||
intl, isError, handleUndoChanges, showErrorModal, errorList, settingsData,
|
||||
}) => (
|
||||
<AlertModal
|
||||
title={intl.formatMessage(messages.modalErrorTitle)}
|
||||
isOpen={isError}
|
||||
variant="danger"
|
||||
footerNode={(
|
||||
<ActionRow>
|
||||
<Button
|
||||
variant="tertiary"
|
||||
onClick={() => showErrorModal(!isError)}
|
||||
>
|
||||
{intl.formatMessage(messages.modalErrorButtonChangeManually)}
|
||||
</Button>
|
||||
<Button onClick={handleUndoChanges}>
|
||||
{intl.formatMessage(messages.modalErrorButtonUndoChanges)}
|
||||
</Button>
|
||||
</ActionRow>
|
||||
)}
|
||||
>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="course-authoring.advanced-settings.modal.error.description"
|
||||
defaultMessage="There was {errorCounter} while trying to save the course settings in the database.
|
||||
Please check the following validation feedbacks and reflect them in your course settings:"
|
||||
values={{ errorCounter: <strong>{errorList.length} validation error </strong> }}
|
||||
/>
|
||||
</p>
|
||||
<hr />
|
||||
<ul className="p-0">
|
||||
{errorList.map((settingName) => (
|
||||
<ModalErrorListItem
|
||||
key={settingName.key}
|
||||
settingName={settingName}
|
||||
settingsData={settingsData}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
</AlertModal>
|
||||
);
|
||||
|
||||
ModalError.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
isError: PropTypes.bool.isRequired,
|
||||
handleUndoChanges: PropTypes.func.isRequired,
|
||||
showErrorModal: PropTypes.func.isRequired,
|
||||
errorList: PropTypes.arrayOf(PropTypes.shape({
|
||||
key: PropTypes.string,
|
||||
message: PropTypes.string,
|
||||
})).isRequired,
|
||||
settingsData: PropTypes.shape({}).isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(ModalError);
|
||||
58
src/advanced-settings/modal-error/ModalError.test.jsx
Normal file
58
src/advanced-settings/modal-error/ModalError.test.jsx
Normal file
@@ -0,0 +1,58 @@
|
||||
import React from 'react';
|
||||
import { render, fireEvent } from '@testing-library/react';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import ModalError from './ModalError';
|
||||
import messages from './messages';
|
||||
|
||||
const handleUndoChangesMock = jest.fn();
|
||||
const showErrorModalMock = jest.fn();
|
||||
|
||||
const errorList = [
|
||||
{ key: 'setting1', message: 'Error 1' },
|
||||
{ key: 'setting2', message: 'Error 2' },
|
||||
];
|
||||
|
||||
const settingsData = {
|
||||
setting1: 'value1',
|
||||
setting2: 'value2',
|
||||
};
|
||||
|
||||
const RootWrapper = () => (
|
||||
<IntlProvider locale="en">
|
||||
<ModalError
|
||||
isError
|
||||
handleUndoChanges={handleUndoChangesMock}
|
||||
showErrorModal={showErrorModalMock}
|
||||
errorList={errorList}
|
||||
settingsData={settingsData}
|
||||
/>
|
||||
</IntlProvider>
|
||||
);
|
||||
|
||||
describe('<ModalError />', () => {
|
||||
it('calls handleUndoChanges when "Undo changes" button is clicked', () => {
|
||||
const { getByText } = render(<RootWrapper />);
|
||||
const undoChangesButton = getByText(messages.modalErrorButtonUndoChanges.defaultMessage);
|
||||
fireEvent.click(undoChangesButton);
|
||||
expect(handleUndoChangesMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
it('calls showErrorModal when "Change manually" button is clicked', () => {
|
||||
const { getByText } = render(<RootWrapper />);
|
||||
const changeManuallyButton = getByText(messages.modalErrorButtonChangeManually.defaultMessage);
|
||||
fireEvent.click(changeManuallyButton);
|
||||
expect(showErrorModalMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
it('renders error message with correct values', () => {
|
||||
const { getByText } = render(<RootWrapper />);
|
||||
expect(getByText(/There was/i)).toBeInTheDocument();
|
||||
expect(getByText(/2 validation error/i)).toBeInTheDocument();
|
||||
expect(getByText(/while trying to save the course settings in the database. Please check the following validation feedbacks and reflect them in your course settings:/i)).toBeInTheDocument();
|
||||
expect(getByText(messages.modalErrorTitle.defaultMessage)).toBeInTheDocument();
|
||||
});
|
||||
it('renders correct number of errors', () => {
|
||||
const { getByText } = render(<RootWrapper />);
|
||||
expect(getByText('Error 1')).toBeInTheDocument();
|
||||
expect(getByText('Error 2')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
31
src/advanced-settings/modal-error/ModalErrorListItem.jsx
Normal file
31
src/advanced-settings/modal-error/ModalErrorListItem.jsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Alert, Icon } from '@edx/paragon';
|
||||
import { Error } from '@edx/paragon/icons';
|
||||
import { capitalize } from 'lodash';
|
||||
|
||||
import { transformKeysToCamelCase } from '../../utils';
|
||||
|
||||
const ModalErrorListItem = ({ settingName, settingsData }) => {
|
||||
const { displayName } = settingsData[transformKeysToCamelCase(settingName)];
|
||||
return (
|
||||
<li className="modal-error-item">
|
||||
<Alert variant="danger">
|
||||
<h4 className="modal-error-item-title">
|
||||
<Icon src={Error} />{capitalize(displayName)}:
|
||||
</h4>
|
||||
<p className="m-0">{settingName.message}</p>
|
||||
</Alert>
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
||||
ModalErrorListItem.propTypes = {
|
||||
settingName: PropTypes.shape({
|
||||
key: PropTypes.string,
|
||||
message: PropTypes.string,
|
||||
}).isRequired,
|
||||
settingsData: PropTypes.shape({}).isRequired,
|
||||
};
|
||||
|
||||
export default ModalErrorListItem;
|
||||
@@ -0,0 +1,34 @@
|
||||
import React from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import ModalErrorListItem from './ModalErrorListItem';
|
||||
|
||||
const settingName = {
|
||||
key: 'exampleKey',
|
||||
message: 'Error message',
|
||||
};
|
||||
|
||||
const settingsData = {
|
||||
exampleKey: {
|
||||
displayName: 'Error field',
|
||||
},
|
||||
};
|
||||
|
||||
const RootWrapper = () => (
|
||||
<IntlProvider locale="en">
|
||||
<ModalErrorListItem settingName={settingName} settingsData={settingsData} />
|
||||
</IntlProvider>
|
||||
);
|
||||
|
||||
describe('<ModalErrorListItem />', () => {
|
||||
it('renders the display name and error message', () => {
|
||||
const { getByText } = render(<RootWrapper />);
|
||||
expect(getByText('Error field:')).toBeInTheDocument();
|
||||
expect(getByText('Error message')).toBeInTheDocument();
|
||||
});
|
||||
it('renders the alert with variant "danger"', () => {
|
||||
const { getByRole } = render(<RootWrapper />);
|
||||
expect(getByRole('alert')).toHaveClass('alert-danger');
|
||||
});
|
||||
});
|
||||
18
src/advanced-settings/modal-error/messages.js
Normal file
18
src/advanced-settings/modal-error/messages.js
Normal file
@@ -0,0 +1,18 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
modalErrorTitle: {
|
||||
id: 'course-authoring.advanced-settings.modal.error.title',
|
||||
defaultMessage: 'Validation error while saving',
|
||||
},
|
||||
modalErrorButtonChangeManually: {
|
||||
id: 'course-authoring.advanced-settings.modal.error.btn.change-manually',
|
||||
defaultMessage: 'Change manually',
|
||||
},
|
||||
modalErrorButtonUndoChanges: {
|
||||
id: 'course-authoring.advanced-settings.modal.error.btn.undo-changes',
|
||||
defaultMessage: 'Undo changes',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
125
src/advanced-settings/scss/AdvencedSettings.scss
Normal file
125
src/advanced-settings/scss/AdvencedSettings.scss
Normal file
@@ -0,0 +1,125 @@
|
||||
@import "variables";
|
||||
|
||||
.setting-items-policies {
|
||||
.setting-items-deprecated-setting {
|
||||
float: right;
|
||||
margin-bottom: 1.75rem;
|
||||
}
|
||||
|
||||
.instructions,
|
||||
strong {
|
||||
font: normal $font-weight-normal .875rem/1.5rem $font-family-base;
|
||||
color: $text-color-base;
|
||||
}
|
||||
}
|
||||
|
||||
.setting-card {
|
||||
margin-bottom: 1.75rem;
|
||||
|
||||
.pgn__card-header .pgn__card-header-title-md {
|
||||
font-size: 1.125rem;
|
||||
padding-top: .625rem;
|
||||
}
|
||||
|
||||
.pgn__icon {
|
||||
color: $headings-color;
|
||||
}
|
||||
}
|
||||
|
||||
.alert-toast {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
padding: 0 .625rem;
|
||||
z-index: $zindex-modal;
|
||||
}
|
||||
|
||||
.alert-proctoring-error {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.setting-items-list {
|
||||
li {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.form-control {
|
||||
resize: none;
|
||||
min-height: 2.75rem;
|
||||
width: $setting-form-control-width;
|
||||
}
|
||||
|
||||
.pgn__card-section {
|
||||
max-width: $setting-form-control-width;
|
||||
}
|
||||
|
||||
.pgn__card-header {
|
||||
padding: 0 1.5rem;
|
||||
}
|
||||
|
||||
.pgn__card-status {
|
||||
padding: .625rem;
|
||||
}
|
||||
|
||||
.pgn__card-header-content {
|
||||
margin-top: 1.438rem;
|
||||
margin-bottom: 1.438rem;
|
||||
display: flex;
|
||||
flex-direction: revert;
|
||||
|
||||
.pgn__card-header-subtitle-md {
|
||||
margin-left: .375rem;
|
||||
margin-top: .125rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.setting-sidebar-supplementary {
|
||||
margin-top: 1.875rem;
|
||||
|
||||
.setting-sidebar-supplementary-about {
|
||||
.setting-sidebar-supplementary-about-title {
|
||||
font: normal $font-weight-bold 1.125rem/1.5rem $font-family-base;
|
||||
color: $headings-color;
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
|
||||
.setting-sidebar-supplementary-about-descriptions {
|
||||
font: normal $font-weight-normal .875rem/1.5rem $font-family-base;
|
||||
color: $text-color-base;
|
||||
}
|
||||
}
|
||||
|
||||
.setting-sidebar-supplementary-other-links ul {
|
||||
list-style: none;
|
||||
|
||||
.setting-sidebar-supplementary-other-link {
|
||||
font: normal $font-weight-normal .875rem/1.5rem $font-family-base;
|
||||
line-height: 1.5rem;
|
||||
color: $info-500;
|
||||
margin-bottom: .5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.setting-sidebar-supplementary-other-title {
|
||||
font: normal $font-weight-bold 1.125rem/1.5rem $font-family-base;
|
||||
color: $headings-color;
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-error-item {
|
||||
list-style: none;
|
||||
|
||||
.pgn__icon {
|
||||
display: inline-block;
|
||||
margin-right: 5px;
|
||||
margin-bottom: 5px;
|
||||
color: $danger;
|
||||
}
|
||||
|
||||
.modal-error-item-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
2
src/advanced-settings/scss/_variables.scss
Normal file
2
src/advanced-settings/scss/_variables.scss
Normal file
@@ -0,0 +1,2 @@
|
||||
$text-color-base: $gray-700;
|
||||
$setting-form-control-width: 34.375rem;
|
||||
24
src/advanced-settings/setting-alert/SettingAlert.jsx
Normal file
24
src/advanced-settings/setting-alert/SettingAlert.jsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import React from 'react';
|
||||
import { Alert } from '@edx/paragon';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const SettingAlert = ({
|
||||
title, description, ...props
|
||||
}) => (
|
||||
<Alert {...props}>
|
||||
<Alert.Heading>{title}</Alert.Heading>
|
||||
<p>{description}</p>
|
||||
</Alert>
|
||||
);
|
||||
|
||||
SettingAlert.propTypes = {
|
||||
title: PropTypes.string,
|
||||
description: PropTypes.string,
|
||||
};
|
||||
|
||||
SettingAlert.defaultProps = {
|
||||
title: undefined,
|
||||
description: undefined,
|
||||
};
|
||||
|
||||
export default SettingAlert;
|
||||
36
src/advanced-settings/setting-alert/SettingsAlert.test.jsx
Normal file
36
src/advanced-settings/setting-alert/SettingsAlert.test.jsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import React from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import SettingAlert from './SettingAlert';
|
||||
|
||||
const alertTitle = 'Test Title';
|
||||
const alertDescription = 'Test Description';
|
||||
const alertClassName = 'custom-class';
|
||||
|
||||
const RootWrapper = () => (
|
||||
<IntlProvider locale="en">
|
||||
<SettingAlert
|
||||
title={alertTitle}
|
||||
description={alertDescription}
|
||||
className={alertClassName}
|
||||
/>
|
||||
</IntlProvider>
|
||||
);
|
||||
|
||||
describe('<SettingAlert />', () => {
|
||||
it('renders the title and description correctly', () => {
|
||||
const { getByText } = render(<RootWrapper />);
|
||||
const titleElement = getByText(alertTitle);
|
||||
const descriptionElement = getByText(alertDescription);
|
||||
expect(titleElement).toBeInTheDocument();
|
||||
expect(descriptionElement).toBeInTheDocument();
|
||||
});
|
||||
it('renders the alert with additional props', () => {
|
||||
const { getByRole } = render(<RootWrapper />);
|
||||
const alertElement = getByRole('alert');
|
||||
const classNameExists = alertElement.classList.contains(alertClassName);
|
||||
expect(alertElement).toBeInTheDocument();
|
||||
expect(classNameExists).toBe(true);
|
||||
});
|
||||
});
|
||||
94
src/advanced-settings/setting-card/SettingCard.jsx
Normal file
94
src/advanced-settings/setting-card/SettingCard.jsx
Normal file
@@ -0,0 +1,94 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Card, Form, Icon, IconButton, OverlayTrigger, Popover,
|
||||
} from '@edx/paragon';
|
||||
import { Info, Warning } from '@edx/paragon/icons';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import { capitalize } from 'lodash';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import TextareaAutosize from 'react-textarea-autosize';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
const SettingCard = ({
|
||||
intl, showDeprecated, name, onChange, value, settingData, handleBlur,
|
||||
}) => {
|
||||
const { deprecated, help, displayName } = settingData;
|
||||
return (
|
||||
<li className={classNames('field-group course-advanced-policy-list-item', { 'd-none': deprecated && !showDeprecated })}>
|
||||
<Card className="flex-column setting-card">
|
||||
<Card.Body className="d-flex justify-content-between">
|
||||
<Card.Header
|
||||
title={capitalize(displayName)}
|
||||
subtitle={(
|
||||
<OverlayTrigger
|
||||
trigger="click"
|
||||
rootClose
|
||||
placement="bottom"
|
||||
overlay={(
|
||||
<Popover id="popover-positioned">
|
||||
<Popover.Content>
|
||||
{/* eslint-disable-next-line react/no-danger */}
|
||||
<div dangerouslySetInnerHTML={{ __html: help }} />
|
||||
</Popover.Content>
|
||||
</Popover>
|
||||
)}
|
||||
>
|
||||
<IconButton
|
||||
src={Info}
|
||||
iconAs={Icon}
|
||||
alt={intl.formatMessage(messages.helpButtonText)}
|
||||
variant="light"
|
||||
/>
|
||||
</OverlayTrigger>
|
||||
)}
|
||||
/>
|
||||
<Card.Section>
|
||||
<Form.Group className="m-0">
|
||||
<Form.Control
|
||||
as={TextareaAutosize}
|
||||
value={value}
|
||||
name={name}
|
||||
onChange={onChange}
|
||||
aria-label={displayName}
|
||||
onBlur={handleBlur}
|
||||
/>
|
||||
</Form.Group>
|
||||
</Card.Section>
|
||||
</Card.Body>
|
||||
{deprecated && (
|
||||
<Card.Status icon={Warning} variant="danger">
|
||||
{intl.formatMessage(messages.deprecated)}
|
||||
</Card.Status>
|
||||
)}
|
||||
</Card>
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
||||
SettingCard.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
settingData: PropTypes.shape({
|
||||
deprecated: PropTypes.bool,
|
||||
help: PropTypes.string,
|
||||
displayName: PropTypes.string,
|
||||
}).isRequired,
|
||||
value: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.bool,
|
||||
PropTypes.number,
|
||||
PropTypes.object,
|
||||
PropTypes.array,
|
||||
]),
|
||||
onChange: PropTypes.func.isRequired,
|
||||
showDeprecated: PropTypes.bool.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
handleBlur: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
SettingCard.defaultProps = {
|
||||
value: undefined,
|
||||
};
|
||||
|
||||
export default injectIntl(SettingCard);
|
||||
68
src/advanced-settings/setting-card/SettingCard.test.jsx
Normal file
68
src/advanced-settings/setting-card/SettingCard.test.jsx
Normal file
@@ -0,0 +1,68 @@
|
||||
import React from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import SettingCard from './SettingCard';
|
||||
import messages from './messages';
|
||||
|
||||
const handleChange = jest.fn();
|
||||
|
||||
const settingData = {
|
||||
deprecated: false,
|
||||
help: 'This is a help message',
|
||||
displayName: 'Setting Name',
|
||||
};
|
||||
|
||||
jest.mock('react-textarea-autosize', () => jest.fn((props) => (
|
||||
<textarea
|
||||
{...props}
|
||||
onFocus={() => {}}
|
||||
onBlur={() => {}}
|
||||
/>
|
||||
)));
|
||||
|
||||
const RootWrapper = () => (
|
||||
<IntlProvider locale="en">
|
||||
<SettingCard
|
||||
intl={{}}
|
||||
isOn
|
||||
name="settingName"
|
||||
onChange={handleChange}
|
||||
value="Setting Value"
|
||||
settingData={settingData}
|
||||
/>
|
||||
</IntlProvider>
|
||||
);
|
||||
|
||||
describe('<SettingCard />', () => {
|
||||
afterEach(() => jest.clearAllMocks());
|
||||
it('renders the setting card with the provided data', () => {
|
||||
const { getByText, getByLabelText } = render(<RootWrapper />);
|
||||
const cardTitle = getByText(/Setting Name/i);
|
||||
const input = getByLabelText(/Setting Name/i);
|
||||
expect(cardTitle).toBeInTheDocument();
|
||||
expect(input).toBeInTheDocument();
|
||||
expect(input.value).toBe('Setting Value');
|
||||
});
|
||||
it('displays the deprecated status when the setting is deprecated', () => {
|
||||
const deprecatedSettingData = { ...settingData, deprecated: true };
|
||||
const { getByText } = render(
|
||||
<IntlProvider locale="en">
|
||||
<SettingCard
|
||||
intl={{}}
|
||||
isOn
|
||||
name="settingName"
|
||||
onChange={handleChange}
|
||||
value="Setting Value"
|
||||
settingData={deprecatedSettingData}
|
||||
/>
|
||||
</IntlProvider>,
|
||||
);
|
||||
const deprecatedStatus = getByText(messages.deprecated.defaultMessage);
|
||||
expect(deprecatedStatus).toBeInTheDocument();
|
||||
});
|
||||
it('does not display the deprecated status when the setting is not deprecated', () => {
|
||||
const { queryByText } = render(<RootWrapper />);
|
||||
expect(queryByText(messages.deprecated.defaultMessage)).toBeNull();
|
||||
});
|
||||
});
|
||||
14
src/advanced-settings/setting-card/messages.js
Normal file
14
src/advanced-settings/setting-card/messages.js
Normal file
@@ -0,0 +1,14 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
deprecated: {
|
||||
id: 'course-authoring.advanced-settings.button.deprecated',
|
||||
defaultMessage: 'Deprecated',
|
||||
},
|
||||
helpButtonText: {
|
||||
id: 'course-authoring.advanced-settings.button.help',
|
||||
defaultMessage: 'Show help text',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
87
src/advanced-settings/settings-sidebar/SettingsSidebar.jsx
Normal file
87
src/advanced-settings/settings-sidebar/SettingsSidebar.jsx
Normal file
@@ -0,0 +1,87 @@
|
||||
import React, { useContext } from 'react';
|
||||
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { AppContext } from '@edx/frontend-platform/react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Hyperlink } from '@edx/paragon';
|
||||
|
||||
import { getPagePath } from '../../utils';
|
||||
import messages from './messages';
|
||||
|
||||
const SettingsSidebar = ({ intl, courseId }) => {
|
||||
const { config } = useContext(AppContext);
|
||||
return (
|
||||
<aside className="setting-sidebar-supplementary">
|
||||
<div className="setting-sidebar-supplementary-about">
|
||||
<h4 className="setting-sidebar-supplementary-about-title">{intl.formatMessage(messages.about)}</h4>
|
||||
<p className="setting-sidebar-supplementary-about-descriptions">
|
||||
{intl.formatMessage(messages.aboutDescription1)}
|
||||
</p>
|
||||
<p className="setting-sidebar-supplementary-about-descriptions">
|
||||
{intl.formatMessage(messages.aboutDescription2)}
|
||||
</p>
|
||||
<p className="setting-sidebar-supplementary-about-descriptions">
|
||||
<FormattedMessage
|
||||
id="course-authoring.advanced-settings.about.description-3"
|
||||
defaultMessage="{notice} When you enter strings as policy values, ensure that you use double quotation marks (“) around the string. Do not use single quotation marks (‘)."
|
||||
values={{ notice: <strong>Note:</strong> }}
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
<hr />
|
||||
<div className="setting-sidebar-supplementary-other">
|
||||
<h4 className="setting-sidebar-supplementary-other-title">{intl.formatMessage(messages.other)}</h4>
|
||||
<nav className="setting-sidebar-supplementary-other-links" aria-label={intl.formatMessage(messages.other)}>
|
||||
<ul className="p-0 mb-0">
|
||||
<li className="setting-sidebar-supplementary-other-link">
|
||||
<Hyperlink
|
||||
rel="noopener"
|
||||
destination={getPagePath(courseId, process.env.ENABLE_NEW_SCHEDULE_DETAILS_PAGE, 'settings/details')}
|
||||
>
|
||||
{intl.formatMessage(messages.otherCourseSettingsLinkToScheduleAndDetails)}
|
||||
</Hyperlink>
|
||||
</li>
|
||||
<li className="setting-sidebar-supplementary-other-link">
|
||||
<Hyperlink
|
||||
rel="noopener"
|
||||
destination={getPagePath(courseId, process.env.ENABLE_NEW_GRADING_PAGE, 'settings/grading')}
|
||||
>
|
||||
{intl.formatMessage(messages.otherCourseSettingsLinkToGrading)}
|
||||
</Hyperlink>
|
||||
</li>
|
||||
<li className="setting-sidebar-supplementary-other-link">
|
||||
<Hyperlink
|
||||
rel="noopener"
|
||||
destination={getPagePath(courseId, process.env.ENABLE_NEW_COURSE_TEAM_PAGE, 'course_team')}
|
||||
>
|
||||
{intl.formatMessage(messages.otherCourseSettingsLinkToCourseTeam)}
|
||||
</Hyperlink>
|
||||
</li>
|
||||
<li className="setting-sidebar-supplementary-other-link">
|
||||
<Hyperlink
|
||||
rel="noopener"
|
||||
destination={`${config.STUDIO_BASE_URL}/group_configurations/${courseId}`}
|
||||
>
|
||||
{intl.formatMessage(messages.otherCourseSettingsLinkToGroupConfigurations)}
|
||||
</Hyperlink>
|
||||
</li>
|
||||
<li className="setting-sidebar-supplementary-other-link">
|
||||
<Hyperlink
|
||||
rel="noopener"
|
||||
destination={`${config.BASE_URL}/course/${courseId}/proctored-exam-settings`}
|
||||
>
|
||||
{intl.formatMessage(messages.otherCourseSettingsLinkToProctoredExamSettings)}
|
||||
</Hyperlink>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
};
|
||||
|
||||
SettingsSidebar.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
courseId: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(SettingsSidebar);
|
||||
@@ -0,0 +1,46 @@
|
||||
import React from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { initializeMockApp } from '@edx/frontend-platform';
|
||||
import { AppProvider } from '@edx/frontend-platform/react';
|
||||
|
||||
import initializeStore from '../../store';
|
||||
import SettingsSidebar from './SettingsSidebar';
|
||||
import messages from './messages';
|
||||
|
||||
const courseId = 'course-123';
|
||||
let store;
|
||||
|
||||
const RootWrapper = () => (
|
||||
<AppProvider store={store}>
|
||||
<IntlProvider locale="en" messages={{}}>
|
||||
<SettingsSidebar intl={{ formatMessage: jest.fn() }} courseId={courseId} />
|
||||
</IntlProvider>
|
||||
</AppProvider>
|
||||
);
|
||||
|
||||
describe('<SettingsSidebar />', () => {
|
||||
beforeEach(() => {
|
||||
initializeMockApp({
|
||||
authenticatedUser: {
|
||||
userId: 3,
|
||||
username: 'abc123',
|
||||
administrator: true,
|
||||
roles: [],
|
||||
},
|
||||
});
|
||||
store = initializeStore();
|
||||
});
|
||||
it('renders about and other sidebar titles correctly', () => {
|
||||
const { getByText } = render(<RootWrapper />);
|
||||
expect(getByText(messages.about.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(messages.other.defaultMessage)).toBeInTheDocument();
|
||||
});
|
||||
it('renders about descriptions correctly', () => {
|
||||
const { getByText } = render(<RootWrapper />);
|
||||
const aboutThirtyDescription = getByText('When you enter strings as policy values, ensure that you use double quotation marks (“) around the string. Do not use single quotation marks (‘).');
|
||||
expect(getByText(messages.aboutDescription1.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(messages.aboutDescription2.defaultMessage)).toBeInTheDocument();
|
||||
expect(aboutThirtyDescription).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
47
src/advanced-settings/settings-sidebar/messages.js
Normal file
47
src/advanced-settings/settings-sidebar/messages.js
Normal file
@@ -0,0 +1,47 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
about: {
|
||||
id: 'course-authoring.advanced-settings.sidebar.about.title',
|
||||
defaultMessage: 'What do advanced settings do?',
|
||||
},
|
||||
aboutDescription1: {
|
||||
id: 'course-authoring.advanced-settings.sidebar.about.description-1',
|
||||
defaultMessage: 'Advanced settings control specific course functionality. On this page, you can edit manual policies, which are JSON-based key and value pairs that control specific course settings.',
|
||||
},
|
||||
aboutDescription2: {
|
||||
id: 'course-authoring.advanced-settings.sidebar.about.description-2',
|
||||
defaultMessage: 'Any policies you modify here override all other information you’ve defined elsewhere in Studio. Do not edit policies unless you are familiar with both their purpose and syntax.',
|
||||
},
|
||||
other: {
|
||||
id: 'course-authoring.advanced-settings.sidebar.other.title',
|
||||
defaultMessage: 'Other course settings',
|
||||
},
|
||||
otherCourseSettingsLinkToScheduleAndDetails: {
|
||||
id: 'course-authoring.advanced-settings.sidebar.links.schedule-and-details',
|
||||
defaultMessage: 'Details & schedule',
|
||||
description: 'Link to Studio Details & schedule page',
|
||||
},
|
||||
otherCourseSettingsLinkToGrading: {
|
||||
id: 'course-authoring.advanced-settings.sidebar.links.grading',
|
||||
defaultMessage: 'Grading',
|
||||
description: 'Link to Studio Grading page',
|
||||
},
|
||||
otherCourseSettingsLinkToCourseTeam: {
|
||||
id: 'course-authoring.advanced-settings.sidebar.links.course-team',
|
||||
defaultMessage: 'Course team',
|
||||
description: 'Link to Studio Course team page',
|
||||
},
|
||||
otherCourseSettingsLinkToGroupConfigurations: {
|
||||
id: 'course-authoring.advanced-settings.sidebar.links.group-configurations',
|
||||
defaultMessage: 'Group configurations',
|
||||
description: 'Link to Studio Group configurations page',
|
||||
},
|
||||
otherCourseSettingsLinkToProctoredExamSettings: {
|
||||
id: 'course-authoring.advanced-settings.sidebar.links.proctored-exam-settings',
|
||||
defaultMessage: 'Proctored exam settings',
|
||||
description: 'Link to Proctored exam settings page',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
48
src/advanced-settings/utils.js
Normal file
48
src/advanced-settings/utils.js
Normal file
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* Validates advanced settings data by checking if the provided settings are correctly formatted JSON.
|
||||
* It performs validation on a given object of settings, detects incorrectly formatted settings,
|
||||
* and sets error fields accordingly using the setErrorFields function.
|
||||
*
|
||||
* @param {object} settingObj - The object containing the settings to validate.
|
||||
* @param {function} setErrorFields - The function to set error fields.
|
||||
* @returns {boolean} - `true` if the data is valid, otherwise `false`.
|
||||
*/
|
||||
export default function validateAdvancedSettingsData(settingObj, setErrorFields, setEditedSettings) {
|
||||
const fieldsWithErrors = [];
|
||||
|
||||
const pushDataToErrorArray = (settingName) => {
|
||||
fieldsWithErrors.push({ key: settingName, message: 'Incorrectly formatted JSON' });
|
||||
};
|
||||
|
||||
Object.entries(settingObj).forEach(([settingName, settingValue]) => {
|
||||
try {
|
||||
JSON.parse(settingValue);
|
||||
} catch (e) {
|
||||
let targetSettingValue = settingValue;
|
||||
const firstNonWhite = settingValue.substring(0, 1);
|
||||
const isValid = !['{', '[', "'"].includes(firstNonWhite);
|
||||
|
||||
if (isValid) {
|
||||
try {
|
||||
targetSettingValue = `"${ targetSettingValue.trim() }"`;
|
||||
JSON.parse(targetSettingValue);
|
||||
setEditedSettings((prevEditedSettings) => ({
|
||||
...prevEditedSettings,
|
||||
[settingName]: targetSettingValue,
|
||||
}));
|
||||
} catch (quotedE) { /* empty */ }
|
||||
}
|
||||
|
||||
pushDataToErrorArray(settingName);
|
||||
}
|
||||
});
|
||||
|
||||
setErrorFields((prevState) => {
|
||||
if (JSON.stringify(prevState) !== JSON.stringify(fieldsWithErrors)) {
|
||||
return fieldsWithErrors;
|
||||
}
|
||||
return prevState;
|
||||
});
|
||||
|
||||
return fieldsWithErrors.length === 0;
|
||||
}
|
||||
29
src/advanced-settings/utils.test.js
Normal file
29
src/advanced-settings/utils.test.js
Normal file
@@ -0,0 +1,29 @@
|
||||
import validateAdvancedSettingsData from './utils';
|
||||
|
||||
describe('validateAdvancedSettingsData', () => {
|
||||
it('should validate correctly formatted settings and return true', () => {
|
||||
const settingObj = {
|
||||
setting1: '{ "key": "value" }',
|
||||
setting2: '{ "key": "value" }',
|
||||
};
|
||||
const setErrorFieldsMock = jest.fn();
|
||||
const setEditedSettingsMock = jest.fn();
|
||||
const isValid = validateAdvancedSettingsData(settingObj, setErrorFieldsMock, setEditedSettingsMock);
|
||||
expect(isValid).toBe(true);
|
||||
expect(setErrorFieldsMock).toHaveBeenCalledTimes(1);
|
||||
expect(setEditedSettingsMock).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
it('should validate incorrectly formatted settings and set error fields', () => {
|
||||
const settingObj = {
|
||||
setting1: '{ "key": "value" }',
|
||||
setting2: 'incorrectJSON',
|
||||
setting3: '{ "key": "value" }',
|
||||
};
|
||||
const setErrorFieldsMock = jest.fn();
|
||||
const setEditedSettingsMock = jest.fn();
|
||||
const isValid = validateAdvancedSettingsData(settingObj, setErrorFieldsMock, setEditedSettingsMock);
|
||||
expect(isValid).toBe(false);
|
||||
expect(setErrorFieldsMock).toHaveBeenCalledTimes(1);
|
||||
expect(setEditedSettingsMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
2
src/assets/scss/_variables.scss
Normal file
2
src/assets/scss/_variables.scss
Normal file
@@ -0,0 +1,2 @@
|
||||
$text-color-base: $gray-700;
|
||||
$text-color-weak: #3E3E3C;
|
||||
@@ -75,7 +75,7 @@ const mockStore = async ({
|
||||
blockId,
|
||||
metadata: { courseStaffOnly: visibility },
|
||||
}), store.dispatch);
|
||||
};
|
||||
};
|
||||
|
||||
describe('CustomPageCard', () => {
|
||||
beforeEach(async () => {
|
||||
|
||||
@@ -57,7 +57,7 @@ const mockStore = async () => {
|
||||
await executeThunk(fetchCustomPages(courseId), store.dispatch);
|
||||
await executeThunk(addSingleCustomPage(courseId), store.dispatch);
|
||||
await executeThunk(updatePageOrder(courseId, [{ id: 'mOckID2' }, { id: 'mOckID1' }]), store.dispatch);
|
||||
};
|
||||
};
|
||||
|
||||
describe('CustomPages', () => {
|
||||
beforeEach(async () => {
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
removeModel,
|
||||
updateModel,
|
||||
updateModels,
|
||||
} from '../../generic/model-store';
|
||||
} from '../../generic/model-store';
|
||||
import {
|
||||
getCustomPages,
|
||||
deleteCustomPage,
|
||||
@@ -24,7 +24,6 @@ import {
|
||||
addCustomPageSuccess,
|
||||
} from './slice';
|
||||
|
||||
/* eslint-disable import/prefer-default-export */
|
||||
export function fetchCustomPages(courseId) {
|
||||
return async (dispatch) => {
|
||||
dispatch(updateLoadingStatus({ courseId, status: RequestStatus.IN_PROGRESS }));
|
||||
|
||||
@@ -28,18 +28,18 @@ export const initialState = {
|
||||
};
|
||||
|
||||
export const generateFetchPageApiResponse = () => ([{
|
||||
type: 'static_tab',
|
||||
title: null,
|
||||
is_hideable: false,
|
||||
is_hidden: false,
|
||||
is_movable: true,
|
||||
course_staff_only: false,
|
||||
name: 'test',
|
||||
tab_id: 'static_tab_1',
|
||||
settings: {
|
||||
url_slug: '1',
|
||||
},
|
||||
id: 'mOckID1',
|
||||
type: 'static_tab',
|
||||
title: null,
|
||||
is_hideable: false,
|
||||
is_hidden: false,
|
||||
is_movable: true,
|
||||
course_staff_only: false,
|
||||
name: 'test',
|
||||
tab_id: 'static_tab_1',
|
||||
settings: {
|
||||
url_slug: '1',
|
||||
},
|
||||
id: 'mOckID1',
|
||||
}]);
|
||||
|
||||
export const generateXblockData = (
|
||||
|
||||
37
src/generic/AlertProctoringError.jsx
Normal file
37
src/generic/AlertProctoringError.jsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import { Alert } from '@edx/paragon';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const AlertProctoringError = ({ proctoringErrorsData, ...props }) => (
|
||||
<ul className="alert-proctoring-error p-0">
|
||||
<Alert {...props}>
|
||||
{proctoringErrorsData.map(({ key, model, message }) => (
|
||||
<li key={key}>
|
||||
<Alert.Heading>{model.displayName}</Alert.Heading>
|
||||
<p>{message}</p>
|
||||
</li>
|
||||
))}
|
||||
</Alert>
|
||||
</ul>
|
||||
);
|
||||
|
||||
AlertProctoringError.propTypes = {
|
||||
variant: PropTypes.string,
|
||||
proctoringErrorsData: PropTypes.arrayOf(PropTypes.shape({
|
||||
key: PropTypes.string,
|
||||
message: PropTypes.string,
|
||||
model: PropTypes.shape({
|
||||
deprecated: PropTypes.bool,
|
||||
displayName: PropTypes.string,
|
||||
help: PropTypes.string,
|
||||
hideOnEnabledPublisher: PropTypes.bool,
|
||||
}),
|
||||
value: PropTypes.string,
|
||||
})).isRequired,
|
||||
};
|
||||
|
||||
AlertProctoringError.defaultProps = {
|
||||
variant: 'danger',
|
||||
};
|
||||
|
||||
export default AlertProctoringError;
|
||||
@@ -56,7 +56,7 @@ const CollapsableEditor = ({
|
||||
</Collapsible.Trigger>
|
||||
<Collapsible.Body className="collapsible-body rounded px-0">{children}</Collapsible.Body>
|
||||
</Collapsible.Advanced>
|
||||
);
|
||||
);
|
||||
|
||||
CollapsableEditor.propTypes = {
|
||||
open: PropTypes.bool,
|
||||
|
||||
@@ -32,7 +32,7 @@ const ConfirmationPopup = ({
|
||||
</Card.Footer>
|
||||
</Card.Body>
|
||||
</Card>
|
||||
);
|
||||
);
|
||||
|
||||
ConfirmationPopup.propTypes = {
|
||||
label: PropTypes.string.isRequired,
|
||||
|
||||
@@ -11,15 +11,15 @@ const ConnectionErrorAlert = ({ intl }) => (
|
||||
id="authoring.alert.error.connection"
|
||||
defaultMessage="We encountered a technical error when loading this page. This might be a temporary issue, so please try again in a few minutes. If the problem persists, please go to the {supportLink} for help."
|
||||
values={{
|
||||
supportLink: (
|
||||
<Alert.Link href={getConfig().SUPPORT_URL}>
|
||||
{intl.formatMessage(messages.supportText)}
|
||||
</Alert.Link>
|
||||
),
|
||||
}}
|
||||
supportLink: (
|
||||
<Alert.Link href={getConfig().SUPPORT_URL}>
|
||||
{intl.formatMessage(messages.supportText)}
|
||||
</Alert.Link>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</Alert>
|
||||
);
|
||||
);
|
||||
|
||||
ConnectionErrorAlert.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
|
||||
@@ -18,7 +18,7 @@ const FieldFeedback = ({
|
||||
<div className={`small ${feedbackClasses}`}>{feedbackMessage}</div>
|
||||
</Form.Control.Feedback>
|
||||
</React.Fragment>
|
||||
) : <React.Fragment key="close1" />}
|
||||
) : <React.Fragment key="close1" />}
|
||||
</TransitionReplace>
|
||||
|
||||
<TransitionReplace>
|
||||
@@ -28,10 +28,10 @@ const FieldFeedback = ({
|
||||
<div className={`small ${feedbackClasses}`}>{errorMessage}</div>
|
||||
</Form.Control.Feedback>
|
||||
</React.Fragment>
|
||||
) : <React.Fragment key="close" />}
|
||||
) : <React.Fragment key="close" />}
|
||||
</TransitionReplace>
|
||||
</>
|
||||
);
|
||||
);
|
||||
|
||||
FieldFeedback.propTypes = {
|
||||
errorCondition: PropTypes.bool.isRequired,
|
||||
|
||||
@@ -6,8 +6,8 @@ const Loading = () => (
|
||||
<div
|
||||
className="d-flex justify-content-center align-items-center flex-column"
|
||||
style={{
|
||||
height: '50vh',
|
||||
}}
|
||||
height: '100vh',
|
||||
}}
|
||||
>
|
||||
<Spinner
|
||||
animation="border"
|
||||
@@ -21,9 +21,9 @@ const Loading = () => (
|
||||
description="Screen-reader message for when a page is loading."
|
||||
/>
|
||||
</span>
|
||||
)}
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
);
|
||||
|
||||
export default Loading;
|
||||
|
||||
@@ -9,6 +9,6 @@ const PermissionDeniedAlert = () => (
|
||||
defaultMessage="You are not authorized to view this page. If you feel you should have access, please reach out to your course team admin to be given access."
|
||||
/>
|
||||
</Alert>
|
||||
);
|
||||
);
|
||||
|
||||
export default PermissionDeniedAlert;
|
||||
|
||||
@@ -11,15 +11,15 @@ const SaveFormConnectionErrorAlert = ({ intl }) => (
|
||||
id="authoring.alert.save.error.connection"
|
||||
defaultMessage="We encountered a technical error when applying changes. This might be a temporary issue, so please try again in a few minutes. If the problem persists, please go to the {supportLink} for help."
|
||||
values={{
|
||||
supportLink: (
|
||||
<Alert.Link href={getConfig().SUPPORT_URL}>
|
||||
{intl.formatMessage(messages.supportText)}
|
||||
</Alert.Link>
|
||||
),
|
||||
}}
|
||||
supportLink: (
|
||||
<Alert.Link href={getConfig().SUPPORT_URL}>
|
||||
{intl.formatMessage(messages.supportText)}
|
||||
</Alert.Link>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</Alert>
|
||||
);
|
||||
);
|
||||
|
||||
SaveFormConnectionErrorAlert.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
|
||||
36
src/generic/alert-message/AlertMessage.test.jsx
Normal file
36
src/generic/alert-message/AlertMessage.test.jsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import React from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import SettingAlert from '.';
|
||||
|
||||
const alertTitle = 'Test Title';
|
||||
const alertDescription = 'Test Description';
|
||||
const alertClassName = 'custom-class';
|
||||
|
||||
const RootWrapper = () => (
|
||||
<IntlProvider locale="en">
|
||||
<SettingAlert
|
||||
title={alertTitle}
|
||||
description={alertDescription}
|
||||
className={alertClassName}
|
||||
/>
|
||||
</IntlProvider>
|
||||
);
|
||||
|
||||
describe('<SettingAlert />', () => {
|
||||
it('renders the title and description correctly', () => {
|
||||
const { getByText } = render(<RootWrapper />);
|
||||
const titleElement = getByText(alertTitle);
|
||||
const descriptionElement = getByText(alertDescription);
|
||||
expect(titleElement).toBeInTheDocument();
|
||||
expect(descriptionElement).toBeInTheDocument();
|
||||
});
|
||||
it('renders the alert with additional props', () => {
|
||||
const { getByRole } = render(<RootWrapper />);
|
||||
const alertElement = getByRole('alert');
|
||||
const classNameExists = alertElement.classList.contains(alertClassName);
|
||||
expect(alertElement).toBeInTheDocument();
|
||||
expect(classNameExists).toBe(true);
|
||||
});
|
||||
});
|
||||
22
src/generic/alert-message/index.jsx
Normal file
22
src/generic/alert-message/index.jsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import React from 'react';
|
||||
import { Alert } from '@edx/paragon';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const AlertMessage = ({ title, description, ...props }) => (
|
||||
<Alert {...props}>
|
||||
<Alert.Heading>{title}</Alert.Heading>
|
||||
<p>{description}</p>
|
||||
</Alert>
|
||||
);
|
||||
|
||||
AlertMessage.propTypes = {
|
||||
title: PropTypes.string,
|
||||
description: PropTypes.string,
|
||||
};
|
||||
|
||||
AlertMessage.defaultProps = {
|
||||
title: undefined,
|
||||
description: undefined,
|
||||
};
|
||||
|
||||
export default AlertMessage;
|
||||
32
src/generic/help-sidebar/HelpSidebar.scss
Normal file
32
src/generic/help-sidebar/HelpSidebar.scss
Normal file
@@ -0,0 +1,32 @@
|
||||
.help-sidebar {
|
||||
margin-top: 8.563rem;
|
||||
|
||||
.help-sidebar-about {
|
||||
.help-sidebar-about-title {
|
||||
color: $black;
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
|
||||
.help-sidebar-about-descriptions {
|
||||
font: normal $font-weight-normal .875rem/1.5rem $font-family-base;
|
||||
color: $text-color-base;
|
||||
}
|
||||
}
|
||||
|
||||
.help-sidebar-other-links ul {
|
||||
list-style: none;
|
||||
|
||||
.help-sidebar-other-link {
|
||||
font: normal $font-weight-normal .875rem/1.5rem $font-family-base;
|
||||
line-height: 1.5rem;
|
||||
color: $info-500;
|
||||
margin-bottom: .5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.help-sidebar-other-title {
|
||||
font: normal $font-weight-bold 1.125rem/1.5rem $font-family-base;
|
||||
color: $black;
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
}
|
||||
44
src/generic/help-sidebar/HelpSidebar.test.jsx
Normal file
44
src/generic/help-sidebar/HelpSidebar.test.jsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import React from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import HelpSidebar from '.';
|
||||
import messages from './messages';
|
||||
|
||||
const mockPathname = '/foo-bar';
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useLocation: () => ({
|
||||
pathname: mockPathname,
|
||||
}),
|
||||
}));
|
||||
|
||||
const RootWrapper = () => (
|
||||
<IntlProvider locale="en">
|
||||
<HelpSidebar
|
||||
courseId="course123"
|
||||
showOtherSettings
|
||||
proctoredExamSettingsUrl=""
|
||||
>
|
||||
<p>Test children</p>
|
||||
</HelpSidebar>
|
||||
</IntlProvider>
|
||||
);
|
||||
|
||||
describe('HelpSidebar', () => {
|
||||
it('renders children correctly', () => {
|
||||
const { getByText } = render(<RootWrapper />);
|
||||
expect(getByText('Test children')).toBeTruthy();
|
||||
});
|
||||
it('should render all sidebar links with correct text', () => {
|
||||
const { getByText } = render(<RootWrapper />);
|
||||
expect(getByText(messages.sidebarTitleOther.defaultMessage)).toBeTruthy();
|
||||
expect(getByText(messages.sidebarLinkToScheduleAndDetails.defaultMessage)).toBeTruthy();
|
||||
expect(getByText(messages.sidebarLinkToGrading.defaultMessage)).toBeTruthy();
|
||||
expect(getByText(messages.sidebarLinkToCourseTeam.defaultMessage)).toBeTruthy();
|
||||
expect(getByText(messages.sidebarLinkToGroupConfigurations.defaultMessage)).toBeTruthy();
|
||||
expect(getByText(messages.sidebarLinkToAdvancedSettings.defaultMessage)).toBeTruthy();
|
||||
expect(getByText(messages.sidebarLinkToProctoredExamSettings.defaultMessage)).toBeTruthy();
|
||||
});
|
||||
});
|
||||
154
src/generic/help-sidebar/index.jsx
Normal file
154
src/generic/help-sidebar/index.jsx
Normal file
@@ -0,0 +1,154 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import classNames from 'classnames';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { Hyperlink } from '@edx/paragon';
|
||||
|
||||
import { getPagePath } from '../../utils';
|
||||
import messages from './messages';
|
||||
|
||||
const HelpSidebar = ({
|
||||
intl,
|
||||
courseId,
|
||||
showOtherSettings,
|
||||
proctoredExamSettingsUrl,
|
||||
children,
|
||||
className,
|
||||
}) => {
|
||||
const { pathname } = useLocation();
|
||||
const scheduleAndDetailsDestination = getPagePath(
|
||||
courseId,
|
||||
process.env.ENABLE_NEW_SCHEDULE_DETAILS_PAGE,
|
||||
'settings/details',
|
||||
);
|
||||
const gradingDestination = getPagePath(
|
||||
courseId,
|
||||
process.env.ENABLE_NEW_GRADING_PAGE,
|
||||
'settings/grading',
|
||||
);
|
||||
const courseTeamDestination = getPagePath(
|
||||
courseId,
|
||||
process.env.ENABLE_NEW_COURSE_TEAM_PAGE,
|
||||
'course_team',
|
||||
);
|
||||
const advancedSettingsDestination = getPagePath(
|
||||
courseId,
|
||||
process.env.ENABLE_NEW_ADVANCED_SETTINGS_PAGE,
|
||||
'settings/advanced',
|
||||
);
|
||||
const groupConfigurationsDestination = new URL(
|
||||
`/group_configurations/${courseId}`,
|
||||
getConfig().STUDIO_BASE_URL,
|
||||
);
|
||||
const proctoredExamSettingsDestination = new URL(
|
||||
`/course/${courseId}/proctored-exam-settings`,
|
||||
getConfig().BASE_URL,
|
||||
);
|
||||
|
||||
return (
|
||||
<aside className={classNames('help-sidebar', className)}>
|
||||
<div className="help-sidebar-about">{children}</div>
|
||||
<hr />
|
||||
{showOtherSettings && (
|
||||
<div className="help-sidebar-other">
|
||||
<h4 className="help-sidebar-other-title">
|
||||
{intl.formatMessage(messages.sidebarTitleOther)}
|
||||
</h4>
|
||||
<nav
|
||||
className="help-sidebar-other-links"
|
||||
aria-label={intl.formatMessage(messages.sidebarTitleOther)}
|
||||
>
|
||||
<ul className="p-0 mb-0">
|
||||
{!scheduleAndDetailsDestination.includes(pathname) && (
|
||||
<li className="help-sidebar-other-link">
|
||||
<Hyperlink destination={scheduleAndDetailsDestination}>
|
||||
{intl.formatMessage(
|
||||
messages.sidebarLinkToScheduleAndDetails,
|
||||
)}
|
||||
</Hyperlink>
|
||||
</li>
|
||||
)}
|
||||
{!gradingDestination.includes(pathname) && (
|
||||
<li className="help-sidebar-other-link">
|
||||
<Hyperlink rel="noopener" destination={gradingDestination}>
|
||||
{intl.formatMessage(messages.sidebarLinkToGrading)}
|
||||
</Hyperlink>
|
||||
</li>
|
||||
)}
|
||||
{!courseTeamDestination.includes(pathname) && (
|
||||
<li className="help-sidebar-other-link">
|
||||
<Hyperlink rel="noopener" destination={courseTeamDestination}>
|
||||
{intl.formatMessage(messages.sidebarLinkToCourseTeam)}
|
||||
</Hyperlink>
|
||||
</li>
|
||||
)}
|
||||
{proctoredExamSettingsUrl
|
||||
&& !proctoredExamSettingsUrl.includes(pathname) && (
|
||||
<li className="help-sidebar-other-link">
|
||||
<Hyperlink
|
||||
rel="noopener"
|
||||
destination={proctoredExamSettingsUrl}
|
||||
>
|
||||
{intl.formatMessage(
|
||||
messages.sidebarLinkToProctoredExamSettings,
|
||||
)}
|
||||
</Hyperlink>
|
||||
</li>
|
||||
)}
|
||||
{!groupConfigurationsDestination.href.includes(pathname) && (
|
||||
<li className="help-sidebar-other-link">
|
||||
<Hyperlink
|
||||
rel="noopener"
|
||||
destination={groupConfigurationsDestination.href}
|
||||
>
|
||||
{intl.formatMessage(
|
||||
messages.sidebarLinkToGroupConfigurations,
|
||||
)}
|
||||
</Hyperlink>
|
||||
</li>
|
||||
)}
|
||||
{!advancedSettingsDestination.includes(pathname) && (
|
||||
<li className="help-sidebar-other-link">
|
||||
<Hyperlink
|
||||
rel="noopener"
|
||||
destination={advancedSettingsDestination}
|
||||
>
|
||||
{intl.formatMessage(messages.sidebarLinkToAdvancedSettings)}
|
||||
</Hyperlink>
|
||||
</li>
|
||||
)}
|
||||
{!proctoredExamSettingsDestination.href.includes(pathname) && !gradingDestination.includes(pathname) && (
|
||||
<li className="help-sidebar-other-link">
|
||||
<Hyperlink
|
||||
rel="noopener"
|
||||
destination={proctoredExamSettingsDestination}
|
||||
>
|
||||
{intl.formatMessage(messages.sidebarLinkToProctoredExamSettings)}
|
||||
</Hyperlink>
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
)}
|
||||
</aside>
|
||||
);
|
||||
};
|
||||
|
||||
HelpSidebar.defaultProps = {
|
||||
proctoredExamSettingsUrl: '',
|
||||
className: undefined,
|
||||
};
|
||||
|
||||
HelpSidebar.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
courseId: PropTypes.string.isRequired,
|
||||
showOtherSettings: PropTypes.bool.isRequired,
|
||||
proctoredExamSettingsUrl: PropTypes.string,
|
||||
children: PropTypes.node.isRequired,
|
||||
className: PropTypes.string,
|
||||
};
|
||||
|
||||
export default injectIntl(HelpSidebar);
|
||||
40
src/generic/help-sidebar/messages.js
Normal file
40
src/generic/help-sidebar/messages.js
Normal file
@@ -0,0 +1,40 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
sidebarTitleOther: {
|
||||
id: 'course-authoring.help-sidebar.other.title',
|
||||
defaultMessage: 'Other course settings',
|
||||
},
|
||||
sidebarLinkToScheduleAndDetails: {
|
||||
id: 'course-authoring.help-sidebar.links.schedule-and-details',
|
||||
defaultMessage: 'Details & schedule',
|
||||
description: 'Link to Studio Schedule & Details page',
|
||||
},
|
||||
sidebarLinkToGrading: {
|
||||
id: 'course-authoring.help-sidebar.links.grading',
|
||||
defaultMessage: 'Grading',
|
||||
description: 'Link to Studio Grading page',
|
||||
},
|
||||
sidebarLinkToCourseTeam: {
|
||||
id: 'course-authoring.help-sidebar.links.course-team',
|
||||
defaultMessage: 'Course team',
|
||||
description: 'Link to Studio Course Team page',
|
||||
},
|
||||
sidebarLinkToGroupConfigurations: {
|
||||
id: 'course-authoring.help-sidebar.links.group-configurations',
|
||||
defaultMessage: 'Group configurations',
|
||||
description: 'Link to Studio Group Configurations page',
|
||||
},
|
||||
sidebarLinkToProctoredExamSettings: {
|
||||
id: 'course-authoring.help-sidebar.links.proctored-exam-settings',
|
||||
defaultMessage: 'Proctored exam settings',
|
||||
description: 'Link to Proctored exam settings page',
|
||||
},
|
||||
sidebarLinkToAdvancedSettings: {
|
||||
id: 'course-authoring.help-sidebar.links.advanced-settings',
|
||||
defaultMessage: 'Advanced settings',
|
||||
description: 'Link to Advanced Settings',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
76
src/generic/internet-connection-alert/index.jsx
Normal file
76
src/generic/internet-connection-alert/index.jsx
Normal file
@@ -0,0 +1,76 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Warning as WarningIcon } from '@edx/paragon/icons';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import AlertMessage from '../alert-message';
|
||||
import messages from './messages';
|
||||
|
||||
const InternetConnectionAlert = ({
|
||||
isFailed, isQueryPending, onQueryProcessing, onInternetConnectionFailed,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const [showAlert, setShowAlert] = useState(false);
|
||||
const [isOnline, setIsOnline] = useState(window.navigator.onLine);
|
||||
|
||||
useEffect(() => {
|
||||
const handleOnlineStatus = () => setIsOnline(window.navigator.onLine);
|
||||
const events = ['online', 'offline'];
|
||||
|
||||
events.forEach(event => {
|
||||
window.addEventListener(event, handleOnlineStatus);
|
||||
});
|
||||
|
||||
return () => {
|
||||
events.forEach(event => {
|
||||
window.removeEventListener(event, handleOnlineStatus);
|
||||
});
|
||||
};
|
||||
}, [isOnline]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isQueryPending) {
|
||||
onQueryProcessing();
|
||||
setShowAlert(!isOnline);
|
||||
|
||||
if (!isOnline) {
|
||||
onInternetConnectionFailed();
|
||||
}
|
||||
} else if (isFailed) {
|
||||
setShowAlert(!isOnline);
|
||||
} else if (isOnline) {
|
||||
setShowAlert(false);
|
||||
}
|
||||
}, [isOnline, isQueryPending, isFailed]);
|
||||
|
||||
return (
|
||||
<AlertMessage
|
||||
show={showAlert}
|
||||
variant="danger"
|
||||
icon={WarningIcon}
|
||||
title={intl.formatMessage(messages.offlineWarningTitle)}
|
||||
description={intl.formatMessage(messages.offlineWarningDescription)}
|
||||
aria-hidden="true"
|
||||
aria-labelledby={intl.formatMessage(
|
||||
messages.offlineWarningTitleAriaLabelledBy,
|
||||
)}
|
||||
aria-describedby={intl.formatMessage(
|
||||
messages.offlineWarningDescriptionAriaDescribedBy,
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
InternetConnectionAlert.defaultProps = {
|
||||
isQueryPending: false,
|
||||
|
||||
};
|
||||
|
||||
InternetConnectionAlert.propTypes = {
|
||||
isFailed: PropTypes.bool.isRequired,
|
||||
isQueryPending: PropTypes.bool,
|
||||
onQueryProcessing: PropTypes.func.isRequired,
|
||||
onInternetConnectionFailed: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default InternetConnectionAlert;
|
||||
22
src/generic/internet-connection-alert/messages.js
Normal file
22
src/generic/internet-connection-alert/messages.js
Normal file
@@ -0,0 +1,22 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
offlineWarningTitle: {
|
||||
id: 'course-authoring.generic.alert.warning.offline.title',
|
||||
defaultMessage: 'Studio\'s having trouble saving your work',
|
||||
},
|
||||
offlineWarningDescription: {
|
||||
id: 'course-authoring.generic.alert.warning.offline.description',
|
||||
defaultMessage: 'This may be happening because of an error with our server or your internet connection. Try refreshing the page or making sure you are online.',
|
||||
},
|
||||
offlineWarningTitleAriaLabelledBy: {
|
||||
id: 'course-authoring.generic.alert.warning.offline.title.aria.labelled-by',
|
||||
defaultMessage: 'alert-internet-error-title',
|
||||
},
|
||||
offlineWarningDescriptionAriaDescribedBy: {
|
||||
id: 'course-authoring.generic.alert.warning.offline.subtitle.aria.described-by',
|
||||
defaultMessage: 'alert-internet-error-description',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
2
src/generic/styles.scss
Normal file
2
src/generic/styles.scss
Normal file
@@ -0,0 +1,2 @@
|
||||
@import "./help-sidebar/HelpSidebar";
|
||||
@import "./sub-header/SubHeader";
|
||||
40
src/generic/sub-header/SubHeader.jsx
Normal file
40
src/generic/sub-header/SubHeader.jsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const SubHeader = ({
|
||||
title, subtitle, contentTitle, description, instruction,
|
||||
}) => (
|
||||
<>
|
||||
<header className="sub-header">
|
||||
<h2 className="sub-header-title">
|
||||
<small className="sub-header-title-subtitle">{subtitle}</small>
|
||||
{title}
|
||||
</h2>
|
||||
</header>
|
||||
<header className="sub-header-content">
|
||||
<h2 className="sub-header-content-title">{contentTitle}</h2>
|
||||
<span className="small text-gray-700">{description}</span>
|
||||
</header>
|
||||
{instruction && (
|
||||
<p className="sub-header-instructions mb-4">{instruction}</p>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
SubHeader.defaultProps = {
|
||||
instruction: '',
|
||||
description: '',
|
||||
};
|
||||
|
||||
SubHeader.propTypes = {
|
||||
title: PropTypes.string.isRequired,
|
||||
subtitle: PropTypes.string.isRequired,
|
||||
contentTitle: PropTypes.string.isRequired,
|
||||
description: PropTypes.string,
|
||||
instruction: PropTypes.oneOfType([
|
||||
PropTypes.element,
|
||||
PropTypes.string,
|
||||
]),
|
||||
};
|
||||
|
||||
export default SubHeader;
|
||||
31
src/generic/sub-header/SubHeader.scss
Normal file
31
src/generic/sub-header/SubHeader.scss
Normal file
@@ -0,0 +1,31 @@
|
||||
.sub-header-title {
|
||||
font: normal $font-weight-bold 2rem/2.25rem $font-family-base;
|
||||
margin-bottom: 1.75rem;
|
||||
color: $black;
|
||||
|
||||
.sub-header-title-subtitle {
|
||||
font: normal $font-weight-normal .875rem/1.5rem $font-family-base;
|
||||
display: block;
|
||||
color: $text-color-base;
|
||||
}
|
||||
}
|
||||
|
||||
.sub-header-content-title {
|
||||
font: normal $font-weight-normal 1.375rem/1.5 $font-family-base;
|
||||
margin-bottom: 0;
|
||||
color: $black;
|
||||
}
|
||||
|
||||
.sub-header-instructions {
|
||||
font: normal $font-weight-normal .875rem/1.5rem $font-family-base;
|
||||
color: $text-color-base;
|
||||
}
|
||||
|
||||
.sub-header-content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border-bottom: $border-width solid $light-400;
|
||||
padding-bottom: 12px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
@@ -353,5 +353,41 @@
|
||||
"header.label.main.menu": "القائمة الرئيسية",
|
||||
"header.label.main.header": "الرئيسية",
|
||||
"header.label.secondary.nav": "الثانوية",
|
||||
"header.label.courseOutline": "الرجوع إلى مخطط المساق الكلّي في الاستوديو"
|
||||
}
|
||||
"header.label.courseOutline": "الرجوع إلى مخطط المساق الكلّي في الاستوديو",
|
||||
"course-authoring.advanced-settings.heading.title": "Advanced settings",
|
||||
"course-authoring.advanced-settings.heading.subtitle": "Settings",
|
||||
"course-authoring.advanced-settings.policies.title": "Manual policy definition",
|
||||
"course-authoring.advanced-settings.alert.warning": "You`ve made some changes",
|
||||
"course-authoring.advanced-settings.alert.warning.descriptions": "Your changes will not take effect until you save your progress. Take care with key and value formatting, as validation is not implemented.",
|
||||
"course-authoring.advanced-settings.alert.success": "Your policy changes have been saved.",
|
||||
"course-authoring.advanced-settings.alert.success.descriptions": "No validation is performed on policy keys or value pairs. If you are having difficulties, check your formatting.",
|
||||
"course-authoring.advanced-settings.alert.proctoring.error": "This course has protected exam setting that are incomplete or invalid.",
|
||||
"course-authoring.advanced-settings.alert.proctoring.error.descriptions": "You will be unable to make changes until the following setting are updated on the page below.",
|
||||
"course-authoring.advanced-settings.alert.button.save": "Save changes",
|
||||
"course-authoring.advanced-settings.alert.button.cancel": "Cancel",
|
||||
"course-authoring.advanced-settings.deprecated.button.show": "Show",
|
||||
"course-authoring.advanced-settings.deprecated.button.hide": "Hide",
|
||||
"course-authoring.advanced-settings.alert.warning.aria.labelledby": "notification-warning-title",
|
||||
"course-authoring.advanced-settings.alert.warning.aria.describedby": "notification-warning-description",
|
||||
"course-authoring.advanced-settings.alert.success.aria.labelledby": "alert-confirmation-title",
|
||||
"course-authoring.advanced-settings.alert.success.aria.describedby": "alert-confirmation-description",
|
||||
"course-authoring.advanced-settings.alert.proctoring.error.aria.labelledby": "alert-danger-title",
|
||||
"course-authoring.advanced-settings.alert.proctoring.error.aria.describedby": "alert-danger-description",
|
||||
"course-authoring.advanced-settings.sidebar.about.title": "What do advanced settings do?",
|
||||
"course-authoring.advanced-settings.sidebar.about.description-1": "Advanced settings control specific course functionality. On this page, you can edit manual policies, which are JSON-based key and value pairs that control specific course settings.",
|
||||
"course-authoring.advanced-settings.sidebar.about.description-2": "Any policies you modify here override all other information you’ve defined elsewhere in Studio. Do not edit policies unless you are familiar with both their purpose and syntax.",
|
||||
"course-authoring.advanced-settings.sidebar.other.title": "Other course settings",
|
||||
"course-authoring.advanced-settings.sidebar.links.schedule-and-details": "Details & schedule",
|
||||
"course-authoring.advanced-settings.sidebar.links.grading": "Grading",
|
||||
"course-authoring.advanced-settings.sidebar.links.course-team": "Course team",
|
||||
"course-authoring.advanced-settings.sidebar.links.group-configurations": "Group configurations",
|
||||
"course-authoring.advanced-settings.button.deprecated": "Deprecated",
|
||||
"course-authoring.advanced-settings.modal.error.title": "Validation error while saving",
|
||||
"course-authoring.advanced-settings.modal.error.btn.change-manually": "Change manually",
|
||||
"course-authoring.advanced-settings.modal.error.btn.undo-changes": "Undo changes",
|
||||
"course-authoring.advanced-settings.modal.error.description": "There was {errorCounter} while trying to save the course settings in the database. Please check the following validation feedbacks and reflect them in your course settings:",
|
||||
"course-authoring.advanced-settings.policies.description": "{notice} Do not modify these policies unless you are familiar with their purpose.",
|
||||
"course-authoring.advanced-settings.deprecated.button.text": "{visibility} Deprecated Settings",
|
||||
"course-authoring.advanced-settings.button.help": "Show help text",
|
||||
"course-authoring.advanced-settings.alert.button.saving": "Saving"
|
||||
}
|
||||
|
||||
@@ -333,10 +333,10 @@
|
||||
"header.links.filesAndUploads": "Files & Uploads",
|
||||
"header.links.textbooks": "Textbooks",
|
||||
"header.links.videoUploads": "Video Uploads",
|
||||
"header.links.scheduleAndDetails": "Schedule & Details",
|
||||
"header.links.scheduleAndDetails": "Details & schedule",
|
||||
"header.links.grading": "Grading",
|
||||
"header.links.courseTeam": "Course Team",
|
||||
"header.links.groupConfigurations": "Group Configurations",
|
||||
"header.links.courseTeam": "Course team",
|
||||
"header.links.groupConfigurations": "Group configurations",
|
||||
"header.links.proctoredExamSettings": "Proctored Exam Settings",
|
||||
"header.links.advancedSettings": "Advanced Settings",
|
||||
"header.links.certificates": "Certificates",
|
||||
@@ -353,5 +353,41 @@
|
||||
"header.label.main.menu": "Main Menu",
|
||||
"header.label.main.header": "Main",
|
||||
"header.label.secondary.nav": "Secondary",
|
||||
"header.label.courseOutline": "Back to course outline in Studio"
|
||||
}
|
||||
"header.label.courseOutline": "Back to course outline in Studio",
|
||||
"course-authoring.advanced-settings.heading.title": "Advanced settings",
|
||||
"course-authoring.advanced-settings.heading.subtitle": "Settings",
|
||||
"course-authoring.advanced-settings.policies.title": "Manual policy definition",
|
||||
"course-authoring.advanced-settings.alert.warning": "You`ve made some changes",
|
||||
"course-authoring.advanced-settings.alert.warning.descriptions": "Your changes will not take effect until you save your progress. Take care with key and value formatting, as validation is not implemented.",
|
||||
"course-authoring.advanced-settings.alert.success": "Your policy changes have been saved.",
|
||||
"course-authoring.advanced-settings.alert.success.descriptions": "No validation is performed on policy keys or value pairs. If you are having difficulties, check your formatting.",
|
||||
"course-authoring.advanced-settings.alert.proctoring.error": "This course has protected exam setting that are incomplete or invalid.",
|
||||
"course-authoring.advanced-settings.alert.proctoring.error.descriptions": "You will be unable to make changes until the following setting are updated on the page below.",
|
||||
"course-authoring.advanced-settings.alert.button.save": "Save changes",
|
||||
"course-authoring.advanced-settings.alert.button.cancel": "Cancel",
|
||||
"course-authoring.advanced-settings.deprecated.button.show": "Show",
|
||||
"course-authoring.advanced-settings.deprecated.button.hide": "Hide",
|
||||
"course-authoring.advanced-settings.alert.warning.aria.labelledby": "notification-warning-title",
|
||||
"course-authoring.advanced-settings.alert.warning.aria.describedby": "notification-warning-description",
|
||||
"course-authoring.advanced-settings.alert.success.aria.labelledby": "alert-confirmation-title",
|
||||
"course-authoring.advanced-settings.alert.success.aria.describedby": "alert-confirmation-description",
|
||||
"course-authoring.advanced-settings.alert.proctoring.error.aria.labelledby": "alert-danger-title",
|
||||
"course-authoring.advanced-settings.alert.proctoring.error.aria.describedby": "alert-danger-description",
|
||||
"course-authoring.advanced-settings.sidebar.about.title": "What do advanced settings do?",
|
||||
"course-authoring.advanced-settings.sidebar.about.description-1": "Advanced settings control specific course functionality. On this page, you can edit manual policies, which are JSON-based key and value pairs that control specific course settings.",
|
||||
"course-authoring.advanced-settings.sidebar.about.description-2": "Any policies you modify here override all other information you’ve defined elsewhere in Studio. Do not edit policies unless you are familiar with both their purpose and syntax.",
|
||||
"course-authoring.advanced-settings.sidebar.other.title": "Other course settings",
|
||||
"course-authoring.advanced-settings.sidebar.links.schedule-and-details": "Details & schedule",
|
||||
"course-authoring.advanced-settings.sidebar.links.grading": "Grading",
|
||||
"course-authoring.advanced-settings.sidebar.links.course-team": "Course team",
|
||||
"course-authoring.advanced-settings.sidebar.links.group-configurations": "Group configurations",
|
||||
"course-authoring.advanced-settings.button.deprecated": "Deprecated",
|
||||
"course-authoring.advanced-settings.modal.error.title": "Validation error while saving",
|
||||
"course-authoring.advanced-settings.modal.error.btn.change-manually": "Change manually",
|
||||
"course-authoring.advanced-settings.modal.error.btn.undo-changes": "Undo changes",
|
||||
"course-authoring.advanced-settings.modal.error.description": "There was {errorCounter} while trying to save the course settings in the database. Please check the following validation feedbacks and reflect them in your course settings:",
|
||||
"course-authoring.advanced-settings.policies.description": "{notice} Do not modify these policies unless you are familiar with their purpose.",
|
||||
"course-authoring.advanced-settings.deprecated.button.text": "{visibility} Deprecated Settings",
|
||||
"course-authoring.advanced-settings.button.help": "Show help text",
|
||||
"course-authoring.advanced-settings.alert.button.saving": "Saving"
|
||||
}
|
||||
|
||||
@@ -353,5 +353,41 @@
|
||||
"header.label.main.menu": "Hauptmenü",
|
||||
"header.label.main.header": "Hauptseite",
|
||||
"header.label.secondary.nav": "Sekundarschule",
|
||||
"header.label.courseOutline": "Zurück zur Kursübersicht im Studio"
|
||||
}
|
||||
"header.label.courseOutline": "Zurück zur Kursübersicht im Studio",
|
||||
"course-authoring.advanced-settings.heading.title": "Advanced settings",
|
||||
"course-authoring.advanced-settings.heading.subtitle": "Settings",
|
||||
"course-authoring.advanced-settings.policies.title": "Manual policy definition",
|
||||
"course-authoring.advanced-settings.alert.warning": "You`ve made some changes",
|
||||
"course-authoring.advanced-settings.alert.warning.descriptions": "Your changes will not take effect until you save your progress. Take care with key and value formatting, as validation is not implemented.",
|
||||
"course-authoring.advanced-settings.alert.success": "Your policy changes have been saved.",
|
||||
"course-authoring.advanced-settings.alert.success.descriptions": "No validation is performed on policy keys or value pairs. If you are having difficulties, check your formatting.",
|
||||
"course-authoring.advanced-settings.alert.proctoring.error": "This course has protected exam setting that are incomplete or invalid.",
|
||||
"course-authoring.advanced-settings.alert.proctoring.error.descriptions": "You will be unable to make changes until the following setting are updated on the page below.",
|
||||
"course-authoring.advanced-settings.alert.button.save": "Save changes",
|
||||
"course-authoring.advanced-settings.alert.button.cancel": "Cancel",
|
||||
"course-authoring.advanced-settings.deprecated.button.show": "Show",
|
||||
"course-authoring.advanced-settings.deprecated.button.hide": "Hide",
|
||||
"course-authoring.advanced-settings.alert.warning.aria.labelledby": "notification-warning-title",
|
||||
"course-authoring.advanced-settings.alert.warning.aria.describedby": "notification-warning-description",
|
||||
"course-authoring.advanced-settings.alert.success.aria.labelledby": "alert-confirmation-title",
|
||||
"course-authoring.advanced-settings.alert.success.aria.describedby": "alert-confirmation-description",
|
||||
"course-authoring.advanced-settings.alert.proctoring.error.aria.labelledby": "alert-danger-title",
|
||||
"course-authoring.advanced-settings.alert.proctoring.error.aria.describedby": "alert-danger-description",
|
||||
"course-authoring.advanced-settings.sidebar.about.title": "What do advanced settings do?",
|
||||
"course-authoring.advanced-settings.sidebar.about.description-1": "Advanced settings control specific course functionality. On this page, you can edit manual policies, which are JSON-based key and value pairs that control specific course settings.",
|
||||
"course-authoring.advanced-settings.sidebar.about.description-2": "Any policies you modify here override all other information you’ve defined elsewhere in Studio. Do not edit policies unless you are familiar with both their purpose and syntax.",
|
||||
"course-authoring.advanced-settings.sidebar.other.title": "Other course settings",
|
||||
"course-authoring.advanced-settings.sidebar.links.schedule-and-details": "Details & schedule",
|
||||
"course-authoring.advanced-settings.sidebar.links.grading": "Grading",
|
||||
"course-authoring.advanced-settings.sidebar.links.course-team": "Course team",
|
||||
"course-authoring.advanced-settings.sidebar.links.group-configurations": "Group configurations",
|
||||
"course-authoring.advanced-settings.button.deprecated": "Deprecated",
|
||||
"course-authoring.advanced-settings.modal.error.title": "Validation error while saving",
|
||||
"course-authoring.advanced-settings.modal.error.btn.change-manually": "Change manually",
|
||||
"course-authoring.advanced-settings.modal.error.btn.undo-changes": "Undo changes",
|
||||
"course-authoring.advanced-settings.modal.error.description": "There was {errorCounter} while trying to save the course settings in the database. Please check the following validation feedbacks and reflect them in your course settings:",
|
||||
"course-authoring.advanced-settings.policies.description": "{notice} Do not modify these policies unless you are familiar with their purpose.",
|
||||
"course-authoring.advanced-settings.deprecated.button.text": "{visibility} Deprecated Settings",
|
||||
"course-authoring.advanced-settings.button.help": "Show help text",
|
||||
"course-authoring.advanced-settings.alert.button.saving": "Saving"
|
||||
}
|
||||
|
||||
@@ -353,5 +353,41 @@
|
||||
"header.label.main.menu": "Menú Principal",
|
||||
"header.label.main.header": "Principal",
|
||||
"header.label.secondary.nav": "Secondary",
|
||||
"header.label.courseOutline": "Volver al esquema del curso en Studio"
|
||||
}
|
||||
"header.label.courseOutline": "Volver al esquema del curso en Studio",
|
||||
"course-authoring.advanced-settings.heading.title": "Advanced settings",
|
||||
"course-authoring.advanced-settings.heading.subtitle": "Settings",
|
||||
"course-authoring.advanced-settings.policies.title": "Manual policy definition",
|
||||
"course-authoring.advanced-settings.alert.warning": "You`ve made some changes",
|
||||
"course-authoring.advanced-settings.alert.warning.descriptions": "Your changes will not take effect until you save your progress. Take care with key and value formatting, as validation is not implemented.",
|
||||
"course-authoring.advanced-settings.alert.success": "Your policy changes have been saved.",
|
||||
"course-authoring.advanced-settings.alert.success.descriptions": "No validation is performed on policy keys or value pairs. If you are having difficulties, check your formatting.",
|
||||
"course-authoring.advanced-settings.alert.proctoring.error": "This course has protected exam setting that are incomplete or invalid.",
|
||||
"course-authoring.advanced-settings.alert.proctoring.error.descriptions": "You will be unable to make changes until the following setting are updated on the page below.",
|
||||
"course-authoring.advanced-settings.alert.button.save": "Save changes",
|
||||
"course-authoring.advanced-settings.alert.button.cancel": "Cancel",
|
||||
"course-authoring.advanced-settings.deprecated.button.show": "Show",
|
||||
"course-authoring.advanced-settings.deprecated.button.hide": "Hide",
|
||||
"course-authoring.advanced-settings.alert.warning.aria.labelledby": "notification-warning-title",
|
||||
"course-authoring.advanced-settings.alert.warning.aria.describedby": "notification-warning-description",
|
||||
"course-authoring.advanced-settings.alert.success.aria.labelledby": "alert-confirmation-title",
|
||||
"course-authoring.advanced-settings.alert.success.aria.describedby": "alert-confirmation-description",
|
||||
"course-authoring.advanced-settings.alert.proctoring.error.aria.labelledby": "alert-danger-title",
|
||||
"course-authoring.advanced-settings.alert.proctoring.error.aria.describedby": "alert-danger-description",
|
||||
"course-authoring.advanced-settings.sidebar.about.title": "What do advanced settings do?",
|
||||
"course-authoring.advanced-settings.sidebar.about.description-1": "Advanced settings control specific course functionality. On this page, you can edit manual policies, which are JSON-based key and value pairs that control specific course settings.",
|
||||
"course-authoring.advanced-settings.sidebar.about.description-2": "Any policies you modify here override all other information you’ve defined elsewhere in Studio. Do not edit policies unless you are familiar with both their purpose and syntax.",
|
||||
"course-authoring.advanced-settings.sidebar.other.title": "Other course settings",
|
||||
"course-authoring.advanced-settings.sidebar.links.schedule-and-details": "Details & schedule",
|
||||
"course-authoring.advanced-settings.sidebar.links.grading": "Grading",
|
||||
"course-authoring.advanced-settings.sidebar.links.course-team": "Course team",
|
||||
"course-authoring.advanced-settings.sidebar.links.group-configurations": "Group configurations",
|
||||
"course-authoring.advanced-settings.button.deprecated": "Deprecated",
|
||||
"course-authoring.advanced-settings.modal.error.title": "Validation error while saving",
|
||||
"course-authoring.advanced-settings.modal.error.btn.change-manually": "Change manually",
|
||||
"course-authoring.advanced-settings.modal.error.btn.undo-changes": "Undo changes",
|
||||
"course-authoring.advanced-settings.modal.error.description": "There was {errorCounter} while trying to save the course settings in the database. Please check the following validation feedbacks and reflect them in your course settings:",
|
||||
"course-authoring.advanced-settings.policies.description": "{notice} Do not modify these policies unless you are familiar with their purpose.",
|
||||
"course-authoring.advanced-settings.deprecated.button.text": "{visibility} Deprecated Settings",
|
||||
"course-authoring.advanced-settings.button.help": "Show help text",
|
||||
"course-authoring.advanced-settings.alert.button.saving": "Saving"
|
||||
}
|
||||
|
||||
@@ -353,5 +353,41 @@
|
||||
"header.label.main.menu": "Menu Principal",
|
||||
"header.label.main.header": "Principal",
|
||||
"header.label.secondary.nav": "Secondaire",
|
||||
"header.label.courseOutline": "Retour au plan de cours dans Studio"
|
||||
}
|
||||
"header.label.courseOutline": "Retour au plan de cours dans Studio",
|
||||
"course-authoring.advanced-settings.heading.title": "Advanced settings",
|
||||
"course-authoring.advanced-settings.heading.subtitle": "Settings",
|
||||
"course-authoring.advanced-settings.policies.title": "Manual policy definition",
|
||||
"course-authoring.advanced-settings.alert.warning": "You`ve made some changes",
|
||||
"course-authoring.advanced-settings.alert.warning.descriptions": "Your changes will not take effect until you save your progress. Take care with key and value formatting, as validation is not implemented.",
|
||||
"course-authoring.advanced-settings.alert.success": "Your policy changes have been saved.",
|
||||
"course-authoring.advanced-settings.alert.success.descriptions": "No validation is performed on policy keys or value pairs. If you are having difficulties, check your formatting.",
|
||||
"course-authoring.advanced-settings.alert.proctoring.error": "This course has protected exam setting that are incomplete or invalid.",
|
||||
"course-authoring.advanced-settings.alert.proctoring.error.descriptions": "You will be unable to make changes until the following setting are updated on the page below.",
|
||||
"course-authoring.advanced-settings.alert.button.save": "Save changes",
|
||||
"course-authoring.advanced-settings.alert.button.cancel": "Cancel",
|
||||
"course-authoring.advanced-settings.deprecated.button.show": "Show",
|
||||
"course-authoring.advanced-settings.deprecated.button.hide": "Hide",
|
||||
"course-authoring.advanced-settings.alert.warning.aria.labelledby": "notification-warning-title",
|
||||
"course-authoring.advanced-settings.alert.warning.aria.describedby": "notification-warning-description",
|
||||
"course-authoring.advanced-settings.alert.success.aria.labelledby": "alert-confirmation-title",
|
||||
"course-authoring.advanced-settings.alert.success.aria.describedby": "alert-confirmation-description",
|
||||
"course-authoring.advanced-settings.alert.proctoring.error.aria.labelledby": "alert-danger-title",
|
||||
"course-authoring.advanced-settings.alert.proctoring.error.aria.describedby": "alert-danger-description",
|
||||
"course-authoring.advanced-settings.sidebar.about.title": "What do advanced settings do?",
|
||||
"course-authoring.advanced-settings.sidebar.about.description-1": "Advanced settings control specific course functionality. On this page, you can edit manual policies, which are JSON-based key and value pairs that control specific course settings.",
|
||||
"course-authoring.advanced-settings.sidebar.about.description-2": "Any policies you modify here override all other information you’ve defined elsewhere in Studio. Do not edit policies unless you are familiar with both their purpose and syntax.",
|
||||
"course-authoring.advanced-settings.sidebar.other.title": "Other course settings",
|
||||
"course-authoring.advanced-settings.sidebar.links.schedule-and-details": "Details & schedule",
|
||||
"course-authoring.advanced-settings.sidebar.links.grading": "Grading",
|
||||
"course-authoring.advanced-settings.sidebar.links.course-team": "Course team",
|
||||
"course-authoring.advanced-settings.sidebar.links.group-configurations": "Group configurations",
|
||||
"course-authoring.advanced-settings.button.deprecated": "Deprecated",
|
||||
"course-authoring.advanced-settings.modal.error.title": "Validation error while saving",
|
||||
"course-authoring.advanced-settings.modal.error.btn.change-manually": "Change manually",
|
||||
"course-authoring.advanced-settings.modal.error.btn.undo-changes": "Undo changes",
|
||||
"course-authoring.advanced-settings.modal.error.description": "There was {errorCounter} while trying to save the course settings in the database. Please check the following validation feedbacks and reflect them in your course settings:",
|
||||
"course-authoring.advanced-settings.policies.description": "{notice} Do not modify these policies unless you are familiar with their purpose.",
|
||||
"course-authoring.advanced-settings.deprecated.button.text": "{visibility} Deprecated Settings",
|
||||
"course-authoring.advanced-settings.button.help": "Show help text",
|
||||
"course-authoring.advanced-settings.alert.button.saving": "Saving"
|
||||
}
|
||||
|
||||
@@ -353,5 +353,41 @@
|
||||
"header.label.main.menu": "Menu principal",
|
||||
"header.label.main.header": "Principal",
|
||||
"header.label.secondary.nav": "Secondaire",
|
||||
"header.label.courseOutline": "Retour au plan de cours dans Studio"
|
||||
}
|
||||
"header.label.courseOutline": "Retour au plan de cours dans Studio",
|
||||
"course-authoring.advanced-settings.heading.title": "Advanced settings",
|
||||
"course-authoring.advanced-settings.heading.subtitle": "Settings",
|
||||
"course-authoring.advanced-settings.policies.title": "Manual policy definition",
|
||||
"course-authoring.advanced-settings.alert.warning": "You`ve made some changes",
|
||||
"course-authoring.advanced-settings.alert.warning.descriptions": "Your changes will not take effect until you save your progress. Take care with key and value formatting, as validation is not implemented.",
|
||||
"course-authoring.advanced-settings.alert.success": "Your policy changes have been saved.",
|
||||
"course-authoring.advanced-settings.alert.success.descriptions": "No validation is performed on policy keys or value pairs. If you are having difficulties, check your formatting.",
|
||||
"course-authoring.advanced-settings.alert.proctoring.error": "This course has protected exam setting that are incomplete or invalid.",
|
||||
"course-authoring.advanced-settings.alert.proctoring.error.descriptions": "You will be unable to make changes until the following setting are updated on the page below.",
|
||||
"course-authoring.advanced-settings.alert.button.save": "Save changes",
|
||||
"course-authoring.advanced-settings.alert.button.cancel": "Cancel",
|
||||
"course-authoring.advanced-settings.deprecated.button.show": "Show",
|
||||
"course-authoring.advanced-settings.deprecated.button.hide": "Hide",
|
||||
"course-authoring.advanced-settings.alert.warning.aria.labelledby": "notification-warning-title",
|
||||
"course-authoring.advanced-settings.alert.warning.aria.describedby": "notification-warning-description",
|
||||
"course-authoring.advanced-settings.alert.success.aria.labelledby": "alert-confirmation-title",
|
||||
"course-authoring.advanced-settings.alert.success.aria.describedby": "alert-confirmation-description",
|
||||
"course-authoring.advanced-settings.alert.proctoring.error.aria.labelledby": "alert-danger-title",
|
||||
"course-authoring.advanced-settings.alert.proctoring.error.aria.describedby": "alert-danger-description",
|
||||
"course-authoring.advanced-settings.sidebar.about.title": "What do advanced settings do?",
|
||||
"course-authoring.advanced-settings.sidebar.about.description-1": "Advanced settings control specific course functionality. On this page, you can edit manual policies, which are JSON-based key and value pairs that control specific course settings.",
|
||||
"course-authoring.advanced-settings.sidebar.about.description-2": "Any policies you modify here override all other information you’ve defined elsewhere in Studio. Do not edit policies unless you are familiar with both their purpose and syntax.",
|
||||
"course-authoring.advanced-settings.sidebar.other.title": "Other course settings",
|
||||
"course-authoring.advanced-settings.sidebar.links.schedule-and-details": "Details & schedule",
|
||||
"course-authoring.advanced-settings.sidebar.links.grading": "Grading",
|
||||
"course-authoring.advanced-settings.sidebar.links.course-team": "Course team",
|
||||
"course-authoring.advanced-settings.sidebar.links.group-configurations": "Group configurations",
|
||||
"course-authoring.advanced-settings.button.deprecated": "Deprecated",
|
||||
"course-authoring.advanced-settings.modal.error.title": "Validation error while saving",
|
||||
"course-authoring.advanced-settings.modal.error.btn.change-manually": "Change manually",
|
||||
"course-authoring.advanced-settings.modal.error.btn.undo-changes": "Undo changes",
|
||||
"course-authoring.advanced-settings.modal.error.description": "There was {errorCounter} while trying to save the course settings in the database. Please check the following validation feedbacks and reflect them in your course settings:",
|
||||
"course-authoring.advanced-settings.policies.description": "{notice} Do not modify these policies unless you are familiar with their purpose.",
|
||||
"course-authoring.advanced-settings.deprecated.button.text": "{visibility} Deprecated Settings",
|
||||
"course-authoring.advanced-settings.button.help": "Show help text",
|
||||
"course-authoring.advanced-settings.alert.button.saving": "Saving"
|
||||
}
|
||||
|
||||
@@ -333,10 +333,10 @@
|
||||
"header.links.filesAndUploads": "Files & Uploads",
|
||||
"header.links.textbooks": "Textbooks",
|
||||
"header.links.videoUploads": "Video Uploads",
|
||||
"header.links.scheduleAndDetails": "Schedule & Details",
|
||||
"header.links.scheduleAndDetails": "Details & schedule",
|
||||
"header.links.grading": "Grading",
|
||||
"header.links.courseTeam": "Course Team",
|
||||
"header.links.groupConfigurations": "Group Configurations",
|
||||
"header.links.courseTeam": "Course team",
|
||||
"header.links.groupConfigurations": "Group configurations",
|
||||
"header.links.proctoredExamSettings": "Proctored Exam Settings",
|
||||
"header.links.advancedSettings": "Advanced Settings",
|
||||
"header.links.certificates": "Certificates",
|
||||
@@ -353,5 +353,41 @@
|
||||
"header.label.main.menu": "Main Menu",
|
||||
"header.label.main.header": "Main",
|
||||
"header.label.secondary.nav": "Secondary",
|
||||
"header.label.courseOutline": "Back to course outline in Studio"
|
||||
}
|
||||
"header.label.courseOutline": "Back to course outline in Studio",
|
||||
"course-authoring.advanced-settings.heading.title": "Advanced settings",
|
||||
"course-authoring.advanced-settings.heading.subtitle": "Settings",
|
||||
"course-authoring.advanced-settings.policies.title": "Manual policy definition",
|
||||
"course-authoring.advanced-settings.alert.warning": "You`ve made some changes",
|
||||
"course-authoring.advanced-settings.alert.warning.descriptions": "Your changes will not take effect until you save your progress. Take care with key and value formatting, as validation is not implemented.",
|
||||
"course-authoring.advanced-settings.alert.success": "Your policy changes have been saved.",
|
||||
"course-authoring.advanced-settings.alert.success.descriptions": "No validation is performed on policy keys or value pairs. If you are having difficulties, check your formatting.",
|
||||
"course-authoring.advanced-settings.alert.proctoring.error": "This course has protected exam setting that are incomplete or invalid.",
|
||||
"course-authoring.advanced-settings.alert.proctoring.error.descriptions": "You will be unable to make changes until the following setting are updated on the page below.",
|
||||
"course-authoring.advanced-settings.alert.button.save": "Save changes",
|
||||
"course-authoring.advanced-settings.alert.button.cancel": "Cancel",
|
||||
"course-authoring.advanced-settings.deprecated.button.show": "Show",
|
||||
"course-authoring.advanced-settings.deprecated.button.hide": "Hide",
|
||||
"course-authoring.advanced-settings.alert.warning.aria.labelledby": "notification-warning-title",
|
||||
"course-authoring.advanced-settings.alert.warning.aria.describedby": "notification-warning-description",
|
||||
"course-authoring.advanced-settings.alert.success.aria.labelledby": "alert-confirmation-title",
|
||||
"course-authoring.advanced-settings.alert.success.aria.describedby": "alert-confirmation-description",
|
||||
"course-authoring.advanced-settings.alert.proctoring.error.aria.labelledby": "alert-danger-title",
|
||||
"course-authoring.advanced-settings.alert.proctoring.error.aria.describedby": "alert-danger-description",
|
||||
"course-authoring.advanced-settings.sidebar.about.title": "What do advanced settings do?",
|
||||
"course-authoring.advanced-settings.sidebar.about.description-1": "Advanced settings control specific course functionality. On this page, you can edit manual policies, which are JSON-based key and value pairs that control specific course settings.",
|
||||
"course-authoring.advanced-settings.sidebar.about.description-2": "Any policies you modify here override all other information you’ve defined elsewhere in Studio. Do not edit policies unless you are familiar with both their purpose and syntax.",
|
||||
"course-authoring.advanced-settings.sidebar.other.title": "Other course settings",
|
||||
"course-authoring.advanced-settings.sidebar.links.schedule-and-details": "Details & schedule",
|
||||
"course-authoring.advanced-settings.sidebar.links.grading": "Grading",
|
||||
"course-authoring.advanced-settings.sidebar.links.course-team": "Course team",
|
||||
"course-authoring.advanced-settings.sidebar.links.group-configurations": "Group configurations",
|
||||
"course-authoring.advanced-settings.button.deprecated": "Deprecated",
|
||||
"course-authoring.advanced-settings.modal.error.title": "Validation error while saving",
|
||||
"course-authoring.advanced-settings.modal.error.btn.change-manually": "Change manually",
|
||||
"course-authoring.advanced-settings.modal.error.btn.undo-changes": "Undo changes",
|
||||
"course-authoring.advanced-settings.modal.error.description": "There was {errorCounter} while trying to save the course settings in the database. Please check the following validation feedbacks and reflect them in your course settings:",
|
||||
"course-authoring.advanced-settings.policies.description": "{notice} Do not modify these policies unless you are familiar with their purpose.",
|
||||
"course-authoring.advanced-settings.deprecated.button.text": "{visibility} Deprecated Settings",
|
||||
"course-authoring.advanced-settings.button.help": "Show help text",
|
||||
"course-authoring.advanced-settings.alert.button.saving": "Saving"
|
||||
}
|
||||
|
||||
@@ -333,10 +333,10 @@
|
||||
"header.links.filesAndUploads": "Files & Uploads",
|
||||
"header.links.textbooks": "Textbooks",
|
||||
"header.links.videoUploads": "Video Uploads",
|
||||
"header.links.scheduleAndDetails": "Schedule & Details",
|
||||
"header.links.scheduleAndDetails": "Details & schedule",
|
||||
"header.links.grading": "Grading",
|
||||
"header.links.courseTeam": "Course Team",
|
||||
"header.links.groupConfigurations": "Group Configurations",
|
||||
"header.links.courseTeam": "Course team",
|
||||
"header.links.groupConfigurations": "Group configurations",
|
||||
"header.links.proctoredExamSettings": "Proctored Exam Settings",
|
||||
"header.links.advancedSettings": "Advanced Settings",
|
||||
"header.links.certificates": "Certificates",
|
||||
@@ -353,5 +353,41 @@
|
||||
"header.label.main.menu": "Main Menu",
|
||||
"header.label.main.header": "Main",
|
||||
"header.label.secondary.nav": "Secondary",
|
||||
"header.label.courseOutline": "Back to course outline in Studio"
|
||||
}
|
||||
"header.label.courseOutline": "Back to course outline in Studio",
|
||||
"course-authoring.advanced-settings.heading.title": "Advanced settings",
|
||||
"course-authoring.advanced-settings.heading.subtitle": "Settings",
|
||||
"course-authoring.advanced-settings.policies.title": "Manual policy definition",
|
||||
"course-authoring.advanced-settings.alert.warning": "You`ve made some changes",
|
||||
"course-authoring.advanced-settings.alert.warning.descriptions": "Your changes will not take effect until you save your progress. Take care with key and value formatting, as validation is not implemented.",
|
||||
"course-authoring.advanced-settings.alert.success": "Your policy changes have been saved.",
|
||||
"course-authoring.advanced-settings.alert.success.descriptions": "No validation is performed on policy keys or value pairs. If you are having difficulties, check your formatting.",
|
||||
"course-authoring.advanced-settings.alert.proctoring.error": "This course has protected exam setting that are incomplete or invalid.",
|
||||
"course-authoring.advanced-settings.alert.proctoring.error.descriptions": "You will be unable to make changes until the following setting are updated on the page below.",
|
||||
"course-authoring.advanced-settings.alert.button.save": "Save changes",
|
||||
"course-authoring.advanced-settings.alert.button.cancel": "Cancel",
|
||||
"course-authoring.advanced-settings.deprecated.button.show": "Show",
|
||||
"course-authoring.advanced-settings.deprecated.button.hide": "Hide",
|
||||
"course-authoring.advanced-settings.alert.warning.aria.labelledby": "notification-warning-title",
|
||||
"course-authoring.advanced-settings.alert.warning.aria.describedby": "notification-warning-description",
|
||||
"course-authoring.advanced-settings.alert.success.aria.labelledby": "alert-confirmation-title",
|
||||
"course-authoring.advanced-settings.alert.success.aria.describedby": "alert-confirmation-description",
|
||||
"course-authoring.advanced-settings.alert.proctoring.error.aria.labelledby": "alert-danger-title",
|
||||
"course-authoring.advanced-settings.alert.proctoring.error.aria.describedby": "alert-danger-description",
|
||||
"course-authoring.advanced-settings.sidebar.about.title": "What do advanced settings do?",
|
||||
"course-authoring.advanced-settings.sidebar.about.description-1": "Advanced settings control specific course functionality. On this page, you can edit manual policies, which are JSON-based key and value pairs that control specific course settings.",
|
||||
"course-authoring.advanced-settings.sidebar.about.description-2": "Any policies you modify here override all other information you’ve defined elsewhere in Studio. Do not edit policies unless you are familiar with both their purpose and syntax.",
|
||||
"course-authoring.advanced-settings.sidebar.other.title": "Other course settings",
|
||||
"course-authoring.advanced-settings.sidebar.links.schedule-and-details": "Details & schedule",
|
||||
"course-authoring.advanced-settings.sidebar.links.grading": "Grading",
|
||||
"course-authoring.advanced-settings.sidebar.links.course-team": "Course team",
|
||||
"course-authoring.advanced-settings.sidebar.links.group-configurations": "Group configurations",
|
||||
"course-authoring.advanced-settings.button.deprecated": "Deprecated",
|
||||
"course-authoring.advanced-settings.modal.error.title": "Validation error while saving",
|
||||
"course-authoring.advanced-settings.modal.error.btn.change-manually": "Change manually",
|
||||
"course-authoring.advanced-settings.modal.error.btn.undo-changes": "Undo changes",
|
||||
"course-authoring.advanced-settings.modal.error.description": "There was {errorCounter} while trying to save the course settings in the database. Please check the following validation feedbacks and reflect them in your course settings:",
|
||||
"course-authoring.advanced-settings.policies.description": "{notice} Do not modify these policies unless you are familiar with their purpose.",
|
||||
"course-authoring.advanced-settings.deprecated.button.text": "{visibility} Deprecated Settings",
|
||||
"course-authoring.advanced-settings.button.help": "Show help text",
|
||||
"course-authoring.advanced-settings.alert.button.saving": "Saving"
|
||||
}
|
||||
|
||||
@@ -353,5 +353,41 @@
|
||||
"header.label.main.menu": "Menu principale",
|
||||
"header.label.main.header": "Principale",
|
||||
"header.label.secondary.nav": "Superiori",
|
||||
"header.label.courseOutline": "Torna alla struttura del corso in Studio"
|
||||
}
|
||||
"header.label.courseOutline": "Torna alla struttura del corso in Studio",
|
||||
"course-authoring.advanced-settings.heading.title": "Advanced settings",
|
||||
"course-authoring.advanced-settings.heading.subtitle": "Settings",
|
||||
"course-authoring.advanced-settings.policies.title": "Manual policy definition",
|
||||
"course-authoring.advanced-settings.alert.warning": "You`ve made some changes",
|
||||
"course-authoring.advanced-settings.alert.warning.descriptions": "Your changes will not take effect until you save your progress. Take care with key and value formatting, as validation is not implemented.",
|
||||
"course-authoring.advanced-settings.alert.success": "Your policy changes have been saved.",
|
||||
"course-authoring.advanced-settings.alert.success.descriptions": "No validation is performed on policy keys or value pairs. If you are having difficulties, check your formatting.",
|
||||
"course-authoring.advanced-settings.alert.proctoring.error": "This course has protected exam setting that are incomplete or invalid.",
|
||||
"course-authoring.advanced-settings.alert.proctoring.error.descriptions": "You will be unable to make changes until the following setting are updated on the page below.",
|
||||
"course-authoring.advanced-settings.alert.button.save": "Save changes",
|
||||
"course-authoring.advanced-settings.alert.button.cancel": "Cancel",
|
||||
"course-authoring.advanced-settings.deprecated.button.show": "Show",
|
||||
"course-authoring.advanced-settings.deprecated.button.hide": "Hide",
|
||||
"course-authoring.advanced-settings.alert.warning.aria.labelledby": "notification-warning-title",
|
||||
"course-authoring.advanced-settings.alert.warning.aria.describedby": "notification-warning-description",
|
||||
"course-authoring.advanced-settings.alert.success.aria.labelledby": "alert-confirmation-title",
|
||||
"course-authoring.advanced-settings.alert.success.aria.describedby": "alert-confirmation-description",
|
||||
"course-authoring.advanced-settings.alert.proctoring.error.aria.labelledby": "alert-danger-title",
|
||||
"course-authoring.advanced-settings.alert.proctoring.error.aria.describedby": "alert-danger-description",
|
||||
"course-authoring.advanced-settings.sidebar.about.title": "What do advanced settings do?",
|
||||
"course-authoring.advanced-settings.sidebar.about.description-1": "Advanced settings control specific course functionality. On this page, you can edit manual policies, which are JSON-based key and value pairs that control specific course settings.",
|
||||
"course-authoring.advanced-settings.sidebar.about.description-2": "Any policies you modify here override all other information you’ve defined elsewhere in Studio. Do not edit policies unless you are familiar with both their purpose and syntax.",
|
||||
"course-authoring.advanced-settings.sidebar.other.title": "Other course settings",
|
||||
"course-authoring.advanced-settings.sidebar.links.schedule-and-details": "Details & schedule",
|
||||
"course-authoring.advanced-settings.sidebar.links.grading": "Grading",
|
||||
"course-authoring.advanced-settings.sidebar.links.course-team": "Course team",
|
||||
"course-authoring.advanced-settings.sidebar.links.group-configurations": "Group configurations",
|
||||
"course-authoring.advanced-settings.button.deprecated": "Deprecated",
|
||||
"course-authoring.advanced-settings.modal.error.title": "Validation error while saving",
|
||||
"course-authoring.advanced-settings.modal.error.btn.change-manually": "Change manually",
|
||||
"course-authoring.advanced-settings.modal.error.btn.undo-changes": "Undo changes",
|
||||
"course-authoring.advanced-settings.modal.error.description": "There was {errorCounter} while trying to save the course settings in the database. Please check the following validation feedbacks and reflect them in your course settings:",
|
||||
"course-authoring.advanced-settings.policies.description": "{notice} Do not modify these policies unless you are familiar with their purpose.",
|
||||
"course-authoring.advanced-settings.deprecated.button.text": "{visibility} Deprecated Settings",
|
||||
"course-authoring.advanced-settings.button.help": "Show help text",
|
||||
"course-authoring.advanced-settings.alert.button.saving": "Saving"
|
||||
}
|
||||
|
||||
@@ -333,10 +333,10 @@
|
||||
"header.links.filesAndUploads": "Files & Uploads",
|
||||
"header.links.textbooks": "Textbooks",
|
||||
"header.links.videoUploads": "Video Uploads",
|
||||
"header.links.scheduleAndDetails": "Schedule & Details",
|
||||
"header.links.scheduleAndDetails": "Details & schedule",
|
||||
"header.links.grading": "Grading",
|
||||
"header.links.courseTeam": "Course Team",
|
||||
"header.links.groupConfigurations": "Group Configurations",
|
||||
"header.links.courseTeam": "Course team",
|
||||
"header.links.groupConfigurations": "Group configurations",
|
||||
"header.links.proctoredExamSettings": "Proctored Exam Settings",
|
||||
"header.links.advancedSettings": "Advanced Settings",
|
||||
"header.links.certificates": "Certificates",
|
||||
@@ -353,5 +353,41 @@
|
||||
"header.label.main.menu": "Main Menu",
|
||||
"header.label.main.header": "Main",
|
||||
"header.label.secondary.nav": "Secondary",
|
||||
"header.label.courseOutline": "Back to course outline in Studio"
|
||||
}
|
||||
"header.label.courseOutline": "Back to course outline in Studio",
|
||||
"course-authoring.advanced-settings.heading.title": "Advanced settings",
|
||||
"course-authoring.advanced-settings.heading.subtitle": "Settings",
|
||||
"course-authoring.advanced-settings.policies.title": "Manual policy definition",
|
||||
"course-authoring.advanced-settings.alert.warning": "You`ve made some changes",
|
||||
"course-authoring.advanced-settings.alert.warning.descriptions": "Your changes will not take effect until you save your progress. Take care with key and value formatting, as validation is not implemented.",
|
||||
"course-authoring.advanced-settings.alert.success": "Your policy changes have been saved.",
|
||||
"course-authoring.advanced-settings.alert.success.descriptions": "No validation is performed on policy keys or value pairs. If you are having difficulties, check your formatting.",
|
||||
"course-authoring.advanced-settings.alert.proctoring.error": "This course has protected exam setting that are incomplete or invalid.",
|
||||
"course-authoring.advanced-settings.alert.proctoring.error.descriptions": "You will be unable to make changes until the following setting are updated on the page below.",
|
||||
"course-authoring.advanced-settings.alert.button.save": "Save changes",
|
||||
"course-authoring.advanced-settings.alert.button.cancel": "Cancel",
|
||||
"course-authoring.advanced-settings.deprecated.button.show": "Show",
|
||||
"course-authoring.advanced-settings.deprecated.button.hide": "Hide",
|
||||
"course-authoring.advanced-settings.alert.warning.aria.labelledby": "notification-warning-title",
|
||||
"course-authoring.advanced-settings.alert.warning.aria.describedby": "notification-warning-description",
|
||||
"course-authoring.advanced-settings.alert.success.aria.labelledby": "alert-confirmation-title",
|
||||
"course-authoring.advanced-settings.alert.success.aria.describedby": "alert-confirmation-description",
|
||||
"course-authoring.advanced-settings.alert.proctoring.error.aria.labelledby": "alert-danger-title",
|
||||
"course-authoring.advanced-settings.alert.proctoring.error.aria.describedby": "alert-danger-description",
|
||||
"course-authoring.advanced-settings.sidebar.about.title": "What do advanced settings do?",
|
||||
"course-authoring.advanced-settings.sidebar.about.description-1": "Advanced settings control specific course functionality. On this page, you can edit manual policies, which are JSON-based key and value pairs that control specific course settings.",
|
||||
"course-authoring.advanced-settings.sidebar.about.description-2": "Any policies you modify here override all other information you’ve defined elsewhere in Studio. Do not edit policies unless you are familiar with both their purpose and syntax.",
|
||||
"course-authoring.advanced-settings.sidebar.other.title": "Other course settings",
|
||||
"course-authoring.advanced-settings.sidebar.links.schedule-and-details": "Details & schedule",
|
||||
"course-authoring.advanced-settings.sidebar.links.grading": "Grading",
|
||||
"course-authoring.advanced-settings.sidebar.links.course-team": "Course team",
|
||||
"course-authoring.advanced-settings.sidebar.links.group-configurations": "Group configurations",
|
||||
"course-authoring.advanced-settings.button.deprecated": "Deprecated",
|
||||
"course-authoring.advanced-settings.modal.error.title": "Validation error while saving",
|
||||
"course-authoring.advanced-settings.modal.error.btn.change-manually": "Change manually",
|
||||
"course-authoring.advanced-settings.modal.error.btn.undo-changes": "Undo changes",
|
||||
"course-authoring.advanced-settings.modal.error.description": "There was {errorCounter} while trying to save the course settings in the database. Please check the following validation feedbacks and reflect them in your course settings:",
|
||||
"course-authoring.advanced-settings.policies.description": "{notice} Do not modify these policies unless you are familiar with their purpose.",
|
||||
"course-authoring.advanced-settings.deprecated.button.text": "{visibility} Deprecated Settings",
|
||||
"course-authoring.advanced-settings.button.help": "Show help text",
|
||||
"course-authoring.advanced-settings.alert.button.saving": "Saving"
|
||||
}
|
||||
|
||||
@@ -353,5 +353,41 @@
|
||||
"header.label.main.menu": "Menu Principal",
|
||||
"header.label.main.header": "Principal",
|
||||
"header.label.secondary.nav": "Secundário",
|
||||
"header.label.courseOutline": "Voltar ao resumo do curso no Studio"
|
||||
}
|
||||
"header.label.courseOutline": "Voltar ao resumo do curso no Studio",
|
||||
"course-authoring.advanced-settings.heading.title": "Advanced settings",
|
||||
"course-authoring.advanced-settings.heading.subtitle": "Settings",
|
||||
"course-authoring.advanced-settings.policies.title": "Manual policy definition",
|
||||
"course-authoring.advanced-settings.alert.warning": "You`ve made some changes",
|
||||
"course-authoring.advanced-settings.alert.warning.descriptions": "Your changes will not take effect until you save your progress. Take care with key and value formatting, as validation is not implemented.",
|
||||
"course-authoring.advanced-settings.alert.success": "Your policy changes have been saved.",
|
||||
"course-authoring.advanced-settings.alert.success.descriptions": "No validation is performed on policy keys or value pairs. If you are having difficulties, check your formatting.",
|
||||
"course-authoring.advanced-settings.alert.proctoring.error": "This course has protected exam setting that are incomplete or invalid.",
|
||||
"course-authoring.advanced-settings.alert.proctoring.error.descriptions": "You will be unable to make changes until the following setting are updated on the page below.",
|
||||
"course-authoring.advanced-settings.alert.button.save": "Save changes",
|
||||
"course-authoring.advanced-settings.alert.button.cancel": "Cancel",
|
||||
"course-authoring.advanced-settings.deprecated.button.show": "Show",
|
||||
"course-authoring.advanced-settings.deprecated.button.hide": "Hide",
|
||||
"course-authoring.advanced-settings.alert.warning.aria.labelledby": "notification-warning-title",
|
||||
"course-authoring.advanced-settings.alert.warning.aria.describedby": "notification-warning-description",
|
||||
"course-authoring.advanced-settings.alert.success.aria.labelledby": "alert-confirmation-title",
|
||||
"course-authoring.advanced-settings.alert.success.aria.describedby": "alert-confirmation-description",
|
||||
"course-authoring.advanced-settings.alert.proctoring.error.aria.labelledby": "alert-danger-title",
|
||||
"course-authoring.advanced-settings.alert.proctoring.error.aria.describedby": "alert-danger-description",
|
||||
"course-authoring.advanced-settings.sidebar.about.title": "What do advanced settings do?",
|
||||
"course-authoring.advanced-settings.sidebar.about.description-1": "Advanced settings control specific course functionality. On this page, you can edit manual policies, which are JSON-based key and value pairs that control specific course settings.",
|
||||
"course-authoring.advanced-settings.sidebar.about.description-2": "Any policies you modify here override all other information you’ve defined elsewhere in Studio. Do not edit policies unless you are familiar with both their purpose and syntax.",
|
||||
"course-authoring.advanced-settings.sidebar.other.title": "Other course settings",
|
||||
"course-authoring.advanced-settings.sidebar.links.schedule-and-details": "Details & schedule",
|
||||
"course-authoring.advanced-settings.sidebar.links.grading": "Grading",
|
||||
"course-authoring.advanced-settings.sidebar.links.course-team": "Course team",
|
||||
"course-authoring.advanced-settings.sidebar.links.group-configurations": "Group configurations",
|
||||
"course-authoring.advanced-settings.button.deprecated": "Deprecated",
|
||||
"course-authoring.advanced-settings.modal.error.title": "Validation error while saving",
|
||||
"course-authoring.advanced-settings.modal.error.btn.change-manually": "Change manually",
|
||||
"course-authoring.advanced-settings.modal.error.btn.undo-changes": "Undo changes",
|
||||
"course-authoring.advanced-settings.modal.error.description": "There was {errorCounter} while trying to save the course settings in the database. Please check the following validation feedbacks and reflect them in your course settings:",
|
||||
"course-authoring.advanced-settings.policies.description": "{notice} Do not modify these policies unless you are familiar with their purpose.",
|
||||
"course-authoring.advanced-settings.deprecated.button.text": "{visibility} Deprecated Settings",
|
||||
"course-authoring.advanced-settings.button.help": "Show help text",
|
||||
"course-authoring.advanced-settings.alert.button.saving": "Saving"
|
||||
}
|
||||
|
||||
@@ -333,10 +333,10 @@
|
||||
"header.links.filesAndUploads": "Files & Uploads",
|
||||
"header.links.textbooks": "Textbooks",
|
||||
"header.links.videoUploads": "Video Uploads",
|
||||
"header.links.scheduleAndDetails": "Schedule & Details",
|
||||
"header.links.scheduleAndDetails": "Details & schedule",
|
||||
"header.links.grading": "Grading",
|
||||
"header.links.courseTeam": "Course Team",
|
||||
"header.links.groupConfigurations": "Group Configurations",
|
||||
"header.links.courseTeam": "Course team",
|
||||
"header.links.groupConfigurations": "Group configurations",
|
||||
"header.links.proctoredExamSettings": "Proctored Exam Settings",
|
||||
"header.links.advancedSettings": "Advanced Settings",
|
||||
"header.links.certificates": "Certificates",
|
||||
@@ -353,5 +353,41 @@
|
||||
"header.label.main.menu": "Main Menu",
|
||||
"header.label.main.header": "Main",
|
||||
"header.label.secondary.nav": "Secondary",
|
||||
"header.label.courseOutline": "Back to course outline in Studio"
|
||||
}
|
||||
"header.label.courseOutline": "Back to course outline in Studio",
|
||||
"course-authoring.advanced-settings.heading.title": "Advanced settings",
|
||||
"course-authoring.advanced-settings.heading.subtitle": "Settings",
|
||||
"course-authoring.advanced-settings.policies.title": "Manual policy definition",
|
||||
"course-authoring.advanced-settings.alert.warning": "You`ve made some changes",
|
||||
"course-authoring.advanced-settings.alert.warning.descriptions": "Your changes will not take effect until you save your progress. Take care with key and value formatting, as validation is not implemented.",
|
||||
"course-authoring.advanced-settings.alert.success": "Your policy changes have been saved.",
|
||||
"course-authoring.advanced-settings.alert.success.descriptions": "No validation is performed on policy keys or value pairs. If you are having difficulties, check your formatting.",
|
||||
"course-authoring.advanced-settings.alert.proctoring.error": "This course has protected exam setting that are incomplete or invalid.",
|
||||
"course-authoring.advanced-settings.alert.proctoring.error.descriptions": "You will be unable to make changes until the following setting are updated on the page below.",
|
||||
"course-authoring.advanced-settings.alert.button.save": "Save changes",
|
||||
"course-authoring.advanced-settings.alert.button.cancel": "Cancel",
|
||||
"course-authoring.advanced-settings.deprecated.button.show": "Show",
|
||||
"course-authoring.advanced-settings.deprecated.button.hide": "Hide",
|
||||
"course-authoring.advanced-settings.alert.warning.aria.labelledby": "notification-warning-title",
|
||||
"course-authoring.advanced-settings.alert.warning.aria.describedby": "notification-warning-description",
|
||||
"course-authoring.advanced-settings.alert.success.aria.labelledby": "alert-confirmation-title",
|
||||
"course-authoring.advanced-settings.alert.success.aria.describedby": "alert-confirmation-description",
|
||||
"course-authoring.advanced-settings.alert.proctoring.error.aria.labelledby": "alert-danger-title",
|
||||
"course-authoring.advanced-settings.alert.proctoring.error.aria.describedby": "alert-danger-description",
|
||||
"course-authoring.advanced-settings.sidebar.about.title": "What do advanced settings do?",
|
||||
"course-authoring.advanced-settings.sidebar.about.description-1": "Advanced settings control specific course functionality. On this page, you can edit manual policies, which are JSON-based key and value pairs that control specific course settings.",
|
||||
"course-authoring.advanced-settings.sidebar.about.description-2": "Any policies you modify here override all other information you’ve defined elsewhere in Studio. Do not edit policies unless you are familiar with both their purpose and syntax.",
|
||||
"course-authoring.advanced-settings.sidebar.other.title": "Other course settings",
|
||||
"course-authoring.advanced-settings.sidebar.links.schedule-and-details": "Details & schedule",
|
||||
"course-authoring.advanced-settings.sidebar.links.grading": "Grading",
|
||||
"course-authoring.advanced-settings.sidebar.links.course-team": "Course team",
|
||||
"course-authoring.advanced-settings.sidebar.links.group-configurations": "Group configurations",
|
||||
"course-authoring.advanced-settings.button.deprecated": "Deprecated",
|
||||
"course-authoring.advanced-settings.modal.error.title": "Validation error while saving",
|
||||
"course-authoring.advanced-settings.modal.error.btn.change-manually": "Change manually",
|
||||
"course-authoring.advanced-settings.modal.error.btn.undo-changes": "Undo changes",
|
||||
"course-authoring.advanced-settings.modal.error.description": "There was {errorCounter} while trying to save the course settings in the database. Please check the following validation feedbacks and reflect them in your course settings:",
|
||||
"course-authoring.advanced-settings.policies.description": "{notice} Do not modify these policies unless you are familiar with their purpose.",
|
||||
"course-authoring.advanced-settings.deprecated.button.text": "{visibility} Deprecated Settings",
|
||||
"course-authoring.advanced-settings.button.help": "Show help text",
|
||||
"course-authoring.advanced-settings.alert.button.saving": "Saving"
|
||||
}
|
||||
|
||||
@@ -333,10 +333,10 @@
|
||||
"header.links.filesAndUploads": "Files & Uploads",
|
||||
"header.links.textbooks": "Textbooks",
|
||||
"header.links.videoUploads": "Video Uploads",
|
||||
"header.links.scheduleAndDetails": "Schedule & Details",
|
||||
"header.links.scheduleAndDetails": "Details & schedule",
|
||||
"header.links.grading": "Grading",
|
||||
"header.links.courseTeam": "Course Team",
|
||||
"header.links.groupConfigurations": "Group Configurations",
|
||||
"header.links.courseTeam": "Course team",
|
||||
"header.links.groupConfigurations": "Group configurations",
|
||||
"header.links.proctoredExamSettings": "Proctored Exam Settings",
|
||||
"header.links.advancedSettings": "Advanced Settings",
|
||||
"header.links.certificates": "Certificates",
|
||||
@@ -353,5 +353,41 @@
|
||||
"header.label.main.menu": "Main Menu",
|
||||
"header.label.main.header": "Main",
|
||||
"header.label.secondary.nav": "Secondary",
|
||||
"header.label.courseOutline": "Back to course outline in Studio"
|
||||
}
|
||||
"header.label.courseOutline": "Back to course outline in Studio",
|
||||
"course-authoring.advanced-settings.heading.title": "Advanced settings",
|
||||
"course-authoring.advanced-settings.heading.subtitle": "Settings",
|
||||
"course-authoring.advanced-settings.policies.title": "Manual policy definition",
|
||||
"course-authoring.advanced-settings.alert.warning": "You`ve made some changes",
|
||||
"course-authoring.advanced-settings.alert.warning.descriptions": "Your changes will not take effect until you save your progress. Take care with key and value formatting, as validation is not implemented.",
|
||||
"course-authoring.advanced-settings.alert.success": "Your policy changes have been saved.",
|
||||
"course-authoring.advanced-settings.alert.success.descriptions": "No validation is performed on policy keys or value pairs. If you are having difficulties, check your formatting.",
|
||||
"course-authoring.advanced-settings.alert.proctoring.error": "This course has protected exam setting that are incomplete or invalid.",
|
||||
"course-authoring.advanced-settings.alert.proctoring.error.descriptions": "You will be unable to make changes until the following setting are updated on the page below.",
|
||||
"course-authoring.advanced-settings.alert.button.save": "Save changes",
|
||||
"course-authoring.advanced-settings.alert.button.cancel": "Cancel",
|
||||
"course-authoring.advanced-settings.deprecated.button.show": "Show",
|
||||
"course-authoring.advanced-settings.deprecated.button.hide": "Hide",
|
||||
"course-authoring.advanced-settings.alert.warning.aria.labelledby": "notification-warning-title",
|
||||
"course-authoring.advanced-settings.alert.warning.aria.describedby": "notification-warning-description",
|
||||
"course-authoring.advanced-settings.alert.success.aria.labelledby": "alert-confirmation-title",
|
||||
"course-authoring.advanced-settings.alert.success.aria.describedby": "alert-confirmation-description",
|
||||
"course-authoring.advanced-settings.alert.proctoring.error.aria.labelledby": "alert-danger-title",
|
||||
"course-authoring.advanced-settings.alert.proctoring.error.aria.describedby": "alert-danger-description",
|
||||
"course-authoring.advanced-settings.sidebar.about.title": "What do advanced settings do?",
|
||||
"course-authoring.advanced-settings.sidebar.about.description-1": "Advanced settings control specific course functionality. On this page, you can edit manual policies, which are JSON-based key and value pairs that control specific course settings.",
|
||||
"course-authoring.advanced-settings.sidebar.about.description-2": "Any policies you modify here override all other information you’ve defined elsewhere in Studio. Do not edit policies unless you are familiar with both their purpose and syntax.",
|
||||
"course-authoring.advanced-settings.sidebar.other.title": "Other course settings",
|
||||
"course-authoring.advanced-settings.sidebar.links.schedule-and-details": "Details & schedule",
|
||||
"course-authoring.advanced-settings.sidebar.links.grading": "Grading",
|
||||
"course-authoring.advanced-settings.sidebar.links.course-team": "Course team",
|
||||
"course-authoring.advanced-settings.sidebar.links.group-configurations": "Group configurations",
|
||||
"course-authoring.advanced-settings.button.deprecated": "Deprecated",
|
||||
"course-authoring.advanced-settings.modal.error.title": "Validation error while saving",
|
||||
"course-authoring.advanced-settings.modal.error.btn.change-manually": "Change manually",
|
||||
"course-authoring.advanced-settings.modal.error.btn.undo-changes": "Undo changes",
|
||||
"course-authoring.advanced-settings.modal.error.description": "There was {errorCounter} while trying to save the course settings in the database. Please check the following validation feedbacks and reflect them in your course settings:",
|
||||
"course-authoring.advanced-settings.policies.description": "{notice} Do not modify these policies unless you are familiar with their purpose.",
|
||||
"course-authoring.advanced-settings.deprecated.button.text": "{visibility} Deprecated Settings",
|
||||
"course-authoring.advanced-settings.button.help": "Show help text",
|
||||
"course-authoring.advanced-settings.alert.button.saving": "Saving"
|
||||
}
|
||||
|
||||
@@ -333,10 +333,10 @@
|
||||
"header.links.filesAndUploads": "Files & Uploads",
|
||||
"header.links.textbooks": "Textbooks",
|
||||
"header.links.videoUploads": "Video Uploads",
|
||||
"header.links.scheduleAndDetails": "Schedule & Details",
|
||||
"header.links.scheduleAndDetails": "Details & schedule",
|
||||
"header.links.grading": "Grading",
|
||||
"header.links.courseTeam": "Course Team",
|
||||
"header.links.groupConfigurations": "Group Configurations",
|
||||
"header.links.courseTeam": "Course team",
|
||||
"header.links.groupConfigurations": "Group configurations",
|
||||
"header.links.proctoredExamSettings": "Proctored Exam Settings",
|
||||
"header.links.advancedSettings": "Advanced Settings",
|
||||
"header.links.certificates": "Certificates",
|
||||
@@ -353,5 +353,41 @@
|
||||
"header.label.main.menu": "Main Menu",
|
||||
"header.label.main.header": "Main",
|
||||
"header.label.secondary.nav": "Secondary",
|
||||
"header.label.courseOutline": "Back to course outline in Studio"
|
||||
}
|
||||
"header.label.courseOutline": "Back to course outline in Studio",
|
||||
"course-authoring.advanced-settings.heading.title": "Advanced settings",
|
||||
"course-authoring.advanced-settings.heading.subtitle": "Settings",
|
||||
"course-authoring.advanced-settings.policies.title": "Manual policy definition",
|
||||
"course-authoring.advanced-settings.alert.warning": "You`ve made some changes",
|
||||
"course-authoring.advanced-settings.alert.warning.descriptions": "Your changes will not take effect until you save your progress. Take care with key and value formatting, as validation is not implemented.",
|
||||
"course-authoring.advanced-settings.alert.success": "Your policy changes have been saved.",
|
||||
"course-authoring.advanced-settings.alert.success.descriptions": "No validation is performed on policy keys or value pairs. If you are having difficulties, check your formatting.",
|
||||
"course-authoring.advanced-settings.alert.proctoring.error": "This course has protected exam setting that are incomplete or invalid.",
|
||||
"course-authoring.advanced-settings.alert.proctoring.error.descriptions": "You will be unable to make changes until the following setting are updated on the page below.",
|
||||
"course-authoring.advanced-settings.alert.button.save": "Save changes",
|
||||
"course-authoring.advanced-settings.alert.button.cancel": "Cancel",
|
||||
"course-authoring.advanced-settings.deprecated.button.show": "Show",
|
||||
"course-authoring.advanced-settings.deprecated.button.hide": "Hide",
|
||||
"course-authoring.advanced-settings.alert.warning.aria.labelledby": "notification-warning-title",
|
||||
"course-authoring.advanced-settings.alert.warning.aria.describedby": "notification-warning-description",
|
||||
"course-authoring.advanced-settings.alert.success.aria.labelledby": "alert-confirmation-title",
|
||||
"course-authoring.advanced-settings.alert.success.aria.describedby": "alert-confirmation-description",
|
||||
"course-authoring.advanced-settings.alert.proctoring.error.aria.labelledby": "alert-danger-title",
|
||||
"course-authoring.advanced-settings.alert.proctoring.error.aria.describedby": "alert-danger-description",
|
||||
"course-authoring.advanced-settings.sidebar.about.title": "What do advanced settings do?",
|
||||
"course-authoring.advanced-settings.sidebar.about.description-1": "Advanced settings control specific course functionality. On this page, you can edit manual policies, which are JSON-based key and value pairs that control specific course settings.",
|
||||
"course-authoring.advanced-settings.sidebar.about.description-2": "Any policies you modify here override all other information you’ve defined elsewhere in Studio. Do not edit policies unless you are familiar with both their purpose and syntax.",
|
||||
"course-authoring.advanced-settings.sidebar.other.title": "Other course settings",
|
||||
"course-authoring.advanced-settings.sidebar.links.schedule-and-details": "Details & schedule",
|
||||
"course-authoring.advanced-settings.sidebar.links.grading": "Grading",
|
||||
"course-authoring.advanced-settings.sidebar.links.course-team": "Course team",
|
||||
"course-authoring.advanced-settings.sidebar.links.group-configurations": "Group configurations",
|
||||
"course-authoring.advanced-settings.button.deprecated": "Deprecated",
|
||||
"course-authoring.advanced-settings.modal.error.title": "Validation error while saving",
|
||||
"course-authoring.advanced-settings.modal.error.btn.change-manually": "Change manually",
|
||||
"course-authoring.advanced-settings.modal.error.btn.undo-changes": "Undo changes",
|
||||
"course-authoring.advanced-settings.modal.error.description": "There was {errorCounter} while trying to save the course settings in the database. Please check the following validation feedbacks and reflect them in your course settings:",
|
||||
"course-authoring.advanced-settings.policies.description": "{notice} Do not modify these policies unless you are familiar with their purpose.",
|
||||
"course-authoring.advanced-settings.deprecated.button.text": "{visibility} Deprecated Settings",
|
||||
"course-authoring.advanced-settings.button.help": "Show help text",
|
||||
"course-authoring.advanced-settings.alert.button.saving": "Saving"
|
||||
}
|
||||
|
||||
@@ -15,10 +15,11 @@ import Placeholder from '@edx/frontend-lib-content-components';
|
||||
import messages from './i18n';
|
||||
|
||||
import initializeStore from './store';
|
||||
import './index.scss';
|
||||
import CourseAuthoringRoutes from './CourseAuthoringRoutes';
|
||||
import Head from './head/Head';
|
||||
|
||||
import './index.scss';
|
||||
|
||||
const App = () => {
|
||||
useEffect(() => {
|
||||
if (process.env.HOTJAR_APP_ID) {
|
||||
@@ -41,7 +42,7 @@ const App = () => {
|
||||
<Route path="/home">
|
||||
{process.env.ENABLE_NEW_HOME_PAGE === 'true'
|
||||
&& (
|
||||
<Placeholder />
|
||||
<Placeholder />
|
||||
)}
|
||||
</Route>
|
||||
<Route
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
@import "~@edx/brand/paragon/variables";
|
||||
@import "~@edx/paragon/scss/core/core";
|
||||
@import "~@edx/brand/paragon/overrides";
|
||||
|
||||
@import "studio-header/header";
|
||||
@import "~@edx/frontend-component-footer/dist/footer";
|
||||
|
||||
@import "proctored-exam-settings/proctoredExamSettings";
|
||||
@import "pages-and-resources/discussions/app-list/AppList";
|
||||
@import "advanced-settings/scss/AdvencedSettings";
|
||||
@import "generic/styles";
|
||||
|
||||
@@ -13,9 +13,10 @@ import DiscussionsSettings from './discussions';
|
||||
import PageGrid from './pages/PageGrid';
|
||||
import { fetchCourseApps } from './data/thunks';
|
||||
import { useModels } from '../generic/model-store';
|
||||
import { getLoadingStatus } from './data/selectors';
|
||||
import { getCourseAppsApiStatus, getLoadingStatus } from './data/selectors';
|
||||
import PagesAndResourcesProvider from './PagesAndResourcesProvider';
|
||||
import { RequestStatus } from '../data/constants';
|
||||
import PermissionDeniedAlert from '../generic/PermissionDeniedAlert';
|
||||
|
||||
const PagesAndResources = ({ courseId, intl }) => {
|
||||
const { path, url } = useRouteMatch();
|
||||
@@ -27,6 +28,7 @@ const PagesAndResources = ({ courseId, intl }) => {
|
||||
|
||||
const courseAppIds = useSelector(state => state.pagesAndResources.courseAppIds);
|
||||
const loadingStatus = useSelector(getLoadingStatus);
|
||||
const courseAppsApiStatus = useSelector(getCourseAppsApiStatus);
|
||||
|
||||
const { config } = useContext(AppContext);
|
||||
const learningCourseURL = `${config.LEARNING_BASE_URL}/course/${courseId}`;
|
||||
@@ -38,6 +40,12 @@ const PagesAndResources = ({ courseId, intl }) => {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
if (courseAppsApiStatus === RequestStatus.DENIED) {
|
||||
return (
|
||||
<PermissionDeniedAlert />
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<PagesAndResourcesProvider courseId={courseId}>
|
||||
<main className="container container-mw-md px-3">
|
||||
|
||||
@@ -36,16 +36,16 @@ import messages from './messages';
|
||||
const AppSettingsForm = ({
|
||||
formikProps, children, showForm,
|
||||
}) => children && (
|
||||
<TransitionReplace>
|
||||
{showForm ? (
|
||||
<React.Fragment key="app-enabled">
|
||||
{children(formikProps)}
|
||||
</React.Fragment>
|
||||
) : (
|
||||
<React.Fragment key="app-disabled" />
|
||||
)}
|
||||
</TransitionReplace>
|
||||
);
|
||||
<TransitionReplace>
|
||||
{showForm ? (
|
||||
<React.Fragment key="app-enabled">
|
||||
{children(formikProps)}
|
||||
</React.Fragment>
|
||||
) : (
|
||||
<React.Fragment key="app-disabled" />
|
||||
)}
|
||||
</TransitionReplace>
|
||||
);
|
||||
|
||||
AppSettingsForm.propTypes = {
|
||||
// Ignore the warning here since we're just passing along the props as-is and the child component should validate
|
||||
@@ -88,7 +88,7 @@ const AppSettingsModalBase = ({
|
||||
</ActionRow>
|
||||
</ModalDialog.Footer>
|
||||
</ModalDialog>
|
||||
);
|
||||
);
|
||||
|
||||
AppSettingsModalBase.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
|
||||
@@ -15,7 +15,7 @@ const CalculatorSettings = ({ intl, onClose }) => (
|
||||
learnMoreText={intl.formatMessage(messages.enableCalculatorLink)}
|
||||
onClose={onClose}
|
||||
/>
|
||||
);
|
||||
);
|
||||
|
||||
CalculatorSettings.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
|
||||
@@ -143,7 +143,7 @@ describe('OpenedXConfigForm', () => {
|
||||
...legacyApiResponse.plugin_configuration,
|
||||
reported_content_email_notifications_flag: true,
|
||||
divided_course_wide_discussions: [],
|
||||
available_division_schemes: [],
|
||||
available_division_schemes: [],
|
||||
},
|
||||
});
|
||||
createComponent();
|
||||
@@ -214,7 +214,7 @@ describe('OpenedXConfigForm', () => {
|
||||
});
|
||||
|
||||
test(
|
||||
'folded discussion topics are in the DOM when divideByCohorts and divideCourseWideTopics are enabled',
|
||||
'folded discussion topics are in the DOM when divideByCohorts and divideCourseWideTopics are enabled',
|
||||
async () => {
|
||||
await mockStore({
|
||||
...legacyApiResponse,
|
||||
@@ -238,7 +238,7 @@ describe('OpenedXConfigForm', () => {
|
||||
expect(container.querySelector(`#checkbox-${id}`)).not.toBeInTheDocument();
|
||||
});
|
||||
},
|
||||
);
|
||||
);
|
||||
|
||||
const updateTopicName = async (topicId, topicName) => {
|
||||
const topicCard = queryByTestId(container, topicId);
|
||||
@@ -267,7 +267,7 @@ describe('OpenedXConfigForm', () => {
|
||||
};
|
||||
|
||||
test(
|
||||
'show required error on field when leaving empty topic name',
|
||||
'show required error on field when leaving empty topic name',
|
||||
async () => {
|
||||
await mockStore(legacyApiResponse);
|
||||
createComponent();
|
||||
@@ -277,7 +277,7 @@ describe('OpenedXConfigForm', () => {
|
||||
assertTopicNameRequiredValidation(topicCard);
|
||||
assertHasErrorValidation();
|
||||
},
|
||||
);
|
||||
);
|
||||
|
||||
test('check field is not collapsible in case of error', async () => {
|
||||
await mockStore(legacyApiResponse);
|
||||
|
||||
@@ -23,7 +23,7 @@ const AnonymousPostingFields = ({
|
||||
helpText={intl.formatMessage(messages.allowAnonymousPostsPeersHelp)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
);
|
||||
|
||||
AnonymousPostingFields.propTypes = {
|
||||
onBlur: PropTypes.func.isRequired,
|
||||
|
||||
@@ -5,17 +5,17 @@ import React from 'react';
|
||||
const AppConfigFormDivider = ({ thick, marginAdj }) => (
|
||||
<hr
|
||||
className={classNames(
|
||||
'my-2 mx-n4 border-light-300',
|
||||
{
|
||||
[`mx-sm-n${marginAdj.sm}`]: marginAdj.sm !== null,
|
||||
[`mx-n${marginAdj.default}`]: marginAdj.default !== null,
|
||||
},
|
||||
)}
|
||||
'my-2 mx-n4 border-light-300',
|
||||
{
|
||||
[`mx-sm-n${marginAdj.sm}`]: marginAdj.sm !== null,
|
||||
[`mx-n${marginAdj.default}`]: marginAdj.default !== null,
|
||||
},
|
||||
)}
|
||||
style={{
|
||||
borderTopWidth: thick ? '3px' : '1px',
|
||||
}}
|
||||
borderTopWidth: thick ? '3px' : '1px',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
);
|
||||
|
||||
AppConfigFormDivider.propTypes = {
|
||||
thick: PropTypes.bool,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useEffect, useContext } from 'react';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
Form, TransitionReplace, Hyperlink, Alert,
|
||||
Form, TransitionReplace, Hyperlink, Alert,
|
||||
} from '@edx/paragon';
|
||||
import { AppContext } from '@edx/frontend-platform/react';
|
||||
import { FieldArray, useFormikContext } from 'formik';
|
||||
@@ -70,12 +70,12 @@ const DivisionByGroupFields = ({ intl }) => {
|
||||
</h5>
|
||||
{!cohortsEnabled
|
||||
&& (
|
||||
<Alert className="bg-light-200 font-weight-normal h5" id="alert">
|
||||
{intl.formatMessage(messages.cohortsEnabled)}
|
||||
<Hyperlink destination={learningCourseURL} target="_blank">
|
||||
{intl.formatMessage(messages.instructorDashboard)}
|
||||
</Hyperlink>
|
||||
</Alert>
|
||||
<Alert className="bg-light-200 font-weight-normal h5" id="alert">
|
||||
{intl.formatMessage(messages.cohortsEnabled)}
|
||||
<Hyperlink destination={learningCourseURL} target="_blank">
|
||||
{intl.formatMessage(messages.instructorDashboard)}
|
||||
</Hyperlink>
|
||||
</Alert>
|
||||
)}
|
||||
<FormSwitchGroup
|
||||
onChange={handleChange}
|
||||
|
||||
@@ -40,7 +40,7 @@ const InContextDiscussionFields = ({
|
||||
onCancel={() => setShowPopup(false)}
|
||||
cancelLabel={intl.formatMessage(messages.cancelButton)}
|
||||
/>
|
||||
)
|
||||
)
|
||||
: (
|
||||
<FormSwitchGroup
|
||||
onChange={() => setShowPopup(true)}
|
||||
@@ -50,7 +50,7 @@ const InContextDiscussionFields = ({
|
||||
label={intl.formatMessage(messages.gradedUnitPagesLabel)}
|
||||
helpText={intl.formatMessage(messages.gradedUnitPagesHelp)}
|
||||
/>
|
||||
)}
|
||||
)}
|
||||
<AppConfigFormDivider />
|
||||
<FormSwitchGroup
|
||||
onChange={onChange}
|
||||
|
||||
@@ -37,7 +37,7 @@ const DiscussionRestrictionItem = ({
|
||||
return setCollapseOpen(isOpen);
|
||||
}, [hasError]);
|
||||
|
||||
const handleOnClose = useCallback(() => {
|
||||
const handleOnClose = useCallback(() => {
|
||||
['startDate', 'startTime', 'endDate', 'endTime'].forEach(field => (
|
||||
setFieldTouched(`${fieldNameCommonBase}.${field}`, true)
|
||||
));
|
||||
@@ -70,8 +70,8 @@ const DiscussionRestrictionItem = ({
|
||||
return (
|
||||
<ConfirmationPopup
|
||||
label={restrictedDate.status === constants.ACTIVE
|
||||
? intl.formatMessage(messages.activeRestrictedDatesDeletionLabel)
|
||||
: intl.formatMessage(messages.restrictedDatesDeletionLabel)}
|
||||
? intl.formatMessage(messages.activeRestrictedDatesDeletionLabel)
|
||||
: intl.formatMessage(messages.restrictedDatesDeletionLabel)}
|
||||
bodyText={intl.formatMessage(deleteRestrictedDatesHelperText[restrictedDate.status])}
|
||||
onConfirm={onDelete}
|
||||
confirmLabel={intl.formatMessage(messages.deleteButton)}
|
||||
@@ -80,8 +80,8 @@ const DiscussionRestrictionItem = ({
|
||||
confirmVariant="plain"
|
||||
confirmButtonClass="text-danger-500 border-gray-300 rounded-0"
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<CollapsableEditor
|
||||
|
||||
@@ -149,7 +149,7 @@ const messages = defineMessages({
|
||||
defaultMessage: 'Questions for the TAs',
|
||||
description: 'Label for a checkbox allowing a user to divide the Questions for the TAs (TA stands for "teaching assistant") course wide topic by cohorts.',
|
||||
},
|
||||
cohortsEnabled: {
|
||||
cohortsEnabled: {
|
||||
id: 'authoring.discussions.builtIn.cohortsEnabled.label',
|
||||
defaultMessage: 'To adjust these settings, enable cohorts on the ',
|
||||
description: 'Label text informing the user to enable cohort',
|
||||
|
||||
@@ -49,10 +49,10 @@ export const decodeDateTime = (date, time) => {
|
||||
|
||||
export const sortRestrictedDatesByStatus = (data, status, order) => (
|
||||
_.orderBy(
|
||||
data.filter(date => date.status === status),
|
||||
data.filter(date => date.status === status),
|
||||
[(obj) => decodeDateTime(obj.startDate, startOfDayTime(obj.startTime))],
|
||||
[order],
|
||||
)
|
||||
[order],
|
||||
)
|
||||
);
|
||||
|
||||
export const formatRestrictedDates = ({
|
||||
|
||||
@@ -26,8 +26,8 @@ const AppList = ({ intl }) => {
|
||||
const ltiProvider = !['openedx', 'legacy'].includes(activeAppId);
|
||||
|
||||
const showOneEdxProvider = useMemo(() => apps.filter(app => (
|
||||
activeAppId === 'openedx' ? app.id !== 'legacy' : app.id !== 'openedx'
|
||||
)), [activeAppId]);
|
||||
activeAppId === 'openedx' ? app.id !== 'legacy' : app.id !== 'openedx'
|
||||
)), [activeAppId]);
|
||||
|
||||
// This could be a bit confusing. activeAppId is the ID of the app that is currently configured
|
||||
// according to the server. selectedAppId is the ID of the app that we _want_ to configure here
|
||||
|
||||
@@ -9,7 +9,8 @@
|
||||
background-color: $white;
|
||||
text-align: center;
|
||||
|
||||
.d-flex, span {
|
||||
.d-flex,
|
||||
span {
|
||||
display: block !important;
|
||||
}
|
||||
}
|
||||
@@ -20,21 +21,20 @@
|
||||
.pgn__data-table-cell-wrap {
|
||||
max-width: unset;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.height-36{
|
||||
.height-36 {
|
||||
height: 2.25rem !important;
|
||||
}
|
||||
|
||||
.line-height-20{
|
||||
.line-height-20 {
|
||||
line-height: 1.25rem !important;
|
||||
}
|
||||
|
||||
.font-size-14{
|
||||
.font-size-14 {
|
||||
font-size: 14px !important;
|
||||
}
|
||||
|
||||
@@ -42,18 +42,18 @@
|
||||
font-weight: 500 !important;
|
||||
}
|
||||
|
||||
.py-7px{
|
||||
padding: 0 7px 0 7px ;
|
||||
.py-7px {
|
||||
padding: 0 7px;
|
||||
}
|
||||
|
||||
.discussion-restriction{
|
||||
.unselected-button{
|
||||
&:hover{
|
||||
background: #e9e6e4 ;
|
||||
.discussion-restriction {
|
||||
.unselected-button {
|
||||
&:hover {
|
||||
background: #E9E6E4 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.action-btn{
|
||||
.action-btn {
|
||||
padding: 10px 16px;
|
||||
width: 80px;
|
||||
height: 44px;
|
||||
@@ -62,28 +62,30 @@
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
.w-92{
|
||||
.w-92 {
|
||||
width: 92px;
|
||||
}
|
||||
|
||||
.card-body-section{
|
||||
.card-body-section {
|
||||
padding-top: 12px !important;
|
||||
padding-bottom: 20px !important;
|
||||
}
|
||||
|
||||
.form-control{
|
||||
border-radius: 0px !important;
|
||||
.form-control {
|
||||
border-radius: 0 !important;
|
||||
font-weight: 400;
|
||||
font-size: 14px;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
.collapsible-card{
|
||||
.collapsible-card {
|
||||
padding: 14px 14px 14px 24px !important;
|
||||
min-height:100px;
|
||||
.collapsible-trigger{
|
||||
padding: 0px !important;
|
||||
.badge{
|
||||
min-height: 100px;
|
||||
|
||||
.collapsible-trigger {
|
||||
padding: 0 !important;
|
||||
|
||||
.badge {
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/* eslint-disable react/jsx-no-constructed-context-values */
|
||||
import React from 'react';
|
||||
import {
|
||||
render, screen, within, queryAllByRole,
|
||||
render, screen, within, queryAllByRole,
|
||||
} from '@testing-library/react';
|
||||
import { initializeMockApp } from '@edx/frontend-platform';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
|
||||
@@ -18,16 +18,16 @@ const FeaturesList = ({ app, intl }) => (
|
||||
{intl.formatMessage(messages['supportedFeatureList-mobile-hide'])}
|
||||
</Collapsible.Visible>
|
||||
</>
|
||||
)}
|
||||
)}
|
||||
styling="basic"
|
||||
>
|
||||
{app.featureIds.map((id) => (
|
||||
<div key={`collapsible-${app.id}&${id}`} className="d-flex mb-1">
|
||||
<SupportedFeature name={intl.formatMessage(messages[`featureName-${id}`])} />
|
||||
</div>
|
||||
))}
|
||||
))}
|
||||
</Collapsible>
|
||||
);
|
||||
);
|
||||
|
||||
export default injectIntl(FeaturesList);
|
||||
|
||||
|
||||
@@ -32,10 +32,10 @@ const FeaturesTable = ({ apps, features, intl }) => {
|
||||
data-testid={`${app.id}-${feature.id.replaceAll('.', '-')}`}
|
||||
>
|
||||
{
|
||||
app.featureIds.includes(feature.id)
|
||||
? <Check id="check-icon" className="text-success-500" />
|
||||
: <Remove id="remove-icon" className="text-light-700" />
|
||||
}
|
||||
app.featureIds.includes(feature.id)
|
||||
? <Check id="check-icon" className="text-success-500" />
|
||||
: <Remove id="remove-icon" className="text-light-700" />
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ const SupportedFeature = ({ name }) => (
|
||||
</span>
|
||||
{name}
|
||||
</>
|
||||
);
|
||||
);
|
||||
|
||||
SupportedFeature.propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
|
||||
@@ -15,7 +15,7 @@ const NotesSettings = ({ intl, onClose }) => (
|
||||
learnMoreText={intl.formatMessage(messages.enableNotesLink)}
|
||||
onClose={onClose}
|
||||
/>
|
||||
);
|
||||
);
|
||||
|
||||
NotesSettings.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
|
||||
@@ -56,11 +56,11 @@ const renderComponent = () => {
|
||||
};
|
||||
|
||||
const mockStore = async ({
|
||||
usernameSharing = false,
|
||||
emailSharing = false,
|
||||
enabled = true,
|
||||
piiSharingAllowed = true,
|
||||
isFreeTier = false,
|
||||
usernameSharing = false,
|
||||
emailSharing = false,
|
||||
enabled = true,
|
||||
piiSharingAllowed = true,
|
||||
isFreeTier = false,
|
||||
}) => {
|
||||
const fetchProviderConfigUrl = `${providersApiUrl}/${courseId}/`;
|
||||
const fetchLiveConfigUrl = `${providerConfigurationApiUrl}/${courseId}/`;
|
||||
@@ -108,7 +108,7 @@ describe('BBB Settings', () => {
|
||||
});
|
||||
|
||||
test(
|
||||
'Connect to support and PII sharing message is visible and plans selection is disabled, When pii sharing is disabled, ',
|
||||
'Connect to support and PII sharing message is visible and plans selection is disabled, When pii sharing is disabled, ',
|
||||
async () => {
|
||||
await mockStore({ piiSharingAllowed: false });
|
||||
renderComponent();
|
||||
@@ -122,7 +122,7 @@ describe('BBB Settings', () => {
|
||||
expect(helpRequestPiiText).toHaveTextContent(messages.piiSharingEnableHelpTextBbb.defaultMessage);
|
||||
expect(container.querySelector('select[name="tierType"]')).toBeDisabled();
|
||||
},
|
||||
);
|
||||
);
|
||||
|
||||
test('free plans message is visible when free plan is selected', async () => {
|
||||
await mockStore({ emailSharing: true, isFreeTier: true });
|
||||
@@ -132,7 +132,7 @@ describe('BBB Settings', () => {
|
||||
const dropDown = container.querySelector('select[name="tierType"]');
|
||||
userEvent.selectOptions(
|
||||
dropDown,
|
||||
getByRole(dropDown, 'option', { name: 'Free' }),
|
||||
getByRole(dropDown, 'option', { name: 'Free' }),
|
||||
);
|
||||
expect(queryByTestId(container, 'free-plan-message')).toBeInTheDocument();
|
||||
expect(queryByTestId(container, 'free-plan-message')).toHaveTextContent(messages.freePlanMessage.defaultMessage);
|
||||
|
||||
@@ -5,8 +5,8 @@ import messages from './messages';
|
||||
import FormikControl from '../../generic/FormikControl';
|
||||
|
||||
const LiveCommonFields = ({
|
||||
intl,
|
||||
values,
|
||||
intl,
|
||||
values,
|
||||
}) => (
|
||||
<>
|
||||
<p className="pb-2">{intl.formatMessage(messages.formInstructions)}</p>
|
||||
@@ -32,7 +32,7 @@ const LiveCommonFields = ({
|
||||
type="input"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
);
|
||||
|
||||
LiveCommonFields.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
|
||||
@@ -91,36 +91,36 @@ const LiveSettings = ({
|
||||
{({ values, setFieldValue }) => (
|
||||
(status === RequestStatus.IN_PROGRESS) ? (
|
||||
<Loading />
|
||||
) : (
|
||||
<>
|
||||
<h4 className="my-3">{intl.formatMessage(messages.selectProvider)}</h4>
|
||||
<SelectableBox.Set
|
||||
type="checkbox"
|
||||
value={values.provider}
|
||||
onChange={(event) => handleProviderChange(event.target.value, setFieldValue, values)}
|
||||
name="provider"
|
||||
columns={3}
|
||||
className="mb-3"
|
||||
>
|
||||
{availableProviders.map((provider) => (
|
||||
<SelectableBox value={provider} type="checkbox" key={provider}>
|
||||
<div className="d-flex flex-column align-items-center">
|
||||
<Icon src={iconsSrc[`${camelCase(provider)}`]} alt={provider} />
|
||||
<span>{intl.formatMessage(messages[`appName-${camelCase(provider)}`])}</span>
|
||||
</div>
|
||||
</SelectableBox>
|
||||
))}
|
||||
</SelectableBox.Set>
|
||||
{values.provider === 'zoom' ? <ZoomSettings values={values} />
|
||||
: (
|
||||
<BBBSettings
|
||||
values={values}
|
||||
setFieldValue={setFieldValue}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
)}
|
||||
) : (
|
||||
<>
|
||||
<h4 className="my-3">{intl.formatMessage(messages.selectProvider)}</h4>
|
||||
<SelectableBox.Set
|
||||
type="checkbox"
|
||||
value={values.provider}
|
||||
onChange={(event) => handleProviderChange(event.target.value, setFieldValue, values)}
|
||||
name="provider"
|
||||
columns={3}
|
||||
className="mb-3"
|
||||
>
|
||||
{availableProviders.map((provider) => (
|
||||
<SelectableBox value={provider} type="checkbox" key={provider}>
|
||||
<div className="d-flex flex-column align-items-center">
|
||||
<Icon src={iconsSrc[`${camelCase(provider)}`]} alt={provider} />
|
||||
<span>{intl.formatMessage(messages[`appName-${camelCase(provider)}`])}</span>
|
||||
</div>
|
||||
</SelectableBox>
|
||||
))}
|
||||
</SelectableBox.Set>
|
||||
{values.provider === 'zoom' ? <ZoomSettings values={values} />
|
||||
: (
|
||||
<BBBSettings
|
||||
values={values}
|
||||
setFieldValue={setFieldValue}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
)}
|
||||
</AppSettingsModal>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -59,10 +59,10 @@ const renderComponent = () => {
|
||||
};
|
||||
|
||||
const mockStore = async ({
|
||||
usernameSharing = false,
|
||||
emailSharing = false,
|
||||
enabled = true,
|
||||
piiSharingAllowed = true,
|
||||
usernameSharing = false,
|
||||
emailSharing = false,
|
||||
enabled = true,
|
||||
piiSharingAllowed = true,
|
||||
}) => {
|
||||
const fetchProviderConfigUrl = `${providersApiUrl}/${courseId}/`;
|
||||
const fetchLiveConfigUrl = `${providerConfigurationApiUrl}/${courseId}/`;
|
||||
|
||||
@@ -7,8 +7,8 @@ import LiveCommonFields from './LiveCommonFields';
|
||||
import FormikControl from '../../generic/FormikControl';
|
||||
|
||||
const ZoomSettings = ({
|
||||
intl,
|
||||
values,
|
||||
intl,
|
||||
values,
|
||||
}) => (
|
||||
// eslint-disable-next-line react/jsx-no-useless-fragment
|
||||
<>
|
||||
@@ -16,25 +16,25 @@ const ZoomSettings = ({
|
||||
<p data-testid="request-pii-sharing">
|
||||
{intl.formatMessage(messages.requestPiiSharingEnable, { provider: providerNames[values.provider] })}
|
||||
</p>
|
||||
) : (
|
||||
<>
|
||||
{(values.piiSharingEmail || values.piiSharingUsername)
|
||||
) : (
|
||||
<>
|
||||
{(values.piiSharingEmail || values.piiSharingUsername)
|
||||
&& (
|
||||
<p data-testid="helper-text">
|
||||
{intl.formatMessage(messages.providerHelperText, { providerName: providerNames[values.provider] })}
|
||||
</p>
|
||||
)}
|
||||
<LiveCommonFields values={values} />
|
||||
<FormikControl
|
||||
name="launchEmail"
|
||||
value={values.launchEmail}
|
||||
floatingLabel={intl.formatMessage(messages.launchEmail)}
|
||||
type="input"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<LiveCommonFields values={values} />
|
||||
<FormikControl
|
||||
name="launchEmail"
|
||||
value={values.launchEmail}
|
||||
floatingLabel={intl.formatMessage(messages.launchEmail)}
|
||||
type="input"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
);
|
||||
|
||||
ZoomSettings.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
|
||||
@@ -54,10 +54,10 @@ const renderComponent = () => {
|
||||
};
|
||||
|
||||
const mockStore = async ({
|
||||
usernameSharing = false,
|
||||
emailSharing = false,
|
||||
enabled = true,
|
||||
piiSharingAllowed = true,
|
||||
usernameSharing = false,
|
||||
emailSharing = false,
|
||||
enabled = true,
|
||||
piiSharingAllowed = true,
|
||||
}) => {
|
||||
const fetchProviderConfigUrl = `${providersApiUrl}/${courseId}/`;
|
||||
const fetchLiveConfigUrl = `${providerConfigurationApiUrl}/${courseId}/`;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user