@evoke-platform/ui-components 1.10.1-dev.6 → 1.11.0-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.
@@ -6,6 +6,7 @@ import React, { useEffect, useRef, useState } from 'react';
6
6
  import '../../../../styles/form-component.css';
7
7
  import { Skeleton, Snackbar } from '../../../core';
8
8
  import { Box } from '../../../layout';
9
+ import { obfuscateValue } from '../../FormV2/components/utils';
9
10
  import { ButtonComponent, DocumentComponent, FormFieldComponent, ImageComponent, ObjectComponent, RepeatableFieldComponent, UserComponent, ViewOnlyComponent, } from '../FormComponents';
10
11
  import { CriteriaComponent } from '../FormComponents/CriteriaComponent/CriteriaComponent';
11
12
  import { addObjectPropertiesToComponentProps, buildComponentPropsFromDocumentProperties, buildComponentPropsFromObjectProperties, containsCorruptedFiles, convertFormToComponents, flattenFormComponents, getAllCriteriaInputs, getFlattenEntries, getPrefixedUrl, isCorruptedFile, } from '../utils';
@@ -353,11 +354,19 @@ export function Form(props) {
353
354
  const submittedFields = {};
354
355
  for (const field in submission.data) {
355
356
  const value = submission.data[field];
357
+ const protectedProperties = object?.properties?.filter((prop) => prop.protection?.maskChar);
356
358
  if (value === '' ||
357
359
  (Array.isArray(value) && !value.length) ||
358
360
  (typeof value === 'object' && isEmpty(value))) {
359
361
  submittedFields[field] = null;
360
362
  }
363
+ else if (protectedProperties?.some((prop) => prop.id === field)) {
364
+ // When protected value hasn't been edited or viewed, don't
365
+ // save the obfuscated value.
366
+ const protectedProperty = protectedProperties.find((prop) => prop.id === field);
367
+ const isProtectedValue = value === obfuscateValue(value, protectedProperty);
368
+ submittedFields[field] = isProtectedValue ? undefined : value;
369
+ }
361
370
  else {
362
371
  if (isObject(value) && 'id' in value && 'name' in value) {
363
372
  submittedFields[field] = pick(value, 'id', 'name');
@@ -5,6 +5,7 @@ import Handlebars from 'no-eval-handlebars';
5
5
  import React from 'react';
6
6
  import ReactDOM from 'react-dom';
7
7
  import { FormField } from '../../../custom';
8
+ import { obfuscateValue } from '../../FormV2/components/utils';
8
9
  import { FormComponentWrapper } from '../Common';
9
10
  import { isPropertyVisible } from '../utils';
10
11
  function isAddressProperties(key) {
@@ -290,6 +291,15 @@ export class FormFieldComponent extends ReactComponent {
290
291
  if (!isPropertyVisible(this.component.conditional, this.root.data)) {
291
292
  return;
292
293
  }
294
+ // When protected value hasn't been edited or viewed, don't validate
295
+ // the obfuscated value
296
+ if (this.component.property.protection) {
297
+ const obfuscatedValue = obfuscateValue(value, this.component.property);
298
+ if (obfuscatedValue === value) {
299
+ delete this.errorDetails['rootError'];
300
+ return;
301
+ }
302
+ }
293
303
  const emptyMask = this.component.inputMaskPlaceholderChar &&
294
304
  this.component.inputMask
295
305
  ?.replaceAll('9', this.component.inputMaskPlaceholderChar)
@@ -490,6 +500,6 @@ export class FormFieldComponent extends ReactComponent {
490
500
  falsePositiveMaskError &&
491
501
  isEmpty(this.errorDetails) &&
492
502
  this.emit('changed-' + this.component.key, e.target.value);
493
- }, ...this.component, id: inputId, defaultValue: this.dataValue, mask: this.component.inputMask, error: this.hasErrors(), size: this.component.fieldHeight ?? 'medium', required: this.component.property.type === 'boolean' ? this.component.strictlyTrue : undefined, isCombobox: this.component.nonStrictEnum }))), root);
503
+ }, ...this.component, id: inputId, defaultValue: this.dataValue, mask: this.component.inputMask, error: this.hasErrors(), size: this.component.fieldHeight ?? 'medium', required: this.component.property.type === 'boolean' ? this.component.strictlyTrue : undefined, isCombobox: this.component.nonStrictEnum, protection: this.component.property.protection }))), root);
494
504
  }
495
505
  }
@@ -1,10 +1,12 @@
1
1
  import { ReactComponent } from '@formio/react';
2
2
  import { get, isEmpty } from 'lodash';
3
3
  import { DateTime } from 'luxon';
4
- import React from 'react';
4
+ import React, { useState } from 'react';
5
5
  import ReactDOM from 'react-dom';
6
+ import { Box } from '../../../..';
6
7
  import { Link, Typography } from '../../../core';
7
8
  import BooleanSelect from '../../FormField/BooleanSelect/BooleanSelect';
9
+ import PropertyProtection from '../../FormV2/components/PropertyProtection';
8
10
  import { FormComponentWrapper } from '../Common/FormComponentWrapper';
9
11
  export class ViewOnlyComponent extends ReactComponent {
10
12
  /**
@@ -26,8 +28,9 @@ export class ViewOnlyComponent extends ReactComponent {
26
28
  this.showValue = this.showValue.bind(this);
27
29
  }
28
30
  showValue(value) {
29
- if ((value === null || value === undefined) && this.component.type !== 'ViewOnlyBoolean')
31
+ if ((value === null || value === undefined) && this.component.type !== 'ViewOnlyBoolean') {
30
32
  return React.createElement("span", null, "\u00A0");
33
+ }
31
34
  switch (this.component.type) {
32
35
  case 'ViewOnlyObject':
33
36
  if (this.component.defaultPages && this.component.property.objectId) {
@@ -76,13 +79,36 @@ export class ViewOnlyComponent extends ReactComponent {
76
79
  if (!root) {
77
80
  root = element;
78
81
  }
82
+ const value = this.component.instance
83
+ ? get(this.component.instance, this.component.key)
84
+ : this.component.defaultValue;
79
85
  /* TODO: You'll see warnings to upgrade to React 18's createRoot();
80
86
  * It'll cause issues with: field-level errors not showing up, conditional visibility not working, focus moving out of the form on keypress
81
87
  * Will need to be revisited later. Possibly look into using this.ref */
82
88
  return ReactDOM.render(React.createElement("div", null,
83
- React.createElement(FormComponentWrapper, { ...this.component, viewOnly: true },
84
- React.createElement(Typography, { variant: "body1", key: this.component.key }, this.showValue(this.component.instance
85
- ? get(this.component.instance, this.component.key)
86
- : this.component.defaultValue)))), root);
89
+ React.createElement(FormComponentWrapper, { ...this.component, viewOnly: true }, this.component.property?.protection ? (React.createElement(ProtectedValue, { property: this.component.property, value: value, instance: this.component.instance, apiServices: this.component.apiServices })) : (React.createElement(Typography, { variant: "body1", key: this.component.key }, this.showValue(value))))), root);
87
90
  }
88
91
  }
92
+ const ProtectedValue = (props) => {
93
+ const { property, value, instance, apiServices } = props;
94
+ const [currentDisplayValue, setCurrentDisplayValue] = useState(value);
95
+ const [protectionMode, setProtectionMode] = useState('mask');
96
+ const formatValue = (val) => {
97
+ if (typeof val !== 'string') {
98
+ return val;
99
+ }
100
+ else if (property.type === 'date' && protectionMode !== 'mask') {
101
+ return DateTime.fromISO(val).toFormat('MM/dd/yyyy');
102
+ }
103
+ else if (property.type === 'date-time') {
104
+ return DateTime.fromISO(val).toFormat('MM/dd/yyyy hh:mm a');
105
+ }
106
+ else if (property.type === 'time') {
107
+ return DateTime.fromISO(DateTime.now().toISODate() + 'T' + val).toFormat('hh:mm a');
108
+ }
109
+ return val;
110
+ };
111
+ return (React.createElement(Box, { sx: { display: 'flex', alignItems: 'center', gap: 1 } },
112
+ React.createElement(Typography, { variant: "body1", key: property.id }, formatValue(currentDisplayValue)),
113
+ !!currentDisplayValue && (React.createElement(PropertyProtection, { parameter: property, protection: property?.protection, mask: property?.mask, value: currentDisplayValue, canEdit: false, setCurrentDisplayValue: setCurrentDisplayValue, mode: protectionMode, setMode: setProtectionMode, instance: instance, apiServices: apiServices }))));
114
+ };
@@ -1,4 +1,4 @@
1
- import { ApiServices, ObjectInstance, Property, UserAccount, ViewLayoutEntityReference } from '@evoke-platform/context';
1
+ import { ApiServices, ObjectInstance, Property, PropertyProtection, UserAccount, ViewLayoutEntityReference } from '@evoke-platform/context';
2
2
  import { ReactComponent } from '@formio/react';
3
3
  import { AutocompleteOption } from '../../core';
4
4
  export type BaseFormComponentProps = {
@@ -52,6 +52,7 @@ export type BaseFormComponentProps = {
52
52
  apiServices: ApiServices;
53
53
  description?: string;
54
54
  displayOption?: 'radioButton' | 'dropdown' | 'dialogBox' | 'switch' | 'checkbox';
55
+ protection?: PropertyProtection;
55
56
  };
56
57
  export type ObjectPropertyInputProps = {
57
58
  id: string;
@@ -1,4 +1,4 @@
1
- import { PropertyProtection as PropertyProtectionType, SelectOption } from '@evoke-platform/context';
1
+ import { ApiServices, PropertyProtection as PropertyProtectionType, SelectOption } from '@evoke-platform/context';
2
2
  import React, { FocusEventHandler, ReactNode } from 'react';
3
3
  import { ObjectProperty } from '../../../types';
4
4
  import { AutocompleteOption } from '../../core';
@@ -38,6 +38,10 @@ export type FormFieldProps = {
38
38
  isCombobox?: boolean;
39
39
  endAdornment?: ReactNode;
40
40
  protection?: PropertyProtectionType;
41
+ /** @deprecated only used for FormV1 since form context is not available */
42
+ instance?: Record<string, unknown>;
43
+ /** @deprecated only used for FormV1 since form context is not available */
44
+ apiServices?: ApiServices;
41
45
  };
42
46
  declare const FormField: (props: FormFieldProps) => React.JSX.Element;
43
47
  export default FormField;
@@ -18,7 +18,7 @@ const FormField = (props) => {
18
18
  setCurrentDisplayValue(defaultValue);
19
19
  }
20
20
  }, [defaultValue]);
21
- const protectionComponent = isProtectedProperty && !!defaultValue ? (React.createElement(PropertyProtection, { parameter: property, protection: protection, mask: mask, canEdit: !readOnly, value: defaultValue, handleChange: (value) => onChange?.(property.id, value, property), setCurrentDisplayValue: setCurrentDisplayValue, mode: protectionMode, setMode: setProtectionMode })) : null;
21
+ const protectionComponent = isProtectedProperty && !!defaultValue ? (React.createElement(PropertyProtection, { parameter: property, protection: protection, mask: mask, canEdit: !readOnly, value: defaultValue, handleChange: (value) => onChange?.(property.id, value, property), setCurrentDisplayValue: setCurrentDisplayValue, mode: protectionMode, setMode: setProtectionMode, instance: props.instance, apiServices: props.apiServices })) : null;
22
22
  const commonProps = {
23
23
  id: id ?? property.id,
24
24
  property,
@@ -1,4 +1,4 @@
1
- import { PropertyProtection as PropertyProtectionType } from '@evoke-platform/context';
1
+ import { ApiServices, PropertyProtection as PropertyProtectionType } from '@evoke-platform/context';
2
2
  import React from 'react';
3
3
  import { ObjectProperty } from '../../../../types';
4
4
  type PropertyProtectionProps = {
@@ -11,6 +11,10 @@ type PropertyProtectionProps = {
11
11
  setCurrentDisplayValue: (value: unknown) => void;
12
12
  mode: 'mask' | 'full' | 'edit';
13
13
  setMode: (mode: 'mask' | 'full' | 'edit') => void;
14
+ /** @deprecated only used for FormV1 since form context is not available */
15
+ instance?: Record<string, unknown>;
16
+ /** @deprecated only used for FormV1 */
17
+ apiServices?: ApiServices;
14
18
  };
15
19
  declare const PropertyProtection: React.FC<PropertyProtectionProps>;
16
20
  export default PropertyProtection;
@@ -1,25 +1,47 @@
1
1
  import { useApiServices } from '@evoke-platform/context';
2
+ import { isEmpty } from 'lodash';
2
3
  import React, { useEffect, useState } from 'react';
3
4
  import { CheckRounded, ClearRounded, EditRounded, VisibilityOffRounded, VisibilityRounded } from '../../../../icons';
4
5
  import { useFormContext } from '../../../../theme/hooks';
5
6
  import { Divider, IconButton, InputAdornment, Snackbar, Tooltip } from '../../../core';
6
7
  import { getPrefixedUrl } from '../../Form/utils';
7
8
  import { obfuscateValue } from './utils';
9
+ // Global cache to persist data across component unmount/remount cycles (needed for FormV1)
10
+ const globalCache = new Map();
8
11
  const PropertyProtection = (props) => {
9
- const { parameter, mask, protection, canEdit, value, mode, setMode, setCurrentDisplayValue, handleChange } = props;
10
- const apiServices = useApiServices();
12
+ const { parameter, mask, protection, canEdit, value, mode, setMode, setCurrentDisplayValue, handleChange, instance: instanceFormV1, apiServices: apiServicesFormV1, } = props;
13
+ const apiServices = apiServicesFormV1 ?? useApiServices();
11
14
  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`]);
15
+ const isFormV1 = !!apiServicesFormV1;
16
+ const getCachedValue = (key) => {
17
+ if (isFormV1) {
18
+ const cache = globalCache.get(parameter.id);
19
+ if (key === 'hasViewPermission')
20
+ return cache?.hasViewPermission;
21
+ if (key === 'fullValue')
22
+ return cache?.fullValue;
23
+ }
24
+ return fetchedOptions?.[`${parameter.id}-${key}`];
25
+ };
26
+ const updateCache = (key, value) => {
27
+ if (isFormV1) {
28
+ globalCache.set(parameter.id, { ...globalCache.get(parameter.id), [key]: value });
29
+ }
30
+ else if (setFetchedOptions) {
31
+ setFetchedOptions({ [`${parameter.id}-${key}`]: value });
32
+ }
33
+ };
34
+ const [hasViewPermission, setHasViewPermission] = useState(getCachedValue('hasViewPermission'));
35
+ const [fullValue, setFullValue] = useState(getCachedValue('fullValue'));
14
36
  const [isLoading, setIsLoading] = useState(hasViewPermission === undefined);
15
37
  const [error, setError] = useState(null);
16
38
  useEffect(() => {
17
- if (hasViewPermission === undefined && instance) {
39
+ if (hasViewPermission === undefined && (instance || instanceFormV1)) {
18
40
  apiServices
19
- .get(getPrefixedUrl(`/objects/${object?.id}/instances/${instance?.id}/checkAccess?action=readProtected&fieldId=${parameter.id}`))
41
+ .get(getPrefixedUrl(`/objects/${object?.id ?? instanceFormV1?.objectId}/instances/${instance?.id ?? instanceFormV1?.id}/checkAccess?action=readProtected&fieldId=${parameter.id}`))
20
42
  .then((viewPermissionCheck) => {
21
43
  setHasViewPermission(viewPermissionCheck.result);
22
- setFetchedOptions({ [`${parameter.id}-hasViewPermission`]: viewPermissionCheck.result });
44
+ updateCache(`hasViewPermission`, viewPermissionCheck.result);
23
45
  })
24
46
  .catch(() => {
25
47
  setError('Failed to check view permission.');
@@ -32,13 +54,13 @@ const PropertyProtection = (props) => {
32
54
  const canViewFull = hasViewPermission || hasValueChangedOrViewed;
33
55
  const fetchFullValue = async () => {
34
56
  // if instance doesn't exist, cannot fetch full value
35
- if (!instance) {
57
+ if (isEmpty(instance) && isEmpty(instanceFormV1)) {
36
58
  return undefined;
37
59
  }
38
60
  try {
39
- const value = await apiServices.get(getPrefixedUrl(`/objects/${object?.id}/instances/${instance?.id}/properties/${parameter.id}?showProtectedValue=true`));
61
+ const value = await apiServices.get(getPrefixedUrl(`/objects/${object?.id ?? instanceFormV1?.objectId}/instances/${instance?.id ?? instanceFormV1?.id}/properties/${parameter.id}?showProtectedValue=true`));
40
62
  setFullValue(value);
41
- setFetchedOptions({ [`${parameter.id}-fullValue`]: value });
63
+ updateCache('fullValue', value);
42
64
  return value;
43
65
  }
44
66
  catch (error) {
@@ -75,7 +97,7 @@ const PropertyProtection = (props) => {
75
97
  // save current value as full value, so value can be reverted as needed
76
98
  if (hasValueChangedOrViewed) {
77
99
  setFullValue(value);
78
- setFetchedOptions({ [`${parameter.id}-fullValue`]: value });
100
+ updateCache('fullValue', value);
79
101
  }
80
102
  }
81
103
  setMode(newMode);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@evoke-platform/ui-components",
3
- "version": "1.10.1-dev.6",
3
+ "version": "1.11.0-dev.0",
4
4
  "description": "",
5
5
  "main": "dist/published/index.js",
6
6
  "module": "dist/published/index.js",