@evoke-platform/ui-components 1.10.0-testing.8 → 1.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/dist/published/components/core/Autocomplete/Autocomplete.js +4 -2
  2. package/dist/published/components/core/Autocomplete/Autocomplete.test.js +112 -3
  3. package/dist/published/components/core/TextField/TextField.js +1 -1
  4. package/dist/published/components/core/TextField/TextField.test.js +0 -2
  5. package/dist/published/components/custom/CriteriaBuilder/CriteriaBuilder.js +25 -3
  6. package/dist/published/components/custom/CriteriaBuilder/CriteriaBuilder.test.d.ts +1 -0
  7. package/dist/published/components/custom/CriteriaBuilder/CriteriaBuilder.test.js +473 -0
  8. package/dist/published/components/custom/CriteriaBuilder/ValueEditor.js +19 -6
  9. package/dist/published/components/custom/Form/FormComponents/DocumentComponent/Document.js +2 -1
  10. package/dist/published/components/custom/Form/FormComponents/RepeatableFieldComponent/RepeatableField.js +1 -1
  11. package/dist/published/components/custom/Form/tests/Form.test.js +0 -2
  12. package/dist/published/components/custom/FormField/DatePickerSelect/DatePickerSelect.js +36 -7
  13. package/dist/published/components/custom/FormField/DateTimePickerSelect/DateTimePickerSelect.js +14 -1
  14. package/dist/published/components/custom/FormField/FormField.d.ts +3 -1
  15. package/dist/published/components/custom/FormField/FormField.js +17 -5
  16. package/dist/published/components/custom/FormField/InputFieldComponent/InputFieldComponent.js +6 -4
  17. package/dist/published/components/custom/FormField/InputFieldComponent/InputFieldComponent.test.js +0 -2
  18. package/dist/published/components/custom/FormField/Select/Select.test.js +0 -2
  19. package/dist/published/components/custom/FormField/TimePickerSelect/TimePickerSelect.js +14 -1
  20. package/dist/published/components/custom/FormV2/FormRenderer.d.ts +2 -1
  21. package/dist/published/components/custom/FormV2/FormRenderer.js +46 -8
  22. package/dist/published/components/custom/FormV2/FormRendererContainer.js +178 -153
  23. package/dist/published/components/custom/FormV2/components/AccordionSections.js +7 -2
  24. package/dist/published/components/custom/FormV2/components/Body.d.ts +1 -1
  25. package/dist/published/components/custom/FormV2/components/DefaultValues.d.ts +2 -2
  26. package/dist/published/components/custom/FormV2/components/DefaultValues.js +36 -28
  27. package/dist/published/components/custom/FormV2/components/FieldWrapper.js +1 -1
  28. package/dist/published/components/custom/FormV2/components/Footer.d.ts +1 -0
  29. package/dist/published/components/custom/FormV2/components/Footer.js +8 -5
  30. package/dist/published/components/custom/FormV2/components/FormContext.d.ts +3 -2
  31. package/dist/published/components/custom/FormV2/components/FormFieldTypes/AddressFields.d.ts +9 -0
  32. package/dist/published/components/custom/FormV2/components/FormFieldTypes/AddressFields.js +32 -15
  33. package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/ActionDialog.js +2 -2
  34. package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/RepeatableField.js +6 -23
  35. package/dist/published/components/custom/FormV2/components/FormFieldTypes/Criteria.js +16 -3
  36. package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/Document.js +22 -4
  37. package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/DocumentList.d.ts +2 -1
  38. package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/DocumentList.js +16 -3
  39. package/dist/published/components/custom/FormV2/components/FormFieldTypes/Image.js +31 -5
  40. package/dist/published/components/custom/FormV2/components/FormFieldTypes/UserProperty.js +15 -3
  41. package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/ObjectPropertyInput.js +115 -87
  42. package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/RelatedObjectInstance.d.ts +2 -3
  43. package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/RelatedObjectInstance.js +43 -20
  44. package/dist/published/components/custom/FormV2/components/Header.d.ts +5 -3
  45. package/dist/published/components/custom/FormV2/components/Header.js +47 -9
  46. package/dist/published/components/custom/FormV2/components/PropertyProtection.d.ts +16 -0
  47. package/dist/published/components/custom/FormV2/components/PropertyProtection.js +113 -0
  48. package/dist/published/components/custom/FormV2/components/RecursiveEntryRenderer.js +47 -24
  49. package/dist/published/components/custom/FormV2/components/ValidationFiles/ValidationErrors.js +1 -1
  50. package/dist/published/components/custom/FormV2/components/types.d.ts +2 -0
  51. package/dist/published/components/custom/FormV2/components/utils.d.ts +6 -4
  52. package/dist/published/components/custom/FormV2/components/utils.js +83 -13
  53. package/dist/published/components/custom/FormV2/tests/FormRenderer.test.js +413 -46
  54. package/dist/published/components/custom/FormV2/tests/FormRendererContainer.test.js +983 -16
  55. package/dist/published/components/custom/FormV2/tests/test-data.d.ts +1 -0
  56. package/dist/published/components/custom/FormV2/tests/test-data.js +138 -0
  57. package/dist/published/components/custom/ViewDetailsV2/InstanceEntryRenderer.d.ts +3 -0
  58. package/dist/published/components/custom/ViewDetailsV2/InstanceEntryRenderer.js +165 -0
  59. package/dist/published/components/custom/ViewDetailsV2/ViewDetailsV2Container.d.ts +13 -0
  60. package/dist/published/components/custom/ViewDetailsV2/ViewDetailsV2Container.js +144 -0
  61. package/dist/published/components/custom/ViewDetailsV2/index.d.ts +3 -0
  62. package/dist/published/components/custom/ViewDetailsV2/index.js +2 -0
  63. package/dist/published/components/custom/index.d.ts +2 -0
  64. package/dist/published/components/custom/index.js +1 -0
  65. package/dist/published/index.d.ts +6 -6
  66. package/dist/published/index.js +1 -1
  67. package/dist/published/stories/CriteriaBuilder.stories.js +6 -0
  68. package/dist/published/stories/FormRenderer.stories.d.ts +8 -4
  69. package/dist/published/stories/FormRendererContainer.stories.d.ts +26 -0
  70. package/dist/published/stories/FormRendererContainer.stories.js +5 -0
  71. package/dist/published/stories/FormRendererData.d.ts +12 -0
  72. package/dist/published/stories/FormRendererData.js +26 -1
  73. package/dist/published/stories/ViewDetailsV2Container.stories.d.ts +26 -0
  74. package/dist/published/stories/ViewDetailsV2Container.stories.js +37 -0
  75. package/dist/published/stories/ViewDetailsV2Data.d.ts +4 -0
  76. package/dist/published/stories/ViewDetailsV2Data.js +203 -0
  77. package/dist/published/stories/sharedMswHandlers.js +49 -10
  78. package/dist/published/theme/hooks.d.ts +4 -3
  79. package/dist/published/types.d.ts +3 -0
  80. package/package.json +12 -8
@@ -1,6 +1,7 @@
1
1
  import { LocalDate, LocalDateTime, LocalTime, nativeJs } from '@js-joda/core';
2
2
  import { omit } from 'lodash';
3
3
  import React, { useEffect, useState } from 'react';
4
+ import { useFormContext } from '../../../../theme/hooks';
4
5
  import { InvalidDate } from '../../../../util';
5
6
  import { DateTimePicker, LocalizationProvider, TextField } from '../../../core';
6
7
  import InputFieldComponent from '../InputFieldComponent/InputFieldComponent';
@@ -30,6 +31,7 @@ const formatDateTime = (date) => {
30
31
  const DateTimePickerSelect = (props) => {
31
32
  const { id, property, defaultValue, error, errorMessage, readOnly, required, size, onBlur, additionalProps } = props;
32
33
  const [value, setValue] = useState(asCalendarDate(defaultValue));
34
+ const { onAutosave } = useFormContext();
33
35
  useEffect(() => {
34
36
  setValue(asCalendarDate(defaultValue));
35
37
  }, [defaultValue]);
@@ -43,8 +45,19 @@ const DateTimePickerSelect = (props) => {
43
45
  setValue(date);
44
46
  props.onChange && props.onChange(property.id, date, property);
45
47
  };
48
+ const handleAccept = async () => {
49
+ // Trigger autosave when date/time is accepted (picker closes after selection)
50
+ if (onAutosave) {
51
+ try {
52
+ await onAutosave(id);
53
+ }
54
+ catch (error) {
55
+ console.error('Autosave failed:', error);
56
+ }
57
+ }
58
+ };
46
59
  return readOnly ? (React.createElement(InputFieldComponent, { ...{ ...props, defaultValue: formatDateTime(value) } })) : (React.createElement(LocalizationProvider, null,
47
- React.createElement(DateTimePicker, { value: value, onChange: handleChange, renderInput: (params) => (React.createElement(TextField, { ...params, id: id, error: error, errorMessage: errorMessage, onBlur: onBlur, fullWidth: true, required: required, sx: { background: 'white', borderRadius: '8px' }, size: size ?? 'medium',
60
+ React.createElement(DateTimePicker, { value: value, onChange: handleChange, onAccept: handleAccept, renderInput: (params) => (React.createElement(TextField, { ...params, id: id, error: error, errorMessage: errorMessage, onBlur: onBlur, fullWidth: true, required: required, sx: { background: 'white', borderRadius: '8px' }, size: size ?? 'medium',
48
61
  // merges MUI inputProps with additionalProps.inputProps in a way that still shows the value
49
62
  inputProps: {
50
63
  ...params.inputProps,
@@ -1,4 +1,4 @@
1
- import { SelectOption } from '@evoke-platform/context';
1
+ import { 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';
@@ -36,6 +36,8 @@ export type FormFieldProps = {
36
36
  description?: string;
37
37
  tooltip?: string;
38
38
  isCombobox?: boolean;
39
+ endAdornment?: ReactNode;
40
+ protection?: PropertyProtectionType;
39
41
  };
40
42
  declare const FormField: (props: FormFieldProps) => React.JSX.Element;
41
43
  export default FormField;
@@ -1,4 +1,5 @@
1
- import React from 'react';
1
+ import React, { useEffect, useState } from 'react';
2
+ import PropertyProtection from '../FormV2/components/PropertyProtection';
2
3
  import AddressFieldComponent from './AddressFieldComponent/addressFieldComponent';
3
4
  import BooleanSelect from './BooleanSelect/BooleanSelect';
4
5
  import DatePickerSelect from './DatePickerSelect/DatePickerSelect';
@@ -8,8 +9,16 @@ import InputFieldComponent from './InputFieldComponent/InputFieldComponent';
8
9
  import Select from './Select/Select';
9
10
  import TimePickerSelect from './TimePickerSelect/TimePickerSelect';
10
11
  const FormField = (props) => {
11
- const { id, defaultValue, error, onChange, property, readOnly, selectOptions, required, strictlyTrue, size, placeholder, errorMessage, onBlur, mask, max, min, isMultiLineText, rows, inputMaskPlaceholderChar, queryAddresses, isOptionEqualToValue, renderOption, disableCloseOnSelect, getOptionLabel, additionalProps, displayOption, sortBy, label, description, tooltip, isCombobox, } = props;
12
- let control;
12
+ const { id, defaultValue, error, onChange, property, readOnly, selectOptions, required, strictlyTrue, size, placeholder, errorMessage, onBlur, mask, max, min, isMultiLineText, rows, inputMaskPlaceholderChar, queryAddresses, isOptionEqualToValue, renderOption, disableCloseOnSelect, getOptionLabel, additionalProps, displayOption, sortBy, label, description, tooltip, isCombobox, protection, } = props;
13
+ const [currentDisplayValue, setCurrentDisplayValue] = useState(defaultValue);
14
+ const isProtectedProperty = !!protection?.maskChar;
15
+ const [protectionMode, setProtectionMode] = useState(isProtectedProperty ? (!currentDisplayValue ? 'edit' : 'mask') : 'full');
16
+ useEffect(() => {
17
+ if (isProtectedProperty && protectionMode === 'edit') {
18
+ setCurrentDisplayValue(defaultValue);
19
+ }
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;
13
22
  const commonProps = {
14
23
  id: id ?? property.id,
15
24
  property,
@@ -17,8 +26,8 @@ const FormField = (props) => {
17
26
  onBlur,
18
27
  error,
19
28
  errorMessage,
20
- readOnly,
21
- defaultValue,
29
+ readOnly: readOnly || (!!isProtectedProperty && protectionMode !== 'edit'),
30
+ defaultValue: isProtectedProperty ? currentDisplayValue : defaultValue,
22
31
  selectOptions,
23
32
  required,
24
33
  strictlyTrue,
@@ -37,7 +46,10 @@ const FormField = (props) => {
37
46
  description,
38
47
  tooltip,
39
48
  isCombobox,
49
+ endAdornment: protectionComponent,
50
+ protection,
40
51
  };
52
+ let control;
41
53
  if (queryAddresses) {
42
54
  control = (React.createElement(AddressFieldComponent, { ...commonProps, mask: mask, inputMaskPlaceholderChar: inputMaskPlaceholderChar, isMultiLineText: isMultiLineText, rows: rows, queryAddresses: queryAddresses }));
43
55
  return control;
@@ -3,6 +3,7 @@ import React, { useEffect, useState } from 'react';
3
3
  import InputMask from 'react-input-mask';
4
4
  import NumberFormat from 'react-number-format';
5
5
  import { Autocomplete, TextField } from '../../../core';
6
+ import { obfuscateValue } from '../../FormV2/components/utils';
6
7
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
7
8
  export const NumericFormat = (props) => {
8
9
  const { inputRef, onChange, defaultValue, ...other } = props;
@@ -15,7 +16,7 @@ export const NumericFormat = (props) => {
15
16
  }, isNumericString: true, fixedDecimalScale: true, allowNegative: true }));
16
17
  };
17
18
  const InputFieldComponent = (props) => {
18
- const { id, property, defaultValue, error, errorMessage, onBlur, readOnly, required, size, placeholder, mask, min, max, isMultiLineText, rows, inputMaskPlaceholderChar, additionalProps, } = props;
19
+ const { id, property, defaultValue, error, errorMessage, onBlur, readOnly, required, size, placeholder, mask, min, max, isMultiLineText, rows, inputMaskPlaceholderChar, additionalProps, endAdornment, protection, } = props;
19
20
  const [value, setValue] = useState(defaultValue ?? '');
20
21
  const [inputValue, setInputValue] = useState('');
21
22
  useEffect(() => {
@@ -43,6 +44,7 @@ const InputFieldComponent = (props) => {
43
44
  : property.type === 'integer'
44
45
  ? { inputProps: { min, max, ...(additionalProps?.inputProps ?? {}) } }
45
46
  : null;
47
+ const isValueProtected = protection?.maskChar && defaultValue === obfuscateValue(defaultValue, { protection, mask });
46
48
  return property.enum && !readOnly ? (React.createElement(Autocomplete, { id: id,
47
49
  // note: this is different between widgets and builder
48
50
  // builder had select options being {label, value}
@@ -53,7 +55,7 @@ const InputFieldComponent = (props) => {
53
55
  ? [...property.enum, defaultValue]
54
56
  : property.enum, onChange: handleSelectChange, renderInput: (params) => (React.createElement(TextField, { ...params, value: value, error: error, errorMessage: errorMessage, fullWidth: true, onBlur: onBlur, size: size ?? 'medium', placeholder: placeholder })), disableClearable: true, value: value, isOptionEqualToValue: (option, value) => {
55
57
  return option.value === value;
56
- }, error: error, required: required, inputValue: inputValue ?? '', onInputChange: handleInputValueChange, ...(additionalProps ?? {}) })) : !mask ? (React.createElement(TextField, { id: id, sx: {
58
+ }, error: error, required: required, inputValue: inputValue ?? '', onInputChange: handleInputValueChange, ...(additionalProps ?? {}) })) : !mask || isValueProtected ? (React.createElement(TextField, { id: id, sx: {
57
59
  background: 'white',
58
60
  borderRadius: '8px',
59
61
  ...(readOnly && {
@@ -65,7 +67,7 @@ const InputFieldComponent = (props) => {
65
67
  backgroundColor: '#f4f6f8',
66
68
  },
67
69
  }),
68
- }, error: error, errorMessage: errorMessage, value: value, onChange: !readOnly ? handleChange : undefined, InputProps: { ...InputProps, readOnly: readOnly }, required: required, fullWidth: true, onBlur: onBlur, placeholder: readOnly ? undefined : placeholder, size: size ?? 'medium', type: property.type === 'integer' ? 'number' : 'text', multiline: property.type === 'string' && !readOnly && isMultiLineText, rows: isMultiLineText ? (rows ? rows : 3) : undefined, ...(additionalProps ?? {}) })) : (React.createElement(InputMask, { mask: mask, maskChar: inputMaskPlaceholderChar ?? '_', value: value, onChange: !readOnly ? handleChange : undefined, onBlur: onBlur, alwaysShowMask: true }, (() => (React.createElement(TextField, { id: id, sx: readOnly
70
+ }, error: error, errorMessage: errorMessage, value: value, onChange: !readOnly ? handleChange : undefined, InputProps: { ...InputProps, endAdornment, readOnly: readOnly }, required: required, fullWidth: true, onBlur: onBlur, placeholder: readOnly ? undefined : placeholder, size: size ?? 'medium', type: property.type === 'integer' ? 'number' : 'text', multiline: property.type === 'string' && !readOnly && isMultiLineText, rows: isMultiLineText ? (rows ? rows : 3) : undefined, ...(additionalProps ?? {}) })) : (React.createElement(InputMask, { mask: mask, maskChar: inputMaskPlaceholderChar ?? '_', value: value, onChange: !readOnly ? handleChange : undefined, onBlur: onBlur, alwaysShowMask: true }, (() => (React.createElement(TextField, { id: id, sx: readOnly
69
71
  ? {
70
72
  '& .MuiOutlinedInput-notchedOutline': {
71
73
  border: 'none',
@@ -75,7 +77,7 @@ const InputFieldComponent = (props) => {
75
77
  backgroundColor: '#f4f6f8',
76
78
  },
77
79
  }
78
- : undefined, required: required, error: error, errorMessage: errorMessage, InputProps: { ...InputProps, readOnly: readOnly }, fullWidth: true, size: size ?? 'medium', type: property.type === 'integer' ? 'number' : 'text', multiline: property.type === 'string' && !readOnly && isMultiLineText, rows: isMultiLineText ? (rows ? rows : 3) : undefined, ...(additionalProps ?? {}) })
80
+ : undefined, required: required, error: error, errorMessage: errorMessage, InputProps: { ...InputProps, endAdornment, readOnly: readOnly }, fullWidth: true, size: size ?? 'medium', type: property.type === 'integer' ? 'number' : 'text', multiline: property.type === 'string' && !readOnly && isMultiLineText, rows: isMultiLineText ? (rows ? rows : 3) : undefined, ...(additionalProps ?? {}) })
79
81
  // Casting to `React.ReactNode` is necessary to resolve TypeScript errors
80
82
  // due to compatibility issues with the outdated `react-input-mask` version
81
83
  // and the newer `@types/react` package.
@@ -1,10 +1,8 @@
1
- import * as matchers from '@testing-library/jest-dom/matchers';
2
1
  import { render, screen } from '@testing-library/react';
3
2
  import { userEvent } from '@testing-library/user-event';
4
3
  import React from 'react';
5
4
  import { describe, expect, it, vi } from 'vitest';
6
5
  import InputField from './InputFieldComponent';
7
- expect.extend(matchers);
8
6
  describe('Free-text input', () => {
9
7
  // Right now an object property is required for this to function, but eventually this should go
10
8
  // away.
@@ -1,10 +1,8 @@
1
- import * as matchers from '@testing-library/jest-dom/matchers';
2
1
  import { render, screen } from '@testing-library/react';
3
2
  import { userEvent } from '@testing-library/user-event';
4
3
  import React from 'react';
5
4
  import { describe, expect, it, vi } from 'vitest';
6
5
  import Select from './Select';
7
- expect.extend(matchers);
8
6
  describe('Single select', () => {
9
7
  // Right now an object property is required for this to function, but eventually this should go
10
8
  // away.
@@ -3,11 +3,13 @@ import { TimePicker } from '@mui/x-date-pickers';
3
3
  import { isUndefined, omit, padStart } from 'lodash';
4
4
  import { DateTime } from 'luxon';
5
5
  import React, { useEffect, useState } from 'react';
6
+ import { useFormContext } from '../../../../theme/hooks';
6
7
  import { InvalidDate } from '../../../../util';
7
8
  import { LocalizationProvider, TextField } from '../../../core';
8
9
  import InputFieldComponent from '../InputFieldComponent/InputFieldComponent';
9
10
  const TimePickerSelect = (props) => {
10
11
  const { id, property, defaultValue, error, errorMessage, readOnly, required, size, onBlur, placeholder, additionalProps, } = props;
12
+ const { onAutosave } = useFormContext();
11
13
  const values = defaultValue ? defaultValue.split(':') : undefined;
12
14
  const hour = values ? parseInt(values[0]) : undefined;
13
15
  const minute = values ? parseInt(values[1]) : undefined;
@@ -41,11 +43,22 @@ const TimePickerSelect = (props) => {
41
43
  props.onChange && props.onChange(property.id, date, property);
42
44
  }
43
45
  };
46
+ const handleAccept = async () => {
47
+ // Trigger autosave when time is accepted (picker closes after selection)
48
+ if (onAutosave) {
49
+ try {
50
+ await onAutosave(id);
51
+ }
52
+ catch (error) {
53
+ console.error('Autosave failed:', error);
54
+ }
55
+ }
56
+ };
44
57
  return readOnly ? (React.createElement(InputFieldComponent, { ...{
45
58
  ...props,
46
59
  defaultValue: value instanceof LocalDateTime ? DateTime.fromISO(value.toString()).toFormat('hh:mm a') : '',
47
60
  } })) : (React.createElement(LocalizationProvider, null,
48
- React.createElement(TimePicker, { value: value, onChange: handleChange, renderInput: (params) => (React.createElement(TextField, { ...params, id: id, error: error, errorMessage: errorMessage, onBlur: onBlur, fullWidth: true, required: required, sx: { background: 'white', borderRadius: '8px' }, size: size ?? 'medium', placeholder: placeholder,
61
+ React.createElement(TimePicker, { value: value, onChange: handleChange, onAccept: handleAccept, renderInput: (params) => (React.createElement(TextField, { ...params, id: id, error: error, errorMessage: errorMessage, onBlur: onBlur, fullWidth: true, required: required, sx: { background: 'white', borderRadius: '8px' }, size: size ?? 'medium', placeholder: placeholder,
49
62
  // merges MUI inputProps with additionalProps.inputProps in a way that still shows the value
50
63
  inputProps: {
51
64
  ...params.inputProps,
@@ -16,7 +16,8 @@ export type FormRendererProps = BaseProps & {
16
16
  form: EvokeForm;
17
17
  title?: string | React.ReactNode;
18
18
  instance?: ObjectInstance | Document;
19
- onChange: (id: string, value: unknown) => void;
19
+ onChange: (id: string, value: unknown) => void | Promise<void>;
20
+ onAutosave?: (fieldId: string) => void | Promise<void>;
20
21
  associatedObject?: {
21
22
  instanceId?: string;
22
23
  propertyId?: string;
@@ -1,6 +1,6 @@
1
1
  import { useObject } from '@evoke-platform/context';
2
2
  import { isEmpty, isEqual, omit } from 'lodash';
3
- import React, { useEffect, useMemo, useState } from 'react';
3
+ import React, { useEffect, useMemo, useRef, useState } from 'react';
4
4
  import { useForm } from 'react-hook-form';
5
5
  import { useWidgetSize } from '../../../theme';
6
6
  import { Box } from '../../layout';
@@ -8,11 +8,11 @@ import { Body } from './components/Body';
8
8
  import { Footer, FooterActions } from './components/Footer';
9
9
  import { FormContext } from './components/FormContext';
10
10
  import Header, { AccordionActions, Title } from './components/Header';
11
- import { assignIdsToSectionsAndRichText, convertDocToParameters, convertPropertiesToParams, entryIsVisible, getEntryId, getNestedParameterIds, isAddressProperty, } from './components/utils';
11
+ import { assignIdsToSectionsAndRichText, convertDocToParameters, convertPropertiesToParams, entryIsVisible, getEntryId, getNestedParameterIds, isAddressProperty, obfuscateValue, } from './components/utils';
12
12
  import { handleValidation } from './components/ValidationFiles/Validation';
13
13
  import ValidationErrors from './components/ValidationFiles/ValidationErrors';
14
14
  const FormRendererInternal = (props) => {
15
- const { onSubmit, onDiscardChanges, onSubmitError, value, fieldHeight, richTextEditor, form, instance, onChange, associatedObject, renderHeader, renderBody, renderFooter, } = props;
15
+ const { onSubmit, onDiscardChanges, onSubmitError: onSubmitErrorOverride, value, fieldHeight, richTextEditor, form, instance, onChange, onAutosave, associatedObject, renderHeader, renderBody, renderFooter, } = props;
16
16
  const { entries, name: title, objectId, actionId, display } = form;
17
17
  const { register, unregister, setValue, reset, handleSubmit, formState: { errors, isSubmitted }, getValues, } = useForm({
18
18
  defaultValues: value,
@@ -32,6 +32,7 @@ const FormRendererInternal = (props) => {
32
32
  const [isInitializing, setIsInitializing] = useState(true);
33
33
  const [parameters, setParameters] = useState();
34
34
  const objectStore = useObject(objectId);
35
+ const validationContainerRef = useRef(null);
35
36
  const updateFetchedOptions = (newData) => {
36
37
  setFetchedOptions((prev) => ({
37
38
  ...prev,
@@ -45,7 +46,7 @@ const FormRendererInternal = (props) => {
45
46
  setExpandAll(false);
46
47
  }
47
48
  const updatedEntries = useMemo(() => {
48
- return assignIdsToSectionsAndRichText(entries, object, parameters);
49
+ return object ? assignIdsToSectionsAndRichText(entries, object, parameters) : [];
49
50
  }, [entries, object, parameters]);
50
51
  useEffect(() => {
51
52
  (async () => {
@@ -78,6 +79,15 @@ const FormRendererInternal = (props) => {
78
79
  if (value) {
79
80
  for (const key of Object.keys(currentValues)) {
80
81
  if (!isEqual(currentValues[key], value[key])) {
82
+ // For protected properties, don't validate initial obfuscated value
83
+ const property = object?.properties?.find((prop) => prop.id === key);
84
+ const isProtectedProperty = property?.protection?.maskChar;
85
+ if (isProtectedProperty) {
86
+ if (value[key] === obfuscateValue(value[key], property)) {
87
+ setValue(key, value[key], { shouldValidate: false });
88
+ continue;
89
+ }
90
+ }
81
91
  setValue(key, value[key], { shouldValidate: true });
82
92
  }
83
93
  }
@@ -108,7 +118,7 @@ const FormRendererInternal = (props) => {
108
118
  }
109
119
  });
110
120
  }
111
- if (!entryIsVisible(entry, getValues(), instance)) {
121
+ if (!entryIsVisible(entry, instance, getValues())) {
112
122
  if (entry.type === 'sections' || entry.type === 'columns') {
113
123
  const fieldsToUnregister = getNestedParameterIds(entry);
114
124
  fieldsToUnregister.forEach(processFieldUnregister);
@@ -121,6 +131,22 @@ const FormRendererInternal = (props) => {
121
131
  }
122
132
  });
123
133
  };
134
+ const removeUneditedProtectedValues = () => {
135
+ const protectedProperties = object?.properties?.filter((prop) => prop.protection?.maskChar);
136
+ if (!protectedProperties || protectedProperties.length === 0) {
137
+ return;
138
+ }
139
+ protectedProperties.forEach((property) => {
140
+ const fieldId = property.id;
141
+ const originalValue = instance?.[fieldId];
142
+ const value = getValues(fieldId);
143
+ // When protected value hasn't been edited or viewed, unregister to
144
+ // avoid saving the obfuscated value.
145
+ if (value === originalValue) {
146
+ processFieldUnregister(fieldId);
147
+ }
148
+ });
149
+ };
124
150
  const processFieldUnregister = (fieldId) => {
125
151
  if (isAddressProperty(fieldId)) {
126
152
  // Unregister entire addressObject to clear hidden field errors, then restore existing values since unregistering address.line1 etc is not working
@@ -134,9 +160,18 @@ const FormRendererInternal = (props) => {
134
160
  unregister(fieldId);
135
161
  }
136
162
  };
163
+ const onSubmitError = (errors) => {
164
+ if (onSubmitErrorOverride) {
165
+ onSubmitErrorOverride(errors);
166
+ }
167
+ else if (validationContainerRef.current) {
168
+ validationContainerRef.current.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
169
+ }
170
+ };
137
171
  async function unregisterHiddenFieldsAndSubmit() {
138
172
  unregisterHiddenFields(entries ?? []);
139
- await handleSubmit((data) => onSubmit && onSubmit(action?.type === 'delete' ? {} : data), (errors) => onSubmitError?.(errors))();
173
+ removeUneditedProtectedValues();
174
+ await handleSubmit((data) => onSubmit && onSubmit(action?.type === 'delete' ? {} : data), (errors) => onSubmitError(errors))();
140
175
  }
141
176
  const headerProps = {
142
177
  title,
@@ -146,8 +181,9 @@ const FormRendererInternal = (props) => {
146
181
  errors,
147
182
  hasAccordions: hasSections && isSmallerThanMd,
148
183
  shouldShowValidationErrors: isSubmitted,
149
- form,
150
184
  action,
185
+ validationContainerRef: validationContainerRef,
186
+ autosaveEnabled: !!form.autosaveActionId,
151
187
  };
152
188
  const footerProps = {
153
189
  onSubmit: unregisterHiddenFieldsAndSubmit,
@@ -155,6 +191,7 @@ const FormRendererInternal = (props) => {
155
191
  action,
156
192
  discardChangesButtonLabel: 'Discard Changes',
157
193
  submitButtonLabel: display?.submitLabel ?? 'Submit',
194
+ disableDiscardChanges: !!form?.autosaveActionId,
158
195
  };
159
196
  return (React.createElement(Box, { ref: containerRef },
160
197
  React.createElement(FormContext.Provider, { value: {
@@ -172,6 +209,7 @@ const FormRendererInternal = (props) => {
172
209
  parameters,
173
210
  fieldHeight,
174
211
  handleChange: onChange,
212
+ onAutosave,
175
213
  triggerFieldReset,
176
214
  showSubmitError: isSubmitted,
177
215
  associatedObject,
@@ -200,7 +238,7 @@ const FormRendererInternal = (props) => {
200
238
  expandedSections,
201
239
  hasAccordions: hasSections && isSmallerThanMd,
202
240
  } })),
203
- (actionId || form.id === 'documentForm') &&
241
+ (action || form.id === 'documentForm') &&
204
242
  onSubmit &&
205
243
  (renderFooter ? renderFooter(footerProps) : React.createElement(Footer, { ...footerProps }))))));
206
244
  };