@evoke-platform/ui-components 1.16.0 → 1.18.0
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/CriteriaBuilder/CriteriaBuilder.js +3 -0
- package/dist/published/components/custom/Form/utils.d.ts +2 -2
- package/dist/published/components/custom/FormField/AddressFieldComponent/addressFieldComponent.js +1 -1
- package/dist/published/components/custom/FormField/BooleanSelect/BooleanSelect.js +15 -7
- package/dist/published/components/custom/FormField/InputFieldComponent/InputFieldComponent.js +1 -1
- package/dist/published/components/custom/FormField/Select/Select.js +1 -1
- package/dist/published/components/custom/FormV2/FormRenderer.d.ts +1 -0
- package/dist/published/components/custom/FormV2/FormRenderer.js +12 -7
- package/dist/published/components/custom/FormV2/FormRendererContainer.d.ts +1 -0
- package/dist/published/components/custom/FormV2/FormRendererContainer.js +52 -31
- package/dist/published/components/custom/FormV2/components/Body.d.ts +1 -0
- package/dist/published/components/custom/FormV2/components/Body.js +4 -2
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/DropdownRepeatableFieldInput.js +3 -0
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/RepeatableField.js +8 -6
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/Document.js +1 -0
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/Image.js +1 -0
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/UserProperty.js +2 -0
- package/dist/published/components/custom/FormV2/components/FormletRenderer.d.ts +7 -0
- package/dist/published/components/custom/FormV2/components/FormletRenderer.js +22 -0
- package/dist/published/components/custom/FormV2/components/HtmlView.js +16 -9
- package/dist/published/components/custom/FormV2/components/MisconfiguredErrorMessage.d.ts +2 -0
- package/dist/published/components/custom/FormV2/components/MisconfiguredErrorMessage.js +15 -0
- package/dist/published/components/custom/FormV2/components/RecursiveEntryRenderer.js +17 -24
- package/dist/published/components/custom/FormV2/components/types.d.ts +2 -1
- package/dist/published/components/custom/FormV2/components/utils.d.ts +7 -2
- package/dist/published/components/custom/FormV2/components/utils.js +84 -4
- package/dist/published/components/custom/FormV2/tests/FormRenderer.test.js +228 -7
- package/dist/published/components/custom/FormV2/tests/FormRendererContainer.test.js +491 -35
- package/dist/published/components/custom/ViewDetailsV2/InstanceEntryRenderer.js +20 -9
- package/dist/published/components/custom/ViewDetailsV2/ViewDetailsV2Container.js +1 -0
- package/dist/published/stories/FormRenderer.stories.d.ts +3 -0
- package/dist/published/stories/FormRenderer.stories.js +1 -0
- package/dist/published/stories/FormRendererContainer.stories.d.ts +5 -0
- package/dist/published/stories/FormRendererData.d.ts +15 -0
- package/dist/published/stories/FormRendererData.js +63 -0
- package/dist/published/stories/sharedMswHandlers.js +4 -2
- package/package.json +1 -1
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import { useApiServices, useAuthenticationContext, } from '@evoke-platform/context';
|
|
2
|
-
import {
|
|
2
|
+
import { Grid } from '@mui/material';
|
|
3
3
|
import { isEmpty } from 'lodash';
|
|
4
4
|
import React, { useMemo } from 'react';
|
|
5
5
|
import useWidgetSize, { useFormContext } from '../../../../theme/hooks';
|
|
6
|
-
import { Skeleton, TextField
|
|
7
|
-
import { Box } from '../../../layout';
|
|
6
|
+
import { Skeleton, TextField } from '../../../core';
|
|
8
7
|
import FormField from '../../FormField';
|
|
9
8
|
import AccordionSections from './AccordionSections';
|
|
10
9
|
import FieldWrapper from './FieldWrapper';
|
|
@@ -21,6 +20,8 @@ import FormSections from './FormSections';
|
|
|
21
20
|
import { entryIsVisible, fetchInitialMiddleObjectInstances, fetchMiddleObject, filterEmptySections, getEntryId, getFieldDefinition, isAddressProperty, isOptionEqualToValue, updateCriteriaInputs, } from './utils';
|
|
22
21
|
import HtmlView from './HtmlView';
|
|
23
22
|
import { useQuery } from '@tanstack/react-query';
|
|
23
|
+
import FormletRenderer from './FormletRenderer';
|
|
24
|
+
import MisconfiguredErrorMessage from './MisconfiguredErrorMessage';
|
|
24
25
|
function getFieldWrapperProps(fieldDefinition, entry, entryId, fieldValue, display, errors, validation) {
|
|
25
26
|
return {
|
|
26
27
|
inputId: entryId,
|
|
@@ -53,8 +54,6 @@ export function RecursiveEntryRenderer(props) {
|
|
|
53
54
|
const display = 'display' in entry ? entry.display : undefined;
|
|
54
55
|
const fieldValue = entry.type === 'readonlyField' ? instance?.[entryId] : getValues ? getValues(entryId) : undefined;
|
|
55
56
|
const fieldDefinition = useMemo(() => {
|
|
56
|
-
if (!object)
|
|
57
|
-
return undefined;
|
|
58
57
|
return getFieldDefinition(entry, object, parameters);
|
|
59
58
|
}, [entry, parameters, object]);
|
|
60
59
|
const validation = fieldDefinition?.validation || {};
|
|
@@ -154,11 +153,15 @@ export function RecursiveEntryRenderer(props) {
|
|
|
154
153
|
}
|
|
155
154
|
}
|
|
156
155
|
else if (fieldDefinition.type === 'richText') {
|
|
157
|
-
return (React.createElement(FieldWrapper, {
|
|
156
|
+
return (React.createElement(FieldWrapper, { ...getFieldWrapperProps(fieldDefinition, entry, entryId, fieldValue, display, errors) }, RichTextEditor ? (React.createElement(RichTextEditor
|
|
158
157
|
// RichTexts get a uniqueId when the form is loaded to prevent issues with multiple rich text fields on one form
|
|
159
158
|
, {
|
|
160
159
|
// RichTexts get a uniqueId when the form is loaded to prevent issues with multiple rich text fields on one form
|
|
161
|
-
id: entry.uniqueId, value: fieldValue, handleUpdate: (value) => handleChange?.(entryId, value),
|
|
160
|
+
id: entry.uniqueId, value: fieldValue, handleUpdate: (value) => handleChange?.(entryId, value), onBlur: () => {
|
|
161
|
+
onAutosave?.(entryId)?.catch((error) => {
|
|
162
|
+
console.error('Autosave failed:', error);
|
|
163
|
+
});
|
|
164
|
+
}, format: "rtf", disabled: entry.type === 'readonlyField', rows: display?.rowCount, hasError: !!errors?.[entryId] })) : (React.createElement(FormField, { id: entryId, property: fieldDefinition, defaultValue: fieldValue, onChange: handleChange, onBlur: () => {
|
|
162
165
|
onAutosave?.(entryId)?.catch((error) => {
|
|
163
166
|
console.error('Autosave failed:', error);
|
|
164
167
|
});
|
|
@@ -218,16 +221,14 @@ export function RecursiveEntryRenderer(props) {
|
|
|
218
221
|
? display?.booleanDisplay
|
|
219
222
|
: display?.choicesDisplay?.type && display.choicesDisplay.type, label: display?.label, description: display?.description, tooltip: display?.tooltip, selectOptions: fieldDefinition?.enum, additionalProps: additionalProps, isCombobox: fieldDefinition.nonStrictEnum, strictlyTrue: fieldDefinition.strictlyTrue, protection: objectProperty?.protection })));
|
|
220
223
|
}
|
|
224
|
+
// Forms from the FormV2 widget will get the effective form and will not need this
|
|
225
|
+
// but it's possible for a form passed into the FormRenderer to include a formlet type
|
|
226
|
+
}
|
|
227
|
+
else if (entry.type === 'formlet') {
|
|
228
|
+
return React.createElement(FormletRenderer, { entry: entry });
|
|
221
229
|
}
|
|
222
230
|
else if (entry.type === 'columns') {
|
|
223
|
-
return (React.createElement(
|
|
224
|
-
display: 'flex',
|
|
225
|
-
alignItems: 'flex-start',
|
|
226
|
-
gap: '30px',
|
|
227
|
-
flexDirection: isXs ? 'column' : 'row',
|
|
228
|
-
} }, entry.columns.map((column, colIndex) => (
|
|
229
|
-
// calculating the width like this rather than flex={column.width} to prevent collections from being too wide
|
|
230
|
-
React.createElement(Box, { key: colIndex, sx: { width: isXs ? '100%' : `calc(${(column.width / 12) * 100}% - 15px)` } }, column.entries?.map((columnEntry, entryIndex) => {
|
|
231
|
+
return (React.createElement(Grid, { container: true, columnSpacing: 4 }, entry.columns.map((column, colIndex) => (React.createElement(Grid, { key: colIndex, item: true, xs: isXs ? 12 : column.width }, column.entries?.map((columnEntry, entryIndex) => {
|
|
231
232
|
return (React.createElement(RecursiveEntryRenderer, { key: entryIndex + (columnEntry?.parameterId ?? ''), entry: columnEntry }));
|
|
232
233
|
}))))));
|
|
233
234
|
}
|
|
@@ -236,15 +237,7 @@ export function RecursiveEntryRenderer(props) {
|
|
|
236
237
|
return filteredEntry ? (isSmallerThanMd ? (React.createElement(AccordionSections, { entry: filteredEntry })) : (React.createElement(FormSections, { entry: filteredEntry }))) : null;
|
|
237
238
|
}
|
|
238
239
|
else if (!fieldDefinition) {
|
|
239
|
-
return
|
|
240
|
-
display: 'flex',
|
|
241
|
-
backgroundColor: '#ffc1073b',
|
|
242
|
-
borderRadius: '8px',
|
|
243
|
-
padding: '16.5px 14px',
|
|
244
|
-
marginTop: '6px',
|
|
245
|
-
} },
|
|
246
|
-
React.createElement(WarningRounded, { sx: { paddingRight: '8px' }, color: "warning" }),
|
|
247
|
-
React.createElement(Typography, { variant: "body2", color: "textSecondary" }, "This field was not configured correctly")));
|
|
240
|
+
return React.createElement(MisconfiguredErrorMessage, null);
|
|
248
241
|
}
|
|
249
242
|
return null;
|
|
250
243
|
}
|
|
@@ -39,7 +39,8 @@ export type FileInstance = {
|
|
|
39
39
|
export type SimpleEditorProps = {
|
|
40
40
|
id: string;
|
|
41
41
|
value: string;
|
|
42
|
-
handleUpdate?: (value: string) => void
|
|
42
|
+
handleUpdate?: (value: string) => void | Promise<void>;
|
|
43
|
+
onBlur?: () => void;
|
|
43
44
|
format: 'rtf' | 'plain' | 'openxml';
|
|
44
45
|
disabled?: boolean;
|
|
45
46
|
rows?: number;
|
|
@@ -20,6 +20,10 @@ export declare const entryIsVisible: (entry: FormEntry, instance?: FieldValues,
|
|
|
20
20
|
*/
|
|
21
21
|
export declare const getNestedParameterIds: (entry: Sections | Columns) => string[];
|
|
22
22
|
export declare const getEntryId: (entry: FormEntry) => string | undefined;
|
|
23
|
+
/**
|
|
24
|
+
* Returns editable field IDs that are currently visible on the form.
|
|
25
|
+
*/
|
|
26
|
+
export declare const getVisibleEditableFieldIds: (entries: FormEntry[], instance?: FieldValues, formValues?: FieldValues) => string[];
|
|
23
27
|
export declare function getPrefixedUrl(url: string): string;
|
|
24
28
|
export declare const isOptionEqualToValue: (option: AutocompleteOption | string, value: unknown) => boolean;
|
|
25
29
|
export declare function addressProperties(addressProperty: Property): Property[];
|
|
@@ -85,7 +89,7 @@ export declare function formatSubmission(submission: FieldValues, apiServices?:
|
|
|
85
89
|
objectId?: string;
|
|
86
90
|
}, parameters?: InputParameter[]): Promise<FieldValues>;
|
|
87
91
|
export declare function filterEmptySections(entry: Sections | Columns, instance?: FieldValues, formData?: FieldValues): Sections | Columns | null;
|
|
88
|
-
export declare function assignIdsToSectionsAndRichText(entries: FormEntry[] | PanelViewEntry[], object
|
|
92
|
+
export declare function assignIdsToSectionsAndRichText(entries: FormEntry[] | PanelViewEntry[], object?: Obj, parameters?: InputParameter[]): FormEntry[] | PanelViewEntry[];
|
|
89
93
|
/**
|
|
90
94
|
* Converts a plain text string to RTF format suitable for a RichTextEditor.
|
|
91
95
|
*
|
|
@@ -99,7 +103,7 @@ export declare function assignIdsToSectionsAndRichText(entries: FormEntry[] | Pa
|
|
|
99
103
|
* This ensures that any plain text input will be safely represented in RTF without losing formatting or characters.
|
|
100
104
|
*/
|
|
101
105
|
export declare function plainTextToRtf(plainText: string): string;
|
|
102
|
-
export declare function getFieldDefinition(entry: FormEntry | PanelViewEntry, object
|
|
106
|
+
export declare function getFieldDefinition(entry: FormEntry | PanelViewEntry, object?: Obj, parameters?: InputParameter[]): InputParameter | Property | undefined;
|
|
103
107
|
export declare function obfuscateValue(value: unknown, property?: Partial<Property> | Partial<ObjectProperty>): unknown;
|
|
104
108
|
export declare function handleFileUpload(apiServices: ApiServices, submission: FieldValues, actionId: string, objectId?: string, instanceId?: string, linkTo?: {
|
|
105
109
|
instanceId: string;
|
|
@@ -123,3 +127,4 @@ export declare function useFormById(formId: string, apiServices: ApiServices, er
|
|
|
123
127
|
export declare const extractPresetValuesFromCriteria: (criteria: Record<string, unknown>) => string[];
|
|
124
128
|
export declare const extractAllCriteria: (flattenFormEntries: FormEntry[], parameters: InputParameter[]) => Record<string, unknown>[];
|
|
125
129
|
export declare const extractPresetValuesFromDynamicDefaultValues: (flattenFormEntries: FormEntry[]) => string[];
|
|
130
|
+
export declare function convertToReadOnly(entry: FormEntry): FormEntry;
|
|
@@ -127,6 +127,48 @@ const getEntryType = (entry, parameters) => {
|
|
|
127
127
|
return parameter?.type;
|
|
128
128
|
}
|
|
129
129
|
};
|
|
130
|
+
/**
|
|
131
|
+
* Returns editable field IDs that are currently visible on the form.
|
|
132
|
+
*/
|
|
133
|
+
export const getVisibleEditableFieldIds = (entries, instance, formValues) => {
|
|
134
|
+
const fieldIds = new Set();
|
|
135
|
+
const collectVisibleIds = (entriesToCheck) => {
|
|
136
|
+
entriesToCheck.forEach((entry) => {
|
|
137
|
+
if (!entryIsVisible(entry, instance, formValues)) {
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
if (entry.type === 'sections') {
|
|
141
|
+
entry.sections.forEach((section) => {
|
|
142
|
+
if (section.entries) {
|
|
143
|
+
collectVisibleIds(section.entries);
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
if (entry.type === 'columns') {
|
|
149
|
+
entry.columns.forEach((column) => {
|
|
150
|
+
if (column.entries) {
|
|
151
|
+
collectVisibleIds(column.entries);
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
if (entry.type !== 'input' && entry.type !== 'inputField') {
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
// Collection entries are handled by their own flow and are not part of autosave payloads.
|
|
160
|
+
if (entry.type === 'inputField' && entry.input.type === 'collection') {
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
const fieldId = getEntryId(entry);
|
|
164
|
+
if (fieldId) {
|
|
165
|
+
fieldIds.add(fieldId);
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
};
|
|
169
|
+
collectVisibleIds(entries ?? []);
|
|
170
|
+
return Array.from(fieldIds);
|
|
171
|
+
};
|
|
130
172
|
export function getPrefixedUrl(url) {
|
|
131
173
|
const wcsMatchers = ['/apps', '/pages', '/widgets'];
|
|
132
174
|
const dataMatchers = [
|
|
@@ -135,8 +177,9 @@ export function getPrefixedUrl(url) {
|
|
|
135
177
|
'/documents',
|
|
136
178
|
'/payments',
|
|
137
179
|
'/forms',
|
|
138
|
-
'/locations',
|
|
139
180
|
'/files',
|
|
181
|
+
'/formlets',
|
|
182
|
+
'/locations',
|
|
140
183
|
];
|
|
141
184
|
const signalrMatchers = ['/hubs'];
|
|
142
185
|
const accessManagementMatchers = ['/users'];
|
|
@@ -856,8 +899,8 @@ export function getFieldDefinition(entry, object, parameters) {
|
|
|
856
899
|
}
|
|
857
900
|
else if (entry.type === 'readonlyField') {
|
|
858
901
|
def = isAddressProperty(entry.propertyId)
|
|
859
|
-
? object
|
|
860
|
-
: object
|
|
902
|
+
? object?.properties?.find((prop) => prop.id === entry.propertyId.split('.')[0])
|
|
903
|
+
: object?.properties?.find((prop) => prop.id === entry.propertyId);
|
|
861
904
|
}
|
|
862
905
|
else if (entry.type === 'inputField') {
|
|
863
906
|
def = entry.input;
|
|
@@ -962,7 +1005,7 @@ export function useFormById(formId, apiServices, errorMessage) {
|
|
|
962
1005
|
queryKey: ['form', formId],
|
|
963
1006
|
enabled: formId !== '_auto_' && !!formId,
|
|
964
1007
|
staleTime: Infinity,
|
|
965
|
-
queryFn: () => apiServices.get(getPrefixedUrl(`/forms/${formId}`)),
|
|
1008
|
+
queryFn: () => apiServices.get(getPrefixedUrl(`/forms/${formId}/effective`)),
|
|
966
1009
|
meta: {
|
|
967
1010
|
errorMessage,
|
|
968
1011
|
},
|
|
@@ -1077,3 +1120,40 @@ export const extractPresetValuesFromDynamicDefaultValues = (flattenFormEntries)
|
|
|
1077
1120
|
}
|
|
1078
1121
|
return allPresetValues;
|
|
1079
1122
|
};
|
|
1123
|
+
export function convertToReadOnly(entry) {
|
|
1124
|
+
if (entry.type === 'columns') {
|
|
1125
|
+
const columns = entry.columns.map((column) => ({
|
|
1126
|
+
...column,
|
|
1127
|
+
entries: column.entries?.map((entry) => convertToReadOnly(entry)),
|
|
1128
|
+
}));
|
|
1129
|
+
return {
|
|
1130
|
+
...entry,
|
|
1131
|
+
columns: columns,
|
|
1132
|
+
};
|
|
1133
|
+
}
|
|
1134
|
+
else if (entry.type === 'sections') {
|
|
1135
|
+
const sections = entry.sections.map((section) => ({
|
|
1136
|
+
...section,
|
|
1137
|
+
entries: section.entries?.map((entry) => convertToReadOnly(entry)),
|
|
1138
|
+
}));
|
|
1139
|
+
return {
|
|
1140
|
+
...entry,
|
|
1141
|
+
sections: sections,
|
|
1142
|
+
};
|
|
1143
|
+
}
|
|
1144
|
+
else if (entry.type === 'input') {
|
|
1145
|
+
return {
|
|
1146
|
+
type: 'readonlyField',
|
|
1147
|
+
propertyId: entry.parameterId,
|
|
1148
|
+
display: entry.display,
|
|
1149
|
+
};
|
|
1150
|
+
}
|
|
1151
|
+
else if (entry.type === 'inputField') {
|
|
1152
|
+
return {
|
|
1153
|
+
type: 'readonlyField',
|
|
1154
|
+
propertyId: entry.input.id,
|
|
1155
|
+
display: entry.display,
|
|
1156
|
+
};
|
|
1157
|
+
}
|
|
1158
|
+
return entry;
|
|
1159
|
+
}
|
|
@@ -8,6 +8,7 @@ import React from 'react';
|
|
|
8
8
|
import { MemoryRouter } from 'react-router-dom';
|
|
9
9
|
import { expect, it } from 'vitest';
|
|
10
10
|
import FormRenderer from '../FormRenderer';
|
|
11
|
+
import { convertToReadOnly } from '../components/utils';
|
|
11
12
|
import { accessibility508Object, createSpecialtyForm, jsonLogicDisplayTestSpecialtyForm, licenseObject, npLicense, npSpecialtyType1, npSpecialtyType2, rnLicense, rnSpecialtyType1, rnSpecialtyType2, simpleConditionDisplayTestSpecialtyForm, specialtyObject, specialtyTypeObject, UpdateAccessibilityFormOne, UpdateAccessibilityFormTwo, users, } from './test-data';
|
|
12
13
|
// Mock ResizeObserver
|
|
13
14
|
global.ResizeObserver = class ResizeObserver {
|
|
@@ -34,6 +35,11 @@ describe('FormRenderer', () => {
|
|
|
34
35
|
if (sanitizedVersion === 'true') {
|
|
35
36
|
return HttpResponse.json(specialtyObject);
|
|
36
37
|
}
|
|
38
|
+
}), http.get('/api/data/objects/formletTestObject/effective', (req) => {
|
|
39
|
+
const sanitizedVersion = new URL(req.request.url).searchParams.get('sanitizedVersion');
|
|
40
|
+
if (sanitizedVersion === 'true') {
|
|
41
|
+
return HttpResponse.json({});
|
|
42
|
+
}
|
|
37
43
|
}), http.get('/api/data/objects/accessibility508Object/effective', (req) => {
|
|
38
44
|
const sanitizedVersion = new URL(req.request.url).searchParams.get('sanitizedVersion');
|
|
39
45
|
if (sanitizedVersion === 'true') {
|
|
@@ -416,7 +422,7 @@ describe('FormRenderer', () => {
|
|
|
416
422
|
});
|
|
417
423
|
describe('when passed a regular related object entry', () => {
|
|
418
424
|
const setupTestMocks = (object, form, instances) => {
|
|
419
|
-
server.use(http.get(`/api/data/objects/${object.id}/effective`, () => HttpResponse.json(object)), http.get(`/api/data/forms/${form.id}`, () => HttpResponse.json(form)), http.get(`/api/data/forms?filter={"where":{"actionId":"${form.actionId}","objectId":"${object.id}"}}`, () => HttpResponse.json([form])), http.get(`/api/data/objects/${object.id}/instances`, () => HttpResponse.json(instances || [])));
|
|
425
|
+
server.use(http.get(`/api/data/objects/${object.id}/effective`, () => HttpResponse.json(object)), http.get(`/api/data/forms/${form.id}/effective`, () => HttpResponse.json(form)), http.get(`/api/data/forms?filter={"where":{"actionId":"${form.actionId}","objectId":"${object.id}"}}`, () => HttpResponse.json([form])), http.get(`/api/data/objects/${object.id}/instances`, () => HttpResponse.json(instances || [])));
|
|
420
426
|
};
|
|
421
427
|
describe('when in table view', () => {
|
|
422
428
|
describe('when mode is existing records only', () => {
|
|
@@ -880,7 +886,7 @@ describe('FormRenderer', () => {
|
|
|
880
886
|
});
|
|
881
887
|
it('displays a not found error in record creation mode if a form could not be found', async () => {
|
|
882
888
|
const user = userEvent.setup();
|
|
883
|
-
server.use(http.get('/api/data/forms/specialtyTypeForm', () => {
|
|
889
|
+
server.use(http.get('/api/data/forms/specialtyTypeForm/effective', () => {
|
|
884
890
|
return HttpResponse.json({ error: 'Not found' }, { status: 404 });
|
|
885
891
|
}));
|
|
886
892
|
render(React.createElement(FormRenderer, { form: form, onChange: () => { } }));
|
|
@@ -1180,7 +1186,7 @@ describe('FormRenderer', () => {
|
|
|
1180
1186
|
});
|
|
1181
1187
|
describe('when passed a dynamic related object entry', () => {
|
|
1182
1188
|
const setupTestMocks = (object, form, instances) => {
|
|
1183
|
-
server.use(http.get(`/api/data/objects/${object.id}/effective`, () => HttpResponse.json(object)), http.get(`/api/data/forms/${form.id}`, () => HttpResponse.json(form)), http.get(`/api/data/forms?filter={"where":{"actionId":"${form.actionId}","objectId":"${object.id}"}}`, () => HttpResponse.json([form])), http.get(`/api/data/objects/${object.id}/instances`, () => HttpResponse.json(instances || [])));
|
|
1189
|
+
server.use(http.get(`/api/data/objects/${object.id}/effective`, () => HttpResponse.json(object)), http.get(`/api/data/forms/${form.id}/effective`, () => HttpResponse.json(form)), http.get(`/api/data/forms?filter={"where":{"actionId":"${form.actionId}","objectId":"${object.id}"}}`, () => HttpResponse.json([form])), http.get(`/api/data/objects/${object.id}/instances`, () => HttpResponse.json(instances || [])));
|
|
1184
1190
|
};
|
|
1185
1191
|
const form = {
|
|
1186
1192
|
id: 'relatedObjectTestForm',
|
|
@@ -1294,7 +1300,7 @@ describe('FormRenderer', () => {
|
|
|
1294
1300
|
});
|
|
1295
1301
|
it('displays a not found error in record creation mode if a form could not be found', async () => {
|
|
1296
1302
|
const user = userEvent.setup();
|
|
1297
|
-
server.use(http.get('/api/data/forms/specialtyTypeForm', () => {
|
|
1303
|
+
server.use(http.get('/api/data/forms/specialtyTypeForm/effective', () => {
|
|
1298
1304
|
return HttpResponse.json({ error: 'Not found' }, { status: 404 });
|
|
1299
1305
|
}));
|
|
1300
1306
|
render(React.createElement(FormRenderer, { form: form, onChange: () => { } }));
|
|
@@ -1306,7 +1312,7 @@ describe('FormRenderer', () => {
|
|
|
1306
1312
|
});
|
|
1307
1313
|
describe('when passed a one-to-many collection entry', () => {
|
|
1308
1314
|
const setupTestMocks = (object, form, instances) => {
|
|
1309
|
-
server.use(http.get(`/api/data/objects/${object.id}/effective`, () => HttpResponse.json(object)), http.get(`/api/data/forms/${form.id}`, () => HttpResponse.json(form)), http.get(`/api/data/forms?filter={"where":{"actionId":"${form.actionId}","objectId":"${object.id}"}}`, () => HttpResponse.json([form])), http.get(`/api/data/objects/${object.id}/instances`, () => HttpResponse.json(instances || [])), http.get(`/api/data/objects/${object.id}/instances/checkAccess`, () => HttpResponse.json({
|
|
1315
|
+
server.use(http.get(`/api/data/objects/${object.id}/effective`, () => HttpResponse.json(object)), http.get(`/api/data/forms/${form.id}/effective`, () => HttpResponse.json(form)), http.get(`/api/data/forms?filter={"where":{"actionId":"${form.actionId}","objectId":"${object.id}"}}`, () => HttpResponse.json([form])), http.get(`/api/data/objects/${object.id}/instances`, () => HttpResponse.json(instances || [])), http.get(`/api/data/objects/${object.id}/instances/checkAccess`, () => HttpResponse.json({
|
|
1310
1316
|
result: true,
|
|
1311
1317
|
})));
|
|
1312
1318
|
};
|
|
@@ -1325,12 +1331,16 @@ describe('FormRenderer', () => {
|
|
|
1325
1331
|
display: {
|
|
1326
1332
|
label: 'Collection',
|
|
1327
1333
|
createActionId: '_create',
|
|
1334
|
+
updateActionId: '_update',
|
|
1328
1335
|
},
|
|
1329
1336
|
},
|
|
1330
1337
|
],
|
|
1331
1338
|
actionId: '_update',
|
|
1332
1339
|
objectId: 'testObjectForCollections',
|
|
1333
1340
|
};
|
|
1341
|
+
let collectionObject;
|
|
1342
|
+
let collectionObjectForm;
|
|
1343
|
+
let collectionObjectUpdateForm;
|
|
1334
1344
|
let scrollIntoViewMock;
|
|
1335
1345
|
let originalScrollIntoView;
|
|
1336
1346
|
beforeEach(() => {
|
|
@@ -1359,7 +1369,7 @@ describe('FormRenderer', () => {
|
|
|
1359
1369
|
],
|
|
1360
1370
|
};
|
|
1361
1371
|
setupTestMocks(collectionFormObject, form);
|
|
1362
|
-
|
|
1372
|
+
collectionObject = {
|
|
1363
1373
|
id: 'collectionObject',
|
|
1364
1374
|
name: 'Collection Object',
|
|
1365
1375
|
properties: [
|
|
@@ -1396,9 +1406,23 @@ describe('FormRenderer', () => {
|
|
|
1396
1406
|
],
|
|
1397
1407
|
defaultFormId: 'collectionObjectForm',
|
|
1398
1408
|
},
|
|
1409
|
+
{
|
|
1410
|
+
id: '_update',
|
|
1411
|
+
name: 'Update',
|
|
1412
|
+
type: 'update',
|
|
1413
|
+
outputEvent: 'updated',
|
|
1414
|
+
parameters: [
|
|
1415
|
+
{
|
|
1416
|
+
id: 'name',
|
|
1417
|
+
name: 'Name',
|
|
1418
|
+
type: 'string',
|
|
1419
|
+
},
|
|
1420
|
+
],
|
|
1421
|
+
defaultFormId: 'collectionObjectUpdateForm',
|
|
1422
|
+
},
|
|
1399
1423
|
],
|
|
1400
1424
|
};
|
|
1401
|
-
|
|
1425
|
+
collectionObjectForm = {
|
|
1402
1426
|
id: 'collectionObjectForm',
|
|
1403
1427
|
name: 'Collection Object Form',
|
|
1404
1428
|
entries: [
|
|
@@ -1426,6 +1450,26 @@ describe('FormRenderer', () => {
|
|
|
1426
1450
|
},
|
|
1427
1451
|
};
|
|
1428
1452
|
setupTestMocks(collectionObject, collectionObjectForm);
|
|
1453
|
+
collectionObjectUpdateForm = {
|
|
1454
|
+
id: 'collectionObjectUpdateForm',
|
|
1455
|
+
name: 'Collection Object Update Form',
|
|
1456
|
+
entries: [
|
|
1457
|
+
{
|
|
1458
|
+
type: 'input',
|
|
1459
|
+
parameterId: 'name',
|
|
1460
|
+
display: {
|
|
1461
|
+
label: 'Name',
|
|
1462
|
+
required: true,
|
|
1463
|
+
},
|
|
1464
|
+
},
|
|
1465
|
+
],
|
|
1466
|
+
actionId: '_update',
|
|
1467
|
+
objectId: 'collectionObject',
|
|
1468
|
+
display: {
|
|
1469
|
+
submitLabel: 'Update Collection Item',
|
|
1470
|
+
},
|
|
1471
|
+
};
|
|
1472
|
+
setupTestMocks(collectionObject, collectionObjectUpdateForm);
|
|
1429
1473
|
});
|
|
1430
1474
|
afterEach(() => {
|
|
1431
1475
|
Element.prototype.scrollIntoView = originalScrollIntoView;
|
|
@@ -1523,6 +1567,48 @@ describe('FormRenderer', () => {
|
|
|
1523
1567
|
await screen.findByRole('columnheader', { name: 'Name' });
|
|
1524
1568
|
screen.getByRole('cell', { name: 'New Collection Item' });
|
|
1525
1569
|
});
|
|
1570
|
+
it('should not send instance metadata fields when updating a collection item', async () => {
|
|
1571
|
+
const user = userEvent.setup();
|
|
1572
|
+
const updateRequestSpy = vitest.fn();
|
|
1573
|
+
const parentInstance = {
|
|
1574
|
+
id: 'testInstanceId',
|
|
1575
|
+
name: 'Test Instance',
|
|
1576
|
+
objectId: 'testObjectForCollections',
|
|
1577
|
+
};
|
|
1578
|
+
const existingCollectionItem = {
|
|
1579
|
+
id: 'existingCollectionItemId',
|
|
1580
|
+
version: 1,
|
|
1581
|
+
objectId: 'collectionObject',
|
|
1582
|
+
name: 'Existing Collection Item',
|
|
1583
|
+
relatedObject: {
|
|
1584
|
+
id: parentInstance.id,
|
|
1585
|
+
name: parentInstance.name,
|
|
1586
|
+
},
|
|
1587
|
+
};
|
|
1588
|
+
server.use(http.get('/api/data/objects/collectionObject/instances', () => HttpResponse.json([existingCollectionItem])), http.get('/api/data/objects/collectionObject/instances/existingCollectionItemId', () => HttpResponse.json(existingCollectionItem)), http.get('/api/data/objects/collectionObject/instances/existingCollectionItemId/object', () => HttpResponse.json(collectionObject)), http.get('/api/data/objects/collectionObject/instances/existingCollectionItemId/checkAccess', () => HttpResponse.json({ result: true })), http.post('/api/data/objects/collectionObject/instances/existingCollectionItemId/actions', async ({ request }) => {
|
|
1589
|
+
const body = (await request.json());
|
|
1590
|
+
updateRequestSpy(body);
|
|
1591
|
+
return HttpResponse.json({
|
|
1592
|
+
...existingCollectionItem,
|
|
1593
|
+
name: body.input.name ?? existingCollectionItem.name,
|
|
1594
|
+
});
|
|
1595
|
+
}));
|
|
1596
|
+
render(React.createElement(FormRenderer, { form: form, instance: parentInstance, onChange: () => { } }));
|
|
1597
|
+
await screen.findByRole('cell', { name: 'Existing Collection Item' });
|
|
1598
|
+
await user.click(await screen.findByLabelText('edit-collection-instance-0'));
|
|
1599
|
+
await screen.findByRole('dialog');
|
|
1600
|
+
const nameField = await screen.findByRole('textbox', { name: /Name */i });
|
|
1601
|
+
await user.clear(nameField);
|
|
1602
|
+
await user.type(nameField, 'Updated Collection Item');
|
|
1603
|
+
await user.click(screen.getByRole('button', { name: 'Update Collection Item' }));
|
|
1604
|
+
await waitFor(() => expect(updateRequestSpy).toHaveBeenCalled());
|
|
1605
|
+
const body = updateRequestSpy.mock.calls[0][0];
|
|
1606
|
+
expect(body.actionId).toBe('_update');
|
|
1607
|
+
expect(body.input).toMatchObject({ name: 'Updated Collection Item' });
|
|
1608
|
+
expect(body.input).not.toHaveProperty('id');
|
|
1609
|
+
expect(body.input).not.toHaveProperty('objectId');
|
|
1610
|
+
expect(body.input).not.toHaveProperty('version');
|
|
1611
|
+
});
|
|
1526
1612
|
it('should show validation errors', async () => {
|
|
1527
1613
|
const user = userEvent.setup();
|
|
1528
1614
|
render(React.createElement(FormRenderer, { form: form, onChange: () => { } }));
|
|
@@ -1648,4 +1734,139 @@ describe('FormRenderer', () => {
|
|
|
1648
1734
|
expect(textField).toHaveValue('Test Input');
|
|
1649
1735
|
});
|
|
1650
1736
|
});
|
|
1737
|
+
describe('when passed a formlet entry', () => {
|
|
1738
|
+
const formlet = {
|
|
1739
|
+
id: 'formletId',
|
|
1740
|
+
name: 'Test Formlet',
|
|
1741
|
+
entries: [
|
|
1742
|
+
{
|
|
1743
|
+
type: 'inputField',
|
|
1744
|
+
input: {
|
|
1745
|
+
type: 'date',
|
|
1746
|
+
id: 'dateId2',
|
|
1747
|
+
},
|
|
1748
|
+
display: {
|
|
1749
|
+
label: 'Date 2',
|
|
1750
|
+
required: false,
|
|
1751
|
+
},
|
|
1752
|
+
},
|
|
1753
|
+
],
|
|
1754
|
+
};
|
|
1755
|
+
beforeEach(() => {
|
|
1756
|
+
server.use(http.get('/api/data/formlets/formletId', () => HttpResponse.json(formlet)));
|
|
1757
|
+
});
|
|
1758
|
+
it('should render formlet entry fields', async () => {
|
|
1759
|
+
const form = {
|
|
1760
|
+
id: 'formWithFormlet',
|
|
1761
|
+
name: 'Form With Formlet',
|
|
1762
|
+
entries: [
|
|
1763
|
+
{
|
|
1764
|
+
type: 'formlet',
|
|
1765
|
+
formletId: 'formletId',
|
|
1766
|
+
},
|
|
1767
|
+
],
|
|
1768
|
+
actionId: '_update',
|
|
1769
|
+
objectId: 'formletTestObject',
|
|
1770
|
+
};
|
|
1771
|
+
render(React.createElement(FormRenderer, { form: form, onChange: () => { } }));
|
|
1772
|
+
await screen.findByLabelText('Date 2');
|
|
1773
|
+
});
|
|
1774
|
+
});
|
|
1775
|
+
describe('convertToReadOnly', () => {
|
|
1776
|
+
it('converts input entries to readonlyField entries', () => {
|
|
1777
|
+
const inputEntry = {
|
|
1778
|
+
type: 'input',
|
|
1779
|
+
parameterId: 'name',
|
|
1780
|
+
display: {
|
|
1781
|
+
label: 'Name',
|
|
1782
|
+
required: true,
|
|
1783
|
+
},
|
|
1784
|
+
};
|
|
1785
|
+
const result = convertToReadOnly(inputEntry);
|
|
1786
|
+
const display = inputEntry.display;
|
|
1787
|
+
expect(result).toEqual({
|
|
1788
|
+
type: 'readonlyField',
|
|
1789
|
+
propertyId: 'name',
|
|
1790
|
+
display,
|
|
1791
|
+
});
|
|
1792
|
+
});
|
|
1793
|
+
it('converts inputField entries to readonlyField entries', () => {
|
|
1794
|
+
const inputFieldEntry = {
|
|
1795
|
+
type: 'inputField',
|
|
1796
|
+
input: {
|
|
1797
|
+
id: 'description',
|
|
1798
|
+
type: 'string',
|
|
1799
|
+
},
|
|
1800
|
+
display: {
|
|
1801
|
+
label: 'Description',
|
|
1802
|
+
},
|
|
1803
|
+
};
|
|
1804
|
+
const result = convertToReadOnly(inputFieldEntry);
|
|
1805
|
+
const display = inputFieldEntry.display;
|
|
1806
|
+
expect(result).toEqual({
|
|
1807
|
+
type: 'readonlyField',
|
|
1808
|
+
propertyId: 'description',
|
|
1809
|
+
display,
|
|
1810
|
+
});
|
|
1811
|
+
});
|
|
1812
|
+
it('recursively converts nested section and column entries', () => {
|
|
1813
|
+
const nestedEntry = {
|
|
1814
|
+
type: 'sections',
|
|
1815
|
+
sections: [
|
|
1816
|
+
{
|
|
1817
|
+
entries: [
|
|
1818
|
+
{
|
|
1819
|
+
type: 'input',
|
|
1820
|
+
parameterId: 'firstName',
|
|
1821
|
+
display: {
|
|
1822
|
+
label: 'First Name',
|
|
1823
|
+
},
|
|
1824
|
+
},
|
|
1825
|
+
{
|
|
1826
|
+
type: 'columns',
|
|
1827
|
+
columns: [
|
|
1828
|
+
{
|
|
1829
|
+
entries: [
|
|
1830
|
+
{
|
|
1831
|
+
type: 'inputField',
|
|
1832
|
+
input: {
|
|
1833
|
+
id: 'lastName',
|
|
1834
|
+
type: 'string',
|
|
1835
|
+
},
|
|
1836
|
+
display: {
|
|
1837
|
+
label: 'Last Name',
|
|
1838
|
+
},
|
|
1839
|
+
},
|
|
1840
|
+
],
|
|
1841
|
+
},
|
|
1842
|
+
],
|
|
1843
|
+
},
|
|
1844
|
+
],
|
|
1845
|
+
},
|
|
1846
|
+
],
|
|
1847
|
+
};
|
|
1848
|
+
const result = convertToReadOnly(nestedEntry);
|
|
1849
|
+
expect(result.sections[0].entries?.[0]).toMatchObject({
|
|
1850
|
+
type: 'readonlyField',
|
|
1851
|
+
propertyId: 'firstName',
|
|
1852
|
+
});
|
|
1853
|
+
expect(result.sections[0].entries?.[1]).toMatchObject({
|
|
1854
|
+
type: 'columns',
|
|
1855
|
+
});
|
|
1856
|
+
const nestedColumnEntry = (result.sections[0].entries?.[1])
|
|
1857
|
+
.columns[0].entries?.[0];
|
|
1858
|
+
expect(nestedColumnEntry).toMatchObject({
|
|
1859
|
+
type: 'readonlyField',
|
|
1860
|
+
propertyId: 'lastName',
|
|
1861
|
+
});
|
|
1862
|
+
});
|
|
1863
|
+
it('returns unsupported entry types unchanged', () => {
|
|
1864
|
+
const contentEntry = {
|
|
1865
|
+
type: 'content',
|
|
1866
|
+
html: '<div>Read only content</div>',
|
|
1867
|
+
};
|
|
1868
|
+
const result = convertToReadOnly(contentEntry);
|
|
1869
|
+
expect(result).toBe(contentEntry);
|
|
1870
|
+
});
|
|
1871
|
+
});
|
|
1651
1872
|
});
|