Discussions config UI part 2 - FullScreenModal and Stepper! (#53)
* Bumping paragon version to latest. * Modifying event handlers to be named with “on” prefix instead of “Handler” suffix * Tweaking a message id. * Removing “Discussion” prefix from discussions components. Seems unnecessary. * Backing pages & resources view with data handling. It still has the list of pages hard coded. * Adding FullScreenModal and Stepper components. These components are pretty close to their final form. They could benefit from some snapshot tests and such; there isn’t much actual functionality in ‘em. Stepper will get a bit more functionality when we add the dynamic drop shadow behavior. Depending on whether the stepper body is at the top or bottom, drop shadows on the header and footer should appear or disappear to indicate more content exists above or below the viewport. * Moving discussions routes inside PagesAndResources Note that the discussions component has been renamed - that’ll be coming in a subsequent commit. Also trying to get consistent about calling it “discussions” * AppList gets less responsibility The AppList is now a child of the top-level “Discussions” component, so it’s no longer responsible for loading the app list or storing the state for the selected app ID. It’s also given a handler for when an app is selected, and no longer has a button to configure the app. * Fleshing out Discussions component The top-level Discussions component (renamed from DiscussionsRoutes) is now responsible for a lot. - it loads the app list - it keeps track of selected app ID - it has handlers for all the various user actions so they can be coordinated here at the top. - it uses component composition to create the majority of the UI, folding together FullScreenModal and Stepper with its route-based views. * Decomposing the app config form The discussion app config form has been decomposed into a container responsible for loading app data, and a component specifically for the LTI configuration form. In the future, ConfigFormContainer will get a second possible child for the edX Forums app, and will switch between the two forms based on the app being configured. Note that I expect that some of the data loading logic from ConfigFormContainer may be better situated in the Discussions component… everything else is happening there, and it may make sense for it to handle loading the app config data as necessary as well.
This commit is contained in:
239
package-lock.json
generated
239
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 (
|
||||
<CourseAuthoringPage courseId={courseId}>
|
||||
<Switch>
|
||||
<PageRoute exact path={`${path}/pages-and-resources`}>
|
||||
<PageRoute path={`${path}/pages-and-resources`}>
|
||||
<PagesAndResources courseId={courseId} />
|
||||
</PageRoute>
|
||||
<PageRoute path={`${path}/pages-and-resources/discussion`}>
|
||||
<DiscussionsRoutes courseId={courseId} />
|
||||
</PageRoute>
|
||||
<PageRoute path={`${path}/proctored-exam-settings`}>
|
||||
<ProctoredExamSettings courseId={courseId} />
|
||||
</PageRoute>
|
||||
|
||||
30
src/generic/full-screen-modal/FullScreenModal.jsx
Normal file
30
src/generic/full-screen-modal/FullScreenModal.jsx
Normal file
@@ -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 (
|
||||
<ModalLayer isOpen onClose={onClose}>
|
||||
<div
|
||||
role="dialog"
|
||||
aria-label={title}
|
||||
className={classNames(
|
||||
'bg-white',
|
||||
'd-flex',
|
||||
'flex-column',
|
||||
'vw-100',
|
||||
'vh-100',
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</ModalLayer>
|
||||
);
|
||||
}
|
||||
|
||||
FullScreenModal.propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
title: PropTypes.string.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
};
|
||||
14
src/generic/full-screen-modal/FullScreenModalBody.jsx
Normal file
14
src/generic/full-screen-modal/FullScreenModalBody.jsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
export default function FullScreenModalBody({ children }) {
|
||||
return (
|
||||
<div className="flex-grow-1 overflow-auto">
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
FullScreenModalBody.propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
};
|
||||
34
src/generic/full-screen-modal/FullScreenModalHeader.jsx
Normal file
34
src/generic/full-screen-modal/FullScreenModalHeader.jsx
Normal file
@@ -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 (
|
||||
<div
|
||||
className={classNames(
|
||||
'bg-primary',
|
||||
'text-white',
|
||||
'd-flex',
|
||||
'justify-content-between',
|
||||
'align-items-center',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<h2 className="pl-3 h6 mb-0">{title}</h2>
|
||||
<ModalCloseButton variant="outline-link" className="text-white">
|
||||
<Close />
|
||||
</ModalCloseButton>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
FullScreenModalHeader.propTypes = {
|
||||
className: PropTypes.string,
|
||||
title: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
FullScreenModalHeader.defaultProps = {
|
||||
className: null,
|
||||
};
|
||||
30
src/generic/stepper/StepIcon.jsx
Normal file
30
src/generic/stepper/StepIcon.jsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
export default function StepIcon({ children, size }) {
|
||||
return (
|
||||
<div
|
||||
className="rounded-circle small mr-2 bg-primary text-white d-flex justify-content-center align-items-center"
|
||||
style={{
|
||||
// TODO: Is there a better way to lock the shape of this thing?
|
||||
width: size,
|
||||
minWidth: size,
|
||||
height: size,
|
||||
minHeight: size,
|
||||
maxWidth: size,
|
||||
maxHeight: size,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
StepIcon.propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
size: PropTypes.string,
|
||||
};
|
||||
|
||||
StepIcon.defaultProps = {
|
||||
size: '1.3rem',
|
||||
};
|
||||
7
src/generic/stepper/StepLine.jsx
Normal file
7
src/generic/stepper/StepLine.jsx
Normal file
@@ -0,0 +1,7 @@
|
||||
import React from 'react';
|
||||
|
||||
export default function StepLine() {
|
||||
return (
|
||||
<div className="border-bottom mx-2" style={{ width: '5rem' }} />
|
||||
);
|
||||
}
|
||||
26
src/generic/stepper/Stepper.jsx
Normal file
26
src/generic/stepper/Stepper.jsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
|
||||
export default function Stepper({ children, className }) {
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'd-flex',
|
||||
'flex-column',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Stepper.propTypes = {
|
||||
className: PropTypes.string,
|
||||
children: PropTypes.node.isRequired,
|
||||
};
|
||||
|
||||
Stepper.defaultProps = {
|
||||
className: null,
|
||||
};
|
||||
25
src/generic/stepper/StepperBody.jsx
Normal file
25
src/generic/stepper/StepperBody.jsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
|
||||
export default function StepperBody({ children, className }) {
|
||||
return (
|
||||
<div className={classNames(
|
||||
'overflow-auto',
|
||||
'flex-grow-1',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
StepperBody.propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
className: PropTypes.string,
|
||||
};
|
||||
|
||||
StepperBody.defaultProps = {
|
||||
className: null,
|
||||
};
|
||||
33
src/generic/stepper/StepperFooter.jsx
Normal file
33
src/generic/stepper/StepperFooter.jsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
|
||||
export default function StepperFooter({ children, className }) {
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'bg-white',
|
||||
'p-2',
|
||||
// position-relative raises this div to a higher 'layer' so that its drop shadow falls
|
||||
// above the content in the div with our content.
|
||||
'position-relative',
|
||||
'w-100',
|
||||
className,
|
||||
)}
|
||||
style={{
|
||||
boxShadow: '0 -0.25rem 0.5rem rgba(0, 0, 0, 0.3)',
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
StepperFooter.propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
className: PropTypes.string,
|
||||
};
|
||||
|
||||
StepperFooter.defaultProps = {
|
||||
className: null,
|
||||
};
|
||||
54
src/generic/stepper/StepperHeader.jsx
Normal file
54
src/generic/stepper/StepperHeader.jsx
Normal file
@@ -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 (
|
||||
<div
|
||||
className={classNames(
|
||||
'p-2',
|
||||
'd-flex',
|
||||
'border-bottom',
|
||||
'border-light',
|
||||
'justify-content-center',
|
||||
'align-items-center',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{steps.map(({ iconLabel, label }, index) => {
|
||||
const isNotLastStep = index < steps.length - 1;
|
||||
return (
|
||||
// eslint-disable-next-line react/no-array-index-key
|
||||
<React.Fragment key={`${index}-${label}`}>
|
||||
<StepIcon>
|
||||
{iconLabel || index + 1}
|
||||
</StepIcon>
|
||||
<span className="font-weight-bold">{label}</span>
|
||||
{isNotLastStep && (
|
||||
<StepLine />
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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,
|
||||
};
|
||||
@@ -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 (
|
||||
<main>
|
||||
<div className="container-fluid bg-info-100 pb-3">
|
||||
@@ -80,6 +40,11 @@ function PagesAndResources({ courseId, intl }) {
|
||||
<PageGrid pages={pages} />
|
||||
<ResourceList />
|
||||
</div>
|
||||
<Switch>
|
||||
<PageRoute path={`${path}/discussions`}>
|
||||
<Discussions courseId={courseId} />
|
||||
</PageRoute>
|
||||
</Switch>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
61
src/pages-and-resources/data/api.js
Normal file
61
src/pages-and-resources/data/api.js
Normal file
@@ -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',
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
31
src/pages-and-resources/data/slice.js
Normal file
31
src/pages-and-resources/data/slice.js
Normal file
@@ -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;
|
||||
33
src/pages-and-resources/data/thunks.js
Normal file
33
src/pages-and-resources/data/thunks.js
Normal file
@@ -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 }));
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -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 (
|
||||
<Card
|
||||
key={app.id}
|
||||
tabIndex={app.isAvailable ? '-1' : ''}
|
||||
onClick={() => { 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);
|
||||
63
src/pages-and-resources/discussions/AppList.jsx
Normal file
63
src/pages-and-resources/discussions/AppList.jsx
Normal file
@@ -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 (
|
||||
<div className="m-5">
|
||||
<h2 className="my-4 text-center">{intl.formatMessage(messages.heading)}</h2>
|
||||
<CardGrid
|
||||
columnSizes={{
|
||||
xs: 12,
|
||||
sm: 6,
|
||||
lg: 4,
|
||||
}}
|
||||
>
|
||||
{apps.map(app => (
|
||||
<AppCard
|
||||
key={app.id}
|
||||
app={app}
|
||||
selected={app.id === selectedAppId}
|
||||
onClick={onSelectApp}
|
||||
/>
|
||||
))}
|
||||
</CardGrid>
|
||||
|
||||
<h2 className="my-3">
|
||||
{intl.formatMessage(messages.supportedFeatures)}
|
||||
</h2>
|
||||
|
||||
<FeaturesTable
|
||||
apps={apps}
|
||||
features={features}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
AppList.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
onSelectApp: PropTypes.func.isRequired,
|
||||
selectedAppId: PropTypes.string,
|
||||
};
|
||||
|
||||
AppList.defaultProps = {
|
||||
selectedAppId: null,
|
||||
};
|
||||
|
||||
export default injectIntl(AppList);
|
||||
@@ -1,4 +1,4 @@
|
||||
describe('DiscussionAppList', () => {
|
||||
describe('AppList', () => {
|
||||
it('will pass because it is an example', () => {
|
||||
|
||||
});
|
||||
46
src/pages-and-resources/discussions/ConfigFormContainer.jsx
Normal file
46
src/pages-and-resources/discussions/ConfigFormContainer.jsx
Normal file
@@ -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 (
|
||||
<LtiConfigForm
|
||||
formRef={formRef}
|
||||
app={app}
|
||||
appConfig={appConfig}
|
||||
onSubmit={onSubmit}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
ConfigFormContainer.propTypes = {
|
||||
courseId: PropTypes.string.isRequired,
|
||||
onSubmit: PropTypes.func.isRequired,
|
||||
// eslint-disable-next-line react/forbid-prop-types
|
||||
formRef: PropTypes.object.isRequired,
|
||||
};
|
||||
@@ -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 (
|
||||
<div className="m-5">
|
||||
<h2 className="my-4 text-center">{intl.formatMessage(messages.heading)}</h2>
|
||||
<CardGrid
|
||||
columnSizes={{
|
||||
xs: 12,
|
||||
sm: 6,
|
||||
lg: 4,
|
||||
}}
|
||||
>
|
||||
{apps.map(app => (
|
||||
<DiscussionAppCard
|
||||
key={app.id}
|
||||
app={app}
|
||||
selected={app.id === selectedAppId}
|
||||
clickHandler={handleSelectApp}
|
||||
/>
|
||||
))}
|
||||
</CardGrid>
|
||||
|
||||
<div className="d-flex justify-content-between align-items-center">
|
||||
<h2 className="my-3">
|
||||
{intl.formatMessage(messages.supportedFeatures)}
|
||||
</h2>
|
||||
{selectedAppId && (
|
||||
<Button variant="primary" onClick={handleConfigureApp}>
|
||||
{intl.formatMessage(messages.configureApp, { name: selectedApp.name })}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<FeaturesTable
|
||||
apps={apps}
|
||||
features={features}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
DiscussionAppList.propTypes = {
|
||||
courseId: PropTypes.string.isRequired,
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(DiscussionAppList);
|
||||
@@ -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 (
|
||||
<DiscussionsConfigForm
|
||||
courseId={courseId}
|
||||
app={app}
|
||||
appConfig={appConfig}
|
||||
submitHandler={handleSubmit}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
DiscussionConfig.propTypes = {
|
||||
courseId: PropTypes.string.isRequired,
|
||||
};
|
||||
150
src/pages-and-resources/discussions/Discussions.jsx
Normal file
150
src/pages-and-resources/discussions/Discussions.jsx
Normal file
@@ -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 : (
|
||||
<Check style={{ width: '1rem', height: '1rem' }} />
|
||||
),
|
||||
},
|
||||
{
|
||||
label: 'Configure discussions',
|
||||
}];
|
||||
|
||||
const submitButtonState = isSubmitting ? 'pending' : 'default';
|
||||
|
||||
return (
|
||||
<FullScreenModal title={intl.formatMessage(messages.configure)} onClose={onClose}>
|
||||
<FullScreenModalHeader title={intl.formatMessage(messages.configure)} />
|
||||
<FullScreenModalBody className="d-flex flex-column">
|
||||
<Stepper className="h-100">
|
||||
<StepperHeader steps={steps} />
|
||||
<StepperBody>
|
||||
<Switch>
|
||||
<PageRoute exact path={`${path}`}>
|
||||
<AppList
|
||||
onSelectApp={onSelectApp}
|
||||
selectedAppId={selectedAppId}
|
||||
/>
|
||||
</PageRoute>
|
||||
<PageRoute path={`${path}/configure/:appId`}>
|
||||
<ConfigFormContainer
|
||||
courseId={courseId}
|
||||
selectedAppId={selectedAppId}
|
||||
onSubmit={onSubmit}
|
||||
formRef={formRef}
|
||||
/>
|
||||
</PageRoute>
|
||||
</Switch>
|
||||
</StepperBody>
|
||||
<StepperFooter className="d-flex justify-content-between align-items-center">
|
||||
<Button variant="outline-primary" onClick={onBack} disabled={isFirstStep}>
|
||||
{intl.formatMessage(messages.backButton)}
|
||||
</Button>
|
||||
{isFirstStep && (
|
||||
<Button variant="primary" onClick={onStartConfig} disabled={!selectedAppId}>
|
||||
{intl.formatMessage(messages.nextButton)}
|
||||
</Button>
|
||||
)}
|
||||
{!isFirstStep && (
|
||||
<StatefulButton
|
||||
labels={{
|
||||
default: intl.formatMessage(messages.applyButton),
|
||||
pending: intl.formatMessage(messages.savingConfig),
|
||||
complete: intl.formatMessage(messages.savedConfig),
|
||||
}}
|
||||
state={submitButtonState}
|
||||
className="mr-3"
|
||||
onClick={onApply}
|
||||
/>
|
||||
)}
|
||||
</StepperFooter>
|
||||
</Stepper>
|
||||
</FullScreenModalBody>
|
||||
</FullScreenModal>
|
||||
);
|
||||
}
|
||||
|
||||
Discussions.propTypes = {
|
||||
courseId: PropTypes.string.isRequired,
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(Discussions);
|
||||
@@ -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 (
|
||||
<Switch>
|
||||
<PageRoute exact path={`${path}`}>
|
||||
<DiscussionAppList courseId={courseId} />
|
||||
</PageRoute>
|
||||
<PageRoute path={`${path}/configure/:appId`}>
|
||||
<DiscussionConfig courseId={courseId} />
|
||||
</PageRoute>
|
||||
</Switch>
|
||||
);
|
||||
}
|
||||
|
||||
DiscussionsRoutes.propTypes = {
|
||||
courseId: PropTypes.string.isRequired,
|
||||
};
|
||||
@@ -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 (
|
||||
<Form className="m-5" onSubmit={handleSubmit}>
|
||||
<Form ref={formRef} className="m-5" onSubmit={handleSubmit}>
|
||||
<h1>{intl.formatMessage(messages.configureApp, { name: app.name })}</h1>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="authoring.discussions.appDocInstructions"
|
||||
defaultMessage="Please visit the {documentationPageLink} for {name} to set up the tool, then paste your consumer key and consumer secret here."
|
||||
description="Instructions for the user to go visit a third party app's documentation to learn how to generate a set of values needed in this form."
|
||||
defaultMessage="{documentationPageLink} to set up the tool, then paste your consumer key and consumer secret below:"
|
||||
description="Instructions for the user to go visit a third party app's documentation to learn how to generate a set of values needed in this form. documentationPageLink says 'Visit the {name} documentation page'"
|
||||
values={{
|
||||
documentationPageLink: (
|
||||
<Hyperlink destination={app.documentationUrl}>
|
||||
{intl.formatMessage(messages.documentationPage)}
|
||||
<Hyperlink
|
||||
destination={app.documentationUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{intl.formatMessage(messages.documentationPage, { name: app.name })}
|
||||
</Hyperlink>
|
||||
),
|
||||
name: app.name,
|
||||
@@ -89,22 +89,11 @@ function DiscussionConfigForm({
|
||||
{intl.formatMessage(messages.launchUrlRequired)}
|
||||
</Form.Control.Feedback>
|
||||
</Form.Group>
|
||||
<StatefulButton
|
||||
labels={{
|
||||
default: intl.formatMessage(messages.saveConfig),
|
||||
pending: intl.formatMessage(messages.savingConfig),
|
||||
complete: intl.formatMessage(messages.savedConfig),
|
||||
}}
|
||||
type="submit"
|
||||
state={submitButtonState}
|
||||
className="mr-3"
|
||||
/>
|
||||
<Button variant="link" onClick={() => history.push(`/course/${courseId}/pages-and-resources/discussion`)}>{intl.formatMessage(messages.backButton)}</Button>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
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);
|
||||
1
src/pages-and-resources/discussions/index.js
Normal file
1
src/pages-and-resources/discussions/index.js
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from './Discussions';
|
||||
@@ -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.',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
@import "./discussions/DiscussionAppList";
|
||||
@import "./discussions/AppList";
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user