+ <>
+ {
+ const appCheckmarkCells = {};
+ // DataTable wants 'data' to be an array of objects where each property of an object
+ // represents a cell in that row, identified by its key.
+ apps.forEach(app => {
+ // If our app's set of feature Ids includes this feature, return a checkmark.
+ // i.e, if this app has the current feature, check it!
+ appCheckmarkCells[app.id] = app.featureIds.includes(feature.id) ? (
+
+
+
+ ) : null;
+ });
+
+ return {
+ feature: feature.name, // 'feature' is the identifier for cells in the first column.
+ // This is spreading the app IDs from appCheckmarkCells into the return array, creating
+ // one object with 'feature' and the app.id keys from above. The values are the JSX
+ // above with the font awesome checkmarks in 'em
+ ...appCheckmarkCells,
+ };
+ })}
+ columns={[
+ {
+ Header: '',
+ accessor: 'feature',
+ },
+ // We're converting our apps array into a bunch of objects with "Header" and "accessor"
+ // keys, like DataTable expects.
+ ...apps.map(app => ({
+ Header: app.name,
+ accessor: app.id,
+ })),
+ ]}
+ >
+
+
+ >
);
}
diff --git a/src/pages-and-resources/discussions/data/api.js b/src/pages-and-resources/discussions/data/api.js
index a19f674eb..b632264a5 100644
--- a/src/pages-and-resources/discussions/data/api.js
+++ b/src/pages-and-resources/discussions/data/api.js
@@ -1,5 +1,53 @@
-/* eslint-disable import/prefer-default-export */
-export function getDiscussionApps() {
+const edXForumsApp = {
+ id: 'edx-forums',
+ name: 'edX Forum',
+ logo: 'https://cdn-blog.lawrencemcdaniel.com/wp-content/uploads/2018/01/22125436/edx-logo.png',
+ description: 'Start conversations with other learners, ask questions, and interact with other learners in the course.',
+ supportLevel: 'Full support',
+ isAvailable: true,
+ documentationUrl: 'https://localhost/docs',
+ featureIds: [
+ 'lti',
+ 'discussion-page',
+ 'embedded-course-sections',
+ 'embedded-course-units',
+ 'wcag-2.1',
+ ],
+};
+
+const piazzaApp = {
+ id: 'piazza',
+ name: 'Piazza',
+ logo: 'https://piazza.com/images/splash2/topbar/piazza_logo_blue.png',
+ description: 'Piazza is designed to connect students, TAs, and professors so every student can get the help they need when they need it.',
+ supportLevel: 'Partial support',
+ isAvailable: true,
+ documentationUrl: 'https://localhost/docs',
+ featureIds: [
+ 'lti',
+ 'discussion-page',
+ 'embedded-course-sections',
+ 'wcag-2.1',
+ ],
+};
+
+const yellowdigApp = {
+ id: 'yellowdig',
+ name: 'Yellowdig',
+ logo: 'https://static.wixstatic.com/media/e53d7e_f8a17bd41db64a57a8d62bea4fdf3174~mv2.png/v1/crop/x_5,y_0,w_895,h_196/fill/w_366,h_80,al_c,q_85,usm_0.66_1.00_0.01/yellowdig-logo.webp',
+ description: 'Yellowdig is the digital solution that impacts the entire student lifecycle and enables lifelong learning.',
+ supportLevel: 'Coming soon',
+ isAvailable: false,
+ documentationUrl: 'https://localhost/docs',
+ featureIds: [
+ 'lti',
+ 'discussion-page',
+ 'embedded-course-sections',
+ 'wcag-2.1',
+ ],
+};
+
+export function getApps() {
return Promise.resolve({
features: [
{
@@ -24,64 +72,107 @@ export function getDiscussionApps() {
},
],
apps: [
+ edXForumsApp,
+ piazzaApp,
+ yellowdigApp,
+ ],
+ activeAppId: 'piazza',
+ });
+}
+
+export function getAppConfig(courseId, appId) {
+ let app = null;
+ switch (appId) {
+ case 'piazza':
+ app = piazzaApp;
+ break;
+ case 'yellowdig':
+ app = yellowdigApp;
+ break;
+ default:
+ app = edXForumsApp;
+ }
+
+ return Promise.resolve({
+ app,
+ appConfig: {
+ id: 'appConfig1',
+ consumerSecret: 'its-a-secret-to-everybody',
+ consumerKey: 'abc123',
+ launchUrl: 'https://localhost/launch',
+ },
+ features: [
{
- id: 'edx-forums',
- name: 'edX Forum',
- logo: 'https://cdn-blog.lawrencemcdaniel.com/wp-content/uploads/2018/01/22125436/edx-logo.png',
- description: 'Start conversations with other learners, ask questions, and interact with other learners in the course.',
- supportLevel: 'Full support',
- isAvailable: true,
- featureIds: [
- 'lti',
- 'discussion-page',
- 'embedded-course-sections',
- 'embedded-course-units',
- 'wcag-2.1',
- ],
+ id: 'lti',
+ name: 'LTI Integration',
},
{
- id: 'piazza',
- name: 'Piazza',
- logo: 'https://cdn-blog.lawrencemcdaniel.com/wp-content/uploads/2018/01/22125436/edx-logo.png',
- description: 'Piazza is designed to connect students, TAs, and professors so every student can get the help they need when they need it',
- supportLevel: 'Partial support',
- isAvailable: true,
- featureIds: [
- 'lti',
- 'discussion-page',
- 'embedded-course-sections',
- 'wcag-2.1',
- ],
+ id: 'discussion-page',
+ name: 'Discussion Page',
},
{
- id: 'yellowdig',
- name: 'Yellowdig',
- logo: 'https://cdn-blog.lawrencemcdaniel.com/wp-content/uploads/2018/01/22125436/edx-logo.png',
- description: 'Yellowdig is the digital solution that impacts the entire student lifecycle and enables lifelong learning.',
- supportLevel: 'Coming soon',
- isAvailable: false,
- featureIds: [
- 'lti',
- 'discussion-page',
- 'embedded-course-sections',
- 'wcag-2.1',
- ],
+ id: 'embedded-course-sections',
+ name: 'Embedded Course Sections',
},
{
- id: 'untitled-forum',
- name: 'Untitled Forum',
- logo: 'https://cdn-blog.lawrencemcdaniel.com/wp-content/uploads/2018/01/22125436/edx-logo.png',
- description: 'Start conversations with other learners, ask questions, and interact with other learners in the course.',
- supportLevel: 'Full support',
- isAvailable: true,
- featureIds: [
- 'lti',
- 'discussion-page',
- 'embedded-course-sections',
- 'embedded-course-units',
- 'wcag-2.1',
- ],
+ id: 'embedded-course-units',
+ name: 'Embedded Course Units',
+ },
+ {
+ id: 'wcag-2.1',
+ name: 'WCAG 2.1 Support',
},
],
});
}
+
+export function postAppConfig(courseId, appId, drafts) {
+ let app = null;
+ switch (appId) {
+ case 'piazza':
+ app = piazzaApp;
+ break;
+ case 'yellowdig':
+ app = yellowdigApp;
+ break;
+ default:
+ app = edXForumsApp;
+ }
+ return new Promise((resolve) => {
+ setTimeout(() => {
+ resolve({
+ app,
+ appConfig: {
+ id: 'appConfig1',
+ consumerSecret: 'its-a-secret-to-everybody',
+ consumerKey: 'abc123',
+ launchUrl: 'https://localhost/launch',
+ documentationUrl: 'https://localhost/docs',
+ ...drafts,
+ },
+ features: [
+ {
+ id: 'lti',
+ name: 'LTI Integration',
+ },
+ {
+ id: 'discussion-page',
+ name: 'Discussion Page',
+ },
+ {
+ id: 'embedded-course-sections',
+ name: 'Embedded Course Sections',
+ },
+ {
+ id: 'embedded-course-units',
+ name: 'Embedded Course Units',
+ },
+ {
+ id: 'wcag-2.1',
+ name: 'WCAG 2.1 Support',
+ },
+ ],
+ });
+ }, 1000);
+ });
+}
diff --git a/src/pages-and-resources/discussions/data/slice.js b/src/pages-and-resources/discussions/data/slice.js
index 82ebb1dc7..9bf00362c 100644
--- a/src/pages-and-resources/discussions/data/slice.js
+++ b/src/pages-and-resources/discussions/data/slice.js
@@ -4,11 +4,16 @@ import { createSlice } from '@reduxjs/toolkit';
export const LOADING = 'LOADING';
export const LOADED = 'LOADED';
export const FAILED = 'FAILED';
+export const SAVING = 'SAVING';
+export const SAVED = 'SAVED';
+export const DIRTY = 'DIRTY';
const slice = createSlice({
name: 'discussions',
initialState: {
appIds: [],
+ activeAppId: null,
+ activeAppConfigId: null,
featureIds: [],
status: LOADING,
},
@@ -16,7 +21,11 @@ const slice = createSlice({
fetchAppsSuccess: (state, { payload }) => {
state.appIds = payload.appIds;
state.featureIds = payload.featureIds;
- state.status = LOADED;
+ },
+ fetchAppConfigSuccess: (state, { payload }) => {
+ state.activeAppId = payload.activeAppId;
+ state.activeAppConfigId = payload.activeAppConfigId;
+ state.featureIds = payload.featureIds;
},
updateStatus: (state, { payload }) => {
state.status = payload.status;
@@ -26,6 +35,7 @@ const slice = createSlice({
export const {
fetchAppsSuccess,
+ fetchAppConfigSuccess,
updateStatus,
} = slice.actions;
diff --git a/src/pages-and-resources/discussions/data/thunks.js b/src/pages-and-resources/discussions/data/thunks.js
index 2f2d4a85b..1a5531b77 100644
--- a/src/pages-and-resources/discussions/data/thunks.js
+++ b/src/pages-and-resources/discussions/data/thunks.js
@@ -1,7 +1,18 @@
-import { getDiscussionApps } from './api';
-import { addModels } from '../../../generic/model-store';
import {
- FAILED, fetchAppsSuccess, LOADING, updateStatus,
+ getApps,
+ getAppConfig,
+ postAppConfig,
+} from './api';
+import { addModel, addModels } from '../../../generic/model-store';
+import {
+ FAILED,
+ fetchAppsSuccess,
+ fetchAppConfigSuccess,
+ LOADING,
+ updateStatus,
+ SAVING,
+ SAVED,
+ LOADED,
} from './slice';
/* eslint-disable import/prefer-default-export */
@@ -10,7 +21,7 @@ export function fetchApps(courseId) {
dispatch(updateStatus({ courseId, status: LOADING }));
try {
- const { apps, features } = await getDiscussionApps(courseId);
+ const { apps, features } = await getApps(courseId);
dispatch(addModels({ modelType: 'apps', models: apps }));
dispatch(addModels({ modelType: 'features', models: features }));
@@ -18,6 +29,7 @@ export function fetchApps(courseId) {
appIds: apps.map(app => app.id),
featureIds: features.map(feature => feature.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
@@ -26,3 +38,51 @@ export function fetchApps(courseId) {
}
};
}
+
+export function fetchAppConfig(courseId, appId) {
+ return async (dispatch) => {
+ dispatch(updateStatus({ courseId, status: LOADING }));
+
+ try {
+ const { app, appConfig, features } = await getAppConfig(courseId, appId);
+
+ dispatch(addModel({ modelType: 'apps', model: app }));
+ dispatch(addModels({ modelType: 'features', models: features }));
+ dispatch(addModel({ modelType: 'appConfigs', model: appConfig }));
+ dispatch(fetchAppConfigSuccess({
+ activeAppId: app.id,
+ activeAppConfigId: appConfig.id,
+ featureIds: features.map(feature => feature.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 }));
+ }
+ };
+}
+
+export function saveAppConfig(courseId, appId, drafts) {
+ return async (dispatch) => {
+ dispatch(updateStatus({ courseId, status: SAVING }));
+
+ try {
+ const { app, appConfig, features } = await postAppConfig(courseId, appId, drafts);
+
+ dispatch(addModel({ modelType: 'apps', model: app }));
+ dispatch(addModels({ modelType: 'features', models: features }));
+ dispatch(addModel({ modelType: 'appConfigs', model: appConfig }));
+
+ dispatch(fetchAppConfigSuccess({
+ activeAppId: app.id,
+ activeAppConfigId: appConfig.id,
+ featureIds: features.map(feature => feature.id),
+ }));
+ dispatch(updateStatus({ courseId, status: SAVED }));
+ } catch (error) {
+ dispatch(updateStatus({ courseId, status: FAILED }));
+ }
+ };
+}
diff --git a/src/pages-and-resources/discussions/messages.js b/src/pages-and-resources/discussions/messages.js
index 9450d4e40..7cb7058f2 100644
--- a/src/pages-and-resources/discussions/messages.js
+++ b/src/pages-and-resources/discussions/messages.js
@@ -17,6 +17,64 @@ const messages = defineMessages({
id: 'authoring.discussions.appLogo',
defaultMessage: '{name} Logo',
},
+ configModalTitle: {
+ id: 'authoring.discussions.modalTitle',
+ defaultMessage: 'Configure {name}',
+ },
+ saveConfig: {
+ id: 'authoring.discussions.saveConfig',
+ defaultMessage: 'Save',
+ description: 'Button allowing a user to save their discussions config.',
+ },
+ savingConfig: {
+ id: 'authoring.discussions.savingConfig',
+ defaultMessage: 'Saving',
+ description: 'Button text shown while a discussion config is being saved.',
+ },
+ savedConfig: {
+ id: 'authoring.discussions.savedConfig',
+ defaultMessage: 'Saved',
+ description: 'Button text shown once a discussion config has been saved to the server.',
+ },
+ documentationPage: {
+ id: 'authoring.discussions.documentationPage',
+ defaultMessage: 'documentation page',
+ },
+ consumerKey: {
+ id: 'authoring.discussions.consumerKey',
+ defaultMessage: 'Consumer Key',
+ description: 'Label for the Consumer Key field.',
+ },
+ consumerSecret: {
+ id: 'authoring.discussions.consumerSecret',
+ defaultMessage: 'Consumer Secret',
+ description: 'Label for the Consumer Secret field.',
+ },
+ launchUrl: {
+ id: 'authoring.discussions.launchUrl',
+ defaultMessage: 'Launch URL',
+ description: 'Label for the Launch URL field.',
+ },
+ consumerKeyRequired: {
+ id: 'authoring.discussions.consumerKey.required',
+ defaultMessage: 'Consumer Key is a required field.',
+ description: 'Tells the user that the Consumer Key field is required and must have a value.',
+ },
+ consumerSecretRequired: {
+ id: 'authoring.discussions.consumerSecret.required',
+ defaultMessage: 'Consumer Secret is a required field.',
+ description: 'Tells the user that the Consumer Secret field is required and must have a value.',
+ },
+ launchUrlRequired: {
+ id: 'authoring.discussions.launchUrl.required',
+ defaultMessage: 'Launch URL is a required field.',
+ description: 'Tells the user that the Launch URL field is required and must have a value.',
+ },
+ backButton: {
+ id: 'authoring.discussions.backButton',
+ defaultMessage: 'Back',
+ description: 'Back button allowing the user to return to discussion app selection.',
+ },
});
export default messages;
diff --git a/src/pages-and-resources/pages/PageCard.jsx b/src/pages-and-resources/pages/PageCard.jsx
index 71750cf25..e3b8a46ac 100644
--- a/src/pages-and-resources/pages/PageCard.jsx
+++ b/src/pages-and-resources/pages/PageCard.jsx
@@ -6,6 +6,8 @@ import { Button } from '@edx/paragon';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faCog } from '@fortawesome/free-solid-svg-icons';
+import { useLocation } from 'react-router';
+import { history } from '@edx/frontend-platform';
import messages from '../messages';
const CoursePageShape = PropTypes.shape({
@@ -21,6 +23,8 @@ const CoursePageShape = PropTypes.shape({
export { CoursePageShape };
function PageCard({ intl, page }) {
+ const { pathname } = useLocation();
+
const pageStatusMsgId = page.isEnabled ? 'pageStatus.enabled' : 'pageStatus.disabled';
const componentClasses = classNames(
'd-flex flex-column align-content-stretch',
@@ -28,6 +32,10 @@ function PageCard({ intl, page }) {
{ 'border-info-300': page.isEnabled, 'border-gray-100': !page.isEnabled },
);
+ const handleClick = () => {
+ history.push(`${pathname}/${page.id}`);
+ };
+
return (