@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.
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/ActionDialog.d.ts +14 -0
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/ActionDialog.js +83 -0
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/DocumentViewerCell.d.ts +13 -0
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/DocumentViewerCell.js +140 -0
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/DropdownRepeatableField.d.ts +17 -0
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/DropdownRepeatableField.js +233 -0
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/DropdownRepeatableFieldInput.d.ts +40 -0
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/DropdownRepeatableFieldInput.js +95 -0
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/RepeatableField.d.ts +12 -0
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/RepeatableField.js +526 -0
- package/dist/published/components/custom/FormV2/components/utils.d.ts +14 -1
- package/dist/published/components/custom/FormV2/components/utils.js +78 -1
- package/package.json +2 -2
|
@@ -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.
|
|
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-
|
|
90
|
+
"@evoke-platform/context": "^1.3.0-dev.6",
|
|
91
91
|
"react": "^18.1.0",
|
|
92
92
|
"react-dom": "^18.1.0"
|
|
93
93
|
},
|