@evoke-platform/ui-components 1.8.0-testing.1 → 1.8.0-testing.11
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/core/TextField/TextField.js +3 -2
- package/dist/published/components/custom/DataGrid/index.d.ts +1 -0
- package/dist/published/components/custom/Form/FormComponents/ObjectComponent/ObjectPropertyInput.js +4 -0
- package/dist/published/components/custom/Form/FormComponents/UserComponent/UserProperty.js +4 -0
- package/dist/published/components/custom/Form/utils.js +76 -44
- package/dist/published/components/custom/FormV2/FormRenderer.d.ts +6 -2
- package/dist/published/components/custom/FormV2/FormRenderer.js +13 -14
- package/dist/published/components/custom/FormV2/FormRendererContainer.d.ts +7 -2
- package/dist/published/components/custom/FormV2/FormRendererContainer.js +56 -109
- package/dist/published/components/custom/FormV2/components/FormContext.d.ts +4 -0
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/ActionDialog.d.ts +9 -5
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/ActionDialog.js +12 -24
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/RepeatableField.d.ts +5 -1
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/RepeatableField.js +80 -30
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/UserProperty.js +1 -1
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/InstanceLookup.js +1 -1
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/ObjectPropertyInput.js +51 -27
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/RelatedObjectInstance.d.ts +5 -5
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/RelatedObjectInstance.js +45 -7
- package/dist/published/components/custom/FormV2/components/RecursiveEntryRenderer.js +8 -6
- package/dist/published/components/custom/FormV2/components/ValidationFiles/ValidationErrorDisplay.d.ts +3 -0
- package/dist/published/components/custom/FormV2/components/ValidationFiles/ValidationErrorDisplay.js +1 -3
- package/dist/published/components/custom/FormV2/components/types.d.ts +7 -1
- package/dist/published/components/custom/FormV2/components/utils.d.ts +27 -2
- package/dist/published/components/custom/FormV2/components/utils.js +108 -2
- package/dist/published/components/custom/FormV2/tests/FormRenderer.test.d.ts +1 -0
- package/dist/published/components/custom/FormV2/tests/FormRenderer.test.js +173 -0
- package/dist/published/components/custom/FormV2/tests/FormRendererContainer.test.d.ts +1 -0
- package/dist/published/components/custom/FormV2/tests/FormRendererContainer.test.js +96 -0
- package/dist/published/components/custom/FormV2/tests/test-data.d.ts +16 -0
- package/dist/published/components/custom/FormV2/tests/test-data.js +394 -0
- package/dist/published/components/custom/index.d.ts +1 -0
- package/dist/published/index.d.ts +1 -1
- package/dist/published/stories/FormRenderer.stories.d.ts +7 -0
- package/dist/published/stories/FormRenderer.stories.js +65 -0
- package/dist/published/stories/FormRendererContainer.stories.d.ts +7 -0
- package/dist/published/stories/FormRendererContainer.stories.js +56 -0
- package/dist/published/stories/FormRendererData.d.ts +116 -0
- package/dist/published/stories/FormRendererData.js +925 -0
- package/dist/published/stories/sharedMswHandlers.d.ts +1 -0
- package/dist/published/stories/sharedMswHandlers.js +100 -0
- package/dist/published/theme/hooks.d.ts +4 -0
- package/package.json +13 -4
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
+
import { FieldErrors } from 'react-hook-form';
|
|
2
3
|
export type ValidationErrorDisplayProps = {
|
|
3
4
|
formId: string;
|
|
4
5
|
title?: string;
|
|
6
|
+
errors?: FieldErrors;
|
|
7
|
+
showSubmitError?: boolean;
|
|
5
8
|
};
|
|
6
9
|
declare function ValidationErrorDisplay(props: ValidationErrorDisplayProps): React.JSX.Element | null;
|
|
7
10
|
export default ValidationErrorDisplay;
|
package/dist/published/components/custom/FormV2/components/ValidationFiles/ValidationErrorDisplay.js
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { useResponsive } from '../../../../../theme';
|
|
3
|
-
import { useFormContext } from '../../../../../theme/hooks';
|
|
4
3
|
import { List, ListItem, Typography } from '../../../../core';
|
|
5
4
|
import { Box } from '../../../../layout';
|
|
6
5
|
function ValidationErrorDisplay(props) {
|
|
7
|
-
const { formId, title } = props;
|
|
8
|
-
const { errors, showSubmitError } = useFormContext();
|
|
6
|
+
const { formId, title, errors, showSubmitError } = props;
|
|
9
7
|
const { isSm, isXs } = useResponsive();
|
|
10
8
|
function extractErrorMessages(errors) {
|
|
11
9
|
const messages = [];
|
|
@@ -38,7 +38,7 @@ export type SimpleEditorProps = {
|
|
|
38
38
|
};
|
|
39
39
|
export type ObjectPropertyInputProps = {
|
|
40
40
|
id: string;
|
|
41
|
-
fieldDefinition: InputParameter;
|
|
41
|
+
fieldDefinition: InputParameter | Property;
|
|
42
42
|
mode: 'default' | 'existingOnly' | 'newOnly';
|
|
43
43
|
nestedFieldsView?: boolean;
|
|
44
44
|
readOnly?: boolean;
|
|
@@ -53,6 +53,8 @@ export type ObjectPropertyInputProps = {
|
|
|
53
53
|
initialValue?: ObjectInstance | null;
|
|
54
54
|
viewLayout?: ViewLayoutEntityReference;
|
|
55
55
|
hasDescription?: boolean;
|
|
56
|
+
createActionId?: string;
|
|
57
|
+
formId?: string;
|
|
56
58
|
};
|
|
57
59
|
export type Page = {
|
|
58
60
|
id: string;
|
|
@@ -85,6 +87,10 @@ export type ExpandedSection = Section & {
|
|
|
85
87
|
export type EntryRendererProps = BaseProps & {
|
|
86
88
|
entry: FormEntry;
|
|
87
89
|
isDocument?: boolean;
|
|
90
|
+
associatedObject?: {
|
|
91
|
+
instanceId?: string;
|
|
92
|
+
propertyId?: string;
|
|
93
|
+
};
|
|
88
94
|
};
|
|
89
95
|
export type SectionsProps = {
|
|
90
96
|
entry: Sections;
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
|
|
1
|
+
/// <reference types="react" />
|
|
2
|
+
import { Action, ApiServices, Column, Columns, FormEntry, InputField, InputParameter, InputParameterReference, Obj, ObjectInstance, Property, Section, Sections, UserAccount } from '@evoke-platform/context';
|
|
2
3
|
import { LocalDateTime } from '@js-joda/core';
|
|
3
4
|
import { FieldErrors, FieldValues } from 'react-hook-form';
|
|
4
5
|
import { AutocompleteOption } from '../../../core';
|
|
5
|
-
import { Document, DocumentData } from './types';
|
|
6
|
+
import { Document, DocumentData, SavedDocumentReference } from './types';
|
|
6
7
|
export declare const scrollIntoViewWithOffset: (el: HTMLElement, offset: number, container?: HTMLElement) => void;
|
|
7
8
|
export declare const normalizeDateTime: (dateTime: LocalDateTime) => string;
|
|
8
9
|
export declare function isAddressProperty(key: string): boolean;
|
|
@@ -61,3 +62,27 @@ export declare function formatDataToDoc(data: DocumentData): {
|
|
|
61
62
|
export declare function getUnnestedEntries(entries: FormEntry[]): FormEntry[];
|
|
62
63
|
export declare const isEmptyWithDefault: (fieldValue: unknown, entry: InputParameterReference | InputField, instance: Record<string, unknown> | object) => boolean | "" | 0 | undefined;
|
|
63
64
|
export declare const docProperties: Property[];
|
|
65
|
+
export declare const uploadDocuments: (files: (File | SavedDocumentReference)[], metadata: Record<string, string>, apiServices: ApiServices, instanceId: string, objectId: string) => Promise<SavedDocumentReference[]>;
|
|
66
|
+
export declare const deleteDocuments: (submittedFields: FieldValues, requestSuccess: boolean, apiServices: ApiServices, object: Obj, instance: FieldValues, action?: Action, setSnackbarError?: React.Dispatch<React.SetStateAction<{
|
|
67
|
+
showAlert: boolean;
|
|
68
|
+
message?: string;
|
|
69
|
+
isError: boolean;
|
|
70
|
+
}>>) => Promise<void>;
|
|
71
|
+
/**
|
|
72
|
+
* Transforms a form submission into a format safe for API submission.
|
|
73
|
+
*
|
|
74
|
+
* Responsibilities:
|
|
75
|
+
* - Uploads any files found in submission fields.
|
|
76
|
+
* - Normalizes related objects (keeping only id and name not the whole instance).
|
|
77
|
+
* - Converts an object of undefined address fields to undefined instead of an empty object.
|
|
78
|
+
* - Normalizes LocalDateTime values to API-friendly format.
|
|
79
|
+
* - Converts empty strings or undefined values to null.
|
|
80
|
+
* - Optionally reports file upload errors via snackbar.
|
|
81
|
+
*
|
|
82
|
+
* Returns the cleaned submission ready for submitting.
|
|
83
|
+
*/
|
|
84
|
+
export declare function formatSubmission(submission: FieldValues, apiServices?: ApiServices, objectId?: string, instanceId?: string, setSnackbarError?: React.Dispatch<React.SetStateAction<{
|
|
85
|
+
showAlert: boolean;
|
|
86
|
+
message?: string;
|
|
87
|
+
isError: boolean;
|
|
88
|
+
}>>): Promise<FieldValues>;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { LocalDateTime } from '@js-joda/core';
|
|
2
2
|
import jsonLogic from 'json-logic-js';
|
|
3
|
-
import { get, isArray, isEmpty, isObject, omit, startCase, transform } from 'lodash';
|
|
3
|
+
import { get, isArray, isEmpty, isObject, omit, pick, startCase, transform } from 'lodash';
|
|
4
4
|
import { DateTime } from 'luxon';
|
|
5
5
|
import Handlebars from 'no-eval-handlebars';
|
|
6
6
|
import { defaultRuleProcessorMongoDB, formatQuery, parseMongoDB } from 'react-querybuilder';
|
|
@@ -116,7 +116,7 @@ export const getEntryId = (entry) => {
|
|
|
116
116
|
};
|
|
117
117
|
export function getPrefixedUrl(url) {
|
|
118
118
|
const wcsMatchers = ['/apps', '/pages', '/widgets'];
|
|
119
|
-
const dataMatchers = ['/objects', '/correspondenceTemplates', '/documents', '/payments', '/locations'];
|
|
119
|
+
const dataMatchers = ['/objects', '/correspondenceTemplates', '/documents', '/payments', '/forms', '/locations'];
|
|
120
120
|
const signalrMatchers = ['/hubs'];
|
|
121
121
|
const accessManagementMatchers = ['/users'];
|
|
122
122
|
const workflowMatchers = ['/workflows'];
|
|
@@ -562,3 +562,109 @@ export const docProperties = [
|
|
|
562
562
|
type: 'string',
|
|
563
563
|
},
|
|
564
564
|
];
|
|
565
|
+
export const uploadDocuments = async (files, metadata, apiServices, instanceId, objectId) => {
|
|
566
|
+
const allDocuments = [];
|
|
567
|
+
const formData = new FormData();
|
|
568
|
+
for (const [index, file] of files.entries()) {
|
|
569
|
+
if ('size' in file) {
|
|
570
|
+
formData.append(`files[${index}]`, file);
|
|
571
|
+
}
|
|
572
|
+
else {
|
|
573
|
+
allDocuments.push(file);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
if (metadata) {
|
|
577
|
+
for (const [key, value] of Object.entries(metadata)) {
|
|
578
|
+
formData.append(key, value);
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
const docs = await apiServices.post(getPrefixedUrl(`/objects/${objectId}/instances/${instanceId}/documents`), formData);
|
|
582
|
+
return allDocuments.concat(docs?.map((doc) => ({
|
|
583
|
+
id: doc.id,
|
|
584
|
+
name: doc.name,
|
|
585
|
+
})) ?? []);
|
|
586
|
+
};
|
|
587
|
+
export const deleteDocuments = async (submittedFields, requestSuccess, apiServices, object, instance, action, setSnackbarError) => {
|
|
588
|
+
const documentProperties = action?.parameters
|
|
589
|
+
? action.parameters.filter((param) => param.type === 'document')
|
|
590
|
+
: object?.properties?.filter((prop) => prop.type === 'document');
|
|
591
|
+
for (const docProperty of documentProperties ?? []) {
|
|
592
|
+
const savedValue = submittedFields[docProperty.id];
|
|
593
|
+
const originalValue = instance?.[docProperty.id];
|
|
594
|
+
const documentsToRemove = requestSuccess
|
|
595
|
+
? (originalValue?.filter((file) => !savedValue?.some((f) => f.id === file.id)) ?? [])
|
|
596
|
+
: (savedValue?.filter((file) => !originalValue?.some((f) => f.id === file.id)) ?? []);
|
|
597
|
+
for (const doc of documentsToRemove) {
|
|
598
|
+
try {
|
|
599
|
+
await apiServices?.delete(getPrefixedUrl(`/objects/${object.id}/instances/${instance.id}/documents/${doc.id}`));
|
|
600
|
+
}
|
|
601
|
+
catch (error) {
|
|
602
|
+
if (error) {
|
|
603
|
+
setSnackbarError &&
|
|
604
|
+
setSnackbarError({
|
|
605
|
+
showAlert: true,
|
|
606
|
+
message: `An error occurred while removing document '${doc.name}'`,
|
|
607
|
+
isError: true,
|
|
608
|
+
});
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
};
|
|
614
|
+
/**
|
|
615
|
+
* Transforms a form submission into a format safe for API submission.
|
|
616
|
+
*
|
|
617
|
+
* Responsibilities:
|
|
618
|
+
* - Uploads any files found in submission fields.
|
|
619
|
+
* - Normalizes related objects (keeping only id and name not the whole instance).
|
|
620
|
+
* - Converts an object of undefined address fields to undefined instead of an empty object.
|
|
621
|
+
* - Normalizes LocalDateTime values to API-friendly format.
|
|
622
|
+
* - Converts empty strings or undefined values to null.
|
|
623
|
+
* - Optionally reports file upload errors via snackbar.
|
|
624
|
+
*
|
|
625
|
+
* Returns the cleaned submission ready for submitting.
|
|
626
|
+
*/
|
|
627
|
+
export async function formatSubmission(submission, apiServices, objectId, instanceId, setSnackbarError) {
|
|
628
|
+
for (const [key, value] of Object.entries(submission)) {
|
|
629
|
+
if (isArray(value)) {
|
|
630
|
+
const fileInArray = value.some((item) => item instanceof File);
|
|
631
|
+
if (fileInArray && instanceId && apiServices && objectId) {
|
|
632
|
+
try {
|
|
633
|
+
const uploadedDocuments = await uploadDocuments(value, {
|
|
634
|
+
type: '',
|
|
635
|
+
view_permission: '',
|
|
636
|
+
}, apiServices, instanceId, objectId);
|
|
637
|
+
submission[key] = uploadedDocuments;
|
|
638
|
+
}
|
|
639
|
+
catch (err) {
|
|
640
|
+
if (err) {
|
|
641
|
+
setSnackbarError &&
|
|
642
|
+
setSnackbarError({
|
|
643
|
+
showAlert: true,
|
|
644
|
+
message: `An error occurred while uploading associated documents`,
|
|
645
|
+
isError: true,
|
|
646
|
+
});
|
|
647
|
+
}
|
|
648
|
+
return submission;
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
// if there are address fields with no value address needs to be set to undefined to be able to submit
|
|
652
|
+
}
|
|
653
|
+
else if (typeof value === 'object' && value !== null) {
|
|
654
|
+
if (Object.values(value).every((v) => v === undefined)) {
|
|
655
|
+
submission[key] = undefined;
|
|
656
|
+
// only submit the name and id of a related object
|
|
657
|
+
}
|
|
658
|
+
else if ('id' in value && 'name' in value) {
|
|
659
|
+
submission[key] = pick(value, 'id', 'name');
|
|
660
|
+
}
|
|
661
|
+
else if (value instanceof LocalDateTime) {
|
|
662
|
+
submission[key] = normalizeDateTime(value);
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
else if (value === '' || value === undefined) {
|
|
666
|
+
submission[key] = null;
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
return submission;
|
|
670
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import * as matchers from '@testing-library/jest-dom/matchers';
|
|
2
|
+
import { render, screen, waitFor, within } from '@testing-library/react';
|
|
3
|
+
import userEvent from '@testing-library/user-event';
|
|
4
|
+
import { isEqual, set } from 'lodash';
|
|
5
|
+
import { http, HttpResponse } from 'msw';
|
|
6
|
+
import { setupServer } from 'msw/node';
|
|
7
|
+
import React from 'react';
|
|
8
|
+
import { MemoryRouter } from 'react-router-dom';
|
|
9
|
+
import { expect, it } from 'vitest';
|
|
10
|
+
import FormRenderer from '../FormRenderer';
|
|
11
|
+
import { createSpecialtyForm, jsonLogicDisplayTestSpecialtyForm, licenseObject, npLicense, npSpecialtyType1, npSpecialtyType2, rnLicense, rnSpecialtyType1, rnSpecialtyType2, simpleConditionDisplayTestSpecialtyForm, specialtyObject, specialtyTypeObject, } from './test-data';
|
|
12
|
+
expect.extend(matchers);
|
|
13
|
+
const removePoppers = () => {
|
|
14
|
+
const portalSelectors = ['.MuiAutocomplete-popper'];
|
|
15
|
+
portalSelectors.forEach((selector) => {
|
|
16
|
+
// eslint-disable-next-line testing-library/no-node-access
|
|
17
|
+
document.querySelectorAll(selector).forEach((el) => el.remove());
|
|
18
|
+
});
|
|
19
|
+
};
|
|
20
|
+
describe('Form component', () => {
|
|
21
|
+
let server;
|
|
22
|
+
beforeAll(() => {
|
|
23
|
+
server = setupServer(http.get('/api/data/objects/specialtyType/effective', () => HttpResponse.json(specialtyTypeObject)), http.get('/api/data/objects/specialtyType/effective', (req) => {
|
|
24
|
+
const sanitizedVersion = new URL(req.request.url).searchParams.get('sanitizedVersion');
|
|
25
|
+
if (sanitizedVersion === 'true') {
|
|
26
|
+
return HttpResponse.json(specialtyTypeObject);
|
|
27
|
+
}
|
|
28
|
+
}), http.get('/api/data/objects/license/effective', (req) => {
|
|
29
|
+
const sanitizedVersion = new URL(req.request.url).searchParams.get('sanitizedVersion');
|
|
30
|
+
if (sanitizedVersion === 'true') {
|
|
31
|
+
return HttpResponse.json(licenseObject);
|
|
32
|
+
}
|
|
33
|
+
}), http.get('/api/data/objects/specialty/effective', (req) => {
|
|
34
|
+
const sanitizedVersion = new URL(req.request.url).searchParams.get('sanitizedVersion');
|
|
35
|
+
if (sanitizedVersion === 'true') {
|
|
36
|
+
return HttpResponse.json(specialtyObject);
|
|
37
|
+
}
|
|
38
|
+
}), http.get('/data/objects/license/effective', () => HttpResponse.json(licenseObject)), http.get('/api/data/objects/license/instances', () => {
|
|
39
|
+
return HttpResponse.json([rnLicense, npLicense]);
|
|
40
|
+
}), http.get('/api/data/objects/specialtyType/instances', (req) => {
|
|
41
|
+
const filter = new URL(req.request.url).searchParams.get('filter');
|
|
42
|
+
if (filter) {
|
|
43
|
+
const whereFilter = JSON.parse(filter).where;
|
|
44
|
+
// The two objects in the array of conditions in the "where" filter represent the potential filters that can be applied when retrieving "specialty" instances.
|
|
45
|
+
// 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.
|
|
46
|
+
// 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.
|
|
47
|
+
if (isEqual(whereFilter, { and: [{}, {}] }))
|
|
48
|
+
return HttpResponse.json([
|
|
49
|
+
rnSpecialtyType1,
|
|
50
|
+
rnSpecialtyType2,
|
|
51
|
+
npSpecialtyType1,
|
|
52
|
+
npSpecialtyType2,
|
|
53
|
+
]);
|
|
54
|
+
else if (isEqual(whereFilter, { and: [{ 'licenseType.id': 'rnLicenseType' }, {}] }))
|
|
55
|
+
return HttpResponse.json([rnSpecialtyType1, rnSpecialtyType2]);
|
|
56
|
+
else if (isEqual(whereFilter, { and: [{ 'licenseType.id': 'npLicenseType' }, {}] }))
|
|
57
|
+
return HttpResponse.json([npSpecialtyType1, npSpecialtyType2]);
|
|
58
|
+
}
|
|
59
|
+
}));
|
|
60
|
+
server.listen();
|
|
61
|
+
});
|
|
62
|
+
afterAll(() => {
|
|
63
|
+
server.close();
|
|
64
|
+
});
|
|
65
|
+
afterEach(() => {
|
|
66
|
+
server.resetHandlers();
|
|
67
|
+
removePoppers();
|
|
68
|
+
});
|
|
69
|
+
describe('validation criteria', () => {
|
|
70
|
+
it(`filters related object field with validation criteria that references a related object's nested data`, async () => {
|
|
71
|
+
const user = userEvent.setup();
|
|
72
|
+
server.use(http.get('/data/objects/license/instances/rnLicense', (req) => {
|
|
73
|
+
const expand = new URL(req.request.url).searchParams.get('expand');
|
|
74
|
+
if (expand === 'licenseType.id') {
|
|
75
|
+
return HttpResponse.json(rnLicense);
|
|
76
|
+
}
|
|
77
|
+
}));
|
|
78
|
+
const FormWithState = () => {
|
|
79
|
+
const [formData, setFormData] = React.useState({});
|
|
80
|
+
const handleChange = (id, value) => {
|
|
81
|
+
setFormData((prev) => {
|
|
82
|
+
const newData = { ...prev };
|
|
83
|
+
set(newData, id, value);
|
|
84
|
+
return newData;
|
|
85
|
+
});
|
|
86
|
+
};
|
|
87
|
+
try {
|
|
88
|
+
return React.createElement(FormRenderer, { form: createSpecialtyForm, value: formData, onChange: handleChange });
|
|
89
|
+
}
|
|
90
|
+
catch (err) {
|
|
91
|
+
console.error('Render error:', err);
|
|
92
|
+
return React.createElement("div", null, "Render error");
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
render(React.createElement(MemoryRouter, null,
|
|
96
|
+
React.createElement(FormWithState, null)));
|
|
97
|
+
const license = await screen.findByRole('combobox', { name: 'License' });
|
|
98
|
+
// Step 1: Open Specialty Type and verify all options
|
|
99
|
+
await user.click(await screen.findByRole('combobox', { name: 'Specialty Type' }));
|
|
100
|
+
let openAutocomplete = await screen.findByRole('listbox');
|
|
101
|
+
await within(openAutocomplete).findByRole('option', { name: 'RN Specialty Type #1' });
|
|
102
|
+
await within(openAutocomplete).findByRole('option', { name: 'RN Specialty Type #2' });
|
|
103
|
+
await within(openAutocomplete).findByRole('option', { name: 'NP Specialty Type #1' });
|
|
104
|
+
await within(openAutocomplete).findByRole('option', { name: 'NP Specialty Type #2' });
|
|
105
|
+
await user.keyboard('{Escape}');
|
|
106
|
+
// Step 2: Select a license
|
|
107
|
+
await user.click(license);
|
|
108
|
+
await user.keyboard('{ArrowDown}');
|
|
109
|
+
await user.keyboard('{Enter}');
|
|
110
|
+
// Step 3: Open Specialty Type again and verify filtered options
|
|
111
|
+
await user.click(await screen.findByRole('combobox', { name: 'Specialty Type' }));
|
|
112
|
+
openAutocomplete = await screen.findByRole('listbox');
|
|
113
|
+
await within(openAutocomplete).findByRole('option', { name: 'RN Specialty Type #1' });
|
|
114
|
+
await within(openAutocomplete).findByRole('option', { name: 'RN Specialty Type #2' });
|
|
115
|
+
await waitFor(() => expect(within(openAutocomplete).queryByRole('option', { name: 'NP Specialty Type #1' })).not.toBeInTheDocument());
|
|
116
|
+
await waitFor(() => expect(within(openAutocomplete).queryByRole('option', { name: 'NP Specialty Type #2' })).not.toBeInTheDocument());
|
|
117
|
+
await user.keyboard('{Escape}');
|
|
118
|
+
await waitFor(() => {
|
|
119
|
+
expect(screen.queryByRole('listbox')).not.toBeInTheDocument();
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
describe('visibility configuration', () => {
|
|
124
|
+
it('shows fields based on instance data using JsonLogic', async () => {
|
|
125
|
+
server.use(http.get('/data/objects/license/instances/rnLicense', () => HttpResponse.json(rnLicense)));
|
|
126
|
+
render(React.createElement(MemoryRouter, null,
|
|
127
|
+
React.createElement(FormRenderer, { form: jsonLogicDisplayTestSpecialtyForm, onChange: () => { }, instance: {
|
|
128
|
+
id: '123',
|
|
129
|
+
objectId: 'specialty',
|
|
130
|
+
name: 'Test Specialty Object Instance',
|
|
131
|
+
} })));
|
|
132
|
+
// Validate that specialty type dropdown renders
|
|
133
|
+
await screen.findByRole('combobox', { name: 'Specialty Type' });
|
|
134
|
+
});
|
|
135
|
+
it('hides fields based on instance data using JsonLogic', async () => {
|
|
136
|
+
server.use(http.get('/data/objects/license/instances/rnLicense', () => HttpResponse.json(rnLicense)));
|
|
137
|
+
render(React.createElement(MemoryRouter, null,
|
|
138
|
+
React.createElement(FormRenderer, { form: jsonLogicDisplayTestSpecialtyForm, onChange: () => { }, instance: {
|
|
139
|
+
id: '123',
|
|
140
|
+
objectId: 'specialty',
|
|
141
|
+
name: 'Test Specialty Object Instance -- hidden',
|
|
142
|
+
} })));
|
|
143
|
+
// Validate that license dropdown renders
|
|
144
|
+
await screen.findByRole('combobox', { name: 'License' });
|
|
145
|
+
// Validate that specialty type dropdown does not render
|
|
146
|
+
expect(screen.queryByRole('combobox', { name: 'Specialty Type' })).not.toBeInTheDocument();
|
|
147
|
+
});
|
|
148
|
+
it('shows fields based on instance data using simple conditions', async () => {
|
|
149
|
+
server.use(http.get('/data/objects/license/instances/rnLicense', () => HttpResponse.json(rnLicense)));
|
|
150
|
+
render(React.createElement(MemoryRouter, null,
|
|
151
|
+
React.createElement(FormRenderer, { form: simpleConditionDisplayTestSpecialtyForm, onChange: () => { }, instance: {
|
|
152
|
+
id: '123',
|
|
153
|
+
objectId: 'specialty',
|
|
154
|
+
name: 'Test Specialty Object Instance',
|
|
155
|
+
} })));
|
|
156
|
+
// Validate that specialty type dropdown renders
|
|
157
|
+
await screen.findByRole('combobox', { name: 'Specialty Type' });
|
|
158
|
+
});
|
|
159
|
+
it('hides fields based on instance data using simple conditions', async () => {
|
|
160
|
+
server.use(http.get('/data/objects/license/instances/rnLicense', () => HttpResponse.json(rnLicense)));
|
|
161
|
+
render(React.createElement(MemoryRouter, null,
|
|
162
|
+
React.createElement(FormRenderer, { form: simpleConditionDisplayTestSpecialtyForm, onChange: () => { }, instance: {
|
|
163
|
+
id: '123',
|
|
164
|
+
objectId: 'specialty',
|
|
165
|
+
name: 'Test Specialty Object Instance -- hidden',
|
|
166
|
+
} })));
|
|
167
|
+
// Validate that license dropdown renders
|
|
168
|
+
await screen.findByRole('combobox', { name: 'License' });
|
|
169
|
+
// Validate that specialty type dropdown does not render
|
|
170
|
+
expect(screen.queryByRole('combobox', { name: 'Specialty Type' })).not.toBeInTheDocument();
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import * as matchers from '@testing-library/jest-dom/matchers';
|
|
2
|
+
import { render, screen, waitFor, within } from '@testing-library/react';
|
|
3
|
+
import userEvent from '@testing-library/user-event';
|
|
4
|
+
import { isEqual } from 'lodash';
|
|
5
|
+
import { http, HttpResponse } from 'msw';
|
|
6
|
+
import { setupServer } from 'msw/node';
|
|
7
|
+
import React from 'react';
|
|
8
|
+
import { MemoryRouter } from 'react-router-dom';
|
|
9
|
+
import { expect, it } from 'vitest';
|
|
10
|
+
import FormRendererContainer from '../FormRendererContainer';
|
|
11
|
+
import { createSpecialtyForm, licenseObject, npLicense, npSpecialtyType1, npSpecialtyType2, rnLicense, rnSpecialtyType1, rnSpecialtyType2, specialtyObject, specialtyTypeObject, } from './test-data';
|
|
12
|
+
expect.extend(matchers);
|
|
13
|
+
const removePoppers = () => {
|
|
14
|
+
const portalSelectors = ['.MuiAutocomplete-popper'];
|
|
15
|
+
portalSelectors.forEach((selector) => {
|
|
16
|
+
// eslint-disable-next-line testing-library/no-node-access
|
|
17
|
+
document.querySelectorAll(selector).forEach((el) => el.remove());
|
|
18
|
+
});
|
|
19
|
+
};
|
|
20
|
+
describe('Form component', () => {
|
|
21
|
+
let server;
|
|
22
|
+
beforeAll(() => {
|
|
23
|
+
server = setupServer(http.get('/api/data/objects/specialtyType/effective', () => HttpResponse.json(specialtyTypeObject)), http.get('/api/data/objects/specialtyType/effective', (req) => {
|
|
24
|
+
const sanitizedVersion = new URL(req.request.url).searchParams.get('sanitizedVersion');
|
|
25
|
+
if (sanitizedVersion === 'true') {
|
|
26
|
+
return HttpResponse.json(specialtyTypeObject);
|
|
27
|
+
}
|
|
28
|
+
}), http.get('/api/data/objects/license/effective', (req) => {
|
|
29
|
+
const sanitizedVersion = new URL(req.request.url).searchParams.get('sanitizedVersion');
|
|
30
|
+
if (sanitizedVersion === 'true') {
|
|
31
|
+
return HttpResponse.json(licenseObject);
|
|
32
|
+
}
|
|
33
|
+
}), http.get('/api/data/objects/specialty/effective', (req) => {
|
|
34
|
+
const sanitizedVersion = new URL(req.request.url).searchParams.get('sanitizedVersion');
|
|
35
|
+
if (sanitizedVersion === 'true') {
|
|
36
|
+
return HttpResponse.json(specialtyObject);
|
|
37
|
+
}
|
|
38
|
+
}), http.get('/data/objects/license/effective', () => HttpResponse.json(licenseObject)), http.get('/api/data/objects/license/instances', () => {
|
|
39
|
+
return HttpResponse.json([rnLicense, npLicense]);
|
|
40
|
+
}), http.get('/api/data/objects/specialtyType/instances', (req) => {
|
|
41
|
+
const filter = new URL(req.request.url).searchParams.get('filter');
|
|
42
|
+
if (filter) {
|
|
43
|
+
const whereFilter = JSON.parse(filter).where;
|
|
44
|
+
// The two objects in the array of conditions in the "where" filter represent the potential filters that can be applied when retrieving "specialty" instances.
|
|
45
|
+
// 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.
|
|
46
|
+
// 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.
|
|
47
|
+
if (isEqual(whereFilter, { and: [{}, {}] }))
|
|
48
|
+
return HttpResponse.json([
|
|
49
|
+
rnSpecialtyType1,
|
|
50
|
+
rnSpecialtyType2,
|
|
51
|
+
npSpecialtyType1,
|
|
52
|
+
npSpecialtyType2,
|
|
53
|
+
]);
|
|
54
|
+
else if (isEqual(whereFilter, { and: [{ 'licenseType.id': 'rnLicenseType' }, {}] }))
|
|
55
|
+
return HttpResponse.json([rnSpecialtyType1, rnSpecialtyType2]);
|
|
56
|
+
else if (isEqual(whereFilter, { and: [{ 'licenseType.id': 'npLicenseType' }, {}] }))
|
|
57
|
+
return HttpResponse.json([npSpecialtyType1, npSpecialtyType2]);
|
|
58
|
+
}
|
|
59
|
+
}));
|
|
60
|
+
server.listen();
|
|
61
|
+
});
|
|
62
|
+
afterAll(() => {
|
|
63
|
+
server.close();
|
|
64
|
+
});
|
|
65
|
+
afterEach(() => {
|
|
66
|
+
server.resetHandlers();
|
|
67
|
+
removePoppers();
|
|
68
|
+
});
|
|
69
|
+
describe('validation criteria', () => {
|
|
70
|
+
it(`filters related object field with validation criteria that references a defaulted related object's nested data`, async () => {
|
|
71
|
+
const user = userEvent.setup();
|
|
72
|
+
server.use(http.get('/api/data/objects/license/instances/rnLicense', () => {
|
|
73
|
+
return HttpResponse.json(rnLicense);
|
|
74
|
+
}), http.get('api/data/forms/specialtyForm', () => {
|
|
75
|
+
return HttpResponse.json(createSpecialtyForm);
|
|
76
|
+
}));
|
|
77
|
+
render(React.createElement(MemoryRouter, null,
|
|
78
|
+
React.createElement(FormRendererContainer, { objectId: 'specialty', formId: 'specialtyForm', dataType: 'objectInstances', actionId: '_create', associatedObject: { propertyId: 'license', instanceId: 'rnLicense' } })));
|
|
79
|
+
// Validate that the license field is hidden
|
|
80
|
+
await waitFor(() => expect(screen.queryByRole('combobox', { name: 'License' })).not.toBeInTheDocument());
|
|
81
|
+
// Validate that specialty type dropdown is only rendering specialty types that are associated with the selected license.
|
|
82
|
+
const specialtyType = await screen.findByRole('combobox', { name: 'Specialty Type' });
|
|
83
|
+
await new Promise((r) => setTimeout(r, 5000));
|
|
84
|
+
await user.click(specialtyType);
|
|
85
|
+
const openAutocomplete = await screen.findByRole('listbox');
|
|
86
|
+
await within(openAutocomplete).findByRole('option', { name: 'RN Specialty Type #1' });
|
|
87
|
+
await within(openAutocomplete).findByRole('option', { name: 'RN Specialty Type #2' });
|
|
88
|
+
await waitFor(() => expect(within(openAutocomplete).queryByRole('option', { name: 'NP Specialty Type #1' })).not.toBeInTheDocument());
|
|
89
|
+
await waitFor(() => expect(within(openAutocomplete).queryByRole('option', { name: 'NP Specialty Type #2' })).not.toBeInTheDocument());
|
|
90
|
+
await user.keyboard('{Escape}');
|
|
91
|
+
await waitFor(() => {
|
|
92
|
+
expect(screen.queryByRole('listbox')).not.toBeInTheDocument();
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { EvokeForm, Obj, ObjectInstance } from '@evoke-platform/context';
|
|
2
|
+
export declare const createSpecialtyForm: EvokeForm;
|
|
3
|
+
export declare const simpleConditionDisplayTestSpecialtyForm: EvokeForm;
|
|
4
|
+
export declare const jsonLogicDisplayTestSpecialtyForm: EvokeForm;
|
|
5
|
+
export declare const licenseObject: Obj;
|
|
6
|
+
export declare const licenseTypeObject: Obj;
|
|
7
|
+
export declare const specialtyObject: Obj;
|
|
8
|
+
export declare const specialtyTypeObject: Obj;
|
|
9
|
+
export declare const rnLicense: ObjectInstance;
|
|
10
|
+
export declare const npLicense: ObjectInstance;
|
|
11
|
+
export declare const rnLicenseType: ObjectInstance;
|
|
12
|
+
export declare const npLicesneType: ObjectInstance;
|
|
13
|
+
export declare const rnSpecialtyType1: ObjectInstance;
|
|
14
|
+
export declare const rnSpecialtyType2: ObjectInstance;
|
|
15
|
+
export declare const npSpecialtyType1: ObjectInstance;
|
|
16
|
+
export declare const npSpecialtyType2: ObjectInstance;
|