Skip to content

Commit

Permalink
feat: changing blackboard to tsx (#976)
Browse files Browse the repository at this point in the history
  • Loading branch information
kiram15 committed Mar 13, 2023
1 parent 68d7ed9 commit 4773e32
Show file tree
Hide file tree
Showing 10 changed files with 759 additions and 671 deletions.
4 changes: 2 additions & 2 deletions src/components/forms/FormWaitModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,15 @@ const FormWaitModal = ({

return (
<AlertModal title={header} isOpen={isOpen} onClose={onClose} hasCloseButton>
<div className="d-flex justify-content-center">
<div className="d-flex mt-2 justify-content-center">
<Spinner
screenReaderText={text}
animation="border"
className="mr-2"
variant="primary"
/>
</div>
<p>{text}</p>
<p className="mt-3">{text}</p>
</AlertModal>
);
};
Expand Down
2 changes: 1 addition & 1 deletion src/components/forms/FormWorkflow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ function FormWorkflow<FormData>({
disabled={hasErrors || awaitingAsyncAction}
>
{nextButtonConfig.buttonText}
{nextButtonConfig.opensNewWindow && <Launch />}
{nextButtonConfig.opensNewWindow && <Launch className="ml-1" />}
</Button>
)}
</span>
Expand Down
20 changes: 13 additions & 7 deletions src/components/settings/SettingsLMSTab/LMSConfigPage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
MOODLE_TYPE,
SAP_TYPE,
} from '../data/constants';
import BlackboardConfig from './LMSConfigs/BlackboardConfig';
import { BlackboardFormConfig } from './LMSConfigs/Blackboard/BlackboardConfig.tsx';
import { CanvasFormConfig } from './LMSConfigs/Canvas/CanvasConfig.tsx';
import CornerstoneConfig from './LMSConfigs/CornerstoneConfig';
import DegreedConfig from './LMSConfigs/DegreedConfig';
Expand All @@ -24,6 +24,7 @@ import FormContextWrapper from '../../forms/FormContextWrapper.tsx';

// TODO: Add remaining configs
const flowConfigs = {
[BLACKBOARD_TYPE]: BlackboardFormConfig,
[CANVAS_TYPE]: CanvasFormConfig,
};

Expand All @@ -50,12 +51,17 @@ const LMSConfigPage = ({
</h3>
{/* TODO: Replace giant switch */}
{LMSType === BLACKBOARD_TYPE && (
<BlackboardConfig
enterpriseCustomerUuid={enterpriseCustomerUuid}
onClick={onClick}
existingData={existingConfigFormData}
existingConfigs={existingConfigs}
setExistingConfigFormData={setExistingConfigFormData}
<FormContextWrapper
formWorkflowConfig={flowConfigs[BLACKBOARD_TYPE]({
enterpriseCustomerUuid,
onSubmit: setExistingConfigFormData,
onClickCancel: handleCloseWorkflow,
existingData: existingConfigFormData,
existingConfigNames: existingConfigs,
})}
onClickOut={handleCloseWorkflow}
onSubmit={setExistingConfigFormData}
formData={existingConfigFormData}
/>
)}
{LMSType === CANVAS_TYPE && (
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,286 @@
import handleErrors from "../../../utils";
import LmsApiService from "../../../../../data/services/LmsApiService";
import { camelCaseDict, snakeCaseDict } from "../../../../../utils";
import {
BLACKBOARD_OAUTH_REDIRECT_URL,
LMS_CONFIG_OAUTH_POLLING_INTERVAL,
LMS_CONFIG_OAUTH_POLLING_TIMEOUT,
SUBMIT_TOAST_MESSAGE,
} from "../../../data/constants";
// @ts-ignore
import BlackboardConfigActivatePage from "./BlackboardConfigActivatePage.tsx";
import BlackboardConfigAuthorizePage, {
validations,
formFieldNames
// @ts-ignore
} from "./BlackboardConfigAuthorizePage.tsx";
import type {
FormWorkflowButtonConfig,
FormWorkflowConfig,
FormWorkflowStep,
FormWorkflowHandlerArgs,
FormWorkflowErrorHandler,
} from "../../../../forms/FormWorkflow";
// @ts-ignore
import { WAITING_FOR_ASYNC_OPERATION } from "../../../../forms/FormWorkflow.tsx";
import {
setWorkflowStateAction,
updateFormFieldsAction,
// @ts-ignore
} from "../../../../forms/data/actions.ts";
import type {
FormFieldValidation,
} from "../../../../forms/FormContext";

export type BlackboardConfigCamelCase = {
blackboardAccountId: string;
blackboardBaseUrl: string;
displayName: string;
clientId: string;
clientSecret: string;
id: string;
active: boolean;
uuid: string;
refreshToken: string;
};

// TODO: Can we generate this dynamically?
// https://www.typescriptlang.org/docs/handbook/2/mapped-types.html
export type BlackboardConfigSnakeCase = {
blackboard_base_url: string;
display_name: string;
id: string;
active: boolean;
uuid: string;
enterprise_customer: string;
refresh_token: string;
};

// TODO: Make this a generic type usable by all lms configs
export type BlackboardFormConfigProps = {
enterpriseCustomerUuid: string;
existingData: BlackboardConfigCamelCase;
existingConfigNames: string[];
onSubmit: (blackboardConfig: BlackboardConfigCamelCase) => void;
onClickCancel: (submitted: boolean, status: string) => Promise<boolean>;
};

export const LMS_AUTHORIZATION_FAILED = "LMS AUTHORIZATION FAILED";

export const BlackboardFormConfig = ({
enterpriseCustomerUuid,
onSubmit,
onClickCancel,
existingData,
existingConfigNames,
}: BlackboardFormConfigProps): FormWorkflowConfig<BlackboardConfigCamelCase> => {
const configNames: string[] = existingConfigNames?.filter( (name) => name !== existingData.displayName);
const checkForDuplicateNames: FormFieldValidation = {
formFieldId: formFieldNames.DISPLAY_NAME,
validator: (formFields: BlackboardConfigCamelCase) => {
return configNames?.includes(formFields.displayName)
? "Display name already taken"
: false;
},
};

const saveChanges = async (
formFields: BlackboardConfigCamelCase,
errHandler: (errMsg: string) => void
) => {
const transformedConfig: BlackboardConfigSnakeCase = snakeCaseDict(
formFields
) as BlackboardConfigSnakeCase;
transformedConfig.enterprise_customer = enterpriseCustomerUuid;
let err = "";

if (formFields.id) {
try {
transformedConfig.active = existingData.active;
await LmsApiService.updateBlackboardConfig(
transformedConfig,
existingData.id
);
onSubmit(formFields);
} catch (error) {
err = handleErrors(error);
}
} else {
try {
transformedConfig.active = false;
await LmsApiService.postNewBlackboardConfig(transformedConfig);
onSubmit(formFields);
} catch (error) {
err = handleErrors(error);
}
}

if (err) {
errHandler(err);
}
return !err;
};

const handleSubmit = async ({
formFields,
formFieldsChanged,
errHandler,
dispatch,
}: FormWorkflowHandlerArgs<BlackboardConfigCamelCase>) => {
let currentFormFields = formFields;
const transformedConfig: BlackboardConfigSnakeCase = snakeCaseDict(
formFields
) as BlackboardConfigSnakeCase;
transformedConfig.enterprise_customer = enterpriseCustomerUuid;
let err = "";
if (formFieldsChanged) {
if (currentFormFields?.id) {
try {
transformedConfig.active = existingData.active;
const response = await LmsApiService.updateBlackboardConfig(
transformedConfig,
existingData.id
);
currentFormFields = camelCaseDict(
response.data
) as BlackboardConfigCamelCase;
onSubmit(currentFormFields);
dispatch?.(updateFormFieldsAction({ formFields: currentFormFields }));
} catch (error) {
err = handleErrors(error);
}
} else {
try {
transformedConfig.active = false;
const response = await LmsApiService.postNewBlackboardConfig(
transformedConfig
);
currentFormFields = camelCaseDict(
response.data
) as BlackboardConfigCamelCase;
onSubmit(currentFormFields);
dispatch?.(updateFormFieldsAction({ formFields: currentFormFields }));
} catch (error) {
err = handleErrors(error);
}
}
}
if (err) {
errHandler?.(err);
} else if (currentFormFields && !currentFormFields?.refreshToken) {
let appKey = existingData.clientId;
let configUuid = existingData.uuid;
if (!appKey || !configUuid) {
try {
const response = await LmsApiService.fetchBlackboardGlobalConfig();
appKey = response.data.results[response.data.results.length - 1].app_key;
configUuid = response.data.uuid;
} catch (error) {
err = handleErrors(error);
}
}
const oauthUrl = `${currentFormFields.blackboardBaseUrl}/learn/api/public/v1/oauth2/authorizationcode?`
+ `redirect_uri=${BLACKBOARD_OAUTH_REDIRECT_URL}&scope=read%20write%20delete%20offline&`
+ `response_type=code&client_id=${appKey}&state=${configUuid}`;
window.open(oauthUrl);

// Open the oauth window for the user
window.open(oauthUrl);
dispatch?.(setWorkflowStateAction(WAITING_FOR_ASYNC_OPERATION, true));
}
return currentFormFields;
};

const awaitAfterSubmit = async ({
formFields,
errHandler,
dispatch,
}: FormWorkflowHandlerArgs<BlackboardConfigCamelCase>) => {
if (formFields?.id) {
let err = "";
try {
const response = await LmsApiService.fetchSingleBlackboardConfig(
formFields.id
);
if (response.data.refresh_token) {
dispatch?.(
setWorkflowStateAction(WAITING_FOR_ASYNC_OPERATION, false)
);
return true;
}
} catch (error) {
err = handleErrors(error);
}
if (err) {
errHandler?.(err);
return false;
}
}

return false;
};

const onAwaitTimeout = async ({
dispatch,
}: FormWorkflowHandlerArgs<BlackboardConfigCamelCase>) => {
dispatch?.(setWorkflowStateAction(WAITING_FOR_ASYNC_OPERATION, false));
dispatch?.(setWorkflowStateAction(LMS_AUTHORIZATION_FAILED, true));
};

const steps: FormWorkflowStep<BlackboardConfigCamelCase>[] = [
{
index: 0,
formComponent: BlackboardConfigAuthorizePage,
validations: validations.concat([checkForDuplicateNames]),
stepName: "Authorize",
saveChanges,
nextButtonConfig: (formFields: BlackboardConfigCamelCase) => {
let config = {
buttonText: "Authorize",
opensNewWindow: false,
onClick: handleSubmit,
};
if (!formFields.refreshToken) {
config = {
...config,
...{
opensNewWindow: true,
awaitSuccess: {
awaitCondition: awaitAfterSubmit,
awaitInterval: LMS_CONFIG_OAUTH_POLLING_INTERVAL,
awaitTimeout: LMS_CONFIG_OAUTH_POLLING_TIMEOUT,
onAwaitTimeout: onAwaitTimeout,
},
},
};
}
return config as FormWorkflowButtonConfig<BlackboardConfigCamelCase>;
},
},
{
index: 1,
formComponent: BlackboardConfigActivatePage,
validations: [],
stepName: "Activate",
saveChanges,
nextButtonConfig: () => ({
buttonText: "Activate",
opensNewWindow: false,
onClick: () => {
onClickCancel(true, SUBMIT_TOAST_MESSAGE);
return Promise.resolve(existingData);
},
}),
},
];

// Go to authorize step for now
const getCurrentStep = () => steps[0];

return {
getCurrentStep,
steps,
};
};

export default BlackboardFormConfig;
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import React from 'react';

import { Form } from '@edx/paragon';

// Page 3 of Blackboard LMS config workflow
const BlackboardConfigActivatePage = () => (
<span>
<Form style={{ maxWidth: '60rem' }}>
<h2>Activate your Blackboard integration</h2>

<p>
Your Blackboard integration has been successfully authorized and is ready to
activate!
</p>

<p>
Once activated, edX For Business will begin syncing content metadata and
learner activity with Blackboard.
</p>
</Form>
</span>
);

export default BlackboardConfigActivatePage;
Loading

0 comments on commit 4773e32

Please sign in to comment.