feat: update the pii sharing flow for live app (#300)

This commit is contained in:
Awais Ansari
2022-05-13 15:22:18 +05:00
committed by GitHub
parent b8956535f2
commit bfe3442ebe
7 changed files with 357 additions and 266 deletions

View File

@@ -231,7 +231,7 @@ function AppSettingsModal({
<div className="d-flex align-items-center">
{enableAppLabel}
{formikProps.values.enabled && (
<Badge className="ml-2" variant="success">
<Badge className="ml-2" variant="success" data-testid="enable-badge">
{intl.formatMessage(messages.enabled)}
</Badge>
)}

View File

@@ -5,10 +5,15 @@ 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 { fetchLiveData, saveLiveConfiguration } from './data/thunks';
import { fetchLiveData, saveLiveConfiguration, saveLiveConfigurationAsDraft } from './data/thunks';
import { selectApp } from './data/slice';
import FormikControl from '../../generic/FormikControl';
import AppSettingsModal from '../app-settings-modal/AppSettingsModal';
import { useModel } from '../../generic/model-store';
import Loading from '../../generic/Loading';
import iconsSrc from './constants';
import { RequestStatus } from '../../data/constants';
import messages from './messages';
function LiveSettings({
@@ -17,12 +22,25 @@ function LiveSettings({
}) {
const dispatch = useDispatch();
const courseId = useSelector(state => state.courseDetail.courseId);
const liveConfiguration = useSelector((state) => state.live.configuration);
const availableProviders = useSelector((state) => state.live.appIds);
const {
piiSharingAllowed, selectedAppId, enabled, status,
} = useSelector(state => state.live);
useEffect(() => {
dispatch(fetchLiveData(courseId));
}, [courseId]);
const appConfig = useModel('liveAppConfigs', selectedAppId);
const app = useModel('liveApps', selectedAppId);
const liveConfiguration = {
enabled: enabled || false,
consumerKey: appConfig?.consumerKey || '',
consumerSecret: appConfig?.consumerSecret || '',
launchUrl: appConfig?.launchUrl || '',
launchEmail: appConfig?.launchEmail || '',
provider: selectedAppId || 'zoom',
piiSharingEnable: piiSharingAllowed || false,
piiSharingUsername: app?.piiSharing.username || false,
piiSharingEmail: app?.piiSharing.email || false,
};
const validationSchema = {
enabled: Yup.boolean(),
@@ -32,91 +50,107 @@ function LiveSettings({
launchEmail: Yup.string().required(intl.formatMessage(messages.launchEmailRequired)),
};
const handleProviderChange = (providerId, setFieldValue, values) => {
dispatch(saveLiveConfigurationAsDraft(values));
dispatch(selectApp({ appId: providerId }));
setFieldValue('provider', providerId);
};
const handleSettingsSave = async (values) => {
await dispatch(saveLiveConfiguration(courseId, values));
};
const handleProviderChange = (selectedProvider, setFieldValue) => {
setFieldValue('provider', selectedProvider);
};
useEffect(() => {
dispatch(fetchLiveData(courseId));
}, [courseId]);
return (
<AppSettingsModal
appId="live"
title={intl.formatMessage(messages.heading)}
enableAppHelp={intl.formatMessage(messages.enableLiveHelp)}
enableAppLabel={intl.formatMessage(messages.enableLiveLabel)}
learnMoreText={intl.formatMessage(messages.enableLiveLink)}
onClose={onClose}
initialValues={liveConfiguration}
validationSchema={validationSchema}
onSettingsSave={handleSettingsSave}
configureBeforeEnable
enableReinitialize
>
{
({ values, setFieldValue }) => (
<>
<AppSettingsModal
appId="live"
title={intl.formatMessage(messages.heading)}
enableAppHelp={intl.formatMessage(messages.enableLiveHelp)}
enableAppLabel={intl.formatMessage(messages.enableLiveLabel)}
learnMoreText={intl.formatMessage(messages.enableLiveLink)}
onClose={onClose}
initialValues={liveConfiguration}
validationSchema={validationSchema}
onSettingsSave={handleSettingsSave}
configureBeforeEnable
enableReinitialize
>
{({ values, setFieldValue }) => (
<>
<h4 className="my-3">{intl.formatMessage(messages.selectProvider)}</h4>
<SelectableBox.Set
type="checkbox"
value={values.provider}
onChange={(event) => handleProviderChange(event.target.value, setFieldValue)}
name="provider"
columns={3}
className="mb-3"
>
{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} />
<span>{intl.formatMessage(messages[`appName-${camelCase(app.id)}`])}</span>
</div>
</SelectableBox>
))}
</SelectableBox.Set>
{values.piiSharingEnable ? (
<>
<p data-testid="helper-text">
{intl.formatMessage(messages.providerHelperText, { providerName: values.provider })}
</p>
<p className="pb-2">{intl.formatMessage(messages.formInstructions)}</p>
<FormikControl
name="consumerKey"
value={values.consumerKey}
floatingLabel={intl.formatMessage(messages.consumerKey)}
className="pb-1"
type="input"
/>
<FormikControl
name="consumerSecret"
value={values.consumerSecret}
floatingLabel={intl.formatMessage(messages.consumerSecret)}
className="pb-1"
type="input"
/>
<FormikControl
name="launchUrl"
value={values.launchUrl}
floatingLabel={intl.formatMessage(messages.launchUrl)}
className="pb-1"
type="input"
/>
<FormikControl
name="launchEmail"
value={values.launchEmail}
floatingLabel={intl.formatMessage(messages.launchEmail)}
type="input"
/>
</>
) : (
<p data-testid="request-pii-sharing">
{intl.formatMessage(messages.requestPiiSharingEnable, { provider: values.provider })}
</p>
)}
{(status === RequestStatus.IN_PROGRESS) ? (
<Loading />
) : (
<>
<h4 className="my-3">{intl.formatMessage(messages.selectProvider)}</h4>
<SelectableBox.Set
type="checkbox"
value={values.provider}
onChange={(event) => handleProviderChange(event.target.value, setFieldValue, values)}
name="provider"
columns={3}
className="mb-3"
>
{availableProviders.map((provider) => (
<SelectableBox value={provider} type="checkbox" key={provider}>
<div className="d-flex flex-column align-items-center">
<Icon src={iconsSrc[`${camelCase(provider)}`]} alt={provider} />
<span>{intl.formatMessage(messages[`appName-${camelCase(provider)}`])}</span>
</div>
</SelectableBox>
))}
</SelectableBox.Set>
{(!values.piiSharingEnable && (values.piiSharingEmail || values.piiSharingUsername)) ? (
<p data-testid="request-pii-sharing">
{intl.formatMessage(messages.requestPiiSharingEnable, { provider: values.provider })}
</p>
) : (
<>
{(values.piiSharingEmail || values.piiSharingUsername)
&& (
<p data-testid="helper-text">
{intl.formatMessage(messages.providerHelperText, { providerName: values.provider })}
</p>
)}
<p className="pb-2">{intl.formatMessage(messages.formInstructions)}</p>
<FormikControl
name="consumerKey"
value={values.consumerKey}
floatingLabel={intl.formatMessage(messages.consumerKey)}
className="pb-1"
type="input"
/>
<FormikControl
name="consumerSecret"
value={values.consumerSecret}
floatingLabel={intl.formatMessage(messages.consumerSecret)}
className="pb-1"
type="input"
/>
<FormikControl
name="launchUrl"
value={values.launchUrl}
floatingLabel={intl.formatMessage(messages.launchUrl)}
className="pb-1"
type="input"
/>
<FormikControl
name="launchEmail"
value={values.launchEmail}
floatingLabel={intl.formatMessage(messages.launchEmail)}
type="input"
/>
</>
)}
</>
)}
</>
)
}
</AppSettingsModal>
)}
</AppSettingsModal>
</>
);
}

View File

@@ -6,6 +6,8 @@ import {
queryByRole,
queryByTestId,
queryByText,
getByRole,
waitForElementToBeRemoved,
} from '@testing-library/react';
import { Switch } from 'react-router-dom';
@@ -22,10 +24,11 @@ import {
generateLiveConfigurationApiResponse,
courseId,
initialState,
configurationProviders,
} from './factories/mockApiResponses';
import { fetchLiveConfiguration } from './data/thunks';
import { providerConfigurationApiUrl } from './data/api';
import { fetchLiveConfiguration, fetchLiveProviders } from './data/thunks';
import { providerConfigurationApiUrl, providersApiUrl } from './data/api';
import messages from './messages';
import PagesAndResourcesProvider from '../PagesAndResourcesProvider';
@@ -51,6 +54,22 @@ const renderComponent = () => {
container = wrapper.container;
};
const mockStore = async ({
usernameSharing = false,
emailSharing = false,
enabled = true,
piiSharingAllowed = true,
}) => {
const fetchProviderConfigUrl = `${providersApiUrl}/${courseId}/`;
const fetchLiveConfigUrl = `${providerConfigurationApiUrl}/${courseId}/`;
axiosMock.onGet(fetchProviderConfigUrl).reply(200, configurationProviders(emailSharing, usernameSharing));
axiosMock.onGet(fetchLiveConfigUrl).reply(200, generateLiveConfigurationApiResponse(enabled, piiSharingAllowed));
await executeThunk(fetchLiveProviders(courseId), store.dispatch);
await executeThunk(fetchLiveConfiguration(courseId), store.dispatch);
};
describe('LiveSettings', () => {
beforeEach(async () => {
initializeMockApp({
@@ -80,30 +99,25 @@ describe('LiveSettings', () => {
expect(headingElement).toHaveTextContent(messages.heading.defaultMessage);
});
test('Displays title, helper and badge when live configuration button is enabled', async () => {
test('Displays title, helper text and badge when live configuration button is enabled', async () => {
await mockStore({ enabled: true });
renderComponent();
const label = container.querySelector('label[for="enable-live-toggle"]');
const helperText = queryByTestId(container, 'helper-text');
const helperText = container.querySelector('#enable-live-toggleHelpText');
const enableBadge = queryByTestId(container, 'enable-badge');
expect(label).toHaveTextContent(messages.enableLiveLabel.defaultMessage);
expect(label.firstChild).toHaveTextContent('Enabled');
expect(helperText).toHaveTextContent(
messages.providerHelperText.defaultMessage.replace('{providerName}', 'zoom'),
);
expect(enableBadge).toHaveTextContent('Enabled');
expect(helperText).toHaveTextContent(messages.enableLiveHelp.defaultMessage);
});
test('Displays title, helper text and hides badge when live configuration button is disabled', async () => {
const fetchProviderConfigUrl = `${providerConfigurationApiUrl}/${courseId}/`;
axiosMock.onGet(fetchProviderConfigUrl).reply(
200,
generateLiveConfigurationApiResponse(false, false),
);
await executeThunk(fetchLiveConfiguration(courseId), store.dispatch);
await mockStore({ enabled: false, piiSharingAllowed: false });
renderComponent();
const label = container.querySelector('label[for="enable-live-toggle"]');
const helperText = queryByText(container, messages.enableLiveHelp.defaultMessage);
const helperText = container.querySelector('#enable-live-toggleHelpText');
expect(label).toHaveTextContent('Live');
expect(label.firstChild).not.toHaveTextContent('Enabled');
@@ -111,33 +125,33 @@ describe('LiveSettings', () => {
});
test('Displays provider heading, helper text and all providers', async () => {
const fetchProviderConfigUrl = `${providerConfigurationApiUrl}/${courseId}/`;
axiosMock.onGet(fetchProviderConfigUrl).reply(
200,
generateLiveConfigurationApiResponse(false, true),
);
await executeThunk(fetchLiveConfiguration(courseId), store.dispatch);
await mockStore({
enabled: true,
piiSharingAllowed: true,
usernameSharing: false,
emailSharing: true,
});
renderComponent();
const spinner = getByRole(container, 'status');
await waitForElementToBeRemoved(spinner);
const providers = queryByRole(container, 'group');
const helperText = queryByTestId(container, 'helper-text');
expect(providers.childElementCount).toBe(1);
expect(providers.childElementCount).toBe(2);
expect(providers).toHaveTextContent('Zoom');
expect(helperText).toHaveTextContent(
messages.providerHelperText.defaultMessage.replace('{providerName}', 'zoom'),
);
});
test('Only helper text and lti fields are visible when pii sharing is enabled', async () => {
const fetchProviderConfigUrl = `${providerConfigurationApiUrl}/${courseId}/`;
axiosMock.onGet(fetchProviderConfigUrl).reply(
200,
generateLiveConfigurationApiResponse(),
);
await executeThunk(fetchLiveConfiguration(courseId), store.dispatch);
test('LTI fields are visible when pii sharing is enabled and email or username sharing required', async () => {
await mockStore({ emailSharing: true });
renderComponent();
const spinner = getByRole(container, 'status');
await waitForElementToBeRemoved(spinner);
const consumerKey = container.querySelector('input[name="consumerKey"]').parentElement;
const consumerSecret = container.querySelector('input[name="consumerSecret"]').parentElement;
const launchUrl = container.querySelector('input[name="launchUrl"]').parentElement;
@@ -153,53 +167,39 @@ describe('LiveSettings', () => {
expect(launchEmail.lastChild).toHaveTextContent(messages.launchEmail.defaultMessage);
});
test('Only connect to support is visible when pii sharing is disabled', async () => {
const fetchProviderConfigUrl = `${providerConfigurationApiUrl}/${courseId}/`;
axiosMock.onGet(fetchProviderConfigUrl).reply(
200,
generateLiveConfigurationApiResponse(false, false),
);
await executeThunk(fetchLiveConfiguration(courseId), store.dispatch);
renderComponent();
test(
'Only connect to support message is visible when pii sharing is disabled and email or username sharing is required',
async () => {
await mockStore({ emailSharing: true, piiSharingAllowed: false });
renderComponent();
const requestPiiText = queryByTestId(container, 'request-pii-sharing');
const consumerKey = container.querySelector('input[name="consumerKey"]');
const consumerSecret = container.querySelector('input[name="consumerSecret"]');
const launchUrl = container.querySelector('input[name="launchUrl"]');
const launchEmail = container.querySelector('input[name="launchEmail"]');
const spinner = getByRole(container, 'status');
await waitForElementToBeRemoved(spinner);
expect(requestPiiText).toHaveTextContent(
messages.requestPiiSharingEnable.defaultMessage.replaceAll('{provider}', 'zoom'),
);
expect(consumerKey).not.toBeInTheDocument();
expect(consumerSecret).not.toBeInTheDocument();
expect(launchUrl).not.toBeInTheDocument();
expect(launchEmail).not.toBeInTheDocument();
});
const requestPiiText = queryByTestId(container, 'request-pii-sharing');
const consumerKey = container.querySelector('input[name="consumerKey"]');
const consumerSecret = container.querySelector('input[name="consumerSecret"]');
const launchUrl = container.querySelector('input[name="launchUrl"]');
const launchEmail = container.querySelector('input[name="launchEmail"]');
test('Form should be submitted and closed when valid data is provided', async () => {
const fetchProviderConfigUrl = `${providerConfigurationApiUrl}/${courseId}/`;
const apiDefaultResponse = generateLiveConfigurationApiResponse();
axiosMock.onPost(fetchProviderConfigUrl, apiDefaultResponse).reply(200, apiDefaultResponse);
axiosMock.onGet(fetchProviderConfigUrl).reply(200, apiDefaultResponse);
await executeThunk(fetchLiveConfiguration(courseId), store.dispatch);
renderComponent();
const saveButton = queryByText(container, 'Save');
await waitFor(async () => {
await act(async () => fireEvent.click(saveButton));
expect(queryByRole(container, 'dialog')).not.toBeInTheDocument();
});
});
expect(requestPiiText).toHaveTextContent(
messages.requestPiiSharingEnable.defaultMessage.replaceAll('{provider}', 'zoom'),
);
expect(consumerKey).not.toBeInTheDocument();
expect(consumerSecret).not.toBeInTheDocument();
expect(launchUrl).not.toBeInTheDocument();
expect(launchEmail).not.toBeInTheDocument();
},
);
test('Provider Configuration should be displayed correctly', async () => {
const fetchProviderConfigUrl = `${providerConfigurationApiUrl}/${courseId}/`;
const apiDefaultResponse = generateLiveConfigurationApiResponse();
axiosMock.onGet(fetchProviderConfigUrl).reply(200, apiDefaultResponse);
await executeThunk(fetchLiveConfiguration(courseId), store.dispatch);
const apiDefaultResponse = generateLiveConfigurationApiResponse(true, true);
await mockStore({ emailSharing: false, piiSharingAllowed: false });
renderComponent();
const spinner = getByRole(container, 'status');
await waitForElementToBeRemoved(spinner);
const consumerKey = container.querySelector('input[name="consumerKey"]');
const consumerSecret = container.querySelector('input[name="consumerSecret"]');
const launchUrl = container.querySelector('input[name="launchUrl"]');
@@ -214,14 +214,14 @@ describe('LiveSettings', () => {
});
test('Unable to save error should be shown on submission if a field is empty', async () => {
const fetchProviderConfigUrl = `${providerConfigurationApiUrl}/${courseId}/`;
const apiDefaultResponse = generateLiveConfigurationApiResponse();
const apiDefaultResponse = generateLiveConfigurationApiResponse(true, true);
apiDefaultResponse.lti_configuration.lti_1p1_client_key = '';
axiosMock.onGet(fetchProviderConfigUrl).reply(200, apiDefaultResponse);
await executeThunk(fetchLiveConfiguration(courseId), store.dispatch);
await mockStore({ emailSharing: false, piiSharingAllowed: false });
renderComponent();
const spinner = getByRole(container, 'status');
await waitForElementToBeRemoved(spinner);
const saveButton = queryByText(container, 'Save');
await waitFor(async () => {

View File

@@ -1,5 +1,5 @@
/* eslint-disable import/prefer-default-export */
import { camelCaseObject, ensureConfig, getConfig } from '@edx/frontend-platform';
import { ensureConfig, getConfig } from '@edx/frontend-platform';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
ensureConfig([
@@ -11,6 +11,80 @@ 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,
}));
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,
};
}
export function normalizeSettings(data) {
return {
enabled: data.enabled,
piiSharingAllowed: data.pii_sharing_allowed,
appConfig: {
id: data.provider_type,
...normalizeLtiConfig(data.lti_configuration),
},
};
}
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.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: ltiConfiguration,
provider_type: data?.provider || 'zoom',
pii_sharing_allowed: data?.piiSharingEnable || false,
};
return apiData;
}
/**
* Fetches providers for provided course
* @param {string} courseId
@@ -19,7 +93,8 @@ export const providerConfigurationApiUrl = `${apiBaseUrl}/api/course_live/course
export async function getLiveProviders(courseId) {
const { data } = await getAuthenticatedHttpClient()
.get(`${providersApiUrl}/${courseId}/`);
return camelCaseObject(data);
return normalizeProviders(data);
}
/**
@@ -30,13 +105,15 @@ export async function getLiveProviders(courseId) {
export async function getLiveConfiguration(courseId) {
const { data } = await getAuthenticatedHttpClient()
.get(`${providerConfigurationApiUrl}/${courseId}/`);
return camelCaseObject(data);
return normalizeSettings(data);
}
export async function postLiveConfiguration(courseId, config) {
const data = await getAuthenticatedHttpClient().post(
const { data } = await getAuthenticatedHttpClient().post(
`${providerConfigurationApiUrl}/${courseId}/`,
config,
deNormalizeSettings(config),
);
return camelCaseObject(data);
return normalizeSettings(data);
}

View File

@@ -5,30 +5,30 @@ import { RequestStatus } from '../../../data/constants';
const slice = createSlice({
name: 'live',
initialState: {
providers: {
available: {},
selectedProvider: {},
},
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,
configuration: {},
saveStatus: RequestStatus.SUCCESSFUL,
},
reducers: {
updateProviders: (state, { payload }) => {
Object.assign(state.providers, payload);
loadApps: (state, { payload }) => {
state.status = RequestStatus.SUCCESSFUL;
state.saveStatus = RequestStatus.SUCCESSFUL;
Object.assign(state, payload);
},
updateConfiguration: (state, { payload }) => {
Object.assign(state.configuration, payload);
state.configuredProvider = payload.provider;
selectApp: (state, { payload }) => {
const { appId } = payload;
state.selectedAppId = appId;
},
updateStatus: (state, { payload }) => {
const { status } = payload;
state.status = status;
},
updateAppIds: (state, { payload }) => {
state.appIds = payload;
},
updateSaveStatus: (state, { payload }) => {
const { status } = payload;
state.saveStatus = status;
@@ -37,11 +37,10 @@ const slice = createSlice({
});
export const {
updateProviders,
updateConfiguration,
loadApps,
selectApp,
updateStatus,
updateSaveStatus,
updateAppIds,
} = slice.actions;
export const {

View File

@@ -1,65 +1,42 @@
import { history } from '@edx/frontend-platform';
import { getLiveConfiguration, getLiveProviders, postLiveConfiguration } from './api';
import { addModel, addModels, updateModel } from '../../../generic/model-store';
import {
updateStatus, updateSaveStatus, updateProviders,
updateAppIds, updateConfiguration,
} from './slice';
getLiveConfiguration,
getLiveProviders,
postLiveConfiguration,
normalizeSettings,
deNormalizeSettings,
} from './api';
import { loadApps, updateStatus, updateSaveStatus } 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 || '',
},
},
function updateLiveSettingsState({
appConfig,
...liveSettings
}) {
return async (dispatch) => {
dispatch(addModel({ modelType: 'liveAppConfigs', model: appConfig }));
dispatch(loadApps(liveSettings));
};
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));
};
}
const { activeAppId, selectedAppId, apps } = await getLiveProviders(courseId);
function updateLiveConfigurationState(config) {
return async (dispatch) => {
const data = normalizeLiveConfig(config);
dispatch(updateConfiguration(data));
dispatch(addModels({ modelType: 'liveApps', models: apps }));
dispatch(loadApps({
activeAppId,
selectedAppId,
appIds: apps.map((app) => app.id),
}));
};
}
export function fetchLiveConfiguration(courseId) {
return async (dispatch) => {
const config = await getLiveConfiguration(courseId);
dispatch(updateLiveConfigurationState(config));
const settings = await getLiveConfiguration(courseId);
dispatch(updateLiveSettingsState(settings));
};
}
@@ -67,9 +44,8 @@ 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 }));
await dispatch(fetchLiveConfiguration(courseId));
} catch (error) {
if (error.response && error.response.status === 403) {
dispatch(updateStatus({ status: RequestStatus.DENIED }));
@@ -84,10 +60,9 @@ 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));
const apps = await postLiveConfiguration(courseId, config);
dispatch(updateLiveSettingsState(apps));
dispatch(updateSaveStatus({ status: RequestStatus.SUCCESSFUL }));
history.push(`/course/${courseId}/pages-and-resources/`);
} catch (error) {
@@ -100,3 +75,12 @@ export function saveLiveConfiguration(courseId, config) {
}
};
}
export function saveLiveConfigurationAsDraft(config) {
return async (dispatch) => {
const { appConfig, ...liveSettings } = normalizeSettings(deNormalizeSettings(config));
dispatch(updateModel({ modelType: 'liveAppConfigs', model: appConfig }));
dispatch(loadApps(liveSettings));
};
}

View File

@@ -31,44 +31,40 @@ export const initialState = {
},
},
},
live: {
providers: {
available: {
zoom: {
name: 'Zoom LTI PRO',
features: [],
},
},
selectedProvider: {},
active: 'zoom',
},
appIds: [
{
id: 'zoom',
},
],
status: 'successful',
configuration: {
courseKey: '',
enabled: true,
consumerKey: '',
consumerSecret: '',
launchUrl: '',
launchEmail: '',
provider: 'zoom',
piiSharingEnable: true,
},
saveStatus: 'successful',
configuredProvider: 'zoom',
},
};
export const configurationProviders = (
emailSharing,
usernameSharing,
) => ({
providers: {
active: 'zoom',
available: {
zoom: {
features: [],
name: 'Zoom LTI PRO',
pii_sharing: {
email: emailSharing,
username: usernameSharing,
},
},
googleMeet: {
features: [],
name: 'Google Meet',
pii_sharing: {
email: true,
username: true,
},
},
},
},
});
export const generateLiveConfigurationApiResponse = (
enabled = true,
piiSharingAllowed = true,
enabled,
piiSharingAllowed,
) => ({
course_key: courseId,
provider_type: 'zoom',
enabled,
lti_configuration: {
lti_1p1_client_key: 'consumer_key',
@@ -82,4 +78,5 @@ export const generateLiveConfigurationApiResponse = (
},
},
pii_sharing_allowed: piiSharingAllowed,
provider_type: 'zoom',
});