@evoke-platform/ui-components 1.13.0-dev.6 → 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/FormV2/FormRenderer.d.ts +1 -1
- package/dist/published/components/custom/FormV2/FormRenderer.js +25 -27
- package/dist/published/components/custom/FormV2/FormRendererContainer.js +70 -66
- 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.js +19 -36
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/DocumentList.js +16 -20
- 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 +38 -16
- package/dist/published/components/custom/FormV2/components/utils.d.ts +6 -4
- package/dist/published/components/custom/FormV2/components/utils.js +25 -25
- 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 +37 -12
- package/dist/published/components/custom/ViewDetailsV2/ViewDetailsV2Container.js +7 -2
- package/package.json +3 -2
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/// <reference types="react" />
|
|
2
|
-
import { Action, ApiServices, Column, Columns, EvokeForm, FormEntry, InputField, InputParameter, InputParameterReference, Obj, ObjectInstance, Property, Section, Sections, UserAccount } from '@evoke-platform/context';
|
|
2
|
+
import { Action, ApiServices, Column, Columns, EvokeForm, FormEntry, InputField, InputParameter, InputParameterReference, Obj, ObjectInstance, ObjWithRoot, PanelViewEntry, Property, Section, Sections, UserAccount } from '@evoke-platform/context';
|
|
3
3
|
import { LocalDateTime } from '@js-joda/core';
|
|
4
4
|
import { FieldErrors, FieldValues } from 'react-hook-form';
|
|
5
5
|
import { ObjectProperty } from '../../../../types';
|
|
@@ -41,7 +41,8 @@ export declare function getDefaultPages(parameters: InputParameter[], defaultPag
|
|
|
41
41
|
[x: string]: string;
|
|
42
42
|
}>;
|
|
43
43
|
export declare function updateCriteriaInputs(criteria: Record<string, unknown>, data: Record<string, unknown>, user?: UserAccount, instance?: Record<string, unknown>): Record<string, unknown>;
|
|
44
|
-
export declare
|
|
44
|
+
export declare const fetchMiddleObject: (fieldDefinition: InputParameter | Property, apiServices: ApiServices) => Promise<ObjWithRoot | undefined>;
|
|
45
|
+
export declare const fetchInitialMiddleObjectInstances: (apiServices: ApiServices, fieldDefinition: InputParameter | Property, instanceId: string) => Promise<ObjectInstance[]>;
|
|
45
46
|
export declare const getErrorCountForSection: (section: Section | Column, errors?: FieldErrors) => number;
|
|
46
47
|
export declare const propertyToParameter: (property: Property) => InputParameter;
|
|
47
48
|
export declare const propertyValidationToParameterValidation: (property: Property) => InputParameter['validation'];
|
|
@@ -84,7 +85,7 @@ export declare function formatSubmission(submission: FieldValues, apiServices?:
|
|
|
84
85
|
objectId?: string;
|
|
85
86
|
}, parameters?: InputParameter[]): Promise<FieldValues>;
|
|
86
87
|
export declare function filterEmptySections(entry: Sections | Columns, instance?: FieldValues, formData?: FieldValues): Sections | Columns | null;
|
|
87
|
-
export declare function assignIdsToSectionsAndRichText(entries: FormEntry[], object: Obj, parameters?: InputParameter[]): FormEntry[];
|
|
88
|
+
export declare function assignIdsToSectionsAndRichText(entries: FormEntry[] | PanelViewEntry[], object: Obj, parameters?: InputParameter[]): FormEntry[] | PanelViewEntry[];
|
|
88
89
|
/**
|
|
89
90
|
* Converts a plain text string to RTF format suitable for a RichTextEditor.
|
|
90
91
|
*
|
|
@@ -98,5 +99,6 @@ export declare function assignIdsToSectionsAndRichText(entries: FormEntry[], obj
|
|
|
98
99
|
* This ensures that any plain text input will be safely represented in RTF without losing formatting or characters.
|
|
99
100
|
*/
|
|
100
101
|
export declare function plainTextToRtf(plainText: string): string;
|
|
101
|
-
export declare function getFieldDefinition(entry: FormEntry, object: Obj, parameters?: InputParameter[]): InputParameter | Property | undefined;
|
|
102
|
+
export declare function getFieldDefinition(entry: FormEntry | PanelViewEntry, object: Obj, parameters?: InputParameter[]): InputParameter | Property | undefined;
|
|
102
103
|
export declare function obfuscateValue(value: unknown, property?: Partial<Property> | Partial<ObjectProperty>): unknown;
|
|
104
|
+
export declare function useFormById(formId: string, apiServices: ApiServices, errorMessage?: string): import("@tanstack/react-query/build/legacy/types").UseQueryResult<EvokeForm, Error>;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { LocalDateTime } from '@js-joda/core';
|
|
2
|
+
import { useQuery } from '@tanstack/react-query';
|
|
2
3
|
import { jsonLogic } from 'json-logic-js-graphql';
|
|
3
4
|
import { get, isArray, isEmpty, isObject, omit, pick, startCase, transform } from 'lodash';
|
|
4
5
|
import { DateTime } from 'luxon';
|
|
@@ -350,31 +351,19 @@ export function updateCriteriaInputs(criteria, data, user, instance) {
|
|
|
350
351
|
},
|
|
351
352
|
}));
|
|
352
353
|
}
|
|
353
|
-
export async
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
const fetchedInitialMiddleObjectInstances = await apiServices.get(getPrefixedUrl(`/objects/${fieldDefinition.objectId}/instances`), {
|
|
367
|
-
params: { filter: JSON.stringify(filter) },
|
|
368
|
-
});
|
|
369
|
-
setFetchedOptions({
|
|
370
|
-
[`${fieldDefinition.id}InitialMiddleObjectInstances`]: fetchedInitialMiddleObjectInstances,
|
|
371
|
-
});
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
catch (error) {
|
|
375
|
-
console.error('Error fetching collection data:', error);
|
|
376
|
-
}
|
|
377
|
-
}
|
|
354
|
+
export const fetchMiddleObject = async (fieldDefinition, apiServices) => {
|
|
355
|
+
return await apiServices.get(getPrefixedUrl(`/objects/${fieldDefinition.objectId}/effective?sanitizedVersion=true`), {
|
|
356
|
+
params: {
|
|
357
|
+
filter: { fields: ['properties', 'actions', 'rootObjectId'] },
|
|
358
|
+
},
|
|
359
|
+
});
|
|
360
|
+
};
|
|
361
|
+
export const fetchInitialMiddleObjectInstances = async (apiServices, fieldDefinition, instanceId) => {
|
|
362
|
+
const filter = getMiddleObjectFilter(fieldDefinition, instanceId);
|
|
363
|
+
return await apiServices.get(getPrefixedUrl(`/objects/${fieldDefinition.objectId}/instances`), {
|
|
364
|
+
params: { filter: JSON.stringify(filter) },
|
|
365
|
+
});
|
|
366
|
+
};
|
|
378
367
|
export const getErrorCountForSection = (section, errors) => {
|
|
379
368
|
const entries = section.entries || [];
|
|
380
369
|
return isArray(section.entries)
|
|
@@ -958,3 +947,14 @@ function applyMaskToObfuscatedValue(value, mask) {
|
|
|
958
947
|
}
|
|
959
948
|
return maskedValue;
|
|
960
949
|
}
|
|
950
|
+
export function useFormById(formId, apiServices, errorMessage) {
|
|
951
|
+
return useQuery({
|
|
952
|
+
queryKey: ['form', formId],
|
|
953
|
+
enabled: formId !== '_auto_' && !!formId,
|
|
954
|
+
staleTime: Infinity,
|
|
955
|
+
queryFn: () => apiServices.get(getPrefixedUrl(`/forms/${formId}`)),
|
|
956
|
+
meta: {
|
|
957
|
+
errorMessage,
|
|
958
|
+
},
|
|
959
|
+
});
|
|
960
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|
1
2
|
import { render as baseRender, screen, waitFor, within } from '@testing-library/react';
|
|
2
3
|
import userEvent from '@testing-library/user-event';
|
|
3
4
|
import { isEmpty, isEqual, set } from 'lodash';
|
|
@@ -14,12 +15,9 @@ global.ResizeObserver = class ResizeObserver {
|
|
|
14
15
|
unobserve() { }
|
|
15
16
|
disconnect() { }
|
|
16
17
|
};
|
|
17
|
-
const WithProviders = ({ children }) => {
|
|
18
|
-
return React.createElement(MemoryRouter, null, children);
|
|
19
|
-
};
|
|
20
|
-
const render = (ui, options) => baseRender(ui, { wrapper: WithProviders, ...options });
|
|
21
18
|
describe('FormRenderer', () => {
|
|
22
19
|
let server;
|
|
20
|
+
let queryClient;
|
|
23
21
|
beforeAll(() => {
|
|
24
22
|
server = setupServer(http.get('/api/data/objects/specialtyType/effective', () => HttpResponse.json(specialtyTypeObject)), http.get('/api/data/objects/specialtyType/effective', (req) => {
|
|
25
23
|
const sanitizedVersion = new URL(req.request.url).searchParams.get('sanitizedVersion');
|
|
@@ -65,12 +63,28 @@ describe('FormRenderer', () => {
|
|
|
65
63
|
}), http.get('/api/accessManagement/users', () => HttpResponse.json(users)));
|
|
66
64
|
server.listen();
|
|
67
65
|
});
|
|
68
|
-
|
|
69
|
-
|
|
66
|
+
beforeEach(() => {
|
|
67
|
+
// Create a fresh QueryClient for each test, need to pass `retry: false` to avoid retries interfering with error state tests
|
|
68
|
+
queryClient = new QueryClient({
|
|
69
|
+
defaultOptions: {
|
|
70
|
+
queries: {
|
|
71
|
+
retry: false,
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
});
|
|
70
75
|
});
|
|
71
76
|
afterEach(() => {
|
|
72
77
|
server.resetHandlers();
|
|
78
|
+
queryClient.clear();
|
|
73
79
|
});
|
|
80
|
+
afterEach(() => {
|
|
81
|
+
server.resetHandlers();
|
|
82
|
+
});
|
|
83
|
+
const WithProviders = ({ children }) => {
|
|
84
|
+
return (React.createElement(QueryClientProvider, { client: queryClient },
|
|
85
|
+
React.createElement(MemoryRouter, null, children)));
|
|
86
|
+
};
|
|
87
|
+
const render = (ui, options) => baseRender(ui, { wrapper: WithProviders, ...options });
|
|
74
88
|
describe('validation criteria', () => {
|
|
75
89
|
it(`filters related object field with validation criteria that references a related object's nested data`, async () => {
|
|
76
90
|
const user = userEvent.setup();
|
|
@@ -414,7 +428,7 @@ describe('FormRenderer', () => {
|
|
|
414
428
|
type: 'input',
|
|
415
429
|
parameterId: 'specialtyType',
|
|
416
430
|
display: {
|
|
417
|
-
label: '
|
|
431
|
+
label: 'Specialty Type',
|
|
418
432
|
mode: 'existingOnly',
|
|
419
433
|
},
|
|
420
434
|
},
|
|
@@ -500,7 +514,7 @@ describe('FormRenderer', () => {
|
|
|
500
514
|
type: 'input',
|
|
501
515
|
parameterId: 'specialtyType',
|
|
502
516
|
display: {
|
|
503
|
-
label: '
|
|
517
|
+
label: 'Specialty Type',
|
|
504
518
|
},
|
|
505
519
|
},
|
|
506
520
|
],
|
|
@@ -586,7 +600,7 @@ describe('FormRenderer', () => {
|
|
|
586
600
|
type: 'input',
|
|
587
601
|
parameterId: 'specialtyType',
|
|
588
602
|
display: {
|
|
589
|
-
label: '
|
|
603
|
+
label: 'Specialty Type',
|
|
590
604
|
relatedObjectDisplay: 'dialogBox',
|
|
591
605
|
mode: 'existingOnly',
|
|
592
606
|
},
|
|
@@ -654,7 +668,7 @@ describe('FormRenderer', () => {
|
|
|
654
668
|
type: 'input',
|
|
655
669
|
parameterId: 'specialtyType',
|
|
656
670
|
display: {
|
|
657
|
-
label: '
|
|
671
|
+
label: 'Specialty Type',
|
|
658
672
|
relatedObjectDisplay: 'dialogBox',
|
|
659
673
|
mode: 'existingOnly',
|
|
660
674
|
},
|
|
@@ -684,7 +698,7 @@ describe('FormRenderer', () => {
|
|
|
684
698
|
type: 'input',
|
|
685
699
|
parameterId: 'specialtyType',
|
|
686
700
|
display: {
|
|
687
|
-
label: '
|
|
701
|
+
label: 'Specialty Type',
|
|
688
702
|
createActionId: '_create',
|
|
689
703
|
},
|
|
690
704
|
},
|
|
@@ -944,7 +958,7 @@ describe('FormRenderer', () => {
|
|
|
944
958
|
type: 'input',
|
|
945
959
|
parameterId: 'specialtyType',
|
|
946
960
|
display: {
|
|
947
|
-
label: '
|
|
961
|
+
label: 'Specialty Type',
|
|
948
962
|
relatedObjectDisplay: 'dropdown',
|
|
949
963
|
mode: 'existingOnly',
|
|
950
964
|
},
|
|
@@ -1021,7 +1035,7 @@ describe('FormRenderer', () => {
|
|
|
1021
1035
|
const user = userEvent.setup();
|
|
1022
1036
|
render(React.createElement(FormRenderer, { form: form, onChange: () => { } }));
|
|
1023
1037
|
// Navigate to and open dropdown
|
|
1024
|
-
const dropdown = await screen.findByRole('combobox', { name: '
|
|
1038
|
+
const dropdown = await screen.findByRole('combobox', { name: 'Specialty Type' });
|
|
1025
1039
|
await user.click(dropdown);
|
|
1026
1040
|
await screen.findByRole('listbox');
|
|
1027
1041
|
// Verify that the existing option is present
|
|
@@ -1176,7 +1190,7 @@ describe('FormRenderer', () => {
|
|
|
1176
1190
|
type: 'input',
|
|
1177
1191
|
parameterId: 'specialtyType',
|
|
1178
1192
|
display: {
|
|
1179
|
-
label: '
|
|
1193
|
+
label: 'Specialty Type',
|
|
1180
1194
|
createActionId: '_create',
|
|
1181
1195
|
relatedObjectId: 'specialtyType',
|
|
1182
1196
|
},
|
|
@@ -1497,7 +1511,26 @@ describe('FormRenderer', () => {
|
|
|
1497
1511
|
id: 'testInstanceId',
|
|
1498
1512
|
name: 'Test Instance',
|
|
1499
1513
|
},
|
|
1500
|
-
}))
|
|
1514
|
+
})),
|
|
1515
|
+
// This is called by refetchRelatedInstances() after successful POST
|
|
1516
|
+
http.get('/api/data/objects/collectionObject/instances', (req) => {
|
|
1517
|
+
const filter = new URL(req.request.url).searchParams.get('filter');
|
|
1518
|
+
if (filter) {
|
|
1519
|
+
// Return the newly created item when refetching
|
|
1520
|
+
return HttpResponse.json([
|
|
1521
|
+
{
|
|
1522
|
+
id: 'newCollectionItemId',
|
|
1523
|
+
name: 'New Collection Item',
|
|
1524
|
+
objectId: 'collectionObject',
|
|
1525
|
+
relatedObject: {
|
|
1526
|
+
id: 'testInstanceId',
|
|
1527
|
+
name: 'Test Instance',
|
|
1528
|
+
},
|
|
1529
|
+
},
|
|
1530
|
+
]);
|
|
1531
|
+
}
|
|
1532
|
+
return HttpResponse.json([]);
|
|
1533
|
+
}));
|
|
1501
1534
|
render(React.createElement(FormRenderer, { form: form, onChange: () => { } }));
|
|
1502
1535
|
const addButton = await screen.findByRole('button', { name: /add/i });
|
|
1503
1536
|
await user.click(addButton);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { QueryCache, QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|
1
2
|
import { render as baseRender, screen, waitFor, within } from '@testing-library/react';
|
|
2
3
|
import userEvent from '@testing-library/user-event';
|
|
3
4
|
import { isEqual } from 'lodash';
|
|
@@ -14,12 +15,9 @@ global.ResizeObserver = class ResizeObserver {
|
|
|
14
15
|
unobserve() { }
|
|
15
16
|
disconnect() { }
|
|
16
17
|
};
|
|
17
|
-
const WithProviders = ({ children }) => {
|
|
18
|
-
return React.createElement(MemoryRouter, null, children);
|
|
19
|
-
};
|
|
20
|
-
const render = (ui, options) => baseRender(ui, { wrapper: WithProviders, ...options });
|
|
21
18
|
describe('FormRendererContainer', () => {
|
|
22
19
|
let server;
|
|
20
|
+
let queryClient;
|
|
23
21
|
beforeAll(() => {
|
|
24
22
|
server = setupServer(http.get('/api/data/objects/specialtyType/effective', () => HttpResponse.json(specialtyTypeObject)), http.get('/api/data/objects/specialtyType/effective', (req) => {
|
|
25
23
|
const sanitizedVersion = new URL(req.request.url).searchParams.get('sanitizedVersion');
|
|
@@ -45,7 +43,7 @@ describe('FormRendererContainer', () => {
|
|
|
45
43
|
// The two objects in the array of conditions in the "where" filter represent the potential filters that can be applied when retrieving "specialty" instances.
|
|
46
44
|
// The first object is for the the validation criteria, but it is empty if the "license" field, which is referenced in the validation criteria, hasn't been filled out yet.
|
|
47
45
|
// The second object is for the search criteria which the user enters in the "specialty" field, but it is empty if no search text has been entered.
|
|
48
|
-
if (isEqual(whereFilter, {
|
|
46
|
+
if (isEqual(whereFilter, {}))
|
|
49
47
|
return HttpResponse.json([
|
|
50
48
|
rnSpecialtyType1,
|
|
51
49
|
rnSpecialtyType2,
|
|
@@ -61,12 +59,35 @@ describe('FormRendererContainer', () => {
|
|
|
61
59
|
}));
|
|
62
60
|
server.listen();
|
|
63
61
|
});
|
|
62
|
+
beforeEach(() => {
|
|
63
|
+
// Create a fresh QueryClient for each test, need to pass `retry: false` to avoid retries interfering with error state tests
|
|
64
|
+
queryClient = new QueryClient({
|
|
65
|
+
queryCache: new QueryCache({
|
|
66
|
+
onError: (error, query) => {
|
|
67
|
+
const message = query.meta?.errorMessage ?? 'Something went wrong:';
|
|
68
|
+
console.error(message, error);
|
|
69
|
+
},
|
|
70
|
+
}),
|
|
71
|
+
defaultOptions: {
|
|
72
|
+
queries: {
|
|
73
|
+
retry: false,
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
});
|
|
64
78
|
afterAll(() => {
|
|
65
79
|
server.close();
|
|
80
|
+
queryClient.clear();
|
|
66
81
|
});
|
|
67
82
|
afterEach(() => {
|
|
68
83
|
server.resetHandlers();
|
|
84
|
+
queryClient.clear();
|
|
69
85
|
});
|
|
86
|
+
const WithProviders = ({ children }) => {
|
|
87
|
+
return (React.createElement(QueryClientProvider, { client: queryClient },
|
|
88
|
+
React.createElement(MemoryRouter, null, children)));
|
|
89
|
+
};
|
|
90
|
+
const render = (ui, options) => baseRender(ui, { wrapper: WithProviders, ...options });
|
|
70
91
|
describe('validation criteria', () => {
|
|
71
92
|
it(`filters related object field with validation criteria that references a defaulted related object's nested data`, async () => {
|
|
72
93
|
const user = userEvent.setup();
|
|
@@ -123,9 +144,6 @@ describe('FormRendererContainer', () => {
|
|
|
123
144
|
});
|
|
124
145
|
}));
|
|
125
146
|
render(React.createElement(FormRendererContainer, { objectId: 'specialty', formId: 'specialtyForm', dataType: 'objectInstances', actionId: '_update', instanceId: 'test-instance' }));
|
|
126
|
-
await waitFor(() => {
|
|
127
|
-
expect(screen.queryByText('Loading...')).not.toBeInTheDocument();
|
|
128
|
-
});
|
|
129
147
|
const nameField = await screen.findByRole('textbox', { name: 'Name' });
|
|
130
148
|
// Clear the existing value and type new value
|
|
131
149
|
await user.clear(nameField);
|
|
@@ -173,9 +191,6 @@ describe('FormRendererContainer', () => {
|
|
|
173
191
|
});
|
|
174
192
|
}));
|
|
175
193
|
render(React.createElement(FormRendererContainer, { objectId: 'specialty', formId: 'specialtyForm', dataType: 'objectInstances', actionId: '_update', instanceId: 'test-instance' }));
|
|
176
|
-
await waitFor(() => {
|
|
177
|
-
expect(screen.queryByText('Loading...')).not.toBeInTheDocument();
|
|
178
|
-
});
|
|
179
194
|
const nameField = await screen.findByRole('textbox', { name: 'Name' });
|
|
180
195
|
await user.type(nameField, 'Test Specialty');
|
|
181
196
|
await user.tab();
|
|
@@ -220,9 +235,6 @@ describe('FormRendererContainer', () => {
|
|
|
220
235
|
return HttpResponse.json({ error: 'Save failed' }, { status: 500 });
|
|
221
236
|
}));
|
|
222
237
|
render(React.createElement(FormRendererContainer, { objectId: 'specialty', formId: 'specialtyForm', dataType: 'objectInstances', actionId: '_update', instanceId: 'test-instance' }));
|
|
223
|
-
await waitFor(() => {
|
|
224
|
-
expect(screen.queryByText('Loading...')).not.toBeInTheDocument();
|
|
225
|
-
});
|
|
226
238
|
const nameField = await screen.findByRole('textbox', { name: 'Name' });
|
|
227
239
|
await user.type(nameField, 'Test Specialty');
|
|
228
240
|
await user.tab();
|
|
@@ -264,9 +276,6 @@ describe('FormRendererContainer', () => {
|
|
|
264
276
|
});
|
|
265
277
|
}));
|
|
266
278
|
render(React.createElement(FormRendererContainer, { objectId: 'specialty', formId: 'specialtyForm', dataType: 'objectInstances', actionId: '_update', instanceId: 'test-instance' }));
|
|
267
|
-
await waitFor(() => {
|
|
268
|
-
expect(screen.queryByText('Loading...')).not.toBeInTheDocument();
|
|
269
|
-
});
|
|
270
279
|
const nameField = await screen.findByRole('textbox', { name: 'Name' });
|
|
271
280
|
// Click into the field and blur it without changing value
|
|
272
281
|
await user.click(nameField);
|
|
@@ -317,9 +326,6 @@ describe('FormRendererContainer', () => {
|
|
|
317
326
|
});
|
|
318
327
|
}));
|
|
319
328
|
render(React.createElement(FormRendererContainer, { objectId: 'license', formId: 'licenseForm', dataType: 'objectInstances', actionId: '_update', instanceId: 'test-license' }));
|
|
320
|
-
await waitFor(() => {
|
|
321
|
-
expect(screen.queryByText('Loading...')).not.toBeInTheDocument();
|
|
322
|
-
});
|
|
323
329
|
// Find the city field
|
|
324
330
|
const cityField = await screen.findByRole('textbox', { name: 'City' });
|
|
325
331
|
// Clear and type new value
|
|
@@ -372,9 +378,6 @@ describe('FormRendererContainer', () => {
|
|
|
372
378
|
});
|
|
373
379
|
}));
|
|
374
380
|
render(React.createElement(FormRendererContainer, { objectId: 'license', formId: 'licenseForm', dataType: 'objectInstances', actionId: '_update', instanceId: 'test-license' }));
|
|
375
|
-
await waitFor(() => {
|
|
376
|
-
expect(screen.queryByText('Loading...')).not.toBeInTheDocument();
|
|
377
|
-
});
|
|
378
381
|
// Find the city field
|
|
379
382
|
const cityField = await screen.findByRole('textbox', { name: 'City' });
|
|
380
383
|
// Click into field and blur without changing
|
|
@@ -433,9 +436,6 @@ describe('FormRendererContainer', () => {
|
|
|
433
436
|
});
|
|
434
437
|
}));
|
|
435
438
|
render(React.createElement(FormRendererContainer, { objectId: 'license', formId: 'licenseForm', dataType: 'objectInstances', actionId: '_update', instanceId: 'test-license' }));
|
|
436
|
-
await waitFor(() => {
|
|
437
|
-
expect(screen.queryByText('Loading...')).not.toBeInTheDocument();
|
|
438
|
-
});
|
|
439
439
|
// Find the line1 field (it's a searchbox because of address autocomplete)
|
|
440
440
|
const line1Field = await screen.findByRole('searchbox', { name: 'Address Line 1' });
|
|
441
441
|
// Type to trigger autocomplete
|
|
@@ -472,9 +472,7 @@ describe('FormRendererContainer', () => {
|
|
|
472
472
|
});
|
|
473
473
|
}));
|
|
474
474
|
render(React.createElement(FormRendererContainer, { objectId: 'specialty', formId: 'specialtyForm', dataType: 'objectInstances', actionId: '_update', instanceId: 'test-instance' }));
|
|
475
|
-
await
|
|
476
|
-
expect(screen.queryByText('Loading...')).not.toBeInTheDocument();
|
|
477
|
-
});
|
|
475
|
+
await screen.findByRole('button', { name: 'Submit' });
|
|
478
476
|
// When autosaveActionId is present the discard button should be hidden
|
|
479
477
|
expect(screen.queryByRole('button', { name: /discard/i })).not.toBeInTheDocument();
|
|
480
478
|
});
|
|
@@ -505,11 +503,7 @@ describe('FormRendererContainer', () => {
|
|
|
505
503
|
}));
|
|
506
504
|
const user = userEvent.setup();
|
|
507
505
|
render(React.createElement(FormRendererContainer, { objectId: 'specialty', formId: 'specialtyForm', dataType: 'objectInstances', actionId: '_update', instanceId: 'test-instance' }));
|
|
508
|
-
await
|
|
509
|
-
expect(screen.queryByText('Loading...')).not.toBeInTheDocument();
|
|
510
|
-
});
|
|
511
|
-
// Change a field value
|
|
512
|
-
const nameInput = screen.getByRole('textbox', { name: /name/i });
|
|
506
|
+
const nameInput = await screen.findByRole('textbox', { name: /name/i });
|
|
513
507
|
await user.clear(nameInput);
|
|
514
508
|
await user.type(nameInput, 'Test Specialty');
|
|
515
509
|
await user.tab();
|
|
@@ -1020,7 +1014,7 @@ describe('FormRendererContainer', () => {
|
|
|
1020
1014
|
properties: [],
|
|
1021
1015
|
};
|
|
1022
1016
|
server.use(http.get(`/api/data/objects/${simpleObject.id}/effective`, () => HttpResponse.json(simpleObject)), http.get(`/api/data/forms/${form.id}`, () => HttpResponse.json(form)));
|
|
1023
|
-
render(React.createElement(FormRendererContainer, { objectId: simpleObject.id, formId: form.id,
|
|
1017
|
+
render(React.createElement(FormRendererContainer, { objectId: simpleObject.id, formId: form.id, actionId: '_create' }));
|
|
1024
1018
|
await screen.findByRole('button', { name: 'Submit' });
|
|
1025
1019
|
});
|
|
1026
1020
|
it('should display a button to discard changes', async () => {
|
|
@@ -1046,7 +1040,7 @@ describe('FormRendererContainer', () => {
|
|
|
1046
1040
|
properties: [],
|
|
1047
1041
|
};
|
|
1048
1042
|
server.use(http.get(`/api/data/objects/${simpleObject.id}/effective`, () => HttpResponse.json(simpleObject)), http.get(`/api/data/forms/${form.id}`, () => HttpResponse.json(form)));
|
|
1049
|
-
render(React.createElement(FormRendererContainer, { objectId: simpleObject.id, formId: form.id,
|
|
1043
|
+
render(React.createElement(FormRendererContainer, { objectId: simpleObject.id, formId: form.id, actionId: '_create' }));
|
|
1050
1044
|
await screen.findByRole('button', { name: 'Discard Changes' });
|
|
1051
1045
|
});
|
|
1052
1046
|
it('should reset the form when discarding changes', async () => {
|
|
@@ -1089,7 +1083,7 @@ describe('FormRendererContainer', () => {
|
|
|
1089
1083
|
};
|
|
1090
1084
|
server.use(http.get(`/api/data/objects/${simpleObject.id}/effective`, () => HttpResponse.json(simpleObject)), http.get(`/api/data/forms/${form.id}`, () => HttpResponse.json(form)));
|
|
1091
1085
|
const user = userEvent.setup();
|
|
1092
|
-
render(React.createElement(FormRendererContainer, { objectId: simpleObject.id, formId: form.id,
|
|
1086
|
+
render(React.createElement(FormRendererContainer, { objectId: simpleObject.id, formId: form.id, actionId: '_create' }));
|
|
1093
1087
|
const firstNameInput = await screen.findByRole('textbox', { name: 'First Name' });
|
|
1094
1088
|
await user.type(firstNameInput, 'John');
|
|
1095
1089
|
const discardButton = await screen.findByRole('button', { name: 'Discard Changes' });
|
|
@@ -1143,10 +1137,8 @@ describe('FormRendererContainer', () => {
|
|
|
1143
1137
|
};
|
|
1144
1138
|
server.use(http.get(`/api/data/objects/simpleObject/effective`, () => HttpResponse.json(simpleObject)), http.get(`/api/data/forms/simpleForm`, () => HttpResponse.json(form)), http.get('/api/data/objects/simpleObject/instances/123', () => HttpResponse.json({
|
|
1145
1139
|
message: 'Not Found',
|
|
1146
|
-
}, { status: 404 })), http.get('/api/data/objects/simpleObject/instances/123/object', () => HttpResponse.json(
|
|
1147
|
-
|
|
1148
|
-
}, { status: 404 })));
|
|
1149
|
-
render(React.createElement(FormRendererContainer, { formId: form.id, actionId: "_update", objectId: "simpleObject", instanceId: '123', dataType: "objectInstances" }));
|
|
1140
|
+
}, { status: 404 })), http.get('/api/data/objects/simpleObject/instances/123/object', () => HttpResponse.json(simpleObject)));
|
|
1141
|
+
render(React.createElement(FormRendererContainer, { formId: form.id, actionId: "_update", objectId: "simpleObject", instanceId: '123' }));
|
|
1150
1142
|
await screen.findByText('The requested content could not be found.');
|
|
1151
1143
|
});
|
|
1152
1144
|
it('should show an unauthorized error if the instance access is unauthorized', async () => {
|
|
@@ -1243,7 +1235,7 @@ describe('FormRendererContainer', () => {
|
|
|
1243
1235
|
properties: [],
|
|
1244
1236
|
};
|
|
1245
1237
|
server.use(http.get(`/api/data/objects/simpleObject/effective`, () => HttpResponse.json(simpleObject)), http.get(`/api/data/forms/simpleForm`, () => HttpResponse.json(form)));
|
|
1246
|
-
render(React.createElement(FormRendererContainer, { formId: form.id, objectId: "simpleObject",
|
|
1238
|
+
render(React.createElement(FormRendererContainer, { formId: form.id, objectId: "simpleObject", actionId: "_create" }));
|
|
1247
1239
|
await screen.findByText('Simple Form');
|
|
1248
1240
|
});
|
|
1249
1241
|
it('should show a not found error when the form cannot be found', async () => {
|
|
@@ -1474,7 +1466,7 @@ describe('FormRendererContainer', () => {
|
|
|
1474
1466
|
});
|
|
1475
1467
|
it('should display validation errors after trying to submit the form', async () => {
|
|
1476
1468
|
const user = userEvent.setup();
|
|
1477
|
-
render(React.createElement(FormRendererContainer, { objectId: validationTestObject.id, formId: form.id,
|
|
1469
|
+
render(React.createElement(FormRendererContainer, { objectId: validationTestObject.id, formId: form.id, actionId: "_create" }));
|
|
1478
1470
|
const submitButton = await screen.findByRole('button', { name: 'Submit' });
|
|
1479
1471
|
await user.click(submitButton);
|
|
1480
1472
|
// List items are named by author, but they don't
|
|
@@ -1485,7 +1477,7 @@ describe('FormRendererContainer', () => {
|
|
|
1485
1477
|
});
|
|
1486
1478
|
it('should clear validation errors after they have been resolved', async () => {
|
|
1487
1479
|
const user = userEvent.setup();
|
|
1488
|
-
render(React.createElement(FormRendererContainer, { objectId: validationTestObject.id, formId: form.id,
|
|
1480
|
+
render(React.createElement(FormRendererContainer, { objectId: validationTestObject.id, formId: form.id, actionId: "_create" }));
|
|
1489
1481
|
const submitButton = await screen.findByRole('button', { name: 'Submit' });
|
|
1490
1482
|
await user.click(submitButton);
|
|
1491
1483
|
// Make sure error elements appear
|
|
@@ -1496,7 +1488,7 @@ describe('FormRendererContainer', () => {
|
|
|
1496
1488
|
});
|
|
1497
1489
|
it('should scroll to validation errors after submission', async () => {
|
|
1498
1490
|
const user = userEvent.setup();
|
|
1499
|
-
render(React.createElement(FormRendererContainer, { objectId: validationTestObject.id, formId: form.id,
|
|
1491
|
+
render(React.createElement(FormRendererContainer, { objectId: validationTestObject.id, formId: form.id, actionId: "_create" }));
|
|
1500
1492
|
const submitButton = await screen.findByRole('button', { name: 'Submit' });
|
|
1501
1493
|
await user.click(submitButton);
|
|
1502
1494
|
expect(scrollIntoViewMock).toHaveBeenCalled();
|
|
@@ -1504,7 +1496,7 @@ describe('FormRendererContainer', () => {
|
|
|
1504
1496
|
it('should not scroll to validation errors after submission if there are none', async () => {
|
|
1505
1497
|
const user = userEvent.setup();
|
|
1506
1498
|
server.use(http.post(`/api/data/objects/${validationTestObject.id}/instances/actions`, () => HttpResponse.json({}, { status: 200 })));
|
|
1507
|
-
render(React.createElement(FormRendererContainer, { objectId: validationTestObject.id, formId: form.id,
|
|
1499
|
+
render(React.createElement(FormRendererContainer, { objectId: validationTestObject.id, formId: form.id, actionId: "_create" }));
|
|
1508
1500
|
const requiredField = await screen.findByRole('textbox', { name: /Required Field */i });
|
|
1509
1501
|
await user.type(requiredField, 'Some content here...');
|
|
1510
1502
|
const submitButton = await screen.findByRole('button', { name: 'Submit' });
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import React from 'react';
|
|
1
2
|
import { EntryRendererProps } from '../FormV2/components/types';
|
|
2
|
-
declare function ViewOnlyEntryRenderer(props: EntryRendererProps):
|
|
3
|
+
declare function ViewOnlyEntryRenderer(props: EntryRendererProps): React.JSX.Element | null;
|
|
3
4
|
export default ViewOnlyEntryRenderer;
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { useApiServices, useApp, } from '@evoke-platform/context';
|
|
2
2
|
import { CancelRounded, CheckCircleRounded } from '@mui/icons-material';
|
|
3
|
+
import { useQuery } from '@tanstack/react-query';
|
|
3
4
|
import DOMPurify from 'dompurify';
|
|
4
5
|
import { isEmpty, isNil } from 'lodash';
|
|
5
6
|
import { DateTime } from 'luxon';
|
|
6
7
|
import React, { useEffect, useMemo, useState } from 'react';
|
|
7
8
|
import { useFormContext } from '../../../theme/hooks';
|
|
8
|
-
import { Link, Typography } from '../../core';
|
|
9
|
+
import { Link, Skeleton, Typography } from '../../core';
|
|
9
10
|
import { Box, Grid } from '../../layout';
|
|
10
11
|
import AccordionSections from '../FormV2/components/AccordionSections';
|
|
11
12
|
import FieldWrapper from '../FormV2/components/FieldWrapper';
|
|
@@ -16,7 +17,7 @@ import Criteria from '../FormV2/components/FormFieldTypes/Criteria';
|
|
|
16
17
|
import { Document } from '../FormV2/components/FormFieldTypes/DocumentFiles/Document';
|
|
17
18
|
import { Image } from '../FormV2/components/FormFieldTypes/Image';
|
|
18
19
|
import PropertyProtection from '../FormV2/components/PropertyProtection';
|
|
19
|
-
import { entryIsVisible,
|
|
20
|
+
import { entryIsVisible, fetchInitialMiddleObjectInstances, fetchMiddleObject, filterEmptySections, getDefaultPages, isAddressProperty, } from '../FormV2/components/utils';
|
|
20
21
|
function ViewOnlyEntryRenderer(props) {
|
|
21
22
|
const { entry } = props;
|
|
22
23
|
const { fetchedOptions, setFetchedOptions, object, instance, richTextEditor: RichTextEditor } = useFormContext();
|
|
@@ -26,8 +27,6 @@ function ViewOnlyEntryRenderer(props) {
|
|
|
26
27
|
const [navigationSlug, setNavigationSlug] = useState(fetchedOptions[`${entryId}NavigationSlug`]);
|
|
27
28
|
const [currentDisplayValue, setCurrentDisplayValue] = useState(instance?.[entryId]);
|
|
28
29
|
const [protectionMode, setProtectionMode] = useState('mask');
|
|
29
|
-
const initialMiddleObjectInstances = fetchedOptions[`${entryId}InitialMiddleObjectInstances`];
|
|
30
|
-
const middleObject = fetchedOptions[`${entryId}MiddleObject`];
|
|
31
30
|
const display = 'display' in entry ? entry.display : undefined;
|
|
32
31
|
const fieldDefinition = useMemo(() => {
|
|
33
32
|
const def = entry.type === 'readonlyField'
|
|
@@ -45,11 +44,35 @@ function ViewOnlyEntryRenderer(props) {
|
|
|
45
44
|
}, [entry, object]);
|
|
46
45
|
const isProtectedProperty = fieldDefinition?.protection?.maskChar;
|
|
47
46
|
const protectionComponent = isProtectedProperty && !isNil(currentDisplayValue) ? (React.createElement(PropertyProtection, { parameter: fieldDefinition, protection: fieldDefinition?.protection, mask: fieldDefinition?.mask, value: currentDisplayValue, canEdit: false, setCurrentDisplayValue: setCurrentDisplayValue, mode: protectionMode, setMode: setProtectionMode })) : null;
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
47
|
+
const { data: middleObject } = useQuery({
|
|
48
|
+
queryKey: [fieldDefinition?.objectId, 'MiddleObject'],
|
|
49
|
+
queryFn: () => fetchMiddleObject(fieldDefinition, apiServices),
|
|
50
|
+
staleTime: Infinity,
|
|
51
|
+
enabled: !!(fieldDefinition?.objectId &&
|
|
52
|
+
fieldDefinition?.type === 'collection' &&
|
|
53
|
+
fieldDefinition?.manyToManyPropertyId),
|
|
54
|
+
meta: {
|
|
55
|
+
errorMessage: 'Failed to fetch middle object: ',
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
const { data: initialMiddleObjectInstances = [], isLoading: isLoadingInstances } = useQuery({
|
|
59
|
+
queryKey: [
|
|
60
|
+
fieldDefinition?.objectId,
|
|
61
|
+
instance?.id,
|
|
62
|
+
fieldDefinition?.relatedPropertyId,
|
|
63
|
+
'InitialMiddleObjectInstances',
|
|
64
|
+
],
|
|
65
|
+
queryFn: () => fetchInitialMiddleObjectInstances(apiServices, fieldDefinition, instance?.id),
|
|
66
|
+
staleTime: Infinity,
|
|
67
|
+
enabled: !!(fieldDefinition?.objectId &&
|
|
68
|
+
instance?.id &&
|
|
69
|
+
fieldDefinition?.type === 'collection' &&
|
|
70
|
+
fieldDefinition?.manyToManyPropertyId &&
|
|
71
|
+
fieldDefinition?.relatedPropertyId),
|
|
72
|
+
meta: {
|
|
73
|
+
errorMessage: 'Failed to fetch middle object instances: ',
|
|
74
|
+
},
|
|
75
|
+
});
|
|
53
76
|
useEffect(() => {
|
|
54
77
|
(async () => {
|
|
55
78
|
if (object?.properties && !fetchedOptions[`${entryId}NavigationSlug`]) {
|
|
@@ -146,9 +169,11 @@ function ViewOnlyEntryRenderer(props) {
|
|
|
146
169
|
React.createElement(Document, { id: entryId, fieldType: fieldDefinition.type, error: false, value: fieldValue, canUpdateProperty: false })));
|
|
147
170
|
}
|
|
148
171
|
else if (fieldDefinition.type === 'collection') {
|
|
149
|
-
|
|
150
|
-
React.createElement(
|
|
151
|
-
|
|
172
|
+
if (fieldDefinition.manyToManyPropertyId && !isEmpty(middleObject)) {
|
|
173
|
+
return isLoadingInstances ? (React.createElement(Skeleton, null)) : (React.createElement(FieldWrapper, { inputId: entryId, inputType: 'collection', label: display?.label || fieldDefinition?.name || 'default', value: fieldValue, required: display?.required || false, viewOnly: true },
|
|
174
|
+
React.createElement(DropdownRepeatableField, { initialMiddleObjectInstances: initialMiddleObjectInstances, id: entryId, middleObject: middleObject, fieldDefinition: fieldDefinition, readOnly: true })));
|
|
175
|
+
}
|
|
176
|
+
return (React.createElement(FieldWrapper, { inputId: entryId, inputType: 'collection', label: display?.label || fieldDefinition?.name || 'default', value: fieldValue, required: display?.required || false, viewOnly: true },
|
|
152
177
|
React.createElement(RepeatableField, { fieldDefinition: fieldDefinition, canUpdateProperty: false, entry: entry, viewLayout: display?.viewLayout })));
|
|
153
178
|
}
|
|
154
179
|
else if (fieldDefinition.type === 'criteria') {
|
|
@@ -6,10 +6,15 @@ import { Skeleton, Snackbar } from '../../core';
|
|
|
6
6
|
import { Box } from '../../layout';
|
|
7
7
|
import ErrorComponent from '../ErrorComponent';
|
|
8
8
|
import { FormContext } from '../FormV2';
|
|
9
|
+
import ConditionalQueryClientProvider from '../FormV2/components/ConditionalQueryClientProvider';
|
|
9
10
|
import Header from '../FormV2/components/Header';
|
|
10
11
|
import { assignIdsToSectionsAndRichText, getPrefixedUrl } from '../FormV2/components/utils';
|
|
11
12
|
import ViewOnlyEntryRenderer from './InstanceEntryRenderer';
|
|
12
13
|
function ViewDetailsV2Container(props) {
|
|
14
|
+
return (React.createElement(ConditionalQueryClientProvider, null,
|
|
15
|
+
React.createElement(ViewDetailsV2ContainerInner, { ...props })));
|
|
16
|
+
}
|
|
17
|
+
function ViewDetailsV2ContainerInner(props) {
|
|
13
18
|
const { instanceId, panelLayoutId, objectId, richTextEditor, renderHeader, renderBody } = props;
|
|
14
19
|
const apiServices = useApiServices();
|
|
15
20
|
const [sanitizedObject, setSanitizedObject] = useState();
|
|
@@ -67,8 +72,8 @@ function ViewDetailsV2Container(props) {
|
|
|
67
72
|
if (panelLayoutId || sanitizedObject?.defaultPanelLayoutId) {
|
|
68
73
|
apiServices
|
|
69
74
|
.get(getPrefixedUrl(`/objects/${objectId}/panelLayouts/${panelLayoutId || sanitizedObject?.defaultPanelLayoutId}`))
|
|
70
|
-
.then((
|
|
71
|
-
setPanelLayout(
|
|
75
|
+
.then((panel) => {
|
|
76
|
+
setPanelLayout(panel);
|
|
72
77
|
})
|
|
73
78
|
.catch((error) => {
|
|
74
79
|
onError(error);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@evoke-platform/ui-components",
|
|
3
|
-
"version": "1.13.0-dev.
|
|
3
|
+
"version": "1.13.0-dev.7",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "dist/published/index.js",
|
|
6
6
|
"module": "dist/published/index.js",
|
|
@@ -96,7 +96,7 @@
|
|
|
96
96
|
"yalc": "^1.0.0-pre.53"
|
|
97
97
|
},
|
|
98
98
|
"peerDependencies": {
|
|
99
|
-
"@evoke-platform/context": "
|
|
99
|
+
"@evoke-platform/context": "1.8.0-dev.4",
|
|
100
100
|
"react": "^18.1.0",
|
|
101
101
|
"react-dom": "^18.1.0"
|
|
102
102
|
},
|
|
@@ -118,6 +118,7 @@
|
|
|
118
118
|
"@mui/x-tree-view": "^7.29.1",
|
|
119
119
|
"@react-querybuilder/dnd": "^5.4.1",
|
|
120
120
|
"@react-querybuilder/material": "^6.5.0",
|
|
121
|
+
"@tanstack/react-query": "^5.90.12",
|
|
121
122
|
"clean-deep": "^3.4.0",
|
|
122
123
|
"commit-and-tag-version": "^12.4.1",
|
|
123
124
|
"devexpress-richedit": "^23.1.5",
|