@evoke-platform/ui-components 1.6.0-dev.14 → 1.6.0-dev.16
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/FormV2/components/FormFieldTypes/AddressFields.d.ts +17 -0
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/AddressFields.js +50 -0
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/Criteria.d.ts +12 -0
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/Criteria.js +94 -0
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/Image.d.ts +12 -0
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/Image.js +108 -0
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/UserProperty.d.ts +16 -0
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/UserProperty.js +126 -0
- package/dist/published/components/custom/FormV2/components/ValidationFiles/Validation.d.ts +3 -0
- package/dist/published/components/custom/FormV2/components/ValidationFiles/Validation.js +164 -0
- package/dist/published/components/custom/FormV2/components/ValidationFiles/ValidationErrorDisplay.d.ts +10 -0
- package/dist/published/components/custom/FormV2/components/ValidationFiles/ValidationErrorDisplay.js +45 -0
- package/dist/published/components/custom/FormV2/components/types.d.ts +8 -0
- package/dist/published/components/custom/FormV2/components/types.js +1 -0
- package/dist/published/components/custom/FormV2/components/utils.d.ts +5 -0
- package/dist/published/components/custom/FormV2/components/utils.js +59 -0
- package/dist/published/theme/hooks.d.ts +8 -0
- package/dist/published/theme/hooks.js +9 -0
- package/package.json +1 -1
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { InputField, InputParameter, InputParameterReference, Property, ReadonlyField } from '@evoke-platform/context';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { FieldErrors, FieldValues } from 'react-hook-form';
|
|
4
|
+
import { Address } from '../../../FormField/AddressFieldComponent';
|
|
5
|
+
interface AddressProps {
|
|
6
|
+
entry: InputParameterReference | ReadonlyField | InputField;
|
|
7
|
+
errors?: FieldErrors;
|
|
8
|
+
handleChange: (propertyId: string, value: string | Address | undefined) => void;
|
|
9
|
+
fieldHeight?: 'small' | 'medium';
|
|
10
|
+
readOnly?: boolean;
|
|
11
|
+
parameters?: InputParameter[];
|
|
12
|
+
instance?: FieldValues;
|
|
13
|
+
entryId: string;
|
|
14
|
+
fieldDefinition: InputParameter | Property;
|
|
15
|
+
}
|
|
16
|
+
declare function AddressFields(props: AddressProps): React.JSX.Element;
|
|
17
|
+
export default AddressFields;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { useApiServices, } from '@evoke-platform/context';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { useFormContext } from '../../../../../theme/hooks';
|
|
4
|
+
import { Typography } from '../../../../core';
|
|
5
|
+
import FormField from '../../../FormField';
|
|
6
|
+
import FieldWrapper from '../FieldWrapper';
|
|
7
|
+
import { getPrefixedUrl, isOptionEqualToValue } from '../utils';
|
|
8
|
+
function AddressFields(props) {
|
|
9
|
+
const { entry, errors, handleChange, fieldHeight, readOnly, parameters, instance, entryId, fieldDefinition } = props;
|
|
10
|
+
const { getValues } = useFormContext();
|
|
11
|
+
const apiServices = useApiServices();
|
|
12
|
+
const addressObject = entryId.split('.')[0];
|
|
13
|
+
const addressField = entryId.split('.')[1];
|
|
14
|
+
const addressValues = entry.type === 'readonlyField' ? instance?.[addressObject] : getValues(addressObject);
|
|
15
|
+
const fieldValue = addressValues?.[addressField];
|
|
16
|
+
const display = entry?.display;
|
|
17
|
+
const validation = fieldDefinition?.validation
|
|
18
|
+
? fieldDefinition.validation
|
|
19
|
+
: {};
|
|
20
|
+
const queryAddresses = async (query) => {
|
|
21
|
+
return await apiServices.get(getPrefixedUrl(`/locations/search`), {
|
|
22
|
+
params: { query: query },
|
|
23
|
+
});
|
|
24
|
+
};
|
|
25
|
+
const handleAddressChange = (name, value) => {
|
|
26
|
+
if (addressField === 'line1' && typeof value === 'object' && value.line1) {
|
|
27
|
+
const addressKeys = ['line1', 'city', 'county', 'state', 'zipCode'];
|
|
28
|
+
addressKeys.forEach((key) => {
|
|
29
|
+
const fullKey = `${addressObject}.${key}`;
|
|
30
|
+
if (parameters?.some((p) => p.id === fullKey)) {
|
|
31
|
+
const fieldValue = value[key];
|
|
32
|
+
handleChange(fullKey, fieldValue);
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
handleChange(name, value);
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
const addressErrors = errors?.[addressObject];
|
|
41
|
+
const addressFieldError = addressErrors?.[addressField];
|
|
42
|
+
return (React.createElement(FieldWrapper, { inputId: entryId, inputType: "string", label: display?.label || 'default', description: !readOnly ? display?.description : undefined, tooltip: display?.tooltip, value: fieldValue, maxLength: 'maxLength' in validation ? validation?.maxLength : 0, required: fieldDefinition?.required || false, showCharCount: !readOnly && display?.charCount, viewOnly: !!readOnly, prefix: display?.prefix, suffix: display?.suffix }, !readOnly ? (React.createElement(FormField, { property: fieldDefinition, defaultValue: fieldValue, onChange: handleAddressChange, isMultiLineText: !!display?.rowCount, readOnly: entry.type === 'readonlyField', ...(addressField === 'line1' && { queryAddresses }), mask: validation?.mask, placeholder: display?.placeholder, isOptionEqualToValue: isOptionEqualToValue, size: fieldHeight, error: !!addressFieldError, errorMessage: addressFieldError?.message, additionalProps: {
|
|
43
|
+
...(display?.description && {
|
|
44
|
+
inputProps: {
|
|
45
|
+
'aria-describedby': `${entryId}-description`,
|
|
46
|
+
},
|
|
47
|
+
}),
|
|
48
|
+
} })) : (React.createElement(Typography, { variant: "body1", key: entryId, sx: { height: '24px', paddingTop: '6px' } }, fieldValue))));
|
|
49
|
+
}
|
|
50
|
+
export default AddressFields;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { InputParameter, Property } from '@evoke-platform/context';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
type CriteriaProps = {
|
|
4
|
+
fieldDefinition: InputParameter | Property;
|
|
5
|
+
value?: CriteriaValue | null;
|
|
6
|
+
handleChange: (propertyId: string, value: CriteriaValue | null) => void;
|
|
7
|
+
canUpdateProperty: boolean;
|
|
8
|
+
error?: boolean;
|
|
9
|
+
};
|
|
10
|
+
type CriteriaValue = Record<string, unknown>;
|
|
11
|
+
export default function Criteria(props: CriteriaProps): React.JSX.Element;
|
|
12
|
+
export {};
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { useApiServices } from '@evoke-platform/context';
|
|
2
|
+
import React, { useCallback, useEffect, useState } from 'react';
|
|
3
|
+
import { useFormContext } from '../../../../../theme/hooks';
|
|
4
|
+
import { Button, CircularProgress, Typography } from '../../../../core';
|
|
5
|
+
import { Box } from '../../../../layout';
|
|
6
|
+
import CriteriaBuilder from '../../../CriteriaBuilder';
|
|
7
|
+
import { addressProperties, getPrefixedUrl } from '../utils';
|
|
8
|
+
export default function Criteria(props) {
|
|
9
|
+
const { handleChange, value, canUpdateProperty, fieldDefinition, error } = props;
|
|
10
|
+
const apiServices = useApiServices();
|
|
11
|
+
const { fetchedOptions, setFetchedOptions } = useFormContext();
|
|
12
|
+
const [loadingError, setLoadingError] = useState(false);
|
|
13
|
+
const [loading, setLoading] = useState(false);
|
|
14
|
+
const [properties, setProperties] = useState(fetchedOptions[`${fieldDefinition.id}Options`] || []);
|
|
15
|
+
const fetchProperties = useCallback(async () => {
|
|
16
|
+
if (fieldDefinition.objectId && !fetchedOptions[`${fieldDefinition.id}Options`]) {
|
|
17
|
+
setLoading(true);
|
|
18
|
+
apiServices.get(getPrefixedUrl(`/objects/${fieldDefinition.objectId}/effective/properties`), { params: { fields: ['properties'] } }, (error, properties) => {
|
|
19
|
+
if (error) {
|
|
20
|
+
console.error('Error fetching object properties', error);
|
|
21
|
+
setLoadingError(true);
|
|
22
|
+
}
|
|
23
|
+
if (properties) {
|
|
24
|
+
const flattenProperties = properties.flatMap((prop) => {
|
|
25
|
+
if (prop.type === 'object' || prop.type === 'user') {
|
|
26
|
+
return [
|
|
27
|
+
{
|
|
28
|
+
id: `${prop.id}.id`,
|
|
29
|
+
name: `${prop.name} Id`,
|
|
30
|
+
type: 'string',
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
id: `${prop.id}.name`,
|
|
34
|
+
name: `${prop.name} Name`,
|
|
35
|
+
type: 'string',
|
|
36
|
+
},
|
|
37
|
+
];
|
|
38
|
+
}
|
|
39
|
+
else if (prop.type === 'address') {
|
|
40
|
+
return addressProperties(prop);
|
|
41
|
+
}
|
|
42
|
+
return prop;
|
|
43
|
+
});
|
|
44
|
+
setProperties(flattenProperties);
|
|
45
|
+
setFetchedOptions((prev) => ({
|
|
46
|
+
...prev,
|
|
47
|
+
[`${fieldDefinition.id}Options`]: flattenProperties.map((prop) => ({
|
|
48
|
+
id: prop.id,
|
|
49
|
+
name: prop.name,
|
|
50
|
+
})),
|
|
51
|
+
}));
|
|
52
|
+
setLoadingError(false);
|
|
53
|
+
}
|
|
54
|
+
setLoading(false);
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
}, [fieldDefinition.objectId, apiServices]);
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
fetchProperties();
|
|
60
|
+
}, [fetchProperties]);
|
|
61
|
+
const handleUpdate = (criteria) => {
|
|
62
|
+
if (criteria || value) {
|
|
63
|
+
handleChange(fieldDefinition.id, criteria ?? null);
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
if (loadingError) {
|
|
67
|
+
return (React.createElement(Box, { sx: { display: 'flex', alignItems: 'center' } },
|
|
68
|
+
React.createElement(Typography, { sx: { color: 'rgb(114 124 132)', fontSize: '14px', paddingLeft: '10px' } }, "An error occurred when retrieving data needed for this criteria."),
|
|
69
|
+
React.createElement(Button, { sx: {
|
|
70
|
+
padding: 0,
|
|
71
|
+
'&:hover': { backgroundColor: 'transparent' },
|
|
72
|
+
minWidth: '44px',
|
|
73
|
+
}, variant: "text", onClick: fetchProperties, disabled: loading }, "Retry"),
|
|
74
|
+
loading && React.createElement(CircularProgress, { size: 20, sx: { paddingLeft: '10px' } })));
|
|
75
|
+
}
|
|
76
|
+
return !!value || canUpdateProperty ? (React.createElement(Box, { sx: { borderRadius: '8px', border: error ? '1px solid #FF0000' : '1px solid #ddd' } },
|
|
77
|
+
React.createElement(CriteriaBuilder, { criteria: value ?? undefined, properties: properties, setCriteria: handleUpdate, disabled: !canUpdateProperty, hideBorder: true, presetValues: [
|
|
78
|
+
{
|
|
79
|
+
label: 'Current Date',
|
|
80
|
+
value: { name: '{{{currentDate}}}', label: 'Current Date' },
|
|
81
|
+
type: 'date',
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
label: 'Current Time',
|
|
85
|
+
value: { name: '{{{currentTime}}}', label: 'Current Time' },
|
|
86
|
+
type: 'time',
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
label: 'Current Date Time',
|
|
90
|
+
value: { name: '{{{currentDateTime}}}', label: 'Current Date Time' },
|
|
91
|
+
type: 'date-time',
|
|
92
|
+
},
|
|
93
|
+
], enablePresetValues: true }))) : (React.createElement(Typography, { variant: "body2", sx: { color: '#637381' } }, "No criteria"));
|
|
94
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
export declare function blobToDataUrl(blob: Blob): Promise<string>;
|
|
3
|
+
type ImageProps = {
|
|
4
|
+
id: string;
|
|
5
|
+
handleChange: (propertyId: string, value: string | null) => void;
|
|
6
|
+
canUpdateProperty?: boolean;
|
|
7
|
+
error?: boolean;
|
|
8
|
+
value?: string;
|
|
9
|
+
hasDescription?: boolean;
|
|
10
|
+
};
|
|
11
|
+
export declare const Image: (props: ImageProps) => React.JSX.Element;
|
|
12
|
+
export {};
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { BackupOutlined, ClearRounded } from '@mui/icons-material';
|
|
2
|
+
import { CardMedia } from '@mui/material';
|
|
3
|
+
import React, { useEffect, useState } from 'react';
|
|
4
|
+
import { useDropzone } from 'react-dropzone';
|
|
5
|
+
import { IconButton, Typography } from '../../../../core';
|
|
6
|
+
import { Box, Grid } from '../../../../layout';
|
|
7
|
+
export function blobToDataUrl(blob) {
|
|
8
|
+
const reader = new FileReader();
|
|
9
|
+
return new Promise((resolve) => {
|
|
10
|
+
reader.onloadend = () => resolve(reader.result);
|
|
11
|
+
reader.readAsDataURL(blob);
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
const styles = {
|
|
15
|
+
imageContainer: {
|
|
16
|
+
margin: '5px 0',
|
|
17
|
+
height: '160px',
|
|
18
|
+
borderRadius: '8px',
|
|
19
|
+
maxWidth: '100%',
|
|
20
|
+
},
|
|
21
|
+
dropzoneContainer: {
|
|
22
|
+
margin: '5px 0',
|
|
23
|
+
height: '160px',
|
|
24
|
+
borderRadius: '8px',
|
|
25
|
+
display: 'flex',
|
|
26
|
+
justifyContent: 'center',
|
|
27
|
+
alignItems: 'center',
|
|
28
|
+
border: '1px dashed #858585',
|
|
29
|
+
position: 'relative',
|
|
30
|
+
cursor: 'pointer',
|
|
31
|
+
},
|
|
32
|
+
icon: {
|
|
33
|
+
color: '#fff',
|
|
34
|
+
zIndex: 40,
|
|
35
|
+
fontSize: '16px',
|
|
36
|
+
},
|
|
37
|
+
deleteIcon: {
|
|
38
|
+
borderRadius: '50%',
|
|
39
|
+
padding: '3px',
|
|
40
|
+
backgroundColor: '#212B36',
|
|
41
|
+
':hover': { backgroundColor: '#212B36', cursor: 'pointer' },
|
|
42
|
+
color: '#fff',
|
|
43
|
+
right: '29px',
|
|
44
|
+
bottom: '138px',
|
|
45
|
+
},
|
|
46
|
+
image: {
|
|
47
|
+
borderRadius: '8px',
|
|
48
|
+
width: 'fit-content',
|
|
49
|
+
maxWidth: '95%',
|
|
50
|
+
height: '160px',
|
|
51
|
+
position: 'relative',
|
|
52
|
+
display: 'inline-block',
|
|
53
|
+
objectFit: 'contain',
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
export const Image = (props) => {
|
|
57
|
+
const { id, handleChange, canUpdateProperty, error, value, hasDescription } = props;
|
|
58
|
+
const [image, setImage] = useState();
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
if (typeof value === 'string') {
|
|
61
|
+
setImage(value);
|
|
62
|
+
}
|
|
63
|
+
}, [value]);
|
|
64
|
+
const handleUpload = async (file) => {
|
|
65
|
+
if (file?.size && file.size <= 300000) {
|
|
66
|
+
const dataUrl = file ? await blobToDataUrl(file) : null;
|
|
67
|
+
setImage(dataUrl);
|
|
68
|
+
handleChange(id, dataUrl);
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
const handleRemove = (e) => {
|
|
72
|
+
setImage(null);
|
|
73
|
+
handleChange(id, '');
|
|
74
|
+
e.stopPropagation();
|
|
75
|
+
};
|
|
76
|
+
const { getRootProps, getInputProps, open } = useDropzone({
|
|
77
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
78
|
+
onDrop: (files) => handleUpload(files?.[0]),
|
|
79
|
+
accept: { 'image/*': ['.png', '.jpg', '.jpeg', '.gif', '.svg'] },
|
|
80
|
+
});
|
|
81
|
+
return (React.createElement(React.Fragment, null, image ? (React.createElement(Box, { sx: styles.imageContainer },
|
|
82
|
+
React.createElement(Box, { sx: { position: 'relative', left: 0, zIndex: 5 } },
|
|
83
|
+
React.createElement(CardMedia, { component: "img", image: image, alt: 'Uploaded Image', sx: styles.image }),
|
|
84
|
+
canUpdateProperty && (React.createElement(IconButton, { onClick: handleRemove, "aria-label": "Remove image", sx: styles.deleteIcon },
|
|
85
|
+
React.createElement(ClearRounded, { sx: styles.icon })))))) : canUpdateProperty ? (React.createElement(Box, { sx: {
|
|
86
|
+
...styles.dropzoneContainer,
|
|
87
|
+
borderColor: error ? 'red' : '#858585',
|
|
88
|
+
}, ...getRootProps(), onClick: open },
|
|
89
|
+
React.createElement("input", { ...getInputProps({ id }), multiple: false, ...(hasDescription ? { 'aria-describedby': `${id}-description` } : undefined) }),
|
|
90
|
+
React.createElement(Grid, { container: true, sx: { width: '100%' } },
|
|
91
|
+
React.createElement(Grid, { item: true, xs: 12, sx: {
|
|
92
|
+
display: 'flex',
|
|
93
|
+
justifyContent: 'center',
|
|
94
|
+
paddingBottom: '5px',
|
|
95
|
+
} },
|
|
96
|
+
React.createElement(BackupOutlined, { sx: { color: '#919EAB', height: '1.5em', width: '1.5em' } })),
|
|
97
|
+
React.createElement(Grid, { item: true, xs: 12 },
|
|
98
|
+
React.createElement(Typography, { variant: "body2", sx: { textAlign: 'center' } },
|
|
99
|
+
"Drag and drop or",
|
|
100
|
+
' ',
|
|
101
|
+
React.createElement(Typography, { component: 'span', color: 'primary', sx: { fontSize: '14px' } }, "select a file"),
|
|
102
|
+
' ',
|
|
103
|
+
"to upload")),
|
|
104
|
+
React.createElement(Grid, { item: true, xs: 12 },
|
|
105
|
+
React.createElement(Typography, { variant: "body2", sx: { color: '#637381', textAlign: 'center' } }, "Max file size of 300KB")),
|
|
106
|
+
React.createElement(Grid, { item: true, xs: 12 },
|
|
107
|
+
React.createElement(Typography, { variant: "body2", sx: { color: '#637381', textAlign: 'center' } }, "JPG, PNG, or GIF"))))) : (React.createElement(Typography, { variant: "body2", sx: { color: '#637381' } }, "No image"))));
|
|
108
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { AutocompleteOption } from '../../../../core';
|
|
3
|
+
export type UserPropertyProps = {
|
|
4
|
+
id: string;
|
|
5
|
+
handleChangeUserProperty: (id: string, user: AutocompleteOption) => void;
|
|
6
|
+
error?: boolean;
|
|
7
|
+
value?: {
|
|
8
|
+
id: string;
|
|
9
|
+
name: string;
|
|
10
|
+
};
|
|
11
|
+
fieldHeight?: 'small' | 'medium';
|
|
12
|
+
readOnly?: boolean;
|
|
13
|
+
hasDescription?: boolean;
|
|
14
|
+
};
|
|
15
|
+
declare const UserProperty: (props: UserPropertyProps) => React.JSX.Element;
|
|
16
|
+
export default UserProperty;
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { useApiServices } from '@evoke-platform/context';
|
|
2
|
+
import { ExpandMore } from '@mui/icons-material';
|
|
3
|
+
import React, { useEffect, useState } from 'react';
|
|
4
|
+
import { useFormContext } from '../../../../../theme/hooks';
|
|
5
|
+
import { Autocomplete, Paper, TextField, Typography } from '../../../../core';
|
|
6
|
+
import { getPrefixedUrl, isOptionEqualToValue } from '../utils';
|
|
7
|
+
const UserProperty = (props) => {
|
|
8
|
+
const { id, handleChangeUserProperty, error, value, fieldHeight, readOnly, hasDescription } = props;
|
|
9
|
+
const { fetchedOptions, setFetchedOptions } = useFormContext();
|
|
10
|
+
const [loadingOptions, setLoadingOptions] = useState(false);
|
|
11
|
+
const apiServices = useApiServices();
|
|
12
|
+
const [options, setOptions] = useState(fetchedOptions[`${id}Options`] || []);
|
|
13
|
+
const [openOptions, setOpenOptions] = useState(false);
|
|
14
|
+
const [users, setUsers] = useState();
|
|
15
|
+
const [userValue, setUserValue] = useState();
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
if (value && typeof value == 'object' && 'name' in value && 'id' in value) {
|
|
18
|
+
setUserValue({ label: value.name, value: value.id });
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
setUserValue(undefined);
|
|
22
|
+
}
|
|
23
|
+
}, [value]);
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
if (!fetchedOptions[`${id}Options`]) {
|
|
26
|
+
setLoadingOptions(true);
|
|
27
|
+
apiServices.get(getPrefixedUrl(`/users`), (error, userList) => {
|
|
28
|
+
setUsers(userList);
|
|
29
|
+
setOptions((userList ?? []).map((user) => ({
|
|
30
|
+
label: user.name,
|
|
31
|
+
value: user.id,
|
|
32
|
+
})));
|
|
33
|
+
setFetchedOptions((prev) => ({
|
|
34
|
+
...prev,
|
|
35
|
+
[`${id}Options`]: (userList ?? []).map((user) => ({
|
|
36
|
+
label: user.name,
|
|
37
|
+
value: user.id,
|
|
38
|
+
})),
|
|
39
|
+
}));
|
|
40
|
+
setLoadingOptions(false);
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
}, [id]);
|
|
44
|
+
return (options && (React.createElement(Autocomplete, { id: id, fullWidth: true, open: openOptions, popupIcon: userValue || readOnly ? '' : React.createElement(ExpandMore, null), PaperComponent: ({ children }) => {
|
|
45
|
+
return (React.createElement(Paper, { sx: {
|
|
46
|
+
borderRadius: '12px',
|
|
47
|
+
boxShadow: '0px 24px 48px 0px rgba(145, 158, 171, 0.2)',
|
|
48
|
+
'& .MuiAutocomplete-listbox': {
|
|
49
|
+
maxHeight: '25vh',
|
|
50
|
+
},
|
|
51
|
+
'& .MuiAutocomplete-noOptions': {
|
|
52
|
+
fontFamily: 'sans-serif',
|
|
53
|
+
fontSize: '14px',
|
|
54
|
+
paddingLeft: '24px',
|
|
55
|
+
color: 'rgba(145, 158, 171, 1)',
|
|
56
|
+
},
|
|
57
|
+
'& .MuiAutocomplete-loading': {
|
|
58
|
+
fontFamily: 'sans-serif',
|
|
59
|
+
fontSize: '14px',
|
|
60
|
+
paddingLeft: '24px',
|
|
61
|
+
color: 'rgba(145, 158, 171, 1)',
|
|
62
|
+
},
|
|
63
|
+
} }, children));
|
|
64
|
+
}, sx: {
|
|
65
|
+
'& button.MuiButtonBase-root': {
|
|
66
|
+
...(!loadingOptions && value ? { visibility: 'visible' } : {}),
|
|
67
|
+
},
|
|
68
|
+
'.MuiAutocomplete-clearIndicator': {
|
|
69
|
+
...(!value ? { display: 'none' } : undefined),
|
|
70
|
+
},
|
|
71
|
+
}, noOptionsText: 'No options available', renderOption: (props, option) => {
|
|
72
|
+
return (React.createElement("li", { ...props, key: option.id },
|
|
73
|
+
React.createElement(Typography, { sx: { marginLeft: '8px', fontSize: '14px' } },
|
|
74
|
+
option.label,
|
|
75
|
+
" ",
|
|
76
|
+
'',
|
|
77
|
+
users?.find((user) => option.value === user.id)?.status === 'Inactive' ? (React.createElement("span", null, "(Inactive)")) : (''))));
|
|
78
|
+
}, onOpen: () => {
|
|
79
|
+
setOpenOptions(true);
|
|
80
|
+
}, onClose: () => setOpenOptions(false), value: userValue ?? '', options: options, getOptionLabel: (option) => {
|
|
81
|
+
if (typeof option === 'string') {
|
|
82
|
+
return options.find((o) => o.value === option)?.label ?? '';
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
{
|
|
86
|
+
if (users?.find((user) => option.value === user.id)?.status === 'Inactive') {
|
|
87
|
+
return option.label + ' (Inactive)';
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
return option.label ?? '';
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}, onKeyDownCapture: (e) => {
|
|
95
|
+
// prevents keyboard trap
|
|
96
|
+
if (e.key === 'Tab') {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
if (value) {
|
|
100
|
+
e.preventDefault();
|
|
101
|
+
}
|
|
102
|
+
}, loading: loadingOptions, isOptionEqualToValue: isOptionEqualToValue, onChange: (event, value) => {
|
|
103
|
+
handleChangeUserProperty(id, value);
|
|
104
|
+
}, renderInput: (params) => (React.createElement(TextField, { ...params, placeholder: !readOnly ? 'Select' : undefined, readOnly: !loadingOptions && !value && readOnly, inputProps: {
|
|
105
|
+
...params.inputProps,
|
|
106
|
+
...(hasDescription ? { 'aria-describedby': `${id}-description` } : undefined),
|
|
107
|
+
}, sx: {
|
|
108
|
+
...(!loadingOptions && value
|
|
109
|
+
? {
|
|
110
|
+
'.MuiOutlinedInput-root': {
|
|
111
|
+
background: 'white',
|
|
112
|
+
border: 'auto',
|
|
113
|
+
},
|
|
114
|
+
'& fieldset': { borderColor: 'auto' },
|
|
115
|
+
'&:hover .MuiOutlinedInput-notchedOutline': {
|
|
116
|
+
border: 'auto',
|
|
117
|
+
},
|
|
118
|
+
caretColor: 'white',
|
|
119
|
+
'& svg': {
|
|
120
|
+
display: readOnly ? 'none' : 'block',
|
|
121
|
+
},
|
|
122
|
+
}
|
|
123
|
+
: {}),
|
|
124
|
+
} })), size: fieldHeight ?? 'medium', readOnly: readOnly, error: error })));
|
|
125
|
+
};
|
|
126
|
+
export default UserProperty;
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { FormEntry, InputParameter } from '@evoke-platform/context';
|
|
2
|
+
import { FieldValues, UseFormRegister } from 'react-hook-form';
|
|
3
|
+
export declare const handleValidation: (entries: FormEntry[], register: UseFormRegister<FieldValues>, formValues: FieldValues, parameters?: InputParameter[], instance?: FieldValues) => void;
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { isArray } from 'lodash';
|
|
2
|
+
import { DateTime } from 'luxon';
|
|
3
|
+
import Handlebars from 'no-eval-handlebars';
|
|
4
|
+
export const handleValidation = (entries, register, formValues, parameters, instance) => {
|
|
5
|
+
entries?.forEach((entry) => {
|
|
6
|
+
if (entry.type === 'sections' || entry.type === 'columns') {
|
|
7
|
+
const subEntries = entry.type === 'sections' ? entry.sections : entry.columns;
|
|
8
|
+
subEntries.forEach((subEntry) => {
|
|
9
|
+
if (subEntry.entries) {
|
|
10
|
+
handleValidation(subEntry.entries, register, formValues, parameters, instance);
|
|
11
|
+
}
|
|
12
|
+
});
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
else if (entry.type !== 'input' && entry.type !== 'inputField') {
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
const display = entry?.display;
|
|
19
|
+
const parameter = entry.type === 'input'
|
|
20
|
+
? parameters?.find((param) => param.id === entry.parameterId)
|
|
21
|
+
: entry.type === 'inputField'
|
|
22
|
+
? entry.input
|
|
23
|
+
: undefined;
|
|
24
|
+
const validation = parameter?.validation || {};
|
|
25
|
+
const fieldName = display?.label;
|
|
26
|
+
const errorMsg = validation?.errorMessage;
|
|
27
|
+
const validationRules = {};
|
|
28
|
+
// Required fields
|
|
29
|
+
if (entry.type !== 'inputField' && parameter?.required) {
|
|
30
|
+
validationRules.required = `${fieldName} is required`;
|
|
31
|
+
}
|
|
32
|
+
if (parameter?.type === 'boolean' && parameter?.strictlyTrue) {
|
|
33
|
+
validationRules.required = {
|
|
34
|
+
value: true,
|
|
35
|
+
message: display?.booleanDisplay === 'switch'
|
|
36
|
+
? `${fieldName} must be toggled on`
|
|
37
|
+
: `${fieldName} must be checked`,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
// Min/max char string fields
|
|
41
|
+
if (typeof validation.minLength === 'number') {
|
|
42
|
+
validationRules.minLength = {
|
|
43
|
+
value: validation.minLength,
|
|
44
|
+
message: `${fieldName} must have at least ${validation.minLength} characters`,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
if (typeof validation.maxLength === 'number') {
|
|
48
|
+
validationRules.maxLength = {
|
|
49
|
+
value: validation.maxLength,
|
|
50
|
+
message: `${fieldName} must have no more than ${validation.maxLength} characters`,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
// Min/max number fields
|
|
54
|
+
if (typeof validation.maximum === 'number') {
|
|
55
|
+
validationRules.max = {
|
|
56
|
+
value: validation.maximum,
|
|
57
|
+
message: errorMsg || `${fieldName} must have a value under ${validation.maximum}`,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
if (typeof validation.minimum === 'number') {
|
|
61
|
+
validationRules.min = {
|
|
62
|
+
value: validation.minimum,
|
|
63
|
+
message: errorMsg || `${fieldName} must have a value over ${validation.minimum}`,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
validationRules.validate = (value) => {
|
|
67
|
+
if (!value)
|
|
68
|
+
return true;
|
|
69
|
+
// Document validation
|
|
70
|
+
if (validation.maxDocuments || validation.minDocuments) {
|
|
71
|
+
const amountOfDocuments = isArray(value) ? value.length : 0;
|
|
72
|
+
const min = validation.minDocuments;
|
|
73
|
+
const max = validation.maxDocuments;
|
|
74
|
+
if (max && min && (amountOfDocuments > max || amountOfDocuments < min)) {
|
|
75
|
+
return (errorMsg ||
|
|
76
|
+
`Please select between ${validation.minDocuments} and ${validation.maxDocuments} document${max > 1 ? 's' : ''}`);
|
|
77
|
+
}
|
|
78
|
+
else if (min && amountOfDocuments < min) {
|
|
79
|
+
return errorMsg || `Please select at least ${min} document${min > 1 ? 's' : ''}`;
|
|
80
|
+
}
|
|
81
|
+
else if (max && amountOfDocuments > max) {
|
|
82
|
+
return errorMsg || `Please select no more than ${max} document${max > 1 ? 's' : ''}`;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
// Date and Time validation
|
|
86
|
+
if (validation.from || validation.to) {
|
|
87
|
+
const data = {
|
|
88
|
+
__today__: DateTime.now().toISODate(),
|
|
89
|
+
input: { ...instance, ...formValues },
|
|
90
|
+
};
|
|
91
|
+
if (validation.from) {
|
|
92
|
+
let earliestAllowed = validation.from;
|
|
93
|
+
if (/{{[\w.]+(?:\s+[\w.]+)*(?:\s+\d+)?}}/.test(earliestAllowed)) {
|
|
94
|
+
earliestAllowed = Handlebars.compileAST(earliestAllowed)(data);
|
|
95
|
+
}
|
|
96
|
+
if (earliestAllowed && value < earliestAllowed) {
|
|
97
|
+
return (errorMsg ||
|
|
98
|
+
`${fieldName} must be ${parameter?.type === 'time' ? 'at' : 'on'} or later than ${earliestAllowed || 'a field with no value'}`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
if (validation.to) {
|
|
102
|
+
let latestAllowed = validation.to;
|
|
103
|
+
if (/{{[\w.]+(?:\s+[\w.]+)*(?:\s+\d+)?}}/.test(latestAllowed)) {
|
|
104
|
+
latestAllowed = Handlebars.compileAST(latestAllowed)(data);
|
|
105
|
+
}
|
|
106
|
+
if (latestAllowed && value > latestAllowed) {
|
|
107
|
+
return (errorMsg ||
|
|
108
|
+
`${fieldName} must be ${parameter?.type === 'time' ? 'at' : 'on'} or before ${latestAllowed || 'a field with no value'}`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
// Regex validation
|
|
113
|
+
if (validation.rules) {
|
|
114
|
+
const rules = validation.rules;
|
|
115
|
+
const failedRules = rules.filter((rule) => {
|
|
116
|
+
const regex = new RegExp(rule.regex);
|
|
117
|
+
return !regex.test(value);
|
|
118
|
+
});
|
|
119
|
+
const operator = validation.operator;
|
|
120
|
+
if (failedRules.length > 0) {
|
|
121
|
+
if (operator === 'all') {
|
|
122
|
+
const messages = failedRules
|
|
123
|
+
.map((rule) => rule.errorMessage || 'Property is not in a valid format')
|
|
124
|
+
.join(' and ');
|
|
125
|
+
return `${fieldName}: ${messages}`;
|
|
126
|
+
}
|
|
127
|
+
if (operator === 'any' && failedRules.length < rules.length) {
|
|
128
|
+
return true; // passes if at least one rule passed
|
|
129
|
+
}
|
|
130
|
+
const messages = failedRules
|
|
131
|
+
.map((rule) => rule.errorMessage || 'Property is not in a valid format')
|
|
132
|
+
.join(' or ');
|
|
133
|
+
return `${fieldName}: ${messages}`;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
// Integer check
|
|
137
|
+
if (parameter?.type === 'integer') {
|
|
138
|
+
if (value && !Number.isInteger(value)) {
|
|
139
|
+
return `${fieldName} must be an integer`;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
// Invalid date check
|
|
143
|
+
if (value?.invalid && value?.dateText) {
|
|
144
|
+
return `Invalid Date`;
|
|
145
|
+
}
|
|
146
|
+
return true;
|
|
147
|
+
};
|
|
148
|
+
register(entry.parameterId || entry.input?.id, validationRules);
|
|
149
|
+
});
|
|
150
|
+
};
|
|
151
|
+
Handlebars.registerHelper('addDays', function (addend1, addend2) {
|
|
152
|
+
const dateAddend1 = DateTime.fromISO(addend1);
|
|
153
|
+
if (dateAddend1.isValid) {
|
|
154
|
+
return dateAddend1.plus({ days: addend2 }).toISODate();
|
|
155
|
+
}
|
|
156
|
+
return undefined;
|
|
157
|
+
});
|
|
158
|
+
Handlebars.registerHelper('subDays', function (minuend, subtrahend) {
|
|
159
|
+
const dateMinuend = DateTime.fromISO(minuend);
|
|
160
|
+
if (dateMinuend.isValid) {
|
|
161
|
+
return dateMinuend.minus({ days: subtrahend }).toISODate();
|
|
162
|
+
}
|
|
163
|
+
return undefined;
|
|
164
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { FieldErrors } from 'react-hook-form';
|
|
3
|
+
export type ValidationErrorDisplayProps = {
|
|
4
|
+
errors: FieldErrors;
|
|
5
|
+
show: boolean;
|
|
6
|
+
formId: string;
|
|
7
|
+
title?: string;
|
|
8
|
+
};
|
|
9
|
+
declare function ValidationErrorDisplay(props: ValidationErrorDisplayProps): React.JSX.Element | null;
|
|
10
|
+
export default ValidationErrorDisplay;
|
package/dist/published/components/custom/FormV2/components/ValidationFiles/ValidationErrorDisplay.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { useResponsive } from '../../../../../theme';
|
|
3
|
+
import { List, ListItem, Typography } from '../../../../core';
|
|
4
|
+
import { Box } from '../../../../layout';
|
|
5
|
+
function ValidationErrorDisplay(props) {
|
|
6
|
+
const { errors, show, formId, title } = props;
|
|
7
|
+
const { isSm, isXs } = useResponsive();
|
|
8
|
+
function extractErrorMessages(errors) {
|
|
9
|
+
const messages = [];
|
|
10
|
+
for (const key in errors) {
|
|
11
|
+
const error = errors[key];
|
|
12
|
+
if (error?.message) {
|
|
13
|
+
messages.push(error.message);
|
|
14
|
+
}
|
|
15
|
+
else if (error) {
|
|
16
|
+
for (const nestedKey in error) {
|
|
17
|
+
const nestedError = error[nestedKey];
|
|
18
|
+
if (nestedError?.message) {
|
|
19
|
+
messages.push(nestedError.message);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return messages;
|
|
25
|
+
}
|
|
26
|
+
const errorMessages = extractErrorMessages(errors);
|
|
27
|
+
return show && errorMessages.length > 0 ? (React.createElement(Box, { id: `validation-error-display-${formId}`, sx: {
|
|
28
|
+
backgroundColor: '#f8d7da',
|
|
29
|
+
borderColor: '#f5c6cb',
|
|
30
|
+
color: '#721c24',
|
|
31
|
+
border: '1px solid #721c24',
|
|
32
|
+
padding: '8px 24px',
|
|
33
|
+
borderRadius: '4px',
|
|
34
|
+
marginBottom: isSm || isXs ? 2 : 3,
|
|
35
|
+
marginTop: !title ? (isSm || isXs ? -2 : -3) : undefined,
|
|
36
|
+
} },
|
|
37
|
+
React.createElement(Typography, { sx: { color: '#721c24', mt: '16px', mb: '8px' } }, "Please fix the following errors before submitting:"),
|
|
38
|
+
React.createElement(List, { sx: {
|
|
39
|
+
listStyleType: 'disc',
|
|
40
|
+
paddingLeft: '40px',
|
|
41
|
+
mb: '8px',
|
|
42
|
+
fontFamily: 'Arial, Helvetica, sans-serif',
|
|
43
|
+
} }, errorMessages.map((msg, index) => (React.createElement(ListItem, { key: index, sx: { display: 'list-item', p: 0 } }, msg)))))) : null;
|
|
44
|
+
}
|
|
45
|
+
export default ValidationErrorDisplay;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { Property } from '@evoke-platform/context';
|
|
2
|
+
import { AutocompleteOption } from '../../../core';
|
|
3
|
+
export declare function getPrefixedUrl(url: string): string;
|
|
4
|
+
export declare const isOptionEqualToValue: (option: AutocompleteOption | string, value: unknown) => boolean;
|
|
5
|
+
export declare function addressProperties(addressProperty: Property): Property[];
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
export function getPrefixedUrl(url) {
|
|
2
|
+
const wcsMatchers = ['/apps', '/pages', '/widgets'];
|
|
3
|
+
const dataMatchers = ['/objects', '/correspondenceTemplates', '/documents', '/payments', '/locations'];
|
|
4
|
+
const signalrMatchers = ['/hubs'];
|
|
5
|
+
const accessManagementMatchers = ['/users'];
|
|
6
|
+
const workflowMatchers = ['/workflows'];
|
|
7
|
+
if (wcsMatchers.some((endpoint) => url.startsWith(endpoint)))
|
|
8
|
+
return `/webContent${url}`;
|
|
9
|
+
if (dataMatchers.some((endpoint) => url.startsWith(endpoint)))
|
|
10
|
+
return `/data${url}`;
|
|
11
|
+
if (signalrMatchers.some((endpoint) => url.startsWith(endpoint)))
|
|
12
|
+
return `/signalr${url}`;
|
|
13
|
+
if (accessManagementMatchers.some((endpoint) => url.startsWith(endpoint)))
|
|
14
|
+
return `/accessManagement${url}`;
|
|
15
|
+
if (workflowMatchers.some((endpoint) => url.startsWith(endpoint)))
|
|
16
|
+
return `/workflow${url}`;
|
|
17
|
+
console.error('Invalid URL');
|
|
18
|
+
return url;
|
|
19
|
+
}
|
|
20
|
+
export const isOptionEqualToValue = (option, value) => {
|
|
21
|
+
if (typeof option === 'string') {
|
|
22
|
+
return option === value;
|
|
23
|
+
}
|
|
24
|
+
return option.value === value;
|
|
25
|
+
};
|
|
26
|
+
export function addressProperties(addressProperty) {
|
|
27
|
+
return [
|
|
28
|
+
{
|
|
29
|
+
id: `${addressProperty.id}.line1`,
|
|
30
|
+
name: `${addressProperty.name} Line 1`,
|
|
31
|
+
type: 'string',
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
id: `${addressProperty.id}.line2`,
|
|
35
|
+
name: `${addressProperty.name} Line 2`,
|
|
36
|
+
type: 'string',
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
id: `${addressProperty.id}.city`,
|
|
40
|
+
name: `${addressProperty.name} City`,
|
|
41
|
+
type: 'string',
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
id: `${addressProperty.id}.county`,
|
|
45
|
+
name: `${addressProperty.name} County`,
|
|
46
|
+
type: 'string',
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
id: `${addressProperty.id}.state`,
|
|
50
|
+
name: `${addressProperty.name} State`,
|
|
51
|
+
type: 'string',
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
id: `${addressProperty.id}.zipCode`,
|
|
55
|
+
name: `${addressProperty.name} Zip Code`,
|
|
56
|
+
type: 'string',
|
|
57
|
+
},
|
|
58
|
+
];
|
|
59
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
/// <reference types="react" />
|
|
1
2
|
import { Breakpoint } from '@mui/material/styles';
|
|
2
3
|
/**
|
|
3
4
|
* Custom hook for responsive design breakpoints using size terminology.
|
|
@@ -107,3 +108,10 @@ export declare const useWidgetSize: (options?: {
|
|
|
107
108
|
resize?: boolean;
|
|
108
109
|
}) => WidgetSizeInfo;
|
|
109
110
|
export default useWidgetSize;
|
|
111
|
+
export declare function useFormContext(): {
|
|
112
|
+
fetchedOptions: import("react-hook-form").FieldValues;
|
|
113
|
+
setFetchedOptions: import("react").Dispatch<import("react").SetStateAction<import("react-hook-form").FieldValues>>;
|
|
114
|
+
getValues: import("react-hook-form").UseFormGetValues<import("react-hook-form").FieldValues>;
|
|
115
|
+
stickyFooter?: boolean | undefined;
|
|
116
|
+
object?: import("@evoke-platform/context").Obj | undefined;
|
|
117
|
+
};
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { useTheme } from '@mui/material';
|
|
2
2
|
import useMediaQuery from '@mui/material/useMediaQuery';
|
|
3
|
+
import { useContext } from 'react';
|
|
3
4
|
import useMeasure from 'react-use-measure';
|
|
5
|
+
import { FormContext } from '../components/custom/FormV2/components/FormContext';
|
|
4
6
|
/**
|
|
5
7
|
* Custom hook for responsive design breakpoints using size terminology.
|
|
6
8
|
* Breakpoints based on MUI default theme:
|
|
@@ -98,3 +100,10 @@ export const useWidgetSize = (options) => {
|
|
|
98
100
|
};
|
|
99
101
|
};
|
|
100
102
|
export default useWidgetSize;
|
|
103
|
+
export function useFormContext() {
|
|
104
|
+
const context = useContext(FormContext);
|
|
105
|
+
if (!context) {
|
|
106
|
+
throw new Error('useFormContext must be used within a FormContext.Provider');
|
|
107
|
+
}
|
|
108
|
+
return context;
|
|
109
|
+
}
|