feat: course live integration (#271)

Co-authored-by: adeel.tajamul <adeel.tajamul@arbisoft.com>
This commit is contained in:
Muhammad Adeel Tajamul
2022-03-28 18:11:50 +05:00
committed by GitHub
parent 2e486088f0
commit 3836b4328e
6 changed files with 221 additions and 26 deletions

View File

@@ -121,6 +121,7 @@ function AppSettingsModal({
enableAppLabel,
enableAppHelp,
learnMoreText,
enableReinitialize,
}) {
const { courseId } = useContext(PagesAndResourcesContext);
const loadingStatus = useSelector(getLoadingStatus);
@@ -188,6 +189,7 @@ function AppSettingsModal({
})
}
onSubmit={handleFormSubmit}
enableReinitialize={enableReinitialize}
>
{(formikProps) => (
<Form onSubmit={handleFormikSubmit(formikProps)}>
@@ -284,6 +286,7 @@ AppSettingsModal.propTypes = {
enableAppHelp: PropTypes.string.isRequired,
learnMoreText: PropTypes.string.isRequired,
configureBeforeEnable: PropTypes.bool,
enableReinitialize: PropTypes.bool,
};
AppSettingsModal.defaultProps = {
@@ -292,6 +295,7 @@ AppSettingsModal.defaultProps = {
initialValues: {},
validationSchema: {},
configureBeforeEnable: false,
enableReinitialize: false,
};
export default injectIntl(AppSettingsModal);

View File

@@ -1,12 +1,12 @@
import React from 'react';
import React, { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { camelCase } from 'lodash';
import { SelectableBox, Icon } from '@edx/paragon';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import PropTypes from 'prop-types';
import * as Yup from 'yup';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { SelectableBox, Icon } from '@edx/paragon';
import { camelCase } from 'lodash';
import { fetchLiveData, saveLiveConfiguration } from './data/thunks';
import FormikControl from '../../generic/FormikControl';
import { useAppSetting } from '../../utils';
import AppExternalLinks from '../discussions/app-config-form/apps/shared/AppExternalLinks';
import AppSettingsModal from '../app-settings-modal/AppSettingsModal';
import iconsSrc from './constants';
import messages from './messages';
@@ -15,17 +15,15 @@ function LiveSettings({
intl,
onClose,
}) {
const [liveConfiguration, saveSettings] = useAppSetting('liveConfiguration');
const liveData = {
consumerKey: liveConfiguration?.consumerKey || '',
consumerSecret: liveConfiguration?.consumerSecret || '',
launchUrl: liveConfiguration?.launchUrl || '',
launchEmail: liveConfiguration?.launchEmail || '',
provider: liveConfiguration?.provider || 'zoom',
piiSharingEnable: liveConfiguration?.piiSharing
? liveConfiguration.piiShareUsername && liveConfiguration.piiShareEmail
: false,
};
const dispatch = useDispatch();
const courseId = useSelector(state => state.courseDetail.courseId);
const liveConfiguration = useSelector((state) => state.live.configuration);
const availableProviders = useSelector((state) => state.live.appIds);
useEffect(() => {
dispatch(fetchLiveData(courseId));
}, [courseId]);
const validationSchema = {
enabled: Yup.boolean(),
consumerKey: Yup.string().required(intl.formatMessage(messages.consumerKeyRequired)),
@@ -34,7 +32,9 @@ function LiveSettings({
launchEmail: Yup.string().required(intl.formatMessage(messages.launchEmailRequired)),
};
const handleSettingsSave = async (values) => saveSettings(values);
const handleSettingsSave = async (values) => {
await dispatch(saveLiveConfiguration(courseId, values));
};
const handleProviderChange = (selectedProvider, setFieldValue) => {
setFieldValue('provider', selectedProvider);
};
@@ -47,10 +47,11 @@ function LiveSettings({
enableAppLabel={intl.formatMessage(messages.enableLiveLabel)}
learnMoreText={intl.formatMessage(messages.enableLiveLink)}
onClose={onClose}
initialValues={liveData}
initialValues={liveConfiguration}
validationSchema={validationSchema}
onSettingsSave={handleSettingsSave}
configureBeforeEnable
enableReinitialize
>
{
({ values, setFieldValue }) => (
@@ -64,7 +65,7 @@ function LiveSettings({
columns={3}
className="mb-3"
>
{[{ id: 'zoom' }, { id: 'google meet' }, { id: 'microsoft teams' }].map((app) => (
{availableProviders.map((app) => (
<SelectableBox value={app.id} type="checkbox" key={app.id}>
<div className="d-flex flex-column align-items-center">
<Icon src={iconsSrc[`${camelCase(app.id)}`]} alt={app.id} />
@@ -74,7 +75,7 @@ function LiveSettings({
))}
</SelectableBox.Set>
<p>{intl.formatMessage(messages.providerHelperText, { providerName: 'Zoom' })}</p>
{liveData.piiSharingEnable ? (
{values.piiSharingEnable ? (
<>
<p className="pb-2">{intl.formatMessage(messages.formInstructions)}</p>
<FormikControl
@@ -104,11 +105,6 @@ function LiveSettings({
floatingLabel={intl.formatMessage(messages.launchEmail)}
type="input"
/>
<AppExternalLinks
externalLinks={liveConfiguration?.documentationLinks}
providerName="live"
showLaunchIcon
/>
</>
) : (
<p>{intl.formatMessage(messages.requestPiiSharingEnable)}</p>

View File

@@ -0,0 +1,42 @@
/* eslint-disable import/prefer-default-export */
import { camelCaseObject, ensureConfig, getConfig } from '@edx/frontend-platform';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
ensureConfig([
'STUDIO_BASE_URL',
], 'Course Apps API service');
const apiBaseUrl = getConfig().STUDIO_BASE_URL;
const providersApiUrl = `${apiBaseUrl}/api/course_live/providers`;
const providerConfigurationApiUrl = `${apiBaseUrl}/api/course_live/course`;
/**
* Fetches providers for provided course
* @param {string} courseId
* @returns {Promise<[{}]>}
*/
export async function getLiveProviders(courseId) {
const { data } = await getAuthenticatedHttpClient()
.get(`${providersApiUrl}/${courseId}/`);
return camelCaseObject(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 camelCaseObject(data);
}
export async function postLiveConfiguration(courseId, config) {
const data = await getAuthenticatedHttpClient().post(
`${providerConfigurationApiUrl}/${courseId}/`,
config,
);
return camelCaseObject(data);
}

View File

@@ -0,0 +1,49 @@
/* eslint-disable no-param-reassign */
import { createSlice } from '@reduxjs/toolkit';
import { RequestStatus } from '../../../data/constants';
const slice = createSlice({
name: 'live',
initialState: {
providers: {
available: {},
selectedProvider: {},
},
appIds: [],
status: RequestStatus.IN_PROGRESS,
configuration: {},
saveStatus: RequestStatus.SUCCESSFUL,
},
reducers: {
updateProviders: (state, { payload }) => {
Object.assign(state.providers, payload);
},
updateConfiguration: (state, { payload }) => {
Object.assign(state.configuration, payload);
state.configuredProvider = payload.provider;
},
updateStatus: (state, { payload }) => {
const { status } = payload;
state.status = status;
},
updateAppIds: (state, { payload }) => {
state.appIds = payload;
},
updateSaveStatus: (state, { payload }) => {
const { status } = payload;
state.saveStatus = status;
},
},
});
export const {
updateProviders,
updateConfiguration,
updateStatus,
updateSaveStatus,
updateAppIds,
} = slice.actions;
export const {
reducer,
} = slice;

View File

@@ -0,0 +1,102 @@
import { history } from '@edx/frontend-platform';
import { getLiveConfiguration, getLiveProviders, postLiveConfiguration } from './api';
import {
updateStatus, updateSaveStatus, updateProviders,
updateAppIds, updateConfiguration,
} from './slice';
import { RequestStatus } from '../../../data/constants';
function normalizeLiveConfig(config) {
const configuration = {};
configuration.courseKey = config?.courseKey || '';
configuration.enabled = config?.enabled || false;
configuration.consumerKey = config?.ltiConfiguration?.lti1P1ClientKey || '';
configuration.consumerSecret = config?.ltiConfiguration?.lti1P1ClientSecret || '';
configuration.launchUrl = config?.ltiConfiguration?.lti1P1LaunchUrl || '';
configuration.launchEmail = config?.ltiConfiguration?.ltiConfig?.additionalParameters?.customInstructorEmail || '';
configuration.provider = config?.providerType || 'zoom';
configuration.piiSharingEnable = config?.piiSharingAllowed || false;
return configuration;
}
function deNormalizeLiveConfig(config) {
const configuration = {};
configuration.course_key = config.courseKey;
configuration.provider_type = config?.provider || 'zoom';
configuration.enabled = config?.enabled || false;
configuration.lti_configuration = {
lti_1p1_client_key: config?.consumerKey || '',
lti_1p1_client_secret: config?.consumerSecret || '',
lti_1p1_launch_url: config?.launchUrl || '',
version: 'lti_1p1',
lti_config: {
additional_parameters: {
custom_instructor_email: config?.launchEmail || '',
},
},
};
configuration.pii_sharing_allowed = config?.piiSharingEnable || false;
return configuration;
}
export function fetchLiveProviders(courseId) {
return async (dispatch) => {
const providers = await getLiveProviders(courseId);
dispatch(updateProviders(providers.providers));
const availableProvidersInfo = [];
Object.keys(providers.providers.available).forEach((key) => { availableProvidersInfo.push({ id: key }); });
dispatch(updateAppIds(availableProvidersInfo));
};
}
function updateLiveConfigurationState(config) {
return async (dispatch) => {
const data = normalizeLiveConfig(config);
dispatch(updateConfiguration(data));
};
}
export function fetchLiveConfiguration(courseId) {
return async (dispatch) => {
const config = await getLiveConfiguration(courseId);
dispatch(updateLiveConfigurationState(config));
};
}
export function fetchLiveData(courseId) {
return async (dispatch) => {
dispatch(updateStatus({ status: RequestStatus.IN_PROGRESS }));
try {
await dispatch(fetchLiveConfiguration(courseId));
await dispatch(fetchLiveProviders(courseId));
dispatch(updateStatus({ status: RequestStatus.SUCCESSFUL }));
} 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) {
return async (dispatch) => {
dispatch(updateSaveStatus({ status: RequestStatus.IN_PROGRESS }));
try {
const configuration = normalizeLiveConfig(
(await postLiveConfiguration(courseId, deNormalizeLiveConfig(config))).data,
);
dispatch(updateConfiguration(configuration));
dispatch(updateSaveStatus({ status: RequestStatus.SUCCESSFUL }));
history.push(`/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 }));
}
}
};
}

View File

@@ -4,6 +4,7 @@ import { reducer as modelsReducer } from './generic/model-store';
import { reducer as courseDetailReducer } from './data/slice';
import { reducer as discussionsReducer } from './pages-and-resources/discussions';
import { reducer as pagesAndResourcesReducer } from './pages-and-resources/data/slice';
import { reducer as liveReducer } from './pages-and-resources/live/data/slice';
export default function initializeStore(preloadedState = undefined) {
return configureStore({
@@ -12,6 +13,7 @@ export default function initializeStore(preloadedState = undefined) {
discussions: discussionsReducer,
pagesAndResources: pagesAndResourcesReducer,
models: modelsReducer,
live: liveReducer,
},
preloadedState,
});