Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: New SSO workflow skeleton #1032

Merged
merged 3 commits into from
Sep 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/components/forms/FormContextWrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
type FormWrapperProps<FormConfigData> = FormWorkflowProps<FormConfigData> & { formData: FormConfigData };

const FormContextWrapper = <FormConfigData extends unknown>({
workflowTitle,
formWorkflowConfig,
onClickOut,
formData,
Expand All @@ -32,7 +33,7 @@ const FormContextWrapper = <FormConfigData extends unknown>({
>
<FormWorkflow
{...{
formWorkflowConfig, onClickOut, isStepperOpen, dispatch,
workflowTitle, formWorkflowConfig, onClickOut, isStepperOpen, dispatch,
}}
/>
</FormContextProvider>
Expand Down
25 changes: 22 additions & 3 deletions src/components/forms/FormWorkflow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ export type FormWorkflowStep<FormData> = {
errHandler: FormWorkflowErrorHandler
) => Promise<boolean>;
nextButtonConfig: (FormData: FormData) => FormWorkflowButtonConfig<FormData>;
showBackButton?: boolean;
showCancelButton?: boolean;
};

export type FormWorkflowConfig<FormData> = {
Expand All @@ -61,14 +63,16 @@ export type FormWorkflowConfig<FormData> = {
};

export type FormWorkflowProps<FormConfigData> = {
workflowTitle: string;
formWorkflowConfig: FormWorkflowConfig<FormConfigData>;
onClickOut: (edited: boolean, msg?: string) => null;
onClickOut: (() => void) | ((edited?: boolean, msg?: string) => null);
dispatch: Dispatch<FormActionArguments>;
isStepperOpen: boolean;
};

// Modal container for multi-step forms
const FormWorkflow = <FormConfigData extends unknown>({
workflowTitle,
formWorkflowConfig,
onClickOut,
isStepperOpen,
Expand Down Expand Up @@ -150,6 +154,15 @@ const FormWorkflow = <FormConfigData extends unknown>({
}
};

const onBack = () => {
if (step?.index !== undefined) {
const previousStep: number = step.index - 1;
if (previousStep >= 0) {
dispatch(setStepAction({ step: formWorkflowConfig.steps[previousStep] }));
}
}
};

const stepBody = (currentStep: FormWorkflowStep<FormConfigData>) => {
if (currentStep) {
const FormComponent: DynamicComponent = currentStep?.formComponent;
Expand All @@ -175,6 +188,10 @@ const FormWorkflow = <FormConfigData extends unknown>({
}
}, [formFields]);

// Show back button only if showBackButton === true
const showBackButton = (step?.index !== undefined) && (step.index > 0) && step.showBackButton;
// Show cancel button by default
const showCancelButton = step?.showCancelButton === undefined || step?.showCancelButton;
return (
<>
<ConfigErrorModal
Expand All @@ -191,12 +208,13 @@ const FormWorkflow = <FormConfigData extends unknown>({
await step?.saveChanges(formFields as FormConfigData, setFormError);
onClickOut(true, SUBMIT_TOAST_MESSAGE);
}
onClickOut(false, 'No changes saved');
}}
/>

{formWorkflowConfig.steps && (
<FullscreenModal
title="New learning platform integration"
title={workflowTitle}
isOpen={isStepperOpen}
onClose={onCancel}
className="stepper-modal"
Expand All @@ -206,7 +224,8 @@ const FormWorkflow = <FormConfigData extends unknown>({
Help Center: Integrations
</HelpCenterButton>
<ActionRow.Spacer />
<Button variant="tertiary" onClick={onCancel}>Cancel</Button>
{showCancelButton && <Button variant="tertiary" onClick={onCancel}>Cancel</Button>}
{showBackButton && <Button variant="tertiary" onClick={onBack}>Back</Button>}
{nextButtonConfig && (
<Button onClick={onNext} disabled={awaitingAsyncAction}>
{nextButtonConfig.buttonText}
Expand Down
3 changes: 3 additions & 0 deletions src/components/forms/ValidatedFormControl.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,15 @@ import { Form } from '@edx/paragon';
import { setFormFieldAction } from './data/actions';
import { useFormContext } from './FormContext';

// TODO: Get from Paragon
type InheritedParagonControlProps = {
autoFocus?: boolean;
className?: string;
type: string;
as?: string;
maxLength?: number;
floatingLabel?: string;
rows?: number;
};

export type ValidatedFormControlProps = {
Expand Down
3 changes: 2 additions & 1 deletion src/components/forms/ValidatedFormRadio.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export type ValidatedFormRadioProps = {
fieldInstructions?: string;
label?: string;
options?: string[][];
isInline?: boolean;
} & InheritedParagonRadioProps;

const ValidatedFormRadio = (props: ValidatedFormRadioProps) => {
Expand Down Expand Up @@ -69,7 +70,7 @@ const ValidatedFormRadio = (props: ValidatedFormRadioProps) => {
<Form.RadioSet
name={formRadioProps.id}
onChange={formRadioProps.onChange}
isInline
isInline={formRadioProps.isInline}
>
{createOptions(formRadioProps.options)}
</Form.RadioSet>
Expand Down
1 change: 1 addition & 0 deletions src/components/settings/SettingsLMSTab/LMSConfigPage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ const LMSConfigPage = ({
return (
<div>
<FormContextWrapper
workflowTitle="New learning platform integration"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is this getting added to the LMS settings tab?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The FormWorkflow originally had this hard-coded, as it was only used in the LMS config stepper.

formWorkflowConfig={formWorkflowConfig}
onClickOut={handleCloseWorkflow}
formData={existingConfigFormData}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@ const SAPConfigEnablePage = () => (
formId={formFieldNames.USER_TYPE}
label="SAP User Type"
options={[['User', 'user'], ['Admin', 'admin']]}
isInline
/>
</Form.Group>
</Form>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ function testBlackboardConfigSetup(formData) {
},
},
})}
workflowTitle="New learning platform integration"
onClickOut={mockOnClick}
formData={formData}
isStepperOpen
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ function testCanvasConfigSetup(formData) {
},
},
})}
workflowTitle="New learning platform integration"
onClickOut={mockOnClick}
formData={formData}
isStepperOpen
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ function testDegreedConfigSetup(formData) {
},
},
})}
workflowTitle="New learning platform integration"
onClickOut={mockOnClick}
formData={formData}
isStepperOpen
Expand Down
30 changes: 29 additions & 1 deletion src/components/settings/SettingsSSOTab/NewSSOStepper.jsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,31 @@
const NewSSOStepper = () => null;
import React, { useState, useContext } from 'react';
import FormContextWrapper from '../../forms/FormContextWrapper';
import { SSOConfigContext } from './SSOConfigContext';
import SSOFormWorkflowConfig from './SSOFormWorkflowConfig';

const NewSSOStepper = () => {
const {
setProviderConfig,
} = useContext(SSOConfigContext);
const [isStepperOpen, setIsStepperOpen] = useState(true);
const handleCloseWorkflow = () => {
setProviderConfig?.(null);
setIsStepperOpen(false);
};

return (isStepperOpen
&& (
<div>
<FormContextWrapper
workflowTitle="New SSO integration"
formWorkflowConfig={SSOFormWorkflowConfig()}
onClickOut={handleCloseWorkflow}
formData={{}}
isStepperOpen={isStepperOpen}
/>
</div>
)
);
};

export default NewSSOStepper;
59 changes: 59 additions & 0 deletions src/components/settings/SettingsSSOTab/SSOFormWorkflowConfig.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import type { FormWorkflowStep } from '../../forms/FormWorkflow';
import SSOConfigConnectStep from './steps/NewSSOConfigConnectStep';
import SSOConfigConfigureStep from './steps/NewSSOConfigConfigureStep';
import SSOConfigAuthorizeStep from './steps/NewSSOConfigAuthorizeStep';
import SSOConfigConfirmStep from './steps/NewSSOConfigConfirmStep';

type SSOConfigCamelCase = {};

export const SSOFormWorkflowConfig = () => {
const placeHolderButton = (buttonName?: string) => () => ({
buttonText: buttonName || 'Next',
opensNewWindow: false,
onClick: () => {},
});

const steps: FormWorkflowStep<SSOConfigCamelCase>[] = [
{
index: 0,
formComponent: SSOConfigConnectStep,
validations: [],
stepName: 'Connect',
nextButtonConfig: placeHolderButton(),
}, {
index: 1,
formComponent: SSOConfigConfigureStep,
validations: [],
stepName: 'Configure',
nextButtonConfig: placeHolderButton('Configure'),
showBackButton: true,
showCancelButton: false,
}, {
index: 2,
formComponent: SSOConfigAuthorizeStep,
validations: [],
stepName: 'Authorize',
nextButtonConfig: placeHolderButton(),
showBackButton: true,
showCancelButton: false,
}, {
index: 3,
formComponent: SSOConfigConfirmStep,
validations: [],
stepName: 'Confirm and Test',
nextButtonConfig: placeHolderButton('Finish'),
showBackButton: true,
showCancelButton: false,
},
];

// Start at the first step
const getCurrentStep = () => steps[0];

return {
getCurrentStep,
steps,
};
};

export default SSOFormWorkflowConfig;
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import React from 'react';
import {
Alert, Form, Hyperlink, Button, Row,
} from '@edx/paragon';
import { Info, Download } from '@edx/paragon/icons';

const handleCheck = () => null;

const SSOConfigAuthorizeStep = () => (
<>
<h2>Authorize edX as a Service Provider</h2>
<Alert variant="info" className="mb-4" icon={Info}>
<h3>Action required in a new window</h3>
Return to this window after completing the following steps in a new window to finish configuring your integration.
</Alert>
<hr />
<p>
1. Download the edX Service Provider metadata as an XML file:
</p>

<Row className="justify-content-center mb-4 ">
<Button variant="primary" iconAfter={Download}>edX Service Provider Metadata</Button>
</Row>

<p>
2. <Hyperlink destination="/" target="_blank">Launch a new window</Hyperlink> and upload the XML file to the list of
authorized SAML Service Providers on your Identity Provider&apos;s portal or website.
</p>
<hr />
<p>Return to this window and check the box once complete</p>

<Form.Checkbox className="mt-4" checked={false} onChange={handleCheck}>
I have authorized edX as a Service Provider
</Form.Checkbox>
</>
);

export default SSOConfigAuthorizeStep;
Loading