@evoke-platform/ui-components 1.6.0-dev.19 → 1.6.0-dev.20

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.
@@ -0,0 +1,14 @@
1
+ import { Action, ActionType, EvokeForm, InputParameter, Obj } from '@evoke-platform/context';
2
+ import React from 'react';
3
+ export type ActionDialogProps = {
4
+ open: boolean;
5
+ onClose: () => void;
6
+ action: Action;
7
+ instanceInput: Record<string, unknown>;
8
+ handleSubmit: (actionType: ActionType, input: Record<string, unknown> | undefined, instanceId?: string, setSubmitting?: (value: boolean) => void) => void;
9
+ object: Obj;
10
+ instanceId?: string;
11
+ relatedParameter?: InputParameter;
12
+ relatedForm?: EvokeForm;
13
+ };
14
+ export declare const ActionDialog: (props: ActionDialogProps) => React.JSX.Element;
@@ -0,0 +1,83 @@
1
+ import { useApiServices } from '@evoke-platform/context';
2
+ import { Close } from '@mui/icons-material';
3
+ import React, { useEffect, useState } from 'react';
4
+ import { Dialog, DialogContent, DialogTitle, IconButton, Skeleton } from '../../../../../core';
5
+ import { Box } from '../../../../../layout';
6
+ import ErrorComponent from '../../../../ErrorComponent';
7
+ import { getPrefixedUrl } from '../../utils';
8
+ const styles = {
9
+ button: {
10
+ textTransform: 'initial',
11
+ fontSize: '14px',
12
+ fontWeight: 700,
13
+ marginRight: '10px',
14
+ },
15
+ dialogTitle: {
16
+ fontSize: '18px',
17
+ fontWeight: 700,
18
+ paddingTop: '35px',
19
+ paddingBottom: '20px',
20
+ },
21
+ closeIcon: {
22
+ position: 'absolute',
23
+ right: '17px',
24
+ top: '22px',
25
+ },
26
+ cancelBtn: {
27
+ border: '1px solid #ced4da',
28
+ width: '75px',
29
+ marginRight: '10px',
30
+ color: 'black',
31
+ },
32
+ deleteBtn: {
33
+ color: '#ffffff',
34
+ backgroundColor: '#A12723',
35
+ '&:hover': { backgroundColor: '#A12723' },
36
+ },
37
+ };
38
+ export const ActionDialog = (props) => {
39
+ const { open, onClose, action, object, instanceId, relatedForm } = props;
40
+ const [loading, setLoading] = useState(false);
41
+ const [hasAccess, setHasAccess] = useState();
42
+ const [form, setForm] = useState();
43
+ const apiServices = useApiServices();
44
+ const isDeleteAction = action.type === 'delete';
45
+ useEffect(() => {
46
+ if (instanceId) {
47
+ setLoading(true);
48
+ apiServices.get(getPrefixedUrl(`/objects/${object.id}/instances/${instanceId}/checkAccess`), {
49
+ params: { action: 'execute', field: action.id },
50
+ }, (error, result) => {
51
+ setHasAccess(result?.result ?? false);
52
+ setLoading(false);
53
+ });
54
+ }
55
+ else {
56
+ setHasAccess(true);
57
+ setLoading(false);
58
+ }
59
+ }, [object, instanceId]);
60
+ useEffect(() => {
61
+ setForm(isDeleteAction && !form
62
+ ? {
63
+ id: '',
64
+ name: '',
65
+ entries: [],
66
+ objectId: object.id,
67
+ actionId: '_delete',
68
+ display: {
69
+ submitLabel: 'Delete',
70
+ },
71
+ }
72
+ : relatedForm);
73
+ }, [relatedForm, action, form]);
74
+ return (React.createElement(Dialog, { maxWidth: 'md', fullWidth: true, open: open, onClose: (e, reason) => reason !== 'backdropClick' && onClose() },
75
+ React.createElement(DialogTitle, { sx: styles.dialogTitle },
76
+ React.createElement(IconButton, { sx: styles.closeIcon, onClick: onClose },
77
+ React.createElement(Close, { fontSize: "small" })),
78
+ action && hasAccess && !loading ? action?.name : ''),
79
+ React.createElement(DialogContent, { sx: { paddingBottom: loading ? undefined : '0px' } }, hasAccess ? (React.createElement(Box, { sx: { width: '100%', marginTop: '10px' } })) : (React.createElement(React.Fragment, null, loading ? (React.createElement(React.Fragment, null,
80
+ React.createElement(Skeleton, { height: '30px', animation: 'wave' }),
81
+ React.createElement(Skeleton, { height: '30px', animation: 'wave' }),
82
+ React.createElement(Skeleton, { height: '30px', animation: 'wave' }))) : (React.createElement(ErrorComponent, { code: 'AccessDenied', message: 'You do not have permission to perform this action.', styles: { boxShadow: 'none' } })))))));
83
+ };
@@ -0,0 +1,13 @@
1
+ import { ObjectInstance } from '@evoke-platform/context';
2
+ import React from 'react';
3
+ export type DocumentViewerCellProps = {
4
+ instance: ObjectInstance;
5
+ propertyId: string;
6
+ setSnackbarError: (error: {
7
+ showAlert: boolean;
8
+ message?: string;
9
+ isError?: boolean;
10
+ }) => void;
11
+ smallerThanMd?: boolean;
12
+ };
13
+ export declare const DocumentViewerCell: (props: DocumentViewerCellProps) => React.JSX.Element;
@@ -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;
@@ -0,0 +1,526 @@
1
+ import { useApiServices, useNotification, } from '@evoke-platform/context';
2
+ import { LocalDateTime } from '@js-joda/core';
3
+ import { get, isEqual, isObject, pick, startCase } from 'lodash';
4
+ import { DateTime } from 'luxon';
5
+ import React, { useCallback, useEffect, useState } from 'react';
6
+ import sift from 'sift';
7
+ import { Edit, ExpandMoreOutlined, TrashCan } from '../../../../../../icons';
8
+ import { useResponsive } from '../../../../../../theme';
9
+ import { useFormContext } from '../../../../../../theme/hooks';
10
+ import { Accordion, AccordionDetails, AccordionSummary, Button, IconButton, Skeleton, Snackbar, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Tooltip, Typography, } from '../../../../../core';
11
+ import { Box } from '../../../../../layout';
12
+ import { getReadableQuery } from '../../../../CriteriaBuilder';
13
+ import { retrieveCustomErrorMessage } from '../../../../Form/utils';
14
+ import { getPrefixedUrl, normalizeDateTime, transformToWhere } from '../../utils';
15
+ import { ActionDialog } from './ActionDialog';
16
+ import { DocumentViewerCell } from './DocumentViewerCell';
17
+ const styles = {
18
+ addButton: {
19
+ backgroundColor: '#ebf4f8',
20
+ boxShadow: 'none',
21
+ color: '#0075a7',
22
+ marginTop: '15px',
23
+ '&:hover': {
24
+ backgroundColor: '#ebf4f8',
25
+ color: '#0075a7',
26
+ boxShadow: 'none',
27
+ },
28
+ },
29
+ tableCell: {
30
+ color: '#637381',
31
+ backgroundColor: '#F4F6F8',
32
+ fontSize: '14px',
33
+ fontWeight: '700',
34
+ padding: '8px 20px',
35
+ whiteSpace: 'nowrap',
36
+ },
37
+ };
38
+ const RepeatableField = (props) => {
39
+ const { fieldDefinition, instance, canUpdateProperty, criteria, viewLayout } = props;
40
+ const { fetchedOptions, setFetchedOptions } = useFormContext();
41
+ const { instanceChanges } = useNotification();
42
+ const apiServices = useApiServices();
43
+ const { smallerThan } = useResponsive();
44
+ const smallerThanMd = smallerThan('md');
45
+ const [reloadOnErrorTrigger, setReloadOnErrorTrigger] = useState(true);
46
+ const [criteriaObjects, setCriteriaObjects] = useState([]);
47
+ const [selectedRow, setSelectedRow] = useState();
48
+ const [dialogType, setDialogType] = useState();
49
+ const [openDialog, setOpenDialog] = useState(false);
50
+ const [users, setUsers] = useState(fetchedOptions[`${fieldDefinition.id}Users`] || []);
51
+ const [error, setError] = useState(false);
52
+ const [relatedInstances, setRelatedInstances] = useState(fetchedOptions[`${fieldDefinition.id}Options`] || []);
53
+ const [relatedObject, setRelatedObject] = useState(fetchedOptions[`${fieldDefinition.id}RelatedObject`]);
54
+ const [hasCreateAction, setHasCreateAction] = useState(fetchedOptions[`${fieldDefinition.id}HasCreateAction`] || false);
55
+ const [loading, setLoading] = useState((relatedObject && relatedInstances) || !fieldDefinition ? false : true);
56
+ const [tableViewLayout, setTableViewLayout] = useState(fetchedOptions[`${fieldDefinition.id}TableViewLayout`]);
57
+ const [snackbarError, setSnackbarError] = useState({
58
+ showAlert: false,
59
+ isError: false,
60
+ });
61
+ const DEFAULT_CREATE_ACTION = '_create';
62
+ const fetchRelatedInstances = useCallback(async (refetch = false) => {
63
+ let relatedObject;
64
+ if (fieldDefinition.objectId) {
65
+ if (!fetchedOptions[`${fieldDefinition.id}RelatedObject`]) {
66
+ try {
67
+ relatedObject = await apiServices.get(getPrefixedUrl(`/objects/${fieldDefinition.objectId}/effective`));
68
+ let defaultTableViewLayout;
69
+ if (relatedObject.viewLayout?.table) {
70
+ defaultTableViewLayout = {
71
+ id: 'default',
72
+ name: 'Default',
73
+ objectId: relatedObject.id,
74
+ ...relatedObject?.viewLayout.table,
75
+ };
76
+ }
77
+ if (viewLayout) {
78
+ apiServices
79
+ .get(getPrefixedUrl(`/objects/${viewLayout.objectId}/tableLayouts/${viewLayout.id}`))
80
+ .then(setTableViewLayout)
81
+ .catch((err) => setTableViewLayout(defaultTableViewLayout));
82
+ }
83
+ else {
84
+ setTableViewLayout(defaultTableViewLayout);
85
+ }
86
+ setRelatedObject(relatedObject);
87
+ }
88
+ catch (err) {
89
+ console.error(err);
90
+ }
91
+ }
92
+ if (fieldDefinition.relatedPropertyId &&
93
+ fieldDefinition.objectId &&
94
+ instance?.id &&
95
+ (!fetchedOptions[`${fieldDefinition.id}Options`] || refetch)) {
96
+ const filterProperty = `${fieldDefinition.relatedPropertyId}.id`;
97
+ const transformedCriteria = criteria ? transformToWhere(criteria) : {};
98
+ const filter = {
99
+ where: { [filterProperty]: instance?.id, ...transformedCriteria },
100
+ limit: 100,
101
+ };
102
+ try {
103
+ const timeout = setTimeout(() => {
104
+ setLoading(false);
105
+ }, 300);
106
+ setLoading(true);
107
+ const instances = await apiServices.get(getPrefixedUrl(`/objects/${fieldDefinition.objectId}/instances`), {
108
+ params: { filter: JSON.stringify(filter) },
109
+ });
110
+ clearTimeout(timeout);
111
+ if (instances) {
112
+ setRelatedInstances(instances);
113
+ }
114
+ }
115
+ catch (error) {
116
+ setError(true);
117
+ }
118
+ }
119
+ setLoading(false);
120
+ }
121
+ relatedObject && checkCreateAccess(relatedObject);
122
+ }, [fieldDefinition]);
123
+ const fetchCriteriaObjects = useCallback(async () => {
124
+ let objectIds = [];
125
+ const criteriaProperties = relatedObject?.properties?.filter((property) => property.type === 'criteria' && property.objectId) ?? [];
126
+ if (tableViewLayout) {
127
+ objectIds = criteriaProperties
128
+ .filter((p) => tableViewLayout.properties.some((column) => column.id === p.id))
129
+ .map((property) => property.objectId);
130
+ }
131
+ else {
132
+ objectIds = criteriaProperties.map((p) => p.objectId);
133
+ }
134
+ const objects = [];
135
+ for (const objectId of new Set(objectIds)) {
136
+ try {
137
+ const criteriaObject = await apiServices.get(getPrefixedUrl(`/objects/${objectId}/effective`), {
138
+ params: { fields: ['id', 'name', 'properties'] },
139
+ });
140
+ objects.push(criteriaObject);
141
+ }
142
+ catch (error) {
143
+ console.error(`Error fetching criteria object with ID ${objectId}:`, error);
144
+ }
145
+ }
146
+ setCriteriaObjects(objects);
147
+ }, [apiServices, relatedObject, tableViewLayout]);
148
+ useEffect(() => {
149
+ if (!fetchedOptions[`${fieldDefinition.id}Users`]) {
150
+ (async () => {
151
+ try {
152
+ const users = await apiServices.get(getPrefixedUrl(`/users`));
153
+ setFetchedOptions({
154
+ [`${fieldDefinition.id}Users`]: users,
155
+ });
156
+ setUsers(users);
157
+ }
158
+ catch (error) {
159
+ console.error(error);
160
+ }
161
+ })();
162
+ }
163
+ }, [apiServices]);
164
+ useEffect(() => {
165
+ fetchRelatedInstances();
166
+ }, [fetchRelatedInstances, reloadOnErrorTrigger, instance]);
167
+ useEffect(() => {
168
+ if (relatedObject)
169
+ fetchCriteriaObjects();
170
+ }, [fetchCriteriaObjects, relatedObject]);
171
+ useEffect(() => {
172
+ if (relatedObject?.rootObjectId) {
173
+ // pass true here so while it doesn't refetch on every tab change it does refetch on changes made
174
+ const callback = () => fetchRelatedInstances(true);
175
+ instanceChanges?.subscribe(relatedObject?.rootObjectId, callback);
176
+ return () => instanceChanges?.unsubscribe(relatedObject?.rootObjectId, callback);
177
+ }
178
+ }, [instanceChanges, relatedObject]);
179
+ const retrieveCriteria = (relatedObjProperty, action, object) => {
180
+ let property;
181
+ if (action.parameters) {
182
+ property = action.parameters.find((param) => param.id === relatedObjProperty);
183
+ return {
184
+ relatedObjectProperty: property,
185
+ criteria: property?.validation?.criteria,
186
+ };
187
+ }
188
+ else if (action.inputProperties) {
189
+ const flattenInputProperties = (entries) => {
190
+ return entries.reduce((acc, entry) => {
191
+ if (entry.components) {
192
+ const components = entry.components.flatMap((s) => s.components ?? []);
193
+ return acc.concat(flattenInputProperties(components ?? []));
194
+ }
195
+ else if (entry.columns) {
196
+ const components = entry.columns.flatMap((c) => c.components ?? []);
197
+ return acc.concat(flattenInputProperties(components ?? []));
198
+ }
199
+ else if (entry.html) {
200
+ return acc;
201
+ }
202
+ else {
203
+ return acc.concat([entry]);
204
+ }
205
+ }, []);
206
+ };
207
+ property = flattenInputProperties(action.inputProperties).find((param) => param.key === relatedObjProperty);
208
+ return {
209
+ relatedObjectProperty: property,
210
+ criteria: (property?.validate).criteria,
211
+ };
212
+ }
213
+ else {
214
+ property = object.properties?.find((prop) => prop.id === relatedObjProperty);
215
+ return {
216
+ relatedObjectProperty: property,
217
+ criteria: property?.validation?.criteria,
218
+ };
219
+ }
220
+ };
221
+ const checkCreateAccess = (relatedObject) => {
222
+ if (fieldDefinition.objectId && canUpdateProperty && !fetchedOptions[`${fieldDefinition.id}HasCreateAction`]) {
223
+ apiServices
224
+ .get(getPrefixedUrl(`/objects/${fieldDefinition.objectId}/instances/checkAccess`), {
225
+ params: { action: 'execute', field: '_create', scope: 'data' },
226
+ })
227
+ .then((checkAccess) => {
228
+ const action = relatedObject.actions?.find((item) => item.id === '_create');
229
+ if (action &&
230
+ fieldDefinition.relatedPropertyId &&
231
+ // TODO: replace with the entries create form or defaultFormId of the
232
+ // default create action, keeping it like this to get minimum changes out so other can use it
233
+ !!fieldDefinition.createForm) {
234
+ const { relatedObjectProperty, criteria } = retrieveCriteria(fieldDefinition.relatedPropertyId, action, relatedObject);
235
+ if (!criteria || JSON.stringify(criteria).includes('{{{input.') || !relatedObjectProperty) {
236
+ setHasCreateAction(checkAccess.result);
237
+ }
238
+ else {
239
+ const validate = sift(criteria);
240
+ setHasCreateAction(validate(instance) && checkAccess.result);
241
+ }
242
+ }
243
+ else {
244
+ setHasCreateAction(false);
245
+ }
246
+ });
247
+ }
248
+ };
249
+ useEffect(() => {
250
+ const updatedOptions = {};
251
+ if ((relatedInstances && !fetchedOptions[`${fieldDefinition.id}Options`]) ||
252
+ fetchedOptions[`${fieldDefinition.id}Options`].length === 0 ||
253
+ !isEqual(relatedInstances, fetchedOptions[`${fieldDefinition.id}Options`])) {
254
+ updatedOptions[`${fieldDefinition.id}Options`] = relatedInstances;
255
+ }
256
+ if (relatedObject && !fetchedOptions[`${fieldDefinition.id}RelatedObject`]) {
257
+ updatedOptions[`${fieldDefinition.id}RelatedObject`] = relatedObject;
258
+ }
259
+ if (tableViewLayout && !fetchedOptions[`${fieldDefinition.id}TableViewLayout`]) {
260
+ updatedOptions[`${fieldDefinition.id}TableViewLayout`] = tableViewLayout;
261
+ }
262
+ if ((hasCreateAction || hasCreateAction === false) && !fetchedOptions[`${fieldDefinition.id}HasCreateAction`]) {
263
+ updatedOptions[`${fieldDefinition.id}HasCreateAction`] = hasCreateAction;
264
+ }
265
+ else if (!hasCreateAction && relatedObject) {
266
+ checkCreateAccess(relatedObject);
267
+ }
268
+ if (Object.keys(updatedOptions).length > 0) {
269
+ setFetchedOptions(updatedOptions);
270
+ }
271
+ }, [relatedObject, relatedInstances, hasCreateAction, tableViewLayout]);
272
+ const deleteRow = (id) => {
273
+ setDialogType('delete');
274
+ setSelectedRow(id);
275
+ setOpenDialog(true);
276
+ };
277
+ const addRow = () => {
278
+ setDialogType('create');
279
+ setSelectedRow(undefined);
280
+ setOpenDialog(true);
281
+ };
282
+ const editRow = (id) => {
283
+ setDialogType('update');
284
+ setSelectedRow(id);
285
+ setOpenDialog(true);
286
+ };
287
+ const ErrorComponent = () => loading ? (React.createElement("div", null,
288
+ React.createElement(Typography, { sx: {
289
+ fontSize: '14px',
290
+ color: '#727c84',
291
+ } }, "Loading..."))) : (React.createElement(Typography, { sx: {
292
+ color: 'rgb(114 124 132)',
293
+ fontSize: '14px',
294
+ } },
295
+ "An error occurred when retrieving this data.",
296
+ ' ',
297
+ React.createElement(Button, { sx: {
298
+ padding: 0,
299
+ '&:hover': {
300
+ backgroundColor: 'transparent',
301
+ },
302
+ 'min-width': '44px',
303
+ }, variant: "text", onClick: () => setReloadOnErrorTrigger((prevState) => !prevState) }, "Retry")));
304
+ const save = async (actionType, input, instanceId) => {
305
+ // date-time fields are stored in the database in ISO format so convert all
306
+ // LocalDateTime objects to ISO format.
307
+ if (isObject(input)) {
308
+ input = Object.entries(input).reduce((agg, [key, value]) => Object.assign(agg, {
309
+ [key]: value instanceof LocalDateTime ? normalizeDateTime(value) : value,
310
+ }), {});
311
+ }
312
+ if (actionType === 'create') {
313
+ const updatedInput = {
314
+ ...input,
315
+ [fieldDefinition?.relatedPropertyId]: { id: instance?.id },
316
+ };
317
+ try {
318
+ const instance = await apiServices.post(getPrefixedUrl(`/objects/${fieldDefinition.objectId}/instances/actions`), {
319
+ actionId: DEFAULT_CREATE_ACTION,
320
+ input: updatedInput,
321
+ });
322
+ const hasAccess = fieldDefinition?.relatedPropertyId && fieldDefinition.relatedPropertyId in instance;
323
+ hasAccess && setRelatedInstances([...relatedInstances, instance]);
324
+ setOpenDialog(false);
325
+ setDialogType(undefined);
326
+ setSelectedRow(undefined);
327
+ }
328
+ catch (err) {
329
+ setSnackbarError({
330
+ showAlert: true,
331
+ message: retrieveCustomErrorMessage(err) ??
332
+ `An error occurred while creating an instance`,
333
+ isError: true,
334
+ });
335
+ }
336
+ }
337
+ else {
338
+ const relatedObjectId = relatedObject?.id;
339
+ try {
340
+ await apiServices.post(getPrefixedUrl(`/objects/${relatedObjectId}/instances/${instanceId}/actions`), {
341
+ actionId: `_${actionType}`,
342
+ input: pick(input, relatedObject?.properties
343
+ ?.filter((property) => !property.formula && property.type !== 'collection')
344
+ .map((property) => property.id) ?? []),
345
+ });
346
+ if (actionType === 'delete') {
347
+ setRelatedInstances((prevInstances) => prevInstances.filter((instance) => instance.id !== instanceId));
348
+ }
349
+ else {
350
+ setRelatedInstances((prevInstances) => prevInstances.map((i) => (i.id === instance?.id ? instance : i)));
351
+ }
352
+ setOpenDialog(false);
353
+ setDialogType(undefined);
354
+ setSelectedRow(undefined);
355
+ }
356
+ catch (err) {
357
+ setSnackbarError({
358
+ showAlert: true,
359
+ message: retrieveCustomErrorMessage(err) ??
360
+ `An error occurred while ${actionType === 'delete' ? ' deleting' : ' updating'} an instance`,
361
+ isError: true,
362
+ });
363
+ }
364
+ }
365
+ };
366
+ const retrieveViewLayout = () => {
367
+ let properties = [];
368
+ if (tableViewLayout?.properties?.length) {
369
+ for (const prop of tableViewLayout.properties) {
370
+ const propertyId = prop.id.split('.')[0];
371
+ const property = relatedObject?.properties?.find((p) => p.id === propertyId);
372
+ if (property) {
373
+ if ((property.type === 'object' && property.id !== property.relatedPropertyId) ||
374
+ property.type === 'address' ||
375
+ property.type === 'user') {
376
+ properties.push({
377
+ ...property,
378
+ id: ['user', 'object'].includes(property.type) && !prop.id.endsWith('.name')
379
+ ? `${prop.id}.name`
380
+ : prop.id,
381
+ name: property.type === 'address'
382
+ ? `${property.name} - ${startCase(prop.id.split('.')[1])}`
383
+ : property.name,
384
+ });
385
+ }
386
+ else {
387
+ properties.push(property);
388
+ }
389
+ }
390
+ }
391
+ }
392
+ else {
393
+ properties =
394
+ relatedObject?.properties
395
+ ?.filter((prop) => !['address', 'image', 'collection'].includes(prop.type))
396
+ .map((prop) => ({
397
+ ...prop,
398
+ id: prop.type === 'object' || prop.type === 'user' ? `${prop.id}.name` : prop.id,
399
+ })) ?? [];
400
+ }
401
+ return properties;
402
+ };
403
+ const columns = retrieveViewLayout();
404
+ const getValue = (relatedInstance, propertyId, propertyType) => {
405
+ const value = get(relatedInstance, propertyId);
406
+ // If the date-like value is empty then there is no need to format.
407
+ if (!value) {
408
+ return value;
409
+ }
410
+ if (propertyType === 'date') {
411
+ return DateTime.fromISO(value).toLocaleString(DateTime.DATE_SHORT);
412
+ }
413
+ if (propertyType === 'date-time') {
414
+ return DateTime.fromISO(value).toLocaleString(DateTime.DATETIME_SHORT);
415
+ }
416
+ if (propertyType === 'time') {
417
+ return DateTime.fromISO(value).toLocaleString(DateTime.TIME_SIMPLE);
418
+ }
419
+ if (propertyType === 'criteria' && typeof value === 'object') {
420
+ const property = relatedObject?.properties?.find((p) => p.id === propertyId);
421
+ return getReadableQuery(value, criteriaObjects.find((o) => o.id === property?.objectId)?.properties ?? []);
422
+ }
423
+ return value;
424
+ };
425
+ return loading ? (React.createElement(React.Fragment, null,
426
+ React.createElement(Skeleton, null),
427
+ React.createElement(Skeleton, null),
428
+ React.createElement(Skeleton, null))) : (React.createElement(React.Fragment, null,
429
+ React.createElement(Box, { sx: { padding: '10px 0' } },
430
+ !relatedInstances?.length ? (!error ? (React.createElement(Typography, { sx: { margin: '-10px 0', color: 'rgb(114 124 132)', fontSize: '14px' } }, "No items added")) : (React.createElement(ErrorComponent, null))) : smallerThanMd ? (React.createElement(React.Fragment, null, relatedInstances?.map((relatedInstance, index) => (React.createElement(Accordion, { key: relatedInstance.id, sx: {
431
+ border: '1px solid #dbe0e4',
432
+ borderTop: index === 0 ? undefined : 'none',
433
+ boxShadow: 'none',
434
+ '&:before': {
435
+ display: 'none',
436
+ },
437
+ marginBottom: 0,
438
+ '&.Mui-expanded': {
439
+ margin: '0px',
440
+ },
441
+ } },
442
+ React.createElement(AccordionSummary, { sx: {
443
+ '&.Mui-expanded': {
444
+ borderBottom: '1px solid #dbe0e4',
445
+ minHeight: '44px',
446
+ borderBottomLeftRadius: '0px',
447
+ borderBottomRightRadius: '0px',
448
+ margin: '0px',
449
+ },
450
+ minHeight: '48px',
451
+ maxHeight: '48px',
452
+ backgroundColor: '#f9fafb',
453
+ // MUI accordion summaries have different border radius for the first and last item
454
+ borderTopLeftRadius: index === 0 ? '3px' : undefined,
455
+ borderTopRightRadius: index === 0 ? '3px' : undefined,
456
+ borderBottomRightRadius: index === relatedInstances.length - 1 ? '3px' : undefined,
457
+ borderBottomLeftRadius: index === relatedInstances.length - 1 ? '3px' : undefined,
458
+ }, expandIcon: React.createElement(ExpandMoreOutlined, { fontSize: "medium" }) },
459
+ React.createElement(Box, { sx: {
460
+ display: 'flex',
461
+ alignItems: 'center',
462
+ width: '100%',
463
+ justifyContent: 'space-between',
464
+ } }, React.createElement(Box, { sx: { display: 'flex', alignItems: 'center', marginY: '10px' } },
465
+ React.createElement(Typography, { sx: { fontSize: '16px', fontWeight: '600', lineHight: '24px' } }, getValue(relatedInstance, 'name', 'string'))))),
466
+ React.createElement(AccordionDetails, null,
467
+ React.createElement(Box, null, columns
468
+ ?.filter((prop) => prop.id !== 'name')
469
+ ?.map((prop) => (React.createElement(Box, { key: prop.id, sx: { mb: 1, display: 'flex', alignItems: 'center' } },
470
+ React.createElement(Typography, { component: "span", sx: { color: '#637381', marginRight: '3px' } },
471
+ prop.name,
472
+ ":"),
473
+ prop.type === 'document' ? (React.createElement(DocumentViewerCell, { instance: relatedInstance, propertyId: prop.id, setSnackbarError: setSnackbarError })) : (React.createElement(Typography, null,
474
+ getValue(relatedInstance, prop.id, prop.type),
475
+ prop.type === 'user' &&
476
+ users?.find((user) => get(relatedInstance, `${prop.id.split('.')[0]}.id`) === user.id)?.status === 'Inactive' &&
477
+ ' (Inactive)')))))),
478
+ canUpdateProperty && (React.createElement(Box, { sx: { mt: 2, display: 'flex', gap: 1 } },
479
+ React.createElement(IconButton, { onClick: () => editRow(relatedInstance.id) },
480
+ React.createElement(Tooltip, { title: "Edit" },
481
+ React.createElement(Edit, null))),
482
+ React.createElement(IconButton, { onClick: () => deleteRow(relatedInstance.id) },
483
+ React.createElement(Tooltip, { title: "Delete" },
484
+ React.createElement(TrashCan, { sx: { ':hover': { color: '#A12723' } } }))))))))))) : (React.createElement(TableContainer, { sx: {
485
+ borderRadius: '6px',
486
+ border: '1px solid #919EAB3D',
487
+ boxShadow: 'none',
488
+ maxHeight: '70vh',
489
+ } },
490
+ React.createElement(Table, { stickyHeader: true, sx: { minWidth: 650 } },
491
+ React.createElement(TableHead, { sx: { backgroundColor: '#F4F6F8' } },
492
+ React.createElement(TableRow, null,
493
+ columns?.map((prop) => React.createElement(TableCell, { sx: styles.tableCell }, prop.name)),
494
+ canUpdateProperty && React.createElement(TableCell, { sx: { ...styles.tableCell, width: '80px' } }))),
495
+ React.createElement(TableBody, null, relatedInstances?.map((relatedInstance, index) => (React.createElement(TableRow, { key: relatedInstance.id },
496
+ columns?.map((prop) => {
497
+ return (React.createElement(TableCell, { sx: { fontSize: '16px' } }, prop.type === 'document' ? (React.createElement(DocumentViewerCell, { instance: relatedInstance, propertyId: prop.id, setSnackbarError: setSnackbarError })) : (React.createElement(Typography, { key: prop.id, sx: prop.id === 'name'
498
+ ? {
499
+ '&:hover': {
500
+ textDecoration: 'underline',
501
+ cursor: 'pointer',
502
+ },
503
+ }
504
+ : {}, onClick: !!fieldDefinition.updatedForm && // TODO: replace with the entries update form
505
+ canUpdateProperty &&
506
+ prop.id === 'name'
507
+ ? () => editRow(relatedInstance.id)
508
+ : undefined },
509
+ getValue(relatedInstance, prop.id, prop.type),
510
+ prop.type === 'user' &&
511
+ users?.find((user) => get(relatedInstance, `${prop.id.split('.')[0]}.id`) === user.id)?.status === 'Inactive' && (React.createElement("span", null, ' (Inactive)'))))));
512
+ }),
513
+ canUpdateProperty && (React.createElement(TableCell, { sx: { width: '80px' } },
514
+ !!fieldDefinition.updateForm && ( // TODO: replace with the entries update form
515
+ React.createElement(IconButton, { "aria-label": `edit-collection-instance-${index}`, onClick: () => editRow(relatedInstance.id) },
516
+ React.createElement(Tooltip, { title: "Edit" },
517
+ React.createElement(Edit, null)))),
518
+ React.createElement(IconButton, { "aria-label": `delete-collection-instance-${index}`, onClick: () => deleteRow(relatedInstance.id) },
519
+ React.createElement(Tooltip, { title: "Delete" },
520
+ React.createElement(TrashCan, { sx: { ':hover': { color: '#A12723' } } })))))))))))),
521
+ hasCreateAction && (React.createElement(Button, { variant: "contained", sx: styles.addButton, onClick: addRow, "aria-label": 'Add' }, "Add"))),
522
+ relatedObject && openDialog && (React.createElement(ActionDialog, { object: relatedObject, open: openDialog, onClose: () => setOpenDialog(false), instanceInput: relatedInstances.find((i) => i.id === selectedRow) ?? {}, handleSubmit: save, action: relatedObject?.actions?.find((a) => a.id ===
523
+ (dialogType === 'create' ? '_create' : dialogType === 'update' ? '_update' : '_delete')), instanceId: selectedRow, relatedParameter: fieldDefinition })),
524
+ React.createElement(Snackbar, { open: snackbarError.showAlert, handleClose: () => setSnackbarError({ isError: snackbarError.isError, showAlert: false }), message: snackbarError.message, error: snackbarError.isError })));
525
+ };
526
+ export default RepeatableField;
@@ -1,4 +1,4 @@
1
- import { Columns, FormEntry, Property, Sections } from '@evoke-platform/context';
1
+ import { Columns, FormEntry, InputParameter, Obj, ObjectInstance, Property, Sections } from '@evoke-platform/context';
2
2
  import { LocalDateTime } from '@js-joda/core';
3
3
  import { FieldValues } from 'react-hook-form';
4
4
  import { AutocompleteOption } from '../../../core';
@@ -20,3 +20,16 @@ export declare const getEntryId: (entry: FormEntry) => string | undefined;
20
20
  export declare function getPrefixedUrl(url: string): string;
21
21
  export declare const isOptionEqualToValue: (option: AutocompleteOption | string, value: unknown) => boolean;
22
22
  export declare function addressProperties(addressProperty: Property): Property[];
23
+ export declare const normalizeDates: (instance: ObjectInstance, evokeObject?: Obj) => ObjectInstance;
24
+ export declare const transformToWhere: (mongoQuery: Record<string, unknown>) => Record<string, unknown>;
25
+ export declare const getMiddleObject: (fieldDefinition: InputParameter | Property, endObjectId: string, endObjectName: string, instance?: FieldValues) => {
26
+ [x: string]: {
27
+ id: any;
28
+ name: any;
29
+ };
30
+ } | undefined;
31
+ export declare const getMiddleObjectFilter: (fieldDefinition: InputParameter | Property, instanceId: string) => {
32
+ where: {
33
+ [x: string]: string;
34
+ };
35
+ };
@@ -1,6 +1,7 @@
1
1
  import { LocalDateTime } from '@js-joda/core';
2
2
  import jsonLogic from 'json-logic-js';
3
- import { get, isArray, isObject } from 'lodash';
3
+ import { get, isArray, isObject, transform } from 'lodash';
4
+ import { DateTime } from 'luxon';
4
5
  export const scrollIntoViewWithOffset = (el, offset, container) => {
5
6
  const elementRect = el.getBoundingClientRect();
6
7
  const containerRect = container ? container.getBoundingClientRect() : document.body.getBoundingClientRect();
@@ -170,3 +171,79 @@ export function addressProperties(addressProperty) {
170
171
  },
171
172
  ];
172
173
  }
174
+ export const normalizeDates = (instance, evokeObject) => {
175
+ const dateProps = ['date', 'date-time', 'time'];
176
+ const properties = evokeObject?.properties
177
+ ?.filter((property) => dateProps.includes(property.type))
178
+ .reduce((agg, property) => Object.assign(agg, { [property.id]: property.type }), {}) ?? {};
179
+ const propKeys = Object.keys(properties);
180
+ const updatedInstance = { ...instance };
181
+ Object.keys(instance).forEach((key) => {
182
+ // Ignore non-datelike and empty fields.
183
+ if (!propKeys.includes(key) || !instance[key]) {
184
+ return;
185
+ }
186
+ switch (properties[key]) {
187
+ case 'date':
188
+ // Casting here is valid because the value has already been
189
+ // determined to be a datelike field that is non-empty.
190
+ updatedInstance[key] = DateTime.fromISO(instance[key]).toLocaleString(DateTime.DATE_SHORT);
191
+ break;
192
+ case 'date-time':
193
+ // Casting here is valid because the value has already been
194
+ // determined to be a datelike field that is non-empty.
195
+ updatedInstance[key] = DateTime.fromISO(instance[key]).toLocaleString(DateTime.DATETIME_SHORT);
196
+ break;
197
+ case 'time':
198
+ // Casting here is valid because the value has already been
199
+ // determined to be a datelike field that is non-empty.
200
+ updatedInstance[key] = DateTime.fromISO(instance[key]).toLocaleString(DateTime.TIME_SIMPLE);
201
+ break;
202
+ }
203
+ });
204
+ return updatedInstance;
205
+ };
206
+ const OPERATOR_MAP = {
207
+ $and: 'and',
208
+ $or: 'or',
209
+ $eq: 'eq',
210
+ $ne: 'neq',
211
+ $lt: 'lt',
212
+ $lte: 'lte',
213
+ $gt: 'gt',
214
+ $gte: 'gte',
215
+ $in: 'inq',
216
+ $nin: 'nin',
217
+ $regex: 'regexp',
218
+ $exists: 'exists',
219
+ $not: 'not',
220
+ };
221
+ export const transformToWhere = (mongoQuery) => {
222
+ return transform(mongoQuery, (result, value, key) => {
223
+ const newKey = typeof key === 'string' && key.startsWith('$') ? OPERATOR_MAP[key] : key;
224
+ if (newKey === undefined) {
225
+ throw new Error(`Unsupported operator ${key}`);
226
+ }
227
+ result[newKey] = isObject(value) ? transformToWhere(value) : value;
228
+ });
229
+ };
230
+ export const getMiddleObject = (fieldDefinition, endObjectId, endObjectName, instance) => {
231
+ if (fieldDefinition.relatedPropertyId && fieldDefinition.manyToManyPropertyId) {
232
+ const middleObject = {
233
+ [fieldDefinition.relatedPropertyId]: {
234
+ id: instance?.id,
235
+ name: instance?.name,
236
+ },
237
+ [fieldDefinition.manyToManyPropertyId]: {
238
+ id: endObjectId,
239
+ name: endObjectName,
240
+ },
241
+ };
242
+ return middleObject;
243
+ }
244
+ };
245
+ export const getMiddleObjectFilter = (fieldDefinition, instanceId) => {
246
+ const filterProperty = `${fieldDefinition.relatedPropertyId}.id`;
247
+ const filter = { where: { [filterProperty]: instanceId } };
248
+ return filter;
249
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@evoke-platform/ui-components",
3
- "version": "1.6.0-dev.19",
3
+ "version": "1.6.0-dev.20",
4
4
  "description": "",
5
5
  "main": "dist/published/index.js",
6
6
  "module": "dist/published/index.js",
@@ -87,7 +87,7 @@
87
87
  "yalc": "^1.0.0-pre.53"
88
88
  },
89
89
  "peerDependencies": {
90
- "@evoke-platform/context": "^1.3.0-0",
90
+ "@evoke-platform/context": "^1.3.0-dev.6",
91
91
  "react": "^18.1.0",
92
92
  "react-dom": "^18.1.0"
93
93
  },