feat: hook discussions provider list up to API (#57)
Prior to this, the list of providers in the UI was hardcoded in the api.js layer. This commit hooks that up to our actual API endpoint, allowing us to load the provider list from the server. It also - out of necessity - changes the way the AppCards are displayed, and what content is in them. This was somewhat opportunistic as our design for them simplified anyway, no longer requiring a logo or a few of the other fields. Because the actual API sends us less display-oriented data (i.e., no names or descriptions for things), we had to modify FeaturesTable and AppCard to fetch these strings from the messages file based on the app and feature IDs. I’m not super thrilled with this approach, since it’s somewhat brittle. Unexpected providers won’t display properly. In the long run, I expect all this may come from some other system. Using translations to load dynamic strings isn’t quite what it was intended for - i.e., we’re not putting “Piazza” in there because we want to translate it, but because the backend didn’t tell us what to use for the key “piazza”.
This commit is contained in:
@@ -3,20 +3,22 @@ import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { Card, Input } from '@edx/paragon';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faLock } from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
function AppCard({
|
||||
app, onClick, intl, selected,
|
||||
}) {
|
||||
const supportText = app.hasFullSupport
|
||||
? intl.formatMessage(messages.appFullSupport)
|
||||
: intl.formatMessage(messages.appPartialSupport);
|
||||
|
||||
return (
|
||||
<Card
|
||||
key={app.id}
|
||||
tabIndex={app.isAvailable ? '-1' : ''}
|
||||
onClick={() => { if (app.isAvailable) { onClick(app.id); } }}
|
||||
onKeyPress={() => { if (app.isAvailable) { onClick(app.id); } }}
|
||||
tabIndex="-1"
|
||||
onClick={() => onClick(app.id)}
|
||||
onKeyPress={() => onClick(app.id)}
|
||||
role="radio"
|
||||
aria-checked={selected}
|
||||
style={{
|
||||
@@ -29,48 +31,28 @@ function AppCard({
|
||||
<div
|
||||
className="position-absolute"
|
||||
style={{
|
||||
// This positioning of 0.75rem aligns the checkbox with the top of the logo
|
||||
top: '0.75rem',
|
||||
right: '0.75rem',
|
||||
}}
|
||||
>
|
||||
{app.isAvailable ? (
|
||||
<Input readOnly type="checkbox" checked={selected} />
|
||||
) : (
|
||||
<FontAwesomeIcon icon={faLock} />
|
||||
)}
|
||||
<Input readOnly type="checkbox" checked={selected} />
|
||||
</div>
|
||||
<Card.Img
|
||||
variant="top"
|
||||
style={{
|
||||
maxHeight: 100,
|
||||
objectFit: 'contain',
|
||||
}}
|
||||
className="py-3 pl-3 pr-5"
|
||||
src={app.logo}
|
||||
alt={intl.formatMessage(messages.appLogo, {
|
||||
name: app.name,
|
||||
})}
|
||||
/>
|
||||
<Card.Body>
|
||||
<Card.Title>{app.name}</Card.Title>
|
||||
<Card.Text>{app.description}</Card.Text>
|
||||
<Card.Title>
|
||||
{intl.formatMessage(messages[`appName-${app.id}`])}
|
||||
</Card.Title>
|
||||
<Card.Subtitle className="mb-2 text-muted">{supportText}</Card.Subtitle>
|
||||
<Card.Text>{intl.formatMessage(messages[`appDescription-${app.id}`])}</Card.Text>
|
||||
</Card.Body>
|
||||
<Card.Footer>
|
||||
{app.supportLevel}
|
||||
</Card.Footer>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
AppCard.propTypes = {
|
||||
app: PropTypes.shape({
|
||||
description: PropTypes.string.isRequired,
|
||||
id: PropTypes.string.isRequired,
|
||||
isAvailable: PropTypes.bool.isRequired,
|
||||
logo: PropTypes.string.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
supportLevel: PropTypes.string.isRequired,
|
||||
featureIds: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
hasFullSupport: PropTypes.bool.isRequired,
|
||||
}).isRequired,
|
||||
onClick: PropTypes.func.isRequired,
|
||||
selected: PropTypes.bool.isRequired,
|
||||
|
||||
@@ -32,7 +32,7 @@ function ConfigFormContainer({
|
||||
|
||||
let form = null;
|
||||
|
||||
if (app.id === 'edx-discussions') {
|
||||
if (app.id === 'legacy') {
|
||||
form = (
|
||||
<LegacyConfigForm
|
||||
formRef={formRef}
|
||||
|
||||
@@ -3,8 +3,10 @@ import PropTypes from 'prop-types';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faCheck } from '@fortawesome/free-solid-svg-icons';
|
||||
import { DataTable } from '@edx/paragon';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import messages from './messages';
|
||||
|
||||
export default function FeaturesTable({ apps, features }) {
|
||||
function FeaturesTable({ apps, features, intl }) {
|
||||
return (
|
||||
<>
|
||||
<DataTable
|
||||
@@ -24,7 +26,7 @@ export default function FeaturesTable({ apps, features }) {
|
||||
});
|
||||
|
||||
return {
|
||||
feature: feature.name, // 'feature' is the identifier for cells in the first column.
|
||||
feature: intl.formatMessage(messages[`featureName-${feature.id}`]), // '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
|
||||
@@ -39,7 +41,7 @@ export default function FeaturesTable({ apps, features }) {
|
||||
// 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,
|
||||
Header: intl.formatMessage(messages[`appName-${app.id}`]),
|
||||
accessor: app.id,
|
||||
})),
|
||||
]}
|
||||
@@ -50,7 +52,10 @@ export default function FeaturesTable({ apps, features }) {
|
||||
);
|
||||
}
|
||||
|
||||
export default injectIntl(FeaturesTable);
|
||||
|
||||
FeaturesTable.propTypes = {
|
||||
apps: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
features: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
@@ -1,3 +1,31 @@
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
|
||||
function normalizeApps(data) {
|
||||
const apps = Object.entries(data.providers.available).map(([key, app]) => ({
|
||||
id: key,
|
||||
featureIds: app.features,
|
||||
hasFullSupport: app.features.length >= data.features.length,
|
||||
}));
|
||||
return {
|
||||
courseId: data.context_key,
|
||||
enabled: data.enabled,
|
||||
features: data.features.map(id => ({
|
||||
id,
|
||||
})),
|
||||
appConfig: data.plugin_configuration,
|
||||
activeAppId: data.providers.active,
|
||||
apps,
|
||||
};
|
||||
}
|
||||
|
||||
export async function getApps(courseId) {
|
||||
const { data } = await getAuthenticatedHttpClient()
|
||||
.get(`${getConfig().LMS_BASE_URL}/discussions/api/v0/${courseId}`);
|
||||
|
||||
return normalizeApps(data);
|
||||
}
|
||||
|
||||
const legacyEdXDiscussions = {
|
||||
id: 'edx-discussions',
|
||||
name: 'edX Discussions',
|
||||
@@ -47,39 +75,6 @@ const yellowdigApp = {
|
||||
],
|
||||
};
|
||||
|
||||
export function getApps() {
|
||||
return Promise.resolve({
|
||||
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',
|
||||
},
|
||||
],
|
||||
apps: [
|
||||
legacyEdXDiscussions,
|
||||
piazzaApp,
|
||||
yellowdigApp,
|
||||
],
|
||||
activeAppId: 'piazza',
|
||||
});
|
||||
}
|
||||
|
||||
export function getAppConfig(courseId, appId) {
|
||||
let app = null;
|
||||
switch (appId) {
|
||||
@@ -124,23 +119,18 @@ export function getAppConfig(courseId, appId) {
|
||||
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',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
@@ -21,6 +21,7 @@ const slice = createSlice({
|
||||
fetchAppsSuccess: (state, { payload }) => {
|
||||
state.appIds = payload.appIds;
|
||||
state.featureIds = payload.featureIds;
|
||||
state.activeAppId = payload.activeAppId;
|
||||
},
|
||||
fetchAppConfigSuccess: (state, { payload }) => {
|
||||
state.activeAppId = payload.activeAppId;
|
||||
|
||||
@@ -21,11 +21,12 @@ export function fetchApps(courseId) {
|
||||
dispatch(updateStatus({ courseId, status: LOADING }));
|
||||
|
||||
try {
|
||||
const { apps, features } = await getApps(courseId);
|
||||
const { apps, features, activeAppId } = await getApps(courseId);
|
||||
|
||||
dispatch(addModels({ modelType: 'apps', models: apps }));
|
||||
dispatch(addModels({ modelType: 'features', models: features }));
|
||||
dispatch(fetchAppsSuccess({
|
||||
activeAppId,
|
||||
appIds: apps.map(app => app.id),
|
||||
featureIds: features.map(feature => feature.id),
|
||||
}));
|
||||
|
||||
@@ -3,7 +3,7 @@ import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
const messages = defineMessages({
|
||||
heading: {
|
||||
id: 'authoring.discussions.heading',
|
||||
defaultMessage: 'Which discussion tool would you like to use for this course?',
|
||||
defaultMessage: 'Select a discussion tool for this course',
|
||||
},
|
||||
supportedFeatures: {
|
||||
id: 'authoring.discussions.supportedFeatures',
|
||||
@@ -60,6 +60,60 @@ const messages = defineMessages({
|
||||
defaultMessage: 'Select discussion tool',
|
||||
description: 'A label for the first step of a wizard where the user chooses a discussion tool to configure.',
|
||||
},
|
||||
appFullSupport: {
|
||||
id: 'authoring.discussions.appFullSupport',
|
||||
defaultMessage: 'Full support',
|
||||
description: 'A label indicating that an app supports the full set of possible features for a discussions app.',
|
||||
},
|
||||
appPartialSupport: {
|
||||
id: 'authoring.discussions.appPartialSupport',
|
||||
defaultMessage: 'Partial support',
|
||||
description: 'A label indicating that an app only supports a subset of the possible features of a discussions app.',
|
||||
},
|
||||
// Legacy
|
||||
'appName-legacy': {
|
||||
id: 'authoring.discussions.appName-legacy',
|
||||
defaultMessage: 'Legacy edX Discussions',
|
||||
description: 'The name of the Legacy edX Discussions app.',
|
||||
},
|
||||
'appDescription-legacy': {
|
||||
id: 'authoring.discussions.appDescription-legacy',
|
||||
defaultMessage: 'Start conversations with other learners, ask questions, and interact with other learners in the course.',
|
||||
description: 'A description of the Legacy edX Discussions app.',
|
||||
},
|
||||
// Piazza
|
||||
'appName-piazza': {
|
||||
id: 'authoring.discussions.appName-piazza',
|
||||
defaultMessage: 'Piazza',
|
||||
description: 'The name of the Piazza app.',
|
||||
},
|
||||
'appDescription-piazza': {
|
||||
id: 'authoring.discussions.appDescription-piazza',
|
||||
defaultMessage: 'Piazza is designed to connect students, TAs, and professors so every student can get the help they need when they need it.',
|
||||
description: 'A description of the Piazza app.',
|
||||
},
|
||||
|
||||
// Features
|
||||
'featureName-discussion-page': {
|
||||
id: 'authoring.discussions.featureName-discussion-page',
|
||||
defaultMessage: 'Discussion Page',
|
||||
description: 'The name of a discussions feature.',
|
||||
},
|
||||
'featureName-embedded-course-sections': {
|
||||
id: 'authoring.discussions.featureName-embedded-course-sections',
|
||||
defaultMessage: 'Embedded Course Sections',
|
||||
description: 'The name of a discussions feature.',
|
||||
},
|
||||
'featureName-lti': {
|
||||
id: 'authoring.discussions.featureName-lti',
|
||||
defaultMessage: 'LTI Integration',
|
||||
description: 'The name of a discussions feature.',
|
||||
},
|
||||
'featureName-wcag-2.1': {
|
||||
id: 'authoring.discussions.featureName-wcag-2.1',
|
||||
defaultMessage: 'WCAG 2.1 Support',
|
||||
description: 'The name of a discussions feature.',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
|
||||
Reference in New Issue
Block a user