From bfe3442ebed906e44058f35117e4daf3d07aa039 Mon Sep 17 00:00:00 2001 From: Awais Ansari <79941147+awais-ansari@users.noreply.github.com> Date: Fri, 13 May 2022 15:22:18 +0500 Subject: [PATCH] feat: update the pii sharing flow for live app (#300) --- .../app-settings-modal/AppSettingsModal.jsx | 2 +- src/pages-and-resources/live/Settings.jsx | 200 ++++++++++-------- .../live/Settings.test.jsx | 148 ++++++------- src/pages-and-resources/live/data/api.js | 89 +++++++- src/pages-and-resources/live/data/slice.js | 31 ++- src/pages-and-resources/live/data/thunks.js | 90 ++++---- .../live/factories/mockApiResponses.jsx | 63 +++--- 7 files changed, 357 insertions(+), 266 deletions(-) diff --git a/src/pages-and-resources/app-settings-modal/AppSettingsModal.jsx b/src/pages-and-resources/app-settings-modal/AppSettingsModal.jsx index 710580511..8c89c43fa 100644 --- a/src/pages-and-resources/app-settings-modal/AppSettingsModal.jsx +++ b/src/pages-and-resources/app-settings-modal/AppSettingsModal.jsx @@ -231,7 +231,7 @@ function AppSettingsModal({
{enableAppLabel} {formikProps.values.enabled && ( - + {intl.formatMessage(messages.enabled)} )} diff --git a/src/pages-and-resources/live/Settings.jsx b/src/pages-and-resources/live/Settings.jsx index 90f380165..74fe099d5 100644 --- a/src/pages-and-resources/live/Settings.jsx +++ b/src/pages-and-resources/live/Settings.jsx @@ -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 ( - - { - ({ values, setFieldValue }) => ( + <> + + {({ values, setFieldValue }) => ( <> -

{intl.formatMessage(messages.selectProvider)}

- handleProviderChange(event.target.value, setFieldValue)} - name="provider" - columns={3} - className="mb-3" - > - {availableProviders.map((app) => ( - -
- - {intl.formatMessage(messages[`appName-${camelCase(app.id)}`])} -
-
- ))} -
- {values.piiSharingEnable ? ( - <> -

- {intl.formatMessage(messages.providerHelperText, { providerName: values.provider })} -

-

{intl.formatMessage(messages.formInstructions)}

- - - - - - ) : ( -

- {intl.formatMessage(messages.requestPiiSharingEnable, { provider: values.provider })} -

- )} + {(status === RequestStatus.IN_PROGRESS) ? ( + + ) : ( + <> +

{intl.formatMessage(messages.selectProvider)}

+ handleProviderChange(event.target.value, setFieldValue, values)} + name="provider" + columns={3} + className="mb-3" + > + {availableProviders.map((provider) => ( + +
+ + {intl.formatMessage(messages[`appName-${camelCase(provider)}`])} +
+
+ ))} +
+ {(!values.piiSharingEnable && (values.piiSharingEmail || values.piiSharingUsername)) ? ( +

+ {intl.formatMessage(messages.requestPiiSharingEnable, { provider: values.provider })} +

+ ) : ( + <> + {(values.piiSharingEmail || values.piiSharingUsername) + && ( +

+ {intl.formatMessage(messages.providerHelperText, { providerName: values.provider })} +

+ )} +

{intl.formatMessage(messages.formInstructions)}

+ + + + + + )} + + )} - ) - } -
+ )} +
+ ); } diff --git a/src/pages-and-resources/live/Settings.test.jsx b/src/pages-and-resources/live/Settings.test.jsx index 727079f44..a71d3886e 100644 --- a/src/pages-and-resources/live/Settings.test.jsx +++ b/src/pages-and-resources/live/Settings.test.jsx @@ -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 () => { diff --git a/src/pages-and-resources/live/data/api.js b/src/pages-and-resources/live/data/api.js index f73c55926..8daa0e77a 100644 --- a/src/pages-and-resources/live/data/api.js +++ b/src/pages-and-resources/live/data/api.js @@ -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); } diff --git a/src/pages-and-resources/live/data/slice.js b/src/pages-and-resources/live/data/slice.js index 8bc6a7013..69ccf6b66 100644 --- a/src/pages-and-resources/live/data/slice.js +++ b/src/pages-and-resources/live/data/slice.js @@ -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 { diff --git a/src/pages-and-resources/live/data/thunks.js b/src/pages-and-resources/live/data/thunks.js index f57021d5e..d9c473a36 100644 --- a/src/pages-and-resources/live/data/thunks.js +++ b/src/pages-and-resources/live/data/thunks.js @@ -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)); + }; +} diff --git a/src/pages-and-resources/live/factories/mockApiResponses.jsx b/src/pages-and-resources/live/factories/mockApiResponses.jsx index ab7f2c2fd..3579390be 100644 --- a/src/pages-and-resources/live/factories/mockApiResponses.jsx +++ b/src/pages-and-resources/live/factories/mockApiResponses.jsx @@ -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', });