@evoke-platform/ui-components 1.10.0-testing.9 → 1.10.1-dev.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 (78) hide show
  1. package/dist/published/components/core/Autocomplete/Autocomplete.js +4 -2
  2. package/dist/published/components/core/Autocomplete/Autocomplete.test.js +112 -3
  3. package/dist/published/components/core/TextField/TextField.js +1 -1
  4. package/dist/published/components/core/TextField/TextField.test.js +0 -2
  5. package/dist/published/components/custom/CriteriaBuilder/CriteriaBuilder.js +24 -2
  6. package/dist/published/components/custom/CriteriaBuilder/CriteriaBuilder.test.js +45 -2
  7. package/dist/published/components/custom/Form/FormComponents/DocumentComponent/Document.js +2 -1
  8. package/dist/published/components/custom/Form/FormComponents/RepeatableFieldComponent/RepeatableField.js +1 -1
  9. package/dist/published/components/custom/Form/tests/Form.test.js +0 -2
  10. package/dist/published/components/custom/FormField/DatePickerSelect/DatePickerSelect.js +36 -7
  11. package/dist/published/components/custom/FormField/DateTimePickerSelect/DateTimePickerSelect.js +14 -1
  12. package/dist/published/components/custom/FormField/FormField.d.ts +3 -1
  13. package/dist/published/components/custom/FormField/FormField.js +17 -5
  14. package/dist/published/components/custom/FormField/InputFieldComponent/InputFieldComponent.js +6 -4
  15. package/dist/published/components/custom/FormField/InputFieldComponent/InputFieldComponent.test.js +0 -2
  16. package/dist/published/components/custom/FormField/Select/Select.test.js +0 -2
  17. package/dist/published/components/custom/FormField/TimePickerSelect/TimePickerSelect.js +14 -1
  18. package/dist/published/components/custom/FormV2/FormRenderer.d.ts +2 -1
  19. package/dist/published/components/custom/FormV2/FormRenderer.js +46 -8
  20. package/dist/published/components/custom/FormV2/FormRendererContainer.js +178 -153
  21. package/dist/published/components/custom/FormV2/components/AccordionSections.js +7 -2
  22. package/dist/published/components/custom/FormV2/components/Body.d.ts +1 -1
  23. package/dist/published/components/custom/FormV2/components/DefaultValues.d.ts +2 -2
  24. package/dist/published/components/custom/FormV2/components/DefaultValues.js +36 -28
  25. package/dist/published/components/custom/FormV2/components/FieldWrapper.js +1 -1
  26. package/dist/published/components/custom/FormV2/components/Footer.d.ts +1 -0
  27. package/dist/published/components/custom/FormV2/components/Footer.js +8 -5
  28. package/dist/published/components/custom/FormV2/components/FormContext.d.ts +3 -2
  29. package/dist/published/components/custom/FormV2/components/FormFieldTypes/AddressFields.d.ts +9 -0
  30. package/dist/published/components/custom/FormV2/components/FormFieldTypes/AddressFields.js +32 -15
  31. package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/ActionDialog.js +2 -2
  32. package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/RepeatableField.js +9 -23
  33. package/dist/published/components/custom/FormV2/components/FormFieldTypes/Criteria.js +16 -3
  34. package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/Document.js +22 -4
  35. package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/DocumentList.d.ts +2 -1
  36. package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/DocumentList.js +16 -3
  37. package/dist/published/components/custom/FormV2/components/FormFieldTypes/Image.js +31 -5
  38. package/dist/published/components/custom/FormV2/components/FormFieldTypes/UserProperty.js +15 -3
  39. package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/ObjectPropertyInput.js +127 -92
  40. package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/RelatedObjectInstance.d.ts +2 -3
  41. package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/RelatedObjectInstance.js +43 -20
  42. package/dist/published/components/custom/FormV2/components/Header.d.ts +5 -3
  43. package/dist/published/components/custom/FormV2/components/Header.js +47 -9
  44. package/dist/published/components/custom/FormV2/components/PropertyProtection.d.ts +16 -0
  45. package/dist/published/components/custom/FormV2/components/PropertyProtection.js +113 -0
  46. package/dist/published/components/custom/FormV2/components/RecursiveEntryRenderer.js +47 -24
  47. package/dist/published/components/custom/FormV2/components/ValidationFiles/ValidationErrors.js +1 -1
  48. package/dist/published/components/custom/FormV2/components/types.d.ts +2 -0
  49. package/dist/published/components/custom/FormV2/components/utils.d.ts +6 -4
  50. package/dist/published/components/custom/FormV2/components/utils.js +83 -13
  51. package/dist/published/components/custom/FormV2/tests/FormRenderer.test.js +411 -44
  52. package/dist/published/components/custom/FormV2/tests/FormRendererContainer.test.js +983 -16
  53. package/dist/published/components/custom/FormV2/tests/test-data.d.ts +1 -0
  54. package/dist/published/components/custom/FormV2/tests/test-data.js +138 -0
  55. package/dist/published/components/custom/ViewDetailsV2/InstanceEntryRenderer.d.ts +3 -0
  56. package/dist/published/components/custom/ViewDetailsV2/InstanceEntryRenderer.js +165 -0
  57. package/dist/published/components/custom/ViewDetailsV2/ViewDetailsV2Container.d.ts +13 -0
  58. package/dist/published/components/custom/ViewDetailsV2/ViewDetailsV2Container.js +144 -0
  59. package/dist/published/components/custom/ViewDetailsV2/index.d.ts +3 -0
  60. package/dist/published/components/custom/ViewDetailsV2/index.js +2 -0
  61. package/dist/published/components/custom/index.d.ts +2 -0
  62. package/dist/published/components/custom/index.js +1 -0
  63. package/dist/published/index.d.ts +6 -6
  64. package/dist/published/index.js +1 -1
  65. package/dist/published/stories/CriteriaBuilder.stories.js +6 -0
  66. package/dist/published/stories/FormRenderer.stories.d.ts +8 -4
  67. package/dist/published/stories/FormRendererContainer.stories.d.ts +26 -0
  68. package/dist/published/stories/FormRendererContainer.stories.js +5 -0
  69. package/dist/published/stories/FormRendererData.d.ts +12 -0
  70. package/dist/published/stories/FormRendererData.js +26 -1
  71. package/dist/published/stories/ViewDetailsV2Container.stories.d.ts +26 -0
  72. package/dist/published/stories/ViewDetailsV2Container.stories.js +37 -0
  73. package/dist/published/stories/ViewDetailsV2Data.d.ts +4 -0
  74. package/dist/published/stories/ViewDetailsV2Data.js +203 -0
  75. package/dist/published/stories/sharedMswHandlers.js +49 -10
  76. package/dist/published/theme/hooks.d.ts +4 -3
  77. package/dist/published/types.d.ts +3 -0
  78. package/package.json +10 -8
@@ -0,0 +1,113 @@
1
+ import { useApiServices } from '@evoke-platform/context';
2
+ import React, { useEffect, useState } from 'react';
3
+ import { CheckRounded, ClearRounded, EditRounded, VisibilityOffRounded, VisibilityRounded } from '../../../../icons';
4
+ import { useFormContext } from '../../../../theme/hooks';
5
+ import { Divider, IconButton, InputAdornment, Snackbar, Tooltip } from '../../../core';
6
+ import { getPrefixedUrl } from '../../Form/utils';
7
+ import { obfuscateValue } from './utils';
8
+ const PropertyProtection = (props) => {
9
+ const { parameter, mask, protection, canEdit, value, mode, setMode, setCurrentDisplayValue, handleChange } = props;
10
+ const apiServices = useApiServices();
11
+ const { object, instance, fetchedOptions, setFetchedOptions } = useFormContext();
12
+ const [hasViewPermission, setHasViewPermission] = useState(fetchedOptions[`${parameter.id}-hasViewPermission`] || undefined);
13
+ const [fullValue, setFullValue] = useState(fetchedOptions[`${parameter.id}-fullValue`]);
14
+ const [isLoading, setIsLoading] = useState(hasViewPermission === undefined);
15
+ const [error, setError] = useState(null);
16
+ useEffect(() => {
17
+ if (hasViewPermission === undefined && instance) {
18
+ apiServices
19
+ .get(getPrefixedUrl(`/objects/${object?.id}/instances/${instance?.id}/checkAccess?action=readProtected&fieldId=${parameter.id}`))
20
+ .then((viewPermissionCheck) => {
21
+ setHasViewPermission(viewPermissionCheck.result);
22
+ setFetchedOptions({ [`${parameter.id}-hasViewPermission`]: viewPermissionCheck.result });
23
+ })
24
+ .catch(() => {
25
+ setError('Failed to check view permission.');
26
+ setHasViewPermission(false);
27
+ })
28
+ .finally(() => setIsLoading(false));
29
+ }
30
+ }, []);
31
+ const hasValueChangedOrViewed = value !== obfuscateValue(value, { protection, mask });
32
+ const canViewFull = hasViewPermission || hasValueChangedOrViewed;
33
+ const fetchFullValue = async () => {
34
+ // if instance doesn't exist, cannot fetch full value
35
+ if (!instance) {
36
+ return undefined;
37
+ }
38
+ try {
39
+ const value = await apiServices.get(getPrefixedUrl(`/objects/${object?.id}/instances/${instance?.id}/properties/${parameter.id}?showProtectedValue=true`));
40
+ setFullValue(value);
41
+ setFetchedOptions({ [`${parameter.id}-fullValue`]: value });
42
+ return value;
43
+ }
44
+ catch (error) {
45
+ setError(`Failed to retrieve full ${parameter.name} value`);
46
+ return '';
47
+ }
48
+ };
49
+ const handleModeChange = async (newMode) => {
50
+ if (newMode === 'edit') {
51
+ if (!canEdit) {
52
+ // sanity check that user can edit
53
+ return;
54
+ }
55
+ if (!hasViewPermission && !hasValueChangedOrViewed) {
56
+ handleChange?.(undefined);
57
+ setCurrentDisplayValue(undefined);
58
+ }
59
+ }
60
+ else if (newMode === 'full') {
61
+ let valueToDisplay;
62
+ if (hasValueChangedOrViewed) {
63
+ // Use the changed value if available
64
+ valueToDisplay = fullValue;
65
+ }
66
+ else {
67
+ valueToDisplay = fullValue ?? (await fetchFullValue());
68
+ }
69
+ setCurrentDisplayValue(valueToDisplay);
70
+ handleChange?.(valueToDisplay);
71
+ }
72
+ else {
73
+ const maskedValue = obfuscateValue(value, { protection, mask });
74
+ setCurrentDisplayValue(maskedValue);
75
+ // save current value as full value, so value can be reverted as needed
76
+ if (hasValueChangedOrViewed) {
77
+ setFullValue(value);
78
+ setFetchedOptions({ [`${parameter.id}-fullValue`]: value });
79
+ }
80
+ }
81
+ setMode(newMode);
82
+ };
83
+ const handleRevert = async () => {
84
+ const revertedValue = fullValue ?? (await fetchFullValue());
85
+ handleChange?.(revertedValue);
86
+ setCurrentDisplayValue(obfuscateValue(revertedValue, { protection, mask }));
87
+ setMode('mask');
88
+ };
89
+ if (isLoading || (!canEdit && !hasViewPermission)) {
90
+ return null;
91
+ }
92
+ return (React.createElement(InputAdornment, { position: "end", sx: { paddingLeft: parameter?.type === 'date' ? '12px' : undefined } },
93
+ mode === 'edit' && (React.createElement(React.Fragment, null,
94
+ React.createElement(IconButton, { onClick: () => handleModeChange('mask'), "aria-label": "Done Editing" },
95
+ React.createElement(Tooltip, { title: "Done Editing" },
96
+ React.createElement(CheckRounded, { fontSize: "small" }))),
97
+ React.createElement(Divider, { orientation: "vertical", sx: { mx: 0.5, height: 24 } }),
98
+ React.createElement(IconButton, { size: "small", onClick: handleRevert, "aria-label": "Revert value" },
99
+ React.createElement(Tooltip, { title: "Revert value" },
100
+ React.createElement(ClearRounded, { fontSize: "small" }))))),
101
+ canEdit && (mode === 'full' || (!hasViewPermission && !hasValueChangedOrViewed && mode === 'mask')) && (React.createElement(IconButton, { onClick: () => handleModeChange('edit'), "aria-label": canViewFull ? 'Edit value' : 'Enter value' },
102
+ React.createElement(Tooltip, { title: "Edit value" },
103
+ React.createElement(EditRounded, { fontSize: "small" })))),
104
+ canEdit && canViewFull && mode === 'full' && (React.createElement(Divider, { orientation: "vertical", sx: { mx: 0.5, height: 24 } })),
105
+ canViewFull && mode === 'mask' && (React.createElement(IconButton, { onClick: () => handleModeChange('full'), "aria-label": "Show full value" },
106
+ React.createElement(Tooltip, { title: "Show full value" },
107
+ React.createElement(VisibilityOffRounded, { fontSize: "small" })))),
108
+ canViewFull && mode === 'full' && (React.createElement(IconButton, { onClick: () => handleModeChange('mask'), "aria-label": "Hide value" },
109
+ React.createElement(Tooltip, { title: "Hide value" },
110
+ React.createElement(VisibilityRounded, { fontSize: "small" })))),
111
+ React.createElement(Snackbar, { open: !!error, handleClose: () => setError(null), error: true, message: error })));
112
+ };
113
+ export default PropertyProtection;
@@ -1,6 +1,7 @@
1
1
  import { useApiServices, useAuthenticationContext, } from '@evoke-platform/context';
2
2
  import { WarningRounded } from '@mui/icons-material';
3
3
  import DOMPurify from 'dompurify';
4
+ import { isEmpty } from 'lodash';
4
5
  import React, { useEffect, useMemo } from 'react';
5
6
  import useWidgetSize, { useFormContext } from '../../../../theme/hooks';
6
7
  import { TextField, Typography } from '../../../core';
@@ -37,11 +38,7 @@ function getFieldWrapperProps(fieldDefinition, entry, entryId, fieldValue, displ
37
38
  }
38
39
  export function RecursiveEntryRenderer(props) {
39
40
  const { entry } = props;
40
- const { fetchedOptions, setFetchedOptions, object, getValues, errors, instance, richTextEditor, parameters, handleChange, fieldHeight, triggerFieldReset, associatedObject, form, width, } = useFormContext();
41
- // If the entry is hidden, clear its value and any nested values, and skip rendering
42
- if (!entryIsVisible(entry, getValues(), instance)) {
43
- return null;
44
- }
41
+ const { fetchedOptions, setFetchedOptions, object, getValues, errors, instance, richTextEditor: RichTextEditor, parameters, handleChange, onAutosave, fieldHeight, triggerFieldReset, associatedObject, form, width, } = useFormContext();
45
42
  const { isBelow, breakpoints } = useWidgetSize({
46
43
  scroll: false,
47
44
  defaultWidth: width,
@@ -52,20 +49,26 @@ export function RecursiveEntryRenderer(props) {
52
49
  const userAccount = useAuthenticationContext()?.account;
53
50
  const entryId = getEntryId(entry) || 'defaultId';
54
51
  const display = 'display' in entry ? entry.display : undefined;
55
- const fieldValue = entry.type === 'readonlyField' ? instance?.[entryId] : getValues(entryId);
52
+ const fieldValue = entry.type === 'readonlyField' ? instance?.[entryId] : getValues ? getValues(entryId) : undefined;
56
53
  const initialMiddleObjectInstances = fetchedOptions[`${entryId}InitialMiddleObjectInstances`];
57
54
  const middleObject = fetchedOptions[`${entryId}MiddleObject`];
58
55
  const fieldDefinition = useMemo(() => {
56
+ if (!object)
57
+ return undefined;
59
58
  return getFieldDefinition(entry, object, parameters, form?.id === 'documentForm');
60
59
  }, [entry, parameters, object]);
61
60
  const validation = fieldDefinition?.validation || {};
62
- if (associatedObject?.propertyId === entryId)
63
- return null;
64
61
  useEffect(() => {
65
62
  if (fieldDefinition?.type === 'collection' && fieldDefinition?.manyToManyPropertyId && instance) {
66
63
  fetchCollectionData(apiServices, fieldDefinition, setFetchedOptions, instance.id, fetchedOptions, initialMiddleObjectInstances);
67
64
  }
68
65
  }, [fieldDefinition, instance]);
66
+ if (associatedObject?.propertyId === entryId)
67
+ return null;
68
+ // If the entry is hidden, clear its value and any nested values, and skip rendering
69
+ if (!getValues || !entryIsVisible(entry, instance, getValues())) {
70
+ return null;
71
+ }
69
72
  if (entry.type === 'content') {
70
73
  return (React.createElement(Box, { dangerouslySetInnerHTML: { __html: DOMPurify.sanitize(entry.html) }, sx: {
71
74
  fontFamily: 'Roboto, Helvetica, Arial, sans-serif',
@@ -74,7 +77,7 @@ export function RecursiveEntryRenderer(props) {
74
77
  else if ((entry.type === 'input' || entry.type === 'readonlyField' || entry.type === 'inputField') &&
75
78
  fieldDefinition) {
76
79
  if (isAddressProperty(entryId)) {
77
- return React.createElement(AddressFields, { entry: entry, entryId: entryId, fieldDefinition: fieldDefinition });
80
+ return (React.createElement(AddressFields, { entry: entry, entryId: entryId, fieldDefinition: fieldDefinition, readOnly: entry.type === 'readonlyField' }));
78
81
  }
79
82
  else if (fieldDefinition.type === 'image') {
80
83
  return (React.createElement(FieldWrapper, { ...getFieldWrapperProps(fieldDefinition, entry, entryId, fieldValue, display, errors) },
@@ -82,7 +85,7 @@ export function RecursiveEntryRenderer(props) {
82
85
  }
83
86
  else if (fieldDefinition.type === 'object') {
84
87
  return (React.createElement(FieldWrapper, { ...getFieldWrapperProps(fieldDefinition, entry, entryId, fieldValue, display, errors) },
85
- React.createElement(ObjectPropertyInput, { fieldDefinition: fieldDefinition, id: entryId, mode: display?.mode || 'default', error: !!errors?.[entryId], displayOption: display?.relatedObjectDisplay || 'dialogBox', initialValue: fieldValue, readOnly: entry.type === 'readonlyField', filter: validation?.criteria
88
+ React.createElement(ObjectPropertyInput, { relatedObjectId: !fieldDefinition.objectId ? display?.relatedObjectId : fieldDefinition.objectId, fieldDefinition: fieldDefinition, id: entryId, mode: display?.mode || 'default', error: !!errors?.[entryId], displayOption: display?.relatedObjectDisplay || 'dialogBox', initialValue: fieldValue, readOnly: entry.type === 'readonlyField', filter: validation?.criteria
86
89
  ? updateCriteriaInputs(validation.criteria, getValues(), userAccount)
87
90
  : undefined, sortBy: typeof display?.defaultValue === 'object' && 'sortBy' in display.defaultValue
88
91
  ? display?.defaultValue.sortBy
@@ -97,21 +100,35 @@ export function RecursiveEntryRenderer(props) {
97
100
  React.createElement(UserProperty, { id: entryId, value: fieldValue, error: !!errors?.[entryId], readOnly: entry.type === 'readonlyField', hasDescription: !!display?.description })));
98
101
  }
99
102
  else if (fieldDefinition.type === 'collection') {
100
- return fieldDefinition?.manyToManyPropertyId ? (middleObject && initialMiddleObjectInstances && (React.createElement(FieldWrapper, { ...getFieldWrapperProps(fieldDefinition, entry, entryId, fieldValue, display, errors) },
101
- React.createElement(DropdownRepeatableField, { initialMiddleObjectInstances: fetchedOptions[`${entryId}MiddleObjectInstances`] || initialMiddleObjectInstances, fieldDefinition: fieldDefinition, id: entryId, middleObject: middleObject, readOnly: entry.type === 'readonlyField', criteria: validation?.criteria, hasDescription: !!display?.description })))) : (React.createElement(FieldWrapper, { ...getFieldWrapperProps(fieldDefinition, entry, entryId, fieldValue, display, errors) },
102
- React.createElement(RepeatableField, { fieldDefinition: fieldDefinition, canUpdateProperty: entry.type !== 'readonlyField', criteria: validation?.criteria, viewLayout: display?.viewLayout, entry: entry })));
103
+ if (fieldDefinition?.manyToManyPropertyId) {
104
+ if (middleObject && !isEmpty(middleObject)) {
105
+ return (initialMiddleObjectInstances && (React.createElement(FieldWrapper, { ...getFieldWrapperProps(fieldDefinition, entry, entryId, fieldValue, display, errors) },
106
+ React.createElement(DropdownRepeatableField, { initialMiddleObjectInstances: fetchedOptions[`${entryId}MiddleObjectInstances`] ||
107
+ initialMiddleObjectInstances, fieldDefinition: fieldDefinition, id: entryId, middleObject: middleObject, readOnly: entry.type === 'readonlyField', criteria: validation?.criteria, hasDescription: !!display?.description }))));
108
+ }
109
+ else {
110
+ // when in the builder preview, the middle object won't be fetched so instead show an empty field
111
+ const singleSelectProperty = structuredClone(fieldDefinition);
112
+ singleSelectProperty.type = 'choices';
113
+ return (React.createElement(FieldWrapper, { ...getFieldWrapperProps(fieldDefinition, entry, entryId, fieldValue, display, errors) },
114
+ React.createElement(FormField, { id: entryId, property: singleSelectProperty, defaultValue: fieldValue || getValues(entryId), selectOptions: [], size: fieldHeight })));
115
+ }
116
+ }
117
+ else {
118
+ return (React.createElement(FieldWrapper, { ...getFieldWrapperProps(fieldDefinition, entry, entryId, fieldValue, display, errors) },
119
+ React.createElement(RepeatableField, { fieldDefinition: fieldDefinition, canUpdateProperty: entry.type !== 'readonlyField', criteria: validation?.criteria, viewLayout: display?.viewLayout, entry: entry })));
120
+ }
103
121
  }
104
122
  else if (fieldDefinition.type === 'richText') {
105
- return (React.createElement(FieldWrapper, { ...getFieldWrapperProps(fieldDefinition, entry, entryId, fieldValue, display, errors) }, richTextEditor ? (React.createElement(richTextEditor, {
123
+ return (React.createElement(FieldWrapper, { ...getFieldWrapperProps(fieldDefinition, entry, entryId, fieldValue, display, errors) }, RichTextEditor && handleChange ? (React.createElement(RichTextEditor
124
+ // RichTexts get a uniqueId when the form is loaded to prevent issues with multiple rich text fields on one form
125
+ , {
106
126
  // RichTexts get a uniqueId when the form is loaded to prevent issues with multiple rich text fields on one form
107
- id: entry.uniqueId,
108
- value: fieldValue,
109
- handleUpdate: (value) => handleChange(entryId, value),
110
- format: 'rtf',
111
- disabled: entry.type === 'readonlyField',
112
- rows: display?.rowCount,
113
- hasError: !!errors?.[entryId],
114
- })) : (React.createElement(FormField, { id: entryId, property: fieldDefinition, defaultValue: fieldValue || getValues(entryId), onChange: handleChange, readOnly: entry.type === 'readonlyField', placeholder: display?.placeholder, error: !!errors?.[entryId], errorMessage: errors?.[entryId]?.message, isMultiLineText: !!display?.rowCount, rows: display?.rowCount, size: fieldHeight }))));
127
+ id: entry.uniqueId, value: fieldValue, handleUpdate: (value) => handleChange(entryId, value), format: "rtf", disabled: entry.type === 'readonlyField', rows: display?.rowCount, hasError: !!errors?.[entryId] })) : (React.createElement(FormField, { id: entryId, property: fieldDefinition, defaultValue: fieldValue, onChange: handleChange, onBlur: () => {
128
+ onAutosave?.(entryId)?.catch((error) => {
129
+ console.error('Autosave failed:', error);
130
+ });
131
+ }, readOnly: entry.type === 'readonlyField', placeholder: display?.placeholder, error: !!errors?.[entryId], errorMessage: errors?.[entryId]?.message, isMultiLineText: !!display?.rowCount, rows: display?.rowCount, size: fieldHeight }))));
115
132
  }
116
133
  else if (fieldDefinition.type === 'document') {
117
134
  return (React.createElement(FieldWrapper, { ...getFieldWrapperProps(fieldDefinition, entry, entryId, fieldValue, display, errors) },
@@ -136,6 +153,7 @@ export function RecursiveEntryRenderer(props) {
136
153
  'aria-describedby': `${entryId}-description`,
137
154
  };
138
155
  }
156
+ const objectProperty = object?.properties?.find((p) => p.id === entryId);
139
157
  return (React.createElement(FieldWrapper
140
158
  /*
141
159
  * Key remounts the field if a value is changed.
@@ -154,9 +172,14 @@ export function RecursiveEntryRenderer(props) {
154
172
  : `${entryId}-reset-false`, ...getFieldWrapperProps(fieldDefinition, entry, entryId, fieldValue, display, errors), errorMessage: undefined },
155
173
  React.createElement(FormField, { id: entryId,
156
174
  // TODO: Ideally the FormField prop should be called parameter but can't change the name for backwards compatibility reasons
157
- property: fieldDefinition, defaultValue: fieldValue || getValues(entryId), onChange: handleChange, readOnly: entry.type === 'readonlyField', placeholder: display?.placeholder, mask: validation?.mask, isOptionEqualToValue: isOptionEqualToValue, error: !!errors?.[entryId], errorMessage: errors?.[entryId]?.message, isMultiLineText: !!display?.rowCount, rows: display?.rowCount, required: entry.display?.required || false, size: fieldHeight, sortBy: display?.choicesDisplay?.sortBy && display.choicesDisplay.sortBy, displayOption: fieldDefinition.type === 'boolean'
175
+ property: fieldDefinition, defaultValue: fieldValue, onChange: handleChange, onBlur: () => {
176
+ // Blur event - reads current value from formData
177
+ onAutosave?.(entryId)?.catch((error) => {
178
+ console.error('Autosave failed:', error);
179
+ });
180
+ }, readOnly: entry.type === 'readonlyField', placeholder: display?.placeholder, mask: validation?.mask, isOptionEqualToValue: isOptionEqualToValue, error: !!errors?.[entryId], errorMessage: errors?.[entryId]?.message, isMultiLineText: !!display?.rowCount, rows: display?.rowCount, required: entry.display?.required || false, size: fieldHeight, sortBy: display?.choicesDisplay?.sortBy && display.choicesDisplay.sortBy, displayOption: fieldDefinition.type === 'boolean'
158
181
  ? display?.booleanDisplay
159
- : 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 })));
182
+ : 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 })));
160
183
  }
161
184
  }
162
185
  else if (entry.type === 'columns') {
@@ -29,7 +29,7 @@ function ValidationErrors(props) {
29
29
  border: '1px solid #721c24',
30
30
  padding: '8px 24px',
31
31
  borderRadius: '4px',
32
- flex: 1,
32
+ flex: '1 1 100%',
33
33
  ...sx,
34
34
  } },
35
35
  React.createElement(Typography, { sx: { color: '#721c24', mt: '16px', mb: '8px' } }, "Please fix the following errors before submitting:"),
@@ -54,6 +54,7 @@ export type ObjectPropertyInputProps = {
54
54
  hasDescription?: boolean;
55
55
  createActionId?: string;
56
56
  formId?: string;
57
+ relatedObjectId?: string;
57
58
  };
58
59
  export type Page = {
59
60
  id: string;
@@ -93,6 +94,7 @@ export type EntryRendererProps = {
93
94
  };
94
95
  export type SectionsProps = {
95
96
  entry: ExpandedSections;
97
+ readOnly?: boolean;
96
98
  };
97
99
  export type DocumentData = {
98
100
  id: string;
@@ -2,6 +2,7 @@
2
2
  import { Action, ApiServices, Column, Columns, EvokeForm, FormEntry, InputField, InputParameter, InputParameterReference, Obj, ObjectInstance, Property, Section, Sections, UserAccount } from '@evoke-platform/context';
3
3
  import { LocalDateTime } from '@js-joda/core';
4
4
  import { FieldErrors, FieldValues } from 'react-hook-form';
5
+ import { ObjectProperty } from '../../../../types';
5
6
  import { AutocompleteOption } from '../../../core';
6
7
  import { Document, DocumentData, SavedDocumentReference } from './types';
7
8
  export declare const scrollIntoViewWithOffset: (el: HTMLElement, offset: number, container?: HTMLElement) => void;
@@ -10,7 +11,7 @@ export declare function isAddressProperty(key: string): boolean;
10
11
  /**
11
12
  * Determine if a form entry is visible or not.
12
13
  */
13
- export declare const entryIsVisible: (entry: FormEntry, formValues: FieldValues, instance?: FieldValues) => boolean;
14
+ export declare const entryIsVisible: (entry: FormEntry, instance?: FieldValues, formValues?: FieldValues) => boolean;
14
15
  /**
15
16
  * Recursively retrieves all parameter IDs from a given entry of type Sections or Columns.
16
17
  *
@@ -86,8 +87,8 @@ export declare function formatSubmission(submission: FieldValues, apiServices?:
86
87
  message?: string;
87
88
  isError: boolean;
88
89
  }>>): Promise<FieldValues>;
89
- export declare function filterEmptySections(entry: Sections | Columns, formData: FieldValues, instance?: FieldValues): Sections | Columns | null;
90
- export declare function assignIdsToSectionsAndRichText(entries: FormEntry[], object?: Obj, parameters?: InputParameter[]): FormEntry[];
90
+ export declare function filterEmptySections(entry: Sections | Columns, instance?: FieldValues, formData?: FieldValues): Sections | Columns | null;
91
+ export declare function assignIdsToSectionsAndRichText(entries: FormEntry[], object: Obj, parameters?: InputParameter[]): FormEntry[];
91
92
  /**
92
93
  * Converts a plain text string to RTF format suitable for a RichTextEditor.
93
94
  *
@@ -101,4 +102,5 @@ export declare function assignIdsToSectionsAndRichText(entries: FormEntry[], obj
101
102
  * This ensures that any plain text input will be safely represented in RTF without losing formatting or characters.
102
103
  */
103
104
  export declare function plainTextToRtf(plainText: string): string;
104
- export declare function getFieldDefinition(entry: FormEntry, object?: Obj, parameters?: InputParameter[], isDocument?: boolean): InputParameter | Property | undefined;
105
+ export declare function getFieldDefinition(entry: FormEntry, object: Obj, parameters?: InputParameter[], isDocument?: boolean): InputParameter | Property | undefined;
106
+ export declare function obfuscateValue(value: unknown, property?: Partial<Property> | Partial<ObjectProperty>): unknown;
@@ -68,7 +68,7 @@ export function isAddressProperty(key) {
68
68
  /**
69
69
  * Determine if a form entry is visible or not.
70
70
  */
71
- export const entryIsVisible = (entry, formValues, instance) => {
71
+ export const entryIsVisible = (entry, instance, formValues) => {
72
72
  const display = 'display' in entry ? entry.display : undefined;
73
73
  const { visibility } = display ?? ('visibility' in entry ? entry : {});
74
74
  if (isObject(visibility) && 'conditions' in visibility && isArray(visibility.conditions)) {
@@ -562,6 +562,7 @@ export const uploadDocuments = async (files, metadata, apiServices, instanceId,
562
562
  const allDocuments = [];
563
563
  const formData = new FormData();
564
564
  for (const [index, file] of files.entries()) {
565
+ // Only upload File instances; SavedDocumentReference objects are already uploaded
565
566
  if ('size' in file) {
566
567
  formData.append(`files[${index}]`, file);
567
568
  }
@@ -621,13 +622,14 @@ export const deleteDocuments = async (submittedFields, requestSuccess, apiServic
621
622
  * Returns the cleaned submission ready for submitting.
622
623
  */
623
624
  export async function formatSubmission(submission, apiServices, objectId, instanceId, form, setSnackbarError) {
625
+ const allEntries = getUnnestedEntries(form?.entries ?? []) ?? [];
624
626
  for (const [key, value] of Object.entries(submission)) {
627
+ const entry = allEntries?.find((entry) => getEntryId(entry) === key);
625
628
  if (isArray(value)) {
629
+ // Only upload if array contains File instances (not SavedDocumentReference)
626
630
  const fileInArray = value.some((item) => item instanceof File);
627
631
  if (fileInArray && instanceId && apiServices && objectId) {
628
632
  try {
629
- const allEntries = getUnnestedEntries(form?.entries ?? []) ?? [];
630
- const entry = allEntries?.find((entry) => getEntryId(entry) === key);
631
633
  const uploadedDocuments = await uploadDocuments(value, {
632
634
  type: '',
633
635
  view_permission: '',
@@ -652,10 +654,16 @@ export async function formatSubmission(submission, apiServices, objectId, instan
652
654
  else if (typeof value === 'object' && value !== null) {
653
655
  if (Object.values(value).every((v) => v === undefined)) {
654
656
  submission[key] = undefined;
655
- // only submit the name and id of a related object
657
+ // only submit the name and id of a regular related object
658
+ // and include objectId if it is a dynamic related object
656
659
  }
657
660
  else if ('id' in value && 'name' in value) {
658
- submission[key] = pick(value, 'id', 'name');
661
+ submission[key] =
662
+ entry &&
663
+ ['input', 'inputField'].includes(entry.type) &&
664
+ entry.display?.relatedObjectId
665
+ ? pick(value, 'id', 'name', 'objectId')
666
+ : pick(value, 'id', 'name');
659
667
  }
660
668
  else if (value instanceof LocalDateTime) {
661
669
  submission[key] = normalizeDateTime(value);
@@ -667,21 +675,21 @@ export async function formatSubmission(submission, apiServices, objectId, instan
667
675
  }
668
676
  return submission;
669
677
  }
670
- export function filterEmptySections(entry, formData, instance) {
678
+ export function filterEmptySections(entry, instance, formData) {
671
679
  if (entry.type === 'sections' && isArray(entry.sections)) {
672
680
  const visibleSections = entry.sections.filter((section) => {
673
681
  if (!section.entries || section.entries.length === 0)
674
682
  return false;
675
683
  for (const sectionEntry of section.entries) {
676
684
  if (sectionEntry.type === 'sections' || sectionEntry.type === 'columns') {
677
- if (sectionEntry.visibility && !entryIsVisible(sectionEntry, formData, instance)) {
685
+ if (sectionEntry.visibility && !entryIsVisible(sectionEntry, instance, formData)) {
678
686
  return false;
679
687
  }
680
- else if (filterEmptySections(sectionEntry, formData, instance)) {
688
+ else if (filterEmptySections(sectionEntry, instance, formData)) {
681
689
  return true;
682
690
  }
683
691
  }
684
- else if (entryIsVisible(sectionEntry, formData, instance)) {
692
+ else if (entryIsVisible(sectionEntry, instance, formData)) {
685
693
  return true;
686
694
  }
687
695
  }
@@ -701,13 +709,13 @@ export function filterEmptySections(entry, formData, instance) {
701
709
  let hasVisibleEntry = false;
702
710
  for (const columnEntry of column.entries) {
703
711
  if (columnEntry.type === 'sections' || columnEntry.type === 'columns') {
704
- if (filterEmptySections(columnEntry, formData, instance)) {
712
+ if (filterEmptySections(columnEntry, instance, formData)) {
705
713
  hasVisibleEntry = true;
706
714
  break;
707
715
  }
708
716
  }
709
717
  else {
710
- if (entryIsVisible(columnEntry, formData, instance)) {
718
+ if (entryIsVisible(columnEntry, instance, formData)) {
711
719
  hasVisibleEntry = true;
712
720
  break;
713
721
  }
@@ -808,8 +816,8 @@ export function getFieldDefinition(entry, object, parameters, isDocument) {
808
816
  def = isDocument
809
817
  ? docProperties.find((prop) => prop.id === entry.propertyId)
810
818
  : isAddressProperty(entry.propertyId)
811
- ? object?.properties?.find((prop) => prop.id === entry.propertyId.split('.')[0])
812
- : object?.properties?.find((prop) => prop.id === entry.propertyId);
819
+ ? object.properties?.find((prop) => prop.id === entry.propertyId.split('.')[0])
820
+ : object.properties?.find((prop) => prop.id === entry.propertyId);
813
821
  }
814
822
  else if (entry.type === 'inputField') {
815
823
  def = entry.input;
@@ -825,3 +833,65 @@ export function getFieldDefinition(entry, object, parameters, isDocument) {
825
833
  }
826
834
  return def;
827
835
  }
836
+ export function obfuscateValue(value, property) {
837
+ const { mask, protection } = property ?? {};
838
+ if (!protection?.maskChar) {
839
+ return value;
840
+ }
841
+ const maskChar = protection.maskChar;
842
+ const stringValue = String(value ?? '');
843
+ const simpleValue = mask && mask.length === stringValue.length ? stripValueOfMask(stringValue, mask) : stringValue;
844
+ let { preserveFirst = 0, preserveLast = 0 } = protection;
845
+ // Preserved character count should never exceed simpleValue length.
846
+ // Also, when using preserve characters, always hide at least 1 character.
847
+ preserveFirst = Math.min(preserveFirst, simpleValue.length - 1);
848
+ preserveLast = Math.min(preserveLast, simpleValue.length - preserveFirst - 1);
849
+ let obfuscatedValue = '';
850
+ if (simpleValue.length) {
851
+ const prefix = simpleValue.slice(0, preserveFirst);
852
+ const suffix = preserveLast ? simpleValue.slice(-preserveLast) : '';
853
+ const maskedSection = maskChar[0].repeat(simpleValue.length - preserveFirst - preserveLast);
854
+ obfuscatedValue = prefix + maskedSection + suffix;
855
+ }
856
+ // Reapply mask to the obfuscated value
857
+ if (mask && mask.length === stringValue.length) {
858
+ obfuscatedValue = applyMaskToObfuscatedValue(obfuscatedValue, mask);
859
+ }
860
+ return obfuscatedValue;
861
+ }
862
+ function stripValueOfMask(value, mask) {
863
+ const valueChars = [...value];
864
+ let simpleValue = '';
865
+ for (const [index, maskChar] of [...mask].entries()) {
866
+ const isPlaceholder = ['9', 'a', '*'].includes(maskChar);
867
+ if (!valueChars[index]) {
868
+ break;
869
+ }
870
+ else if (isPlaceholder) {
871
+ simpleValue += valueChars[index];
872
+ }
873
+ }
874
+ return simpleValue;
875
+ }
876
+ function applyMaskToObfuscatedValue(value, mask) {
877
+ const valueChars = [...value];
878
+ let maskedValue = '';
879
+ let valueIndex = 0;
880
+ for (const maskChar of mask) {
881
+ const isPlaceholder = ['9', 'a', '*'].includes(maskChar);
882
+ if (isPlaceholder) {
883
+ if (valueIndex < valueChars.length) {
884
+ maskedValue += valueChars[valueIndex];
885
+ valueIndex++;
886
+ }
887
+ else {
888
+ // Stop if value is shorter than mask
889
+ break;
890
+ }
891
+ }
892
+ else {
893
+ maskedValue += maskChar;
894
+ }
895
+ }
896
+ return maskedValue;
897
+ }