@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.
- package/dist/published/components/core/Autocomplete/Autocomplete.js +4 -2
- package/dist/published/components/core/Autocomplete/Autocomplete.test.js +112 -3
- package/dist/published/components/core/TextField/TextField.js +1 -1
- package/dist/published/components/core/TextField/TextField.test.js +0 -2
- package/dist/published/components/custom/CriteriaBuilder/CriteriaBuilder.js +24 -2
- package/dist/published/components/custom/CriteriaBuilder/CriteriaBuilder.test.js +45 -2
- package/dist/published/components/custom/Form/FormComponents/DocumentComponent/Document.js +2 -1
- package/dist/published/components/custom/Form/FormComponents/RepeatableFieldComponent/RepeatableField.js +1 -1
- package/dist/published/components/custom/Form/tests/Form.test.js +0 -2
- package/dist/published/components/custom/FormField/DatePickerSelect/DatePickerSelect.js +36 -7
- package/dist/published/components/custom/FormField/DateTimePickerSelect/DateTimePickerSelect.js +14 -1
- package/dist/published/components/custom/FormField/FormField.d.ts +3 -1
- package/dist/published/components/custom/FormField/FormField.js +17 -5
- package/dist/published/components/custom/FormField/InputFieldComponent/InputFieldComponent.js +6 -4
- package/dist/published/components/custom/FormField/InputFieldComponent/InputFieldComponent.test.js +0 -2
- package/dist/published/components/custom/FormField/Select/Select.test.js +0 -2
- package/dist/published/components/custom/FormField/TimePickerSelect/TimePickerSelect.js +14 -1
- package/dist/published/components/custom/FormV2/FormRenderer.d.ts +2 -1
- package/dist/published/components/custom/FormV2/FormRenderer.js +46 -8
- package/dist/published/components/custom/FormV2/FormRendererContainer.js +178 -153
- package/dist/published/components/custom/FormV2/components/AccordionSections.js +7 -2
- package/dist/published/components/custom/FormV2/components/Body.d.ts +1 -1
- package/dist/published/components/custom/FormV2/components/DefaultValues.d.ts +2 -2
- package/dist/published/components/custom/FormV2/components/DefaultValues.js +36 -28
- package/dist/published/components/custom/FormV2/components/FieldWrapper.js +1 -1
- package/dist/published/components/custom/FormV2/components/Footer.d.ts +1 -0
- package/dist/published/components/custom/FormV2/components/Footer.js +8 -5
- package/dist/published/components/custom/FormV2/components/FormContext.d.ts +3 -2
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/AddressFields.d.ts +9 -0
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/AddressFields.js +32 -15
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/ActionDialog.js +2 -2
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/RepeatableField.js +9 -23
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/Criteria.js +16 -3
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/Document.js +22 -4
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/DocumentList.d.ts +2 -1
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/DocumentList.js +16 -3
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/Image.js +31 -5
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/UserProperty.js +15 -3
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/ObjectPropertyInput.js +127 -92
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/RelatedObjectInstance.d.ts +2 -3
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/RelatedObjectInstance.js +43 -20
- package/dist/published/components/custom/FormV2/components/Header.d.ts +5 -3
- package/dist/published/components/custom/FormV2/components/Header.js +47 -9
- package/dist/published/components/custom/FormV2/components/PropertyProtection.d.ts +16 -0
- package/dist/published/components/custom/FormV2/components/PropertyProtection.js +113 -0
- package/dist/published/components/custom/FormV2/components/RecursiveEntryRenderer.js +47 -24
- package/dist/published/components/custom/FormV2/components/ValidationFiles/ValidationErrors.js +1 -1
- package/dist/published/components/custom/FormV2/components/types.d.ts +2 -0
- package/dist/published/components/custom/FormV2/components/utils.d.ts +6 -4
- package/dist/published/components/custom/FormV2/components/utils.js +83 -13
- package/dist/published/components/custom/FormV2/tests/FormRenderer.test.js +411 -44
- package/dist/published/components/custom/FormV2/tests/FormRendererContainer.test.js +983 -16
- package/dist/published/components/custom/FormV2/tests/test-data.d.ts +1 -0
- package/dist/published/components/custom/FormV2/tests/test-data.js +138 -0
- package/dist/published/components/custom/ViewDetailsV2/InstanceEntryRenderer.d.ts +3 -0
- package/dist/published/components/custom/ViewDetailsV2/InstanceEntryRenderer.js +165 -0
- package/dist/published/components/custom/ViewDetailsV2/ViewDetailsV2Container.d.ts +13 -0
- package/dist/published/components/custom/ViewDetailsV2/ViewDetailsV2Container.js +144 -0
- package/dist/published/components/custom/ViewDetailsV2/index.d.ts +3 -0
- package/dist/published/components/custom/ViewDetailsV2/index.js +2 -0
- package/dist/published/components/custom/index.d.ts +2 -0
- package/dist/published/components/custom/index.js +1 -0
- package/dist/published/index.d.ts +6 -6
- package/dist/published/index.js +1 -1
- package/dist/published/stories/CriteriaBuilder.stories.js +6 -0
- package/dist/published/stories/FormRenderer.stories.d.ts +8 -4
- package/dist/published/stories/FormRendererContainer.stories.d.ts +26 -0
- package/dist/published/stories/FormRendererContainer.stories.js +5 -0
- package/dist/published/stories/FormRendererData.d.ts +12 -0
- package/dist/published/stories/FormRendererData.js +26 -1
- package/dist/published/stories/ViewDetailsV2Container.stories.d.ts +26 -0
- package/dist/published/stories/ViewDetailsV2Container.stories.js +37 -0
- package/dist/published/stories/ViewDetailsV2Data.d.ts +4 -0
- package/dist/published/stories/ViewDetailsV2Data.js +203 -0
- package/dist/published/stories/sharedMswHandlers.js +49 -10
- package/dist/published/theme/hooks.d.ts +4 -3
- package/dist/published/types.d.ts +3 -0
- 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
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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) },
|
|
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
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
|
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') {
|
package/dist/published/components/custom/FormV2/components/ValidationFiles/ValidationErrors.js
CHANGED
|
@@ -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,
|
|
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,
|
|
90
|
-
export declare function assignIdsToSectionsAndRichText(entries: FormEntry[], object
|
|
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
|
|
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,
|
|
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] =
|
|
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,
|
|
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,
|
|
685
|
+
if (sectionEntry.visibility && !entryIsVisible(sectionEntry, instance, formData)) {
|
|
678
686
|
return false;
|
|
679
687
|
}
|
|
680
|
-
else if (filterEmptySections(sectionEntry,
|
|
688
|
+
else if (filterEmptySections(sectionEntry, instance, formData)) {
|
|
681
689
|
return true;
|
|
682
690
|
}
|
|
683
691
|
}
|
|
684
|
-
else if (entryIsVisible(sectionEntry,
|
|
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,
|
|
712
|
+
if (filterEmptySections(columnEntry, instance, formData)) {
|
|
705
713
|
hasVisibleEntry = true;
|
|
706
714
|
break;
|
|
707
715
|
}
|
|
708
716
|
}
|
|
709
717
|
else {
|
|
710
|
-
if (entryIsVisible(columnEntry,
|
|
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
|
|
812
|
-
: object
|
|
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
|
+
}
|