diff --git a/package-lock.json b/package-lock.json index f2697f368..d9c200880 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3518,13 +3518,14 @@ } }, "@edx/paragon": { - "version": "13.10.0", - "resolved": "https://registry.npmjs.org/@edx/paragon/-/paragon-13.10.0.tgz", - "integrity": "sha512-hC5jNgDCZcpnfW8udBfvH+5d5vMz0AARnSj/6jDdpWCpwvFfJqGuiUhQNIyceJ9HXyBZAadr9qvZcEqpNxVTDQ==", + "version": "13.13.5", + "resolved": "https://registry.npmjs.org/@edx/paragon/-/paragon-13.13.5.tgz", + "integrity": "sha512-6q60Lj5dbzZbcXfNrpHXqn4tKQYf1KmcISOe//un0JTSQr6/LnZjHZoZfmzDaRX/2KEDaLCTqCefvKTpB26qCQ==", "requires": { "@fortawesome/fontawesome-svg-core": "^1.2.30", "@fortawesome/free-solid-svg-icons": "^5.14.0", "@fortawesome/react-fontawesome": "^0.1.11", + "@popperjs/core": "^2.6.0", "airbnb-prop-types": "^2.12.0", "bootstrap": "4.6.0", "classnames": "^2.2.6", @@ -3534,13 +3535,14 @@ "prop-types": "^15.7.2", "react-bootstrap": "^1.2.2", "react-focus-on": "^3.5.0", + "react-popper": "^2.2.4", "react-proptype-conditional-require": "^1.0.4", "react-responsive": "^6.1.1", "react-table": "^7.6.1", "react-transition-group": "^4.0.0", "sanitize-html": "^1.20.0", "tabbable": "^4.0.0", - "uncontrollable": "7.1.1" + "uncontrollable": "7.2.1" }, "dependencies": { "@fortawesome/fontawesome-common-types": { @@ -4821,9 +4823,9 @@ } }, "@popperjs/core": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.6.0.tgz", - "integrity": "sha512-cPqjjzuFWNK3BSKLm0abspP0sp/IGOli4p5I5fKFAzdS8fvjdOwDCfZqAaIiXd9lPkOWi3SUUfZof3hEb7J/uw==" + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.9.0.tgz", + "integrity": "sha512-wjtKehFAIARq2OxK8j3JrggNlEslJfNuSm2ArteIbKyRMts2g0a7KzTxfRVNUM+O0gnBJ2hNV8nWPOYBgI1sew==" }, "@reduxjs/toolkit": { "version": "1.5.0", @@ -4858,9 +4860,9 @@ }, "dependencies": { "lodash": { - "version": "4.17.20", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", - "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==" + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" } } }, @@ -5501,16 +5503,16 @@ }, "dependencies": { "csstype": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.6.tgz", - "integrity": "sha512-+ZAmfyWMT7TiIlzdqJgjMb7S4f1beorDbWbsocyK4RaiqA5RTX3K14bnBWmmA9QEM0gRdsjyyrEmcyga8Zsxmw==" + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.7.tgz", + "integrity": "sha512-KxnUB0ZMlnUWCsx2Z8MUsr6qV6ja1w9ArPErJaJaF8a5SOWoHLIszeCTKGRGRgtLgYrs1E8CHkNSP1VZTTPc9g==" } } }, "@types/react-transition-group": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.0.tgz", - "integrity": "sha512-/QfLHGpu+2fQOqQaXh8MG9q03bFENooTb/it4jr5kKaZlDQfWvjqWZg48AwzPVMBHlRuTRAY7hRHCEOXz5kV6w==", + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.1.tgz", + "integrity": "sha512-vIo69qKKcYoJ8wKCJjwSgCTM+z3chw3g18dkrDfVX665tMH7tmbDxEAnPdey4gTlwZz5QuHGzd+hul0OVZDqqQ==", "requires": { "@types/react": "*" } @@ -11464,48 +11466,65 @@ "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" }, "function.prototype.name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.3.tgz", - "integrity": "sha512-H51qkbNSp8mtkJt+nyW1gyStBiKZxfRqySNUR99ylq6BPXHKI4SEvIlTKp4odLfjRKJV04DFWMU3G/YRlQOsag==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.4.tgz", + "integrity": "sha512-iqy1pIotY/RmhdFZygSSlW0wko2yxkSCKqsuv4pr8QESohpYyG/Z7B/XXvPRKTJS//960rgguE5mSRUsDdaJrQ==", "requires": { - "call-bind": "^1.0.0", + "call-bind": "^1.0.2", "define-properties": "^1.1.3", - "es-abstract": "^1.18.0-next.1", - "functions-have-names": "^1.2.1" + "es-abstract": "^1.18.0-next.2", + "functions-have-names": "^1.2.2" }, "dependencies": { + "call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "requires": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + } + }, "es-abstract": { - "version": "1.18.0-next.2", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0-next.2.tgz", - "integrity": "sha512-Ih4ZMFHEtZupnUh6497zEL4y2+w8+1ljnCyaTa+adcoafI1GOvMwFlDjBLfWR7y9VLfrjRJe9ocuHY1PSR9jjw==", + "version": "1.18.0-next.3", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0-next.3.tgz", + "integrity": "sha512-VMzHx/Bczjg59E6jZOQjHeN3DEoptdhejpARgflAViidlqSpjdq9zA6lKwlhRRs/lOw1gHJv2xkkSFRgvEwbQg==", "requires": { "call-bind": "^1.0.2", "es-to-primitive": "^1.2.1", "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2", + "get-intrinsic": "^1.1.1", "has": "^1.0.3", - "has-symbols": "^1.0.1", - "is-callable": "^1.2.2", + "has-symbols": "^1.0.2", + "is-callable": "^1.2.3", "is-negative-zero": "^2.0.1", - "is-regex": "^1.1.1", + "is-regex": "^1.1.2", + "is-string": "^1.0.5", "object-inspect": "^1.9.0", "object-keys": "^1.1.1", "object.assign": "^4.1.2", - "string.prototype.trimend": "^1.0.3", - "string.prototype.trimstart": "^1.0.3" + "string.prototype.trimend": "^1.0.4", + "string.prototype.trimstart": "^1.0.4", + "unbox-primitive": "^1.0.0" }, "dependencies": { - "call-bind": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", - "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "get-intrinsic": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz", + "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==", "requires": { "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" + "has": "^1.0.3", + "has-symbols": "^1.0.1" } } } }, + "has-symbols": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz", + "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==" + }, "is-callable": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.3.tgz", @@ -11518,17 +11537,6 @@ "requires": { "call-bind": "^1.0.2", "has-symbols": "^1.0.1" - }, - "dependencies": { - "call-bind": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", - "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", - "requires": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" - } - } } }, "object-inspect": { @@ -11548,20 +11556,20 @@ } }, "string.prototype.trimend": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.3.tgz", - "integrity": "sha512-ayH0pB+uf0U28CtjlLvL7NaohvR1amUvVZk+y3DYb0Ey2PUV5zPkkKy9+U1ndVEIXO8hNg18eIv9Jntbii+dKw==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz", + "integrity": "sha512-y9xCjw1P23Awk8EvTpcyL2NIr1j7wJ39f+k6lvRnSMz+mz9CGz9NYPelDk42kOz6+ql8xjfK8oYzy3jAP5QU5A==", "requires": { - "call-bind": "^1.0.0", + "call-bind": "^1.0.2", "define-properties": "^1.1.3" } }, "string.prototype.trimstart": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.3.tgz", - "integrity": "sha512-oBIBUy5lea5tt0ovtOFiEQaBkoBBkyJhZXzJYrSmDo5IUUqbOPvVezuRs/agBIdZ2p2Eo1FD6bD9USyBLfl3xg==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.4.tgz", + "integrity": "sha512-jh6e984OBfvxS50tdY2nRZnoC5/mLFKOREQfw8t5yytkoUsJRNxvI/E39qu1sD0OtWI3OC0XgKSmcWwziwYuZw==", "requires": { - "call-bind": "^1.0.0", + "call-bind": "^1.0.2", "define-properties": "^1.1.3" } } @@ -11859,6 +11867,11 @@ } } }, + "has-bigints": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.1.tgz", + "integrity": "sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA==" + }, "has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", @@ -13052,6 +13065,11 @@ "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", "dev": true }, + "is-bigint": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.1.tgz", + "integrity": "sha512-J0ELF4yHFxHy0cmSxZuheDOz2luOdVvqjwmEcj8H/L1JHeuEDSDbeRP+Dk9kFVk5RTFzbucJ2Kb9F7ixY2QaCg==" + }, "is-binary-path": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz", @@ -13061,6 +13079,14 @@ "binary-extensions": "^1.0.0" } }, + "is-boolean-object": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.0.tgz", + "integrity": "sha512-a7Uprx8UtD+HWdyYwnD1+ExtTgqQtD2k/1yJgtXP6wnMm8byhkoTZRl+95LLThpzNZJ5aEvi46cdH+ayMFRwmA==", + "requires": { + "call-bind": "^1.0.0" + } + }, "is-buffer": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.4.tgz", @@ -13315,6 +13341,11 @@ } } }, + "is-number-object": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.4.tgz", + "integrity": "sha512-zohwelOAur+5uXtk8O3GPQ1eAcu4ZX3UwxQhUlfFFMNpUd83gXgjbhJh6HmB6LUNV/ieOLQuDwJO3dWJosUeMw==" + }, "is-obj": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", @@ -13416,8 +13447,7 @@ "is-string": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.5.tgz", - "integrity": "sha512-buY6VNRjhQMiF1qWDouloZlQbRhDPCebwxSjxMjxgemYT46YMd2NR0/H+fBhEfWX4A/w9TBJ+ol+okqJKFE6vQ==", - "dev": true + "integrity": "sha512-buY6VNRjhQMiF1qWDouloZlQbRhDPCebwxSjxMjxgemYT46YMd2NR0/H+fBhEfWX4A/w9TBJ+ol+okqJKFE6vQ==" }, "is-svg": { "version": "3.0.0", @@ -20648,9 +20678,9 @@ } }, "react-bootstrap": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/react-bootstrap/-/react-bootstrap-1.4.3.tgz", - "integrity": "sha512-4tYhk26KRnK0myMEp2wvNjOvnHMwWfa6pWFIiCtj9wewYaTxP7TrCf7MwcIMBgUzyX0SJXx6UbbDG0+hObiXNg==", + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/react-bootstrap/-/react-bootstrap-1.5.1.tgz", + "integrity": "sha512-jbJNGx9n4JvKgxlvT8DLKSeF3VcqnPJXS9LFdzoZusiZCCGoYecZ9qSCBH5n2A+kjmuura9JkvxI9l7HD+bIdQ==", "requires": { "@babel/runtime": "^7.4.2", "@restart/context": "^2.1.4", @@ -20666,7 +20696,7 @@ "invariant": "^2.2.4", "prop-types": "^15.7.2", "prop-types-extra": "^1.1.0", - "react-overlays": "^4.1.0", + "react-overlays": "^5.0.0", "react-transition-group": "^4.4.1", "uncontrollable": "^7.0.0", "warning": "^4.0.3" @@ -20681,9 +20711,9 @@ }, "dependencies": { "@babel/runtime": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.12.13.tgz", - "integrity": "sha512-8+3UMPBrjFa/6TtKi/7sehPKqfAm4g6K+YQjyyFOLUTxzOngcRZTlAVY8sc2CORJYqdHQY8gRPHmn+qo15rCBw==", + "version": "7.13.9", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.13.9.tgz", + "integrity": "sha512-aY2kU+xgJ3dJ1eU6FMB9EH8dIe8dmusF1xEku52joLvw6eAFN0AI+WxCLDnpev2LEejWBAy2sBvBOBAjI3zmvA==", "requires": { "regenerator-runtime": "^0.13.4" } @@ -21063,9 +21093,9 @@ "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==" }, "react-overlays": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/react-overlays/-/react-overlays-4.1.1.tgz", - "integrity": "sha512-WtJifh081e6M24KnvTQoNjQEpz7HoLxqt8TwZM7LOYIkYJ8i/Ly1Xi7RVte87ZVnmqQ4PFaFiNHZhSINPSpdBQ==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/react-overlays/-/react-overlays-5.0.0.tgz", + "integrity": "sha512-TKbqfAv23TFtCJ2lzISdx76p97G/DP8Rp4TOFdqM9n8GTruVYgE3jX7Zgb8+w7YJ18slTVcDTQ1/tFzdCqjVhA==", "requires": { "@babel/runtime": "^7.12.1", "@popperjs/core": "^2.5.3", @@ -21078,17 +21108,17 @@ }, "dependencies": { "@babel/runtime": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.12.13.tgz", - "integrity": "sha512-8+3UMPBrjFa/6TtKi/7sehPKqfAm4g6K+YQjyyFOLUTxzOngcRZTlAVY8sc2CORJYqdHQY8gRPHmn+qo15rCBw==", + "version": "7.13.9", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.13.9.tgz", + "integrity": "sha512-aY2kU+xgJ3dJ1eU6FMB9EH8dIe8dmusF1xEku52joLvw6eAFN0AI+WxCLDnpev2LEejWBAy2sBvBOBAjI3zmvA==", "requires": { "regenerator-runtime": "^0.13.4" } }, "csstype": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.6.tgz", - "integrity": "sha512-+ZAmfyWMT7TiIlzdqJgjMb7S4f1beorDbWbsocyK4RaiqA5RTX3K14bnBWmmA9QEM0gRdsjyyrEmcyga8Zsxmw==" + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.7.tgz", + "integrity": "sha512-KxnUB0ZMlnUWCsx2Z8MUsr6qV6ja1w9ArPErJaJaF8a5SOWoHLIszeCTKGRGRgtLgYrs1E8CHkNSP1VZTTPc9g==" }, "dom-helpers": { "version": "5.2.0", @@ -21101,6 +21131,22 @@ } } }, + "react-popper": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/react-popper/-/react-popper-2.2.4.tgz", + "integrity": "sha512-NacOu4zWupdQjVXq02XpTD3yFPSfg5a7fex0wa3uGKVkFK7UN6LvVxgcb+xYr56UCuWiNPMH20tntdVdJRwYew==", + "requires": { + "react-fast-compare": "^3.0.1", + "warning": "^4.0.2" + }, + "dependencies": { + "react-fast-compare": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.0.tgz", + "integrity": "sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA==" + } + } + }, "react-proptype-conditional-require": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/react-proptype-conditional-require/-/react-proptype-conditional-require-1.0.4.tgz", @@ -24227,6 +24273,17 @@ "integrity": "sha512-6OSu9PTIzmn9TCDiovULTnET6BgXtDYL4Gg4szY+cGsc3JP1dQL8qvE8kShTRx1NIw4Q9IBHlwODjkjWEtMUyA==", "dev": true }, + "unbox-primitive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.0.tgz", + "integrity": "sha512-P/51NX+JXyxK/aigg1/ZgyccdAxm5K1+n8+tvqSntjOivPt19gvm1VC49RWYetsiub8WViUchdxl/KWHHB0kzA==", + "requires": { + "function-bind": "^1.1.1", + "has-bigints": "^1.0.0", + "has-symbols": "^1.0.0", + "which-boxed-primitive": "^1.0.1" + } + }, "unbzip2-stream": { "version": "1.4.3", "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz", @@ -24239,30 +24296,14 @@ } }, "uncontrollable": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/uncontrollable/-/uncontrollable-7.1.1.tgz", - "integrity": "sha512-EcPYhot3uWTS3w00R32R2+vS8Vr53tttrvMj/yA1uYRhf8hbTG2GyugGqWDY0qIskxn0uTTojVd6wPYW9ZEf8Q==", + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/uncontrollable/-/uncontrollable-7.2.1.tgz", + "integrity": "sha512-svtcfoTADIB0nT9nltgjujTi7BzVmwjZClOmskKu/E8FW9BXzg9os8OLr4f8Dlnk0rYWJIWr4wv9eKUXiQvQwQ==", "requires": { "@babel/runtime": "^7.6.3", - "@types/react": "^16.9.11", + "@types/react": ">=16.9.11", "invariant": "^2.2.4", "react-lifecycles-compat": "^3.0.4" - }, - "dependencies": { - "@types/react": { - "version": "16.14.4", - "resolved": "https://registry.npmjs.org/@types/react/-/react-16.14.4.tgz", - "integrity": "sha512-ETj7GbkPGjca/A4trkVeGvoIakmLV6ZtX3J8dcmOpzKzWVybbrOxanwaIPG71GZwImoMDY6Fq4wIe34lEqZ0FQ==", - "requires": { - "@types/prop-types": "*", - "csstype": "^3.0.2" - } - }, - "csstype": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.6.tgz", - "integrity": "sha512-+ZAmfyWMT7TiIlzdqJgjMb7S4f1beorDbWbsocyK4RaiqA5RTX3K14bnBWmmA9QEM0gRdsjyyrEmcyga8Zsxmw==" - } } }, "unicode-canonical-property-names-ecmascript": { @@ -25566,6 +25607,18 @@ "isexe": "^2.0.0" } }, + "which-boxed-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", + "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", + "requires": { + "is-bigint": "^1.0.1", + "is-boolean-object": "^1.1.0", + "is-number-object": "^1.0.4", + "is-string": "^1.0.5", + "is-symbol": "^1.0.3" + } + }, "which-module": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", diff --git a/package.json b/package.json index af9bbd4b3..7662e5eae 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,7 @@ "@edx/brand": "npm:@edx/brand-openedx@1.1.0", "@edx/frontend-component-footer": "10.0.11", "@edx/frontend-platform": "1.8.0", - "@edx/paragon": "13.10.0", + "@edx/paragon": "13.13.5", "@fortawesome/fontawesome-svg-core": "1.2.28", "@fortawesome/free-brands-svg-icons": "5.11.2", "@fortawesome/free-regular-svg-icons": "5.11.2", diff --git a/src/CourseAuthoringRoutes.jsx b/src/CourseAuthoringRoutes.jsx index d9b94c906..4e3d75ec1 100644 --- a/src/CourseAuthoringRoutes.jsx +++ b/src/CourseAuthoringRoutes.jsx @@ -6,7 +6,6 @@ import { PageRoute } from '@edx/frontend-platform/react'; import CourseAuthoringPage from './CourseAuthoringPage'; import { PagesAndResources } from './pages-and-resources'; import ProctoredExamSettings from './proctored-exam-settings/ProctoredExamSettings'; -import DiscussionsRoutes from './pages-and-resources/discussions/DiscussionsRoutes'; /** * As of this writing, these routes are mounted at a path prefixed with the following: @@ -28,12 +27,9 @@ export default function CourseAuthoringRoutes({ courseId }) { return ( - + - - - diff --git a/src/generic/full-screen-modal/FullScreenModal.jsx b/src/generic/full-screen-modal/FullScreenModal.jsx new file mode 100644 index 000000000..33c2a7e7d --- /dev/null +++ b/src/generic/full-screen-modal/FullScreenModal.jsx @@ -0,0 +1,30 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { ModalLayer } from '@edx/paragon'; +import classNames from 'classnames'; + +export default function FullScreenModal({ children, title, onClose }) { + return ( + +
+ {children} +
+
+ ); +} + +FullScreenModal.propTypes = { + children: PropTypes.node.isRequired, + title: PropTypes.string.isRequired, + onClose: PropTypes.func.isRequired, +}; diff --git a/src/generic/full-screen-modal/FullScreenModalBody.jsx b/src/generic/full-screen-modal/FullScreenModalBody.jsx new file mode 100644 index 000000000..43ae47876 --- /dev/null +++ b/src/generic/full-screen-modal/FullScreenModalBody.jsx @@ -0,0 +1,14 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +export default function FullScreenModalBody({ children }) { + return ( +
+ {children} +
+ ); +} + +FullScreenModalBody.propTypes = { + children: PropTypes.node.isRequired, +}; diff --git a/src/generic/full-screen-modal/FullScreenModalHeader.jsx b/src/generic/full-screen-modal/FullScreenModalHeader.jsx new file mode 100644 index 000000000..3fe4f4a52 --- /dev/null +++ b/src/generic/full-screen-modal/FullScreenModalHeader.jsx @@ -0,0 +1,34 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { ModalCloseButton } from '@edx/paragon'; +import { Close } from '@edx/paragon/icons'; +import classNames from 'classnames'; + +export default function FullScreenModalHeader({ className, title }) { + return ( +
+

{title}

+ + + +
+ ); +} + +FullScreenModalHeader.propTypes = { + className: PropTypes.string, + title: PropTypes.string.isRequired, +}; + +FullScreenModalHeader.defaultProps = { + className: null, +}; diff --git a/src/generic/stepper/StepIcon.jsx b/src/generic/stepper/StepIcon.jsx new file mode 100644 index 000000000..03c9f0b48 --- /dev/null +++ b/src/generic/stepper/StepIcon.jsx @@ -0,0 +1,30 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +export default function StepIcon({ children, size }) { + return ( +
+ {children} +
+ ); +} + +StepIcon.propTypes = { + children: PropTypes.node.isRequired, + size: PropTypes.string, +}; + +StepIcon.defaultProps = { + size: '1.3rem', +}; diff --git a/src/generic/stepper/StepLine.jsx b/src/generic/stepper/StepLine.jsx new file mode 100644 index 000000000..5c084a7af --- /dev/null +++ b/src/generic/stepper/StepLine.jsx @@ -0,0 +1,7 @@ +import React from 'react'; + +export default function StepLine() { + return ( +
+ ); +} diff --git a/src/generic/stepper/Stepper.jsx b/src/generic/stepper/Stepper.jsx new file mode 100644 index 000000000..96cecd8e5 --- /dev/null +++ b/src/generic/stepper/Stepper.jsx @@ -0,0 +1,26 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import classNames from 'classnames'; + +export default function Stepper({ children, className }) { + return ( +
+ {children} +
+ ); +} + +Stepper.propTypes = { + className: PropTypes.string, + children: PropTypes.node.isRequired, +}; + +Stepper.defaultProps = { + className: null, +}; diff --git a/src/generic/stepper/StepperBody.jsx b/src/generic/stepper/StepperBody.jsx new file mode 100644 index 000000000..79db027a4 --- /dev/null +++ b/src/generic/stepper/StepperBody.jsx @@ -0,0 +1,25 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import classNames from 'classnames'; + +export default function StepperBody({ children, className }) { + return ( +
+ {children} +
+ ); +} + +StepperBody.propTypes = { + children: PropTypes.node.isRequired, + className: PropTypes.string, +}; + +StepperBody.defaultProps = { + className: null, +}; diff --git a/src/generic/stepper/StepperFooter.jsx b/src/generic/stepper/StepperFooter.jsx new file mode 100644 index 000000000..ccb5bffb9 --- /dev/null +++ b/src/generic/stepper/StepperFooter.jsx @@ -0,0 +1,33 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import classNames from 'classnames'; + +export default function StepperFooter({ children, className }) { + return ( +
+ {children} +
+ ); +} + +StepperFooter.propTypes = { + children: PropTypes.node.isRequired, + className: PropTypes.string, +}; + +StepperFooter.defaultProps = { + className: null, +}; diff --git a/src/generic/stepper/StepperHeader.jsx b/src/generic/stepper/StepperHeader.jsx new file mode 100644 index 000000000..2f816ab71 --- /dev/null +++ b/src/generic/stepper/StepperHeader.jsx @@ -0,0 +1,54 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import classNames from 'classnames'; +import StepIcon from './StepIcon'; +import StepLine from './StepLine'; + +export default function StepperHeader({ steps, className }) { + return ( +
+ {steps.map(({ iconLabel, label }, index) => { + const isNotLastStep = index < steps.length - 1; + return ( + // eslint-disable-next-line react/no-array-index-key + + + {iconLabel || index + 1} + + {label} + {isNotLastStep && ( + + )} + + ); + })} +
+ ); +} + +StepperHeader.propTypes = { + steps: PropTypes.arrayOf( + PropTypes.shape({ + iconLabel: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.node, + ]), + label: PropTypes.string.isRequired, + }), + ).isRequired, + className: PropTypes.string, +}; + +StepperHeader.defaultProps = { + className: null, +}; diff --git a/src/pages-and-resources/PagesAndResources.jsx b/src/pages-and-resources/PagesAndResources.jsx index d53925a91..9714a2b42 100644 --- a/src/pages-and-resources/PagesAndResources.jsx +++ b/src/pages-and-resources/PagesAndResources.jsx @@ -1,73 +1,33 @@ -import React, { useContext } from 'react'; +import React, { useContext, useEffect } from 'react'; import PropTypes from 'prop-types'; import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; -import { AppContext } from '@edx/frontend-platform/react'; +import { AppContext, PageRoute } from '@edx/frontend-platform/react'; + +import { Switch, useRouteMatch } from 'react-router'; +import { useDispatch, useSelector } from 'react-redux'; import messages from './messages'; +import Discussions from './discussions'; + import PageGrid from './pages/PageGrid'; import ResourceList from './resources/ResourcesList'; -// XXX this is just for testing and should be removed ASAP -const pages = [ - { - id: 'discussion', - title: 'Discussion', - isEnabled: false, - showSettings: false, - showStatus: false, - showEnable: true, - description: 'Encourage participation and engagement in your course with discussion forums', - }, - { - id: 'teams', - title: 'Teams', - isEnabled: true, - showSettings: true, - showStatus: true, - showEnable: false, - description: 'Leverage teams to allow learners to connect by topic of interest', - }, - { - id: 'progress', - title: 'Progress', - isEnabled: false, - showSettings: true, - showStatus: true, - showEnable: false, - description: 'Allow students to track their progress throughout the course lorem ipsum', - }, - { - id: 'textbooks', - title: 'Textbooks', - isEnabled: true, - showSettings: true, - showStatus: true, - showEnable: false, - description: 'Provide links to applicable resources for your course', - }, - { - id: 'notes', - title: 'Notes', - isEnabled: true, - showSettings: true, - showStatus: true, - showEnable: false, - description: 'Support individual note taking that is visible only to the students', - }, - { - id: 'wiki', - title: 'Wiki', - isEnabled: false, - showSettings: false, - showStatus: false, - showEnable: true, - description: 'Share your wiki content to provide additional course material', - }, -]; +import { fetchPages } from './data/thunks'; +import { useModels } from '../generic/model-store'; function PagesAndResources({ courseId, intl }) { + const { path } = useRouteMatch(); const { config } = useContext(AppContext); const lmsCourseURL = `${config.LMS_BASE_URL}/courses/${courseId}`; + + const dispatch = useDispatch(); + useEffect(() => { + dispatch(fetchPages(courseId)); + }, [courseId]); + + const pageIds = useSelector(state => state.pagesAndResources.pageIds); + const pages = useModels('pages', pageIds); + return (
@@ -80,6 +40,11 @@ function PagesAndResources({ courseId, intl }) {
+ + + + +
); } diff --git a/src/pages-and-resources/data/api.js b/src/pages-and-resources/data/api.js new file mode 100644 index 000000000..75d725b94 --- /dev/null +++ b/src/pages-and-resources/data/api.js @@ -0,0 +1,61 @@ +/* eslint-disable import/prefer-default-export */ +export function getPages() { + return Promise.resolve({ + pages: [ + { + id: 'discussions', + title: 'Discussions', + isEnabled: false, + showSettings: false, + showStatus: false, + showEnable: true, + description: 'Encourage participation and engagement in your course with discussion forums', + }, + { + id: 'teams', + title: 'Teams', + isEnabled: true, + showSettings: true, + showStatus: true, + showEnable: false, + description: 'Leverage teams to allow learners to connect by topic of interest', + }, + { + id: 'progress', + title: 'Progress', + isEnabled: false, + showSettings: true, + showStatus: true, + showEnable: false, + description: 'Allow students to track their progress throughout the course lorem ipsum', + }, + { + id: 'textbooks', + title: 'Textbooks', + isEnabled: true, + showSettings: true, + showStatus: true, + showEnable: false, + description: 'Provide links to applicable resources for your course', + }, + { + id: 'notes', + title: 'Notes', + isEnabled: true, + showSettings: true, + showStatus: true, + showEnable: false, + description: 'Support individual note taking that is visible only to the students', + }, + { + id: 'wiki', + title: 'Wiki', + isEnabled: false, + showSettings: false, + showStatus: false, + showEnable: true, + description: 'Share your wiki content to provide additional course material', + }, + ], + }); +} diff --git a/src/pages-and-resources/data/slice.js b/src/pages-and-resources/data/slice.js new file mode 100644 index 000000000..a2c8d2de5 --- /dev/null +++ b/src/pages-and-resources/data/slice.js @@ -0,0 +1,31 @@ +/* eslint-disable no-param-reassign */ +import { createSlice } from '@reduxjs/toolkit'; + +export const LOADING = 'LOADING'; +export const LOADED = 'LOADED'; +export const FAILED = 'FAILED'; + +const slice = createSlice({ + name: 'pagesAndResources', + initialState: { + pageIds: [], + status: LOADING, + }, + reducers: { + fetchPagesSuccess: (state, { payload }) => { + state.pageIds = payload.pageIds; + }, + updateStatus: (state, { payload }) => { + state.status = payload.status; + }, + }, +}); + +export const { + fetchPagesSuccess, + updateStatus, +} = slice.actions; + +export const { + reducer, +} = slice; diff --git a/src/pages-and-resources/data/thunks.js b/src/pages-and-resources/data/thunks.js new file mode 100644 index 000000000..046df678f --- /dev/null +++ b/src/pages-and-resources/data/thunks.js @@ -0,0 +1,33 @@ +import { + getPages, +} from './api'; +import { addModels } from '../../generic/model-store'; +import { + FAILED, + fetchPagesSuccess, + LOADING, + updateStatus, + LOADED, +} from './slice'; + +/* eslint-disable import/prefer-default-export */ +export function fetchPages(courseId) { + return async (dispatch) => { + dispatch(updateStatus({ courseId, status: LOADING })); + + try { + const { pages } = await getPages(courseId); + + dispatch(addModels({ modelType: 'pages', models: pages })); + dispatch(fetchPagesSuccess({ + pageIds: pages.map(page => page.id), + })); + dispatch(updateStatus({ courseId, status: LOADED })); + } catch (error) { + // TODO: We need generic error handling in the app for when a request just fails... in other + // parts of the app (proctored exam settings) we show a nice message and ask the user to + // reload/try again later. + dispatch(updateStatus({ courseId, status: FAILED })); + } + }; +} diff --git a/src/pages-and-resources/discussions/DiscussionAppCard.jsx b/src/pages-and-resources/discussions/AppCard.jsx similarity index 85% rename from src/pages-and-resources/discussions/DiscussionAppCard.jsx rename to src/pages-and-resources/discussions/AppCard.jsx index a40dd62b6..c272aa833 100644 --- a/src/pages-and-resources/discussions/DiscussionAppCard.jsx +++ b/src/pages-and-resources/discussions/AppCard.jsx @@ -8,15 +8,15 @@ import { faLock } from '@fortawesome/free-solid-svg-icons'; import messages from './messages'; -function DiscussionAppCard({ - app, clickHandler, intl, selected, +function AppCard({ + app, onClick, intl, selected, }) { return ( { if (app.isAvailable) { clickHandler(app.id); } }} - onKeyPress={() => { if (app.isAvailable) { clickHandler(app.id); } }} + onClick={() => { if (app.isAvailable) { onClick(app.id); } }} + onKeyPress={() => { if (app.isAvailable) { onClick(app.id); } }} role="radio" aria-checked={selected} style={{ @@ -63,7 +63,7 @@ function DiscussionAppCard({ ); } -DiscussionAppCard.propTypes = { +AppCard.propTypes = { app: PropTypes.shape({ description: PropTypes.string.isRequired, id: PropTypes.string.isRequired, @@ -72,9 +72,9 @@ DiscussionAppCard.propTypes = { name: PropTypes.string.isRequired, supportLevel: PropTypes.string.isRequired, }).isRequired, - clickHandler: PropTypes.func.isRequired, + onClick: PropTypes.func.isRequired, selected: PropTypes.bool.isRequired, intl: intlShape.isRequired, }; -export default injectIntl(DiscussionAppCard); +export default injectIntl(AppCard); diff --git a/src/pages-and-resources/discussions/AppList.jsx b/src/pages-and-resources/discussions/AppList.jsx new file mode 100644 index 000000000..472246827 --- /dev/null +++ b/src/pages-and-resources/discussions/AppList.jsx @@ -0,0 +1,63 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; +import { CardGrid } from '@edx/paragon'; +import { useSelector } from 'react-redux'; + +import { useModels } from '../../generic/model-store'; + +import AppCard from './AppCard'; +import messages from './messages'; +import FeaturesTable from './FeaturesTable'; + +function AppList({ + intl, onSelectApp, selectedAppId, +}) { + const appIds = useSelector(state => state.discussions.appIds); + const featureIds = useSelector(state => state.discussions.featureIds); + const apps = useModels('apps', appIds); + const features = useModels('features', featureIds); + + return ( +
+

{intl.formatMessage(messages.heading)}

+ + {apps.map(app => ( + + ))} + + +

+ {intl.formatMessage(messages.supportedFeatures)} +

+ + +
+ ); +} + +AppList.propTypes = { + intl: intlShape.isRequired, + onSelectApp: PropTypes.func.isRequired, + selectedAppId: PropTypes.string, +}; + +AppList.defaultProps = { + selectedAppId: null, +}; + +export default injectIntl(AppList); diff --git a/src/pages-and-resources/discussions/DiscussionAppList.scss b/src/pages-and-resources/discussions/AppList.scss similarity index 100% rename from src/pages-and-resources/discussions/DiscussionAppList.scss rename to src/pages-and-resources/discussions/AppList.scss diff --git a/src/pages-and-resources/discussions/DiscussionAppList.test.jsx b/src/pages-and-resources/discussions/AppList.test.jsx similarity index 62% rename from src/pages-and-resources/discussions/DiscussionAppList.test.jsx rename to src/pages-and-resources/discussions/AppList.test.jsx index f8961aa71..ed9dc000a 100644 --- a/src/pages-and-resources/discussions/DiscussionAppList.test.jsx +++ b/src/pages-and-resources/discussions/AppList.test.jsx @@ -1,4 +1,4 @@ -describe('DiscussionAppList', () => { +describe('AppList', () => { it('will pass because it is an example', () => { }); diff --git a/src/pages-and-resources/discussions/ConfigFormContainer.jsx b/src/pages-and-resources/discussions/ConfigFormContainer.jsx new file mode 100644 index 000000000..628325557 --- /dev/null +++ b/src/pages-and-resources/discussions/ConfigFormContainer.jsx @@ -0,0 +1,46 @@ +import React, { useEffect } from 'react'; +import PropTypes from 'prop-types'; +import { useDispatch, useSelector } from 'react-redux'; + +import { useRouteMatch } from 'react-router'; +import { useModel } from '../../generic/model-store'; + +import LtiConfigForm from './LtiConfigForm'; +import { fetchAppConfig } from './data/thunks'; + +// eslint-disable-next-line no-unused-vars +export default function ConfigFormContainer({ + courseId, onSubmit, formRef, +}) { + const { params: { appId: routeAppId } } = useRouteMatch(); + + const dispatch = useDispatch(); + useEffect(() => { + dispatch(fetchAppConfig(courseId, routeAppId)); + }, [courseId]); + + const { activeAppId, activeAppConfigId } = useSelector(state => state.discussions); + + const app = useModel('apps', activeAppId); + const appConfig = useModel('appConfigs', activeAppConfigId); + + if (!appConfig || !app) { + return null; + } + + return ( + + ); +} + +ConfigFormContainer.propTypes = { + courseId: PropTypes.string.isRequired, + onSubmit: PropTypes.func.isRequired, + // eslint-disable-next-line react/forbid-prop-types + formRef: PropTypes.object.isRequired, +}; diff --git a/src/pages-and-resources/discussions/DiscussionAppList.jsx b/src/pages-and-resources/discussions/DiscussionAppList.jsx deleted file mode 100644 index 3c2c8999b..000000000 --- a/src/pages-and-resources/discussions/DiscussionAppList.jsx +++ /dev/null @@ -1,90 +0,0 @@ -import React, { useCallback, useEffect, useState } from 'react'; -import PropTypes from 'prop-types'; -import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; -import { - Button, CardGrid, -} from '@edx/paragon'; -import { useDispatch, useSelector } from 'react-redux'; -import { history } from '@edx/frontend-platform'; -import { useLocation } from 'react-router'; - -import { useModel, useModels } from '../../generic/model-store'; - -import messages from './messages'; -import { fetchApps } from './data/thunks'; -import DiscussionAppCard from './DiscussionAppCard'; -import FeaturesTable from './FeaturesTable'; - -function DiscussionAppList({ courseId, intl }) { - const [selectedAppId, setSelectedAppId] = useState(null); - const { pathname } = useLocation(); - - const dispatch = useDispatch(); - useEffect(() => { - dispatch(fetchApps(courseId)); - }, [courseId]); - - const appIds = useSelector(state => state.discussions.appIds); - const featureIds = useSelector(state => state.discussions.featureIds); - const apps = useModels('apps', appIds); - const features = useModels('features', featureIds); - - const selectedApp = useModel('apps', selectedAppId); - - const handleSelectApp = useCallback((appId) => { - if (selectedAppId === appId) { - setSelectedAppId(null); - } else { - setSelectedAppId(appId); - } - }, [selectedAppId]); - - const handleConfigureApp = () => { - history.push(`${pathname}/configure/${selectedAppId}`); - }; - - return ( -
-

{intl.formatMessage(messages.heading)}

- - {apps.map(app => ( - - ))} - - -
-

- {intl.formatMessage(messages.supportedFeatures)} -

- {selectedAppId && ( - - )} -
- - -
- ); -} - -DiscussionAppList.propTypes = { - courseId: PropTypes.string.isRequired, - intl: intlShape.isRequired, -}; - -export default injectIntl(DiscussionAppList); diff --git a/src/pages-and-resources/discussions/DiscussionConfig.jsx b/src/pages-and-resources/discussions/DiscussionConfig.jsx deleted file mode 100644 index 22bedfa2b..000000000 --- a/src/pages-and-resources/discussions/DiscussionConfig.jsx +++ /dev/null @@ -1,46 +0,0 @@ -import React, { useCallback, useEffect } from 'react'; -import PropTypes from 'prop-types'; -import { useRouteMatch } from 'react-router'; -import { useDispatch, useSelector } from 'react-redux'; - -import { history } from '@edx/frontend-platform'; -import { useModel } from '../../generic/model-store'; -import { fetchAppConfig, saveAppConfig } from './data/thunks'; -import DiscussionsConfigForm from './DiscussionsConfigForm'; - -export default function DiscussionConfig({ courseId }) { - const { params: { appId } } = useRouteMatch(); - - const dispatch = useDispatch(); - useEffect(() => { - dispatch(fetchAppConfig(courseId, appId)); - }, [courseId]); - - const { activeAppId, activeAppConfigId } = useSelector(state => state.discussions); - - const app = useModel('apps', activeAppId); - const appConfig = useModel('appConfigs', activeAppConfigId); - - const handleSubmit = useCallback((values) => { - dispatch(saveAppConfig(courseId, appId, values)).then(() => { - history.push(`/course/${courseId}/pages-and-resources`); - }); - }, []); - - if (!appConfig || !app) { - return null; - } - - return ( - - ); -} - -DiscussionConfig.propTypes = { - courseId: PropTypes.string.isRequired, -}; diff --git a/src/pages-and-resources/discussions/Discussions.jsx b/src/pages-and-resources/discussions/Discussions.jsx new file mode 100644 index 000000000..471d62492 --- /dev/null +++ b/src/pages-and-resources/discussions/Discussions.jsx @@ -0,0 +1,150 @@ +import React, { + useCallback, useEffect, useRef, useState, +} from 'react'; +import PropTypes from 'prop-types'; +import { useDispatch } from 'react-redux'; +import { + Switch, + useLocation, + useRouteMatch, +} from 'react-router'; +import { history } from '@edx/frontend-platform'; +import { PageRoute } from '@edx/frontend-platform/react'; +import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; +import { Button, StatefulButton } from '@edx/paragon'; +import { Check } from '@edx/paragon/icons'; + +import FullScreenModal from '../../generic/full-screen-modal/FullScreenModal'; +import Stepper from '../../generic/stepper/Stepper'; + +import messages from './messages'; +import AppList from './AppList'; +import ConfigFormContainer from './ConfigFormContainer'; +import { fetchApps, saveAppConfig } from './data/thunks'; +import StepperFooter from '../../generic/stepper/StepperFooter'; +import StepperHeader from '../../generic/stepper/StepperHeader'; +import StepperBody from '../../generic/stepper/StepperBody'; +import FullScreenModalHeader from '../../generic/full-screen-modal/FullScreenModalHeader'; +import FullScreenModalBody from '../../generic/full-screen-modal/FullScreenModalBody'; + +function Discussions({ courseId, intl }) { + const discussionsPath = `/course/${courseId}/pages-and-resources/discussions`; + + const [selectedAppId, setSelectedAppId] = useState(null); + const [isSubmitting, setIsSubmitting] = useState(false); + const formRef = useRef(); + + const { path } = useRouteMatch(); + const { pathname } = useLocation(); + const dispatch = useDispatch(); + + useEffect(() => { + dispatch(fetchApps(courseId)); + }, [courseId]); + + const onSelectApp = useCallback((appId) => { + if (selectedAppId === appId) { + setSelectedAppId(null); + } else { + setSelectedAppId(appId); + } + }, [selectedAppId]); + + const onClose = useCallback(() => { + history.push(`/course/${courseId}/pages-and-resources`); + }, [courseId]); + + const onStartConfig = useCallback(() => { + history.push(`${discussionsPath}/configure/${selectedAppId}`); + }, [discussionsPath, selectedAppId]); + + // This causes the form to be submitted from a button outside the form. + const onApply = () => { + setIsSubmitting(true); + formRef.current.requestSubmit(); + }; + + // This is a callback that gets called after the form has been submitted successfully. + const onSubmit = useCallback((values) => { + dispatch(saveAppConfig(courseId, selectedAppId, values)).then(() => { + history.push(`/course/${courseId}/pages-and-resources`); + }); + }, [courseId, selectedAppId, courseId]); + + const onBack = useCallback(() => { + history.push(discussionsPath); + setSelectedAppId(null); + }, [discussionsPath]); + + const isFirstStep = pathname === discussionsPath; + + const steps = [{ + label: 'Select discussion tool', + iconLabel: isFirstStep ? undefined : ( + + ), + }, + { + label: 'Configure discussions', + }]; + + const submitButtonState = isSubmitting ? 'pending' : 'default'; + + return ( + + + + + + + + + + + + + + + + + + {isFirstStep && ( + + )} + {!isFirstStep && ( + + )} + + + + + ); +} + +Discussions.propTypes = { + courseId: PropTypes.string.isRequired, + intl: intlShape.isRequired, +}; + +export default injectIntl(Discussions); diff --git a/src/pages-and-resources/discussions/DiscussionsRoutes.jsx b/src/pages-and-resources/discussions/DiscussionsRoutes.jsx deleted file mode 100644 index 23c700c07..000000000 --- a/src/pages-and-resources/discussions/DiscussionsRoutes.jsx +++ /dev/null @@ -1,40 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { Switch, useRouteMatch } from 'react-router'; -import { PageRoute } from '@edx/frontend-platform/react'; - -import DiscussionAppList from './DiscussionAppList'; -import DiscussionConfig from './DiscussionConfig'; - -/** - * As of this writing, these routes are mounted at a path prefixed with the following: - * - * /course/:courseId - * - * Meaning that their absolute paths look like: - * - * /course/:courseId/course-pages - * /course/:courseId/proctored-exam-settings - * - * This component and CourseAuthoringPage should maybe be combined once we no longer need to have - * CourseAuthoringPage split out for use in LegacyProctoringRoute. Once that route is removed, we - * can move the Header/Footer rendering to this component and likely pull the course detail loading - * in as well, and it'd feel a bit better-factored and the roles would feel more clear. - */ -export default function DiscussionsRoutes({ courseId }) { - const { path } = useRouteMatch(); - return ( - - - - - - - - - ); -} - -DiscussionsRoutes.propTypes = { - courseId: PropTypes.string.isRequired, -}; diff --git a/src/pages-and-resources/discussions/DiscussionsConfigForm.jsx b/src/pages-and-resources/discussions/LtiConfigForm.jsx similarity index 71% rename from src/pages-and-resources/discussions/DiscussionsConfigForm.jsx rename to src/pages-and-resources/discussions/LtiConfigForm.jsx index 6931de6f3..bc7eec278 100644 --- a/src/pages-and-resources/discussions/DiscussionsConfigForm.jsx +++ b/src/pages-and-resources/discussions/LtiConfigForm.jsx @@ -2,16 +2,15 @@ import React from 'react'; import PropTypes from 'prop-types'; import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n'; import { - StatefulButton, Form, Button, Hyperlink, + Form, Hyperlink, } from '@edx/paragon'; import { useFormik } from 'formik'; import * as Yup from 'yup'; -import { history } from '@edx/frontend-platform'; import messages from './messages'; -function DiscussionConfigForm({ - courseId, appConfig, app, submitHandler, intl, +function LtiConfigForm({ + appConfig, app, onSubmit, intl, formRef, }) { const { handleSubmit, @@ -19,7 +18,6 @@ function DiscussionConfigForm({ handleBlur, values, errors, - isSubmitting, } = useFormik({ initialValues: appConfig, validationSchema: Yup.object().shape({ @@ -27,23 +25,25 @@ function DiscussionConfigForm({ consumerSecret: Yup.string().required(intl.formatMessage(messages.consumerSecretRequired)), launchUrl: Yup.string().required(intl.formatMessage(messages.launchUrlRequired)), }), - onSubmit: submitHandler, + onSubmit, }); - const submitButtonState = isSubmitting ? 'pending' : 'default'; - return ( -
+

{intl.formatMessage(messages.configureApp, { name: app.name })}

- {intl.formatMessage(messages.documentationPage)} + + {intl.formatMessage(messages.documentationPage, { name: app.name })} ), name: app.name, @@ -89,22 +89,11 @@ function DiscussionConfigForm({ {intl.formatMessage(messages.launchUrlRequired)} - - ); } -DiscussionConfigForm.propTypes = { +LtiConfigForm.propTypes = { app: PropTypes.shape({ id: PropTypes.string.isRequired, name: PropTypes.string.isRequired, @@ -115,9 +104,10 @@ DiscussionConfigForm.propTypes = { consumerSecret: PropTypes.string.isRequired, launchUrl: PropTypes.string.isRequired, }).isRequired, - courseId: PropTypes.string.isRequired, intl: intlShape.isRequired, - submitHandler: PropTypes.func.isRequired, + onSubmit: PropTypes.func.isRequired, + // eslint-disable-next-line react/forbid-prop-types + formRef: PropTypes.object.isRequired, }; -export default injectIntl(DiscussionConfigForm); +export default injectIntl(LtiConfigForm); diff --git a/src/pages-and-resources/discussions/index.js b/src/pages-and-resources/discussions/index.js new file mode 100644 index 000000000..ba2706fef --- /dev/null +++ b/src/pages-and-resources/discussions/index.js @@ -0,0 +1 @@ +export { default } from './Discussions'; diff --git a/src/pages-and-resources/discussions/messages.js b/src/pages-and-resources/discussions/messages.js index 7cb7058f2..e01e66e7b 100644 --- a/src/pages-and-resources/discussions/messages.js +++ b/src/pages-and-resources/discussions/messages.js @@ -10,9 +10,13 @@ const messages = defineMessages({ defaultMessage: 'Supported Features', }, configureApp: { - id: 'authoring.discussions.configureApp', + id: 'authoring.discussions.configure.app', defaultMessage: 'Configure {name}', }, + configure: { + id: 'authoring.discussions.configure', + defaultMessage: 'Configure Discussions', + }, appLogo: { id: 'authoring.discussions.appLogo', defaultMessage: '{name} Logo', @@ -38,7 +42,7 @@ const messages = defineMessages({ }, documentationPage: { id: 'authoring.discussions.documentationPage', - defaultMessage: 'documentation page', + defaultMessage: 'Visit the {name} documentation page', }, consumerKey: { id: 'authoring.discussions.consumerKey', @@ -73,7 +77,17 @@ const messages = defineMessages({ backButton: { id: 'authoring.discussions.backButton', defaultMessage: 'Back', - description: 'Back button allowing the user to return to discussion app selection.', + description: 'Button allowing the user to return to discussion app selection.', + }, + nextButton: { + id: 'authoring.discussions.nexyButton', + defaultMessage: 'Next', + description: 'Button allowing the user to advance to the second step of discussion configuration.', + }, + applyButton: { + id: 'authoring.discussions.applyButton', + defaultMessage: 'Apply', + description: 'Button allowing the user to submit their discussion configuration.', }, }); diff --git a/src/pages-and-resources/index.scss b/src/pages-and-resources/index.scss index 483b4c2e0..a9680e4cc 100644 --- a/src/pages-and-resources/index.scss +++ b/src/pages-and-resources/index.scss @@ -1 +1 @@ -@import "./discussions/DiscussionAppList"; +@import "./discussions/AppList"; diff --git a/src/store.js b/src/store.js index c489d5643..971d5ca9c 100644 --- a/src/store.js +++ b/src/store.js @@ -3,12 +3,14 @@ import { configureStore } from '@reduxjs/toolkit'; import { reducer as modelsReducer } from './generic/model-store'; import { reducer as courseDetailReducer } from './data/slice'; import { reducer as discussionsReducer } from './pages-and-resources/discussions/data/slice'; +import { reducer as pagesAndResourcesReducer } from './pages-and-resources/data/slice'; export default function initializeStore() { return configureStore({ reducer: { courseDetail: courseDetailReducer, discussions: discussionsReducer, + pagesAndResources: pagesAndResourcesReducer, models: modelsReducer, }, });