@evoke-platform/ui-components 1.10.0-dev.34 → 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/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/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 +2 -1
- package/dist/published/components/custom/FormV2/components/utils.d.ts +2 -0
- package/dist/published/components/custom/FormV2/components/utils.js +62 -0
- package/dist/published/components/custom/ViewDetailsV2/InstanceEntryRenderer.js +13 -3
- package/dist/published/types.d.ts +3 -0
- package/package.json +1 -1
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { DateTimeFormatter } from '@js-joda/core';
|
|
2
2
|
import { omit } from 'lodash';
|
|
3
|
-
import React, { useEffect, useState } from 'react';
|
|
3
|
+
import React, { useCallback, useEffect, useState } from 'react';
|
|
4
4
|
import { useFormContext } from '../../../../theme/hooks';
|
|
5
5
|
import { InvalidDate, LocalDate, nativeJs } from '../../../../util';
|
|
6
6
|
import { DatePicker, LocalizationProvider, TextField } from '../../../core';
|
|
7
|
+
import { obfuscateValue } from '../../FormV2/components/utils';
|
|
7
8
|
import InputFieldComponent from '../InputFieldComponent/InputFieldComponent';
|
|
8
9
|
function asCalendarDate(value) {
|
|
9
10
|
if (!value) {
|
|
@@ -28,12 +29,16 @@ const asMonthDayYearFormat = (date) => {
|
|
|
28
29
|
}
|
|
29
30
|
};
|
|
30
31
|
const DatePickerSelect = (props) => {
|
|
31
|
-
const { id, property, defaultValue, error, errorMessage, readOnly, required, size, onBlur, onChange, additionalProps, } = props;
|
|
32
|
-
const [value, setValue] = useState(asCalendarDate(defaultValue));
|
|
32
|
+
const { id, property, defaultValue, error, errorMessage, readOnly, required, size, onBlur, onChange, additionalProps, endAdornment, protection, } = props;
|
|
33
33
|
const { onAutosave } = useFormContext();
|
|
34
|
+
const processValue = useCallback((val) => {
|
|
35
|
+
const isProtected = protection?.maskChar && val === obfuscateValue(val, { protection });
|
|
36
|
+
return isProtected && typeof val === 'string' ? val : asCalendarDate(val);
|
|
37
|
+
}, [protection]);
|
|
38
|
+
const [value, setValue] = useState(() => processValue(defaultValue));
|
|
34
39
|
useEffect(() => {
|
|
35
|
-
setValue(
|
|
36
|
-
}, [defaultValue]);
|
|
40
|
+
setValue(processValue(defaultValue));
|
|
41
|
+
}, [defaultValue, processValue]);
|
|
37
42
|
const handleChange = (date) => {
|
|
38
43
|
setValue(date);
|
|
39
44
|
onChange && onChange(property.id, date, property);
|
|
@@ -49,12 +54,23 @@ const DatePickerSelect = (props) => {
|
|
|
49
54
|
}
|
|
50
55
|
}
|
|
51
56
|
};
|
|
52
|
-
return readOnly ? (React.createElement(InputFieldComponent, { ...{
|
|
57
|
+
return readOnly ? (React.createElement(InputFieldComponent, { ...{
|
|
58
|
+
...props,
|
|
59
|
+
defaultValue: protection?.maskChar && value === obfuscateValue(value, { protection })
|
|
60
|
+
? value
|
|
61
|
+
: asMonthDayYearFormat(value),
|
|
62
|
+
endAdornment,
|
|
63
|
+
} })) : (React.createElement(LocalizationProvider, null,
|
|
53
64
|
React.createElement(DatePicker, { value: value, onChange: handleChange, onAccept: handleAccept, inputFormat: "MM/dd/yyyy", 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',
|
|
54
65
|
// merges MUI inputProps with additionalProps.inputProps in a way that still shows the value
|
|
55
66
|
inputProps: {
|
|
56
67
|
...params.inputProps,
|
|
57
68
|
...(additionalProps?.inputProps ?? {}),
|
|
69
|
+
}, InputProps: {
|
|
70
|
+
...params.InputProps,
|
|
71
|
+
endAdornment: (React.createElement(React.Fragment, null,
|
|
72
|
+
params.InputProps?.endAdornment,
|
|
73
|
+
endAdornment)),
|
|
58
74
|
}, ...omit(additionalProps, ['inputProps']) })) })));
|
|
59
75
|
};
|
|
60
76
|
export default DatePickerSelect;
|
|
@@ -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
|
-
|
|
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;
|
package/dist/published/components/custom/FormField/InputFieldComponent/InputFieldComponent.js
CHANGED
|
@@ -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.
|
|
@@ -122,6 +122,22 @@ const FormRendererInternal = (props) => {
|
|
|
122
122
|
}
|
|
123
123
|
});
|
|
124
124
|
};
|
|
125
|
+
const removeUneditedProtectedValues = () => {
|
|
126
|
+
const protectedProperties = object?.properties?.filter((prop) => prop.protection?.maskChar);
|
|
127
|
+
if (!protectedProperties || protectedProperties.length === 0) {
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
protectedProperties.forEach((property) => {
|
|
131
|
+
const fieldId = property.id;
|
|
132
|
+
const originalValue = instance?.[fieldId];
|
|
133
|
+
const value = getValues(fieldId);
|
|
134
|
+
// When protected value hasn't been edited or viewed, unregister to
|
|
135
|
+
// avoid saving the obfuscated value.
|
|
136
|
+
if (value === originalValue) {
|
|
137
|
+
processFieldUnregister(fieldId);
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
};
|
|
125
141
|
const processFieldUnregister = (fieldId) => {
|
|
126
142
|
if (isAddressProperty(fieldId)) {
|
|
127
143
|
// Unregister entire addressObject to clear hidden field errors, then restore existing values since unregistering address.line1 etc is not working
|
|
@@ -145,6 +161,7 @@ const FormRendererInternal = (props) => {
|
|
|
145
161
|
};
|
|
146
162
|
async function unregisterHiddenFieldsAndSubmit() {
|
|
147
163
|
unregisterHiddenFields(entries ?? []);
|
|
164
|
+
removeUneditedProtectedValues();
|
|
148
165
|
await handleSubmit((data) => onSubmit && onSubmit(action?.type === 'delete' ? {} : data), (errors) => onSubmitError(errors))();
|
|
149
166
|
}
|
|
150
167
|
const headerProps = {
|
|
@@ -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;
|
|
@@ -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;
|
|
@@ -833,3 +833,65 @@ export function getFieldDefinition(entry, object, parameters, isDocument) {
|
|
|
833
833
|
}
|
|
834
834
|
return def;
|
|
835
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
|
+
}
|
|
@@ -15,6 +15,7 @@ import RepeatableField from '../FormV2/components/FormFieldTypes/CollectionFiles
|
|
|
15
15
|
import Criteria from '../FormV2/components/FormFieldTypes/Criteria';
|
|
16
16
|
import { Document } from '../FormV2/components/FormFieldTypes/DocumentFiles/Document';
|
|
17
17
|
import { Image } from '../FormV2/components/FormFieldTypes/Image';
|
|
18
|
+
import PropertyProtection from '../FormV2/components/PropertyProtection';
|
|
18
19
|
import { entryIsVisible, fetchCollectionData, filterEmptySections, getDefaultPages, isAddressProperty, } from '../FormV2/components/utils';
|
|
19
20
|
function ViewOnlyEntryRenderer(props) {
|
|
20
21
|
const { entry } = props;
|
|
@@ -23,6 +24,8 @@ function ViewOnlyEntryRenderer(props) {
|
|
|
23
24
|
const apiServices = useApiServices();
|
|
24
25
|
const { defaultPages, findDefaultPageSlugFor } = useApp();
|
|
25
26
|
const [navigationSlug, setNavigationSlug] = useState(fetchedOptions[`${entryId}NavigationSlug`]);
|
|
27
|
+
const [currentDisplayValue, setCurrentDisplayValue] = useState(instance?.[entryId]);
|
|
28
|
+
const [protectionMode, setProtectionMode] = useState('mask');
|
|
26
29
|
const initialMiddleObjectInstances = fetchedOptions[`${entryId}InitialMiddleObjectInstances`];
|
|
27
30
|
const middleObject = fetchedOptions[`${entryId}MiddleObject`];
|
|
28
31
|
const display = 'display' in entry ? entry.display : undefined;
|
|
@@ -40,6 +43,8 @@ function ViewOnlyEntryRenderer(props) {
|
|
|
40
43
|
}
|
|
41
44
|
return def;
|
|
42
45
|
}, [entry, object]);
|
|
46
|
+
const isProtectedProperty = fieldDefinition?.protection?.maskChar;
|
|
47
|
+
const protectionComponent = isProtectedProperty ? (React.createElement(PropertyProtection, { parameter: fieldDefinition, protection: fieldDefinition?.protection, mask: fieldDefinition?.mask, value: currentDisplayValue, canEdit: false, setCurrentDisplayValue: setCurrentDisplayValue, mode: protectionMode, setMode: setProtectionMode })) : null;
|
|
43
48
|
useEffect(() => {
|
|
44
49
|
if (fieldDefinition?.type === 'collection' && fieldDefinition?.manyToManyPropertyId && instance) {
|
|
45
50
|
fetchCollectionData(apiServices, fieldDefinition, setFetchedOptions, instance.id, fetchedOptions, initialMiddleObjectInstances);
|
|
@@ -72,7 +77,7 @@ function ViewOnlyEntryRenderer(props) {
|
|
|
72
77
|
return (React.createElement(AddressFields, { entry: entry, viewOnly: true, entryId: entryId, fieldDefinition: fieldDefinition }));
|
|
73
78
|
}
|
|
74
79
|
else {
|
|
75
|
-
let fieldValue = instance?.[entryId] || '';
|
|
80
|
+
let fieldValue = currentDisplayValue ?? (instance?.[entryId] || '');
|
|
76
81
|
switch (fieldDefinition?.type) {
|
|
77
82
|
case 'object':
|
|
78
83
|
if (navigationSlug && fieldDefinition?.objectId) {
|
|
@@ -93,7 +98,10 @@ function ViewOnlyEntryRenderer(props) {
|
|
|
93
98
|
fieldValue = fieldValue && fieldValue.name;
|
|
94
99
|
break;
|
|
95
100
|
case 'date':
|
|
96
|
-
fieldValue =
|
|
101
|
+
fieldValue =
|
|
102
|
+
isProtectedProperty && protectionMode === 'mask'
|
|
103
|
+
? fieldValue
|
|
104
|
+
: fieldValue && DateTime.fromISO(fieldValue).toFormat('MM/dd/yyyy');
|
|
97
105
|
break;
|
|
98
106
|
case 'date-time':
|
|
99
107
|
fieldValue =
|
|
@@ -136,7 +144,9 @@ function ViewOnlyEntryRenderer(props) {
|
|
|
136
144
|
React.createElement(Criteria, { value: fieldValue, fieldDefinition: fieldDefinition, canUpdateProperty: false })));
|
|
137
145
|
}
|
|
138
146
|
else {
|
|
139
|
-
return (React.createElement(FieldWrapper, { inputId: entryId, inputType: fieldDefinition?.type || 'string', label: display?.label || fieldDefinition?.name || 'default', value: fieldValue, required: display?.required || false, prefix: display?.prefix, suffix: display?.suffix, viewOnly: true }, fieldDefinition?.type === 'boolean' && fieldValue ? (React.createElement(CheckCircleRounded, { "aria-label": "Checked", color: "success" })) : fieldDefinition?.type === 'boolean' ? (React.createElement(CancelRounded, { "aria-label": "Unchecked", color: "error" })) : (React.createElement(
|
|
147
|
+
return (React.createElement(FieldWrapper, { inputId: entryId, inputType: fieldDefinition?.type || 'string', label: display?.label || fieldDefinition?.name || 'default', value: fieldValue, required: display?.required || false, prefix: display?.prefix, suffix: display?.suffix, viewOnly: true }, fieldDefinition?.type === 'boolean' && fieldValue ? (React.createElement(CheckCircleRounded, { "aria-label": "Checked", color: "success" })) : fieldDefinition?.type === 'boolean' ? (React.createElement(CancelRounded, { "aria-label": "Unchecked", color: "error" })) : (React.createElement(Box, { sx: { display: 'flex', alignItems: 'center', gap: 1 } },
|
|
148
|
+
React.createElement(Typography, { variant: "body1", key: entryId, sx: { height: '24px' } }, fieldValue),
|
|
149
|
+
fieldDefinition?.protection?.maskChar && protectionComponent))));
|
|
140
150
|
}
|
|
141
151
|
}
|
|
142
152
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { PropertyProtection } from '@evoke-platform/context';
|
|
1
2
|
export type EvokeObject = {
|
|
2
3
|
id: string;
|
|
3
4
|
name: string;
|
|
@@ -13,6 +14,8 @@ export type ObjectProperty = {
|
|
|
13
14
|
searchable?: boolean;
|
|
14
15
|
objectId?: string;
|
|
15
16
|
formula?: string;
|
|
17
|
+
mask?: string;
|
|
18
|
+
protection?: PropertyProtection;
|
|
16
19
|
};
|
|
17
20
|
export type Obj = {
|
|
18
21
|
id: string;
|