From 3c661e15cbee19a5013ffb2228aa4489bcf5d922 Mon Sep 17 00:00:00 2001 From: Braden MacDonald Date: Wed, 28 Feb 2024 08:50:54 -0800 Subject: [PATCH] Convert "Pages & Resources" page to a plugin system (#638) * feat: Make "Pages & Resources" course apps into plugins * feat: move ora_settings * feat: move proctoring * feat: move progress * feat: move teams * feat: move wiki * feat: move Xpert settings * fix: add webpack.prod.config.js * fix: clean up unused parts of package.json files * feat: Add an error message when displaying a Course App Plugin fails * chore: fix various eslint warnings * chore: fix jest tests * fix: error preventing "npm ci" from working * feat: better tests for * chore: move xpert_unit_summary into same dir as other plugins * fix: eslint-import-resolver-webpack is a dev dependency * chore: move learning_assistant to be a plugin too * feat: for compatibility, install 2U plugins by default * fix: bug with learning_assistant package.json --- .eslintrc.js | 17 + jest.config.js | 1 + package-lock.json | 340 ++++++++++++++++++ package.json | 11 + plugins/course-apps/calculator/Settings.jsx | 31 ++ .../course-apps}/calculator/messages.js | 0 plugins/course-apps/calculator/package.json | 17 + plugins/course-apps/edxnotes/Settings.jsx | 31 ++ .../course-apps}/edxnotes/messages.js | 0 plugins/course-apps/edxnotes/package.json | 17 + .../learning_assistant/Settings.jsx | 13 +- .../learning_assistant/Settings.test.jsx | 4 +- .../learning_assistant/messages.js | 0 .../learning_assistant/package.json | 19 + .../course-apps}/live/BBBSettings.jsx | 7 +- .../course-apps}/live/BbbSettings.test.jsx | 8 +- .../course-apps}/live/LiveCommonFields.jsx | 3 +- .../course-apps}/live/Settings.jsx | 9 +- .../course-apps}/live/Settings.test.jsx | 7 +- .../course-apps}/live/ZoomSettings.jsx | 3 +- .../course-apps}/live/ZoomSettings.test.jsx | 6 +- .../course-apps}/live/constants.js | 0 .../course-apps}/live/data/api.js | 0 .../course-apps}/live/data/slice.js | 2 +- .../course-apps}/live/data/thunks.js | 5 +- .../live/factories/mockApiResponses.jsx | 0 .../course-apps}/live/messages.js | 0 plugins/course-apps/live/package.json | 22 ++ .../course-apps}/ora_settings/Settings.jsx | 8 +- .../ora_settings/Settings.test.jsx | 8 +- .../__snapshots__/Settings.test.jsx.snap | 0 .../course-apps}/ora_settings/messages.js | 0 plugins/course-apps/ora_settings/package.json | 19 + .../course-apps}/proctoring/Settings.jsx | 19 +- .../course-apps}/proctoring/Settings.test.jsx | 8 +- .../course-apps}/proctoring/messages.js | 0 plugins/course-apps/proctoring/package.json | 20 ++ .../course-apps}/progress/Settings.jsx | 6 +- .../course-apps}/progress/messages.js | 0 plugins/course-apps/progress/package.json | 18 + .../course-apps}/teams/GroupEditor.jsx | 6 +- .../course-apps}/teams/Settings.jsx | 8 +- .../course-apps}/teams/messages.js | 0 plugins/course-apps/teams/package.json | 20 ++ .../course-apps}/wiki/Settings.jsx | 6 +- .../course-apps}/wiki/messages.js | 0 plugins/course-apps/wiki/package.json | 18 + .../course-apps/xpert_unit_summary/README.rst | 4 + .../xpert_unit_summary/Settings.jsx | 2 +- .../xpert_unit_summary/Settings.test.jsx | 9 +- .../xpert_unit_summary}/appInfo.js | 0 .../xpert_unit_summary}/data/api.js | 0 .../xpert_unit_summary}/data/thunks.js | 9 +- .../xpert_unit_summary}/messages.js | 0 .../xpert_unit_summary/package.json | 21 ++ .../settings-modal/ResetIcon.jsx | 0 .../settings-modal/SettingsModal.jsx | 25 +- .../settings-modal/SettingsModal.scss | 3 + .../settings-modal/messages.js | 0 src/index.scss | 1 - src/pages-and-resources/PagesAndResources.jsx | 60 +--- .../PagesAndResources.scss | 1 - .../PagesAndResources.test.jsx | 89 +++-- src/pages-and-resources/SettingsComponent.jsx | 30 +- .../SettingsComponent.test.jsx | 81 ++++- .../SettingsComponent.test.jsx.snap | 3 - .../app-settings-modal/AppSettingsModal.jsx | 2 + .../calculator/Settings.jsx | 25 -- .../apps/openedx/OpenedXConfigForm.jsx | 4 +- src/pages-and-resources/edxnotes/Settings.jsx | 25 -- src/pages-and-resources/messages.js | 4 + .../xpert-unit-summary/index.js | 10 - src/store.js | 7 +- webpack.dev.config.js | 5 + webpack.prod.config.js | 5 + 75 files changed, 909 insertions(+), 253 deletions(-) create mode 100644 plugins/course-apps/calculator/Settings.jsx rename {src/pages-and-resources => plugins/course-apps}/calculator/messages.js (100%) create mode 100644 plugins/course-apps/calculator/package.json create mode 100644 plugins/course-apps/edxnotes/Settings.jsx rename {src/pages-and-resources => plugins/course-apps}/edxnotes/messages.js (100%) create mode 100644 plugins/course-apps/edxnotes/package.json rename {src/pages-and-resources => plugins/course-apps}/learning_assistant/Settings.jsx (83%) rename {src/pages-and-resources => plugins/course-apps}/learning_assistant/Settings.test.jsx (93%) rename {src/pages-and-resources => plugins/course-apps}/learning_assistant/messages.js (100%) create mode 100644 plugins/course-apps/learning_assistant/package.json rename {src/pages-and-resources => plugins/course-apps}/live/BBBSettings.jsx (95%) rename {src/pages-and-resources => plugins/course-apps}/live/BbbSettings.test.jsx (96%) rename {src/pages-and-resources => plugins/course-apps}/live/LiveCommonFields.jsx (94%) rename {src/pages-and-resources => plugins/course-apps}/live/Settings.jsx (94%) rename {src/pages-and-resources => plugins/course-apps}/live/Settings.test.jsx (96%) rename {src/pages-and-resources => plugins/course-apps}/live/ZoomSettings.jsx (95%) rename {src/pages-and-resources => plugins/course-apps}/live/ZoomSettings.test.jsx (96%) rename {src/pages-and-resources => plugins/course-apps}/live/constants.js (100%) rename {src/pages-and-resources => plugins/course-apps}/live/data/api.js (100%) rename {src/pages-and-resources => plugins/course-apps}/live/data/slice.js (95%) rename {src/pages-and-resources => plugins/course-apps}/live/data/thunks.js (94%) rename {src/pages-and-resources => plugins/course-apps}/live/factories/mockApiResponses.jsx (100%) rename {src/pages-and-resources => plugins/course-apps}/live/messages.js (100%) create mode 100644 plugins/course-apps/live/package.json rename {src/pages-and-resources => plugins/course-apps}/ora_settings/Settings.jsx (86%) rename {src/pages-and-resources => plugins/course-apps}/ora_settings/Settings.test.jsx (75%) rename {src/pages-and-resources => plugins/course-apps}/ora_settings/__snapshots__/Settings.test.jsx.snap (100%) rename {src/pages-and-resources => plugins/course-apps}/ora_settings/messages.js (100%) create mode 100644 plugins/course-apps/ora_settings/package.json rename {src/pages-and-resources => plugins/course-apps}/proctoring/Settings.jsx (97%) rename {src/pages-and-resources => plugins/course-apps}/proctoring/Settings.test.jsx (99%) rename {src/pages-and-resources => plugins/course-apps}/proctoring/messages.js (100%) create mode 100644 plugins/course-apps/proctoring/package.json rename {src/pages-and-resources => plugins/course-apps}/progress/Settings.jsx (89%) rename {src/pages-and-resources => plugins/course-apps}/progress/messages.js (100%) create mode 100644 plugins/course-apps/progress/package.json rename {src/pages-and-resources => plugins/course-apps}/teams/GroupEditor.jsx (96%) rename {src/pages-and-resources => plugins/course-apps}/teams/Settings.jsx (94%) rename {src/pages-and-resources => plugins/course-apps}/teams/messages.js (100%) create mode 100644 plugins/course-apps/teams/package.json rename {src/pages-and-resources => plugins/course-apps}/wiki/Settings.jsx (86%) rename {src/pages-and-resources => plugins/course-apps}/wiki/messages.js (100%) create mode 100644 plugins/course-apps/wiki/package.json create mode 100644 plugins/course-apps/xpert_unit_summary/README.rst rename src/pages-and-resources/xpert-unit-summary/XpertUnitSummarySettings.jsx => plugins/course-apps/xpert_unit_summary/Settings.jsx (93%) rename src/pages-and-resources/xpert-unit-summary/XpertUnitSummarySettings.test.jsx => plugins/course-apps/xpert_unit_summary/Settings.test.jsx (97%) rename {src/pages-and-resources/xpert-unit-summary => plugins/course-apps/xpert_unit_summary}/appInfo.js (100%) rename {src/pages-and-resources/xpert-unit-summary => plugins/course-apps/xpert_unit_summary}/data/api.js (100%) rename {src/pages-and-resources/xpert-unit-summary => plugins/course-apps/xpert_unit_summary}/data/thunks.js (94%) rename {src/pages-and-resources/xpert-unit-summary => plugins/course-apps/xpert_unit_summary}/messages.js (100%) create mode 100644 plugins/course-apps/xpert_unit_summary/package.json rename {src/pages-and-resources/xpert-unit-summary => plugins/course-apps/xpert_unit_summary}/settings-modal/ResetIcon.jsx (100%) rename {src/pages-and-resources/xpert-unit-summary => plugins/course-apps/xpert_unit_summary}/settings-modal/SettingsModal.jsx (93%) rename {src/pages-and-resources/xpert-unit-summary => plugins/course-apps/xpert_unit_summary}/settings-modal/SettingsModal.scss (88%) rename {src/pages-and-resources/xpert-unit-summary => plugins/course-apps/xpert_unit_summary}/settings-modal/messages.js (100%) delete mode 100644 src/pages-and-resources/PagesAndResources.scss delete mode 100644 src/pages-and-resources/__snapshots__/SettingsComponent.test.jsx.snap delete mode 100644 src/pages-and-resources/calculator/Settings.jsx delete mode 100644 src/pages-and-resources/edxnotes/Settings.jsx delete mode 100644 src/pages-and-resources/xpert-unit-summary/index.js diff --git a/.eslintrc.js b/.eslintrc.js index f5cfb442c8..8b03261fc1 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,3 +1,4 @@ +const path = require('path'); // eslint-disable-next-line import/no-extraneous-dependencies const { createConfig } = require('@openedx/frontend-build'); @@ -13,5 +14,21 @@ module.exports = createConfig( indent: ['error', 2], 'no-restricted-exports': 'off', }, + settings: { + // Import URLs should be resolved using aliases + 'import/resolver': { + webpack: { + config: path.resolve(__dirname, 'webpack.dev.config.js'), + }, + }, + }, + overrides: [ + { + files: ['plugins/**/*.test.jsx'], + rules: { + 'import/no-extraneous-dependencies': 'off', + }, + }, + ], }, ); diff --git a/jest.config.js b/jest.config.js index db3f44b207..0af1e62af2 100644 --- a/jest.config.js +++ b/jest.config.js @@ -11,6 +11,7 @@ module.exports = createConfig('jest', { ], moduleNameMapper: { '^lodash-es$': 'lodash', + '^CourseAuthoring/(.*)$': '/src/$1', }, modulePathIgnorePatterns: [ '/src/pages-and-resources/utils.test.jsx', diff --git a/package-lock.json b/package-lock.json index 918f88973e..2df108aad5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,6 +23,16 @@ "@fortawesome/free-regular-svg-icons": "5.15.4", "@fortawesome/free-solid-svg-icons": "5.15.4", "@fortawesome/react-fontawesome": "0.2.0", + "@openedx-plugins/course-app-calculator": "file:plugins/course-apps/calculator", + "@openedx-plugins/course-app-edxnotes": "file:plugins/course-apps/edxnotes", + "@openedx-plugins/course-app-learning_assistant": "file:plugins/course-apps/learning_assistant", + "@openedx-plugins/course-app-live": "file:plugins/course-apps/live", + "@openedx-plugins/course-app-ora_settings": "file:plugins/course-apps/ora_settings", + "@openedx-plugins/course-app-proctoring": "file:plugins/course-apps/proctoring", + "@openedx-plugins/course-app-progress": "file:plugins/course-apps/progress", + "@openedx-plugins/course-app-teams": "file:plugins/course-apps/teams", + "@openedx-plugins/course-app-wiki": "file:plugins/course-apps/wiki", + "@openedx-plugins/course-app-xpert_unit_summary": "file:plugins/course-apps/xpert_unit_summary", "@openedx/paragon": "^21.5.7", "@reduxjs/toolkit": "1.9.7", "@tanstack/react-query": "4.36.1", @@ -65,6 +75,7 @@ "@testing-library/user-event": "^13.2.1", "axios": "^0.27.2", "axios-mock-adapter": "1.22.0", + "eslint-import-resolver-webpack": "^0.13.8", "glob": "7.2.3", "husky": "7.0.4", "jest-canvas-mock": "^2.5.2", @@ -4721,6 +4732,46 @@ "node": ">= 8" } }, + "node_modules/@openedx-plugins/course-app-calculator": { + "resolved": "plugins/course-apps/calculator", + "link": true + }, + "node_modules/@openedx-plugins/course-app-edxnotes": { + "resolved": "plugins/course-apps/edxnotes", + "link": true + }, + "node_modules/@openedx-plugins/course-app-learning_assistant": { + "resolved": "plugins/course-apps/learning_assistant", + "link": true + }, + "node_modules/@openedx-plugins/course-app-live": { + "resolved": "plugins/course-apps/live", + "link": true + }, + "node_modules/@openedx-plugins/course-app-ora_settings": { + "resolved": "plugins/course-apps/ora_settings", + "link": true + }, + "node_modules/@openedx-plugins/course-app-proctoring": { + "resolved": "plugins/course-apps/proctoring", + "link": true + }, + "node_modules/@openedx-plugins/course-app-progress": { + "resolved": "plugins/course-apps/progress", + "link": true + }, + "node_modules/@openedx-plugins/course-app-teams": { + "resolved": "plugins/course-apps/teams", + "link": true + }, + "node_modules/@openedx-plugins/course-app-wiki": { + "resolved": "plugins/course-apps/wiki", + "link": true + }, + "node_modules/@openedx-plugins/course-app-xpert_unit_summary": { + "resolved": "plugins/course-apps/xpert_unit_summary", + "link": true + }, "node_modules/@openedx/frontend-build": { "version": "13.0.27", "resolved": "https://registry.npmjs.org/@openedx/frontend-build/-/frontend-build-13.0.27.tgz", @@ -6622,6 +6673,21 @@ "node": ">=0.10.0" } }, + "node_modules/array.prototype.find": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/array.prototype.find/-/array.prototype.find-2.2.2.tgz", + "integrity": "sha512-DRumkfW97iZGOfn+lIXbkVrXL04sfYKX+EfOodo8XboR5sxPDVvOjZTF/rysusa9lmhmSOeD6Vp6RKQP+eP4Tg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-shim-unscopables": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/array.prototype.flat": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.2.tgz", @@ -9436,6 +9502,99 @@ "ms": "^2.1.1" } }, + "node_modules/eslint-import-resolver-webpack": { + "version": "0.13.8", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-webpack/-/eslint-import-resolver-webpack-0.13.8.tgz", + "integrity": "sha512-Y7WIaXWV+Q21Rz/PJgUxiW/FTBOWmU8NTLdz+nz9mMoiz5vAev/fOaQxwD7qRzTfE3HSm1qsxZ5uRd7eX+VEtA==", + "dev": true, + "dependencies": { + "array.prototype.find": "^2.2.2", + "debug": "^3.2.7", + "enhanced-resolve": "^0.9.1", + "find-root": "^1.1.0", + "hasown": "^2.0.0", + "interpret": "^1.4.0", + "is-core-module": "^2.13.1", + "is-regex": "^1.1.4", + "lodash": "^4.17.21", + "resolve": "^2.0.0-next.5", + "semver": "^5.7.2" + }, + "engines": { + "node": ">= 6" + }, + "peerDependencies": { + "eslint-plugin-import": ">=1.4.0", + "webpack": ">=1.11.0" + } + }, + "node_modules/eslint-import-resolver-webpack/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-import-resolver-webpack/node_modules/enhanced-resolve": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-0.9.1.tgz", + "integrity": "sha512-kxpoMgrdtkXZ5h0SeraBS1iRntpTpQ3R8ussdb38+UAFnMGX5DDyJXePm+OCHOcoXvHDw7mc2erbJBpDnl7TPw==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.1.2", + "memory-fs": "^0.2.0", + "tapable": "^0.1.8" + }, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/eslint-import-resolver-webpack/node_modules/interpret": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz", + "integrity": "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==", + "dev": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/eslint-import-resolver-webpack/node_modules/resolve": { + "version": "2.0.0-next.5", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", + "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==", + "dev": true, + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/eslint-import-resolver-webpack/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/eslint-import-resolver-webpack/node_modules/tapable": { + "version": "0.1.10", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-0.1.10.tgz", + "integrity": "sha512-jX8Et4hHg57mug1/079yitEKWGB3LCwoxByLsNim89LABq8NqgiX+6iYVOsq0vX8uJHkU+DZ5fnq95f800bEsQ==", + "dev": true, + "engines": { + "node": ">=0.6" + } + }, "node_modules/eslint-module-utils": { "version": "2.8.0", "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.8.0.tgz", @@ -10360,6 +10519,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/find-root": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", + "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==", + "dev": true + }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -14515,6 +14680,12 @@ "node": ">= 4.0.0" } }, + "node_modules/memory-fs": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.2.0.tgz", + "integrity": "sha512-+y4mDxU4rvXXu5UDSGCGNiesFmwCHuefGMoPCO1WYucNYj7DsLqrFaa2fXVI0H+NNiPTwwzKwspn9yTZqUGqng==", + "dev": true + }, "node_modules/meow": { "version": "9.0.0", "resolved": "https://registry.npmjs.org/meow/-/meow-9.0.0.tgz", @@ -21682,6 +21853,175 @@ "engines": { "node": ">=10" } + }, + "plugins/course-apps/calculator": { + "version": "0.1.0", + "peerDependencies": { + "@edx/frontend-app-course-authoring": "*", + "@edx/frontend-platform": "*", + "@openedx/paragon": "*", + "prop-types": "*", + "react": "*" + }, + "peerDependenciesMeta": { + "@edx/frontend-app-course-authoring": { + "optional": true + } + } + }, + "plugins/course-apps/edxnotes": { + "version": "0.1.0", + "peerDependencies": { + "@edx/frontend-app-course-authoring": "*", + "@edx/frontend-platform": "*", + "@openedx/paragon": "*", + "prop-types": "*", + "react": "*" + }, + "peerDependenciesMeta": { + "@edx/frontend-app-course-authoring": { + "optional": true + } + } + }, + "plugins/course-apps/learning_assistant": { + "version": "0.1.0", + "peerDependencies": { + "@edx/frontend-app-course-authoring": "*", + "@edx/frontend-platform": "*", + "@openedx/paragon": "*", + "prop-types": "*", + "react": "*", + "yup": "*" + }, + "peerDependenciesMeta": { + "@edx/frontend-app-course-authoring": { + "optional": true + } + } + }, + "plugins/course-apps/live": { + "version": "0.1.0", + "peerDependencies": { + "@edx/frontend-app-course-authoring": "*", + "@edx/frontend-platform": "*", + "@openedx/paragon": "*", + "@reduxjs/toolkit": "*", + "lodash": "*", + "prop-types": "*", + "react": "*", + "react-redux": "*", + "react-router-dom": "*", + "yup": "*" + }, + "peerDependenciesMeta": { + "@edx/frontend-app-course-authoring": { + "optional": true + } + } + }, + "plugins/course-apps/ora_settings": { + "version": "0.1.0", + "peerDependencies": { + "@edx/frontend-app-course-authoring": "*", + "@edx/frontend-platform": "*", + "@openedx/paragon": "*", + "prop-types": "*", + "react": "*", + "yup": "*" + }, + "peerDependenciesMeta": { + "@edx/frontend-app-course-authoring": { + "optional": true + } + } + }, + "plugins/course-apps/proctoring": { + "version": "0.1.0", + "peerDependencies": { + "@edx/frontend-app-course-authoring": "*", + "@edx/frontend-platform": "*", + "@openedx/paragon": "*", + "classnames": "*", + "email-validator": "*", + "moment": "*", + "prop-types": "*", + "react": "*" + }, + "peerDependenciesMeta": { + "@edx/frontend-app-course-authoring": { + "optional": true + } + } + }, + "plugins/course-apps/progress": { + "version": "0.1.0", + "peerDependencies": { + "@edx/frontend-app-course-authoring": "*", + "@edx/frontend-platform": "*", + "@openedx/paragon": "*", + "prop-types": "*", + "react": "*", + "yup": "*" + }, + "peerDependenciesMeta": { + "@edx/frontend-app-course-authoring": { + "optional": true + } + } + }, + "plugins/course-apps/teams": { + "version": "0.1.0", + "peerDependencies": { + "@edx/frontend-app-course-authoring": "*", + "@edx/frontend-platform": "*", + "@openedx/paragon": "*", + "formik": "*", + "prop-types": "*", + "react": "*", + "uuid": "*", + "yup": "*" + }, + "peerDependenciesMeta": { + "@edx/frontend-app-course-authoring": { + "optional": true + } + } + }, + "plugins/course-apps/wiki": { + "version": "0.1.0", + "peerDependencies": { + "@edx/frontend-app-course-authoring": "*", + "@edx/frontend-platform": "*", + "@openedx/paragon": "*", + "prop-types": "*", + "react": "*", + "yup": "*" + }, + "peerDependenciesMeta": { + "@edx/frontend-app-course-authoring": { + "optional": true + } + } + }, + "plugins/course-apps/xpert_unit_summary": { + "version": "0.1.0", + "peerDependencies": { + "@edx/frontend-app-course-authoring": "*", + "@edx/frontend-platform": "*", + "@openedx/paragon": "*", + "formik": "*", + "prop-types": "*", + "react": "*", + "react-redux": "*", + "react-router-dom": "*", + "yup": "*" + }, + "peerDependenciesMeta": { + "@edx/frontend-app-course-authoring": { + "optional": true + } + } } } } diff --git a/package.json b/package.json index 60bd1168ec..9f6df4687b 100644 --- a/package.json +++ b/package.json @@ -51,6 +51,16 @@ "@fortawesome/free-regular-svg-icons": "5.15.4", "@fortawesome/free-solid-svg-icons": "5.15.4", "@fortawesome/react-fontawesome": "0.2.0", + "@openedx-plugins/course-app-calculator": "file:plugins/course-apps/calculator", + "@openedx-plugins/course-app-edxnotes": "file:plugins/course-apps/edxnotes", + "@openedx-plugins/course-app-live": "file:plugins/course-apps/live", + "@openedx-plugins/course-app-ora_settings": "file:plugins/course-apps/ora_settings", + "@openedx-plugins/course-app-proctoring": "file:plugins/course-apps/proctoring", + "@openedx-plugins/course-app-progress": "file:plugins/course-apps/progress", + "@openedx-plugins/course-app-teams": "file:plugins/course-apps/teams", + "@openedx-plugins/course-app-wiki": "file:plugins/course-apps/wiki", + "@openedx-plugins/course-app-learning_assistant": "file:plugins/course-apps/learning_assistant", + "@openedx-plugins/course-app-xpert_unit_summary": "file:plugins/course-apps/xpert_unit_summary", "@reduxjs/toolkit": "1.9.7", "@tanstack/react-query": "4.36.1", "broadcast-channel": "^7.0.0", @@ -92,6 +102,7 @@ "@testing-library/user-event": "^13.2.1", "axios": "^0.27.2", "axios-mock-adapter": "1.22.0", + "eslint-import-resolver-webpack": "^0.13.8", "glob": "7.2.3", "husky": "7.0.4", "jest-canvas-mock": "^2.5.2", diff --git a/plugins/course-apps/calculator/Settings.jsx b/plugins/course-apps/calculator/Settings.jsx new file mode 100644 index 0000000000..7bfed1f658 --- /dev/null +++ b/plugins/course-apps/calculator/Settings.jsx @@ -0,0 +1,31 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import { useIntl } from '@edx/frontend-platform/i18n'; + +import AppSettingsModal from 'CourseAuthoring/pages-and-resources/app-settings-modal/AppSettingsModal'; +import messages from './messages'; + +/** + * Settings widget for the "calculator" Course App. + * @param {{onClose: () => void}} props + */ +const CalculatorSettings = ({ onClose }) => { + const intl = useIntl(); + return ( + + ); +}; + +CalculatorSettings.propTypes = { + onClose: PropTypes.func.isRequired, +}; + +export default CalculatorSettings; diff --git a/src/pages-and-resources/calculator/messages.js b/plugins/course-apps/calculator/messages.js similarity index 100% rename from src/pages-and-resources/calculator/messages.js rename to plugins/course-apps/calculator/messages.js diff --git a/plugins/course-apps/calculator/package.json b/plugins/course-apps/calculator/package.json new file mode 100644 index 0000000000..6f4a98670e --- /dev/null +++ b/plugins/course-apps/calculator/package.json @@ -0,0 +1,17 @@ +{ + "name": "@openedx-plugins/course-app-calculator", + "version": "0.1.0", + "description": "Calculator configuration for courses using it", + "peerDependencies": { + "@edx/frontend-app-course-authoring": "*", + "@edx/frontend-platform": "*", + "@openedx/paragon": "*", + "prop-types": "*", + "react": "*" + }, + "peerDependenciesMeta": { + "@edx/frontend-app-course-authoring": { + "optional": true + } + } +} diff --git a/plugins/course-apps/edxnotes/Settings.jsx b/plugins/course-apps/edxnotes/Settings.jsx new file mode 100644 index 0000000000..b6672320ca --- /dev/null +++ b/plugins/course-apps/edxnotes/Settings.jsx @@ -0,0 +1,31 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import { useIntl } from '@edx/frontend-platform/i18n'; + +import AppSettingsModal from 'CourseAuthoring/pages-and-resources/app-settings-modal/AppSettingsModal'; +import messages from './messages'; + +/** + * Settings widget for the "edxnotes" Course App. + * @param {{onClose: () => void}} props + */ +const NotesSettings = ({ onClose }) => { + const intl = useIntl(); + return ( + + ); +}; + +NotesSettings.propTypes = { + onClose: PropTypes.func.isRequired, +}; + +export default NotesSettings; diff --git a/src/pages-and-resources/edxnotes/messages.js b/plugins/course-apps/edxnotes/messages.js similarity index 100% rename from src/pages-and-resources/edxnotes/messages.js rename to plugins/course-apps/edxnotes/messages.js diff --git a/plugins/course-apps/edxnotes/package.json b/plugins/course-apps/edxnotes/package.json new file mode 100644 index 0000000000..ed2287db22 --- /dev/null +++ b/plugins/course-apps/edxnotes/package.json @@ -0,0 +1,17 @@ +{ + "name": "@openedx-plugins/course-app-edxnotes", + "version": "0.1.0", + "description": "edxnotes configuration for courses using it", + "peerDependencies": { + "@edx/frontend-app-course-authoring": "*", + "@edx/frontend-platform": "*", + "@openedx/paragon": "*", + "prop-types": "*", + "react": "*" + }, + "peerDependenciesMeta": { + "@edx/frontend-app-course-authoring": { + "optional": true + } + } +} diff --git a/src/pages-and-resources/learning_assistant/Settings.jsx b/plugins/course-apps/learning_assistant/Settings.jsx similarity index 83% rename from src/pages-and-resources/learning_assistant/Settings.jsx rename to plugins/course-apps/learning_assistant/Settings.jsx index b2abd8224b..7b9bb916b3 100644 --- a/src/pages-and-resources/learning_assistant/Settings.jsx +++ b/plugins/course-apps/learning_assistant/Settings.jsx @@ -1,16 +1,18 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; +import { useIntl } from '@edx/frontend-platform/i18n'; import { Hyperlink } from '@openedx/paragon'; -import AppSettingsModal from '../app-settings-modal/AppSettingsModal'; +import AppSettingsModal from 'CourseAuthoring/pages-and-resources/app-settings-modal/AppSettingsModal'; +import { useModel } from 'CourseAuthoring/generic/model-store'; + import messages from './messages'; -import { useModel } from '../../generic/model-store'; -const LearningAssistantSettings = ({ intl, onClose }) => { +const LearningAssistantSettings = ({ onClose }) => { const appId = 'learning_assistant'; const appInfo = useModel('courseApps', appId); + const intl = useIntl(); // We need to render more than one link, so we use the bodyChildren prop. const bodyChildren = ( @@ -55,8 +57,7 @@ const LearningAssistantSettings = ({ intl, onClose }) => { }; LearningAssistantSettings.propTypes = { - intl: intlShape.isRequired, onClose: PropTypes.func.isRequired, }; -export default injectIntl(LearningAssistantSettings); +export default LearningAssistantSettings; diff --git a/src/pages-and-resources/learning_assistant/Settings.test.jsx b/plugins/course-apps/learning_assistant/Settings.test.jsx similarity index 93% rename from src/pages-and-resources/learning_assistant/Settings.test.jsx rename to plugins/course-apps/learning_assistant/Settings.test.jsx index 087434857d..a9bfa58200 100644 --- a/src/pages-and-resources/learning_assistant/Settings.test.jsx +++ b/plugins/course-apps/learning_assistant/Settings.test.jsx @@ -1,9 +1,9 @@ import React from 'react'; import { screen, waitFor } from '@testing-library/react'; +import { RequestStatus } from 'CourseAuthoring/data/constants'; +import { render } from 'CourseAuthoring/pages-and-resources/utils.test'; import LearningAssistantSettings from './Settings'; -import { render } from '../utils.test'; -import { RequestStatus } from '../../data/constants'; const onClose = () => { }; diff --git a/src/pages-and-resources/learning_assistant/messages.js b/plugins/course-apps/learning_assistant/messages.js similarity index 100% rename from src/pages-and-resources/learning_assistant/messages.js rename to plugins/course-apps/learning_assistant/messages.js diff --git a/plugins/course-apps/learning_assistant/package.json b/plugins/course-apps/learning_assistant/package.json new file mode 100644 index 0000000000..0c96b6fc5c --- /dev/null +++ b/plugins/course-apps/learning_assistant/package.json @@ -0,0 +1,19 @@ +{ + "name": "@openedx-plugins/course-app-learning_assistant", + "version": "0.1.0", + "description": "Learning Assistant configuration for courses using it", + "peerDependencies": { + "@edx/frontend-app-course-authoring": "*", + "@edx/frontend-platform": "*", + "@openedx/paragon": "*", + "prop-types": "*", + "react": "*", + "yup": "*" + }, + "peerDependenciesMeta": { + "@edx/frontend-app-course-authoring": { + "optional": true + } + } +} + \ No newline at end of file diff --git a/src/pages-and-resources/live/BBBSettings.jsx b/plugins/course-apps/live/BBBSettings.jsx similarity index 95% rename from src/pages-and-resources/live/BBBSettings.jsx rename to plugins/course-apps/live/BBBSettings.jsx index 461a6140a8..02b09a82b0 100644 --- a/src/pages-and-resources/live/BBBSettings.jsx +++ b/plugins/course-apps/live/BBBSettings.jsx @@ -3,11 +3,12 @@ import { getConfig } from '@edx/frontend-platform'; import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n'; import { Form, Hyperlink } from '@openedx/paragon'; import PropTypes from 'prop-types'; -import messages from './messages'; +import AppConfigFormDivider from 'CourseAuthoring/pages-and-resources/discussions/app-config-form/apps/shared/AppConfigFormDivider'; +import { useModel } from 'CourseAuthoring/generic/model-store'; + import { providerNames, bbbPlanTypes } from './constants'; -import AppConfigFormDivider from '../discussions/app-config-form/apps/shared/AppConfigFormDivider'; import LiveCommonFields from './LiveCommonFields'; -import { useModel } from '../../generic/model-store'; +import messages from './messages'; const BbbSettings = ({ intl, diff --git a/src/pages-and-resources/live/BbbSettings.test.jsx b/plugins/course-apps/live/BbbSettings.test.jsx similarity index 96% rename from src/pages-and-resources/live/BbbSettings.test.jsx rename to plugins/course-apps/live/BbbSettings.test.jsx index 86c2193e2a..143d4eafdc 100644 --- a/src/pages-and-resources/live/BbbSettings.test.jsx +++ b/plugins/course-apps/live/BbbSettings.test.jsx @@ -15,8 +15,10 @@ import { AppProvider, PageWrap } from '@edx/frontend-platform/react'; import { IntlProvider } from '@edx/frontend-platform/i18n'; import userEvent from '@testing-library/user-event'; -import initializeStore from '../../store'; -import { executeThunk } from '../../utils'; +import initializeStore from 'CourseAuthoring/store'; +import { executeThunk } from 'CourseAuthoring/utils'; +import PagesAndResourcesProvider from 'CourseAuthoring/pages-and-resources/PagesAndResourcesProvider'; + import LiveSettings from './Settings'; import { generateLiveConfigurationApiResponse, @@ -24,11 +26,9 @@ import { initialState, configurationProviders, } from './factories/mockApiResponses'; - import { fetchLiveConfiguration, fetchLiveProviders } from './data/thunks'; import { providerConfigurationApiUrl, providersApiUrl } from './data/api'; import messages from './messages'; -import PagesAndResourcesProvider from '../PagesAndResourcesProvider'; let axiosMock; let container; diff --git a/src/pages-and-resources/live/LiveCommonFields.jsx b/plugins/course-apps/live/LiveCommonFields.jsx similarity index 94% rename from src/pages-and-resources/live/LiveCommonFields.jsx rename to plugins/course-apps/live/LiveCommonFields.jsx index c357a36bf7..6e49e62df0 100644 --- a/src/pages-and-resources/live/LiveCommonFields.jsx +++ b/plugins/course-apps/live/LiveCommonFields.jsx @@ -1,8 +1,9 @@ import React from 'react'; import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; import PropTypes from 'prop-types'; +import FormikControl from 'CourseAuthoring/generic/FormikControl'; + import messages from './messages'; -import FormikControl from '../../generic/FormikControl'; const LiveCommonFields = ({ intl, diff --git a/src/pages-and-resources/live/Settings.jsx b/plugins/course-apps/live/Settings.jsx similarity index 94% rename from src/pages-and-resources/live/Settings.jsx rename to plugins/course-apps/live/Settings.jsx index 0e5c8679d9..3891b09603 100644 --- a/src/pages-and-resources/live/Settings.jsx +++ b/plugins/course-apps/live/Settings.jsx @@ -6,13 +6,14 @@ import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; import PropTypes from 'prop-types'; import * as Yup from 'yup'; import { useNavigate } from 'react-router-dom'; +import AppSettingsModal from 'CourseAuthoring/pages-and-resources/app-settings-modal/AppSettingsModal'; +import { useModel } from 'CourseAuthoring/generic/model-store'; +import Loading from 'CourseAuthoring/generic/Loading'; +import { RequestStatus } from 'CourseAuthoring/data/constants'; + import { fetchLiveData, saveLiveConfiguration, saveLiveConfigurationAsDraft } from './data/thunks'; import { selectApp } from './data/slice'; -import AppSettingsModal from '../app-settings-modal/AppSettingsModal'; -import { useModel } from '../../generic/model-store'; -import Loading from '../../generic/Loading'; import { iconsSrc, bbbPlanTypes } from './constants'; -import { RequestStatus } from '../../data/constants'; import messages from './messages'; import ZoomSettings from './ZoomSettings'; import BBBSettings from './BBBSettings'; diff --git a/src/pages-and-resources/live/Settings.test.jsx b/plugins/course-apps/live/Settings.test.jsx similarity index 96% rename from src/pages-and-resources/live/Settings.test.jsx rename to plugins/course-apps/live/Settings.test.jsx index 586265d06f..71bfc509f1 100644 --- a/src/pages-and-resources/live/Settings.test.jsx +++ b/plugins/course-apps/live/Settings.test.jsx @@ -18,8 +18,10 @@ import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; import { AppProvider, PageWrap } from '@edx/frontend-platform/react'; import { IntlProvider } from '@edx/frontend-platform/i18n'; -import initializeStore from '../../store'; -import { executeThunk } from '../../utils'; +import initializeStore from 'CourseAuthoring/store'; +import { executeThunk } from 'CourseAuthoring/utils'; +import PagesAndResourcesProvider from 'CourseAuthoring/pages-and-resources/PagesAndResourcesProvider'; + import LiveSettings from './Settings'; import { generateLiveConfigurationApiResponse, @@ -31,7 +33,6 @@ import { import { fetchLiveConfiguration, fetchLiveProviders } from './data/thunks'; import { providerConfigurationApiUrl, providersApiUrl } from './data/api'; import messages from './messages'; -import PagesAndResourcesProvider from '../PagesAndResourcesProvider'; let axiosMock; let container; diff --git a/src/pages-and-resources/live/ZoomSettings.jsx b/plugins/course-apps/live/ZoomSettings.jsx similarity index 95% rename from src/pages-and-resources/live/ZoomSettings.jsx rename to plugins/course-apps/live/ZoomSettings.jsx index 0d25dfa953..edb17f830e 100644 --- a/src/pages-and-resources/live/ZoomSettings.jsx +++ b/plugins/course-apps/live/ZoomSettings.jsx @@ -1,10 +1,11 @@ import React from 'react'; import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; import PropTypes from 'prop-types'; +import FormikControl from 'CourseAuthoring/generic/FormikControl'; + import messages from './messages'; import { providerNames } from './constants'; import LiveCommonFields from './LiveCommonFields'; -import FormikControl from '../../generic/FormikControl'; const ZoomSettings = ({ intl, diff --git a/src/pages-and-resources/live/ZoomSettings.test.jsx b/plugins/course-apps/live/ZoomSettings.test.jsx similarity index 96% rename from src/pages-and-resources/live/ZoomSettings.test.jsx rename to plugins/course-apps/live/ZoomSettings.test.jsx index 72706c9c85..a0e0836131 100644 --- a/src/pages-and-resources/live/ZoomSettings.test.jsx +++ b/plugins/course-apps/live/ZoomSettings.test.jsx @@ -13,8 +13,9 @@ import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; import { AppProvider, PageWrap } from '@edx/frontend-platform/react'; import { IntlProvider } from '@edx/frontend-platform/i18n'; -import initializeStore from '../../store'; -import { executeThunk } from '../../utils'; +import initializeStore from 'CourseAuthoring/store'; +import { executeThunk } from 'CourseAuthoring/utils'; +import PagesAndResourcesProvider from 'CourseAuthoring/pages-and-resources/PagesAndResourcesProvider'; import LiveSettings from './Settings'; import { generateLiveConfigurationApiResponse, @@ -26,7 +27,6 @@ import { import { fetchLiveConfiguration, fetchLiveProviders } from './data/thunks'; import { providerConfigurationApiUrl, providersApiUrl } from './data/api'; import messages from './messages'; -import PagesAndResourcesProvider from '../PagesAndResourcesProvider'; let axiosMock; let container; diff --git a/src/pages-and-resources/live/constants.js b/plugins/course-apps/live/constants.js similarity index 100% rename from src/pages-and-resources/live/constants.js rename to plugins/course-apps/live/constants.js diff --git a/src/pages-and-resources/live/data/api.js b/plugins/course-apps/live/data/api.js similarity index 100% rename from src/pages-and-resources/live/data/api.js rename to plugins/course-apps/live/data/api.js diff --git a/src/pages-and-resources/live/data/slice.js b/plugins/course-apps/live/data/slice.js similarity index 95% rename from src/pages-and-resources/live/data/slice.js rename to plugins/course-apps/live/data/slice.js index 69ccf6b669..5f4c2a72d8 100644 --- a/src/pages-and-resources/live/data/slice.js +++ b/plugins/course-apps/live/data/slice.js @@ -1,6 +1,6 @@ /* eslint-disable no-param-reassign */ import { createSlice } from '@reduxjs/toolkit'; -import { RequestStatus } from '../../../data/constants'; +import { RequestStatus } from 'CourseAuthoring/data/constants'; const slice = createSlice({ name: 'live', diff --git a/src/pages-and-resources/live/data/thunks.js b/plugins/course-apps/live/data/thunks.js similarity index 94% rename from src/pages-and-resources/live/data/thunks.js rename to plugins/course-apps/live/data/thunks.js index cf29001956..6c31082896 100644 --- a/src/pages-and-resources/live/data/thunks.js +++ b/plugins/course-apps/live/data/thunks.js @@ -1,4 +1,6 @@ -import { addModel, addModels, updateModel } from '../../../generic/model-store'; +import { addModel, addModels, updateModel } from 'CourseAuthoring/generic/model-store'; +import { RequestStatus } from 'CourseAuthoring/data/constants'; + import { getLiveConfiguration, getLiveProviders, @@ -7,7 +9,6 @@ import { deNormalizeSettings, } from './api'; import { loadApps, updateStatus, updateSaveStatus } from './slice'; -import { RequestStatus } from '../../../data/constants'; function updateLiveSettingsState({ appConfig, diff --git a/src/pages-and-resources/live/factories/mockApiResponses.jsx b/plugins/course-apps/live/factories/mockApiResponses.jsx similarity index 100% rename from src/pages-and-resources/live/factories/mockApiResponses.jsx rename to plugins/course-apps/live/factories/mockApiResponses.jsx diff --git a/src/pages-and-resources/live/messages.js b/plugins/course-apps/live/messages.js similarity index 100% rename from src/pages-and-resources/live/messages.js rename to plugins/course-apps/live/messages.js diff --git a/plugins/course-apps/live/package.json b/plugins/course-apps/live/package.json new file mode 100644 index 0000000000..6fbd074feb --- /dev/null +++ b/plugins/course-apps/live/package.json @@ -0,0 +1,22 @@ +{ + "name": "@openedx-plugins/course-app-live", + "version": "0.1.0", + "description": "Live course configuration for courses using it", + "peerDependencies": { + "@edx/frontend-app-course-authoring": "*", + "@edx/frontend-platform": "*", + "@openedx/paragon": "*", + "@reduxjs/toolkit": "*", + "lodash": "*", + "prop-types": "*", + "react": "*", + "react-redux": "*", + "react-router-dom": "*", + "yup": "*" + }, + "peerDependenciesMeta": { + "@edx/frontend-app-course-authoring": { + "optional": true + } + } +} diff --git a/src/pages-and-resources/ora_settings/Settings.jsx b/plugins/course-apps/ora_settings/Settings.jsx similarity index 86% rename from src/pages-and-resources/ora_settings/Settings.jsx rename to plugins/course-apps/ora_settings/Settings.jsx index 205c24ed01..b3e3c0d287 100644 --- a/src/pages-and-resources/ora_settings/Settings.jsx +++ b/plugins/course-apps/ora_settings/Settings.jsx @@ -5,11 +5,11 @@ import * as Yup from 'yup'; import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; import { Hyperlink } from '@openedx/paragon'; -import { useModel } from '../../generic/model-store'; +import { useModel } from 'CourseAuthoring/generic/model-store'; -import FormSwitchGroup from '../../generic/FormSwitchGroup'; -import { useAppSetting } from '../../utils'; -import AppSettingsModal from '../app-settings-modal/AppSettingsModal'; +import FormSwitchGroup from 'CourseAuthoring/generic/FormSwitchGroup'; +import { useAppSetting } from 'CourseAuthoring/utils'; +import AppSettingsModal from 'CourseAuthoring/pages-and-resources/app-settings-modal/AppSettingsModal'; import messages from './messages'; const ORASettings = ({ intl, onClose }) => { diff --git a/src/pages-and-resources/ora_settings/Settings.test.jsx b/plugins/course-apps/ora_settings/Settings.test.jsx similarity index 75% rename from src/pages-and-resources/ora_settings/Settings.test.jsx rename to plugins/course-apps/ora_settings/Settings.test.jsx index 01f1200e8b..d74cab9e69 100644 --- a/src/pages-and-resources/ora_settings/Settings.test.jsx +++ b/plugins/course-apps/ora_settings/Settings.test.jsx @@ -9,14 +9,14 @@ jest.mock('@edx/frontend-platform/i18n', () => ({ jest.mock('yup', () => ({ boolean: jest.fn().mockReturnValue('Yub.boolean'), })); -jest.mock('../../generic/model-store', () => ({ +jest.mock('CourseAuthoring/generic/model-store', () => ({ useModel: jest.fn().mockReturnValue({ documentationLinks: { learnMoreConfiguration: 'https://learnmore.test' } }), })); -jest.mock('../../generic/FormSwitchGroup', () => 'FormSwitchGroup'); -jest.mock('../../utils', () => ({ +jest.mock('CourseAuthoring/generic/FormSwitchGroup', () => 'FormSwitchGroup'); +jest.mock('CourseAuthoring/utils', () => ({ useAppSetting: jest.fn().mockReturnValue(['abitrary value', jest.fn().mockName('saveSetting')]), })); -jest.mock('../app-settings-modal/AppSettingsModal', () => 'AppSettingsModal'); +jest.mock('CourseAuthoring/pages-and-resources/app-settings-modal/AppSettingsModal', () => 'AppSettingsModal'); const props = { onClose: jest.fn().mockName('onClose'), diff --git a/src/pages-and-resources/ora_settings/__snapshots__/Settings.test.jsx.snap b/plugins/course-apps/ora_settings/__snapshots__/Settings.test.jsx.snap similarity index 100% rename from src/pages-and-resources/ora_settings/__snapshots__/Settings.test.jsx.snap rename to plugins/course-apps/ora_settings/__snapshots__/Settings.test.jsx.snap diff --git a/src/pages-and-resources/ora_settings/messages.js b/plugins/course-apps/ora_settings/messages.js similarity index 100% rename from src/pages-and-resources/ora_settings/messages.js rename to plugins/course-apps/ora_settings/messages.js diff --git a/plugins/course-apps/ora_settings/package.json b/plugins/course-apps/ora_settings/package.json new file mode 100644 index 0000000000..d6de338820 --- /dev/null +++ b/plugins/course-apps/ora_settings/package.json @@ -0,0 +1,19 @@ +{ + "name": "@openedx-plugins/course-app-ora_settings", + "version": "0.1.0", + "description": "Open Response Assessment configuration for courses using it", + "peerDependencies": { + "@edx/frontend-app-course-authoring": "*", + "@edx/frontend-platform": "*", + "@openedx/paragon": "*", + "prop-types": "*", + "react": "*", + "yup": "*" + }, + "peerDependenciesMeta": { + "@edx/frontend-app-course-authoring": { + "optional": true + } + } +} + \ No newline at end of file diff --git a/src/pages-and-resources/proctoring/Settings.jsx b/plugins/course-apps/proctoring/Settings.jsx similarity index 97% rename from src/pages-and-resources/proctoring/Settings.jsx rename to plugins/course-apps/proctoring/Settings.jsx index 9ff4af443b..255152aa71 100644 --- a/src/pages-and-resources/proctoring/Settings.jsx +++ b/plugins/course-apps/proctoring/Settings.jsx @@ -13,15 +13,16 @@ import { ActionRow, Alert, Badge, Form, Hyperlink, ModalDialog, StatefulButton, } from '@openedx/paragon'; -import ExamsApiService from '../../data/services/ExamsApiService'; -import StudioApiService from '../../data/services/StudioApiService'; -import Loading from '../../generic/Loading'; -import ConnectionErrorAlert from '../../generic/ConnectionErrorAlert'; -import FormSwitchGroup from '../../generic/FormSwitchGroup'; -import { useModel } from '../../generic/model-store'; -import PermissionDeniedAlert from '../../generic/PermissionDeniedAlert'; -import { useIsMobile } from '../../utils'; -import { PagesAndResourcesContext } from '../PagesAndResourcesProvider'; +import ExamsApiService from 'CourseAuthoring/data/services/ExamsApiService'; +import StudioApiService from 'CourseAuthoring/data/services/StudioApiService'; +import Loading from 'CourseAuthoring/generic/Loading'; +import ConnectionErrorAlert from 'CourseAuthoring/generic/ConnectionErrorAlert'; +import FormSwitchGroup from 'CourseAuthoring/generic/FormSwitchGroup'; +import { useModel } from 'CourseAuthoring/generic/model-store'; +import PermissionDeniedAlert from 'CourseAuthoring/generic/PermissionDeniedAlert'; +import { useIsMobile } from 'CourseAuthoring/utils'; +import { PagesAndResourcesContext } from 'CourseAuthoring/pages-and-resources/PagesAndResourcesProvider'; + import messages from './messages'; const ProctoringSettings = ({ intl, onClose }) => { diff --git a/src/pages-and-resources/proctoring/Settings.test.jsx b/plugins/course-apps/proctoring/Settings.test.jsx similarity index 99% rename from src/pages-and-resources/proctoring/Settings.test.jsx rename to plugins/course-apps/proctoring/Settings.test.jsx index 1e8d79caa3..bd544ebb3b 100644 --- a/src/pages-and-resources/proctoring/Settings.test.jsx +++ b/plugins/course-apps/proctoring/Settings.test.jsx @@ -9,10 +9,10 @@ import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; import { IntlProvider, injectIntl } from '@edx/frontend-platform/i18n'; import { AppProvider } from '@edx/frontend-platform/react'; -import StudioApiService from '../../data/services/StudioApiService'; -import ExamsApiService from '../../data/services/ExamsApiService'; -import initializeStore from '../../store'; -import PagesAndResourcesProvider from '../PagesAndResourcesProvider'; +import StudioApiService from 'CourseAuthoring/data/services/StudioApiService'; +import ExamsApiService from 'CourseAuthoring/data/services/ExamsApiService'; +import initializeStore from 'CourseAuthoring/store'; +import PagesAndResourcesProvider from 'CourseAuthoring/pages-and-resources/PagesAndResourcesProvider'; import ProctoredExamSettings from './Settings'; const defaultProps = { diff --git a/src/pages-and-resources/proctoring/messages.js b/plugins/course-apps/proctoring/messages.js similarity index 100% rename from src/pages-and-resources/proctoring/messages.js rename to plugins/course-apps/proctoring/messages.js diff --git a/plugins/course-apps/proctoring/package.json b/plugins/course-apps/proctoring/package.json new file mode 100644 index 0000000000..82801ab695 --- /dev/null +++ b/plugins/course-apps/proctoring/package.json @@ -0,0 +1,20 @@ +{ + "name": "@openedx-plugins/course-app-proctoring", + "version": "0.1.0", + "description": "Proctoring configuration for courses using it", + "peerDependencies": { + "@edx/frontend-app-course-authoring": "*", + "@edx/frontend-platform": "*", + "@openedx/paragon": "*", + "classnames": "*", + "email-validator": "*", + "react": "*", + "prop-types": "*", + "moment": "*" + }, + "peerDependenciesMeta": { + "@edx/frontend-app-course-authoring": { + "optional": true + } + } +} diff --git a/src/pages-and-resources/progress/Settings.jsx b/plugins/course-apps/progress/Settings.jsx similarity index 89% rename from src/pages-and-resources/progress/Settings.jsx rename to plugins/course-apps/progress/Settings.jsx index 965f110b1b..248ae4abb4 100644 --- a/src/pages-and-resources/progress/Settings.jsx +++ b/plugins/course-apps/progress/Settings.jsx @@ -3,9 +3,9 @@ import PropTypes from 'prop-types'; import React from 'react'; import * as Yup from 'yup'; import { getConfig } from '@edx/frontend-platform'; -import FormSwitchGroup from '../../generic/FormSwitchGroup'; -import { useAppSetting } from '../../utils'; -import AppSettingsModal from '../app-settings-modal/AppSettingsModal'; +import FormSwitchGroup from 'CourseAuthoring/generic/FormSwitchGroup'; +import { useAppSetting } from 'CourseAuthoring/utils'; +import AppSettingsModal from 'CourseAuthoring/pages-and-resources/app-settings-modal/AppSettingsModal'; import messages from './messages'; const ProgressSettings = ({ intl, onClose }) => { diff --git a/src/pages-and-resources/progress/messages.js b/plugins/course-apps/progress/messages.js similarity index 100% rename from src/pages-and-resources/progress/messages.js rename to plugins/course-apps/progress/messages.js diff --git a/plugins/course-apps/progress/package.json b/plugins/course-apps/progress/package.json new file mode 100644 index 0000000000..1541af3903 --- /dev/null +++ b/plugins/course-apps/progress/package.json @@ -0,0 +1,18 @@ +{ + "name": "@openedx-plugins/course-app-progress", + "version": "0.1.0", + "description": "Progress configuration for courses using it", + "peerDependencies": { + "@edx/frontend-app-course-authoring": "*", + "@edx/frontend-platform": "*", + "@openedx/paragon": "*", + "prop-types": "*", + "react": "*", + "yup": "*" + }, + "peerDependenciesMeta": { + "@edx/frontend-app-course-authoring": { + "optional": true + } + } +} diff --git a/src/pages-and-resources/teams/GroupEditor.jsx b/plugins/course-apps/teams/GroupEditor.jsx similarity index 96% rename from src/pages-and-resources/teams/GroupEditor.jsx rename to plugins/course-apps/teams/GroupEditor.jsx index 3785e4432c..3eaedeb040 100644 --- a/src/pages-and-resources/teams/GroupEditor.jsx +++ b/plugins/course-apps/teams/GroupEditor.jsx @@ -2,10 +2,10 @@ import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; import { Button, Form, TransitionReplace } from '@openedx/paragon'; import PropTypes from 'prop-types'; import React, { useState } from 'react'; -import { GroupTypes, TeamSizes } from '../../data/constants'; +import { GroupTypes, TeamSizes } from 'CourseAuthoring/data/constants'; -import CollapsableEditor from '../../generic/CollapsableEditor'; -import FormikControl from '../../generic/FormikControl'; +import CollapsableEditor from 'CourseAuthoring/generic/CollapsableEditor'; +import FormikControl from 'CourseAuthoring/generic/FormikControl'; import messages from './messages'; // Maps a team type to its corresponding intl message diff --git a/src/pages-and-resources/teams/Settings.jsx b/plugins/course-apps/teams/Settings.jsx similarity index 94% rename from src/pages-and-resources/teams/Settings.jsx rename to plugins/course-apps/teams/Settings.jsx index d6984e5a24..6f25140911 100644 --- a/src/pages-and-resources/teams/Settings.jsx +++ b/plugins/course-apps/teams/Settings.jsx @@ -7,10 +7,10 @@ import PropTypes from 'prop-types'; import React from 'react'; import { v4 as uuid } from 'uuid'; import * as Yup from 'yup'; -import { GroupTypes, TeamSizes } from '../../data/constants'; -import FormikControl from '../../generic/FormikControl'; -import { setupYupExtensions, useAppSetting } from '../../utils'; -import AppSettingsModal from '../app-settings-modal/AppSettingsModal'; +import { GroupTypes, TeamSizes } from 'CourseAuthoring/data/constants'; +import FormikControl from 'CourseAuthoring/generic/FormikControl'; +import { setupYupExtensions, useAppSetting } from 'CourseAuthoring/utils'; +import AppSettingsModal from 'CourseAuthoring/pages-and-resources/app-settings-modal/AppSettingsModal'; import GroupEditor from './GroupEditor'; import messages from './messages'; diff --git a/src/pages-and-resources/teams/messages.js b/plugins/course-apps/teams/messages.js similarity index 100% rename from src/pages-and-resources/teams/messages.js rename to plugins/course-apps/teams/messages.js diff --git a/plugins/course-apps/teams/package.json b/plugins/course-apps/teams/package.json new file mode 100644 index 0000000000..64471e694f --- /dev/null +++ b/plugins/course-apps/teams/package.json @@ -0,0 +1,20 @@ +{ + "name": "@openedx-plugins/course-app-teams", + "version": "0.1.0", + "description": "Teams configuration for courses using it", + "peerDependencies": { + "@edx/frontend-app-course-authoring": "*", + "@edx/frontend-platform": "*", + "@openedx/paragon": "*", + "formik": "*", + "prop-types": "*", + "react": "*", + "uuid": "*", + "yup": "*" + }, + "peerDependenciesMeta": { + "@edx/frontend-app-course-authoring": { + "optional": true + } + } +} diff --git a/src/pages-and-resources/wiki/Settings.jsx b/plugins/course-apps/wiki/Settings.jsx similarity index 86% rename from src/pages-and-resources/wiki/Settings.jsx rename to plugins/course-apps/wiki/Settings.jsx index a2711629c2..e71b27f81b 100644 --- a/src/pages-and-resources/wiki/Settings.jsx +++ b/plugins/course-apps/wiki/Settings.jsx @@ -3,9 +3,9 @@ import PropTypes from 'prop-types'; import React from 'react'; import * as Yup from 'yup'; -import FormSwitchGroup from '../../generic/FormSwitchGroup'; -import { useAppSetting } from '../../utils'; -import AppSettingsModal from '../app-settings-modal/AppSettingsModal'; +import FormSwitchGroup from 'CourseAuthoring/generic/FormSwitchGroup'; +import { useAppSetting } from 'CourseAuthoring/utils'; +import AppSettingsModal from 'CourseAuthoring/pages-and-resources/app-settings-modal/AppSettingsModal'; import messages from './messages'; const WikiSettings = ({ intl, onClose }) => { diff --git a/src/pages-and-resources/wiki/messages.js b/plugins/course-apps/wiki/messages.js similarity index 100% rename from src/pages-and-resources/wiki/messages.js rename to plugins/course-apps/wiki/messages.js diff --git a/plugins/course-apps/wiki/package.json b/plugins/course-apps/wiki/package.json new file mode 100644 index 0000000000..e14e897db3 --- /dev/null +++ b/plugins/course-apps/wiki/package.json @@ -0,0 +1,18 @@ +{ + "name": "@openedx-plugins/course-app-wiki", + "version": "0.1.0", + "description": "Wiki configuration for courses using it", + "peerDependencies": { + "@edx/frontend-app-course-authoring": "*", + "@edx/frontend-platform": "*", + "@openedx/paragon": "*", + "prop-types": "*", + "react": "*", + "yup": "*" + }, + "peerDependenciesMeta": { + "@edx/frontend-app-course-authoring": { + "optional": true + } + } +} diff --git a/plugins/course-apps/xpert_unit_summary/README.rst b/plugins/course-apps/xpert_unit_summary/README.rst new file mode 100644 index 0000000000..6c3c162782 --- /dev/null +++ b/plugins/course-apps/xpert_unit_summary/README.rst @@ -0,0 +1,4 @@ +Xpert Unit Summaries Configuration Plugin +========================================= + +Install this using ``npm install plugins/course-apps/xpert_unit_summary/ --no-save``. diff --git a/src/pages-and-resources/xpert-unit-summary/XpertUnitSummarySettings.jsx b/plugins/course-apps/xpert_unit_summary/Settings.jsx similarity index 93% rename from src/pages-and-resources/xpert-unit-summary/XpertUnitSummarySettings.jsx rename to plugins/course-apps/xpert_unit_summary/Settings.jsx index 664cad5c49..99825b9b72 100644 --- a/src/pages-and-resources/xpert-unit-summary/XpertUnitSummarySettings.jsx +++ b/plugins/course-apps/xpert_unit_summary/Settings.jsx @@ -2,8 +2,8 @@ import React, { useCallback, useContext, useEffect } from 'react'; import { useDispatch } from 'react-redux'; import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; +import { PagesAndResourcesContext } from 'CourseAuthoring/pages-and-resources/PagesAndResourcesProvider'; import { useNavigate } from 'react-router-dom'; -import { PagesAndResourcesContext } from '../PagesAndResourcesProvider'; import SettingsModal from './settings-modal/SettingsModal'; import messages from './messages'; diff --git a/src/pages-and-resources/xpert-unit-summary/XpertUnitSummarySettings.test.jsx b/plugins/course-apps/xpert_unit_summary/Settings.test.jsx similarity index 97% rename from src/pages-and-resources/xpert-unit-summary/XpertUnitSummarySettings.test.jsx rename to plugins/course-apps/xpert_unit_summary/Settings.test.jsx index 000b099992..69f9ba5e9d 100644 --- a/src/pages-and-resources/xpert-unit-summary/XpertUnitSummarySettings.test.jsx +++ b/plugins/course-apps/xpert_unit_summary/Settings.test.jsx @@ -10,12 +10,13 @@ import { queryByTestId, render, waitFor, getByText, fireEvent, } from '@testing-library/react'; import MockAdapter from 'axios-mock-adapter'; -import PagesAndResourcesProvider from '../PagesAndResourcesProvider'; -import { XpertUnitSummarySettings } from './index'; -import initializeStore from '../../store'; +import PagesAndResourcesProvider from 'CourseAuthoring/pages-and-resources/PagesAndResourcesProvider'; +import initializeStore from 'CourseAuthoring/store'; +import { executeThunk } from 'CourseAuthoring/utils'; + +import XpertUnitSummarySettings from './Settings'; import * as API from './data/api'; import * as Thunks from './data/thunks'; -import { executeThunk } from '../../utils'; const courseId = 'course-v1:edX+TestX+Test_Course'; let axiosMock; diff --git a/src/pages-and-resources/xpert-unit-summary/appInfo.js b/plugins/course-apps/xpert_unit_summary/appInfo.js similarity index 100% rename from src/pages-and-resources/xpert-unit-summary/appInfo.js rename to plugins/course-apps/xpert_unit_summary/appInfo.js diff --git a/src/pages-and-resources/xpert-unit-summary/data/api.js b/plugins/course-apps/xpert_unit_summary/data/api.js similarity index 100% rename from src/pages-and-resources/xpert-unit-summary/data/api.js rename to plugins/course-apps/xpert_unit_summary/data/api.js diff --git a/src/pages-and-resources/xpert-unit-summary/data/thunks.js b/plugins/course-apps/xpert_unit_summary/data/thunks.js similarity index 94% rename from src/pages-and-resources/xpert-unit-summary/data/thunks.js rename to plugins/course-apps/xpert_unit_summary/data/thunks.js index b79a36ccb1..270fdce5f9 100644 --- a/src/pages-and-resources/xpert-unit-summary/data/thunks.js +++ b/plugins/course-apps/xpert_unit_summary/data/thunks.js @@ -1,12 +1,11 @@ +import { updateSavingStatus, updateLoadingStatus, updateResetStatus } from 'CourseAuthoring/pages-and-resources/data/slice'; +import { RequestStatus } from 'CourseAuthoring/data/constants'; +import { addModel, updateModel } from 'CourseAuthoring/generic/model-store'; + import { getXpertSettings, postXpertSettings, getXpertPluginConfigurable, deleteXpertSettings, } from './api'; -import { updateSavingStatus, updateLoadingStatus, updateResetStatus } from '../../data/slice'; -import { RequestStatus } from '../../../data/constants'; - -import { addModel, updateModel } from '../../../generic/model-store'; - export function updateXpertSettings(courseId, state) { return async (dispatch) => { dispatch(updateSavingStatus({ status: RequestStatus.IN_PROGRESS })); diff --git a/src/pages-and-resources/xpert-unit-summary/messages.js b/plugins/course-apps/xpert_unit_summary/messages.js similarity index 100% rename from src/pages-and-resources/xpert-unit-summary/messages.js rename to plugins/course-apps/xpert_unit_summary/messages.js diff --git a/plugins/course-apps/xpert_unit_summary/package.json b/plugins/course-apps/xpert_unit_summary/package.json new file mode 100644 index 0000000000..e8850ecdfa --- /dev/null +++ b/plugins/course-apps/xpert_unit_summary/package.json @@ -0,0 +1,21 @@ +{ + "name": "@openedx-plugins/course-app-xpert_unit_summary", + "version": "0.1.0", + "description": "Xpert Unit Summaries configuration for courses using it", + "peerDependencies": { + "@edx/frontend-app-course-authoring": "*", + "@edx/frontend-platform": "*", + "@openedx/paragon": "*", + "formik": "*", + "prop-types": "*", + "yup": "*", + "react": "*", + "react-redux": "*", + "react-router-dom": "*" + }, + "peerDependenciesMeta": { + "@edx/frontend-app-course-authoring": { + "optional": true + } + } +} diff --git a/src/pages-and-resources/xpert-unit-summary/settings-modal/ResetIcon.jsx b/plugins/course-apps/xpert_unit_summary/settings-modal/ResetIcon.jsx similarity index 100% rename from src/pages-and-resources/xpert-unit-summary/settings-modal/ResetIcon.jsx rename to plugins/course-apps/xpert_unit_summary/settings-modal/ResetIcon.jsx diff --git a/src/pages-and-resources/xpert-unit-summary/settings-modal/SettingsModal.jsx b/plugins/course-apps/xpert_unit_summary/settings-modal/SettingsModal.jsx similarity index 93% rename from src/pages-and-resources/xpert-unit-summary/settings-modal/SettingsModal.jsx rename to plugins/course-apps/xpert_unit_summary/settings-modal/SettingsModal.jsx index 8c31ddad0a..2d98304d11 100644 --- a/src/pages-and-resources/xpert-unit-summary/settings-modal/SettingsModal.jsx +++ b/plugins/course-apps/xpert_unit_summary/settings-modal/SettingsModal.jsx @@ -24,22 +24,25 @@ import React, { import { useDispatch, useSelector } from 'react-redux'; import * as Yup from 'yup'; -import { RequestStatus } from '../../../data/constants'; -import ConnectionErrorAlert from '../../../generic/ConnectionErrorAlert'; -import FormSwitchGroup from '../../../generic/FormSwitchGroup'; -import Loading from '../../../generic/Loading'; -import { useModel } from '../../../generic/model-store'; -import PermissionDeniedAlert from '../../../generic/PermissionDeniedAlert'; -import { useIsMobile } from '../../../utils'; -import { getLoadingStatus, getSavingStatus, getResetStatus } from '../../data/selectors'; -import { updateSavingStatus, updateResetStatus } from '../../data/slice'; +import { RequestStatus } from 'CourseAuthoring/data/constants'; +import ConnectionErrorAlert from 'CourseAuthoring/generic/ConnectionErrorAlert'; +import FormSwitchGroup from 'CourseAuthoring/generic/FormSwitchGroup'; +import Loading from 'CourseAuthoring/generic/Loading'; +import { useModel } from 'CourseAuthoring/generic/model-store'; +import PermissionDeniedAlert from 'CourseAuthoring/generic/PermissionDeniedAlert'; +import { useIsMobile } from 'CourseAuthoring/utils'; +import { getLoadingStatus, getSavingStatus, getResetStatus } from 'CourseAuthoring/pages-and-resources/data/selectors'; +import { updateSavingStatus, updateResetStatus } from 'CourseAuthoring/pages-and-resources/data/slice'; +import AppConfigFormDivider from 'CourseAuthoring/pages-and-resources/discussions/app-config-form/apps/shared/AppConfigFormDivider'; +import { PagesAndResourcesContext } from 'CourseAuthoring/pages-and-resources/PagesAndResourcesProvider'; + import { updateXpertSettings, resetXpertSettings, removeXpertSettings } from '../data/thunks'; -import AppConfigFormDivider from '../../discussions/app-config-form/apps/shared/AppConfigFormDivider'; -import { PagesAndResourcesContext } from '../../PagesAndResourcesProvider'; import messages from './messages'; import appInfo from '../appInfo'; import ResetIcon from './ResetIcon'; +import './SettingsModal.scss'; + const AppSettingsForm = ({ formikProps, children, showForm, }) => children && ( diff --git a/src/pages-and-resources/xpert-unit-summary/settings-modal/SettingsModal.scss b/plugins/course-apps/xpert_unit_summary/settings-modal/SettingsModal.scss similarity index 88% rename from src/pages-and-resources/xpert-unit-summary/settings-modal/SettingsModal.scss rename to plugins/course-apps/xpert_unit_summary/settings-modal/SettingsModal.scss index 82942e2105..264e0c7b1a 100644 --- a/src/pages-and-resources/xpert-unit-summary/settings-modal/SettingsModal.scss +++ b/plugins/course-apps/xpert_unit_summary/settings-modal/SettingsModal.scss @@ -1,3 +1,6 @@ +@import "~@edx/brand/paragon/variables"; +@import "~@openedx/paragon/scss/core/utilities-only"; + .summary-radio { display: flex; align-items: center; diff --git a/src/pages-and-resources/xpert-unit-summary/settings-modal/messages.js b/plugins/course-apps/xpert_unit_summary/settings-modal/messages.js similarity index 100% rename from src/pages-and-resources/xpert-unit-summary/settings-modal/messages.js rename to plugins/course-apps/xpert_unit_summary/settings-modal/messages.js diff --git a/src/index.scss b/src/index.scss index 9c3325d241..c8f328f335 100755 --- a/src/index.scss +++ b/src/index.scss @@ -13,7 +13,6 @@ @import "studio-home/scss/StudioHome"; @import "generic/styles"; @import "schedule-and-details/ScheduleAndDetails"; -@import "pages-and-resources/PagesAndResources"; @import "course-team/CourseTeam"; @import "course-updates/CourseUpdates"; @import "export-page/CourseExportPage"; diff --git a/src/pages-and-resources/PagesAndResources.jsx b/src/pages-and-resources/PagesAndResources.jsx index 1356799bf5..313f8043fa 100644 --- a/src/pages-and-resources/PagesAndResources.jsx +++ b/src/pages-and-resources/PagesAndResources.jsx @@ -5,17 +5,10 @@ import { PageWrap, AppContext } from '@edx/frontend-platform/react'; import { Routes, Route } from 'react-router-dom'; import { useDispatch, useSelector } from 'react-redux'; -import _ from 'lodash'; import { Button, Hyperlink } from '@openedx/paragon'; import messages from './messages'; import DiscussionsSettings from './discussions'; -import { - XpertUnitSummarySettings, - fetchXpertPluginConfigurable, - fetchXpertSettings, - appInfo as XpertAppInfo, -} from './xpert-unit-summary'; import PageGrid from './pages/PageGrid'; import { fetchCourseApps } from './data/thunks'; @@ -34,8 +27,6 @@ const PagesAndResources = ({ courseId, intl }) => { const dispatch = useDispatch(); useEffect(() => { dispatch(fetchCourseApps(courseId)); - dispatch(fetchXpertPluginConfigurable(courseId)); - dispatch(fetchXpertSettings(courseId)); }, [courseId]); const courseAppIds = useSelector(state => state.pagesAndResources.courseAppIds); @@ -46,32 +37,21 @@ const PagesAndResources = ({ courseId, intl }) => { const learningCourseURL = `${config.LEARNING_BASE_URL}/course/${courseId}`; const redirectUrl = `/course/${courseId}/pages-and-resources`; - // Most pages here are driven by a course app. The one exception is the Xpert unit summaries page. + // The pages here are driven by course apps. The list of course app IDs comes from the LMS API. + // We display all enabled course apps regardless of whether or not the corresponding frontend plugin is available. const pages = useModels('courseApps', courseAppIds); - const xpertPluginConfigurable = useModel('XpertSettings.enabled', 'xpert-unit-summary'); - const xpertSettings = useModel('XpertSettings', 'xpert-unit-summary'); - // These pages appear in a separate "Content Permissions" section at the bottom of the page. - // If there are no content permission pages, this section will not appear. + // We want the Xpert learning assistant and unit summaries to appear in the "Content Permissions" section instead, + // so we remove them from pages and add them to contentPermissionsPages. const contentPermissionsPages = []; - // Xpert unit summaries - if (xpertPluginConfigurable?.enabled) { - contentPermissionsPages.push({ - ...XpertAppInfo, - enabled: xpertSettings?.enabled !== undefined, - }); - } - - // Xpert learning assistant - if (_.some(pages, (page) => page.id === 'learning_assistant')) { - const index = pages.findIndex(app => app.id === 'learning_assistant'); - - // We want the Xpert learning assistant page to appear in the "Content Permissions" section instead, - // so we remove it from pages and add it to contentPermissionsPages. - const [page] = pages.splice(index, 1); - contentPermissionsPages.push(page); - } + ['xpert_unit_summary', 'learning_assistant'].forEach(separateAppId => { + const index = pages.findIndex(app => app.id === separateAppId); + if (index !== -1) { + const [page] = pages.splice(index, 1); + contentPermissionsPages.push(page); + } + }); if (loadingStatus === RequestStatus.IN_PROGRESS) { // eslint-disable-next-line react/jsx-no-useless-fragment @@ -99,10 +79,16 @@ const PagesAndResources = ({ courseId, intl }) => { - + + } /> + } /> + } /> + } /> + + { - !_.isEmpty(contentPermissionsPages) && ( + (contentPermissionsPages.length > 0) && ( <>

{intl.formatMessage(messages.contentPermissions)}

@@ -111,14 +97,6 @@ const PagesAndResources = ({ courseId, intl }) => { ) } - - - } /> - } /> - } /> - } /> - } /> - ); diff --git a/src/pages-and-resources/PagesAndResources.scss b/src/pages-and-resources/PagesAndResources.scss deleted file mode 100644 index b577fe0f4b..0000000000 --- a/src/pages-and-resources/PagesAndResources.scss +++ /dev/null @@ -1 +0,0 @@ -@import "./xpert-unit-summary/settings-modal/SettingsModal"; diff --git a/src/pages-and-resources/PagesAndResources.test.jsx b/src/pages-and-resources/PagesAndResources.test.jsx index b30aedaafb..180c3f8674 100644 --- a/src/pages-and-resources/PagesAndResources.test.jsx +++ b/src/pages-and-resources/PagesAndResources.test.jsx @@ -1,10 +1,7 @@ -import { camelCaseObject } from '@edx/frontend-platform'; import { screen, waitFor } from '@testing-library/react'; import { PagesAndResources } from '.'; -import * as pagesAndResourcesApi from './data/api'; import { render } from './utils.test'; -import * as xpertUnitSummaryApi from './xpert-unit-summary/data/api'; const courseId = 'course-v1:edX+TestX+Test_Course'; @@ -14,56 +11,80 @@ describe('PagesAndResources', () => { }); it('doesn\'t show content permissions section if relevant apps are not enabled', () => { - jest.spyOn(pagesAndResourcesApi, 'getCourseApps').mockResolvedValue(camelCaseObject([])); - - const apiResponse = { response: { enabled: true } }; - jest.spyOn(xpertUnitSummaryApi, 'getXpertSettings').mockResolvedValue(apiResponse); - jest.spyOn(xpertUnitSummaryApi, 'getXpertPluginConfigurable').mockResolvedValue(apiResponse); + const initialState = { + models: { + courseApps: {}, + }, + pagesAndResources: { + courseAppIds: [], + }, + }; render( - , + , + { preloadedState: initialState }, ); expect(screen.queryByRole('heading', { name: 'Content permissions' })).not.toBeInTheDocument(); }); it('show content permissions section if Learning Assistant app is enabled', async () => { - const apiResponse = [ - { - id: 'learning_assistant', - enabled: true, - name: 'Learning Assistant', - description: 'Learning Assistant description', - allowed_operations: { - configure: false, - enable: true, + const initialState = { + models: { + courseApps: { + learning_assistant: { + id: 'learning_assistant', + enabled: true, + name: 'Learning Assistant', + description: 'Learning Assistant description', + allowedOperations: { + configure: false, + enable: true, + }, + documentationLinks: {}, + }, }, - documentation_links: {}, }, - ]; - - jest.spyOn(pagesAndResourcesApi, 'getCourseApps').mockResolvedValue(camelCaseObject(apiResponse)); + pagesAndResources: { + courseAppIds: ['learning_assistant'], + }, + }; render( - , + , + { preloadedState: initialState }, ); await waitFor(() => expect(screen.getByRole('heading', { name: 'Content permissions' })).toBeInTheDocument()); await waitFor(() => expect(screen.getByText('Learning Assistant')).toBeInTheDocument()); }); - it('show content permissions section if Xpert learning summaries app is enabled', async () => { - const apiResponse = { response: { enabled: true } }; - jest.spyOn(xpertUnitSummaryApi, 'getXpertSettings').mockResolvedValue(apiResponse); - jest.spyOn(xpertUnitSummaryApi, 'getXpertPluginConfigurable').mockResolvedValue(apiResponse); + it('show content permissions section if Xpert learning summaries app is enabled', async () => { + const initialState = { + models: { + courseApps: { + xpert_unit_summary: { + id: 'xpert_unit_summary', + enabled: false, + name: 'Xpert unit summaries', + description: 'Use generative AI to summarize course content and reinforce learning.', + allowedOperations: { + enable: true, + configure: true, + }, + documentationLinks: { + learnMoreConfiguration: 'https://openai.com/', + }, + }, + }, + }, + pagesAndResources: { + courseAppIds: ['xpert_unit_summary'], + }, + }; render( - , + , + { preloadedState: initialState }, ); await waitFor(() => expect(screen.getByRole('heading', { name: 'Content permissions' })).toBeInTheDocument()); diff --git a/src/pages-and-resources/SettingsComponent.jsx b/src/pages-and-resources/SettingsComponent.jsx index e15c598faa..a45d64db10 100644 --- a/src/pages-and-resources/SettingsComponent.jsx +++ b/src/pages-and-resources/SettingsComponent.jsx @@ -1,23 +1,29 @@ import React from 'react'; import PropTypes from 'prop-types'; import { useParams, useNavigate } from 'react-router-dom'; +import { useIntl } from '@edx/frontend-platform/i18n'; +import { ErrorAlert } from '@edx/frontend-lib-content-components'; + +import messages from './messages'; + +const PluginLoadFailedError = () => { + const intl = useIntl(); + return {intl.formatMessage(messages.errorShowingConfiguration)}; +}; const SettingsComponent = ({ url }) => { const { appId } = useParams(); const navigate = useNavigate(); - const LazyLoadedComponent = React.lazy(async () => { - try { - // There seems to be a bug in babel-eslint that causes the checker to crash with the following error - // if we use a template string here: - // TypeError: Cannot read property 'range' of null with using template strings here. - // Ref: https://github.com/babel/babel-eslint/issues/530 - return await import(`./${appId}/Settings.jsx`); - } catch (error) { - console.trace(error); // eslint-disable-line no-console - return null; - } - }); + const LazyLoadedComponent = React.useMemo( + () => React.lazy(() => + import(`@openedx-plugins/course-app-${appId}/Settings.jsx`).catch((err) => { // eslint-disable-line + // If we couldn't load this plugin, log the details to the console. + console.trace(err); // eslint-disable-line no-console + return { default: PluginLoadFailedError }; + })), + [appId], + ); return navigate(url)} />; }; diff --git a/src/pages-and-resources/SettingsComponent.test.jsx b/src/pages-and-resources/SettingsComponent.test.jsx index 382d3992a4..21b3aa4244 100644 --- a/src/pages-and-resources/SettingsComponent.test.jsx +++ b/src/pages-and-resources/SettingsComponent.test.jsx @@ -1,26 +1,83 @@ import React, { Suspense } from 'react'; -import { BrowserRouter } from 'react-router-dom'; -import { render } from '@testing-library/react'; +import { useParams } from 'react-router-dom'; +import { render, screen, waitFor } from '@testing-library/react'; +import { IntlProvider } from '@edx/frontend-platform/i18n'; +import { AppProvider } from '@edx/frontend-platform/react'; +import { initializeMockApp } from '@edx/frontend-platform/testing'; +import PagesAndResourcesProvider from 'CourseAuthoring/pages-and-resources/PagesAndResourcesProvider'; +import initializeStore from 'CourseAuthoring/store'; +import { RequestStatus } from 'CourseAuthoring/data/constants'; import SettingsComponent from './SettingsComponent'; jest.mock('react-router-dom', () => ({ ...jest.requireActual('react-router-dom'), - useParams: () => ({ - appId: 'wiki', - }), + useParams: jest.fn(), })); +jest.mock('CourseAuthoring/utils', () => ({ + useAppSetting: () => [false, () => undefined], + useIsMobile: () => false, +})); + +let store; + +// eslint-disable-next-line react/prop-types +const RequiredProviders = ({ children }) => ( + + + + {children} + + + +); + describe('SettingsComponent', () => { + beforeEach(async () => { + initializeMockApp(); + store = initializeStore({ + models: { + courseApps: { + wiki: {}, + }, + }, + pagesAndResources: { + loadingStatus: RequestStatus.SUCCESSFUL, + }, + }); + }); + test('renders LazyLoadedComponent when provided with props', async () => { - const { asFragment } = render( - - - - - , + useParams.mockImplementation(() => ({ appId: 'wiki' })); + + const rendered = render( + + + , + { wrapper: RequiredProviders }, + ); + + await waitFor(() => expect(rendered.getByText('Configure wiki')).toBeInTheDocument()); + + const modalComponent = screen.getByRole('dialog'); + expect(modalComponent.querySelector('#enable-wiki-toggleHelpText')).toContainHTML('The course wiki can be set up'); + }); + + test('renders error message when plugin is unavilable when provided with props', async () => { + // Silence noisy error about the plugin failing to load, when we do that deliberately. + jest.spyOn(console, 'trace').mockImplementation(() => {}); + // Specify an invalid course app, with no matching plugin: + useParams.mockImplementation(() => ({ appId: 'invalid-plugin' })); + + const rendered = render( + + + , + { wrapper: RequiredProviders }, ); - expect(asFragment).toMatchSnapshot(); + const errorMessage = 'An error occurred when loading the configuration UI'; + await waitFor(() => expect(rendered.container).toHaveTextContent(errorMessage)); }); }); diff --git a/src/pages-and-resources/__snapshots__/SettingsComponent.test.jsx.snap b/src/pages-and-resources/__snapshots__/SettingsComponent.test.jsx.snap deleted file mode 100644 index 65a738980a..0000000000 --- a/src/pages-and-resources/__snapshots__/SettingsComponent.test.jsx.snap +++ /dev/null @@ -1,3 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`SettingsComponent renders LazyLoadedComponent when provided with props 1`] = `[Function]`; diff --git a/src/pages-and-resources/app-settings-modal/AppSettingsModal.jsx b/src/pages-and-resources/app-settings-modal/AppSettingsModal.jsx index ec16da774c..477b269bb9 100644 --- a/src/pages-and-resources/app-settings-modal/AppSettingsModal.jsx +++ b/src/pages-and-resources/app-settings-modal/AppSettingsModal.jsx @@ -126,6 +126,8 @@ const AppSettingsModal = ({ const updateSettingsRequestStatus = useSelector(getSavingStatus); const alertRef = useRef(null); const [saveError, setSaveError] = useState(false); + // FIXME: open the "Live" settings, then refresh the page. The courseApps model is not loaded, and an error occurs + // when trying to access 'appInfo.documentationLinks'. This happens even before the refactor to use plugins. const appInfo = useModel('courseApps', appId); const dispatch = useDispatch(); const submitButtonState = updateSettingsRequestStatus === RequestStatus.IN_PROGRESS ? 'pending' : 'default'; diff --git a/src/pages-and-resources/calculator/Settings.jsx b/src/pages-and-resources/calculator/Settings.jsx deleted file mode 100644 index 138532de81..0000000000 --- a/src/pages-and-resources/calculator/Settings.jsx +++ /dev/null @@ -1,25 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; - -import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; - -import AppSettingsModal from '../app-settings-modal/AppSettingsModal'; -import messages from './messages'; - -const CalculatorSettings = ({ intl, onClose }) => ( - -); - -CalculatorSettings.propTypes = { - intl: intlShape.isRequired, - onClose: PropTypes.func.isRequired, -}; - -export default injectIntl(CalculatorSettings); diff --git a/src/pages-and-resources/discussions/app-config-form/apps/openedx/OpenedXConfigForm.jsx b/src/pages-and-resources/discussions/app-config-form/apps/openedx/OpenedXConfigForm.jsx index c5bc4740df..f11f1b7e59 100644 --- a/src/pages-and-resources/discussions/app-config-form/apps/openedx/OpenedXConfigForm.jsx +++ b/src/pages-and-resources/discussions/app-config-form/apps/openedx/OpenedXConfigForm.jsx @@ -6,8 +6,8 @@ import React, { useState } from 'react'; import { useSelector } from 'react-redux'; import * as Yup from 'yup'; -import { useModel, useModels } from '../../../../../generic/model-store'; -import { setupYupExtensions } from '../../../../../utils'; +import { useModel, useModels } from 'CourseAuthoring/generic/model-store'; +import { setupYupExtensions } from 'CourseAuthoring/utils'; import messages from '../../messages'; import { checkFieldErrors } from '../../utils'; import AnonymousPostingFields from '../shared/AnonymousPostingFields'; diff --git a/src/pages-and-resources/edxnotes/Settings.jsx b/src/pages-and-resources/edxnotes/Settings.jsx deleted file mode 100644 index bfc510bbac..0000000000 --- a/src/pages-and-resources/edxnotes/Settings.jsx +++ /dev/null @@ -1,25 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; - -import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; - -import AppSettingsModal from '../app-settings-modal/AppSettingsModal'; -import messages from './messages'; - -const NotesSettings = ({ intl, onClose }) => ( - -); - -NotesSettings.propTypes = { - intl: intlShape.isRequired, - onClose: PropTypes.func.isRequired, -}; - -export default injectIntl(NotesSettings); diff --git a/src/pages-and-resources/messages.js b/src/pages-and-resources/messages.js index 97148c29c1..cd8670dddf 100644 --- a/src/pages-and-resources/messages.js +++ b/src/pages-and-resources/messages.js @@ -13,6 +13,10 @@ const messages = defineMessages({ id: 'course-authoring.pages-resources.viewLive.button', defaultMessage: 'View live', }, + errorShowingConfiguration: { + id: 'course-authoring.pages-resources.courseAppPlugin.errorMessage', + defaultMessage: 'An error occurred when loading the configuration UI for that app.', + }, enabled: { id: 'course-authoring.badge.enabled', defaultMessage: 'Enabled', diff --git a/src/pages-and-resources/xpert-unit-summary/index.js b/src/pages-and-resources/xpert-unit-summary/index.js deleted file mode 100644 index 1ba14190b3..0000000000 --- a/src/pages-and-resources/xpert-unit-summary/index.js +++ /dev/null @@ -1,10 +0,0 @@ -import XpertUnitSummarySettings from './XpertUnitSummarySettings'; -import appInfo from './appInfo'; -import { fetchXpertPluginConfigurable, fetchXpertSettings } from './data/thunks'; - -export { - XpertUnitSummarySettings, - appInfo, - fetchXpertPluginConfigurable, - fetchXpertSettings, -}; diff --git a/src/store.js b/src/store.js index ee193470b8..2d36dcf454 100644 --- a/src/store.js +++ b/src/store.js @@ -1,15 +1,18 @@ import { configureStore } from '@reduxjs/toolkit'; +// FIXME: because the 'live' plugin is using Redux, we have to hard-code a reference to it here. +// If this app + the plugin were using React-query, there'd be no issues. +import { reducer as liveReducer } from '@openedx-plugins/course-app-live/data/slice'; + import { reducer as modelsReducer } from './generic/model-store'; import { reducer as courseDetailReducer } from './data/slice'; -import { reducer as discussionsReducer } from './pages-and-resources/discussions'; +import { reducer as discussionsReducer } from './pages-and-resources/discussions/data/slice'; import { reducer as pagesAndResourcesReducer } from './pages-and-resources/data/slice'; import { reducer as customPagesReducer } from './custom-pages/data/slice'; import { reducer as advancedSettingsReducer } from './advanced-settings/data/slice'; import { reducer as gradingSettingsReducer } from './grading-settings/data/slice'; import { reducer as studioHomeReducer } from './studio-home/data/slice'; import { reducer as scheduleAndDetailsReducer } from './schedule-and-details/data/slice'; -import { reducer as liveReducer } from './pages-and-resources/live/data/slice'; import { reducer as filesReducer } from './files-and-videos/files-page/data/slice'; import { reducer as courseTeamReducer } from './course-team/data/slice'; import { reducer as CourseUpdatesReducer } from './course-updates/data/slice'; diff --git a/webpack.dev.config.js b/webpack.dev.config.js index 1e112fecf5..57ee9080e0 100644 --- a/webpack.dev.config.js +++ b/webpack.dev.config.js @@ -1,7 +1,12 @@ +const path = require('path'); const { createConfig } = require('@openedx/frontend-build'); const config = createConfig('webpack-dev', { resolve: { + alias: { + // Plugins can use 'CourseAuthoring' as an import alias for this app: + CourseAuthoring: path.resolve(__dirname, 'src/'), + }, fallback: { fs: false, constants: false, diff --git a/webpack.prod.config.js b/webpack.prod.config.js index 097148f66b..c5e13b0417 100644 --- a/webpack.prod.config.js +++ b/webpack.prod.config.js @@ -1,7 +1,12 @@ +const path = require('path'); const { createConfig } = require('@openedx/frontend-build'); const config = createConfig('webpack-prod', { resolve: { + alias: { + // Plugins can use 'CourseAuthoring' as an import alias for this app: + CourseAuthoring: path.resolve(__dirname, 'src/'), + }, fallback: { fs: false, constants: false,