@evoke-platform/ui-components 1.10.0-dev.33 → 1.10.0-dev.35
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/published/components/custom/CriteriaBuilder/CriteriaBuilder.js +24 -2
- package/dist/published/components/custom/CriteriaBuilder/CriteriaBuilder.test.js +45 -0
- package/dist/published/components/custom/FormField/DatePickerSelect/DatePickerSelect.js +22 -6
- 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/FormV2/FormRenderer.js +17 -0
- package/dist/published/components/custom/FormV2/FormRendererContainer.js +66 -83
- 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/FormFieldTypes/relatedObjectFiles/ObjectPropertyInput.js +13 -13
- 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 +5 -4
- 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 +3 -2
- package/dist/published/components/custom/FormV2/components/types.d.ts +1 -0
- package/dist/published/components/custom/FormV2/components/utils.d.ts +2 -0
- package/dist/published/components/custom/FormV2/components/utils.js +72 -4
- package/dist/published/components/custom/FormV2/tests/FormRenderer.test.js +127 -1
- package/dist/published/components/custom/ViewDetailsV2/InstanceEntryRenderer.js +13 -3
- package/dist/published/stories/CriteriaBuilder.stories.js +6 -0
- package/dist/published/types.d.ts +3 -0
- package/package.json +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { isArray, isEmpty, uniq } from 'lodash';
|
|
2
2
|
import { DateTime } from 'luxon';
|
|
3
3
|
import { getEntryId, getPrefixedUrl, isAddressProperty } from './utils';
|
|
4
|
-
export async function evalDefaultVals(parameters, entry, fieldValue, fieldId, apiServices, userAccount, formValues, updatedRelatedObjectValue) {
|
|
4
|
+
export async function evalDefaultVals(parameters, unnestedEntries, entry, fieldValue, fieldId, apiServices, userAccount, formValues, updatedRelatedObjectValue) {
|
|
5
5
|
const updates = [];
|
|
6
6
|
const parameter = parameters.find((param) => param.id === fieldId);
|
|
7
7
|
const defaultValue = entry.display?.defaultValue;
|
|
@@ -15,9 +15,9 @@ export async function evalDefaultVals(parameters, entry, fieldValue, fieldId, ap
|
|
|
15
15
|
const groups = regex.exec(item)?.groups;
|
|
16
16
|
if (groups?.relatedObjectProperty && groups?.nestedProperty) {
|
|
17
17
|
const relatedObjectParameter = parameters.find((param) => param.id === groups?.relatedObjectProperty);
|
|
18
|
-
let
|
|
19
|
-
if (!
|
|
20
|
-
|
|
18
|
+
let relatedObjectInstance = updatedRelatedObjectValue;
|
|
19
|
+
if (!relatedObjectInstance && !isEmpty(formValues)) {
|
|
20
|
+
relatedObjectInstance = formValues[groups.relatedObjectProperty];
|
|
21
21
|
}
|
|
22
22
|
if (updatedRelatedObjectValue?.[groups.nestedProperty]) {
|
|
23
23
|
fieldValue = uniq([
|
|
@@ -28,9 +28,14 @@ export async function evalDefaultVals(parameters, entry, fieldValue, fieldId, ap
|
|
|
28
28
|
]);
|
|
29
29
|
updates.push({ fieldId, fieldValue });
|
|
30
30
|
}
|
|
31
|
-
else if (
|
|
31
|
+
else if (relatedObjectInstance?.id && relatedObjectParameter) {
|
|
32
|
+
let relatedObjectId = relatedObjectParameter.objectId;
|
|
33
|
+
if (!relatedObjectId) {
|
|
34
|
+
const relatedObjectParamEntry = unnestedEntries.find((e) => getEntryId(e) === relatedObjectParameter.id);
|
|
35
|
+
relatedObjectId = relatedObjectParamEntry?.display?.relatedObjectId;
|
|
36
|
+
}
|
|
32
37
|
const instance = await new Promise((resolve) => {
|
|
33
|
-
apiServices.get(getPrefixedUrl(`/objects/${
|
|
38
|
+
apiServices.get(getPrefixedUrl(`/objects/${relatedObjectId}/instances/${relatedObjectInstance?.id}`), (error, instance) => {
|
|
34
39
|
if (error) {
|
|
35
40
|
console.error(error);
|
|
36
41
|
return resolve(undefined);
|
|
@@ -64,17 +69,22 @@ export async function evalDefaultVals(parameters, entry, fieldValue, fieldId, ap
|
|
|
64
69
|
const groups = regex.exec(defaultValue)?.groups;
|
|
65
70
|
if (groups?.relatedObjectProperty && groups?.addressProperty && groups?.nestedAddressProperty) {
|
|
66
71
|
const relatedObjectParameter = parameters.find((param) => param.id === groups?.relatedObjectProperty);
|
|
67
|
-
let
|
|
68
|
-
if (!
|
|
69
|
-
|
|
72
|
+
let relatedObjectInstance = updatedRelatedObjectValue;
|
|
73
|
+
if (!relatedObjectInstance && !isEmpty(formValues)) {
|
|
74
|
+
relatedObjectInstance = formValues[groups.relatedObjectProperty];
|
|
70
75
|
}
|
|
71
76
|
if (updatedRelatedObjectValue?.[groups.addressProperty]?.[groups.nestedAddressProperty]) {
|
|
72
77
|
fieldValue = updatedRelatedObjectValue?.[groups.addressProperty]?.[groups.nestedAddressProperty];
|
|
73
78
|
updates.push({ fieldId, fieldValue });
|
|
74
79
|
}
|
|
75
|
-
else if (
|
|
80
|
+
else if (relatedObjectInstance?.id && relatedObjectParameter) {
|
|
81
|
+
let relatedObjectId = relatedObjectParameter.objectId;
|
|
82
|
+
if (!relatedObjectId) {
|
|
83
|
+
const relatedObjectParamEntry = unnestedEntries.find((e) => getEntryId(e) === relatedObjectParameter.id);
|
|
84
|
+
relatedObjectId = relatedObjectParamEntry?.display?.relatedObjectId;
|
|
85
|
+
}
|
|
76
86
|
const instance = await new Promise((resolve) => {
|
|
77
|
-
apiServices.get(getPrefixedUrl(`/objects/${
|
|
87
|
+
apiServices.get(getPrefixedUrl(`/objects/${relatedObjectId}/instances/${relatedObjectInstance?.id}`), (error, instance) => {
|
|
78
88
|
if (error) {
|
|
79
89
|
console.error(error);
|
|
80
90
|
return resolve(undefined);
|
|
@@ -99,17 +109,22 @@ export async function evalDefaultVals(parameters, entry, fieldValue, fieldId, ap
|
|
|
99
109
|
const groups = regex.exec(defaultValue)?.groups;
|
|
100
110
|
if (groups?.relatedObjectProperty && groups?.nestedProperty) {
|
|
101
111
|
const relatedObjectParameter = parameters.find((param) => param.id === groups?.relatedObjectProperty);
|
|
102
|
-
let
|
|
103
|
-
if (!
|
|
104
|
-
|
|
112
|
+
let relatedObjectInstance = updatedRelatedObjectValue;
|
|
113
|
+
if (!relatedObjectInstance && !isEmpty(formValues)) {
|
|
114
|
+
relatedObjectInstance = formValues[groups.relatedObjectProperty];
|
|
105
115
|
}
|
|
106
116
|
if (updatedRelatedObjectValue?.[groups.nestedProperty]) {
|
|
107
117
|
fieldValue = updatedRelatedObjectValue[groups.nestedProperty];
|
|
108
118
|
updates.push({ fieldId, fieldValue });
|
|
109
119
|
}
|
|
110
|
-
else if (
|
|
120
|
+
else if (relatedObjectInstance?.id && relatedObjectParameter) {
|
|
121
|
+
let relatedObjectId = relatedObjectParameter.objectId;
|
|
122
|
+
if (!relatedObjectId) {
|
|
123
|
+
const relatedObjectParamEntry = unnestedEntries.find((e) => getEntryId(e) === relatedObjectParameter.id);
|
|
124
|
+
relatedObjectId = relatedObjectParamEntry?.display?.relatedObjectId;
|
|
125
|
+
}
|
|
111
126
|
const instance = await new Promise((resolve) => {
|
|
112
|
-
apiServices.get(getPrefixedUrl(`/objects/${
|
|
127
|
+
apiServices.get(getPrefixedUrl(`/objects/${relatedObjectId}/instances/${relatedObjectInstance?.id}`), (error, instance) => {
|
|
113
128
|
if (error) {
|
|
114
129
|
console.error(error);
|
|
115
130
|
return resolve(undefined);
|
|
@@ -153,16 +168,9 @@ export async function evalDefaultVals(parameters, entry, fieldValue, fieldId, ap
|
|
|
153
168
|
}
|
|
154
169
|
return updates;
|
|
155
170
|
}
|
|
156
|
-
export async function processValueUpdate(
|
|
171
|
+
export async function processValueUpdate(unnestedEntries, parameters, updatedRelatedObjectValue, apiServices, changedEntryId, formValues, userAccount) {
|
|
157
172
|
const updates = [];
|
|
158
|
-
for (const entry of
|
|
159
|
-
if (entry.type === 'sections' || entry.type === 'columns') {
|
|
160
|
-
const subEntries = entry.type === 'sections' ? entry.sections : entry.columns;
|
|
161
|
-
for (const subEntry of subEntries) {
|
|
162
|
-
const subUpdates = await processValueUpdate(subEntry.entries, parameters, updatedRelatedObjectValue, apiServices, changedEntryId, formValues, userAccount);
|
|
163
|
-
updates.push(...subUpdates);
|
|
164
|
-
}
|
|
165
|
-
}
|
|
173
|
+
for (const entry of unnestedEntries) {
|
|
166
174
|
if ((entry.type === 'input' || entry.type === 'inputField') && entry?.display?.defaultValue) {
|
|
167
175
|
const parameterId = getEntryId(entry);
|
|
168
176
|
if (!parameterId)
|
|
@@ -175,7 +183,7 @@ export async function processValueUpdate(entries, parameters, updatedRelatedObje
|
|
|
175
183
|
groups?.addressProperty &&
|
|
176
184
|
groups?.nestedAddressProperty &&
|
|
177
185
|
changedEntryId === groups.relatedObjectProperty) {
|
|
178
|
-
const result = await evalDefaultVals(parameters, entry, formValues?.[addressObject]?.[addressField], parameterId, apiServices, userAccount, formValues, updatedRelatedObjectValue);
|
|
186
|
+
const result = await evalDefaultVals(parameters, unnestedEntries, entry, formValues?.[addressObject]?.[addressField], parameterId, apiServices, userAccount, formValues, updatedRelatedObjectValue);
|
|
179
187
|
updates.push(...result);
|
|
180
188
|
}
|
|
181
189
|
}
|
|
@@ -187,7 +195,7 @@ export async function processValueUpdate(entries, parameters, updatedRelatedObje
|
|
|
187
195
|
if (groups?.relatedObjectProperty &&
|
|
188
196
|
groups?.nestedProperty &&
|
|
189
197
|
changedEntryId === groups.relatedObjectProperty) {
|
|
190
|
-
const result = await evalDefaultVals(parameters, entry, entry.display.defaultValue, parameterId, apiServices, userAccount, formValues, updatedRelatedObjectValue);
|
|
198
|
+
const result = await evalDefaultVals(parameters, unnestedEntries, entry, entry.display.defaultValue, parameterId, apiServices, userAccount, formValues, updatedRelatedObjectValue);
|
|
191
199
|
updates.push(...result);
|
|
192
200
|
}
|
|
193
201
|
}
|
|
@@ -198,7 +206,7 @@ export async function processValueUpdate(entries, parameters, updatedRelatedObje
|
|
|
198
206
|
if (groups?.relatedObjectProperty &&
|
|
199
207
|
groups?.nestedProperty &&
|
|
200
208
|
changedEntryId === groups.relatedObjectProperty) {
|
|
201
|
-
const result = await evalDefaultVals(parameters, entry, formValues?.[parameterId], parameterId, apiServices, userAccount, formValues, updatedRelatedObjectValue);
|
|
209
|
+
const result = await evalDefaultVals(parameters, unnestedEntries, entry, formValues?.[parameterId], parameterId, apiServices, userAccount, formValues, updatedRelatedObjectValue);
|
|
202
210
|
updates.push(...result);
|
|
203
211
|
}
|
|
204
212
|
}
|
|
@@ -10,7 +10,7 @@ import { Box } from '../../../../../layout';
|
|
|
10
10
|
import { getDefaultPages, getPrefixedUrl, transformToWhere } from '../../utils';
|
|
11
11
|
import RelatedObjectInstance from './RelatedObjectInstance';
|
|
12
12
|
const ObjectPropertyInput = (props) => {
|
|
13
|
-
const { id, fieldDefinition, readOnly, error, mode, displayOption, filter, defaultValueCriteria, sortBy, orderBy, isModal, initialValue, viewLayout, hasDescription, createActionId, formId, } = props;
|
|
13
|
+
const { id, fieldDefinition, readOnly, error, mode, displayOption, filter, defaultValueCriteria, sortBy, orderBy, isModal, initialValue, viewLayout, hasDescription, createActionId, formId, relatedObjectId, } = props;
|
|
14
14
|
const { fetchedOptions, setFetchedOptions, parameters, fieldHeight, handleChange: handleChangeObjectField, onAutosave: onAutosave, instance, } = useFormContext();
|
|
15
15
|
const { defaultPages, findDefaultPageSlugFor } = useApp();
|
|
16
16
|
const [selectedInstance, setSelectedInstance] = useState(initialValue || undefined);
|
|
@@ -88,7 +88,7 @@ const ObjectPropertyInput = (props) => {
|
|
|
88
88
|
});
|
|
89
89
|
if (updatedFilter.where) {
|
|
90
90
|
setLoadingOptions(true);
|
|
91
|
-
apiServices.get(getPrefixedUrl(`/objects/${
|
|
91
|
+
apiServices.get(getPrefixedUrl(`/objects/${relatedObjectId}/instances?filter=${encodeURIComponent(JSON.stringify(updatedFilter))}`), async (error, instances) => {
|
|
92
92
|
if (error) {
|
|
93
93
|
console.error(error);
|
|
94
94
|
setLoadingOptions(false);
|
|
@@ -114,7 +114,7 @@ const ObjectPropertyInput = (props) => {
|
|
|
114
114
|
});
|
|
115
115
|
}
|
|
116
116
|
}
|
|
117
|
-
}, [
|
|
117
|
+
}, [relatedObjectId, defaultValueCriteria, sortBy, orderBy]);
|
|
118
118
|
const getDropdownOptions = useCallback(() => {
|
|
119
119
|
if (((!fetchedOptions?.[`${id}Options`] ||
|
|
120
120
|
(fetchedOptions?.[`${id}Options`]).length === 0) &&
|
|
@@ -129,7 +129,7 @@ const ObjectPropertyInput = (props) => {
|
|
|
129
129
|
direction: 'asc',
|
|
130
130
|
};
|
|
131
131
|
updatedFilter.order = `${propertyId} ${direction}`;
|
|
132
|
-
apiServices.get(getPrefixedUrl(`/objects/${
|
|
132
|
+
apiServices.get(getPrefixedUrl(`/objects/${relatedObjectId}/instances?filter=${JSON.stringify(updatedFilter)}`), (error, instances) => {
|
|
133
133
|
if (error) {
|
|
134
134
|
console.error(error);
|
|
135
135
|
setLoadingOptions(false);
|
|
@@ -143,7 +143,7 @@ const ObjectPropertyInput = (props) => {
|
|
|
143
143
|
});
|
|
144
144
|
}
|
|
145
145
|
}, [
|
|
146
|
-
|
|
146
|
+
relatedObjectId,
|
|
147
147
|
updatedCriteria,
|
|
148
148
|
layout,
|
|
149
149
|
fetchedOptions?.[`${id}Options`],
|
|
@@ -183,10 +183,10 @@ const ObjectPropertyInput = (props) => {
|
|
|
183
183
|
}
|
|
184
184
|
};
|
|
185
185
|
fetchForm();
|
|
186
|
-
}, [action, formId, id,
|
|
186
|
+
}, [action, formId, id, apiServices, fetchedOptions]);
|
|
187
187
|
useEffect(() => {
|
|
188
188
|
if (!fetchedOptions[`${id}RelatedObject`]) {
|
|
189
|
-
apiServices.get(getPrefixedUrl(`/objects/${
|
|
189
|
+
apiServices.get(getPrefixedUrl(`/objects/${relatedObjectId}/effective?sanitizedVersion=true`), (error, object) => {
|
|
190
190
|
if (error) {
|
|
191
191
|
console.error(error);
|
|
192
192
|
}
|
|
@@ -195,15 +195,15 @@ const ObjectPropertyInput = (props) => {
|
|
|
195
195
|
}
|
|
196
196
|
});
|
|
197
197
|
}
|
|
198
|
-
}, [
|
|
198
|
+
}, [relatedObjectId, fetchedOptions, id]);
|
|
199
199
|
useEffect(() => {
|
|
200
200
|
(async () => {
|
|
201
201
|
if (parameters && fetchedOptions[`${id}NavigationSlug`] === undefined) {
|
|
202
202
|
const pages = await getDefaultPages(parameters, defaultPages, findDefaultPageSlugFor);
|
|
203
|
-
if (
|
|
204
|
-
setNavigationSlug(pages[
|
|
203
|
+
if (relatedObjectId && pages[relatedObjectId]) {
|
|
204
|
+
setNavigationSlug(pages[relatedObjectId]);
|
|
205
205
|
setFetchedOptions({
|
|
206
|
-
[`${id}NavigationSlug`]: pages[
|
|
206
|
+
[`${id}NavigationSlug`]: pages[relatedObjectId],
|
|
207
207
|
});
|
|
208
208
|
}
|
|
209
209
|
else {
|
|
@@ -214,7 +214,7 @@ const ObjectPropertyInput = (props) => {
|
|
|
214
214
|
}
|
|
215
215
|
}
|
|
216
216
|
})();
|
|
217
|
-
}, [parameters, defaultPages, findDefaultPageSlugFor,
|
|
217
|
+
}, [parameters, defaultPages, findDefaultPageSlugFor, relatedObjectId, fetchedOptions]);
|
|
218
218
|
const handleClose = () => {
|
|
219
219
|
setOpenCreateDialog(false);
|
|
220
220
|
};
|
|
@@ -522,7 +522,7 @@ const ObjectPropertyInput = (props) => {
|
|
|
522
522
|
event.stopPropagation();
|
|
523
523
|
setOpenCreateDialog(true);
|
|
524
524
|
}, "aria-label": `Add` }, "Add")))),
|
|
525
|
-
React.createElement(RelatedObjectInstance, { open: openCreateDialog, title: form?.name ?? `Add ${fieldDefinition.name}`, handleClose: handleClose, setSelectedInstance: setSelectedInstance, relatedObject: relatedObject, id: id, mode: mode, displayOption: displayOption, setOptions: setOptions, options: options, filter: updatedCriteria, layout: layout, formId: formId ?? form?.id, actionId: createActionId, setSnackbarError: setSnackbarError
|
|
525
|
+
React.createElement(RelatedObjectInstance, { open: openCreateDialog, title: form?.name ?? `Add ${fieldDefinition.name}`, handleClose: handleClose, setSelectedInstance: setSelectedInstance, relatedObject: relatedObject, id: id, mode: mode, displayOption: displayOption, setOptions: setOptions, options: options, filter: updatedCriteria, layout: layout, formId: formId ?? form?.id, actionId: createActionId, setSnackbarError: setSnackbarError }),
|
|
526
526
|
React.createElement(Snackbar, { open: snackbarError.showAlert, handleClose: () => setSnackbarError({
|
|
527
527
|
isError: snackbarError.isError,
|
|
528
528
|
showAlert: false,
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Obj, ObjectInstance, TableViewLayout } from '@evoke-platform/context';
|
|
2
2
|
import React from 'react';
|
|
3
3
|
import { BaseProps } from '../../types';
|
|
4
4
|
export type RelatedObjectInstanceProps = BaseProps & {
|
|
5
5
|
id: string;
|
|
6
6
|
open: boolean;
|
|
7
7
|
title: string;
|
|
8
|
-
relatedObject
|
|
8
|
+
relatedObject?: Obj;
|
|
9
9
|
setSelectedInstance: (selectedInstance: ObjectInstance) => void;
|
|
10
10
|
handleClose: () => void;
|
|
11
11
|
mode: 'default' | 'existingOnly' | 'newOnly';
|
|
@@ -21,7 +21,6 @@ export type RelatedObjectInstanceProps = BaseProps & {
|
|
|
21
21
|
layout?: TableViewLayout;
|
|
22
22
|
formId?: string;
|
|
23
23
|
actionId?: string;
|
|
24
|
-
fieldDefinition: InputParameter;
|
|
25
24
|
};
|
|
26
25
|
declare const RelatedObjectInstance: (props: RelatedObjectInstanceProps) => React.JSX.Element;
|
|
27
26
|
export default RelatedObjectInstance;
|
|
@@ -6,6 +6,7 @@ import { Close } from '../../../../../../icons';
|
|
|
6
6
|
import useWidgetSize, { useFormContext } from '../../../../../../theme/hooks';
|
|
7
7
|
import { Button, Dialog, DialogContent, DialogTitle, FormControlLabel, IconButton, Radio, RadioGroup, } from '../../../../../core';
|
|
8
8
|
import Box from '../../../../../layout/Box/Box';
|
|
9
|
+
import ErrorComponent from '../../../../ErrorComponent';
|
|
9
10
|
import FormRenderer from '../../../FormRenderer';
|
|
10
11
|
import FormRendererContainer from '../../../FormRendererContainer';
|
|
11
12
|
import Body from '../../Body';
|
|
@@ -28,7 +29,7 @@ const styles = {
|
|
|
28
29
|
},
|
|
29
30
|
};
|
|
30
31
|
const RelatedObjectInstance = (props) => {
|
|
31
|
-
const { relatedObject, open, title, id, setSelectedInstance, handleClose, mode, displayOption, filter, layout, formId, actionId,
|
|
32
|
+
const { relatedObject, open, title, id, setSelectedInstance, handleClose, mode, displayOption, filter, layout, formId, actionId, setSnackbarError, setOptions, options, } = props;
|
|
32
33
|
const { handleChange: handleChangeObjectField, onAutosave, richTextEditor, fieldHeight, width } = useFormContext();
|
|
33
34
|
const [selectedRow, setSelectedRow] = useState();
|
|
34
35
|
const [relationType, setRelationType] = useState(displayOption === 'dropdown' || mode === 'newOnly' ? 'new' : 'existing');
|
|
@@ -120,7 +121,7 @@ const RelatedObjectInstance = (props) => {
|
|
|
120
121
|
}, value: relationType },
|
|
121
122
|
React.createElement(FormControlLabel, { value: "existing", control: React.createElement(Radio, { sx: { '&.Mui-checked': { color: 'primary' } } }), label: "Existing" }),
|
|
122
123
|
React.createElement(FormControlLabel, { value: "new", control: React.createElement(Radio, { sx: { '&.Mui-checked': { color: 'primary' } } }), label: "New" }))) : null;
|
|
123
|
-
const DialogForm = useCallback(() => (React.createElement(FormRendererContainer, { formId: formId, display: { fieldHeight: fieldHeight ?? 'medium' }, actionId: actionId, objectId:
|
|
124
|
+
const DialogForm = useCallback(() => (React.createElement(FormRendererContainer, { formId: formId, display: { fieldHeight: fieldHeight ?? 'medium' }, actionId: actionId, objectId: relatedObject.id, onSubmit: createNewInstance, onDiscardChanges: onClose, onSubmitError: handleSubmitError, richTextEditor: richTextEditor, renderHeader: () => null, renderBody: (bodyProps) => (React.createElement(DialogContent, { sx: styles.dialogContent },
|
|
124
125
|
relationType === 'new' ? (React.createElement("div", { ref: validationErrorsRef }, !isEmpty(bodyProps.errors) && bodyProps.shouldShowValidationErrors ? (React.createElement(FormRenderer.ValidationErrors, { errors: bodyProps.errors, sx: {
|
|
125
126
|
my: isSm || isXs ? 2 : 3,
|
|
126
127
|
} })) : null)) : null,
|
|
@@ -136,7 +137,7 @@ const RelatedObjectInstance = (props) => {
|
|
|
136
137
|
React.createElement(RadioButtons, null),
|
|
137
138
|
defaultContainer)),
|
|
138
139
|
status === 'ready' && defaultContainer));
|
|
139
|
-
}, sx: { border: 'none' } })), [formId, actionId,
|
|
140
|
+
}, sx: { border: 'none' } })), [formId, actionId, relatedObject, fieldHeight, richTextEditor, RadioButtons]);
|
|
140
141
|
return (React.createElement(Dialog, { fullWidth: true, maxWidth: "md", open: open, onClose: (e, reason) => reason !== 'backdropClick' && handleClose(), sx: {
|
|
141
142
|
background: 'none',
|
|
142
143
|
}, PaperProps: {
|
|
@@ -159,7 +160,7 @@ const RelatedObjectInstance = (props) => {
|
|
|
159
160
|
title,
|
|
160
161
|
React.createElement(IconButton, { onClick: onClose, "aria-label": "Close" },
|
|
161
162
|
React.createElement(Close, { fontSize: "small" })))),
|
|
162
|
-
relationType === 'new' ? (React.createElement(DialogForm, null)) : ((mode === 'default' || mode === 'existingOnly') &&
|
|
163
|
+
relationType === 'new' ? (relatedObject ? (React.createElement(DialogForm, null)) : (React.createElement(ErrorComponent, { code: "Misconfigured" }))) : ((mode === 'default' || mode === 'existingOnly') &&
|
|
163
164
|
relatedObject && (React.createElement(React.Fragment, null,
|
|
164
165
|
React.createElement(DialogContent, { sx: styles.dialogContent },
|
|
165
166
|
shouldShowRadioButtons && React.createElement(RadioButtons, null),
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { PropertyProtection as PropertyProtectionType } from '@evoke-platform/context';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { ObjectProperty } from '../../../../types';
|
|
4
|
+
type PropertyProtectionProps = {
|
|
5
|
+
parameter: ObjectProperty;
|
|
6
|
+
protection?: PropertyProtectionType;
|
|
7
|
+
mask?: string;
|
|
8
|
+
canEdit: boolean;
|
|
9
|
+
value: unknown;
|
|
10
|
+
handleChange?: (value: unknown) => void;
|
|
11
|
+
setCurrentDisplayValue: (value: unknown) => void;
|
|
12
|
+
mode: 'mask' | 'full' | 'edit';
|
|
13
|
+
setMode: (mode: 'mask' | 'full' | 'edit') => void;
|
|
14
|
+
};
|
|
15
|
+
declare const PropertyProtection: React.FC<PropertyProtectionProps>;
|
|
16
|
+
export default PropertyProtection;
|
|
@@ -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;
|
|
@@ -85,7 +85,7 @@ export function RecursiveEntryRenderer(props) {
|
|
|
85
85
|
}
|
|
86
86
|
else if (fieldDefinition.type === 'object') {
|
|
87
87
|
return (React.createElement(FieldWrapper, { ...getFieldWrapperProps(fieldDefinition, entry, entryId, fieldValue, display, errors) },
|
|
88
|
-
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
|
|
89
89
|
? updateCriteriaInputs(validation.criteria, getValues(), userAccount)
|
|
90
90
|
: undefined, sortBy: typeof display?.defaultValue === 'object' && 'sortBy' in display.defaultValue
|
|
91
91
|
? display?.defaultValue.sortBy
|
|
@@ -153,6 +153,7 @@ export function RecursiveEntryRenderer(props) {
|
|
|
153
153
|
'aria-describedby': `${entryId}-description`,
|
|
154
154
|
};
|
|
155
155
|
}
|
|
156
|
+
const objectProperty = object?.properties?.find((p) => p.id === entryId);
|
|
156
157
|
return (React.createElement(FieldWrapper
|
|
157
158
|
/*
|
|
158
159
|
* Key remounts the field if a value is changed.
|
|
@@ -178,7 +179,7 @@ export function RecursiveEntryRenderer(props) {
|
|
|
178
179
|
});
|
|
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'
|
|
180
181
|
? display?.booleanDisplay
|
|
181
|
-
: 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 })));
|
|
182
183
|
}
|
|
183
184
|
}
|
|
184
185
|
else if (entry.type === 'columns') {
|
|
@@ -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;
|
|
@@ -102,3 +103,4 @@ export declare function assignIdsToSectionsAndRichText(entries: FormEntry[], obj
|
|
|
102
103
|
*/
|
|
103
104
|
export declare function plainTextToRtf(plainText: string): string;
|
|
104
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;
|
|
@@ -622,14 +622,14 @@ export const deleteDocuments = async (submittedFields, requestSuccess, apiServic
|
|
|
622
622
|
* Returns the cleaned submission ready for submitting.
|
|
623
623
|
*/
|
|
624
624
|
export async function formatSubmission(submission, apiServices, objectId, instanceId, form, setSnackbarError) {
|
|
625
|
+
const allEntries = getUnnestedEntries(form?.entries ?? []) ?? [];
|
|
625
626
|
for (const [key, value] of Object.entries(submission)) {
|
|
627
|
+
const entry = allEntries?.find((entry) => getEntryId(entry) === key);
|
|
626
628
|
if (isArray(value)) {
|
|
627
629
|
// Only upload if array contains File instances (not SavedDocumentReference)
|
|
628
630
|
const fileInArray = value.some((item) => item instanceof File);
|
|
629
631
|
if (fileInArray && instanceId && apiServices && objectId) {
|
|
630
632
|
try {
|
|
631
|
-
const allEntries = getUnnestedEntries(form?.entries ?? []) ?? [];
|
|
632
|
-
const entry = allEntries?.find((entry) => getEntryId(entry) === key);
|
|
633
633
|
const uploadedDocuments = await uploadDocuments(value, {
|
|
634
634
|
type: '',
|
|
635
635
|
view_permission: '',
|
|
@@ -654,10 +654,16 @@ export async function formatSubmission(submission, apiServices, objectId, instan
|
|
|
654
654
|
else if (typeof value === 'object' && value !== null) {
|
|
655
655
|
if (Object.values(value).every((v) => v === undefined)) {
|
|
656
656
|
submission[key] = undefined;
|
|
657
|
-
// 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
|
|
658
659
|
}
|
|
659
660
|
else if ('id' in value && 'name' in value) {
|
|
660
|
-
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');
|
|
661
667
|
}
|
|
662
668
|
else if (value instanceof LocalDateTime) {
|
|
663
669
|
submission[key] = normalizeDateTime(value);
|
|
@@ -827,3 +833,65 @@ export function getFieldDefinition(entry, object, parameters, isDocument) {
|
|
|
827
833
|
}
|
|
828
834
|
return def;
|
|
829
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
|
+
}
|