@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.
Files changed (25) hide show
  1. package/dist/published/components/custom/CriteriaBuilder/CriteriaBuilder.js +3 -0
  2. package/dist/published/components/custom/Form/utils.d.ts +2 -2
  3. package/dist/published/components/custom/FormField/AddressFieldComponent/addressFieldComponent.js +1 -1
  4. package/dist/published/components/custom/FormField/BooleanSelect/BooleanSelect.js +15 -7
  5. package/dist/published/components/custom/FormField/InputFieldComponent/InputFieldComponent.js +1 -1
  6. package/dist/published/components/custom/FormField/Select/Select.js +1 -1
  7. package/dist/published/components/custom/FormV2/FormRenderer.js +5 -5
  8. package/dist/published/components/custom/FormV2/FormRendererContainer.js +20 -10
  9. package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/DropdownRepeatableFieldInput.js +3 -0
  10. package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/RepeatableField.js +1 -1
  11. package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/Document.js +1 -0
  12. package/dist/published/components/custom/FormV2/components/FormFieldTypes/Image.js +1 -0
  13. package/dist/published/components/custom/FormV2/components/FormFieldTypes/UserProperty.js +2 -0
  14. package/dist/published/components/custom/FormV2/components/FormletRenderer.d.ts +6 -0
  15. package/dist/published/components/custom/FormV2/components/FormletRenderer.js +30 -0
  16. package/dist/published/components/custom/FormV2/components/HtmlView.js +12 -9
  17. package/dist/published/components/custom/FormV2/components/RecursiveEntryRenderer.js +8 -10
  18. package/dist/published/components/custom/FormV2/components/utils.d.ts +6 -2
  19. package/dist/published/components/custom/FormV2/components/utils.js +54 -4
  20. package/dist/published/components/custom/FormV2/tests/FormRenderer.test.js +48 -5
  21. package/dist/published/components/custom/FormV2/tests/FormRendererContainer.test.js +279 -35
  22. package/dist/published/stories/FormRendererData.d.ts +15 -0
  23. package/dist/published/stories/FormRendererData.js +63 -0
  24. package/dist/published/stories/sharedMswHandlers.js +4 -2
  25. 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;
@@ -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
- const descriptionStyles = {
8
- color: '#999 !important',
9
- whiteSpace: 'normal',
10
- paddingBottom: '4px',
11
- marginX: 0,
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 ?? {}) })) }),
@@ -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 object ? assignIdsToSectionsAndRichText(entries, object, parameters) : [];
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.actions?.find((a) => a.id === actionId);
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' && entry.type !== 'columns' && entry.type !== 'content') {
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 submission = await formatSubmission(cleanedData, apiServices, objectId, instanceId, form, setSnackbarError, undefined, parameters);
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: pick(submission, sanitizedObject?.properties
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 && !formDataRef.current) || !form || !sanitizedObject;
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) => {
@@ -85,6 +85,9 @@ export const DropdownRepeatableFieldInput = (props) => {
85
85
  : undefined),
86
86
  }, onChange: (event) => {
87
87
  setSearchValue(event.target.value);
88
+ }, sx: {
89
+ backgroundColor: 'white',
90
+ borderRadius: '8px',
88
91
  } })),
89
92
  loading,
90
93
  sortBy: 'NONE',
@@ -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, null, relatedInstances?.map((relatedInstance, index) => (React.createElement(TableRow, { key: relatedInstance.id },
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
  ? {
@@ -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%' } },
@@ -29,6 +29,7 @@ const styles = {
29
29
  border: '1px dashed #858585',
30
30
  position: 'relative',
31
31
  cursor: 'pointer',
32
+ backgroundColor: 'white',
32
33
  },
33
34
  icon: {
34
35
  color: '#fff',
@@ -141,6 +141,8 @@ const UserProperty = (props) => {
141
141
  },
142
142
  }
143
143
  : {}),
144
+ backgroundColor: 'white',
145
+ borderRadius: '8px',
144
146
  } })), size: fieldHeight ?? 'medium', readOnly: readOnly, error: error })));
145
147
  };
146
148
  export default UserProperty;
@@ -0,0 +1,6 @@
1
+ import React from 'react';
2
+ import { FormletReference } from '@evoke-platform/context';
3
+ declare function FormletRenderer(props: {
4
+ entry: FormletReference;
5
+ }): React.JSX.Element;
6
+ export default FormletRenderer;
@@ -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 (React.createElement(Box, { sx: {
37
- width: '100%',
38
- height: '100%',
39
- '.ql-container.ql-snow': {
40
- border: 'none',
41
- minHeight: 20,
42
- },
43
- } },
44
- React.createElement("div", { ref: containerRef })));
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(Box, { sx: {
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: Obj, parameters?: InputParameter[]): FormEntry[] | PanelViewEntry[];
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: Obj, parameters?: InputParameter[]): InputParameter | Property | undefined;
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 = ['/objects', '/correspondenceTemplates', '/documents', '/payments', '/forms', '/locations'];
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.properties?.find((prop) => prop.id === entry.propertyId.split('.')[0])
796
- : object.properties?.find((prop) => prop.id === entry.propertyId);
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
  });