@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.
- package/.babelrc +12 -0
- package/.eslintrc.json +36 -0
- package/.releaserc.json +25 -0
- package/README.md +251 -0
- package/jest.config.ts +11 -0
- package/package.json +17 -0
- package/project.json +55 -0
- package/rollup.config.js +14 -0
- package/src/index.ts +166 -0
- package/src/lib/Additional/HelpContent.tsx +95 -0
- package/src/lib/Additional/index.ts +1 -0
- package/src/lib/Additional/styled-components.ts +27 -0
- package/src/lib/Cells/DateCell.tsx +10 -0
- package/src/lib/Cells/IntegerCell.tsx +10 -0
- package/src/lib/Cells/NumberCell.tsx +10 -0
- package/src/lib/Cells/TextCell.tsx +10 -0
- package/src/lib/Cells/TimeCell.tsx +10 -0
- package/src/lib/Cells/index.tsx +14 -0
- package/src/lib/Context/index.tsx +172 -0
- package/src/lib/Controls/FileUploader/ContextMenu.tsx +50 -0
- package/src/lib/Controls/FileUploader/FileUploaderControl.tsx +131 -0
- package/src/lib/Controls/FileUploader/FileUploaderTester.tsx +3 -0
- package/src/lib/Controls/FileUploader/index.tsx +2 -0
- package/src/lib/Controls/FileUploader/styled-components.tsx +10 -0
- package/src/lib/Controls/FormStepper/FormStepperControl.tsx +269 -0
- package/src/lib/Controls/FormStepper/FormStepperTester.tsx +22 -0
- package/src/lib/Controls/FormStepper/index.tsx +2 -0
- package/src/lib/Controls/FormStepper/styled-components.tsx +17 -0
- package/src/lib/Controls/Inputs/InputBaseControl.tsx +52 -0
- package/src/lib/Controls/Inputs/InputBooleanControl.tsx +67 -0
- package/src/lib/Controls/Inputs/InputBooleanRadioControl.tsx +74 -0
- package/src/lib/Controls/Inputs/InputDateControl.tsx +90 -0
- package/src/lib/Controls/Inputs/InputDateTimeControl.tsx +46 -0
- package/src/lib/Controls/Inputs/InputEnum.tsx +74 -0
- package/src/lib/Controls/Inputs/InputEnumAutoComplete.tsx +73 -0
- package/src/lib/Controls/Inputs/InputEnumRadios.tsx +43 -0
- package/src/lib/Controls/Inputs/InputIntegerControl.tsx +63 -0
- package/src/lib/Controls/Inputs/InputMultiLineTextControl.tsx +63 -0
- package/src/lib/Controls/Inputs/InputNumberControl.tsx +63 -0
- package/src/lib/Controls/Inputs/InputTextControl.tsx +62 -0
- package/src/lib/Controls/Inputs/InputTimeControl.tsx +46 -0
- package/src/lib/Controls/Inputs/index.tsx +13 -0
- package/src/lib/Controls/Inputs/inputControl.spec.ts +84 -0
- package/src/lib/Controls/Inputs/type.ts +3 -0
- package/src/lib/Controls/ObjectArray/DeleteDialog.tsx +49 -0
- package/src/lib/Controls/ObjectArray/ObjectArray.tsx +59 -0
- package/src/lib/Controls/ObjectArray/ObjectArrayToolBar.tsx +51 -0
- package/src/lib/Controls/ObjectArray/ObjectListControl.tsx +368 -0
- package/src/lib/Controls/ObjectArray/index.tsx +1 -0
- package/src/lib/Controls/ObjectArray/styled-components.tsx +13 -0
- package/src/lib/Controls/index.tsx +4 -0
- package/src/lib/ErrorHandling/GoAErrorControl.tsx +53 -0
- package/src/lib/ErrorHandling/MessageControl.tsx +19 -0
- package/src/lib/ErrorHandling/categorizationValidation.spec.ts +98 -0
- package/src/lib/ErrorHandling/controlValildation.spec.ts +57 -0
- package/src/lib/ErrorHandling/errorCheck.spec.ts +185 -0
- package/src/lib/ErrorHandling/errorCheck.tsx +86 -0
- package/src/lib/ErrorHandling/layoutValildation.spec.ts +47 -0
- package/src/lib/ErrorHandling/otherValildation.spec.ts +74 -0
- package/src/lib/ErrorHandling/schemaValidation.ts +115 -0
- package/src/lib/common/Grid.tsx +55 -0
- package/src/lib/jsonforms-components.module.scss +7 -0
- package/src/lib/jsonforms-components.spec.tsx +10 -0
- package/src/lib/jsonforms-components.tsx +14 -0
- package/src/lib/layouts/GroupControl.tsx +25 -0
- package/src/lib/layouts/HorizontalLayoutControl.tsx +30 -0
- package/src/lib/layouts/VerticalLayoutControl.tsx +28 -0
- package/src/lib/layouts/index.ts +3 -0
- package/src/lib/util/layout.tsx +68 -0
- package/src/lib/util/schemaUtils.ts +9 -0
- package/src/lib/util/stringUtils.ts +98 -0
- package/src/lib/util/style-component.ts +8 -0
- package/tsconfig.json +20 -0
- package/tsconfig.lib.json +19 -0
- 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,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
|
+
});
|