@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.
Files changed (24) hide show
  1. package/dist/published/components/custom/CriteriaBuilder/CriteriaBuilder.js +24 -2
  2. package/dist/published/components/custom/CriteriaBuilder/CriteriaBuilder.test.js +45 -0
  3. package/dist/published/components/custom/FormField/DatePickerSelect/DatePickerSelect.js +22 -6
  4. package/dist/published/components/custom/FormField/FormField.d.ts +3 -1
  5. package/dist/published/components/custom/FormField/FormField.js +17 -5
  6. package/dist/published/components/custom/FormField/InputFieldComponent/InputFieldComponent.js +6 -4
  7. package/dist/published/components/custom/FormV2/FormRenderer.js +17 -0
  8. package/dist/published/components/custom/FormV2/FormRendererContainer.js +66 -83
  9. package/dist/published/components/custom/FormV2/components/DefaultValues.d.ts +2 -2
  10. package/dist/published/components/custom/FormV2/components/DefaultValues.js +36 -28
  11. package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/ObjectPropertyInput.js +13 -13
  12. package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/RelatedObjectInstance.d.ts +2 -3
  13. package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/RelatedObjectInstance.js +5 -4
  14. package/dist/published/components/custom/FormV2/components/PropertyProtection.d.ts +16 -0
  15. package/dist/published/components/custom/FormV2/components/PropertyProtection.js +113 -0
  16. package/dist/published/components/custom/FormV2/components/RecursiveEntryRenderer.js +3 -2
  17. package/dist/published/components/custom/FormV2/components/types.d.ts +1 -0
  18. package/dist/published/components/custom/FormV2/components/utils.d.ts +2 -0
  19. package/dist/published/components/custom/FormV2/components/utils.js +72 -4
  20. package/dist/published/components/custom/FormV2/tests/FormRenderer.test.js +127 -1
  21. package/dist/published/components/custom/ViewDetailsV2/InstanceEntryRenderer.js +13 -3
  22. package/dist/published/stories/CriteriaBuilder.stories.js +6 -0
  23. package/dist/published/types.d.ts +3 -0
  24. 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 relatedObject = updatedRelatedObjectValue;
19
- if (!relatedObject && !isEmpty(formValues)) {
20
- relatedObject = formValues[groups.relatedObjectProperty];
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 (relatedObject && relatedObject.id && relatedObjectParameter) {
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/${relatedObjectParameter.objectId}/instances/${relatedObject?.id}`), (error, instance) => {
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 relatedObject = updatedRelatedObjectValue;
68
- if (!relatedObject && !isEmpty(formValues)) {
69
- relatedObject = formValues[groups.relatedObjectProperty];
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 (relatedObject && relatedObject.id && relatedObjectParameter) {
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/${relatedObjectParameter.objectId}/instances/${relatedObject?.id}`), (error, instance) => {
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 relatedObject = updatedRelatedObjectValue;
103
- if (!relatedObject && !isEmpty(formValues)) {
104
- relatedObject = formValues[groups.relatedObjectProperty];
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 (relatedObject?.id && relatedObjectParameter) {
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/${relatedObjectParameter.objectId}/instances/${relatedObject?.id}`), (error, instance) => {
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(entries, parameters, updatedRelatedObjectValue, apiServices, changedEntryId, formValues, userAccount) {
171
+ export async function processValueUpdate(unnestedEntries, parameters, updatedRelatedObjectValue, apiServices, changedEntryId, formValues, userAccount) {
157
172
  const updates = [];
158
- for (const entry of entries || []) {
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/${fieldDefinition.objectId}/instances?filter=${encodeURIComponent(JSON.stringify(updatedFilter))}`), async (error, instances) => {
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
- }, [fieldDefinition, defaultValueCriteria, sortBy, orderBy]);
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/${fieldDefinition.objectId}/instances?filter=${JSON.stringify(updatedFilter)}`), (error, instances) => {
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
- fieldDefinition,
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, fieldDefinition.objectId, apiServices, fetchedOptions]);
186
+ }, [action, formId, id, apiServices, fetchedOptions]);
187
187
  useEffect(() => {
188
188
  if (!fetchedOptions[`${id}RelatedObject`]) {
189
- apiServices.get(getPrefixedUrl(`/objects/${fieldDefinition.objectId}/effective?sanitizedVersion=true`), (error, object) => {
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
- }, [fieldDefinition.objectId, fetchedOptions, id]);
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 (fieldDefinition.objectId && pages[fieldDefinition.objectId]) {
204
- setNavigationSlug(pages[fieldDefinition.objectId]);
203
+ if (relatedObjectId && pages[relatedObjectId]) {
204
+ setNavigationSlug(pages[relatedObjectId]);
205
205
  setFetchedOptions({
206
- [`${id}NavigationSlug`]: pages[fieldDefinition.objectId],
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, fieldDefinition, fetchedOptions]);
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, fieldDefinition: fieldDefinition }),
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 { InputParameter, Obj, ObjectInstance, TableViewLayout } from '@evoke-platform/context';
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: Obj | undefined;
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, fieldDefinition, setSnackbarError, setOptions, options, } = props;
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: fieldDefinition.objectId, onSubmit: createNewInstance, onDiscardChanges: onClose, onSubmitError: handleSubmitError, richTextEditor: richTextEditor, renderHeader: () => null, renderBody: (bodyProps) => (React.createElement(DialogContent, { sx: styles.dialogContent },
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, fieldDefinition, fieldHeight, richTextEditor, RadioButtons]);
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') {
@@ -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;
@@ -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] = 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');
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
+ }