feat: course live integration (#271)
Co-authored-by: adeel.tajamul <adeel.tajamul@arbisoft.com>
This commit is contained in:
committed by
GitHub
parent
2e486088f0
commit
3836b4328e
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
42
src/pages-and-resources/live/data/api.js
Normal file
42
src/pages-and-resources/live/data/api.js
Normal 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);
|
||||
}
|
||||
49
src/pages-and-resources/live/data/slice.js
Normal file
49
src/pages-and-resources/live/data/slice.js
Normal 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;
|
||||
102
src/pages-and-resources/live/data/thunks.js
Normal file
102
src/pages-and-resources/live/data/thunks.js
Normal 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 }));
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user