Skip to content

Commit

Permalink
✨[open-formulieren/open-forms#3597] Add type checking
Browse files Browse the repository at this point in the history
  • Loading branch information
CharString authored and Viicos committed Dec 5, 2023
1 parent 3851db2 commit 6c2160c
Show file tree
Hide file tree
Showing 10 changed files with 363 additions and 6 deletions.
8 changes: 7 additions & 1 deletion .storybook/decorators.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ import {
DEFAULT_VALIDATOR_PLUGINS,
sleep,
} from '@/tests/sharedUtils';
import {VariableDefinition, createTypeCheck} from '@/utils/jsonlogic';

import static_variables from '../src/components/static_variables.json';

export const ModalDecorator: Decorator = (Story, {parameters}) => {
if (parameters?.modal?.noModal) return <Story />;
Expand Down Expand Up @@ -54,14 +57,16 @@ export const BuilderContextDecorator: Decorator = (Story, context) => {
const defaultFileTypes = context.parameters.builder?.defaultFileTypes || DEFAULT_FILE_TYPES;
const defaultdocumentTypes =
context.parameters.builder?.defaultdocumentTypes || DEFAULT_DOCUMENT_TYPES;
const components = context?.args?.componentTree || defaultComponentTree;
const staticVariables = static_variables as VariableDefinition[]; // source is inferred as string not as ""

return (
<BuilderContext.Provider
value={{
uniquifyKey: key => key,
supportedLanguageCodes: supportedLanguageCodes,
componentTranslationsRef: {current: translationsStore},
getFormComponents: () => context?.args?.componentTree || defaultComponentTree,
getFormComponents: () => components,
getValidatorPlugins: async () => {
await sleep(context.parameters?.builder?.validatorPluginsDelay || 0);
return context?.args?.validatorPlugins || defaultValidatorPlugins;
Expand All @@ -86,6 +91,7 @@ export const BuilderContextDecorator: Decorator = (Story, context) => {
getDocumentTypes: async () => context?.args?.documentTypes || defaultdocumentTypes,
getConfidentialityLevels: async () => CONFIDENTIALITY_LEVELS,
getAuthPlugins: async () => DEFAULT_AUTH_PLUGINS,
validateLogic: createTypeCheck({components, staticVariables}),
}}
>
<Story />
Expand Down
17 changes: 15 additions & 2 deletions src/components/ComponentConfiguration.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,14 @@ import {
DEFAULT_FILE_TYPES,
} from '@/tests/sharedUtils';
import {AnyComponentSchema} from '@/types';
import {VariableDefinition, createTypeCheck} from '@/utils/jsonlogic';

import ComponentConfiguration from './ComponentConfiguration';
import {BuilderInfo} from './ComponentEditForm';
import {PrefillAttributeOption, PrefillPluginOption} from './builder/prefill';
import {RegistrationAttributeOption} from './builder/registration/registration-attribute';
import {ValidatorOption} from './builder/validate/validator-select';
import static_variables from './static_variables.json';

export default {
title: 'Public API/ComponentConfiguration',
Expand All @@ -31,6 +33,7 @@ export default {
args: {
isNew: true,
otherComponents: [{type: 'select', label: 'A select', key: 'aSelect'}],
variableDefinitions: static_variables,
validatorPlugins: [
{id: 'phone-intl', label: 'Phone (international)'},
{id: 'phone-nl', label: 'Phone (Dutch)'},
Expand Down Expand Up @@ -82,6 +85,7 @@ interface TemplateArgs {
};
};
otherComponents: AnyComponentSchema[];
variableDefinitions: VariableDefinition[];
validatorPlugins: ValidatorOption[];
registrationAttributes: RegistrationAttributeOption[];
prefillPlugins: PrefillPluginOption[];
Expand All @@ -97,6 +101,7 @@ interface TemplateArgs {
const Template: StoryFn<TemplateArgs> = ({
component,
otherComponents,
variableDefinitions,
validatorPlugins,
registrationAttributes,
prefillPlugins,
Expand All @@ -115,6 +120,10 @@ const Template: StoryFn<TemplateArgs> = ({
supportedLanguageCodes={supportedLanguageCodes}
componentTranslationsRef={{current: translationsStore}}
getFormComponents={() => otherComponents}
validateLogic={createTypeCheck({
formVariables: variableDefinitions,
components: otherComponents,
})}
getValidatorPlugins={async () => validatorPlugins}
getRegistrationAttributes={async () => registrationAttributes}
getPrefillPlugins={async () => prefillPlugins}
Expand Down Expand Up @@ -1140,13 +1149,17 @@ export const SelectBoxes: Story = {
const itemsExpressionInput = canvas.getByLabelText('Items expression');
await userEvent.clear(itemsExpressionInput);
// { needs to be escaped: https://github.com/testing-library/user-event/issues/584
const expression = '{"var": "someVar"}'.replace(/[{[]/g, '$&$&');
const expression = '{"var": "current_year"}'.replace(/[{[]/g, '$&$&');
await userEvent.type(itemsExpressionInput, expression);

await expect(editForm.queryByLabelText('Default value')).toBeNull();
await expect(preview.getByRole('checkbox', {name: /Options from expression:/})).toBeVisible();

await userEvent.click(canvas.getByRole('button', {name: 'Save'}));
expect(itemsExpressionInput).toHaveAttribute('aria-invalid', 'true');
expect(itemsExpressionInput).toHaveAttribute('aria-errormessage');
const errorMessageId = itemsExpressionInput.getAttribute('aria-errormessage') ?? '';
expect(document.getElementById(errorMessageId)).toBeVisible();
expect(args.onSubmit).toHaveBeenCalledWith({
id: 'wqimsadk',
type: 'selectboxes',
Expand All @@ -1163,7 +1176,7 @@ export const SelectBoxes: Story = {
isSensitiveData: false,
openForms: {
dataSrc: 'variable',
itemsExpression: {var: 'someVar'},
itemsExpression: {var: 'current_year'}, // valid JSON, invalid expression
translations: {},
},
defaultValue: {},
Expand Down
3 changes: 3 additions & 0 deletions src/components/ComponentConfiguration.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export interface ComponentConfigurationProps extends BuilderContextType, Compone
*
* @param options.uniquifyKey Function to make component key unique in the context of all existing components.
* @param options.getFormComponents Function returning all other Formio components in the builder context.
* @param options.validateLogic Function to validate JsonLogic expressions in the context of the form.
* @param options.componentTranslationsRef Object containing the existing translations from other components, keyed by language code. Each entry is a map of literal => translation.
* @param options.isNew Whether the Formio component is a new component being added or an existing being edited.
* @param options.component The (starter) schema of the Formio component being edited.
Expand All @@ -27,6 +28,7 @@ const ComponentConfiguration: React.FC<ComponentConfigurationProps> = ({
supportedLanguageCodes = ['nl', 'en'],
componentTranslationsRef,
getFormComponents,
validateLogic,
getValidatorPlugins,
getRegistrationAttributes,
getPrefillPlugins,
Expand All @@ -49,6 +51,7 @@ const ComponentConfiguration: React.FC<ComponentConfigurationProps> = ({
supportedLanguageCodes,
componentTranslationsRef,
getFormComponents,
validateLogic,
getValidatorPlugins,
getRegistrationAttributes,
getPrefillPlugins,
Expand Down
16 changes: 13 additions & 3 deletions src/components/builder/values/items-expression.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,26 @@
import {JSONObject} from '@open-formulieren/types/lib/types';
import {type JSONObject} from '@open-formulieren/types/lib/types';
import {useFormikContext} from 'formik';
import {useContext} from 'react';
import {FormattedMessage} from 'react-intl';

import JSONEdit from '@/components/JSONEdit';
import {Component, Description} from '@/components/formio';
import {BuilderContext} from '@/context';

const NAME = 'openForms.itemsExpression';

/**
* The `ItemsExpression` component is used to specify the JsonLogic expression to
* calculate the values/options for a component.
*
* @todo: this would really benefit from a nice, context-aware JsonLogic editor.
* @todo: this would really benefit from a nice JsonLogic editor.
*/
export const ItemsExpression: React.FC = () => {
const {getFieldProps} = useFormikContext();
const {value = ''} = getFieldProps<JSONObject | string | undefined>(NAME);

const {validateLogic} = useContext(BuilderContext);

const htmlId = `editform-${NAME}`;
return (
<Component
Expand All @@ -32,7 +36,13 @@ export const ItemsExpression: React.FC = () => {
}
>
<div>
<JSONEdit name={NAME} data={value} rows={3} id={htmlId} />
<JSONEdit
name={NAME}
data={value}
rows={3}
id={htmlId}
validateLogic={logic => validateLogic(logic, [['', '']])}
/>
</div>

<Description
Expand Down
152 changes: 152 additions & 0 deletions src/components/static_variables.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
[
{
"form": null,
"formDefinition": null,
"name": "Nu",
"key": "now",
"source": "",
"serviceFetchConfiguration": null,
"prefillPlugin": "",
"prefillAttribute": "",
"prefillIdentifierRole": "main",
"dataType": "datetime",
"dataFormat": "",
"isSensitiveData": false,
"initialValue": "2023-11-24T09:22:39.320053Z"
},
{
"form": null,
"formDefinition": null,
"name": "Vandaag",
"key": "today",
"source": "",
"serviceFetchConfiguration": null,
"prefillPlugin": "",
"prefillAttribute": "",
"prefillIdentifierRole": "main",
"dataType": "date",
"dataFormat": "",
"isSensitiveData": false,
"initialValue": "2023-11-24"
},
{
"form": null,
"formDefinition": null,
"name": "Huidig jaar",
"key": "current_year",
"source": "",
"serviceFetchConfiguration": null,
"prefillPlugin": "",
"prefillAttribute": "",
"prefillIdentifierRole": "main",
"dataType": "int",
"dataFormat": "",
"isSensitiveData": false,
"initialValue": 2023
},
{
"form": null,
"formDefinition": null,
"name": "Omgeving",
"key": "environment",
"source": "",
"serviceFetchConfiguration": null,
"prefillPlugin": "",
"prefillAttribute": "",
"prefillIdentifierRole": "main",
"dataType": "string",
"dataFormat": "",
"isSensitiveData": false,
"initialValue": "development"
},
{
"form": null,
"formDefinition": null,
"name": "Formuliernaam",
"key": "form_name",
"source": "",
"serviceFetchConfiguration": null,
"prefillPlugin": "",
"prefillAttribute": "",
"prefillIdentifierRole": "main",
"dataType": "string",
"dataFormat": "",
"isSensitiveData": false,
"initialValue": ""
},
{
"form": null,
"formDefinition": null,
"name": "Formulier-ID",
"key": "form_id",
"source": "",
"serviceFetchConfiguration": null,
"prefillPlugin": "",
"prefillAttribute": "",
"prefillIdentifierRole": "main",
"dataType": "string",
"dataFormat": "",
"isSensitiveData": false,
"initialValue": ""
},
{
"form": null,
"formDefinition": null,
"name": "Authenticatie",
"key": "auth",
"source": "",
"serviceFetchConfiguration": null,
"prefillPlugin": "",
"prefillAttribute": "",
"prefillIdentifierRole": "main",
"dataType": "object",
"dataFormat": "",
"isSensitiveData": false,
"initialValue": null
},
{
"form": null,
"formDefinition": null,
"name": "Authenticatie BSN",
"key": "auth_bsn",
"source": "",
"serviceFetchConfiguration": null,
"prefillPlugin": "",
"prefillAttribute": "",
"prefillIdentifierRole": "main",
"dataType": "string",
"dataFormat": "",
"isSensitiveData": false,
"initialValue": ""
},
{
"form": null,
"formDefinition": null,
"name": "Authenticatie KvK",
"key": "auth_kvk",
"source": "",
"serviceFetchConfiguration": null,
"prefillPlugin": "",
"prefillAttribute": "",
"prefillIdentifierRole": "main",
"dataType": "string",
"dataFormat": "",
"isSensitiveData": false,
"initialValue": ""
},
{
"form": null,
"formDefinition": null,
"name": "Authenticatie Pseudo",
"key": "auth_pseudo",
"source": "",
"serviceFetchConfiguration": null,
"prefillPlugin": "",
"prefillAttribute": "",
"prefillIdentifierRole": "main",
"dataType": "string",
"dataFormat": "",
"isSensitiveData": false,
"initialValue": ""
}
]
4 changes: 4 additions & 0 deletions src/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import {ValidatorOption} from '@/components/builder/validate/validator-select';
import {AuthPluginOption} from '@/registry/cosignV1/edit';
import {AnyComponentSchema} from '@/types';

import {JsonLogicTypeChecker, createTypeCheck} from './utils/jsonlogic';

/*
Translations
*/
Expand Down Expand Up @@ -57,6 +59,7 @@ export interface BuilderContextType {
supportedLanguageCodes: SupportedLocales[];
componentTranslationsRef: ComponentTranslationsRef;
getFormComponents: () => AnyComponentSchema[];
validateLogic: JsonLogicTypeChecker;
getValidatorPlugins: (componentType: string) => Promise<ValidatorOption[]>;
getRegistrationAttributes: (componentType: string) => Promise<RegistrationAttributeOption[]>;
getPrefillPlugins: (componentType: string) => Promise<PrefillPluginOption[]>;
Expand All @@ -73,6 +76,7 @@ const BuilderContext = React.createContext<BuilderContextType>({
supportedLanguageCodes: ['nl', 'en'],
componentTranslationsRef: {current: null},
getFormComponents: () => [],
validateLogic: createTypeCheck(),
getValidatorPlugins: async () => [],
getRegistrationAttributes: async () => [],
getPrefillPlugins: async () => [],
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export {default as ComponentEditForm} from '@/components/ComponentEditForm';
export {default as ComponentConfiguration} from '@/components/ComponentConfiguration';
export * from '@/utils/jsonlogic';
6 changes: 6 additions & 0 deletions src/tests/test-utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
sleep,
} from '@/tests/sharedUtils';
import {AnyComponentSchema} from '@/types';
import {VariableDefinition, createTypeCheck} from '@/utils/jsonlogic';

import {PrefillAttributeOption, PrefillPluginOption} from '../components/builder/prefill';
import {RegistrationAttributeOption} from '../components/builder/registration/registration-attribute';
Expand All @@ -38,6 +39,7 @@ interface BuilderOptions {
documentTypes: DocumentTypeOption[];
confidentialityLevels: SelectOption[];
registrationAttributesDelay: number;
staticVariables: VariableDefinition[];
}

interface contextRenderOptions {
Expand Down Expand Up @@ -91,6 +93,10 @@ const contextRender = (
getConfidentialityLevels: async () =>
builderOptions.confidentialityLevels || CONFIDENTIALITY_LEVELS,
getAuthPlugins: async () => DEFAULT_AUTH_PLUGINS,
validateLogic: createTypeCheck({
components: builderOptions.componentTree || DEFAULT_COMPONENT_TREE,
staticVariables: builderOptions.staticVariables,
}),
}}
>
{children}
Expand Down
Loading

0 comments on commit 6c2160c

Please sign in to comment.