@evoke-platform/ui-components 1.13.0-dev.5 → 1.13.0-dev.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/published/components/custom/Form/FormComponents/DocumentComponent/Document.js +1 -1
- package/dist/published/components/custom/Form/FormComponents/DocumentComponent/DocumentList.js +6 -3
- package/dist/published/components/custom/FormV2/FormRenderer.d.ts +1 -1
- package/dist/published/components/custom/FormV2/FormRenderer.js +25 -27
- package/dist/published/components/custom/FormV2/FormRendererContainer.js +93 -86
- package/dist/published/components/custom/FormV2/components/ConditionalQueryClientProvider.d.ts +5 -0
- package/dist/published/components/custom/FormV2/components/ConditionalQueryClientProvider.js +21 -0
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/DropdownRepeatableField.js +86 -143
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/DropdownRepeatableFieldInput.d.ts +0 -2
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/DropdownRepeatableFieldInput.js +1 -4
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/RepeatableField.js +104 -184
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/Criteria.js +36 -49
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/Document.d.ts +3 -2
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/Document.js +51 -32
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/DocumentList.d.ts +4 -3
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/DocumentList.js +40 -38
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/UserProperty.js +17 -21
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/ObjectPropertyInput.js +95 -169
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/RelatedObjectInstance.d.ts +0 -2
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/RelatedObjectInstance.js +12 -6
- package/dist/published/components/custom/FormV2/components/RecursiveEntryRenderer.d.ts +2 -1
- package/dist/published/components/custom/FormV2/components/RecursiveEntryRenderer.js +39 -17
- package/dist/published/components/custom/FormV2/components/types.d.ts +6 -1
- package/dist/published/components/custom/FormV2/components/utils.d.ts +10 -11
- package/dist/published/components/custom/FormV2/components/utils.js +169 -93
- package/dist/published/components/custom/FormV2/tests/FormRenderer.test.js +48 -15
- package/dist/published/components/custom/FormV2/tests/FormRendererContainer.test.js +38 -46
- package/dist/published/components/custom/ViewDetailsV2/InstanceEntryRenderer.d.ts +2 -1
- package/dist/published/components/custom/ViewDetailsV2/InstanceEntryRenderer.js +38 -13
- package/dist/published/components/custom/ViewDetailsV2/ViewDetailsV2Container.js +7 -2
- package/package.json +3 -2
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import {
|
|
2
|
+
import { DocumentReference } from '../../types';
|
|
3
3
|
type DocumentListProps = {
|
|
4
|
-
handleChange?: (propertyId: string, value: (File |
|
|
4
|
+
handleChange?: (propertyId: string, value: (File | DocumentReference)[] | undefined) => void;
|
|
5
5
|
onAutosave?: (fieldId: string) => void | Promise<void>;
|
|
6
6
|
id: string;
|
|
7
|
+
fieldType?: 'document' | 'file';
|
|
7
8
|
canUpdateProperty: boolean;
|
|
8
|
-
value: (File |
|
|
9
|
+
value: (File | DocumentReference)[] | undefined;
|
|
9
10
|
setSnackbarError: (type: 'error' | 'success', message: string) => void;
|
|
10
11
|
};
|
|
11
12
|
export declare const DocumentList: (props: DocumentListProps) => React.JSX.Element;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useApiServices } from '@evoke-platform/context';
|
|
2
|
-
import {
|
|
2
|
+
import { useQuery } from '@tanstack/react-query';
|
|
3
3
|
import prettyBytes from 'pretty-bytes';
|
|
4
4
|
import React, { useEffect, useState } from 'react';
|
|
5
5
|
import { FileWithExtension, LaunchRounded, TrashCan, WarningRounded } from '../../../../../../icons';
|
|
@@ -24,29 +24,32 @@ const viewableFileTypes = [
|
|
|
24
24
|
'text/plain',
|
|
25
25
|
];
|
|
26
26
|
export const DocumentList = (props) => {
|
|
27
|
-
const { handleChange, onAutosave, id, canUpdateProperty, value: documents, setSnackbarError } = props;
|
|
27
|
+
const { handleChange, onAutosave, id, fieldType = 'document', canUpdateProperty, value: documents, setSnackbarError, } = props;
|
|
28
28
|
const apiServices = useApiServices();
|
|
29
29
|
const { fetchedOptions, setFetchedOptions, object, instance } = useFormContext();
|
|
30
30
|
// Determine property type once at component level
|
|
31
|
-
const
|
|
32
|
-
const isFileType = propertyType === 'file';
|
|
33
|
-
const [hasViewPermission, setHasViewPermission] = useState(fetchedOptions[`${id}ViewPermission`] ?? true);
|
|
31
|
+
const isFileType = fieldType === 'file';
|
|
34
32
|
// savedDocuments is either FileInstance[] or DocumentType[], never a mix
|
|
35
33
|
const [savedDocuments, setSavedDocuments] = useState(fetchedOptions[`${id}SavedDocuments`]);
|
|
36
34
|
useEffect(() => {
|
|
37
|
-
|
|
35
|
+
// Use documents prop (value) as the source of truth, not instance[id]
|
|
36
|
+
// This ensures newly uploaded files trigger a fetch even before they're saved to instance
|
|
37
|
+
const currentValue = documents;
|
|
38
38
|
if (currentValue?.length) {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
39
|
+
// Filter out File objects only - we want to fetch details for all DocumentReferences including unsaved ones
|
|
40
|
+
const currentDocumentIds = currentValue
|
|
41
|
+
.filter((doc) => !(doc instanceof File))
|
|
42
|
+
.map((doc) => doc.id);
|
|
43
|
+
if (currentDocumentIds.length) {
|
|
44
|
+
// Check if there are any document IDs that we haven't fetched yet
|
|
45
|
+
const savedDocumentIds = savedDocuments?.map((doc) => doc.id) ?? [];
|
|
46
|
+
const missingDocumentIds = currentDocumentIds.filter((id) => !savedDocumentIds.includes(id));
|
|
47
|
+
if (missingDocumentIds.length > 0) {
|
|
48
|
+
getDocuments(currentDocumentIds);
|
|
49
|
+
}
|
|
47
50
|
}
|
|
48
51
|
}
|
|
49
|
-
}, [
|
|
52
|
+
}, [documents, savedDocuments]);
|
|
50
53
|
useEffect(() => {
|
|
51
54
|
if (fetchedOptions[`${id}SavedDocuments`]) {
|
|
52
55
|
setSavedDocuments(fetchedOptions[`${id}SavedDocuments`]);
|
|
@@ -79,30 +82,31 @@ export const DocumentList = (props) => {
|
|
|
79
82
|
}
|
|
80
83
|
});
|
|
81
84
|
};
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
.
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
};
|
|
85
|
+
const { data: hasViewPermission = false } = useQuery({
|
|
86
|
+
queryKey: ['hasViewPermission', object?.id, instance?.id],
|
|
87
|
+
queryFn: async () => {
|
|
88
|
+
const endpoint = isFileType
|
|
89
|
+
? getPrefixedUrl(`/objects/sys__file/instances/checkAccess?action=read&field=content`)
|
|
90
|
+
: getPrefixedUrl(`/objects/${object.id}/instances/${instance.id}/documents/checkAccess?action=view`);
|
|
91
|
+
try {
|
|
92
|
+
const viewPermissionCheck = await apiServices.get(endpoint);
|
|
93
|
+
return viewPermissionCheck.result;
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
enabled: !!instance?.id && !!object?.id && !!instance?.[id]?.length,
|
|
100
|
+
staleTime: Infinity,
|
|
101
|
+
});
|
|
99
102
|
const isFile = (doc) => doc instanceof File;
|
|
103
|
+
const isUnsavedFile = (doc) => isFile(doc) || !!doc.unsaved;
|
|
100
104
|
const fileExists = (doc) => savedDocuments?.find((d) => d.id === doc.id);
|
|
101
105
|
const handleRemove = async (index) => {
|
|
102
106
|
const updatedDocuments = documents?.filter((_, i) => i !== index) ?? [];
|
|
103
107
|
const newValue = updatedDocuments.length === 0 ? undefined : updatedDocuments;
|
|
104
108
|
try {
|
|
105
|
-
handleChange
|
|
109
|
+
handleChange?.(id, newValue);
|
|
106
110
|
}
|
|
107
111
|
catch (error) {
|
|
108
112
|
console.error('Failed to update field:', error);
|
|
@@ -124,9 +128,7 @@ export const DocumentList = (props) => {
|
|
|
124
128
|
: savedDocuments?.find((savedDocument) => savedDocument.id === doc.id)?.contentType;
|
|
125
129
|
if (!isFile(doc)) {
|
|
126
130
|
try {
|
|
127
|
-
|
|
128
|
-
const propertyType = object?.properties?.find((p) => p.id === id)?.type;
|
|
129
|
-
const contentEndpoint = propertyType === 'file'
|
|
131
|
+
const contentEndpoint = isFileType
|
|
130
132
|
? getPrefixedUrl(`/files/${doc.id}/content`)
|
|
131
133
|
: getPrefixedUrl(`/objects/${object?.id}/instances/${instance?.id}/documents/${doc.id}/content`);
|
|
132
134
|
const documentResponse = await apiServices.get(contentEndpoint, { responseType: 'blob' });
|
|
@@ -189,10 +191,10 @@ export const DocumentList = (props) => {
|
|
|
189
191
|
} }, doc.name)),
|
|
190
192
|
React.createElement(Grid, { item: true, xs: 12 },
|
|
191
193
|
React.createElement(Typography, { sx: { fontSize: '12px', color: '#637381' } }, getDocumentSize(doc)))),
|
|
192
|
-
(
|
|
194
|
+
(isUnsavedFile(doc) || (hasViewPermission && !isFile(doc) && fileExists(doc))) && (React.createElement(Grid, { item: true },
|
|
193
195
|
React.createElement(IconButton, { "aria-label": "open document", sx: { ...styles.icon, marginRight: '16px' }, onClick: () => openDocument(index) },
|
|
194
196
|
React.createElement(LaunchRounded, { sx: { color: '#637381', fontSize: '22px' } })))),
|
|
195
|
-
!isFile(doc) && savedDocuments && !fileExists(doc) && (React.createElement(Chip, { label: "Deleted", sx: {
|
|
197
|
+
!isFile(doc) && !isUnsavedFile(doc) && savedDocuments && !fileExists(doc) && (React.createElement(Chip, { label: "Deleted", sx: {
|
|
196
198
|
marginRight: '16px',
|
|
197
199
|
backgroundColor: 'rgba(222, 48, 36, 0.16)',
|
|
198
200
|
color: '#A91813',
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
import { useApiServices } from '@evoke-platform/context';
|
|
2
2
|
import { Close, ExpandMore } from '@mui/icons-material';
|
|
3
|
+
import { useQuery } from '@tanstack/react-query';
|
|
3
4
|
import React, { useEffect, useState } from 'react';
|
|
4
5
|
import { useFormContext } from '../../../../../theme/hooks';
|
|
5
6
|
import { Autocomplete, IconButton, Paper, TextField, Typography } from '../../../../core';
|
|
6
7
|
import { getPrefixedUrl, isOptionEqualToValue } from '../utils';
|
|
7
8
|
const UserProperty = (props) => {
|
|
8
9
|
const { id, error, value, readOnly, hasDescription } = props;
|
|
9
|
-
const {
|
|
10
|
+
const { handleChange, onAutosave: onAutosave, fieldHeight } = useFormContext();
|
|
10
11
|
const [loadingOptions, setLoadingOptions] = useState(false);
|
|
11
12
|
const apiServices = useApiServices();
|
|
12
|
-
const [options, setOptions] = useState(
|
|
13
|
+
const [options, setOptions] = useState([]);
|
|
13
14
|
const [openOptions, setOpenOptions] = useState(false);
|
|
14
|
-
const [users, setUsers] = useState();
|
|
15
15
|
const [userValue, setUserValue] = useState();
|
|
16
16
|
useEffect(() => {
|
|
17
17
|
if (value && typeof value == 'object' && 'name' in value && 'id' in value) {
|
|
@@ -21,25 +21,21 @@ const UserProperty = (props) => {
|
|
|
21
21
|
setUserValue(undefined);
|
|
22
22
|
}
|
|
23
23
|
}, [value]);
|
|
24
|
+
const { data: users } = useQuery({
|
|
25
|
+
queryKey: ['users'],
|
|
26
|
+
queryFn: () => apiServices.get(getPrefixedUrl(`/users`)),
|
|
27
|
+
staleTime: Infinity,
|
|
28
|
+
meta: {
|
|
29
|
+
errorMessage: 'Error fetching users: ',
|
|
30
|
+
},
|
|
31
|
+
});
|
|
24
32
|
useEffect(() => {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
value: user.id,
|
|
32
|
-
})));
|
|
33
|
-
setFetchedOptions({
|
|
34
|
-
[`${id}Options`]: (userList ?? []).map((user) => ({
|
|
35
|
-
label: user.name,
|
|
36
|
-
value: user.id,
|
|
37
|
-
})),
|
|
38
|
-
});
|
|
39
|
-
setLoadingOptions(false);
|
|
40
|
-
});
|
|
41
|
-
}
|
|
42
|
-
}, [id]);
|
|
33
|
+
setOptions((users ?? []).map((user) => ({
|
|
34
|
+
label: user.name,
|
|
35
|
+
value: user.id,
|
|
36
|
+
})));
|
|
37
|
+
setLoadingOptions(false);
|
|
38
|
+
}, [users]);
|
|
43
39
|
async function handleChangeUserProperty(id, value) {
|
|
44
40
|
const updatedValue = typeof value?.value === 'string' ? { name: value.label, id: value.value } : null;
|
|
45
41
|
try {
|
|
@@ -1,53 +1,41 @@
|
|
|
1
1
|
import { useApiServices, useApp, useNavigate, } from '@evoke-platform/context';
|
|
2
|
+
import { useQuery } from '@tanstack/react-query';
|
|
2
3
|
import cleanDeep from 'clean-deep';
|
|
3
|
-
import { cloneDeep, debounce, isEmpty,
|
|
4
|
+
import { cloneDeep, debounce, isEmpty, isNil } from 'lodash';
|
|
4
5
|
import Handlebars from 'no-eval-handlebars';
|
|
5
|
-
import React, {
|
|
6
|
+
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
|
6
7
|
import { Close } from '../../../../../../icons';
|
|
7
8
|
import { useFormContext } from '../../../../../../theme/hooks';
|
|
8
9
|
import { Autocomplete, Button, IconButton, Link, ListItem, Paper, Snackbar, TextField, Tooltip, Typography, } from '../../../../../core';
|
|
9
10
|
import { Box } from '../../../../../layout';
|
|
10
|
-
import { getDefaultPages, getPrefixedUrl, transformToWhere } from '../../utils';
|
|
11
|
+
import { getDefaultPages, getPrefixedUrl, transformToWhere, useFormById } from '../../utils';
|
|
11
12
|
import RelatedObjectInstance from './RelatedObjectInstance';
|
|
12
13
|
const ObjectPropertyInput = (props) => {
|
|
13
14
|
const { id, fieldDefinition, readOnly, error, mode, displayOption, filter, defaultValueCriteria, sortBy, orderBy, isModal, initialValue, viewLayout, hasDescription, createActionId, formId, relatedObjectId, } = props;
|
|
14
15
|
const { fetchedOptions, setFetchedOptions, fieldHeight, handleChange: handleChangeObjectField, onAutosave: onAutosave, instance, } = useFormContext();
|
|
15
16
|
const { defaultPages, findDefaultPageSlugFor } = useApp();
|
|
17
|
+
const [debouncedDropdownInput, setDebouncedDropdownInput] = useState();
|
|
16
18
|
const [selectedInstance, setSelectedInstance] = useState(initialValue || undefined);
|
|
17
19
|
const [openCreateDialog, setOpenCreateDialog] = useState(false);
|
|
18
|
-
const [loadingOptions, setLoadingOptions] = useState(false);
|
|
19
|
-
const [navigationSlug, setNavigationSlug] = useState(fetchedOptions[`${id}NavigationSlug`]);
|
|
20
|
-
const [relatedObject, setRelatedObject] = useState(fetchedOptions[`${id}RelatedObject`]);
|
|
21
20
|
const [dropdownInput, setDropdownInput] = useState();
|
|
22
21
|
const [openOptions, setOpenOptions] = useState(false);
|
|
23
|
-
const [hasFetched, setHasFetched] = useState(fetchedOptions[`${id}OptionsHaveFetched`] || false);
|
|
24
|
-
const [options, setOptions] = useState(fetchedOptions[`${id}Options`] || []);
|
|
25
22
|
const [layout, setLayout] = useState(fetchedOptions[`${id}ViewLayout`]);
|
|
26
|
-
const [form, setForm] = useState();
|
|
27
23
|
const [snackbarError, setSnackbarError] = useState({
|
|
28
24
|
showAlert: false,
|
|
29
25
|
isError: true,
|
|
30
26
|
});
|
|
27
|
+
const { data: relatedObject } = useQuery({
|
|
28
|
+
queryKey: [relatedObjectId, 'sanitized'],
|
|
29
|
+
queryFn: () => apiServices.get(getPrefixedUrl(`/objects/${relatedObjectId}/effective?sanitizedVersion=true`)),
|
|
30
|
+
enabled: !!relatedObjectId,
|
|
31
|
+
staleTime: Infinity,
|
|
32
|
+
});
|
|
31
33
|
const action = relatedObject?.actions?.find((action) => action.id === createActionId);
|
|
32
34
|
const apiServices = useApiServices();
|
|
33
35
|
const navigateTo = useNavigate();
|
|
34
|
-
const updatedCriteria = useMemo(() => {
|
|
35
|
-
let criteria = filter ? { where: transformToWhere(filter) } : undefined;
|
|
36
|
-
if (dropdownInput) {
|
|
37
|
-
const nameQuery = transformToWhere({
|
|
38
|
-
name: {
|
|
39
|
-
like: dropdownInput,
|
|
40
|
-
options: 'i',
|
|
41
|
-
},
|
|
42
|
-
});
|
|
43
|
-
criteria = {
|
|
44
|
-
...criteria,
|
|
45
|
-
where: criteria?.where ? { and: [criteria.where, nameQuery] } : nameQuery,
|
|
46
|
-
};
|
|
47
|
-
}
|
|
48
|
-
return criteria;
|
|
49
|
-
}, [filter, dropdownInput]);
|
|
50
36
|
const listboxRef = useRef(null);
|
|
37
|
+
const formIdToFetch = formId || action?.defaultFormId;
|
|
38
|
+
const { data: form } = useFormById(formIdToFetch ?? '', apiServices, 'Error fetching form: ');
|
|
51
39
|
useEffect(() => {
|
|
52
40
|
if (relatedObject) {
|
|
53
41
|
let defaultViewLayout;
|
|
@@ -85,143 +73,102 @@ const ObjectPropertyInput = (props) => {
|
|
|
85
73
|
}
|
|
86
74
|
}
|
|
87
75
|
}, [displayOption, relatedObject, viewLayout]);
|
|
76
|
+
const debouncedSetDropdownInput = useMemo(() => debounce((value) => {
|
|
77
|
+
setDebouncedDropdownInput(value);
|
|
78
|
+
}, 200), []);
|
|
88
79
|
useEffect(() => {
|
|
89
|
-
|
|
90
|
-
|
|
80
|
+
debouncedSetDropdownInput(dropdownInput);
|
|
81
|
+
return () => {
|
|
82
|
+
debouncedSetDropdownInput.cancel();
|
|
83
|
+
};
|
|
84
|
+
}, [dropdownInput, debouncedSetDropdownInput]);
|
|
85
|
+
const updatedCriteria = useMemo(() => {
|
|
86
|
+
let criteria = filter ? { where: transformToWhere(filter) } : undefined;
|
|
87
|
+
if (debouncedDropdownInput) {
|
|
88
|
+
const nameQuery = transformToWhere({
|
|
89
|
+
name: {
|
|
90
|
+
like: debouncedDropdownInput,
|
|
91
|
+
options: 'i',
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
criteria = {
|
|
95
|
+
...criteria,
|
|
96
|
+
where: criteria?.where ? { and: [criteria.where, nameQuery] } : nameQuery,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
return criteria;
|
|
100
|
+
}, [filter, debouncedDropdownInput]);
|
|
101
|
+
const { data: defaultInstances, isLoading: isLoadingDefaultInstances } = useQuery({
|
|
102
|
+
queryKey: ['defaultInstances', relatedObjectId, updatedCriteria],
|
|
103
|
+
queryFn: async () => {
|
|
91
104
|
const updatedFilter = cleanDeep({
|
|
92
105
|
where: transformToWhere({ $and: [defaultValueCriteria, updatedCriteria?.where ?? {}] }),
|
|
93
106
|
order: orderBy && sortBy ? encodeURIComponent(sortBy + ' ' + orderBy) : undefined,
|
|
94
107
|
limit: 1,
|
|
95
108
|
});
|
|
96
109
|
if (updatedFilter.where) {
|
|
97
|
-
|
|
98
|
-
apiServices.get(getPrefixedUrl(`/objects/${relatedObjectId}/instances?filter=${encodeURIComponent(JSON.stringify(updatedFilter))}`), async (error, instances) => {
|
|
99
|
-
if (error) {
|
|
100
|
-
console.error(error);
|
|
101
|
-
setLoadingOptions(false);
|
|
102
|
-
}
|
|
103
|
-
if (instances && instances.length > 0) {
|
|
104
|
-
setSelectedInstance(instances[0]);
|
|
105
|
-
try {
|
|
106
|
-
handleChangeObjectField && (await handleChangeObjectField(id, instances[0]));
|
|
107
|
-
}
|
|
108
|
-
catch (error) {
|
|
109
|
-
console.error('Failed to update field:', error);
|
|
110
|
-
setLoadingOptions(false);
|
|
111
|
-
return;
|
|
112
|
-
}
|
|
113
|
-
try {
|
|
114
|
-
await onAutosave?.(id);
|
|
115
|
-
}
|
|
116
|
-
catch (error) {
|
|
117
|
-
console.error('Autosave failed:', error);
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
setLoadingOptions(false);
|
|
121
|
-
});
|
|
110
|
+
return apiServices.get(getPrefixedUrl(`/objects/${relatedObjectId}/instances?filter=${encodeURIComponent(JSON.stringify(updatedFilter))}`));
|
|
122
111
|
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
(fetchedOptions?.[`${id}Options`]).length === 0) &&
|
|
128
|
-
!hasFetched) ||
|
|
129
|
-
!isEqual(fetchedOptions?.[`${id}UpdatedCriteria`], updatedCriteria)) {
|
|
130
|
-
setFetchedOptions({ [`${id}UpdatedCriteria`]: updatedCriteria });
|
|
131
|
-
setLoadingOptions(true);
|
|
132
|
-
const updatedFilter = cloneDeep(updatedCriteria) || {};
|
|
133
|
-
updatedFilter.limit = 100;
|
|
134
|
-
const { propertyId, direction } = layout?.sort ?? {
|
|
135
|
-
propertyId: 'name',
|
|
136
|
-
direction: 'asc',
|
|
137
|
-
};
|
|
138
|
-
updatedFilter.order = `${propertyId} ${direction}`;
|
|
139
|
-
apiServices.get(getPrefixedUrl(`/objects/${relatedObjectId}/instances?filter=${JSON.stringify(updatedFilter)}`), (error, instances) => {
|
|
140
|
-
if (error) {
|
|
141
|
-
console.error(error);
|
|
142
|
-
setLoadingOptions(false);
|
|
143
|
-
}
|
|
144
|
-
if (instances) {
|
|
145
|
-
setOptions(instances);
|
|
146
|
-
setLoadingOptions(false);
|
|
147
|
-
// so if you go off a section too quickly and it doesn't fetch it re-fetches but doesn't cause an infinite loop
|
|
148
|
-
setHasFetched(true);
|
|
149
|
-
}
|
|
150
|
-
});
|
|
151
|
-
}
|
|
152
|
-
}, [
|
|
153
|
-
relatedObjectId,
|
|
154
|
-
updatedCriteria,
|
|
155
|
-
layout,
|
|
156
|
-
fetchedOptions?.[`${id}Options`],
|
|
157
|
-
fetchedOptions?.[`${id}UpdatedCriteria`],
|
|
158
|
-
hasFetched,
|
|
159
|
-
id,
|
|
160
|
-
]);
|
|
161
|
-
const debouncedGetDropdownOptions = useCallback(debounce(getDropdownOptions, 200), [getDropdownOptions]);
|
|
162
|
-
useEffect(() => {
|
|
163
|
-
if (displayOption === 'dropdown') {
|
|
164
|
-
debouncedGetDropdownOptions();
|
|
165
|
-
return () => debouncedGetDropdownOptions.cancel();
|
|
166
|
-
}
|
|
167
|
-
}, [displayOption, debouncedGetDropdownOptions]);
|
|
168
|
-
useEffect(() => {
|
|
169
|
-
setSelectedInstance(initialValue);
|
|
170
|
-
}, [initialValue]);
|
|
112
|
+
},
|
|
113
|
+
enabled: !isEmpty(defaultValueCriteria) && !selectedInstance && (!instance || !instance[id]),
|
|
114
|
+
staleTime: Infinity,
|
|
115
|
+
});
|
|
171
116
|
useEffect(() => {
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
let evokeForm;
|
|
178
|
-
if (formId || action?.defaultFormId) {
|
|
179
|
-
evokeForm = await apiServices.get(getPrefixedUrl(`/forms/${formId || action?.defaultFormId}`));
|
|
117
|
+
if (defaultInstances?.[0]) {
|
|
118
|
+
setSelectedInstance(defaultInstances[0]);
|
|
119
|
+
(async () => {
|
|
120
|
+
try {
|
|
121
|
+
handleChangeObjectField && (await handleChangeObjectField(id, defaultInstances[0]));
|
|
180
122
|
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
[`${id}Form`]: evokeForm,
|
|
185
|
-
});
|
|
123
|
+
catch (error) {
|
|
124
|
+
console.error('Failed to update field:', error);
|
|
125
|
+
return;
|
|
186
126
|
}
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
console.error('Error fetching form:', error);
|
|
190
|
-
}
|
|
191
|
-
};
|
|
192
|
-
fetchForm();
|
|
193
|
-
}, [action, formId, id, apiServices, fetchedOptions]);
|
|
194
|
-
useEffect(() => {
|
|
195
|
-
if (!fetchedOptions[`${id}RelatedObject`]) {
|
|
196
|
-
apiServices.get(getPrefixedUrl(`/objects/${relatedObjectId}/effective?sanitizedVersion=true`), (error, object) => {
|
|
197
|
-
if (error) {
|
|
198
|
-
console.error(error);
|
|
127
|
+
try {
|
|
128
|
+
await onAutosave?.(id);
|
|
199
129
|
}
|
|
200
|
-
|
|
201
|
-
|
|
130
|
+
catch (error) {
|
|
131
|
+
console.error('Autosave failed:', error);
|
|
202
132
|
}
|
|
203
|
-
});
|
|
133
|
+
})();
|
|
204
134
|
}
|
|
205
|
-
}, [
|
|
135
|
+
}, [defaultInstances]);
|
|
136
|
+
// Construct filter from debounced criteria
|
|
137
|
+
const updatedFilter = useMemo(() => {
|
|
138
|
+
const filter = cloneDeep(updatedCriteria) || {};
|
|
139
|
+
filter.limit = 100;
|
|
140
|
+
const { propertyId, direction } = layout?.sort ?? {
|
|
141
|
+
propertyId: 'name',
|
|
142
|
+
direction: 'asc',
|
|
143
|
+
};
|
|
144
|
+
filter.order = `${propertyId} ${direction}`;
|
|
145
|
+
return filter;
|
|
146
|
+
}, [updatedCriteria, layout]);
|
|
147
|
+
const {
|
|
148
|
+
// Sets the default value of options to an empty array
|
|
149
|
+
data: options = [], isLoading: isLoadingOptions, } = useQuery({
|
|
150
|
+
queryKey: ['dropdownOptions', relatedObjectId, updatedFilter],
|
|
151
|
+
queryFn: () => apiServices.get(getPrefixedUrl(`/objects/${relatedObjectId}/instances?filter=${JSON.stringify(updatedFilter)}`)),
|
|
152
|
+
staleTime: 300000,
|
|
153
|
+
// Keep old data while fetching new data
|
|
154
|
+
placeholderData: (previousData) => previousData,
|
|
155
|
+
});
|
|
206
156
|
useEffect(() => {
|
|
207
|
-
(
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
else {
|
|
217
|
-
// setting the nav slug to null if there is no default page for this object to avoid re-fetching
|
|
218
|
-
setFetchedOptions({
|
|
219
|
-
[`${id}NavigationSlug`]: null,
|
|
220
|
-
});
|
|
221
|
-
}
|
|
157
|
+
setSelectedInstance(initialValue);
|
|
158
|
+
}, [initialValue]);
|
|
159
|
+
const loadingOptions = isLoadingOptions || isLoadingDefaultInstances;
|
|
160
|
+
const { data: navigationSlug } = useQuery({
|
|
161
|
+
queryKey: ['navigationSlug', id, relatedObjectId],
|
|
162
|
+
queryFn: async () => {
|
|
163
|
+
const pages = await getDefaultPages([{ ...fieldDefinition, objectId: relatedObjectId }], defaultPages, findDefaultPageSlugFor);
|
|
164
|
+
if (relatedObjectId && pages[relatedObjectId]) {
|
|
165
|
+
return pages[relatedObjectId];
|
|
222
166
|
}
|
|
223
|
-
|
|
224
|
-
|
|
167
|
+
return null;
|
|
168
|
+
},
|
|
169
|
+
enabled: !!(relatedObjectId && defaultPages && findDefaultPageSlugFor),
|
|
170
|
+
staleTime: Infinity,
|
|
171
|
+
});
|
|
225
172
|
const handleClose = () => {
|
|
226
173
|
setOpenCreateDialog(false);
|
|
227
174
|
};
|
|
@@ -229,27 +176,6 @@ const ObjectPropertyInput = (props) => {
|
|
|
229
176
|
const template = Handlebars.compileAST(expression);
|
|
230
177
|
return instance ? template(instance) : undefined;
|
|
231
178
|
};
|
|
232
|
-
useEffect(() => {
|
|
233
|
-
if (relatedObject && !fetchedOptions[`${id}RelatedObject`]) {
|
|
234
|
-
setFetchedOptions({
|
|
235
|
-
[`${id}RelatedObject`]: relatedObject,
|
|
236
|
-
});
|
|
237
|
-
}
|
|
238
|
-
if (options &&
|
|
239
|
-
(!fetchedOptions[`${id}Options`] || fetchedOptions[`${id}Options`].length === 0) &&
|
|
240
|
-
hasFetched &&
|
|
241
|
-
!fetchedOptions[`${id}OptionsHaveFetched`]) {
|
|
242
|
-
setFetchedOptions({
|
|
243
|
-
[`${id}Options`]: options,
|
|
244
|
-
[`${id}OptionsHaveFetched`]: hasFetched,
|
|
245
|
-
});
|
|
246
|
-
}
|
|
247
|
-
if (navigationSlug && !fetchedOptions[`${id}NavigationSlug`]) {
|
|
248
|
-
setFetchedOptions({
|
|
249
|
-
[`${id}NavigationSlug`]: navigationSlug,
|
|
250
|
-
});
|
|
251
|
-
}
|
|
252
|
-
}, [relatedObject, options, hasFetched, navigationSlug, fetchedOptions, id]);
|
|
253
179
|
const dropdownOptions = [
|
|
254
180
|
...options.map((o) => ({ label: o.name, value: o.id })),
|
|
255
181
|
...(mode !== 'existingOnly' && relatedObject?.actions?.some((a) => a.id === createActionId)
|
|
@@ -422,7 +348,7 @@ const ObjectPropertyInput = (props) => {
|
|
|
422
348
|
}
|
|
423
349
|
}, selectOnFocus: false, onBlur: () => {
|
|
424
350
|
if (dropdownInput) {
|
|
425
|
-
|
|
351
|
+
setDropdownInput(undefined);
|
|
426
352
|
}
|
|
427
353
|
}, renderInput: (params) => (React.createElement(TextField, { ...params, placeholder: selectedInstance?.id || readOnly ? '' : 'Select', readOnly: !loadingOptions && !selectedInstance?.id && readOnly, onChange: (event) => setDropdownInput(event.target.value), sx: {
|
|
428
354
|
...(!loadingOptions && selectedInstance?.id
|
|
@@ -529,7 +455,7 @@ const ObjectPropertyInput = (props) => {
|
|
|
529
455
|
event.stopPropagation();
|
|
530
456
|
setOpenCreateDialog(true);
|
|
531
457
|
}, "aria-label": `Add` }, "Add")))),
|
|
532
|
-
React.createElement(RelatedObjectInstance, { open: openCreateDialog, title: form?.name ?? `Add ${fieldDefinition.name}`, handleClose: handleClose, setSelectedInstance: setSelectedInstance, relatedObject: relatedObject, id: id, mode: mode, displayOption: displayOption,
|
|
458
|
+
React.createElement(RelatedObjectInstance, { open: openCreateDialog, title: form?.name ?? `Add ${fieldDefinition.name}`, handleClose: handleClose, setSelectedInstance: setSelectedInstance, relatedObject: relatedObject, id: id, mode: mode, displayOption: displayOption, filter: updatedCriteria, layout: layout, formId: formIdToFetch, actionId: createActionId, setSnackbarError: setSnackbarError }),
|
|
533
459
|
React.createElement(Snackbar, { open: snackbarError.showAlert, handleClose: () => setSnackbarError({
|
|
534
460
|
isError: snackbarError.isError,
|
|
535
461
|
showAlert: false,
|
|
@@ -15,8 +15,6 @@ export type RelatedObjectInstanceProps = BaseProps & {
|
|
|
15
15
|
isError: boolean;
|
|
16
16
|
}>>;
|
|
17
17
|
displayOption?: 'dropdown' | 'dialogBox';
|
|
18
|
-
setOptions: (options: ObjectInstance[]) => void;
|
|
19
|
-
options: ObjectInstance[];
|
|
20
18
|
filter?: Record<string, unknown>;
|
|
21
19
|
layout?: TableViewLayout;
|
|
22
20
|
formId?: string;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useApiServices } from '@evoke-platform/context';
|
|
2
2
|
import { DialogActions } from '@mui/material';
|
|
3
|
+
import { useQueryClient } from '@tanstack/react-query';
|
|
3
4
|
import { isEmpty } from 'lodash';
|
|
4
5
|
import React, { useCallback, useRef, useState } from 'react';
|
|
5
6
|
import { Close } from '../../../../../../icons';
|
|
@@ -29,7 +30,7 @@ const styles = {
|
|
|
29
30
|
},
|
|
30
31
|
};
|
|
31
32
|
const RelatedObjectInstance = (props) => {
|
|
32
|
-
const { relatedObject, open, title, id, setSelectedInstance, handleClose, mode, displayOption, filter, layout, formId, actionId, setSnackbarError,
|
|
33
|
+
const { relatedObject, open, title, id, setSelectedInstance, handleClose, mode, displayOption, filter, layout, formId, actionId, setSnackbarError, } = props;
|
|
33
34
|
const { handleChange: handleChangeObjectField, onAutosave, richTextEditor, fieldHeight, width } = useFormContext();
|
|
34
35
|
const [selectedRow, setSelectedRow] = useState();
|
|
35
36
|
const [relationType, setRelationType] = useState(displayOption === 'dropdown' || mode === 'newOnly' ? 'new' : 'existing');
|
|
@@ -40,6 +41,7 @@ const RelatedObjectInstance = (props) => {
|
|
|
40
41
|
defaultWidth: width,
|
|
41
42
|
});
|
|
42
43
|
const { isXs, isSm } = breakpoints;
|
|
44
|
+
const queryClient = useQueryClient();
|
|
43
45
|
const linkExistingInstance = async () => {
|
|
44
46
|
if (selectedRow && handleChangeObjectField) {
|
|
45
47
|
setSelectedInstance(selectedRow);
|
|
@@ -93,7 +95,11 @@ const RelatedObjectInstance = (props) => {
|
|
|
93
95
|
message: 'New instance created',
|
|
94
96
|
isError: false,
|
|
95
97
|
});
|
|
96
|
-
|
|
98
|
+
// Clear option cache to then fetch newly created instance
|
|
99
|
+
queryClient.invalidateQueries({
|
|
100
|
+
queryKey: ['dropdownOptions', relatedObject.id],
|
|
101
|
+
exact: false,
|
|
102
|
+
});
|
|
97
103
|
onClose();
|
|
98
104
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
99
105
|
}
|
|
@@ -113,14 +119,14 @@ const RelatedObjectInstance = (props) => {
|
|
|
113
119
|
}
|
|
114
120
|
};
|
|
115
121
|
const shouldShowRadioButtons = displayOption !== 'dropdown' && mode !== 'existingOnly' && mode !== 'newOnly' && actionId;
|
|
116
|
-
const RadioButtons = () => shouldShowRadioButtons ? (React.createElement(RadioGroup, { row: true, "aria-label": "Relation Type", onChange: (event) => {
|
|
122
|
+
const RadioButtons = useCallback(() => shouldShowRadioButtons ? (React.createElement(RadioGroup, { row: true, "aria-label": "Relation Type", onChange: (event) => {
|
|
117
123
|
const { value } = event.target;
|
|
118
124
|
if (value === 'new' || value === 'existing') {
|
|
119
125
|
setRelationType(value);
|
|
120
126
|
}
|
|
121
127
|
}, value: relationType },
|
|
122
128
|
React.createElement(FormControlLabel, { value: "existing", control: React.createElement(Radio, { sx: { '&.Mui-checked': { color: 'primary' } } }), label: "Existing" }),
|
|
123
|
-
React.createElement(FormControlLabel, { value: "new", control: React.createElement(Radio, { sx: { '&.Mui-checked': { color: 'primary' } } }), label: "New" }))) : null;
|
|
129
|
+
React.createElement(FormControlLabel, { value: "new", control: React.createElement(Radio, { sx: { '&.Mui-checked': { color: 'primary' } } }), label: "New" }))) : null, [shouldShowRadioButtons, relationType]);
|
|
124
130
|
const DialogForm = useCallback(() => (React.createElement(FormRendererContainer, { formId: formId, display: { fieldHeight: fieldHeight ?? 'medium' }, actionId: actionId, objectId: relatedObject.id, onSubmit: createNewInstance, onDiscardChanges: onClose, onSubmitError: handleSubmitError, richTextEditor: richTextEditor, renderHeader: () => null, renderBody: (bodyProps) => (React.createElement(DialogContent, { sx: styles.dialogContent },
|
|
125
131
|
relationType === 'new' ? (React.createElement("div", { ref: validationErrorsRef }, !isEmpty(bodyProps.errors) && bodyProps.shouldShowValidationErrors ? (React.createElement(FormRenderer.ValidationErrors, { errors: bodyProps.errors, sx: {
|
|
126
132
|
my: isSm || isXs ? 2 : 3,
|
|
@@ -146,7 +152,7 @@ const RelatedObjectInstance = (props) => {
|
|
|
146
152
|
maxWidth: '950px',
|
|
147
153
|
width: '100%',
|
|
148
154
|
},
|
|
149
|
-
} },
|
|
155
|
+
} }, open && (React.createElement(React.Fragment, null,
|
|
150
156
|
React.createElement(DialogTitle, { sx: {
|
|
151
157
|
padding: 3,
|
|
152
158
|
borderBottom: '1px solid #e9ecef',
|
|
@@ -176,6 +182,6 @@ const RelatedObjectInstance = (props) => {
|
|
|
176
182
|
marginLeft: '8px',
|
|
177
183
|
width: '85px',
|
|
178
184
|
'&:hover': { boxShadow: 'none' },
|
|
179
|
-
}, onClick: linkExistingInstance, variant: "contained", disabled: !selectedRow, "aria-label": `Add` }, "Add")))))));
|
|
185
|
+
}, onClick: linkExistingInstance, variant: "contained", disabled: !selectedRow, "aria-label": `Add` }, "Add")))))))));
|
|
180
186
|
};
|
|
181
187
|
export default RelatedObjectInstance;
|