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:
Peter Kulko
2023-07-19 17:45:50 +03:00
committed by GitHub
parent e05e6325c9
commit 87ead24e20
120 changed files with 4032 additions and 640 deletions

View File

@@ -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

View File

@@ -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'

View File

@@ -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
View 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
View File

@@ -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,

View File

@@ -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",

View File

@@ -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>

View File

@@ -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 () => {

View File

@@ -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>

View 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);

View 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');
});
});
});

View 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: [],
},
};

View File

@@ -0,0 +1,2 @@
// eslint-disable-next-line import/prefer-default-export
export { default as advancedSettingsMock } from './advancedSettings';

View 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);
}

View 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;

View 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;

View 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;
}
};
}

View File

@@ -0,0 +1,2 @@
/* eslint-disable import/prefer-default-export */
export { default as AdvancedSettings } from './AdvancedSettings';

View 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;

View 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);

View 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();
});
});

View 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;

View File

@@ -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');
});
});

View 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;

View 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;
}
}

View File

@@ -0,0 +1,2 @@
$text-color-base: $gray-700;
$setting-form-control-width: 34.375rem;

View 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;

View 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);
});
});

View 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);

View 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();
});
});

View 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;

View 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);

View File

@@ -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();
});
});

View 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 youve 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;

View 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;
}

View 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);
});
});

View File

@@ -0,0 +1,2 @@
$text-color-base: $gray-700;
$text-color-weak: #3E3E3C;

View File

@@ -75,7 +75,7 @@ const mockStore = async ({
blockId,
metadata: { courseStaffOnly: visibility },
}), store.dispatch);
};
};
describe('CustomPageCard', () => {
beforeEach(async () => {

View File

@@ -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 () => {

View File

@@ -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 }));

View File

@@ -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 = (

View 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;

View File

@@ -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,

View File

@@ -32,7 +32,7 @@ const ConfirmationPopup = ({
</Card.Footer>
</Card.Body>
</Card>
);
);
ConfirmationPopup.propTypes = {
label: PropTypes.string.isRequired,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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;

View File

@@ -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;

View File

@@ -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,

View 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);
});
});

View 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;

View 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;
}
}

View 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();
});
});

View 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);

View 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;

View 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;

View 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
View File

@@ -0,0 +1,2 @@
@import "./help-sidebar/HelpSidebar";
@import "./sub-header/SubHeader";

View 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;

View 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;
}

View File

@@ -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 youve 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"
}

View File

@@ -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 youve 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"
}

View File

@@ -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 youve 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"
}

View File

@@ -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 youve 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"
}

View File

@@ -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 youve 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"
}

View File

@@ -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 youve 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"
}

View File

@@ -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 youve 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"
}

View File

@@ -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 youve 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"
}

View File

@@ -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 youve 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"
}

View File

@@ -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 youve 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"
}

View File

@@ -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 youve 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"
}

View File

@@ -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 youve 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"
}

View File

@@ -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 youve 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"
}

View File

@@ -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 youve 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"
}

View File

@@ -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

View File

@@ -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";

View File

@@ -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">

View File

@@ -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,

View File

@@ -15,7 +15,7 @@ const CalculatorSettings = ({ intl, onClose }) => (
learnMoreText={intl.formatMessage(messages.enableCalculatorLink)}
onClose={onClose}
/>
);
);
CalculatorSettings.propTypes = {
intl: intlShape.isRequired,

View File

@@ -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);

View File

@@ -23,7 +23,7 @@ const AnonymousPostingFields = ({
helpText={intl.formatMessage(messages.allowAnonymousPostsPeersHelp)}
/>
</>
);
);
AnonymousPostingFields.propTypes = {
onBlur: PropTypes.func.isRequired,

View File

@@ -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,

View File

@@ -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}

View File

@@ -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}

View File

@@ -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

View File

@@ -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',

View File

@@ -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 = ({

View File

@@ -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

View File

@@ -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;
}

View File

@@ -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';

View File

@@ -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);

View File

@@ -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>
);
}

View File

@@ -9,7 +9,7 @@ const SupportedFeature = ({ name }) => (
</span>
{name}
</>
);
);
SupportedFeature.propTypes = {
name: PropTypes.string.isRequired,

View File

@@ -15,7 +15,7 @@ const NotesSettings = ({ intl, onClose }) => (
learnMoreText={intl.formatMessage(messages.enableNotesLink)}
onClose={onClose}
/>
);
);
NotesSettings.propTypes = {
intl: intlShape.isRequired,

View File

@@ -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);

View File

@@ -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,

View File

@@ -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>
);
};

View File

@@ -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}/`;

View File

@@ -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,

View File

@@ -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