@evoke-platform/ui-components 1.6.0-testing.12 → 1.6.0-testing.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/dist/published/components/custom/CriteriaBuilder/CriteriaBuilder.d.ts +2 -3
  2. package/dist/published/components/custom/FormField/AddressFieldComponent/AddressFieldComponent.test.js +1 -1
  3. package/dist/published/components/custom/FormField/AddressFieldComponent/addressFieldComponent.js +1 -1
  4. package/dist/published/components/custom/FormField/BooleanSelect/BooleanSelect.js +3 -3
  5. package/dist/published/components/custom/FormField/DatePickerSelect/DatePickerSelect.js +7 -1
  6. package/dist/published/components/custom/FormField/DateTimePickerSelect/DateTimePickerSelect.js +7 -1
  7. package/dist/published/components/custom/FormField/FormField.d.ts +3 -2
  8. package/dist/published/components/custom/FormField/InputFieldComponent/InputFieldComponent.js +6 -3
  9. package/dist/published/components/custom/FormField/Select/Select.test.js +16 -41
  10. package/dist/published/components/custom/FormField/TimePickerSelect/TimePickerSelect.js +13 -3
  11. package/dist/published/components/custom/FormV2/FormRenderer.d.ts +19 -0
  12. package/dist/published/components/custom/FormV2/FormRenderer.js +183 -0
  13. package/dist/published/components/custom/FormV2/components/AccordionSections.d.ts +4 -0
  14. package/dist/published/components/custom/FormV2/components/AccordionSections.js +131 -0
  15. package/dist/published/components/custom/FormV2/components/ActionButtons.d.ts +19 -0
  16. package/dist/published/components/custom/FormV2/components/ActionButtons.js +106 -0
  17. package/dist/published/components/custom/FormV2/components/FieldWrapper.d.ts +24 -0
  18. package/dist/published/components/custom/FormV2/components/FieldWrapper.js +100 -0
  19. package/dist/published/components/custom/FormV2/components/FormContext.d.ts +12 -0
  20. package/dist/published/components/custom/FormV2/components/FormContext.js +8 -0
  21. package/dist/published/components/custom/FormV2/components/FormFieldTypes/AddressFields.d.ts +17 -0
  22. package/dist/published/components/custom/FormV2/components/FormFieldTypes/AddressFields.js +50 -0
  23. package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/ActionDialog.d.ts +14 -0
  24. package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/ActionDialog.js +88 -0
  25. package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/DocumentViewerCell.d.ts +13 -0
  26. package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/DocumentViewerCell.js +140 -0
  27. package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/DropdownRepeatableField.d.ts +17 -0
  28. package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/DropdownRepeatableField.js +233 -0
  29. package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/DropdownRepeatableFieldInput.d.ts +40 -0
  30. package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/DropdownRepeatableFieldInput.js +95 -0
  31. package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/RepeatableField.d.ts +12 -0
  32. package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/RepeatableField.js +526 -0
  33. package/dist/published/components/custom/FormV2/components/FormFieldTypes/Criteria.d.ts +12 -0
  34. package/dist/published/components/custom/FormV2/components/FormFieldTypes/Criteria.js +93 -0
  35. package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/Document.d.ts +16 -0
  36. package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/Document.js +73 -0
  37. package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/DocumentList.d.ts +13 -0
  38. package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/DocumentList.js +179 -0
  39. package/dist/published/components/custom/FormV2/components/FormFieldTypes/Image.d.ts +12 -0
  40. package/dist/published/components/custom/FormV2/components/FormFieldTypes/Image.js +108 -0
  41. package/dist/published/components/custom/FormV2/components/FormFieldTypes/UserProperty.d.ts +16 -0
  42. package/dist/published/components/custom/FormV2/components/FormFieldTypes/UserProperty.js +129 -0
  43. package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/InstanceLookup.d.ts +15 -0
  44. package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/InstanceLookup.js +226 -0
  45. package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/ObjectPropertyInput.d.ts +4 -0
  46. package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/ObjectPropertyInput.js +439 -0
  47. package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/RelatedObjectInstance.d.ts +29 -0
  48. package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/RelatedObjectInstance.js +74 -0
  49. package/dist/published/components/custom/FormV2/components/FormSections.d.ts +4 -0
  50. package/dist/published/components/custom/FormV2/components/FormSections.js +104 -0
  51. package/dist/published/components/custom/FormV2/components/RecursiveEntryRenderer.d.ts +2 -0
  52. package/dist/published/components/custom/FormV2/components/RecursiveEntryRenderer.js +209 -0
  53. package/dist/published/components/custom/FormV2/components/TabNav.d.ts +10 -0
  54. package/dist/published/components/custom/FormV2/components/TabNav.js +23 -0
  55. package/dist/published/components/custom/FormV2/components/ValidationFiles/Validation.d.ts +3 -0
  56. package/dist/published/components/custom/FormV2/components/ValidationFiles/Validation.js +176 -0
  57. package/dist/published/components/custom/FormV2/components/ValidationFiles/ValidationErrorDisplay.d.ts +10 -0
  58. package/dist/published/components/custom/FormV2/components/ValidationFiles/ValidationErrorDisplay.js +45 -0
  59. package/dist/published/components/custom/FormV2/components/types.d.ts +131 -0
  60. package/dist/published/components/custom/FormV2/components/types.js +1 -0
  61. package/dist/published/components/custom/FormV2/components/utils.d.ts +47 -0
  62. package/dist/published/components/custom/FormV2/components/utils.js +434 -0
  63. package/dist/published/components/custom/FormV2/index.d.ts +1 -0
  64. package/dist/published/components/custom/FormV2/index.js +1 -0
  65. package/dist/published/components/custom/index.d.ts +1 -0
  66. package/dist/published/components/custom/index.js +1 -0
  67. package/dist/published/index.d.ts +2 -2
  68. package/dist/published/index.js +2 -2
  69. package/dist/published/stories/FormField.stories.js +2 -1
  70. package/dist/published/theme/hooks.d.ts +7 -0
  71. package/dist/published/theme/hooks.js +9 -0
  72. package/package.json +4 -2
@@ -0,0 +1,140 @@
1
+ import { useApiServices } from '@evoke-platform/context';
2
+ import React, { useState } from 'react';
3
+ import { AutorenewRounded, Close, FileWithExtension, LaunchRounded } from '../../../../../../icons';
4
+ import { Button, Dialog, DialogContent, DialogTitle, IconButton, Menu, MenuItem, Typography, } from '../../../../../core';
5
+ import { Grid } from '../../../../../layout';
6
+ import { getPrefixedUrl } from '../../utils';
7
+ const DocumentView = (props) => {
8
+ const { document } = props;
9
+ return (React.createElement(React.Fragment, null,
10
+ React.createElement(Grid, { item: true, sx: {
11
+ display: 'flex',
12
+ justifyContent: 'center',
13
+ padding: '7px',
14
+ } },
15
+ React.createElement(FileWithExtension, { fontFamily: "Arial", fileExtension: document.name?.split('.')?.pop() ?? '', sx: {
16
+ height: '1rem',
17
+ width: '1rem',
18
+ } })),
19
+ React.createElement(Grid, { item: true, xs: 12, sx: {
20
+ width: '100%',
21
+ overflow: 'hidden',
22
+ textOverflow: 'ellipsis',
23
+ } },
24
+ React.createElement(Typography, { noWrap: true, sx: {
25
+ fontSize: '14px',
26
+ fontWeight: 700,
27
+ lineHeight: '15px',
28
+ width: '100%',
29
+ } }, document?.name))));
30
+ };
31
+ export const DocumentViewerCell = (props) => {
32
+ const { instance, propertyId, setSnackbarError, smallerThanMd } = props;
33
+ const apiServices = useApiServices();
34
+ const [anchorEl, setAnchorEl] = useState(null);
35
+ const [isLoading, setIsLoading] = useState(false);
36
+ const downloadDocument = async (doc, instance) => {
37
+ setIsLoading(true);
38
+ try {
39
+ const documentResponse = await apiServices.get(getPrefixedUrl(`/objects/${instance.objectId}/instances/${instance.id}/documents/${doc.id}/content`), { responseType: 'blob' });
40
+ const contentType = documentResponse.type;
41
+ const blob = new Blob([documentResponse], { type: contentType });
42
+ const url = URL.createObjectURL(blob);
43
+ // Let the browser handle whether to open the document to view in a new tab or download it.
44
+ window.open(url, '_blank');
45
+ setIsLoading(false);
46
+ URL.revokeObjectURL(url);
47
+ }
48
+ catch (error) {
49
+ const status = error.status;
50
+ let message = 'An error occurred while downloading the document.';
51
+ if (status === 403) {
52
+ message = 'You do not have permission to download this document.';
53
+ }
54
+ else if (status === 404) {
55
+ message = 'Document not found.';
56
+ }
57
+ setIsLoading(false);
58
+ setSnackbarError({
59
+ showAlert: true,
60
+ message,
61
+ isError: true,
62
+ });
63
+ }
64
+ };
65
+ return (React.createElement(React.Fragment, null, instance[propertyId]?.length ? (React.createElement(React.Fragment, null,
66
+ React.createElement(Button, { sx: {
67
+ display: 'flex',
68
+ alignItems: 'center',
69
+ justifyContent: 'flex-start',
70
+ padding: '6px 10px',
71
+ }, color: 'inherit', onClick: async (event) => {
72
+ event.stopPropagation();
73
+ const documents = instance[propertyId];
74
+ if (documents.length === 1) {
75
+ await downloadDocument(documents[0], instance);
76
+ }
77
+ else {
78
+ setAnchorEl(event.currentTarget);
79
+ }
80
+ }, variant: "text", "aria-haspopup": "menu", "aria-controls": `document-menu-${instance.id}-${propertyId}`, "aria-expanded": anchorEl ? 'true' : 'false' },
81
+ isLoading ? (React.createElement(AutorenewRounded, { sx: {
82
+ color: '#637381',
83
+ width: '20px',
84
+ height: '20px',
85
+ } })) : (React.createElement(LaunchRounded, { sx: {
86
+ color: '#637381',
87
+ width: '20px',
88
+ height: '20px',
89
+ } })),
90
+ React.createElement(Typography, { sx: {
91
+ marginLeft: '8px',
92
+ fontWeight: 400,
93
+ fontSize: '14px',
94
+ } }, isLoading ? 'Preparing document...' : 'View Document')),
95
+ !smallerThanMd ? (React.createElement(Menu, { id: `document-menu-${instance.id}-${propertyId}`, anchorEl: anchorEl, open: Boolean(anchorEl), onClose: () => {
96
+ setAnchorEl(null);
97
+ }, sx: {
98
+ '& .MuiPaper-root': {
99
+ borderRadius: '12px',
100
+ boxShadow: 'rgba(145, 158, 171, 0.2)',
101
+ },
102
+ }, variant: 'menu', PaperProps: {
103
+ tabIndex: 0,
104
+ sx: {
105
+ maxHeight: 200,
106
+ maxWidth: 300,
107
+ minWidth: 300,
108
+ },
109
+ component: 'nav',
110
+ } }, instance[propertyId].map((document) => (React.createElement(MenuItem, { key: document.id, onClick: async (e) => {
111
+ setAnchorEl(null);
112
+ await downloadDocument(document, instance);
113
+ }, "aria-label": document.name },
114
+ React.createElement(DocumentView, { document: document })))))) : (React.createElement(Dialog, { open: Boolean(anchorEl), onClose: () => setAnchorEl(null) },
115
+ React.createElement(DialogTitle, { sx: {
116
+ display: 'flex',
117
+ justifyContent: 'space-between',
118
+ alignItems: 'center',
119
+ paddingBottom: '0px',
120
+ } },
121
+ React.createElement(Typography, { sx: { fontSize: '14px', color: '#637381' } }, "Select a document to download"),
122
+ React.createElement(IconButton, { onClick: () => setAnchorEl(null) },
123
+ React.createElement(Close, { fontSize: "small" }))),
124
+ React.createElement(DialogContent, { sx: { padding: '20px 16px' } }, instance[propertyId].map((document) => (React.createElement(Grid, null,
125
+ React.createElement(Button, { color: 'inherit', onClick: async () => {
126
+ setAnchorEl(null);
127
+ await downloadDocument(document, instance);
128
+ }, sx: {
129
+ maxWidth: '100%',
130
+ overflow: 'hidden',
131
+ textOverflow: 'ellipsis',
132
+ whiteSpace: 'nowrap',
133
+ padding: '2px 0px',
134
+ }, key: document.id },
135
+ React.createElement(DocumentView, { document: document })))))))))) : (React.createElement(Typography, { sx: {
136
+ fontStyle: 'italic',
137
+ marginLeft: '12px',
138
+ fontSize: '14px',
139
+ } }, "No documents"))));
140
+ };
@@ -0,0 +1,17 @@
1
+ import { ObjWithRoot as EvokeObjectWithRoot, InputParameter, ObjectInstance, Property, ViewLayoutEntityReference } from '@evoke-platform/context';
2
+ import React from 'react';
3
+ import { FieldValues } from 'react-hook-form';
4
+ export type DropdownRepeatableFieldProps = {
5
+ id: string;
6
+ fieldDefinition: InputParameter | Property;
7
+ instance?: FieldValues;
8
+ criteria?: object;
9
+ readOnly: boolean;
10
+ initialMiddleObjectInstances: ObjectInstance[];
11
+ middleObject: EvokeObjectWithRoot;
12
+ fieldHeight?: 'small' | 'medium';
13
+ hasDescription?: boolean;
14
+ viewLayout?: ViewLayoutEntityReference;
15
+ };
16
+ declare const DropdownRepeatableField: (props: DropdownRepeatableFieldProps) => React.JSX.Element;
17
+ export default DropdownRepeatableField;
@@ -0,0 +1,233 @@
1
+ import { useApiServices, useNotification, } from '@evoke-platform/context';
2
+ import { debounce, isArray, isEmpty, isEqual } from 'lodash';
3
+ import React, { useCallback, useEffect, useState } from 'react';
4
+ import { useFormContext } from '../../../../../../theme/hooks';
5
+ import { Skeleton } from '../../../../../core';
6
+ import { retrieveCustomErrorMessage } from '../../../../Form/utils';
7
+ import { getMiddleObject, getMiddleObjectFilter, getPrefixedUrl, transformToWhere } from '../../utils';
8
+ import { DropdownRepeatableFieldInput } from './DropdownRepeatableFieldInput';
9
+ const DropdownRepeatableField = (props) => {
10
+ const { id, fieldDefinition, criteria, instance, readOnly, initialMiddleObjectInstances, middleObject, fieldHeight, hasDescription, viewLayout, } = props;
11
+ const { fetchedOptions, setFetchedOptions } = useFormContext();
12
+ const [layout, setLayout] = useState();
13
+ const [loading, setLoading] = useState(false);
14
+ const [layoutLoaded, setLayoutLoaded] = useState(false);
15
+ const [searchValue, setSearchValue] = useState('');
16
+ const [middleObjectInstances, setMiddleObjectInstances] = useState(initialMiddleObjectInstances);
17
+ const [endObject, setEndObject] = useState(fetchedOptions[`${fieldDefinition.id}EndObject`]);
18
+ const [endObjectInstances, setEndObjectInstances] = useState(fetchedOptions[`${fieldDefinition.id}EndObjectInstances`] || []);
19
+ const [initialLoading, setInitialLoading] = useState(endObjectInstances ? false : true);
20
+ const [selectedOptions, setSelectedOptions] = useState([]);
21
+ const [hasFetched, setHasFetched] = useState(!!fetchedOptions[`${fieldDefinition.id}EndObjectInstancesHaveFetched`] || false);
22
+ const [snackbarError, setSnackbarError] = useState({
23
+ showAlert: false,
24
+ isError: true,
25
+ });
26
+ const { instanceChanges } = useNotification();
27
+ const apiServices = useApiServices();
28
+ const getMiddleObjectInstances = async () => {
29
+ const filter = instance ? getMiddleObjectFilter(fieldDefinition, instance.id) : {};
30
+ try {
31
+ return await apiServices.get(getPrefixedUrl(`/objects/${fieldDefinition.objectId}/instances`), {
32
+ params: { filter: JSON.stringify(filter) },
33
+ });
34
+ }
35
+ catch (err) {
36
+ console.error(err);
37
+ return [];
38
+ }
39
+ };
40
+ const fetchMiddleObjectInstances = async () => {
41
+ const newInstances = await getMiddleObjectInstances();
42
+ setMiddleObjectInstances(newInstances);
43
+ };
44
+ const setDropDownSelections = (instances) => {
45
+ setSelectedOptions(instances
46
+ .filter((currInstance) => fieldDefinition.manyToManyPropertyId in currInstance)
47
+ .map((currInstance) => ({
48
+ label: currInstance[fieldDefinition.manyToManyPropertyId]?.name,
49
+ endObjectId: currInstance[fieldDefinition.manyToManyPropertyId].id,
50
+ middleObjectId: currInstance.id,
51
+ }))
52
+ .sort((instanceA, instanceB) => instanceA.label.localeCompare(instanceB.label)));
53
+ };
54
+ useEffect(() => {
55
+ const endObjectProperty = middleObject?.properties?.find((currProperty) => fieldDefinition.manyToManyPropertyId === currProperty.id);
56
+ if (endObjectProperty && endObjectProperty.objectId && !fetchedOptions[`${fieldDefinition.id}EndObject`]) {
57
+ setLayoutLoaded(false);
58
+ apiServices.get(getPrefixedUrl(`/objects/${endObjectProperty.objectId}/effective`), { params: { filter: { fields: ['id', 'name', 'properties', 'viewLayout'] } } }, (error, effectiveObject) => {
59
+ if (error) {
60
+ console.error(error);
61
+ }
62
+ else {
63
+ // If there's no error then the effective object is defined.
64
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
65
+ const endObject = effectiveObject;
66
+ setEndObject(endObject);
67
+ let defaultLayout;
68
+ if (endObject.viewLayout?.dropdown) {
69
+ defaultLayout = {
70
+ id: 'default',
71
+ name: 'Default',
72
+ objectId: endObject.id,
73
+ ...endObject.viewLayout.dropdown,
74
+ };
75
+ }
76
+ if (viewLayout) {
77
+ apiServices
78
+ .get(getPrefixedUrl(`/objects/${viewLayout.objectId}/dropdownLayouts/${viewLayout.id}`))
79
+ .then(setLayout)
80
+ .catch(() => setLayout(defaultLayout))
81
+ .finally(() => setLayoutLoaded(true));
82
+ }
83
+ else {
84
+ setLayout(defaultLayout);
85
+ setLayoutLoaded(true);
86
+ }
87
+ }
88
+ });
89
+ }
90
+ }, [middleObject, viewLayout]);
91
+ useEffect(() => {
92
+ instanceChanges?.subscribe(middleObject.rootObjectId, () => {
93
+ fetchMiddleObjectInstances();
94
+ });
95
+ return () => instanceChanges?.unsubscribe(middleObject.rootObjectId);
96
+ }, [instanceChanges, fetchMiddleObjectInstances]);
97
+ const fetchEndObjectInstances = useCallback((searchedName) => {
98
+ if ((fieldDefinition.objectId &&
99
+ fieldDefinition.manyToManyPropertyId &&
100
+ endObjectInstances.length === 0 &&
101
+ !hasFetched) ||
102
+ (searchedName !== undefined && searchedName !== '')) {
103
+ setLoading(true);
104
+ const endObjectProperty = middleObject.properties?.find((currProperty) => fieldDefinition.manyToManyPropertyId === currProperty.id);
105
+ if (endObjectProperty?.objectId) {
106
+ const { propertyId, direction } = layout?.sort ?? {
107
+ propertyId: 'name',
108
+ direction: 'asc',
109
+ };
110
+ const filter = {
111
+ limit: 100,
112
+ order: `${propertyId} ${direction}`,
113
+ };
114
+ let searchCriteria = criteria && !isEmpty(criteria) ? transformToWhere(criteria) : {};
115
+ if (searchedName?.length) {
116
+ const nameCriteria = transformToWhere({
117
+ name: {
118
+ like: searchedName,
119
+ options: 'i',
120
+ },
121
+ });
122
+ searchCriteria = !isEmpty(criteria)
123
+ ? {
124
+ and: [searchCriteria, nameCriteria],
125
+ }
126
+ : nameCriteria;
127
+ }
128
+ filter.where = searchCriteria;
129
+ apiServices.get(getPrefixedUrl(`/objects/${endObjectProperty.objectId}/instances`), { params: { filter: JSON.stringify(filter) } }, (error, instances) => {
130
+ if (!error && instances) {
131
+ setEndObjectInstances(instances);
132
+ setHasFetched(true);
133
+ }
134
+ setInitialLoading(false);
135
+ setLoading(false);
136
+ });
137
+ }
138
+ }
139
+ else if (endObjectInstances.length !== 0) {
140
+ setInitialLoading(false);
141
+ }
142
+ }, [fieldDefinition.objectId, fieldDefinition.manyToManyPropertyId, middleObject]);
143
+ const debouncedEndObjectSearch = useCallback(debounce(fetchEndObjectInstances, 500), [fetchEndObjectInstances]);
144
+ useEffect(() => {
145
+ if (!fetchedOptions[`${fieldDefinition.id}EndObjectInstances`] ||
146
+ (isArray(fetchedOptions[`${fieldDefinition.id}EndObjectInstances`]) &&
147
+ fetchedOptions[`${fieldDefinition.id}EndObjectInstances`].length === 0)) {
148
+ setFetchedOptions({
149
+ [`${fieldDefinition.id}EndObjectInstances`]: endObjectInstances,
150
+ [`${fieldDefinition.id}EndObjectInstancesHaveFetched`]: hasFetched,
151
+ });
152
+ }
153
+ if (!fetchedOptions[`${fieldDefinition.id}EndObject`]) {
154
+ setFetchedOptions({
155
+ [`${fieldDefinition.id}EndObject`]: endObject,
156
+ });
157
+ }
158
+ if (!isEqual(middleObjectInstances, initialMiddleObjectInstances)) {
159
+ setFetchedOptions({
160
+ [`${fieldDefinition.id}MiddleObjectInstances`]: middleObjectInstances,
161
+ });
162
+ }
163
+ }, [endObjectInstances, endObject, middleObjectInstances]);
164
+ useEffect(() => {
165
+ const updateFetchedOptions = (key, value) => {
166
+ if (!fetchedOptions[key]) {
167
+ setFetchedOptions({ [key]: value });
168
+ }
169
+ };
170
+ updateFetchedOptions(`${fieldDefinition.id}EndObjectInstances`, endObjectInstances);
171
+ updateFetchedOptions(`${fieldDefinition.id}EndObjectInstancesHaveFetched`, hasFetched);
172
+ updateFetchedOptions(`${fieldDefinition.id}EndObject`, endObject);
173
+ if (!isEqual(middleObjectInstances, initialMiddleObjectInstances)) {
174
+ setFetchedOptions({ [`${fieldDefinition.id}MiddleObjectInstances`]: middleObjectInstances });
175
+ }
176
+ }, [
177
+ endObjectInstances,
178
+ endObject,
179
+ middleObjectInstances,
180
+ fetchedOptions,
181
+ fieldDefinition.id,
182
+ hasFetched,
183
+ initialMiddleObjectInstances,
184
+ setFetchedOptions,
185
+ ]);
186
+ useEffect(() => {
187
+ debouncedEndObjectSearch(searchValue);
188
+ return () => debouncedEndObjectSearch.cancel();
189
+ }, [searchValue, debouncedEndObjectSearch]);
190
+ useEffect(() => {
191
+ if (layoutLoaded) {
192
+ fetchEndObjectInstances();
193
+ }
194
+ }, [fetchEndObjectInstances, layoutLoaded]);
195
+ const saveMiddleInstance = async (endObjectId, endObjectName) => {
196
+ if (fieldDefinition.objectId) {
197
+ const middleObject = getMiddleObject(fieldDefinition, endObjectId, endObjectName, instance);
198
+ try {
199
+ const newInstance = await apiServices.post(getPrefixedUrl(`/objects/${fieldDefinition.objectId}/instances/actions`), { actionId: `_create`, input: middleObject });
200
+ setMiddleObjectInstances((prevObjectInstances) => [
201
+ ...prevObjectInstances,
202
+ newInstance,
203
+ ]);
204
+ }
205
+ catch (err) {
206
+ setSnackbarError({
207
+ showAlert: true,
208
+ message: retrieveCustomErrorMessage(err) ??
209
+ 'An error occurred while adding an instance',
210
+ isError: true,
211
+ });
212
+ setDropDownSelections(middleObjectInstances);
213
+ }
214
+ }
215
+ };
216
+ const removeMiddleInstance = async (instanceId) => {
217
+ try {
218
+ await apiServices.post(getPrefixedUrl(`/objects/${fieldDefinition.objectId}/instances/${instanceId}/actions`), { actionId: '_delete' });
219
+ setMiddleObjectInstances((prevInstances) => prevInstances.filter((curr) => curr.id !== instanceId));
220
+ }
221
+ catch (err) {
222
+ setDropDownSelections(middleObjectInstances);
223
+ setSnackbarError({
224
+ showAlert: true,
225
+ message: retrieveCustomErrorMessage(err) ??
226
+ 'An error occurred while deleting the instance',
227
+ isError: true,
228
+ });
229
+ }
230
+ };
231
+ return initialLoading || !middleObject || !middleObjectInstances || !endObjectInstances || !endObject ? (React.createElement(Skeleton, null)) : (React.createElement(React.Fragment, null, middleObjectInstances && endObject && (React.createElement(DropdownRepeatableFieldInput, { id: id, fieldDefinition: fieldDefinition, readOnly: readOnly || !middleObject.actions?.some((action) => action.id === '_create'), layout: layout, middleObjectInstances: middleObjectInstances, endObjectInstances: endObjectInstances ?? [], endObject: endObject, searchValue: searchValue, loading: loading, handleSaveMiddleInstance: saveMiddleInstance, handleRemoveMiddleInstance: removeMiddleInstance, setSearchValue: setSearchValue, setSnackbarError: setSnackbarError, snackbarError: snackbarError, selectedOptions: selectedOptions, setSelectedOptions: setSelectedOptions, setDropdownSelections: setDropDownSelections, fieldHeight: fieldHeight, hasDescription: hasDescription }))));
232
+ };
233
+ export default DropdownRepeatableField;
@@ -0,0 +1,40 @@
1
+ import { DropdownViewLayout, InputParameter, Obj, ObjectInstance, Property } from '@evoke-platform/context';
2
+ import React from 'react';
3
+ import { AutocompleteOption } from '../../../../../core';
4
+ type DropdownRepeatableFieldInputProps = {
5
+ id: string;
6
+ fieldDefinition: InputParameter | Property;
7
+ readOnly: boolean;
8
+ layout?: DropdownViewLayout;
9
+ middleObjectInstances: ObjectInstance[];
10
+ endObjectInstances: ObjectInstance[];
11
+ endObject: Pick<Obj, 'id' | 'name' | 'properties'>;
12
+ searchValue: string;
13
+ loading: boolean;
14
+ fieldHeight?: 'small' | 'medium';
15
+ handleSaveMiddleInstance: (endObjectId: string, endObjectName: string) => void;
16
+ handleRemoveMiddleInstance: (instanceId: string) => void;
17
+ setSearchValue: (value: string) => void;
18
+ setSelectedOptions: (selectedOptions: DropdownRepeatableFieldInputOption[]) => void;
19
+ selectedOptions: DropdownRepeatableFieldInputOption[];
20
+ setSnackbarError: (snackbarError: {
21
+ showAlert: boolean;
22
+ message?: string;
23
+ isError: boolean;
24
+ }) => void;
25
+ snackbarError: {
26
+ showAlert: boolean;
27
+ message?: string;
28
+ isError: boolean;
29
+ };
30
+ setDropdownSelections?: (middleObjectInstances: ObjectInstance[]) => void;
31
+ hasDescription?: boolean;
32
+ };
33
+ export type DropdownRepeatableFieldInputOption = AutocompleteOption & {
34
+ endObjectId: string;
35
+ middleObjectId?: string;
36
+ subLabel?: string;
37
+ hidden?: boolean;
38
+ };
39
+ export declare const DropdownRepeatableFieldInput: (props: DropdownRepeatableFieldInputProps) => React.JSX.Element;
40
+ export default DropdownRepeatableFieldInput;
@@ -0,0 +1,95 @@
1
+ import { difference, isEmpty, isObject } from 'lodash';
2
+ import Handlebars from 'no-eval-handlebars';
3
+ import React, { useEffect, useState } from 'react';
4
+ import { Snackbar, TextField, Typography } from '../../../../../core';
5
+ import FormField from '../../../../FormField';
6
+ import { normalizeDates } from '../../utils';
7
+ const isDropdownRepeatableFieldInputOption = (option) => isObject(option) && 'label' in option && 'endObjectId' in option;
8
+ export const DropdownRepeatableFieldInput = (props) => {
9
+ const { id, fieldDefinition, readOnly, layout, middleObjectInstances, endObjectInstances, endObject, searchValue, loading, handleSaveMiddleInstance, handleRemoveMiddleInstance, setSearchValue, selectedOptions, setSnackbarError, snackbarError, setDropdownSelections, fieldHeight, hasDescription, } = props;
10
+ const [selectOptions, setSelectOptions] = useState([]);
11
+ useEffect(() => {
12
+ setDropdownSelections && setDropdownSelections(middleObjectInstances);
13
+ }, [middleObjectInstances]);
14
+ useEffect(() => {
15
+ const manyToManyPropertyId = fieldDefinition.manyToManyPropertyId;
16
+ if (manyToManyPropertyId) {
17
+ const enums = endObjectInstances.map((endObjectInstance) => {
18
+ const normalizedInstance = normalizeDates(endObjectInstance, endObject);
19
+ return {
20
+ label: normalizedInstance.name,
21
+ subLabel: layout?.secondaryTextExpression
22
+ ? compileExpression(normalizedInstance, layout.secondaryTextExpression)
23
+ : undefined,
24
+ endObjectId: normalizedInstance.id,
25
+ value: undefined,
26
+ };
27
+ });
28
+ setSelectOptions([
29
+ ...enums,
30
+ ...selectedOptions
31
+ .filter((selectedOption) => !enums.find((availableOption) => availableOption.endObjectId === selectedOption.endObjectId))
32
+ .map((option) => ({ ...option, hidden: true })),
33
+ ]);
34
+ }
35
+ }, [endObjectInstances, layout]);
36
+ const handleChange = (key, newSelectedOptions) => {
37
+ // Delete middle objects that have been removed
38
+ // Add middle objects that have been added
39
+ // You can only really add or remove one value at a time
40
+ const addedValues = difference(newSelectedOptions, selectedOptions);
41
+ if (!isEmpty(addedValues)) {
42
+ addedValues.forEach((newValue) => {
43
+ if (isDropdownRepeatableFieldInputOption(newValue) &&
44
+ fieldDefinition.relatedPropertyId &&
45
+ fieldDefinition.manyToManyPropertyId) {
46
+ handleSaveMiddleInstance(newValue.endObjectId, newValue.label);
47
+ }
48
+ });
49
+ }
50
+ const removedValues = difference(selectedOptions, newSelectedOptions);
51
+ if (!isEmpty(removedValues)) {
52
+ removedValues.forEach((removedValue) => {
53
+ if (isObject(removedValue) && removedValue.middleObjectId) {
54
+ handleRemoveMiddleInstance(removedValue.middleObjectId);
55
+ }
56
+ });
57
+ }
58
+ };
59
+ const compileExpression = (instance, expression) => {
60
+ const template = Handlebars.compileAST(expression);
61
+ return template(instance);
62
+ };
63
+ return (React.createElement(React.Fragment, null, !readOnly ? (fieldDefinition && (React.createElement(React.Fragment, null,
64
+ React.createElement(FormField, { id: id, property: {
65
+ ...fieldDefinition,
66
+ type: 'array',
67
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
68
+ enum: selectOptions,
69
+ }, onChange: handleChange, defaultValue: selectedOptions, isOptionEqualToValue: (option, value) => isDropdownRepeatableFieldInputOption(value) &&
70
+ isDropdownRepeatableFieldInputOption(option) &&
71
+ option.endObjectId === value.endObjectId, size: fieldHeight, renderOption: (props, option) => {
72
+ return isObject(props) && isDropdownRepeatableFieldInputOption(option) ? (React.createElement("li", { ...props, key: option.endObjectId },
73
+ React.createElement(Typography, null,
74
+ option.label,
75
+ React.createElement("br", null),
76
+ React.createElement(Typography, { variant: "caption", color: "#586069" }, option.subLabel ? option.subLabel : '')))) : null;
77
+ }, disableCloseOnSelect: true, additionalProps: {
78
+ filterOptions: (options) => {
79
+ return options.filter((option) => !option.hidden);
80
+ },
81
+ inputValue: searchValue,
82
+ renderInput: (params) => (React.createElement(TextField, { ...params, inputProps: {
83
+ ...params.inputProps,
84
+ ...(hasDescription
85
+ ? { 'aria-describedby': `${id}-description` }
86
+ : undefined),
87
+ }, onChange: (event) => {
88
+ setSearchValue(event.target.value);
89
+ } })),
90
+ loading,
91
+ sortBy: 'NONE',
92
+ } }),
93
+ React.createElement(Snackbar, { open: snackbarError.showAlert, handleClose: () => setSnackbarError({ isError: snackbarError.isError, showAlert: false }), message: snackbarError.message, error: snackbarError.isError })))) : (React.createElement(Typography, null, selectedOptions && selectedOptions.map((option) => option.label).join(', ')))));
94
+ };
95
+ export default DropdownRepeatableFieldInput;
@@ -0,0 +1,12 @@
1
+ import { InputParameter, Property, ViewLayoutEntityReference } from '@evoke-platform/context';
2
+ import React from 'react';
3
+ import { FieldValues } from 'react-hook-form';
4
+ export type ObjectPropertyInputProps = {
5
+ fieldDefinition: InputParameter | Property;
6
+ instance?: FieldValues;
7
+ canUpdateProperty: boolean;
8
+ criteria?: object;
9
+ viewLayout?: ViewLayoutEntityReference;
10
+ };
11
+ declare const RepeatableField: (props: ObjectPropertyInputProps) => React.JSX.Element;
12
+ export default RepeatableField;