@evoke-platform/ui-components 1.15.1 → 1.17.0
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/dist/published/components/custom/CriteriaBuilder/CriteriaBuilder.js +3 -0
- package/dist/published/components/custom/Form/utils.d.ts +2 -2
- package/dist/published/components/custom/FormField/AddressFieldComponent/addressFieldComponent.js +1 -1
- package/dist/published/components/custom/FormField/BooleanSelect/BooleanSelect.js +15 -7
- package/dist/published/components/custom/FormField/InputFieldComponent/InputFieldComponent.js +1 -1
- package/dist/published/components/custom/FormField/Select/Select.js +1 -1
- package/dist/published/components/custom/FormV2/FormRenderer.js +5 -5
- package/dist/published/components/custom/FormV2/FormRendererContainer.js +20 -10
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/DropdownRepeatableFieldInput.js +3 -0
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/RepeatableField.js +1 -1
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/Document.js +1 -0
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/Image.js +1 -0
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/UserProperty.js +2 -0
- package/dist/published/components/custom/FormV2/components/FormletRenderer.d.ts +6 -0
- package/dist/published/components/custom/FormV2/components/FormletRenderer.js +30 -0
- package/dist/published/components/custom/FormV2/components/HtmlView.js +12 -9
- package/dist/published/components/custom/FormV2/components/RecursiveEntryRenderer.js +8 -10
- package/dist/published/components/custom/FormV2/components/utils.d.ts +6 -2
- package/dist/published/components/custom/FormV2/components/utils.js +54 -4
- package/dist/published/components/custom/FormV2/tests/FormRenderer.test.js +48 -5
- package/dist/published/components/custom/FormV2/tests/FormRendererContainer.test.js +279 -35
- package/dist/published/stories/FormRendererData.d.ts +15 -0
- package/dist/published/stories/FormRendererData.js +63 -0
- package/dist/published/stories/sharedMswHandlers.js +4 -2
- package/package.json +2 -2
|
@@ -463,6 +463,9 @@ const CriteriaBuilder = (props) => {
|
|
|
463
463
|
borderStyle: 'hidden',
|
|
464
464
|
background: '#fff',
|
|
465
465
|
},
|
|
466
|
+
'.ruleGroup:not(:has(.rule, .ruleGroup .ruleGroup))': {
|
|
467
|
+
backgroundColor: 'transparent',
|
|
468
|
+
},
|
|
466
469
|
'.ruleGroup-header': {
|
|
467
470
|
display: 'block',
|
|
468
471
|
},
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ActionInput, ActionInputType, ApiServices, AxiosError, FormEntry, InputParameter, InputParameterReference, Obj, ObjectInstance, Property, PropertyType, UserAccount } from '@evoke-platform/context';
|
|
1
|
+
import { ActionInput, ActionInputType, ApiServices, AxiosError, FormEntry, FormletReference, InputParameter, InputParameterReference, Obj, ObjectInstance, Property, PropertyType, UserAccount } from '@evoke-platform/context';
|
|
2
2
|
import { ReactComponent } from '@formio/react';
|
|
3
3
|
import { LocalDateTime } from '@js-joda/core';
|
|
4
4
|
import { AutocompleteOption } from '../../core';
|
|
@@ -7,7 +7,7 @@ export declare function determineComponentType(properties: Property[], parameter
|
|
|
7
7
|
export declare function determineParameterType(componentType: string): PropertyType;
|
|
8
8
|
export declare function getFlattenEntries(entries: FormEntry[]): InputParameterReference[];
|
|
9
9
|
export declare function convertFormToComponents(entries: FormEntry[], parameters: InputParameter[], object: Obj): ActionInput[];
|
|
10
|
-
export declare function convertComponentsToForm(components: ActionInput[]): FormEntry[];
|
|
10
|
+
export declare function convertComponentsToForm(components: ActionInput[]): Exclude<FormEntry, FormletReference>[];
|
|
11
11
|
export declare function getMiddleObjectFilter(property: Property, instance: ObjectInstance): {
|
|
12
12
|
where: {
|
|
13
13
|
[x: string]: string;
|
package/dist/published/components/custom/FormField/AddressFieldComponent/addressFieldComponent.js
CHANGED
|
@@ -48,7 +48,7 @@ const AddressFieldComponent = (props) => {
|
|
|
48
48
|
!mask ? (React.createElement(TextField, { id: id, inputRef: textFieldRef, onChange: !readOnly ? handleChange : undefined, error: error, errorMessage: errorMessage, value: value, fullWidth: true, onBlur: onBlur, size: size ?? 'medium', placeholder: readOnly ? undefined : placeholder, InputProps: {
|
|
49
49
|
type: 'search',
|
|
50
50
|
autoComplete: 'off',
|
|
51
|
-
}, required: required, readOnly: readOnly, multiline: property.type === 'string' && !readOnly && isMultiLineText, rows: isMultiLineText ? (rows ? rows : 3) : undefined, ...(additionalProps ?? {}) })) : (React.createElement(InputMask, { mask: mask, maskChar: inputMaskPlaceholderChar ?? '_', value: value, onChange: !readOnly ? handleChange : undefined, onBlur: onBlur, alwaysShowMask: true }, (() => (React.createElement(TextField, { id: id, inputRef: textFieldRef, sx: readOnly
|
|
51
|
+
}, required: required, readOnly: readOnly, multiline: property.type === 'string' && !readOnly && isMultiLineText, rows: isMultiLineText ? (rows ? rows : 3) : undefined, ...(additionalProps ?? {}), sx: { backgroundColor: 'white' } })) : (React.createElement(InputMask, { mask: mask, maskChar: inputMaskPlaceholderChar ?? '_', value: value, onChange: !readOnly ? handleChange : undefined, onBlur: onBlur, alwaysShowMask: true }, (() => (React.createElement(TextField, { id: id, inputRef: textFieldRef, sx: readOnly
|
|
52
52
|
? {
|
|
53
53
|
'& .MuiOutlinedInput-notchedOutline': {
|
|
54
54
|
border: 'none',
|
|
@@ -4,11 +4,17 @@ import { CheckBox as CheckBoxIcon, CheckBoxOutlineBlank, Help, ToggleOff, Toggle
|
|
|
4
4
|
import { defaultTheme } from '../../../../theme/defaultTheme';
|
|
5
5
|
import { Autocomplete, Checkbox, FormControl, FormControlLabel, FormHelperText, IconButton, Switch, TextField, Tooltip, Typography, } from '../../../core';
|
|
6
6
|
import InputFieldComponent from '../InputFieldComponent/InputFieldComponent';
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
whiteSpace: 'normal',
|
|
10
|
-
|
|
11
|
-
|
|
7
|
+
import { Box } from '../../../layout';
|
|
8
|
+
const styles = {
|
|
9
|
+
descriptionStyles: { color: '#999 !important', whiteSpace: 'normal', paddingBottom: '4px', marginX: 0 },
|
|
10
|
+
checkboxIconBox: {
|
|
11
|
+
backgroundColor: 'white',
|
|
12
|
+
width: '18px',
|
|
13
|
+
height: '18px',
|
|
14
|
+
display: 'flex',
|
|
15
|
+
alignItems: 'center',
|
|
16
|
+
justifyContent: 'center',
|
|
17
|
+
},
|
|
12
18
|
};
|
|
13
19
|
const BooleanSelect = (props) => {
|
|
14
20
|
const { id, property, defaultValue, error, errorMessage, readOnly, size, displayOption, label, strictlyTrue, tooltip, description, placeholder, onBlur, additionalProps, } = props;
|
|
@@ -31,7 +37,7 @@ const BooleanSelect = (props) => {
|
|
|
31
37
|
},
|
|
32
38
|
];
|
|
33
39
|
const descriptionComponent = () => {
|
|
34
|
-
return (description && (React.createElement(FormHelperText, { sx: descriptionStyles, component: Typography }, parse(description))));
|
|
40
|
+
return (description && (React.createElement(FormHelperText, { sx: styles.descriptionStyles, component: Typography }, parse(description))));
|
|
35
41
|
};
|
|
36
42
|
const labelComponent = () => {
|
|
37
43
|
return (React.createElement(Typography, { component: "span", variant: "body2", sx: { wordWrap: 'break-word', ...defaultTheme.typography.body2 } },
|
|
@@ -58,7 +64,9 @@ const BooleanSelect = (props) => {
|
|
|
58
64
|
return displayOption === 'dropdown' ? (React.createElement(Autocomplete, { renderInput: (params) => (React.createElement(TextField, { ...params, error: error, errorMessage: errorMessage, onBlur: onBlur, fullWidth: true, sx: { background: 'white' }, placeholder: placeholder, size: size ?? 'medium' })), value: booleanOptions.find((opt) => opt.value === value) ?? '', onChange: (e, selectedValue) => handleChange(selectedValue.value), isOptionEqualToValue: (option, val) => option?.value === val?.value, options: booleanOptions, disableClearable: true, sx: { background: 'white', borderRadius: '8px' }, ...(additionalProps ?? {}), sortBy: "NONE", required: strictlyTrue })) : (React.createElement(FormControl, { required: strictlyTrue, error: error, fullWidth: true },
|
|
59
65
|
React.createElement(FormControlLabel, { labelPlacement: "end", label: labelComponent(), sx: { marginLeft: '-8px' }, control: displayOption === 'switch' ? (React.createElement(Switch, { id: id, "aria-required": strictlyTrue, "aria-invalid": error, size: size ?? 'medium', name: property.id, checked: value, onChange: (e) => handleChange(e.target.checked), sx: {
|
|
60
66
|
alignSelf: 'start',
|
|
61
|
-
}, ...(additionalProps ?? {}) })) : (React.createElement(Checkbox, { id: id, "aria-required": strictlyTrue, "aria-invalid": error, size: size ?? 'medium', checked: value, name: property.id, onChange: (e) => handleChange(e.target.checked), sx:
|
|
67
|
+
}, ...(additionalProps ?? {}) })) : (React.createElement(Checkbox, { id: id, "aria-required": strictlyTrue, "aria-invalid": error, size: size ?? 'medium', checked: value, name: property.id, onChange: (e) => handleChange(e.target.checked), icon: React.createElement(Box, { sx: styles.checkboxIconBox },
|
|
68
|
+
React.createElement(CheckBoxOutlineBlank, { fontSize: size ?? 'medium' })), checkedIcon: React.createElement(Box, { sx: styles.checkboxIconBox },
|
|
69
|
+
React.createElement(CheckBoxIcon, { fontSize: size ?? 'medium' })), sx: {
|
|
62
70
|
alignSelf: 'start',
|
|
63
71
|
padding: '4px 9px 9px 9px',
|
|
64
72
|
}, ...(additionalProps ?? {}) })) }),
|
package/dist/published/components/custom/FormField/InputFieldComponent/InputFieldComponent.js
CHANGED
|
@@ -56,7 +56,6 @@ const InputFieldComponent = (props) => {
|
|
|
56
56
|
: property.enum, onChange: handleSelectChange, renderInput: (params) => (React.createElement(TextField, { ...params, value: value, error: error, errorMessage: errorMessage, fullWidth: true, onBlur: onBlur, size: size ?? 'medium', placeholder: placeholder })), disableClearable: true, value: value, isOptionEqualToValue: (option, value) => {
|
|
57
57
|
return option.value === value;
|
|
58
58
|
}, error: error, required: required, inputValue: inputValue ?? '', onInputChange: handleInputValueChange, ...(additionalProps ?? {}) })) : !mask || isValueProtected ? (React.createElement(TextField, { id: id, sx: {
|
|
59
|
-
background: 'white',
|
|
60
59
|
borderRadius: '8px',
|
|
61
60
|
...(readOnly && {
|
|
62
61
|
'& .MuiOutlinedInput-notchedOutline': {
|
|
@@ -67,6 +66,7 @@ const InputFieldComponent = (props) => {
|
|
|
67
66
|
backgroundColor: '#f4f6f8',
|
|
68
67
|
},
|
|
69
68
|
}),
|
|
69
|
+
backgroundColor: 'white',
|
|
70
70
|
}, error: error, errorMessage: errorMessage, value: value, onChange: !readOnly ? handleChange : undefined, InputProps: { ...InputProps, endAdornment, readOnly: readOnly }, required: required, fullWidth: true, onBlur: onBlur, placeholder: readOnly ? undefined : placeholder, size: size ?? 'medium', type: property.type === 'integer' ? 'number' : 'text', multiline: property.type === 'string' && !readOnly && isMultiLineText, rows: isMultiLineText ? (rows ? rows : 3) : undefined, ...(additionalProps ?? {}) })) : (React.createElement(InputMask, { mask: mask, maskChar: inputMaskPlaceholderChar ?? '_', value: value, onChange: !readOnly ? handleChange : undefined, onBlur: onBlur, alwaysShowMask: true }, (() => (React.createElement(TextField, { id: id, sx: readOnly
|
|
71
71
|
? {
|
|
72
72
|
'& .MuiOutlinedInput-notchedOutline': {
|
|
@@ -161,7 +161,7 @@ const Select = (props) => {
|
|
|
161
161
|
React.createElement(Typography, { variant: "caption" }, "Clear Selection"))))) : (React.createElement(Autocomplete, { multiple: property?.type === 'array', id: id, sortBy: sortBy, renderInput: (params) => (React.createElement(TextField, { ...params, value: value, fullWidth: true, onBlur: onBlur, inputProps: {
|
|
162
162
|
...params.inputProps,
|
|
163
163
|
'aria-describedby': isCombobox ? `${id}-instructions` : undefined,
|
|
164
|
-
} })), value: value ?? (property?.type === 'array' ? [] : undefined), onChange: handleChange, options: selectOptions ?? property?.enum ?? [], inputValue: inputValue ?? '', error: error, errorMessage: errorMessage, required: required, onInputChange: handleInputValueChange, size: size, filterOptions: (options, params) => {
|
|
164
|
+
}, sx: { backgroundColor: 'white', borderRadius: '8px' } })), value: value ?? (property?.type === 'array' ? [] : undefined), onChange: handleChange, options: selectOptions ?? property?.enum ?? [], inputValue: inputValue ?? '', error: error, errorMessage: errorMessage, required: required, onInputChange: handleInputValueChange, size: size, filterOptions: (options, params) => {
|
|
165
165
|
const filtered = filter(options, params);
|
|
166
166
|
const { inputValue } = params;
|
|
167
167
|
// Suggest to the user to add a new value.
|
|
@@ -54,17 +54,17 @@ const FormRendererInternal = (props) => {
|
|
|
54
54
|
enabled: !!objectId,
|
|
55
55
|
});
|
|
56
56
|
const updatedEntries = useMemo(() => {
|
|
57
|
-
return
|
|
57
|
+
return assignIdsToSectionsAndRichText(entries, object, parameters);
|
|
58
58
|
}, [entries, object, parameters]);
|
|
59
59
|
useEffect(() => {
|
|
60
|
-
if (!object)
|
|
60
|
+
if (objectId && !object)
|
|
61
61
|
return;
|
|
62
|
-
const action = object
|
|
62
|
+
const action = object?.actions?.find((a) => a.id === actionId);
|
|
63
63
|
setAction(action);
|
|
64
64
|
// if forms action is synced with object properties then convertPropertiesToParams
|
|
65
|
-
setParameters(action?.parameters ?? convertPropertiesToParams(object));
|
|
65
|
+
setParameters(action?.parameters ?? (object ? convertPropertiesToParams(object) : []));
|
|
66
66
|
setIsInitializing(false);
|
|
67
|
-
}, [object, actionId]);
|
|
67
|
+
}, [object, actionId, objectId]);
|
|
68
68
|
useEffect(() => {
|
|
69
69
|
const currentValues = getValues();
|
|
70
70
|
if (value) {
|
|
@@ -9,7 +9,7 @@ import ErrorComponent from '../ErrorComponent';
|
|
|
9
9
|
import ConditionalQueryClientProvider from './components/ConditionalQueryClientProvider';
|
|
10
10
|
import { evalDefaultVals, processValueUpdate } from './components/DefaultValues';
|
|
11
11
|
import Header from './components/Header';
|
|
12
|
-
import { convertPropertiesToParams, createFileLinks, deleteDocuments, encodePageSlug, extractAllCriteria, extractPresetValuesFromCriteria, extractPresetValuesFromDynamicDefaultValues, formatSubmission, getEntryId, getPrefixedUrl, getUnnestedEntries, isAddressProperty, isEmptyWithDefault, plainTextToRtf, useFormById, } from './components/utils';
|
|
12
|
+
import { convertPropertiesToParams, createFileLinks, deleteDocuments, encodePageSlug, extractAllCriteria, extractPresetValuesFromCriteria, extractPresetValuesFromDynamicDefaultValues, formatSubmission, getEntryId, getPrefixedUrl, getUnnestedEntries, getVisibleEditableFieldIds, isAddressProperty, isEmptyWithDefault, plainTextToRtf, useFormById, } from './components/utils';
|
|
13
13
|
import FormRenderer from './FormRenderer';
|
|
14
14
|
import { DepGraph } from 'dependency-graph';
|
|
15
15
|
// Wrapper to provide QueryClient context for FormRendererContainer if this is not a nested form
|
|
@@ -164,7 +164,11 @@ function FormRendererContainerInner(props) {
|
|
|
164
164
|
set(result, fieldId, fieldValue);
|
|
165
165
|
}
|
|
166
166
|
}
|
|
167
|
-
else if (entry.type !== 'sections' &&
|
|
167
|
+
else if (entry.type !== 'sections' &&
|
|
168
|
+
entry.type !== 'columns' &&
|
|
169
|
+
entry.type !== 'content' &&
|
|
170
|
+
// there cannot be formlets here since the `form` is the effective form
|
|
171
|
+
entry.type !== 'formlet') {
|
|
168
172
|
const parameter = parameters?.find((param) => param.id === fieldId);
|
|
169
173
|
if (associatedObject?.propertyId === fieldId &&
|
|
170
174
|
associatedObject?.instanceId &&
|
|
@@ -183,9 +187,6 @@ function FormRendererContainerInner(props) {
|
|
|
183
187
|
console.error(error);
|
|
184
188
|
}
|
|
185
189
|
}
|
|
186
|
-
else if (entry.type === 'formlet') {
|
|
187
|
-
// TODO: this should eventually fetch the formletId then get the fields and default values of those fields
|
|
188
|
-
}
|
|
189
190
|
else if (entry.type !== 'readonlyField') {
|
|
190
191
|
if (isEmptyWithDefault(fieldValue, entry, result)) {
|
|
191
192
|
if (fieldId && parameters && parameters.length > 0) {
|
|
@@ -303,6 +304,10 @@ function FormRendererContainerInner(props) {
|
|
|
303
304
|
// Deep clone to avoid reference issues
|
|
304
305
|
setLastSavedData(cloneDeep(defaultValues));
|
|
305
306
|
}
|
|
307
|
+
else {
|
|
308
|
+
// if there is a form with no entries
|
|
309
|
+
setFormData({});
|
|
310
|
+
}
|
|
306
311
|
};
|
|
307
312
|
getInitialValues();
|
|
308
313
|
}, [instanceId, instance, flattenFormEntries, getDefaultValues]);
|
|
@@ -404,6 +409,12 @@ function FormRendererContainerInner(props) {
|
|
|
404
409
|
if (!form?.autosaveActionId || !formDataRef.current) {
|
|
405
410
|
return;
|
|
406
411
|
}
|
|
412
|
+
const visibleEditableFieldIds = getVisibleEditableFieldIds(form.entries ?? [], instance, formDataRef.current);
|
|
413
|
+
const allowedParameterIds = parameters?.filter((parameter) => parameter.type !== 'collection').map((parameter) => parameter.id) ?? [];
|
|
414
|
+
const autosaveFieldIds = visibleEditableFieldIds.filter((id) => allowedParameterIds.includes(id));
|
|
415
|
+
if (!autosaveFieldIds.includes(fieldId)) {
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
407
418
|
const currentValue = get(formDataRef.current, fieldId);
|
|
408
419
|
const lastValue = get(lastSavedData, fieldId);
|
|
409
420
|
if (isEqual(currentValue, lastValue)) {
|
|
@@ -412,14 +423,13 @@ function FormRendererContainerInner(props) {
|
|
|
412
423
|
try {
|
|
413
424
|
setIsSaving(true);
|
|
414
425
|
const cleanedData = removeUneditedProtectedValues(formDataRef.current);
|
|
415
|
-
const
|
|
426
|
+
const scopedData = pick(cleanedData, autosaveFieldIds);
|
|
427
|
+
const submission = await formatSubmission(scopedData, apiServices, objectId, instanceId, form, setSnackbarError, undefined, parameters);
|
|
416
428
|
// Handle object instance autosave
|
|
417
429
|
if (instanceId && action?.type === 'update') {
|
|
418
430
|
await apiServices.post(getPrefixedUrl(`/objects/${objectId}/instances/${instanceId}/actions`), {
|
|
419
431
|
actionId: form.autosaveActionId,
|
|
420
|
-
input:
|
|
421
|
-
?.filter((property) => !property.formula && property.type !== 'collection')
|
|
422
|
-
.map((property) => property.id) ?? []),
|
|
432
|
+
input: submission,
|
|
423
433
|
});
|
|
424
434
|
}
|
|
425
435
|
setLastSavedData(cloneDeep(formDataRef.current));
|
|
@@ -456,7 +466,7 @@ function FormRendererContainerInner(props) {
|
|
|
456
466
|
setFormData(newData);
|
|
457
467
|
}
|
|
458
468
|
}
|
|
459
|
-
const isLoading = (instanceId &&
|
|
469
|
+
const isLoading = !form || !sanitizedObject || (instanceId && formDataRef.current === undefined);
|
|
460
470
|
const status = error ? 'error' : isLoading ? 'loading' : 'ready';
|
|
461
471
|
// Compose a header renderer that injects the saving indicator into the rendered header
|
|
462
472
|
const composedRenderHeader = (props) => {
|
|
@@ -451,7 +451,7 @@ const RepeatableField = (props) => {
|
|
|
451
451
|
React.createElement(TableRow, null,
|
|
452
452
|
columns?.map((prop) => (React.createElement(TableCell, { sx: styles.tableCell }, prop.name))),
|
|
453
453
|
canUpdateProperty && React.createElement(TableCell, { sx: { ...styles.tableCell, width: '80px' } }))),
|
|
454
|
-
React.createElement(TableBody,
|
|
454
|
+
React.createElement(TableBody, { sx: { backgroundColor: 'white' } }, relatedInstances?.map((relatedInstance, index) => (React.createElement(TableRow, { key: relatedInstance.id },
|
|
455
455
|
columns?.map((prop) => {
|
|
456
456
|
return (React.createElement(TableCell, { sx: { fontSize: '16px' } }, prop.type === 'document' ? (React.createElement(DocumentViewerCell, { instance: relatedInstance, propertyId: prop.id, setSnackbarError: setSnackbarError })) : (React.createElement(Typography, { key: prop.id, sx: prop.id === 'name'
|
|
457
457
|
? {
|
package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/Document.js
CHANGED
|
@@ -93,6 +93,7 @@ export const Document = (props) => {
|
|
|
93
93
|
border: `1px dashed ${error ? 'red' : uploadDisabled ? '#DFE3E8' : '#858585'}`,
|
|
94
94
|
position: 'relative',
|
|
95
95
|
cursor: uploadDisabled ? 'cursor' : 'pointer',
|
|
96
|
+
backgroundColor: 'white',
|
|
96
97
|
}, ...getRootProps(), onClick: open },
|
|
97
98
|
React.createElement("input", { ...getInputProps({ id }), disabled: uploadDisabled, ...(hasDescription ? { 'aria-describedby': `${id}-description` } : undefined) }),
|
|
98
99
|
React.createElement(Grid, { container: true, sx: { width: '100%' } },
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { useApiServices } from '@evoke-platform/context';
|
|
3
|
+
import { getPrefixedUrl } from './utils';
|
|
4
|
+
import { useQuery } from '@tanstack/react-query';
|
|
5
|
+
import { RecursiveEntryRenderer } from './RecursiveEntryRenderer';
|
|
6
|
+
import { Skeleton, Typography } from '../../../core';
|
|
7
|
+
import { Box } from '../../../layout';
|
|
8
|
+
import { WarningRounded } from '../../../../icons';
|
|
9
|
+
function FormletRenderer(props) {
|
|
10
|
+
const { entry } = props;
|
|
11
|
+
const apiServices = useApiServices();
|
|
12
|
+
const { data: formlet, isLoading } = useQuery({
|
|
13
|
+
queryKey: [entry.formletId, 'formlet'],
|
|
14
|
+
queryFn: () => apiServices.get(getPrefixedUrl(`/formlets/${entry.formletId}`)),
|
|
15
|
+
staleTime: Infinity,
|
|
16
|
+
enabled: !!entry.formletId,
|
|
17
|
+
});
|
|
18
|
+
if (isLoading)
|
|
19
|
+
return React.createElement(Skeleton, null);
|
|
20
|
+
return formlet ? (React.createElement(React.Fragment, null, formlet.entries?.map((formletEntry, index) => (React.createElement(RecursiveEntryRenderer, { key: index, entry: formletEntry }))))) : (React.createElement(Box, { sx: {
|
|
21
|
+
display: 'flex',
|
|
22
|
+
backgroundColor: '#ffc1073b',
|
|
23
|
+
borderRadius: '8px',
|
|
24
|
+
padding: '16.5px 14px',
|
|
25
|
+
marginTop: '6px',
|
|
26
|
+
} },
|
|
27
|
+
React.createElement(WarningRounded, { sx: { paddingRight: '8px' }, color: "warning" }),
|
|
28
|
+
React.createElement(Typography, { variant: "body2", color: "textSecondary" }, "This field was not configured correctly")));
|
|
29
|
+
}
|
|
30
|
+
export default FormletRenderer;
|
|
@@ -33,14 +33,17 @@ const HtmlView = ({ value }) => {
|
|
|
33
33
|
quillRef.current.setContents([]);
|
|
34
34
|
quillRef.current.clipboard.dangerouslyPasteHTML(DOMPurify.sanitize(value));
|
|
35
35
|
}, [value]);
|
|
36
|
-
return (
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
36
|
+
return (
|
|
37
|
+
// Needs to be wrapped in a Box to prevent quill from setting height: 100% on the container, which causes it to overflow its parent and ignore the maxHeight we set on the containerRef (only happens when in a grid item)
|
|
38
|
+
React.createElement(Box, null,
|
|
39
|
+
React.createElement(Box, { sx: {
|
|
40
|
+
width: '100%',
|
|
41
|
+
height: '100%',
|
|
42
|
+
'.ql-container.ql-snow': {
|
|
43
|
+
border: 'none',
|
|
44
|
+
minHeight: 20,
|
|
45
|
+
},
|
|
46
|
+
} },
|
|
47
|
+
React.createElement("div", { ref: containerRef }))));
|
|
45
48
|
};
|
|
46
49
|
export default HtmlView;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useApiServices, useAuthenticationContext, } from '@evoke-platform/context';
|
|
2
2
|
import { WarningRounded } from '@mui/icons-material';
|
|
3
|
+
import { Grid } from '@mui/material';
|
|
3
4
|
import { isEmpty } from 'lodash';
|
|
4
5
|
import React, { useMemo } from 'react';
|
|
5
6
|
import useWidgetSize, { useFormContext } from '../../../../theme/hooks';
|
|
@@ -20,6 +21,7 @@ import FormSections from './FormSections';
|
|
|
20
21
|
import { entryIsVisible, fetchInitialMiddleObjectInstances, fetchMiddleObject, filterEmptySections, getEntryId, getFieldDefinition, isAddressProperty, isOptionEqualToValue, updateCriteriaInputs, } from './utils';
|
|
21
22
|
import HtmlView from './HtmlView';
|
|
22
23
|
import { useQuery } from '@tanstack/react-query';
|
|
24
|
+
import FormletRenderer from './FormletRenderer';
|
|
23
25
|
function getFieldWrapperProps(fieldDefinition, entry, entryId, fieldValue, display, errors, validation) {
|
|
24
26
|
return {
|
|
25
27
|
inputId: entryId,
|
|
@@ -52,8 +54,6 @@ export function RecursiveEntryRenderer(props) {
|
|
|
52
54
|
const display = 'display' in entry ? entry.display : undefined;
|
|
53
55
|
const fieldValue = entry.type === 'readonlyField' ? instance?.[entryId] : getValues ? getValues(entryId) : undefined;
|
|
54
56
|
const fieldDefinition = useMemo(() => {
|
|
55
|
-
if (!object)
|
|
56
|
-
return undefined;
|
|
57
57
|
return getFieldDefinition(entry, object, parameters);
|
|
58
58
|
}, [entry, parameters, object]);
|
|
59
59
|
const validation = fieldDefinition?.validation || {};
|
|
@@ -214,16 +214,14 @@ export function RecursiveEntryRenderer(props) {
|
|
|
214
214
|
? display?.booleanDisplay
|
|
215
215
|
: display?.choicesDisplay?.type && display.choicesDisplay.type, label: display?.label, description: display?.description, tooltip: display?.tooltip, selectOptions: fieldDefinition?.enum, additionalProps: additionalProps, isCombobox: fieldDefinition.nonStrictEnum, strictlyTrue: fieldDefinition.strictlyTrue, protection: objectProperty?.protection })));
|
|
216
216
|
}
|
|
217
|
+
// Forms from the FormV2 widget will get the effective form and will not need this
|
|
218
|
+
// but it's possible for a form passed into the FormRenderer to include a formlet type
|
|
219
|
+
}
|
|
220
|
+
else if (entry.type === 'formlet') {
|
|
221
|
+
return React.createElement(FormletRenderer, { entry: entry });
|
|
217
222
|
}
|
|
218
223
|
else if (entry.type === 'columns') {
|
|
219
|
-
return (React.createElement(
|
|
220
|
-
display: 'flex',
|
|
221
|
-
alignItems: 'flex-start',
|
|
222
|
-
gap: '30px',
|
|
223
|
-
flexDirection: isXs ? 'column' : 'row',
|
|
224
|
-
} }, entry.columns.map((column, colIndex) => (
|
|
225
|
-
// calculating the width like this rather than flex={column.width} to prevent collections from being too wide
|
|
226
|
-
React.createElement(Box, { key: colIndex, sx: { width: isXs ? '100%' : `calc(${(column.width / 12) * 100}% - 15px)` } }, column.entries?.map((columnEntry, entryIndex) => {
|
|
224
|
+
return (React.createElement(Grid, { container: true, columnSpacing: 4 }, entry.columns.map((column, colIndex) => (React.createElement(Grid, { key: colIndex, item: true, xs: isXs ? 12 : column.width }, column.entries?.map((columnEntry, entryIndex) => {
|
|
227
225
|
return (React.createElement(RecursiveEntryRenderer, { key: entryIndex + (columnEntry?.parameterId ?? ''), entry: columnEntry }));
|
|
228
226
|
}))))));
|
|
229
227
|
}
|
|
@@ -20,6 +20,10 @@ export declare const entryIsVisible: (entry: FormEntry, instance?: FieldValues,
|
|
|
20
20
|
*/
|
|
21
21
|
export declare const getNestedParameterIds: (entry: Sections | Columns) => string[];
|
|
22
22
|
export declare const getEntryId: (entry: FormEntry) => string | undefined;
|
|
23
|
+
/**
|
|
24
|
+
* Returns editable field IDs that are currently visible on the form.
|
|
25
|
+
*/
|
|
26
|
+
export declare const getVisibleEditableFieldIds: (entries: FormEntry[], instance?: FieldValues, formValues?: FieldValues) => string[];
|
|
23
27
|
export declare function getPrefixedUrl(url: string): string;
|
|
24
28
|
export declare const isOptionEqualToValue: (option: AutocompleteOption | string, value: unknown) => boolean;
|
|
25
29
|
export declare function addressProperties(addressProperty: Property): Property[];
|
|
@@ -87,7 +91,7 @@ export declare function formatSubmission(submission: FieldValues, apiServices?:
|
|
|
87
91
|
propertyId: string;
|
|
88
92
|
}, parameters?: InputParameter[]): Promise<FieldValues>;
|
|
89
93
|
export declare function filterEmptySections(entry: Sections | Columns, instance?: FieldValues, formData?: FieldValues): Sections | Columns | null;
|
|
90
|
-
export declare function assignIdsToSectionsAndRichText(entries: FormEntry[] | PanelViewEntry[], object
|
|
94
|
+
export declare function assignIdsToSectionsAndRichText(entries: FormEntry[] | PanelViewEntry[], object?: Obj, parameters?: InputParameter[]): FormEntry[] | PanelViewEntry[];
|
|
91
95
|
/**
|
|
92
96
|
* Converts a plain text string to RTF format suitable for a RichTextEditor.
|
|
93
97
|
*
|
|
@@ -101,7 +105,7 @@ export declare function assignIdsToSectionsAndRichText(entries: FormEntry[] | Pa
|
|
|
101
105
|
* This ensures that any plain text input will be safely represented in RTF without losing formatting or characters.
|
|
102
106
|
*/
|
|
103
107
|
export declare function plainTextToRtf(plainText: string): string;
|
|
104
|
-
export declare function getFieldDefinition(entry: FormEntry | PanelViewEntry, object
|
|
108
|
+
export declare function getFieldDefinition(entry: FormEntry | PanelViewEntry, object?: Obj, parameters?: InputParameter[]): InputParameter | Property | undefined;
|
|
105
109
|
export declare function obfuscateValue(value: unknown, property?: Partial<Property> | Partial<ObjectProperty>): unknown;
|
|
106
110
|
export declare function useFormById(formId: string, apiServices: ApiServices, errorMessage?: string): import("@tanstack/react-query/build/legacy/types").UseQueryResult<EvokeForm, Error>;
|
|
107
111
|
/**
|
|
@@ -117,9 +117,59 @@ export const getEntryId = (entry) => {
|
|
|
117
117
|
? entry.input.id
|
|
118
118
|
: undefined;
|
|
119
119
|
};
|
|
120
|
+
/**
|
|
121
|
+
* Returns editable field IDs that are currently visible on the form.
|
|
122
|
+
*/
|
|
123
|
+
export const getVisibleEditableFieldIds = (entries, instance, formValues) => {
|
|
124
|
+
const fieldIds = new Set();
|
|
125
|
+
const collectVisibleIds = (entriesToCheck) => {
|
|
126
|
+
entriesToCheck.forEach((entry) => {
|
|
127
|
+
if (!entryIsVisible(entry, instance, formValues)) {
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
if (entry.type === 'sections') {
|
|
131
|
+
entry.sections.forEach((section) => {
|
|
132
|
+
if (section.entries) {
|
|
133
|
+
collectVisibleIds(section.entries);
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
if (entry.type === 'columns') {
|
|
139
|
+
entry.columns.forEach((column) => {
|
|
140
|
+
if (column.entries) {
|
|
141
|
+
collectVisibleIds(column.entries);
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
if (entry.type !== 'input' && entry.type !== 'inputField') {
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
// Collection entries are handled by their own flow and are not part of autosave payloads.
|
|
150
|
+
if (entry.type === 'inputField' && entry.input.type === 'collection') {
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
const fieldId = getEntryId(entry);
|
|
154
|
+
if (fieldId) {
|
|
155
|
+
fieldIds.add(fieldId);
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
};
|
|
159
|
+
collectVisibleIds(entries ?? []);
|
|
160
|
+
return Array.from(fieldIds);
|
|
161
|
+
};
|
|
120
162
|
export function getPrefixedUrl(url) {
|
|
121
163
|
const wcsMatchers = ['/apps', '/pages', '/widgets'];
|
|
122
|
-
const dataMatchers = [
|
|
164
|
+
const dataMatchers = [
|
|
165
|
+
'/objects',
|
|
166
|
+
'/correspondenceTemplates',
|
|
167
|
+
'/documents',
|
|
168
|
+
'/payments',
|
|
169
|
+
'/forms',
|
|
170
|
+
'/formlets',
|
|
171
|
+
'/locations',
|
|
172
|
+
];
|
|
123
173
|
const signalrMatchers = ['/hubs'];
|
|
124
174
|
const accessManagementMatchers = ['/users'];
|
|
125
175
|
const workflowMatchers = ['/workflows'];
|
|
@@ -792,8 +842,8 @@ export function getFieldDefinition(entry, object, parameters) {
|
|
|
792
842
|
}
|
|
793
843
|
else if (entry.type === 'readonlyField') {
|
|
794
844
|
def = isAddressProperty(entry.propertyId)
|
|
795
|
-
? object
|
|
796
|
-
: object
|
|
845
|
+
? object?.properties?.find((prop) => prop.id === entry.propertyId.split('.')[0])
|
|
846
|
+
: object?.properties?.find((prop) => prop.id === entry.propertyId);
|
|
797
847
|
}
|
|
798
848
|
else if (entry.type === 'inputField') {
|
|
799
849
|
def = entry.input;
|
|
@@ -876,7 +926,7 @@ export function useFormById(formId, apiServices, errorMessage) {
|
|
|
876
926
|
queryKey: ['form', formId],
|
|
877
927
|
enabled: formId !== '_auto_' && !!formId,
|
|
878
928
|
staleTime: Infinity,
|
|
879
|
-
queryFn: () => apiServices.get(getPrefixedUrl(`/forms/${formId}`)),
|
|
929
|
+
queryFn: () => apiServices.get(getPrefixedUrl(`/forms/${formId}/effective`)),
|
|
880
930
|
meta: {
|
|
881
931
|
errorMessage,
|
|
882
932
|
},
|
|
@@ -34,6 +34,11 @@ describe('FormRenderer', () => {
|
|
|
34
34
|
if (sanitizedVersion === 'true') {
|
|
35
35
|
return HttpResponse.json(specialtyObject);
|
|
36
36
|
}
|
|
37
|
+
}), http.get('/api/data/objects/formletTestObject/effective', (req) => {
|
|
38
|
+
const sanitizedVersion = new URL(req.request.url).searchParams.get('sanitizedVersion');
|
|
39
|
+
if (sanitizedVersion === 'true') {
|
|
40
|
+
return HttpResponse.json({});
|
|
41
|
+
}
|
|
37
42
|
}), http.get('/api/data/objects/accessibility508Object/effective', (req) => {
|
|
38
43
|
const sanitizedVersion = new URL(req.request.url).searchParams.get('sanitizedVersion');
|
|
39
44
|
if (sanitizedVersion === 'true') {
|
|
@@ -416,7 +421,7 @@ describe('FormRenderer', () => {
|
|
|
416
421
|
});
|
|
417
422
|
describe('when passed a regular related object entry', () => {
|
|
418
423
|
const setupTestMocks = (object, form, instances) => {
|
|
419
|
-
server.use(http.get(`/api/data/objects/${object.id}/effective`, () => HttpResponse.json(object)), http.get(`/api/data/forms/${form.id}`, () => HttpResponse.json(form)), http.get(`/api/data/forms?filter={"where":{"actionId":"${form.actionId}","objectId":"${object.id}"}}`, () => HttpResponse.json([form])), http.get(`/api/data/objects/${object.id}/instances`, () => HttpResponse.json(instances || [])));
|
|
424
|
+
server.use(http.get(`/api/data/objects/${object.id}/effective`, () => HttpResponse.json(object)), http.get(`/api/data/forms/${form.id}/effective`, () => HttpResponse.json(form)), http.get(`/api/data/forms?filter={"where":{"actionId":"${form.actionId}","objectId":"${object.id}"}}`, () => HttpResponse.json([form])), http.get(`/api/data/objects/${object.id}/instances`, () => HttpResponse.json(instances || [])));
|
|
420
425
|
};
|
|
421
426
|
describe('when in table view', () => {
|
|
422
427
|
describe('when mode is existing records only', () => {
|
|
@@ -880,7 +885,7 @@ describe('FormRenderer', () => {
|
|
|
880
885
|
});
|
|
881
886
|
it('displays a not found error in record creation mode if a form could not be found', async () => {
|
|
882
887
|
const user = userEvent.setup();
|
|
883
|
-
server.use(http.get('/api/data/forms/specialtyTypeForm', () => {
|
|
888
|
+
server.use(http.get('/api/data/forms/specialtyTypeForm/effective', () => {
|
|
884
889
|
return HttpResponse.json({ error: 'Not found' }, { status: 404 });
|
|
885
890
|
}));
|
|
886
891
|
render(React.createElement(FormRenderer, { form: form, onChange: () => { } }));
|
|
@@ -1180,7 +1185,7 @@ describe('FormRenderer', () => {
|
|
|
1180
1185
|
});
|
|
1181
1186
|
describe('when passed a dynamic related object entry', () => {
|
|
1182
1187
|
const setupTestMocks = (object, form, instances) => {
|
|
1183
|
-
server.use(http.get(`/api/data/objects/${object.id}/effective`, () => HttpResponse.json(object)), http.get(`/api/data/forms/${form.id}`, () => HttpResponse.json(form)), http.get(`/api/data/forms?filter={"where":{"actionId":"${form.actionId}","objectId":"${object.id}"}}`, () => HttpResponse.json([form])), http.get(`/api/data/objects/${object.id}/instances`, () => HttpResponse.json(instances || [])));
|
|
1188
|
+
server.use(http.get(`/api/data/objects/${object.id}/effective`, () => HttpResponse.json(object)), http.get(`/api/data/forms/${form.id}/effective`, () => HttpResponse.json(form)), http.get(`/api/data/forms?filter={"where":{"actionId":"${form.actionId}","objectId":"${object.id}"}}`, () => HttpResponse.json([form])), http.get(`/api/data/objects/${object.id}/instances`, () => HttpResponse.json(instances || [])));
|
|
1184
1189
|
};
|
|
1185
1190
|
const form = {
|
|
1186
1191
|
id: 'relatedObjectTestForm',
|
|
@@ -1294,7 +1299,7 @@ describe('FormRenderer', () => {
|
|
|
1294
1299
|
});
|
|
1295
1300
|
it('displays a not found error in record creation mode if a form could not be found', async () => {
|
|
1296
1301
|
const user = userEvent.setup();
|
|
1297
|
-
server.use(http.get('/api/data/forms/specialtyTypeForm', () => {
|
|
1302
|
+
server.use(http.get('/api/data/forms/specialtyTypeForm/effective', () => {
|
|
1298
1303
|
return HttpResponse.json({ error: 'Not found' }, { status: 404 });
|
|
1299
1304
|
}));
|
|
1300
1305
|
render(React.createElement(FormRenderer, { form: form, onChange: () => { } }));
|
|
@@ -1306,7 +1311,7 @@ describe('FormRenderer', () => {
|
|
|
1306
1311
|
});
|
|
1307
1312
|
describe('when passed a one-to-many collection entry', () => {
|
|
1308
1313
|
const setupTestMocks = (object, form, instances) => {
|
|
1309
|
-
server.use(http.get(`/api/data/objects/${object.id}/effective`, () => HttpResponse.json(object)), http.get(`/api/data/forms/${form.id}`, () => HttpResponse.json(form)), http.get(`/api/data/forms?filter={"where":{"actionId":"${form.actionId}","objectId":"${object.id}"}}`, () => HttpResponse.json([form])), http.get(`/api/data/objects/${object.id}/instances`, () => HttpResponse.json(instances || [])), http.get(`/api/data/objects/${object.id}/instances/checkAccess`, () => HttpResponse.json({
|
|
1314
|
+
server.use(http.get(`/api/data/objects/${object.id}/effective`, () => HttpResponse.json(object)), http.get(`/api/data/forms/${form.id}/effective`, () => HttpResponse.json(form)), http.get(`/api/data/forms?filter={"where":{"actionId":"${form.actionId}","objectId":"${object.id}"}}`, () => HttpResponse.json([form])), http.get(`/api/data/objects/${object.id}/instances`, () => HttpResponse.json(instances || [])), http.get(`/api/data/objects/${object.id}/instances/checkAccess`, () => HttpResponse.json({
|
|
1310
1315
|
result: true,
|
|
1311
1316
|
})));
|
|
1312
1317
|
};
|
|
@@ -1648,4 +1653,42 @@ describe('FormRenderer', () => {
|
|
|
1648
1653
|
expect(textField).toHaveValue('Test Input');
|
|
1649
1654
|
});
|
|
1650
1655
|
});
|
|
1656
|
+
describe('when passed a formlet entry', () => {
|
|
1657
|
+
const formlet = {
|
|
1658
|
+
id: 'formletId',
|
|
1659
|
+
name: 'Test Formlet',
|
|
1660
|
+
entries: [
|
|
1661
|
+
{
|
|
1662
|
+
type: 'inputField',
|
|
1663
|
+
input: {
|
|
1664
|
+
type: 'date',
|
|
1665
|
+
id: 'dateId2',
|
|
1666
|
+
},
|
|
1667
|
+
display: {
|
|
1668
|
+
label: 'Date 2',
|
|
1669
|
+
required: false,
|
|
1670
|
+
},
|
|
1671
|
+
},
|
|
1672
|
+
],
|
|
1673
|
+
};
|
|
1674
|
+
beforeEach(() => {
|
|
1675
|
+
server.use(http.get('/api/data/formlets/formletId', () => HttpResponse.json(formlet)));
|
|
1676
|
+
});
|
|
1677
|
+
it('should render formlet entry fields', async () => {
|
|
1678
|
+
const form = {
|
|
1679
|
+
id: 'formWithFormlet',
|
|
1680
|
+
name: 'Form With Formlet',
|
|
1681
|
+
entries: [
|
|
1682
|
+
{
|
|
1683
|
+
type: 'formlet',
|
|
1684
|
+
formletId: 'formletId',
|
|
1685
|
+
},
|
|
1686
|
+
],
|
|
1687
|
+
actionId: '_update',
|
|
1688
|
+
objectId: 'formletTestObject',
|
|
1689
|
+
};
|
|
1690
|
+
render(React.createElement(FormRenderer, { form: form, onChange: () => { } }));
|
|
1691
|
+
await screen.findByLabelText('Date 2');
|
|
1692
|
+
});
|
|
1693
|
+
});
|
|
1651
1694
|
});
|