feat: update the pii sharing flow for live app (#300)
This commit is contained in:
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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));
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user