Skip to content

Commit

Permalink
🎨 Clean up usage of FallbackSchema
Browse files Browse the repository at this point in the history
FallbackSchema (i.e. unknown component types) need to be handled as
soon as possible, since they make it harder to operate in type-safe
ways on downstream components. Being able to reason only in
AnyCompnentSchema simplifies implementations and guard clauses.
  • Loading branch information
sergei-maertens committed Aug 15, 2024
1 parent 8e91793 commit d76a52e
Show file tree
Hide file tree
Showing 11 changed files with 131 additions and 144 deletions.
5 changes: 5 additions & 0 deletions i18n/messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -464,6 +464,11 @@
"description": "Values table: accessible label to remove an option",
"originalDefault": "Remove"
},
"ISVTEk": {
"defaultMessage": "The component type <code>{type}</code> is unknown. We can only display the JSON definition.",
"description": "Informational message about unsupported component",
"originalDefault": "The component type <code>{type}</code> is unknown. We can only display the JSON definition."
},
"JDYF2q": {
"defaultMessage": "The data entered in this component will be removed in accordance with the privacy settings.",
"description": "Tooltip for 'IsSensitiveData' builder field",
Expand Down
5 changes: 5 additions & 0 deletions i18n/messages/nl.json
Original file line number Diff line number Diff line change
Expand Up @@ -470,6 +470,11 @@
"description": "Values table: accessible label to remove an option",
"originalDefault": "Remove"
},
"ISVTEk": {
"defaultMessage": "The component type <code>{type}</code> is unknown. We can only display the JSON definition.",
"description": "Informational message about unsupported component",
"originalDefault": "The component type <code>{type}</code> is unknown. We can only display the JSON definition."
},
"JDYF2q": {
"defaultMessage": "Gegevens opgevoerd in dit component worden geschoond volgens de privacy-instellingen.",
"description": "Tooltip for 'IsSensitiveData' builder field",
Expand Down
95 changes: 67 additions & 28 deletions src/components/ComponentConfiguration.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
import {JSONEditor} from '@open-formulieren/monaco-json-editor';
import {FallbackSchema} from '@open-formulieren/types';
import {FormattedMessage} from 'react-intl';

import {BuilderContext, BuilderContextType} from '@/context';
import {isKnownComponentType} from '@/registry';

import ComponentEditForm, {ComponentEditFormProps} from './ComponentEditForm';

export interface ComponentConfigurationProps extends BuilderContextType, ComponentEditFormProps {}
type MergedProps = BuilderContextType & ComponentEditFormProps;
export type ComponentConfigurationProps = Omit<MergedProps, 'component'> & {
component: MergedProps['component'] | FallbackSchema;
};

/**
* The main entrypoint to edit a component in the builder modal.
Expand Down Expand Up @@ -42,34 +50,65 @@ const ComponentConfiguration: React.FC<ComponentConfigurationProps> = ({
onCancel,
onRemove,
onSubmit,
}) => (
<BuilderContext.Provider
value={{
uniquifyKey,
supportedLanguageCodes,
richTextColors,
theme,
getFormComponents,
getValidatorPlugins,
getRegistrationAttributes,
getPrefillPlugins,
getPrefillAttributes,
getFileTypes,
serverUploadLimit,
getDocumentTypes,
getConfidentialityLevels,
getAuthPlugins,
}}
>
<ComponentEditForm
isNew={isNew}
component={component}
builderInfo={builderInfo}
onCancel={onCancel}
onRemove={onRemove}
onSubmit={onSubmit}
}) => {
if (!isKnownComponentType(component)) {
return <Fallback theme={theme} component={component} />;
}
return (
<BuilderContext.Provider
value={{
uniquifyKey,
supportedLanguageCodes,
richTextColors,
theme,
getFormComponents,
getValidatorPlugins,
getRegistrationAttributes,
getPrefillPlugins,
getPrefillAttributes,
getFileTypes,
serverUploadLimit,
getDocumentTypes,
getConfidentialityLevels,
getAuthPlugins,
}}
>
<ComponentEditForm
isNew={isNew}
component={component}
builderInfo={builderInfo}
onCancel={onCancel}
onRemove={onRemove}
onSubmit={onSubmit}
/>
</BuilderContext.Provider>
);
};

interface FallbackProps {
component: FallbackSchema;
theme: BuilderContextType['theme'];
}

const Fallback: React.FC<FallbackProps> = ({component, theme}) => (
<>
<FormattedMessage
tagName="p"
description="Informational message about unsupported component"
defaultMessage="The component type <code>{type}</code> is unknown. We can only display the JSON definition."
values={{
type: component.type ?? '-',
code: chunks => <code>{chunks}</code>,
}}
/>
<JSONEditor
wrapperProps={{className: 'json-editor'}}
value={component}
onChange={() => alert('Editing is not possible in unknown components.')}
theme={theme}
readOnly
/>
</BuilderContext.Provider>
</>
);

export default ComponentConfiguration;
29 changes: 9 additions & 20 deletions src/components/ComponentEditForm.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import {AnyComponentSchema} from '@open-formulieren/types';
import {Form, Formik} from 'formik';
import {ExtendedComponentSchema} from 'formiojs/types/components/schema';
import {cloneDeep, merge} from 'lodash';
Expand All @@ -6,8 +7,7 @@ import {FormattedMessage, useIntl} from 'react-intl';
import {toFormikValidationSchema} from 'zod-formik-adapter';

import {BuilderContext} from '@/context';
import {Fallback, getRegistryEntry, isKnownComponentType} from '@/registry';
import {AnyComponentSchema, FallbackSchema} from '@/types';
import {getRegistryEntry} from '@/registry';

import GenericComponentPreview from './ComponentPreview';

Expand All @@ -22,13 +22,11 @@ export interface BuilderInfo {

export interface ComponentEditFormProps {
isNew: boolean;
// it is (currently) possible someone drags a component type into the canvas that we
// don't know (yet), so we need to handle FallbackSchema.
component: AnyComponentSchema | FallbackSchema;
component: AnyComponentSchema;
builderInfo: BuilderInfo;
onCancel: (e: React.MouseEvent<HTMLButtonElement>) => void;
onRemove: (e: React.MouseEvent<HTMLButtonElement>) => void;
onSubmit: (component: AnyComponentSchema | FallbackSchema) => void;
onSubmit: (component: AnyComponentSchema) => void;
}

type ButtonRowProps = Pick<ComponentEditFormProps, 'onCancel' | 'onRemove'> & {
Expand Down Expand Up @@ -76,7 +74,7 @@ const ComponentEditForm: React.FC<ComponentEditFormProps> = ({
const hasPreview = registryEntry.preview !== null;

let initialValues = cloneDeep(component);
if (isNew && isKnownComponentType(component)) {
if (isNew) {
// Formio.js mutates components when adding children (like fieldset layout component),
// which apparently goes all the way to our default value definition, which is
// supposed to be static.
Expand All @@ -86,17 +84,12 @@ const ComponentEditForm: React.FC<ComponentEditFormProps> = ({
initialValues = merge(defaultValues, initialValues);
}

// we infer the specific schema from the EditForm component obtained from the registry.
// This gives a specific schema rather than AnyComponentSchema and allows us to type
// check the values accordingly.
type ComponentSchema = React.ComponentProps<typeof EditForm>['component'];

const Wrapper = hasPreview ? LayoutWithPreview : LayoutWithoutPreview;

// Markup (mostly) taken from formio's default templates - there's room for improvement here
// to de-bootstrapify it.
return (
<Formik<ComponentSchema>
<Formik<typeof component>
validateOnChange={false}
validateOnBlur
initialValues={initialValues}
Expand Down Expand Up @@ -148,11 +141,7 @@ const ComponentEditForm: React.FC<ComponentEditFormProps> = ({
<div className="formio-component formio-component-label-hidden">
<div className="formio-form">
<div className="formio-component-tabs">
{isKnownComponentType(component) ? (
<EditForm component={component} />
) : (
<Fallback.edit component={component} />
)}
<EditForm component={component} />
</div>
</div>
</div>
Expand All @@ -175,8 +164,8 @@ const ComponentEditForm: React.FC<ComponentEditFormProps> = ({
type EditFormLayoutProps = PropsWithChildren<
Pick<ComponentEditFormProps, 'onCancel' | 'onRemove'> & {
onSubmit: () => void;
onComponentChange: (value: AnyComponentSchema | FallbackSchema) => void;
component: AnyComponentSchema | FallbackSchema;
onComponentChange: (value: AnyComponentSchema) => void;
component: AnyComponentSchema;
}
>;

Expand Down
34 changes: 2 additions & 32 deletions src/components/ComponentPreview.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,38 +14,6 @@ const Template: StoryFn<typeof ComponentPreview> = ({component}) => (
<ComponentPreview onComponentChange={fn()} component={component} />
);

export const Default: Story = {
render: Template,

args: {
component: {
id: 'foo',
type: 'textfield',
label: 'A text field',
validate: {
required: false,
},
},
},
};

export const Fallback: Story = {
render: Template,

args: {
component: {
id: 'fallback',
// should never accidentally be an actual type
type: '85230383-896e-40ce-a1a9-35a090b73f17',
} as any,
},

play: async ({canvasElement}) => {
const canvas = within(canvasElement);
await canvas.findByTestId('jsonPreview');
},
};

export const TextField: Story = {
render: Template,

Expand Down Expand Up @@ -141,6 +109,7 @@ export const Email: Story = {
label: 'Email preview',
description: 'A preview of the email Formio component',
hidden: true, // must be ignored
validateOn: 'blur',
},
},

Expand Down Expand Up @@ -174,6 +143,7 @@ export const EmailMultiple: Story = {
description: 'Description only once',
hidden: true, // must be ignored
multiple: true,
validateOn: 'blur',
},
},

Expand Down
20 changes: 8 additions & 12 deletions src/components/ComponentPreview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,19 @@ import React, {useContext, useState} from 'react';
import {FormattedMessage} from 'react-intl';

import {BuilderContext} from '@/context';
import {Fallback, getRegistryEntry, isKnownComponentType} from '@/registry';
import {AnyComponentSchema, FallbackSchema, hasOwnProperty} from '@/types';
import {getRegistryEntry} from '@/registry';
import {AnyComponentSchema, hasOwnProperty} from '@/types';

/*
Generic preview (preview + wrapper with view mode)
*/

export interface ComponentPreviewWrapperProps {
component: AnyComponentSchema | FallbackSchema;
component: AnyComponentSchema;
/** Initial values for the preview component, e.g. `{"componentKey": "some_value"}` */
initialValues: Record<string, unknown>;
/** Handler to be called when the component JSON definition changes */
onComponentChange: (value: AnyComponentSchema | FallbackSchema) => void;
onComponentChange: (value: AnyComponentSchema) => void;
children: React.ReactNode;
}

Expand Down Expand Up @@ -69,8 +69,8 @@ const ComponentPreviewWrapper: React.FC<ComponentPreviewWrapperProps> = ({
};

export interface GenericComponentPreviewProps {
component: AnyComponentSchema | FallbackSchema;
onComponentChange: (value: AnyComponentSchema | FallbackSchema) => void;
component: AnyComponentSchema;
onComponentChange: (value: AnyComponentSchema) => void;
}

/**
Expand All @@ -86,7 +86,7 @@ const GenericComponentPreview: React.FC<GenericComponentPreviewProps> = ({
component,
onComponentChange,
}) => {
const key = isKnownComponentType(component) ? component.key : '';
const {key} = component;
const entry = getRegistryEntry(component);
const {preview: PreviewComponent, defaultValue = ''} = entry;
if (PreviewComponent === null) {
Expand All @@ -109,11 +109,7 @@ const GenericComponentPreview: React.FC<GenericComponentPreviewProps> = ({
component={component}
initialValues={initialValues}
>
{isKnownComponentType(component) ? (
<PreviewComponent component={component} />
) : (
<Fallback.preview component={component} />
)}
<PreviewComponent component={component} />
</ComponentPreviewWrapper>
);
};
Expand Down
15 changes: 10 additions & 5 deletions src/components/builder/simple-conditional.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import {AnyComponentSchema} from '@open-formulieren/types';
import {useFormikContext} from 'formik';
import {ExtendedComponentSchema, Utils as FormioUtils} from 'formiojs';
import {Utils as FormioUtils} from 'formiojs';
import type {ConditionalOptions} from 'formiojs';
import {useContext, useEffect} from 'react';
import {FormattedMessage} from 'react-intl';
import usePrevious from 'react-use/esm/usePrevious';

import {TextField} from '@/components/formio/textfield';
import {BuilderContext} from '@/context';
import {getRegistryEntry} from '@/registry';
import Fallback from '@/registry/fallback';
import {ComparisonValueProps} from '@/registry/types';
import {hasOwnProperty} from '@/types';

import {Panel, Select} from '../formio';
import ComponentSelect from './component-select';
Expand All @@ -22,7 +24,7 @@ export const ComparisonValueInput: React.FC = () => {
const {values, setFieldValue} = useFormikContext<SimpleConditional>();

const componentKey = values?.conditional?.when;
const chosenComponent: ExtendedComponentSchema = componentKey
const chosenComponent: AnyComponentSchema = componentKey
? FormioUtils.getComponent(getFormComponents(), componentKey, false)
: null;
const previousWhen = usePrevious(componentKey);
Expand All @@ -43,7 +45,7 @@ export const ComparisonValueInput: React.FC = () => {

const registryEntry = getRegistryEntry(chosenComponent);
const {comparisonValue} = registryEntry;
const InputComponent = comparisonValue || Fallback.comparisonValue;
const InputComponent = comparisonValue || TextField;

const props: ComparisonValueProps = {
name: 'conditional.eq',
Expand All @@ -54,7 +56,10 @@ export const ComparisonValueInput: React.FC = () => {
/>
),
};
if (chosenComponent.hasOwnProperty('multiple')) props.multiple = chosenComponent.multiple;

if (hasOwnProperty(chosenComponent, 'multiple')) {
props.multiple = chosenComponent.multiple as boolean;
}

return <InputComponent {...props} />;
};
Expand Down
Loading

0 comments on commit d76a52e

Please sign in to comment.