@evoke-platform/ui-components 1.10.1-dev.6 → 1.11.0-dev.1
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/Form/Common/Form.js +9 -0
- package/dist/published/components/custom/Form/FormComponents/FormFieldComponent.js +11 -1
- package/dist/published/components/custom/Form/FormComponents/ViewOnlyComponent.js +32 -6
- package/dist/published/components/custom/Form/types.d.ts +2 -1
- package/dist/published/components/custom/FormField/FormField.d.ts +5 -1
- package/dist/published/components/custom/FormField/FormField.js +1 -1
- package/dist/published/components/custom/FormField/Select/Select.js +8 -1
- package/dist/published/components/custom/FormField/Select/Select.test.js +101 -0
- package/dist/published/components/custom/FormV2/components/PropertyProtection.d.ts +5 -1
- package/dist/published/components/custom/FormV2/components/PropertyProtection.js +33 -11
- package/package.json +1 -1
|
@@ -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,
|
|
@@ -118,7 +118,7 @@ const Select = (props) => {
|
|
|
118
118
|
setValue(e.target.value);
|
|
119
119
|
setInputValue(e.target.value);
|
|
120
120
|
onChange && onChange(property.id, e.target.value, property);
|
|
121
|
-
}, size: 'small', onFocus: () => setIsOtherFocused(true), onBlur: (e) => {
|
|
121
|
+
}, size: 'small', onFocus: () => setIsOtherFocused(true), onBlur: async (e) => {
|
|
122
122
|
// If another radio button is focused, change the value to that radio button's value.
|
|
123
123
|
const targetElement = e.nativeEvent.relatedTarget;
|
|
124
124
|
if (targetElement?.defaultValue && targetElement.name === `radioGroup-${id}`) {
|
|
@@ -128,6 +128,13 @@ const Select = (props) => {
|
|
|
128
128
|
onChange && onChange(property.id, e.target.value, property);
|
|
129
129
|
}
|
|
130
130
|
setIsOtherFocused(false);
|
|
131
|
+
// Trigger autosave when the custom value field loses focus
|
|
132
|
+
try {
|
|
133
|
+
await onAutosave?.(id);
|
|
134
|
+
}
|
|
135
|
+
catch (error) {
|
|
136
|
+
console.error('Autosave failed:', error);
|
|
137
|
+
}
|
|
131
138
|
} })))))),
|
|
132
139
|
onChange && !readOnly && value && (React.createElement(Box, { sx: {
|
|
133
140
|
':hover': { cursor: 'pointer' },
|
|
@@ -2,6 +2,7 @@ import { render, screen } from '@testing-library/react';
|
|
|
2
2
|
import { userEvent } from '@testing-library/user-event';
|
|
3
3
|
import React from 'react';
|
|
4
4
|
import { describe, expect, it, vi } from 'vitest';
|
|
5
|
+
import { FormContext } from '../../FormV2/components/FormContext';
|
|
5
6
|
import Select from './Select';
|
|
6
7
|
describe('Single select', () => {
|
|
7
8
|
// Right now an object property is required for this to function, but eventually this should go
|
|
@@ -61,6 +62,27 @@ describe('Single select', () => {
|
|
|
61
62
|
await user.click(customOption);
|
|
62
63
|
expect(onChangeMock).toBeCalledWith('selectOptions', expect.objectContaining({ label: 'Add "custom option"', value: 'custom option' }), choiceProperty);
|
|
63
64
|
});
|
|
65
|
+
it('triggers autosave when entering a custom value in dropdown combobox', async () => {
|
|
66
|
+
const user = userEvent.setup();
|
|
67
|
+
const onChangeMock = vi.fn((name, value, property) => { });
|
|
68
|
+
const onAutosaveMock = vi.fn();
|
|
69
|
+
const options = ['option 1', 'option 2', 'option 3'];
|
|
70
|
+
render(React.createElement(FormContext.Provider, { value: {
|
|
71
|
+
fetchedOptions: {},
|
|
72
|
+
setFetchedOptions: () => { },
|
|
73
|
+
onAutosave: onAutosaveMock,
|
|
74
|
+
width: 1200,
|
|
75
|
+
} },
|
|
76
|
+
React.createElement(Select, { id: "testSelect", property: choiceProperty, selectOptions: options, onChange: onChangeMock, isCombobox: true })));
|
|
77
|
+
const input = screen.getByRole('combobox');
|
|
78
|
+
await user.click(input);
|
|
79
|
+
await user.type(input, 'custom option');
|
|
80
|
+
// Select the custom option from dropdown
|
|
81
|
+
const customOption = await screen.findByRole('option', { name: 'Add "custom option"' });
|
|
82
|
+
await user.click(customOption);
|
|
83
|
+
// Verify autosave was triggered
|
|
84
|
+
expect(onAutosaveMock).toHaveBeenCalledWith('testSelect');
|
|
85
|
+
});
|
|
64
86
|
});
|
|
65
87
|
describe('Multi select', () => {
|
|
66
88
|
// Right now an object property is required for this to function, but eventually this should go
|
|
@@ -122,6 +144,34 @@ describe('Multi select', () => {
|
|
|
122
144
|
expect(onChangeMock).toBeCalledTimes(2);
|
|
123
145
|
expect(onChangeMock).lastCalledWith('multiSelect', ['custom option 1', 'option 1'], multiChoiceProperty);
|
|
124
146
|
});
|
|
147
|
+
it('triggers autosave when entering custom values in multiselect combobox', async () => {
|
|
148
|
+
const user = userEvent.setup();
|
|
149
|
+
const onChangeMock = vi.fn((name, value, property) => { });
|
|
150
|
+
const onAutosaveMock = vi.fn();
|
|
151
|
+
const options = ['option 1', 'option 2', 'option 3'];
|
|
152
|
+
render(React.createElement(FormContext.Provider, { value: {
|
|
153
|
+
fetchedOptions: {},
|
|
154
|
+
setFetchedOptions: () => { },
|
|
155
|
+
onAutosave: onAutosaveMock,
|
|
156
|
+
width: 1200,
|
|
157
|
+
} },
|
|
158
|
+
React.createElement(Select, { id: "multiSelect", property: multiChoiceProperty, selectOptions: options, onChange: onChangeMock, isCombobox: true })));
|
|
159
|
+
const input = screen.getByRole('combobox');
|
|
160
|
+
await user.click(input);
|
|
161
|
+
await user.type(input, 'custom option 1');
|
|
162
|
+
// Select the first custom option
|
|
163
|
+
const customOption1 = await screen.findByRole('option', { name: 'Add "custom option 1"' });
|
|
164
|
+
await user.click(customOption1);
|
|
165
|
+
// Verify autosave was triggered for first custom value
|
|
166
|
+
expect(onAutosaveMock).toHaveBeenCalledWith('multiSelect');
|
|
167
|
+
onAutosaveMock.mockClear();
|
|
168
|
+
// Add a second custom option
|
|
169
|
+
await user.type(input, 'custom option 2');
|
|
170
|
+
const customOption2 = await screen.findByRole('option', { name: 'Add "custom option 2"' });
|
|
171
|
+
await user.click(customOption2);
|
|
172
|
+
// Verify autosave was triggered for second custom value
|
|
173
|
+
expect(onAutosaveMock).toHaveBeenCalledWith('multiSelect');
|
|
174
|
+
});
|
|
125
175
|
});
|
|
126
176
|
describe('Radio Single select', () => {
|
|
127
177
|
const choiceProperty = {
|
|
@@ -166,4 +216,55 @@ describe('Radio Single select', () => {
|
|
|
166
216
|
await user.type(otherTextField, 'custom option');
|
|
167
217
|
expect(onChangeMock).toBeCalledWith('selectOptions', expect.stringContaining('custom option'), choiceProperty);
|
|
168
218
|
});
|
|
219
|
+
it('triggers autosave when a custom value is entered', async () => {
|
|
220
|
+
const user = userEvent.setup();
|
|
221
|
+
const onChangeMock = vi.fn((name, value, property) => { });
|
|
222
|
+
const onAutosaveMock = vi.fn();
|
|
223
|
+
const options = ['option 1', 'option 2', 'option 3'];
|
|
224
|
+
render(React.createElement(FormContext.Provider, { value: {
|
|
225
|
+
fetchedOptions: {},
|
|
226
|
+
setFetchedOptions: () => { },
|
|
227
|
+
onAutosave: onAutosaveMock,
|
|
228
|
+
width: 1200,
|
|
229
|
+
} },
|
|
230
|
+
React.createElement(Select, { id: "testSelect", property: choiceProperty, selectOptions: options, displayOption: 'radioButton', sortBy: 'ASC', onChange: onChangeMock, isCombobox: true })));
|
|
231
|
+
// Select the "Other" radio option
|
|
232
|
+
await user.click(screen.getByRole('radio', { name: 'Other' }));
|
|
233
|
+
const otherTextField = await screen.findByRole('textbox', { name: 'Other' });
|
|
234
|
+
// Type a custom value
|
|
235
|
+
await user.type(otherTextField, 'custom option');
|
|
236
|
+
// Blur the text field by clicking elsewhere
|
|
237
|
+
await user.tab();
|
|
238
|
+
// Verify autosave was triggered
|
|
239
|
+
expect(onAutosaveMock).toHaveBeenCalledWith('testSelect');
|
|
240
|
+
});
|
|
241
|
+
it('triggers autosave when clearing the custom value', async () => {
|
|
242
|
+
const user = userEvent.setup();
|
|
243
|
+
const onChangeMock = vi.fn((name, value, property) => { });
|
|
244
|
+
const onAutosaveMock = vi.fn();
|
|
245
|
+
const options = ['option 1', 'option 2', 'option 3'];
|
|
246
|
+
render(React.createElement(FormContext.Provider, { value: {
|
|
247
|
+
fetchedOptions: {},
|
|
248
|
+
setFetchedOptions: () => { },
|
|
249
|
+
onAutosave: onAutosaveMock,
|
|
250
|
+
width: 1200,
|
|
251
|
+
} },
|
|
252
|
+
React.createElement(Select, { id: "testSelect", property: choiceProperty, selectOptions: options, displayOption: 'radioButton', sortBy: 'ASC', onChange: onChangeMock, isCombobox: true })));
|
|
253
|
+
// Select the "Other" radio option and enter a custom value
|
|
254
|
+
await user.click(await screen.findByRole('radio', { name: 'Other' }));
|
|
255
|
+
const otherTextField = await screen.findByRole('textbox', { name: 'Other' });
|
|
256
|
+
await user.type(otherTextField, 'custom option');
|
|
257
|
+
// Blur to set the value and trigger the first autosave
|
|
258
|
+
await user.tab();
|
|
259
|
+
// Clear the mock to isolate the clear action
|
|
260
|
+
onAutosaveMock.mockClear();
|
|
261
|
+
onChangeMock.mockClear();
|
|
262
|
+
// Click the clear button (now visible since value is set)
|
|
263
|
+
const clearButton = await screen.findByRole('button', { name: 'Clear' });
|
|
264
|
+
await user.click(clearButton);
|
|
265
|
+
// Verify onChange was called with empty value
|
|
266
|
+
expect(onChangeMock).toHaveBeenCalledWith('selectOptions', '');
|
|
267
|
+
// Verify autosave was triggered after clearing
|
|
268
|
+
expect(onAutosaveMock).toHaveBeenCalledWith('testSelect');
|
|
269
|
+
});
|
|
169
270
|
});
|
|
@@ -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
|
|
13
|
-
const
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
100
|
+
updateCache('fullValue', value);
|
|
79
101
|
}
|
|
80
102
|
}
|
|
81
103
|
setMode(newMode);
|