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

Initial server UI schema impl #3507

Draft
wants to merge 11 commits into
base: master
Choose a base branch
from
1 change: 1 addition & 0 deletions packages/@okta/i18n/src/properties/login.properties
Original file line number Diff line number Diff line change
Expand Up @@ -778,6 +778,7 @@ authbutton.divider.text = or
## Registration Default text
registration.signup.label=Don't have an account?
registration.signup.text=Sign up
register.account.label=Don't have an account? <$1>Sign up</$1>
registration.complete.title=Verification email sent
registration.complete.confirm.text=To finish signing in, check your email.
registration.form.title=Create Account
Expand Down
4 changes: 3 additions & 1 deletion playground/mocks/config/responseConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ const idx = {
],

'/idp/idx/introspect': [
'identify',
'identify-with-uischema',
// 'identify',
// 'identify-with-uischema',
// 'error-identify-multiple-errors',
// 'authenticator-enroll-ov-qr-enable-biometrics',
// 'authenticator-verification-okta-verify-push',
Expand Down
495 changes: 495 additions & 0 deletions playground/mocks/data/idp/idx/identify-with-uischema.json

Large diffs are not rendered by default.

8 changes: 8 additions & 0 deletions src/v3/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,19 @@
"@emotion/react": "^11.9.3",
"@emotion/styled": "^11.9.3",
"@hcaptcha/react-hcaptcha": "^1.10.1",
"@jsonforms/core": "^3.1.0",
"@jsonforms/react": "^3.1.0",
"@jsonforms/vanilla-renderers": "3.1.0",
"@jsonforms/material-renderers": "3.1.0",
"@mui/icons-material": "^5.8.4",
"@mui/lab": "^5.0.0-alpha.160",
"@mui/material": "^5.8.5",
"@okta/odyssey-design-tokens": "1.13.0",
"@okta/odyssey-react-mui": "1.13.0",
"@mui/x-date-pickers": "^6.18.7",
"@okta/okta-auth-js": "^7.4.3",
"ajv": "^8.12.0",
"ajv-errors": "^3.0.0",
"chroma-js": "^2.4.2",
"cross-fetch": "^3.1.5",
"dompurify": "^3.0.0",
Expand Down
134 changes: 134 additions & 0 deletions src/v3/src/components/Form/jsonforms/Form.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
/*
* Copyright (c) 2022-present, Okta, Inc. and/or its affiliates. All rights reserved.
* The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.")
*
* You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
* See the License for the specific language governing permissions and limitations under the License.
*/

import { JsonFormsCore } from '@jsonforms/core';
import { JsonForms } from '@jsonforms/react';
import { vanillaCells } from '@jsonforms/vanilla-renderers';
import { Box } from '@mui/material';
import Ajv, { ErrorObject } from 'ajv';
import AjvErrors from 'ajv-errors';
import { FunctionComponent, h } from 'preact';
import { useCallback, useEffect } from 'preact/hooks';

import { useWidgetContext } from '../../../contexts';
import { useOnSubmit, useOnSubmitValidation } from '../../../hooks';
import {
FormBag,
SubmitEvent,
} from '../../../types';
import { isCaptchaEnabled } from '../../../util';
import AuthContent from '../../AuthContent/AuthContent';
import { renderers } from './renderers';

const ajv = new Ajv({
allErrors: true,
verbose: true,
$data: true,
});
AjvErrors(ajv, {});

const Form: FunctionComponent<{
schema: FormBag['schema'];
uischema: FormBag['uischema'];
}> = ({ schema, uischema }) => {
// const validate = ajv.compile(schema);
const {
data,
idxTransaction: currTransaction,
message,
setMessage,
dataSchemaRef,
setWidgetRendered,
setData,
setFormErrors,
} = useWidgetContext();
const onSubmitHandler = useOnSubmit();
// const onValidationHandler = useOnSubmitValidation();

const onChange = (event: Pick<JsonFormsCore, 'data' | 'errors'>) => {
setFormErrors(event.errors || []);
// validate(event.data);
// setFormErrors(validate.errors || []);
setData(event.data);
};

useEffect(() => {
setWidgetRendered(true);
}, [currTransaction, setWidgetRendered]);

const handleSubmit = useCallback(async (e: SubmitEvent) => {
e.preventDefault();
setWidgetRendered(false);

setMessage(undefined);

const {
submit: {
actionParams: params,
step,
includeImmutableData,
},
captchaRef,
} = dataSchemaRef.current!;

if (currTransaction && isCaptchaEnabled(currTransaction)) {
// launch the captcha challenge
captchaRef?.current?.execute();
} else {
// submit request
onSubmitHandler({
includeData: true,
includeImmutableData,
params,
step,
});
}
}, [
currTransaction,
// data,
dataSchemaRef,
onSubmitHandler,
// onValidationHandler,
setMessage,
setWidgetRendered,
]);

return (
<Box
component="form"
noValidate
onSubmit={handleSubmit}
className={'o-form'} // TODO: FIXME OKTA-578584 - update page objects using .o-form selectors
data-se="o-form"
sx={{
maxInlineSize: '100%',
wordBreak: 'break-word',
}}
>
<AuthContent>
<JsonForms
schema={schema}
uischema={uischema}
data={data}
renderers={renderers}
cells={vanillaCells}
// disable client side validation to pass parity stage testing
// validationMode="NoValidation"
ajv={ajv}
onChange={onChange}
/>
</AuthContent>
</Box>
);
};

export default Form;
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
/*
* Copyright (c) 2022-present, Okta, Inc. and/or its affiliates. All rights reserved.
* The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.")
*
* You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
* See the License for the specific language governing permissions and limitations under the License.
*/

import { RendererProps } from '@jsonforms/core';
import { withJsonFormsRendererProps } from '@jsonforms/react';
import { Box } from '@mui/material';
import { Button as OdyButton, useOdysseyDesignTokens } from '@okta/odyssey-react-mui';
import { FunctionComponent, h } from 'preact';

import { useWidgetContext } from '../../../../../contexts';
import { useAutoFocus, useOnSubmit } from '../../../../../hooks';
import { ClickHandler } from '../../../../../types';
import Spinner from '../../../../Spinner';

const ButtonElement: FunctionComponent<RendererProps> = ({
uischema,
}) => {
const widgetContext = useWidgetContext();
const { loading } = widgetContext;
const onSubmitHandler = useOnSubmit();
const tokens = useOdysseyDesignTokens();
const {
// label,
// focus,
// ariaDescribedBy,
options: {
/** Unused props */
focus,
type = 'submit',
// ariaLabel,
// variant,
// dataType,
// dataSe,
// Icon,
// iconAlt,
// includeData,
// isActionStep,
// step,
// stepToRender,
// classes,
disabled,
onClick,
/* END */

wide,
id,
style,
label,
events,
target,
image,
} = {},
} = uischema;
const STYLE_TO_VARIANT: Record<string, 'primary' | 'secondary'> = {
PRIMARY_BUTTON: 'primary',
SECONDARY_BUTTON: 'secondary',
};

const ButtonImageIcon = typeof image !== 'undefined' ? (
<Box
component="img"
src={image.rendition.mainHref}
alt={image.altText.text}
aria-hidden
/>
) : undefined;

const focusRef = useAutoFocus<HTMLButtonElement>(focus);

const customClickHandler = () => onClick?.(widgetContext);

const handleClick: ClickHandler = async () => {
onSubmitHandler({
params: events?.[0]?.action?.actionParams,
includeData: events?.[0]?.action?.includeFormData,
step: events?.[0]?.action?.step,
});
};

return (
<Box sx={{marginBlockEnd: tokens.Spacing3}}>
<OdyButton
type={type}
label={label}
variant={STYLE_TO_VARIANT[style] ?? 'primary'}
isFullWidth={wide ?? true}
ref={focusRef}
isDisabled={loading || disabled}
// className={classes}
startIcon={loading ? <Spinner /> : ButtonImageIcon}
// aria-describedby={ariaDescribedBy}
// data-type={dataType}
testId={id}
// aria-label={ariaLabel}
// eslint-disable-next-line react/jsx-props-no-spreading
{...(type !== 'submit' && { onClick: typeof onClick === 'function' ? customClickHandler : handleClick })}
/>
</Box>
);
};


const WrappedButtonElement = withJsonFormsRendererProps(ButtonElement);
export default WrappedButtonElement;
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/*
* Copyright (c) 2022-present, Okta, Inc. and/or its affiliates. All rights reserved.
* The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.")
*
* You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
* See the License for the specific language governing permissions and limitations under the License.
*/

import { ControlProps } from '@jsonforms/core';
import { withJsonFormsControlProps } from '@jsonforms/react';
import { Box } from '@mui/material';
import { Checkbox as OdyCheckbox, CheckboxGroup, useOdysseyDesignTokens } from '@okta/odyssey-react-mui';
import { FunctionComponent, h } from 'preact';

import { useWidgetContext } from '../../../../../contexts';
import { useAutoFocus } from '../../../../../hooks';
import { ChangeEvent } from '../../../../../types';

const CheckboxControl: FunctionComponent<ControlProps> = ({
data,
handleChange,
path,
config,
schema,
uischema,
}) => {
const { loading } = useWidgetContext();
const tokens = useOdysseyDesignTokens();

const {
// showAsterisk,
options: {
/** Unused props */
focus,
noTranslate,
mutable,
/** END */
label,
value,
} = {},
} = uischema;
const isReadOnly = mutable === false;
// const labelInfo = getTranslationInfo(translations, 'label');
// const descriptionInfo = getTranslationInfo(translations, 'description');
const focusRef = useAutoFocus<HTMLInputElement>(focus);

return (
<Box sx={{marginBlockEnd: tokens.Spacing3}}>
<CheckboxGroup
// errorMessage={errorMessage}
// errorMessageList={errorMessageList}
label=""
>
<OdyCheckbox
// hint={description}
id={`${path}-checkbox`}
inputRef={focusRef}
isChecked={value === true}
isDisabled={loading || isReadOnly}
label={label}
name={path}
// onBlur={(e: ChangeEvent<HTMLInputElement>) => {
// handleBlur?.(e?.currentTarget?.checked);
// }}
onChange={(e: ChangeEvent<HTMLInputElement>) => {
handleChange(path, e.currentTarget.checked)
}}
testId={path}
/>
</CheckboxGroup>
</Box>
);
};

const WrappedCheckboxControl = withJsonFormsControlProps(CheckboxControl);
export default WrappedCheckboxControl;
Loading