Convert "Pages & Resources" page to a plugin system (#638)
* feat: Make "Pages & Resources" course apps into plugins * feat: move ora_settings * feat: move proctoring * feat: move progress * feat: move teams * feat: move wiki * feat: move Xpert settings * fix: add webpack.prod.config.js * fix: clean up unused parts of package.json files * feat: Add an error message when displaying a Course App Plugin fails * chore: fix various eslint warnings * chore: fix jest tests * fix: error preventing "npm ci" from working * feat: better tests for <SettingsComponent> * chore: move xpert_unit_summary into same dir as other plugins * fix: eslint-import-resolver-webpack is a dev dependency * chore: move learning_assistant to be a plugin too * feat: for compatibility, install 2U plugins by default * fix: bug with learning_assistant package.json
This commit is contained in:
124
plugins/course-apps/live/data/api.js
Normal file
124
plugins/course-apps/live/data/api.js
Normal file
@@ -0,0 +1,124 @@
|
||||
/* eslint-disable import/prefer-default-export */
|
||||
import { ensureConfig, getConfig } from '@edx/frontend-platform';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
import { bbbPlanTypes } from '../constants';
|
||||
|
||||
ensureConfig([
|
||||
'STUDIO_BASE_URL',
|
||||
], 'Course Apps API service');
|
||||
|
||||
const apiBaseUrl = getConfig().STUDIO_BASE_URL;
|
||||
|
||||
export const providersApiUrl = `${apiBaseUrl}/api/course_live/providers`;
|
||||
export const providerConfigurationApiUrl = `${apiBaseUrl}/api/course_live/course`;
|
||||
|
||||
function normalizeProviders(data) {
|
||||
const apps = Object.entries(data.providers.available).map(([key, app]) => ({
|
||||
id: key,
|
||||
featureIds: app.features,
|
||||
name: app.name,
|
||||
piiSharing: app.pii_sharing,
|
||||
hasFreeTier: app.has_free_tier,
|
||||
}));
|
||||
|
||||
return {
|
||||
activeAppId: data.providers.active,
|
||||
selectedAppId: data.providers.active,
|
||||
apps,
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeLtiConfig(data) {
|
||||
if (!data || Object.keys(data).length < 1) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return {
|
||||
consumerKey: data.lti_1p1_client_key,
|
||||
consumerSecret: data.lti_1p1_client_secret,
|
||||
launchUrl: data.lti_1p1_launch_url,
|
||||
launchEmail: data.lti_config?.additional_parameters?.custom_instructor_email,
|
||||
tierType: data.tierType,
|
||||
};
|
||||
}
|
||||
|
||||
export function normalizeSettings(data) {
|
||||
let tier;
|
||||
if (data.provider_type === 'big_blue_button') {
|
||||
tier = data.free_tier === true ? bbbPlanTypes.free : bbbPlanTypes.commercial;
|
||||
}
|
||||
return {
|
||||
enabled: data.enabled,
|
||||
piiSharingAllowed: data.pii_sharing_allowed,
|
||||
appConfig: {
|
||||
id: data.provider_type,
|
||||
...normalizeLtiConfig({ ...data.lti_configuration, tierType: tier }),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function deNormalizeSettings(data) {
|
||||
const ltiConfiguration = {};
|
||||
if (data.consumerKey) {
|
||||
ltiConfiguration.lti_1p1_client_key = data.consumerKey;
|
||||
}
|
||||
if (data.consumerSecret) {
|
||||
ltiConfiguration.lti_1p1_client_secret = data.consumerSecret;
|
||||
}
|
||||
if (data.launchUrl) {
|
||||
ltiConfiguration.lti_1p1_launch_url = data.launchUrl;
|
||||
}
|
||||
if (data?.provider === 'zoom' || data.tierType !== 'Free') {
|
||||
ltiConfiguration.lti_config = {};
|
||||
if (data.launchEmail) {
|
||||
ltiConfiguration.lti_config.additional_parameters = {
|
||||
custom_instructor_email: data.launchEmail,
|
||||
};
|
||||
}
|
||||
}
|
||||
if (Object.keys(ltiConfiguration).length > 0) {
|
||||
// Only add this in if we're sending LTI fields.
|
||||
// TODO: Eventually support LTI v1.3 here.
|
||||
ltiConfiguration.version = 'lti_1p1';
|
||||
}
|
||||
|
||||
const apiData = {
|
||||
enabled: data?.enabled || false,
|
||||
lti_configuration: Object.keys(ltiConfiguration).length ? ltiConfiguration : undefined,
|
||||
provider_type: data?.provider || 'zoom',
|
||||
pii_sharing_allowed: data?.piiSharingEnable || false,
|
||||
free_tier: data?.provider === 'zoom' ? false : Boolean(data.tierType === 'Free'),
|
||||
};
|
||||
return apiData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches providers for provided course
|
||||
* @param {string} courseId
|
||||
* @returns {Promise<[{}]>}
|
||||
*/
|
||||
export async function getLiveProviders(courseId) {
|
||||
const { data } = await getAuthenticatedHttpClient()
|
||||
.get(`${providersApiUrl}/${courseId}/`);
|
||||
|
||||
return normalizeProviders(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches provider settings for provided course
|
||||
* @param {string} courseId
|
||||
* @returns {Promise<[{}]>}
|
||||
*/
|
||||
export async function getLiveConfiguration(courseId) {
|
||||
const { data } = await getAuthenticatedHttpClient()
|
||||
.get(`${providerConfigurationApiUrl}/${courseId}/`);
|
||||
return normalizeSettings(data);
|
||||
}
|
||||
|
||||
export async function postLiveConfiguration(courseId, config) {
|
||||
const { data } = await getAuthenticatedHttpClient().post(
|
||||
`${providerConfigurationApiUrl}/${courseId}/`,
|
||||
deNormalizeSettings(config),
|
||||
);
|
||||
return normalizeSettings(data);
|
||||
}
|
||||
48
plugins/course-apps/live/data/slice.js
Normal file
48
plugins/course-apps/live/data/slice.js
Normal file
@@ -0,0 +1,48 @@
|
||||
/* eslint-disable no-param-reassign */
|
||||
import { createSlice } from '@reduxjs/toolkit';
|
||||
import { RequestStatus } from 'CourseAuthoring/data/constants';
|
||||
|
||||
const slice = createSlice({
|
||||
name: 'live',
|
||||
initialState: {
|
||||
appIds: [],
|
||||
// activeAppId is the ID of the app that has been configured for the course.
|
||||
activeAppId: null,
|
||||
// selectedAppId is the ID of the app that has been selected in the UI. This happens when an
|
||||
// activeAppId has been configured but the user is about to configure a different provider
|
||||
// instead.
|
||||
selectedAppId: null,
|
||||
status: RequestStatus.IN_PROGRESS,
|
||||
saveStatus: RequestStatus.SUCCESSFUL,
|
||||
},
|
||||
reducers: {
|
||||
loadApps: (state, { payload }) => {
|
||||
state.status = RequestStatus.SUCCESSFUL;
|
||||
state.saveStatus = RequestStatus.SUCCESSFUL;
|
||||
Object.assign(state, payload);
|
||||
},
|
||||
selectApp: (state, { payload }) => {
|
||||
const { appId } = payload;
|
||||
state.selectedAppId = appId;
|
||||
},
|
||||
updateStatus: (state, { payload }) => {
|
||||
const { status } = payload;
|
||||
state.status = status;
|
||||
},
|
||||
updateSaveStatus: (state, { payload }) => {
|
||||
const { status } = payload;
|
||||
state.saveStatus = status;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const {
|
||||
loadApps,
|
||||
selectApp,
|
||||
updateStatus,
|
||||
updateSaveStatus,
|
||||
} = slice.actions;
|
||||
|
||||
export const {
|
||||
reducer,
|
||||
} = slice;
|
||||
86
plugins/course-apps/live/data/thunks.js
Normal file
86
plugins/course-apps/live/data/thunks.js
Normal file
@@ -0,0 +1,86 @@
|
||||
import { addModel, addModels, updateModel } from 'CourseAuthoring/generic/model-store';
|
||||
import { RequestStatus } from 'CourseAuthoring/data/constants';
|
||||
|
||||
import {
|
||||
getLiveConfiguration,
|
||||
getLiveProviders,
|
||||
postLiveConfiguration,
|
||||
normalizeSettings,
|
||||
deNormalizeSettings,
|
||||
} from './api';
|
||||
import { loadApps, updateStatus, updateSaveStatus } from './slice';
|
||||
|
||||
function updateLiveSettingsState({
|
||||
appConfig,
|
||||
...liveSettings
|
||||
}) {
|
||||
return async (dispatch) => {
|
||||
dispatch(addModel({ modelType: 'liveAppConfigs', model: appConfig }));
|
||||
dispatch(loadApps(liveSettings));
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchLiveProviders(courseId) {
|
||||
return async (dispatch) => {
|
||||
const { activeAppId, selectedAppId, apps } = await getLiveProviders(courseId);
|
||||
|
||||
dispatch(addModels({ modelType: 'liveApps', models: apps }));
|
||||
dispatch(loadApps({
|
||||
activeAppId,
|
||||
selectedAppId,
|
||||
appIds: apps.map((app) => app.id),
|
||||
}));
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchLiveConfiguration(courseId) {
|
||||
return async (dispatch) => {
|
||||
const settings = await getLiveConfiguration(courseId);
|
||||
dispatch(updateLiveSettingsState(settings));
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchLiveData(courseId) {
|
||||
return async (dispatch) => {
|
||||
dispatch(updateStatus({ status: RequestStatus.IN_PROGRESS }));
|
||||
try {
|
||||
await dispatch(fetchLiveProviders(courseId));
|
||||
await dispatch(fetchLiveConfiguration(courseId));
|
||||
} catch (error) {
|
||||
if (error.response && error.response.status === 403) {
|
||||
dispatch(updateStatus({ status: RequestStatus.DENIED }));
|
||||
} else {
|
||||
dispatch(updateStatus({ status: RequestStatus.FAILED }));
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function saveLiveConfiguration(courseId, config, navigate) {
|
||||
return async (dispatch) => {
|
||||
dispatch(updateSaveStatus({ status: RequestStatus.IN_PROGRESS }));
|
||||
try {
|
||||
const apps = await postLiveConfiguration(courseId, config);
|
||||
dispatch(updateLiveSettingsState(apps));
|
||||
|
||||
dispatch(updateSaveStatus({ status: RequestStatus.SUCCESSFUL }));
|
||||
navigate(`/course/${courseId}/pages-and-resources/`);
|
||||
} catch (error) {
|
||||
if (error.response && error.response.status === 403) {
|
||||
dispatch(updateSaveStatus({ status: RequestStatus.DENIED }));
|
||||
dispatch(updateStatus({ status: RequestStatus.DENIED }));
|
||||
} else {
|
||||
dispatch(updateSaveStatus({ status: RequestStatus.FAILED }));
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function saveLiveConfigurationAsDraft(config) {
|
||||
return async (dispatch) => {
|
||||
const { appConfig, ...liveSettings } = normalizeSettings(deNormalizeSettings(config));
|
||||
|
||||
dispatch(updateModel({ modelType: 'liveAppConfigs', model: appConfig }));
|
||||
dispatch(loadApps(liveSettings));
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user