@abgov/jsonforms-components 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/.babelrc +12 -0
  2. package/.eslintrc.json +36 -0
  3. package/.releaserc.json +25 -0
  4. package/README.md +251 -0
  5. package/jest.config.ts +11 -0
  6. package/package.json +17 -0
  7. package/project.json +55 -0
  8. package/rollup.config.js +14 -0
  9. package/src/index.ts +166 -0
  10. package/src/lib/Additional/HelpContent.tsx +95 -0
  11. package/src/lib/Additional/index.ts +1 -0
  12. package/src/lib/Additional/styled-components.ts +27 -0
  13. package/src/lib/Cells/DateCell.tsx +10 -0
  14. package/src/lib/Cells/IntegerCell.tsx +10 -0
  15. package/src/lib/Cells/NumberCell.tsx +10 -0
  16. package/src/lib/Cells/TextCell.tsx +10 -0
  17. package/src/lib/Cells/TimeCell.tsx +10 -0
  18. package/src/lib/Cells/index.tsx +14 -0
  19. package/src/lib/Context/index.tsx +172 -0
  20. package/src/lib/Controls/FileUploader/ContextMenu.tsx +50 -0
  21. package/src/lib/Controls/FileUploader/FileUploaderControl.tsx +131 -0
  22. package/src/lib/Controls/FileUploader/FileUploaderTester.tsx +3 -0
  23. package/src/lib/Controls/FileUploader/index.tsx +2 -0
  24. package/src/lib/Controls/FileUploader/styled-components.tsx +10 -0
  25. package/src/lib/Controls/FormStepper/FormStepperControl.tsx +269 -0
  26. package/src/lib/Controls/FormStepper/FormStepperTester.tsx +22 -0
  27. package/src/lib/Controls/FormStepper/index.tsx +2 -0
  28. package/src/lib/Controls/FormStepper/styled-components.tsx +17 -0
  29. package/src/lib/Controls/Inputs/InputBaseControl.tsx +52 -0
  30. package/src/lib/Controls/Inputs/InputBooleanControl.tsx +67 -0
  31. package/src/lib/Controls/Inputs/InputBooleanRadioControl.tsx +74 -0
  32. package/src/lib/Controls/Inputs/InputDateControl.tsx +90 -0
  33. package/src/lib/Controls/Inputs/InputDateTimeControl.tsx +46 -0
  34. package/src/lib/Controls/Inputs/InputEnum.tsx +74 -0
  35. package/src/lib/Controls/Inputs/InputEnumAutoComplete.tsx +73 -0
  36. package/src/lib/Controls/Inputs/InputEnumRadios.tsx +43 -0
  37. package/src/lib/Controls/Inputs/InputIntegerControl.tsx +63 -0
  38. package/src/lib/Controls/Inputs/InputMultiLineTextControl.tsx +63 -0
  39. package/src/lib/Controls/Inputs/InputNumberControl.tsx +63 -0
  40. package/src/lib/Controls/Inputs/InputTextControl.tsx +62 -0
  41. package/src/lib/Controls/Inputs/InputTimeControl.tsx +46 -0
  42. package/src/lib/Controls/Inputs/index.tsx +13 -0
  43. package/src/lib/Controls/Inputs/inputControl.spec.ts +84 -0
  44. package/src/lib/Controls/Inputs/type.ts +3 -0
  45. package/src/lib/Controls/ObjectArray/DeleteDialog.tsx +49 -0
  46. package/src/lib/Controls/ObjectArray/ObjectArray.tsx +59 -0
  47. package/src/lib/Controls/ObjectArray/ObjectArrayToolBar.tsx +51 -0
  48. package/src/lib/Controls/ObjectArray/ObjectListControl.tsx +368 -0
  49. package/src/lib/Controls/ObjectArray/index.tsx +1 -0
  50. package/src/lib/Controls/ObjectArray/styled-components.tsx +13 -0
  51. package/src/lib/Controls/index.tsx +4 -0
  52. package/src/lib/ErrorHandling/GoAErrorControl.tsx +53 -0
  53. package/src/lib/ErrorHandling/MessageControl.tsx +19 -0
  54. package/src/lib/ErrorHandling/categorizationValidation.spec.ts +98 -0
  55. package/src/lib/ErrorHandling/controlValildation.spec.ts +57 -0
  56. package/src/lib/ErrorHandling/errorCheck.spec.ts +185 -0
  57. package/src/lib/ErrorHandling/errorCheck.tsx +86 -0
  58. package/src/lib/ErrorHandling/layoutValildation.spec.ts +47 -0
  59. package/src/lib/ErrorHandling/otherValildation.spec.ts +74 -0
  60. package/src/lib/ErrorHandling/schemaValidation.ts +115 -0
  61. package/src/lib/common/Grid.tsx +55 -0
  62. package/src/lib/jsonforms-components.module.scss +7 -0
  63. package/src/lib/jsonforms-components.spec.tsx +10 -0
  64. package/src/lib/jsonforms-components.tsx +14 -0
  65. package/src/lib/layouts/GroupControl.tsx +25 -0
  66. package/src/lib/layouts/HorizontalLayoutControl.tsx +30 -0
  67. package/src/lib/layouts/VerticalLayoutControl.tsx +28 -0
  68. package/src/lib/layouts/index.ts +3 -0
  69. package/src/lib/util/layout.tsx +68 -0
  70. package/src/lib/util/schemaUtils.ts +9 -0
  71. package/src/lib/util/stringUtils.ts +98 -0
  72. package/src/lib/util/style-component.ts +8 -0
  73. package/tsconfig.json +20 -0
  74. package/tsconfig.lib.json +19 -0
  75. package/tsconfig.spec.json +20 -0
@@ -0,0 +1,59 @@
1
+ import React, { useCallback, useState } from 'react';
2
+ import {
3
+ ArrayLayoutProps,
4
+ RankedTester,
5
+ isObjectArrayControl,
6
+ isPrimitiveArrayControl,
7
+ or,
8
+ rankWith,
9
+ uiTypeIs,
10
+ and,
11
+ } from '@jsonforms/core';
12
+ import { withJsonFormsArrayLayoutProps } from '@jsonforms/react';
13
+ import { ObjectArrayControl } from './ObjectListControl';
14
+ import { Hidden } from '@mui/material';
15
+ import { DeleteDialog } from './DeleteDialog';
16
+
17
+ export const ArrayControl = (props: ArrayLayoutProps) => {
18
+ const [open, setOpen] = useState(false);
19
+ const [path, setPath] = useState<string>();
20
+ const [rowData, setRowData] = useState<number>(0);
21
+ const { removeItems, visible } = props;
22
+
23
+ const openDeleteDialog = useCallback(
24
+ (p: string, rowIndex: number) => {
25
+ setOpen(true);
26
+ setPath(p);
27
+ setRowData(rowIndex);
28
+ },
29
+ [setOpen, setPath, setRowData]
30
+ );
31
+ const deleteCancel = useCallback(() => setOpen(false), [setOpen]);
32
+
33
+ // eslint-disable-next-line
34
+ const deleteConfirm = useCallback(() => {
35
+ const p = path?.substring(0, path.lastIndexOf('.'));
36
+ if (removeItems && p) {
37
+ removeItems(p, [rowData])();
38
+ }
39
+ setOpen(false);
40
+ // eslint-disable-next-line
41
+ }, [setOpen, path, rowData]);
42
+
43
+ return (
44
+ <Hidden xsUp={!visible}>
45
+ <ObjectArrayControl {...props} openDeleteDialog={openDeleteDialog} />
46
+ <DeleteDialog
47
+ open={open}
48
+ onCancel={deleteCancel}
49
+ onConfirm={deleteConfirm}
50
+ title={props.translations.deleteDialogTitle || ''}
51
+ message={props.translations.deleteDialogMessage || ''}
52
+ />
53
+ </Hidden>
54
+ );
55
+ };
56
+
57
+ export const GoAArrayControlTester: RankedTester = rankWith(3, or(isObjectArrayControl, isPrimitiveArrayControl));
58
+ export const GoAArrayControlRenderer = withJsonFormsArrayLayoutProps(ArrayControl);
59
+ export const GoAListWithDetailsTester: RankedTester = rankWith(3, and(uiTypeIs('ListWithDetail')));
@@ -0,0 +1,51 @@
1
+ import React from 'react';
2
+ import { ControlElement, createDefaultValue, JsonSchema, ArrayTranslations } from '@jsonforms/core';
3
+ import { GoAButton } from '@abgov/react-components-new';
4
+
5
+ export interface ObjectArrayToolbarProps {
6
+ numColumns: number;
7
+ errors: string;
8
+ label: string;
9
+ path: string;
10
+ uischema: ControlElement;
11
+ schema: JsonSchema;
12
+ rootSchema: JsonSchema;
13
+ enabled: boolean;
14
+ translations: ArrayTranslations;
15
+ // eslint-disable-next-line
16
+ addItem(path: string, value: any): () => void;
17
+ }
18
+
19
+ const ObjectArrayToolBar = React.memo(function TableToolbar({
20
+ numColumns,
21
+ errors,
22
+ label,
23
+ path,
24
+ addItem,
25
+ schema,
26
+ enabled,
27
+ translations,
28
+ rootSchema,
29
+ uischema,
30
+ }: ObjectArrayToolbarProps) {
31
+ const buttonPosition = uischema?.options?.addButtonPosition || 'left';
32
+ return (
33
+ <>
34
+ {/* Note: Paul 2024-01-05: need to add the GoATooltip after the upgrade of the ui components */}
35
+ {/* <GoATooltip content={translations.addTooltip}> */}
36
+ <div style={{ textAlign: buttonPosition }}>
37
+ <GoAButton
38
+ disabled={!enabled}
39
+ testId={`object-array-toolbar-${label}`}
40
+ aria-label={translations.addAriaLabel}
41
+ onClick={addItem(path, createDefaultValue(schema, rootSchema))}
42
+ >
43
+ {uischema?.options?.addButtonText || 'Add'}
44
+ </GoAButton>
45
+ </div>
46
+ {/* </GoATooltip> */}
47
+ </>
48
+ );
49
+ });
50
+
51
+ export default ObjectArrayToolBar;
@@ -0,0 +1,368 @@
1
+ import isEmpty from 'lodash/isEmpty';
2
+ import { JsonFormsStateContext, useJsonForms } from '@jsonforms/react';
3
+ import range from 'lodash/range';
4
+ import React from 'react';
5
+ import { FormHelperText, Hidden, Typography } from '@mui/material';
6
+ import {
7
+ ArrayLayoutProps,
8
+ ControlElement,
9
+ JsonSchema,
10
+ Paths,
11
+ JsonFormsRendererRegistryEntry,
12
+ JsonFormsCellRendererRegistryEntry,
13
+ ArrayTranslations,
14
+ UISchemaElement,
15
+ Layout,
16
+ } from '@jsonforms/core';
17
+
18
+ import { WithDeleteDialogSupport } from './DeleteDialog';
19
+ import ObjectArrayToolBar from './ObjectArrayToolBar';
20
+ import merge from 'lodash/merge';
21
+ import { JsonFormsDispatch } from '@jsonforms/react';
22
+ import { GoAGrid, GoAIconButton, GoAContainer } from '@abgov/react-components-new';
23
+ import { ToolBarHeader, ObjectArrayTitle } from './styled-components';
24
+
25
+ // eslint-disable-next-line
26
+ const extractScopesFromUISchema = (uischema: any): string[] => {
27
+ const scopes: string[] = [];
28
+
29
+ if (uischema?.elements) {
30
+ // eslint-disable-next-line
31
+ uischema?.elements?.forEach((element: any) => {
32
+ if (element?.elements) {
33
+ // eslint-disable-next-line
34
+ element?.elements?.forEach((internalElement: any) => {
35
+ if (internalElement?.scope) {
36
+ scopes.push(internalElement?.scope);
37
+ }
38
+ });
39
+ }
40
+ });
41
+ }
42
+
43
+ return scopes;
44
+ };
45
+
46
+ const GenerateRows = (
47
+ Cell: React.ComponentType<OwnPropsOfNonEmptyCell>,
48
+ schema: JsonSchema,
49
+ rowPath: string,
50
+ enabled: boolean,
51
+ cells?: JsonFormsCellRendererRegistryEntry[],
52
+ uischema?: ControlElement
53
+ ) => {
54
+ if (schema.type === 'object') {
55
+ const props = {
56
+ schema,
57
+ rowPath,
58
+ enabled,
59
+ cells,
60
+ uischema,
61
+ };
62
+ return <Cell {...props} />;
63
+ } else {
64
+ // primitives
65
+ const props = {
66
+ schema,
67
+ rowPath,
68
+ cellPath: rowPath,
69
+ enabled,
70
+ };
71
+ return <Cell key={rowPath} {...props} />;
72
+ }
73
+ };
74
+
75
+ const getValidColumnProps = (scopedSchema: JsonSchema) => {
76
+ if (scopedSchema.type === 'object' && typeof scopedSchema.properties === 'object') {
77
+ return Object.keys(scopedSchema.properties).filter((prop) => scopedSchema.properties?.[prop].type !== 'array');
78
+ }
79
+ // primitives
80
+ return [''];
81
+ };
82
+
83
+ export interface EmptyListProps {
84
+ numColumns: number;
85
+ translations: ArrayTranslations;
86
+ }
87
+
88
+ const EmptyList = ({ numColumns, translations }: EmptyListProps) => (
89
+ <GoAGrid minChildWidth="30ch">
90
+ <Typography align="center">
91
+ <b>{translations.noDataMessage}</b>
92
+ </Typography>
93
+ </GoAGrid>
94
+ );
95
+
96
+ interface NonEmptyCellProps extends OwnPropsOfNonEmptyCell {
97
+ rootSchema?: JsonSchema;
98
+ errors: string;
99
+ enabled: boolean;
100
+ }
101
+ interface OwnPropsOfNonEmptyCell {
102
+ rowPath: string;
103
+ propName?: string;
104
+ schema: JsonSchema;
105
+ enabled: boolean;
106
+ renderers?: JsonFormsRendererRegistryEntry[];
107
+ cells?: JsonFormsCellRendererRegistryEntry[];
108
+ uischema?: ControlElement;
109
+ }
110
+ const ctxToNonEmptyCellProps = (ctx: JsonFormsStateContext, ownProps: OwnPropsOfNonEmptyCell): NonEmptyCellProps => {
111
+ const path = ownProps.rowPath + (ownProps.schema.type === 'object' ? '.' + ownProps.propName : '');
112
+ const errors = '';
113
+ return {
114
+ uischema: ownProps.uischema,
115
+ rowPath: ownProps.rowPath,
116
+ schema: ownProps.schema,
117
+ rootSchema: ctx.core?.schema,
118
+ errors,
119
+ enabled: ownProps.enabled,
120
+ cells: ownProps.cells || ctx.cells,
121
+ renderers: ownProps.renderers || ctx.renderers,
122
+ };
123
+ };
124
+
125
+ interface NonEmptyRowComponentProps {
126
+ propName?: string;
127
+ schema: JsonSchema;
128
+ rootSchema?: JsonSchema;
129
+ rowPath: string;
130
+ errors: string;
131
+ enabled: boolean;
132
+ renderers?: JsonFormsRendererRegistryEntry[];
133
+ cells?: JsonFormsCellRendererRegistryEntry[];
134
+ isValid: boolean;
135
+ uischema?: ControlElement | Layout;
136
+ }
137
+ const NonEmptyCellComponent = React.memo(function NonEmptyCellComponent({
138
+ schema,
139
+ errors,
140
+ enabled,
141
+ renderers,
142
+ cells,
143
+ rowPath,
144
+ isValid,
145
+ uischema,
146
+ }: NonEmptyRowComponentProps) {
147
+ const propNames = getValidColumnProps(schema);
148
+ const propScopes = (uischema as ControlElement)?.scope
149
+ ? propNames.map((name) => {
150
+ return `#/properties/${name}`;
151
+ })
152
+ : [];
153
+
154
+ const scopesInElements = extractScopesFromUISchema(uischema);
155
+ const scopesNotInElements = propScopes.filter((scope) => {
156
+ return !scopesInElements.includes(scope);
157
+ });
158
+
159
+ /* Create default elements for scope not defined in the uischema
160
+ * future work: merge the options
161
+ */
162
+ const uiSchemaElementsForNotDefined = {
163
+ type: uischema?.options?.defaultType || 'VerticalLayout',
164
+ elements: scopesNotInElements.map((scope) => {
165
+ return {
166
+ type: 'Control',
167
+ scope,
168
+ };
169
+ }),
170
+ };
171
+ return (
172
+ <>
173
+ {
174
+ // eslint-disable-next-line
175
+ (uischema as Layout)?.elements?.map((element: UISchemaElement) => {
176
+ return (
177
+ <JsonFormsDispatch
178
+ key={rowPath}
179
+ schema={schema}
180
+ uischema={element}
181
+ path={rowPath}
182
+ enabled={enabled}
183
+ renderers={renderers}
184
+ cells={cells}
185
+ />
186
+ );
187
+ })
188
+ }
189
+ <JsonFormsDispatch
190
+ schema={schema}
191
+ uischema={uiSchemaElementsForNotDefined}
192
+ path={rowPath}
193
+ enabled={enabled}
194
+ renderers={renderers}
195
+ cells={cells}
196
+ />
197
+ <FormHelperText error={!isValid}>{!isValid && errors}</FormHelperText>
198
+ </>
199
+ );
200
+ });
201
+
202
+ const NonEmptyCell = (ownProps: OwnPropsOfNonEmptyCell) => {
203
+ const ctx = useJsonForms();
204
+ const emptyCellProps = ctxToNonEmptyCellProps(ctx, ownProps);
205
+ const isValid = isEmpty(emptyCellProps.errors);
206
+
207
+ return <NonEmptyCellComponent {...emptyCellProps} isValid={isValid} />;
208
+ };
209
+
210
+ interface NonEmptyRowProps {
211
+ childPath: string;
212
+ schema: JsonSchema;
213
+ rowIndex: number;
214
+ showSortButtons: boolean;
215
+ enabled: boolean;
216
+ cells?: JsonFormsCellRendererRegistryEntry[];
217
+ path: string;
218
+ translations: ArrayTranslations;
219
+ uischema: ControlElement;
220
+ }
221
+
222
+ const NonEmptyRowComponent = ({
223
+ childPath,
224
+ schema,
225
+ rowIndex,
226
+ openDeleteDialog,
227
+ enabled,
228
+ cells,
229
+ path,
230
+ translations,
231
+ uischema,
232
+ }: NonEmptyRowProps & WithDeleteDialogSupport) => {
233
+ return (
234
+ <div key={childPath}>
235
+ {enabled ? (
236
+ <GoAContainer>
237
+ <GoAGrid minChildWidth="30ch">
238
+ <GoAIconButton
239
+ icon="trash"
240
+ aria-label={translations.removeAriaLabel}
241
+ onClick={() => openDeleteDialog(childPath, rowIndex)}
242
+ ></GoAIconButton>
243
+ </GoAGrid>
244
+ {GenerateRows(NonEmptyCell, schema, childPath, enabled, cells, uischema)}
245
+ </GoAContainer>
246
+ ) : null}
247
+ </div>
248
+ );
249
+ };
250
+ export const NonEmptyList = React.memo(NonEmptyRowComponent);
251
+ interface TableRowsProp {
252
+ data: number;
253
+ path: string;
254
+ schema: JsonSchema;
255
+ uischema: ControlElement;
256
+ //eslint-disable-next-line
257
+ config?: any;
258
+ enabled: boolean;
259
+ cells?: JsonFormsCellRendererRegistryEntry[];
260
+ translations: ArrayTranslations;
261
+ }
262
+ const ObjectArrayList = ({
263
+ data,
264
+ path,
265
+ schema,
266
+ openDeleteDialog,
267
+ uischema,
268
+ config,
269
+ enabled,
270
+ cells,
271
+ translations,
272
+ }: TableRowsProp & WithDeleteDialogSupport) => {
273
+ const isEmptyList = data === 0;
274
+
275
+ if (isEmptyList) {
276
+ return <EmptyList numColumns={getValidColumnProps(schema).length + 1} translations={translations} />;
277
+ }
278
+
279
+ const appliedUiSchemaOptions = merge({}, config, uischema.options);
280
+
281
+ return (
282
+ <>
283
+ {range(data).map((index: number) => {
284
+ const childPath = Paths.compose(path, `${index}`);
285
+
286
+ return (
287
+ <NonEmptyList
288
+ key={childPath}
289
+ childPath={childPath}
290
+ rowIndex={index}
291
+ schema={schema}
292
+ openDeleteDialog={openDeleteDialog}
293
+ showSortButtons={appliedUiSchemaOptions.showSortButtons || appliedUiSchemaOptions.showArrayTableSortButtons}
294
+ enabled={enabled}
295
+ cells={cells}
296
+ path={path}
297
+ uischema={uischema}
298
+ translations={translations}
299
+ />
300
+ );
301
+ })}
302
+ </>
303
+ );
304
+ };
305
+
306
+ // eslint-disable-next-line
307
+ export class ObjectArrayControl extends React.Component<ArrayLayoutProps & WithDeleteDialogSupport, any> {
308
+ // eslint-disable-next-line
309
+ addItem = (path: string, value: any) => this.props.addItem(path, value);
310
+ render() {
311
+ const {
312
+ label,
313
+ path,
314
+ schema,
315
+ rootSchema,
316
+ uischema,
317
+ errors,
318
+ openDeleteDialog,
319
+ visible,
320
+ enabled,
321
+ cells,
322
+ translations,
323
+ data,
324
+ config,
325
+ ...additionalProps
326
+ } = this.props;
327
+
328
+ const controlElement = uischema as ControlElement;
329
+ // eslint-disable-next-line
330
+ const listTitle = label || uischema.options?.title;
331
+
332
+ return (
333
+ <Hidden xsUp={!visible}>
334
+ <div>
335
+ <ToolBarHeader>
336
+ {listTitle && <ObjectArrayTitle>{listTitle}</ObjectArrayTitle>}
337
+ <ObjectArrayToolBar
338
+ errors={errors}
339
+ label={label}
340
+ addItem={this.addItem}
341
+ numColumns={0}
342
+ path={path}
343
+ uischema={controlElement}
344
+ schema={schema}
345
+ rootSchema={rootSchema}
346
+ enabled={enabled}
347
+ translations={translations}
348
+ />
349
+ </ToolBarHeader>
350
+ <div>
351
+ <ObjectArrayList
352
+ path={path}
353
+ schema={schema}
354
+ uischema={uischema}
355
+ enabled={enabled}
356
+ openDeleteDialog={openDeleteDialog}
357
+ translations={translations}
358
+ data={data}
359
+ cells={cells}
360
+ config={config}
361
+ {...additionalProps}
362
+ />
363
+ </div>
364
+ </div>
365
+ </Hidden>
366
+ );
367
+ }
368
+ }
@@ -0,0 +1 @@
1
+ export * from './ObjectArray';
@@ -0,0 +1,13 @@
1
+ import styled from 'styled-components';
2
+
3
+ export const DeleteDialogContent = styled.div`
4
+ margin-bottom: 1rem;
5
+ `;
6
+
7
+ export const ToolBarHeader = styled.div`
8
+ margin-bottom: 1.5rem;
9
+ `;
10
+
11
+ export const ObjectArrayTitle = styled.h2`
12
+ margin-bottom: 1.5rem;
13
+ `;
@@ -0,0 +1,4 @@
1
+ export * from './Inputs';
2
+ export * from './FormStepper';
3
+ export * from './FileUploader';
4
+ export * from './ObjectArray';
@@ -0,0 +1,53 @@
1
+ /**
2
+ * The error control is invoked whenever no other control can be found. It is used to display
3
+ * an error to the developers indicating that they have misconfigured one or more of their schemas and
4
+ * that they need to take a close look. It will attempt to provide information to guide the developer
5
+ * toward fixing the error.
6
+ */
7
+
8
+ import React from 'react';
9
+ import { ControlProps, JsonSchema, RankedTester, rankWith } from '@jsonforms/core';
10
+ import { withJsonFormsControlProps } from '@jsonforms/react';
11
+ import { isNullSchema, isValidJsonObject } from './errorCheck';
12
+ import { getUISchemaErrors } from './schemaValidation';
13
+ import { MessageControl } from './MessageControl';
14
+
15
+ const isValidJsonSchema = (schema: JsonSchema): string | null => {
16
+ if (isNullSchema(schema)) {
17
+ return '';
18
+ }
19
+ if (!isValidJsonObject(schema)) {
20
+ return 'Unable to render: json schema is not valid.';
21
+ }
22
+ return null;
23
+ };
24
+
25
+ // Some 'errors' need not be reported, but we want to handle them
26
+ // here. e.g. A layout with empty elements should be quietly ignored.
27
+ // this is handled by the errors !== '' check.
28
+ const ErrorControl = (props: ControlProps): JSX.Element => {
29
+ const { schema, uischema } = props;
30
+ // Report data schema errors over ui schema ones, as errors in the former
31
+ // can cause cascading errors in the latter.
32
+ const dataSchemaErrors = isValidJsonSchema(schema);
33
+ if (dataSchemaErrors && dataSchemaErrors !== '') {
34
+ return <p>{dataSchemaErrors}</p>;
35
+ }
36
+ const uiSchemaErrors = getUISchemaErrors(uischema, schema);
37
+ if (uiSchemaErrors && uiSchemaErrors !== '') {
38
+ return MessageControl(uiSchemaErrors);
39
+ }
40
+ return <span />;
41
+ };
42
+
43
+ /**
44
+ * Note: by returning a rank of 1000, we are saying that this renderer is very important,
45
+ * one that must get used if there are any errors whatsoever.
46
+ */
47
+ export const GoAErrorControlTester: RankedTester = rankWith(1000, (uischema, schema, context) => {
48
+ const validJsonSchema = isValidJsonSchema(schema);
49
+ const validUiSchema = getUISchemaErrors(uischema, schema);
50
+ return validUiSchema != null || validJsonSchema != null;
51
+ });
52
+
53
+ export default withJsonFormsControlProps(ErrorControl);
@@ -0,0 +1,19 @@
1
+ import { GoACallout } from '@abgov/react-components-new';
2
+ import React from 'react';
3
+
4
+ /**
5
+ * Used internally by registered Controls, the MessageControl
6
+ * is used to display an error message if a component cannot be rendered
7
+ * due to input errors - typically from options.componentProps.
8
+ *
9
+ * NOTE: The component itself is not, and should not, be registered.
10
+ *
11
+ * @param message the message to be displayed
12
+ *
13
+ * @returns component for displaying the message in the correct style
14
+ */
15
+
16
+ // TODO: Add styling
17
+ export const MessageControl = (message: string): JSX.Element => {
18
+ return <GoACallout type="emergency">{message}</GoACallout>;
19
+ };
@@ -0,0 +1,98 @@
1
+ import {
2
+ errCategorizationHasNoElements,
3
+ errCategorizationHasNoVariant,
4
+ errCategorizationHasNonCategories,
5
+ errNoElements,
6
+ getUISchemaErrors,
7
+ } from './schemaValidation';
8
+
9
+ const dataSchema = {
10
+ type: 'object',
11
+ properties: {
12
+ firstName: {
13
+ type: 'string',
14
+ },
15
+ birthDate: {
16
+ type: 'string',
17
+ format: 'date',
18
+ },
19
+ },
20
+ };
21
+
22
+ const validCategorization = {
23
+ type: 'Categorization',
24
+ elements: [
25
+ {
26
+ type: 'Category',
27
+ elements: [
28
+ {
29
+ type: 'Category',
30
+ elements: [
31
+ {
32
+ type: 'Control',
33
+ scope: '#/properties/firstName',
34
+ },
35
+ ],
36
+ },
37
+ ],
38
+ },
39
+ ],
40
+ options: { variant: 'stepper' },
41
+ };
42
+ const categorizationWithNoCategories = {
43
+ type: 'Categorization',
44
+ elements: [
45
+ {
46
+ type: 'Category',
47
+ elements: [{ type: 'Control', scope: '#/properties/firstName' }],
48
+ },
49
+ {
50
+ type: 'VerticalLayout',
51
+ elements: [{ type: 'Control', scope: '#/properties/birthDate' }],
52
+ },
53
+ ],
54
+ options: { variant: 'stepper' },
55
+ };
56
+ const categorizationWithNoElements = {
57
+ type: 'Categorization',
58
+ options: { variant: 'stepper' },
59
+ };
60
+ const categoryHasNoElements = {
61
+ type: 'Categorization',
62
+ elements: [{ type: 'Category' }],
63
+ options: { variant: 'stepper' },
64
+ };
65
+ const categorizationWithNoVariant = {
66
+ type: 'Categorization',
67
+ elements: [{ type: 'Category', elements: [] }],
68
+ };
69
+
70
+ describe('check error processing', () => {
71
+ describe('categorization validation', () => {
72
+ it('ignores valid categorizations', () => {
73
+ const err = getUISchemaErrors(validCategorization, dataSchema);
74
+ expect(err).toBe(null);
75
+ });
76
+
77
+ it('can detect non-category element', () => {
78
+ const err = getUISchemaErrors(categorizationWithNoCategories, dataSchema);
79
+ console.log(err);
80
+ expect(err).toMatch(errCategorizationHasNonCategories);
81
+ });
82
+
83
+ it('can detect categorization without elements', () => {
84
+ const err = getUISchemaErrors(categorizationWithNoElements, dataSchema);
85
+ expect(err).toMatch(errCategorizationHasNoElements);
86
+ });
87
+
88
+ it('can detect a Category with no elements', () => {
89
+ const err = getUISchemaErrors(categoryHasNoElements, dataSchema);
90
+ expect(err).toMatch(errNoElements('Category'));
91
+ });
92
+
93
+ it('can detect Categorization with no variant', () => {
94
+ const err = getUISchemaErrors(categorizationWithNoVariant, dataSchema);
95
+ expect(err).toMatch(errCategorizationHasNoVariant);
96
+ });
97
+ });
98
+ });