@openmrs/esm-form-engine-lib 2.1.0-pre.1501 → 2.1.0-pre.1502
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/47245761e3f779c4/47245761e3f779c4.gz +0 -0
- package/6f1d94035d69e5e1/6f1d94035d69e5e1.gz +0 -0
- package/__mocks__/forms/rfe-forms/sample_ui-select-ext.json +48 -0
- package/aaf8197a12df0c40/aaf8197a12df0c40.gz +0 -0
- package/aba5c979c0dbf1c7/aba5c979c0dbf1c7.gz +0 -0
- package/dist/openmrs-esm-form-engine-lib.js +1 -1
- package/package.json +1 -1
- package/src/components/inputs/select/dropdown.test.tsx +2 -3
- package/src/components/inputs/ui-select-extended/ui-select-extended.component.tsx +57 -50
- package/src/components/inputs/ui-select-extended/ui-select-extended.scss +4 -0
- package/src/components/inputs/ui-select-extended/ui-select-extended.test.tsx +239 -169
- package/src/components/inputs/unspecified/unspecified.test.tsx +1 -22
- package/src/datasources/concept-data-source.ts +10 -10
- package/src/datasources/data-source.ts +11 -0
- package/src/hooks/useFormFieldsMeta.ts +1 -1
- package/src/index.ts +0 -1
- package/src/transformers/default-schema-transformer.ts +5 -0
- package/src/types/index.ts +8 -1
- package/src/types/schema.ts +2 -0
- package/src/utils/form-helper.test.ts +0 -1
- package/src/form-context.tsx +0 -42
- /package/src/hooks/{useDatasourceDependentValue.ts → useDataSourceDependentValue.ts} +0 -0
Binary file
|
Binary file
|
@@ -0,0 +1,48 @@
|
|
1
|
+
{
|
2
|
+
"encounterType": "e22e39fd-7db2-45e7-80f1-60fa0d5a4378",
|
3
|
+
"name": "Sample UI Select",
|
4
|
+
"processor": "EncounterFormProcessor",
|
5
|
+
"referencedForms": [],
|
6
|
+
"uuid": "f7768d34-8e41-4f6b-a276-12c12e023165",
|
7
|
+
"version": "1.0",
|
8
|
+
"pages": [
|
9
|
+
{
|
10
|
+
"label": "First Page",
|
11
|
+
"sections": [
|
12
|
+
{
|
13
|
+
"label": "A Section",
|
14
|
+
"isExpanded": "true",
|
15
|
+
"questions": [
|
16
|
+
{
|
17
|
+
"label": "Transfer Location",
|
18
|
+
"type": "obs",
|
19
|
+
"questionOptions": {
|
20
|
+
"rendering": "ui-select-extended",
|
21
|
+
"concept": "160540AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
|
22
|
+
"datasource": {
|
23
|
+
"name": "location_datasource",
|
24
|
+
"config": {
|
25
|
+
"tag": "test-tag"
|
26
|
+
}
|
27
|
+
}
|
28
|
+
},
|
29
|
+
"meta": {},
|
30
|
+
"id": "patient_transfer_location"
|
31
|
+
},
|
32
|
+
{
|
33
|
+
"label": "Problem",
|
34
|
+
"type": "obs",
|
35
|
+
"questionOptions": {
|
36
|
+
"isSearchable": true,
|
37
|
+
"rendering": "problem",
|
38
|
+
"concept": "4b59ac07-cf72-4f46-b8c0-4f62b1779f7e"
|
39
|
+
},
|
40
|
+
"id": "problem"
|
41
|
+
}
|
42
|
+
]
|
43
|
+
}
|
44
|
+
]
|
45
|
+
}
|
46
|
+
],
|
47
|
+
"description": "Sample UI Select"
|
48
|
+
}
|
Binary file
|
Binary file
|
@@ -1 +1 @@
|
|
1
|
-
var _openmrs_esm_form_engine_lib;(()=>{"use strict";var e,r,t,n,o,i,a,l,s,u,f,p,d,c,h,m,v,g,b={8008:(e,r,t)=>{var n={"./start":()=>Promise.all([t.e(901),t.e(
|
1
|
+
var _openmrs_esm_form_engine_lib;(()=>{"use strict";var e,r,t,n,o,i,a,l,s,u,f,p,d,c,h,m,v,g,b={8008:(e,r,t)=>{var n={"./start":()=>Promise.all([t.e(901),t.e(420),t.e(72),t.e(385),t.e(514)]).then((()=>()=>t(5514)))},o=(e,r)=>(t.R=r,r=t.o(n,e)?n[e]():Promise.resolve().then((()=>{throw new Error('Module "'+e+'" does not exist in container.')})),t.R=void 0,r),i=(e,r)=>{if(t.S){var n="default",o=t.S[n];if(o&&o!==e)throw new Error("Container initialization failed as it has already been initialized with a different share scope");return t.S[n]=e,t.I(n,r)}};t.d(r,{get:()=>o,init:()=>i})}},y={};function w(e){var r=y[e];if(void 0!==r)return r.exports;var t=y[e]={id:e,loaded:!1,exports:{}};return b[e].call(t.exports,t,t.exports,w),t.loaded=!0,t.exports}w.m=b,w.c=y,w.n=e=>{var r=e&&e.__esModule?()=>e.default:()=>e;return w.d(r,{a:r}),r},r=Object.getPrototypeOf?e=>Object.getPrototypeOf(e):e=>e.__proto__,w.t=function(t,n){if(1&n&&(t=this(t)),8&n)return t;if("object"==typeof t&&t){if(4&n&&t.__esModule)return t;if(16&n&&"function"==typeof t.then)return t}var o=Object.create(null);w.r(o);var i={};e=e||[null,r({}),r([]),r(r)];for(var a=2&n&&t;"object"==typeof a&&!~e.indexOf(a);a=r(a))Object.getOwnPropertyNames(a).forEach((e=>i[e]=()=>t[e]));return i.default=()=>t,w.d(o,i),o},w.d=(e,r)=>{for(var t in r)w.o(r,t)&&!w.o(e,t)&&Object.defineProperty(e,t,{enumerable:!0,get:r[t]})},w.f={},w.e=e=>Promise.all(Object.keys(w.f).reduce(((r,t)=>(w.f[t](e,r),r)),[])),w.u=e=>e+".js",w.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(e){if("object"==typeof window)return window}}(),w.o=(e,r)=>Object.prototype.hasOwnProperty.call(e,r),t={},n="@openmrs/esm-form-engine-lib:",w.l=(e,r,o,i)=>{if(t[e])t[e].push(r);else{var a,l;if(void 0!==o)for(var s=document.getElementsByTagName("script"),u=0;u<s.length;u++){var f=s[u];if(f.getAttribute("src")==e||f.getAttribute("data-webpack")==n+o){a=f;break}}a||(l=!0,(a=document.createElement("script")).charset="utf-8",a.timeout=120,w.nc&&a.setAttribute("nonce",w.nc),a.setAttribute("data-webpack",n+o),a.src=e),t[e]=[r];var p=(r,n)=>{a.onerror=a.onload=null,clearTimeout(d);var o=t[e];if(delete t[e],a.parentNode&&a.parentNode.removeChild(a),o&&o.forEach((e=>e(n))),r)return r(n)},d=setTimeout(p.bind(null,void 0,{type:"timeout",target:a}),12e4);a.onerror=p.bind(null,a.onerror),a.onload=p.bind(null,a.onload),l&&document.head.appendChild(a)}},w.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},w.nmd=e=>(e.paths=[],e.children||(e.children=[]),e),(()=>{w.S={};var e={},r={};w.I=(t,n)=>{n||(n=[]);var o=r[t];if(o||(o=r[t]={}),!(n.indexOf(o)>=0)){if(n.push(o),e[t])return e[t];w.o(w.S,t)||(w.S[t]={});var i=w.S[t],a="@openmrs/esm-form-engine-lib",l=(e,r,t,n)=>{var o=i[e]=i[e]||{},l=o[r];(!l||!l.loaded&&(!n!=!l.eager?n:a>l.from))&&(o[r]={get:t,from:a,eager:!!n})},s=[];return"default"===t&&(l("@openmrs/esm-framework","5.8.2-pre.2368",(()=>Promise.all([w.e(151),w.e(72),w.e(766)]).then((()=>()=>w(5151))))),l("@openmrs/esm-patient-common-lib","8.1.1-pre.5183",(()=>Promise.all([w.e(617),w.e(901),w.e(72),w.e(465),w.e(385),w.e(70)]).then((()=>()=>w(8617))))),l("dayjs","1.11.11",(()=>w.e(353).then((()=>()=>w(4353))))),l("i18next","23.11.4",(()=>w.e(635).then((()=>()=>w(2635))))),l("react-i18next","11.18.6",(()=>Promise.all([w.e(422),w.e(72)]).then((()=>()=>w(4422))))),l("react","18.3.1",(()=>w.e(540).then((()=>()=>w(6540))))),l("swr/_internal","2.2.5",(()=>Promise.all([w.e(993),w.e(72)]).then((()=>()=>w(4993))))),l("swr/immutable","2.2.5",(()=>Promise.all([w.e(225),w.e(72),w.e(465)]).then((()=>()=>w(4225))))),l("swr/infinite","2.2.5",(()=>Promise.all([w.e(41),w.e(72),w.e(465)]).then((()=>()=>w(3041)))))),e[t]=s.length?Promise.all(s).then((()=>e[t]=1)):1}}})(),(()=>{var e;w.g.importScripts&&(e=w.g.location+"");var r=w.g.document;if(!e&&r&&(r.currentScript&&(e=r.currentScript.src),!e)){var t=r.getElementsByTagName("script");if(t.length)for(var n=t.length-1;n>-1&&(!e||!/^http(s?):/.test(e));)e=t[n--].src}if(!e)throw new Error("Automatic publicPath is not supported in this browser");e=e.replace(/#.*$/,"").replace(/\?.*$/,"").replace(/\/[^\/]+$/,"/"),w.p=e})(),o=e=>{var r=e=>e.split(".").map((e=>+e==e?+e:e)),t=/^([^-+]+)?(?:-([^+]+))?(?:\+(.+))?$/.exec(e),n=t[1]?r(t[1]):[];return t[2]&&(n.length++,n.push.apply(n,r(t[2]))),t[3]&&(n.push([]),n.push.apply(n,r(t[3]))),n},i=(e,r)=>{e=o(e),r=o(r);for(var t=0;;){if(t>=e.length)return t<r.length&&"u"!=(typeof r[t])[0];var n=e[t],i=(typeof n)[0];if(t>=r.length)return"u"==i;var a=r[t],l=(typeof a)[0];if(i!=l)return"o"==i&&"n"==l||"s"==l||"u"==i;if("o"!=i&&"u"!=i&&n!=a)return n<a;t++}},a=e=>{var r=e[0],t="";if(1===e.length)return"*";if(r+.5){t+=0==r?">=":-1==r?"<":1==r?"^":2==r?"~":r>0?"=":"!=";for(var n=1,o=1;o<e.length;o++)n--,t+="u"==(typeof(l=e[o]))[0]?"-":(n>0?".":"")+(n=2,l);return t}var i=[];for(o=1;o<e.length;o++){var l=e[o];i.push(0===l?"not("+s()+")":1===l?"("+s()+" || "+s()+")":2===l?i.pop()+" "+i.pop():a(l))}return s();function s(){return i.pop().replace(/^\((.+)\)$/,"$1")}},l=(e,r)=>{if(0 in e){r=o(r);var t=e[0],n=t<0;n&&(t=-t-1);for(var i=0,a=1,s=!0;;a++,i++){var u,f,p=a<e.length?(typeof e[a])[0]:"";if(i>=r.length||"o"==(f=(typeof(u=r[i]))[0]))return!s||("u"==p?a>t&&!n:""==p!=n);if("u"==f){if(!s||"u"!=p)return!1}else if(s)if(p==f)if(a<=t){if(u!=e[a])return!1}else{if(n?u>e[a]:u<e[a])return!1;u!=e[a]&&(s=!1)}else if("s"!=p&&"n"!=p){if(n||a<=t)return!1;s=!1,a--}else{if(a<=t||f<p!=n)return!1;s=!1}else"s"!=p&&"n"!=p&&(s=!1,a--)}}var d=[],c=d.pop.bind(d);for(i=1;i<e.length;i++){var h=e[i];d.push(1==h?c()|c():2==h?c()&c():h?l(h,r):!c())}return!!c()},s=(e,r)=>{var t=e[r];return Object.keys(t).reduce(((e,r)=>!e||!t[e].loaded&&i(e,r)?r:e),0)},u=(e,r,t,n)=>"Unsatisfied version "+t+" from "+(t&&e[r][t].from)+" of shared singleton module "+r+" (required "+a(n)+")",f=(e,r,t,n)=>{var o=s(e,t);return l(n,o)||p(u(e,t,o,n)),d(e[t][o])},p=e=>{"undefined"!=typeof console&&console.warn&&console.warn(e)},d=e=>(e.loaded=1,e.get()),c=(e=>function(r,t,n,o){var i=w.I(r);return i&&i.then?i.then(e.bind(e,r,w.S[r],t,n,o)):e(0,w.S[r],t,n,o)})(((e,r,t,n,o)=>r&&w.o(r,t)?f(r,0,t,n):o())),h={},m={6072:()=>c("default","react",[1,18],(()=>w.e(540).then((()=>()=>w(6540))))),6766:()=>c("default","i18next",[1,23],(()=>w.e(635).then((()=>()=>w(2635))))),8465:()=>c("default","swr/_internal",[1,2],(()=>w.e(993).then((()=>()=>w(4993))))),3941:()=>c("default","react-i18next",[1,11],(()=>w.e(422).then((()=>()=>w(4422))))),5972:()=>c("default","@openmrs/esm-framework",[1,5],(()=>Promise.all([w.e(151),w.e(766)]).then((()=>()=>w(5151))))),6656:()=>c("default","@openmrs/esm-patient-common-lib",[1,8],(()=>Promise.all([w.e(617),w.e(465)]).then((()=>()=>w(8617))))),4209:()=>c("default","swr/immutable",[1,2],(()=>Promise.all([w.e(225),w.e(465)]).then((()=>()=>w(4225))))),231:()=>c("default","dayjs",[1,1],(()=>w.e(353).then((()=>()=>w(4353))))),6339:()=>c("default","swr/infinite",[1,2],(()=>Promise.all([w.e(41),w.e(465)]).then((()=>()=>w(3041)))))},v={70:[4209],72:[6072],385:[3941,5972,6656],465:[8465],514:[231,4209,6339],766:[6766]},g={},w.f.consumes=(e,r)=>{w.o(v,e)&&v[e].forEach((e=>{if(w.o(h,e))return r.push(h[e]);if(!g[e]){var t=r=>{h[e]=0,w.m[e]=t=>{delete w.c[e],t.exports=r()}};g[e]=!0;var n=r=>{delete h[e],w.m[e]=t=>{throw delete w.c[e],r}};try{var o=m[e]();o.then?r.push(h[e]=o.then(t).catch(n)):t(o)}catch(e){n(e)}}}))},(()=>{var e={719:0};w.f.j=(r,t)=>{var n=w.o(e,r)?e[r]:void 0;if(0!==n)if(n)t.push(n[2]);else if(/^(385|465|72|766)$/.test(r))e[r]=0;else{var o=new Promise(((t,o)=>n=e[r]=[t,o]));t.push(n[2]=o);var i=w.p+w.u(r),a=new Error;w.l(i,(t=>{if(w.o(e,r)&&(0!==(n=e[r])&&(e[r]=void 0),n)){var o=t&&("load"===t.type?"missing":t.type),i=t&&t.target&&t.target.src;a.message="Loading chunk "+r+" failed.\n("+o+": "+i+")",a.name="ChunkLoadError",a.type=o,a.request=i,n[1](a)}}),"chunk-"+r,r)}};var r=(r,t)=>{var n,o,[i,a,l]=t,s=0;if(i.some((r=>0!==e[r]))){for(n in a)w.o(a,n)&&(w.m[n]=a[n]);l&&l(w)}for(r&&r(t);s<i.length;s++)o=i[s],w.o(e,o)&&e[o]&&e[o][0](),e[o]=0},t=globalThis.webpackChunk_openmrs_esm_form_engine_lib=globalThis.webpackChunk_openmrs_esm_form_engine_lib||[];t.forEach(r.bind(null,0)),t.push=r.bind(null,t.push.bind(t))})(),w.nc=void 0;var _=w(8008);_openmrs_esm_form_engine_lib=_})();
|
package/package.json
CHANGED
@@ -1,6 +1,5 @@
|
|
1
1
|
import React from 'react';
|
2
2
|
import { act, fireEvent, render, screen } from '@testing-library/react';
|
3
|
-
import { type EncounterContext, FormContext } from '../../../form-context';
|
4
3
|
import Dropdown from './dropdown.component';
|
5
4
|
import { type FormField } from '../../../types';
|
6
5
|
|
@@ -29,7 +28,7 @@ const question: FormField = {
|
|
29
28
|
id: 'patient-past-program',
|
30
29
|
};
|
31
30
|
|
32
|
-
const encounterContext
|
31
|
+
const encounterContext = {
|
33
32
|
patient: {
|
34
33
|
id: '833db896-c1f0-11eb-8529-0242ac130003',
|
35
34
|
},
|
@@ -154,4 +153,4 @@ describe.skip('dropdown input field', () => {
|
|
154
153
|
});
|
155
154
|
});
|
156
155
|
});
|
157
|
-
});
|
156
|
+
});
|
@@ -1,87 +1,89 @@
|
|
1
|
-
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
1
|
+
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
2
2
|
import debounce from 'lodash-es/debounce';
|
3
|
-
import { ComboBox, DropdownSkeleton, Layer } from '@carbon/react';
|
3
|
+
import { ComboBox, DropdownSkeleton, Layer, InlineLoading } from '@carbon/react';
|
4
4
|
import { isTrue } from '../../../utils/boolean-utils';
|
5
5
|
import { useTranslation } from 'react-i18next';
|
6
6
|
import { getRegisteredDataSource } from '../../../registry/registry';
|
7
7
|
import { getControlTemplate } from '../../../registry/inbuilt-components/control-templates';
|
8
|
-
import { type FormFieldInputProps } from '../../../types';
|
8
|
+
import { type DataSource, type FormFieldInputProps } from '../../../types';
|
9
9
|
import { isEmpty } from '../../../validators/form-validator';
|
10
10
|
import { shouldUseInlineLayout } from '../../../utils/form-helper';
|
11
11
|
import FieldValueView from '../../value/view/field-value-view.component';
|
12
12
|
import styles from './ui-select-extended.scss';
|
13
13
|
import { useFormProviderContext } from '../../../provider/form-provider';
|
14
14
|
import FieldLabel from '../../field-label/field-label.component';
|
15
|
-
import useDataSourceDependentValue from '../../../hooks/useDatasourceDependentValue';
|
16
15
|
import { useWatch } from 'react-hook-form';
|
16
|
+
import useDataSourceDependentValue from '../../../hooks/useDataSourceDependentValue';
|
17
|
+
import { isViewMode } from '../../../utils/common-utils';
|
18
|
+
import { type OpenmrsResource } from '@openmrs/esm-framework';
|
17
19
|
|
18
20
|
const UiSelectExtended: React.FC<FormFieldInputProps> = ({ field, errors, warnings, setFieldValue }) => {
|
19
21
|
const { t } = useTranslation();
|
20
22
|
const [items, setItems] = useState([]);
|
21
23
|
const [isLoading, setIsLoading] = useState(false);
|
24
|
+
const [isSearching, setIsSearching] = useState(false);
|
22
25
|
const [searchTerm, setSearchTerm] = useState('');
|
23
26
|
const isProcessingSelection = useRef(false);
|
24
27
|
const [dataSource, setDataSource] = useState(null);
|
25
28
|
const [config, setConfig] = useState({});
|
26
|
-
const [savedSearchableItem, setSavedSearchableItem] = useState({});
|
27
29
|
const dataSourceDependentValue = useDataSourceDependentValue(field);
|
30
|
+
const isSearchable = isTrue(field.questionOptions.isSearchable);
|
28
31
|
const {
|
29
32
|
layoutType,
|
30
33
|
sessionMode,
|
31
34
|
workspaceLayout,
|
32
|
-
methods: { control },
|
35
|
+
methods: { control, getFieldState },
|
33
36
|
} = useFormProviderContext();
|
34
37
|
|
35
38
|
const value = useWatch({ control, name: field.id, exact: true });
|
39
|
+
const { isDirty } = getFieldState(field.id);
|
36
40
|
|
37
41
|
const isInline = useMemo(() => {
|
38
|
-
if (
|
42
|
+
if (isViewMode(sessionMode) || isTrue(field.readonly)) {
|
39
43
|
return shouldUseInlineLayout(field.inlineRendering, layoutType, workspaceLayout, sessionMode);
|
40
44
|
}
|
41
45
|
return false;
|
42
46
|
}, [sessionMode, field.readonly, field.inlineRendering, layoutType, workspaceLayout]);
|
43
47
|
|
44
|
-
|
45
|
-
const dataSource = field.questionOptions?.datasource?.name;
|
46
|
-
setConfig(
|
47
|
-
dataSource
|
48
|
-
? field.questionOptions.datasource?.config
|
49
|
-
: getControlTemplate(field.questionOptions.rendering)?.datasource?.config,
|
50
|
-
);
|
51
|
-
getRegisteredDataSource(dataSource ? dataSource : field.questionOptions.rendering).then((ds) => setDataSource(ds));
|
52
|
-
}, [field.questionOptions?.datasource]);
|
48
|
+
const selectedItem = useMemo(() => items.find((item) => item.uuid == value) || null, [items, value]);
|
53
49
|
|
54
|
-
const
|
55
|
-
|
56
|
-
const debouncedSearch = debounce((searchTerm, dataSource) => {
|
57
|
-
setItems([]);
|
58
|
-
setIsLoading(true);
|
50
|
+
const debouncedSearch = debounce((searchTerm: string, dataSource: DataSource<OpenmrsResource>) => {
|
51
|
+
setIsSearching(true);
|
59
52
|
dataSource
|
60
53
|
.fetchData(searchTerm, config)
|
61
54
|
.then((dataItems) => {
|
62
|
-
|
63
|
-
|
55
|
+
if (dataItems.length) {
|
56
|
+
const currentSelectedItem = items.find((item) => item.uuid == value);
|
57
|
+
const newItems = dataItems.map(dataSource.toUuidAndDisplay);
|
58
|
+
if (currentSelectedItem && !newItems.some((item) => item.uuid == currentSelectedItem.uuid)) {
|
59
|
+
newItems.unshift(currentSelectedItem);
|
60
|
+
}
|
61
|
+
setItems(newItems);
|
62
|
+
}
|
63
|
+
setIsSearching(false);
|
64
64
|
})
|
65
65
|
.catch((err) => {
|
66
66
|
console.error(err);
|
67
|
-
|
68
|
-
setItems([]);
|
67
|
+
setIsSearching(false);
|
69
68
|
});
|
70
69
|
}, 300);
|
71
70
|
|
72
|
-
const
|
73
|
-
|
74
|
-
.
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
71
|
+
const searchTermHasMatchingItem = useCallback(
|
72
|
+
(searchTerm: string) => {
|
73
|
+
return items.some((item) => item.display?.toLowerCase().includes(searchTerm.toLowerCase()));
|
74
|
+
},
|
75
|
+
[items],
|
76
|
+
);
|
77
|
+
|
78
|
+
useEffect(() => {
|
79
|
+
const dataSource = field.questionOptions?.datasource?.name;
|
80
|
+
setConfig(
|
81
|
+
dataSource
|
82
|
+
? field.questionOptions.datasource?.config
|
83
|
+
: getControlTemplate(field.questionOptions.rendering)?.datasource?.config,
|
84
|
+
);
|
85
|
+
getRegisteredDataSource(dataSource ? dataSource : field.questionOptions.rendering).then((ds) => setDataSource(ds));
|
86
|
+
}, [field.questionOptions?.datasource]);
|
85
87
|
|
86
88
|
useEffect(() => {
|
87
89
|
// If not searchable, preload the items
|
@@ -103,29 +105,32 @@ const UiSelectExtended: React.FC<FormFieldInputProps> = ({ field, errors, warnin
|
|
103
105
|
}, [dataSource, config, dataSourceDependentValue]);
|
104
106
|
|
105
107
|
useEffect(() => {
|
106
|
-
if (dataSource &&
|
108
|
+
if (dataSource && isSearchable && !isEmpty(searchTerm) && !searchTermHasMatchingItem(searchTerm)) {
|
107
109
|
debouncedSearch(searchTerm, dataSource);
|
108
110
|
}
|
109
111
|
}, [dataSource, searchTerm, config]);
|
110
112
|
|
111
113
|
useEffect(() => {
|
112
|
-
if (
|
113
|
-
|
114
|
-
isTrue(field.questionOptions.isSearchable) &&
|
115
|
-
isEmpty(searchTerm) &&
|
116
|
-
value &&
|
117
|
-
!Object.keys(savedSearchableItem).length
|
118
|
-
) {
|
114
|
+
if (value && !isDirty && dataSource && isSearchable && sessionMode !== 'enter' && !items.length) {
|
115
|
+
// While in edit mode, search-based instances should fetch the initial item (previously selected value) to resolve its display property
|
119
116
|
setIsLoading(true);
|
120
|
-
|
117
|
+
try {
|
118
|
+
dataSource.fetchSingleItem(value).then((item) => {
|
119
|
+
setItems([dataSource.toUuidAndDisplay(item)]);
|
120
|
+
setIsLoading(false);
|
121
|
+
});
|
122
|
+
} catch (error) {
|
123
|
+
console.error(error);
|
124
|
+
setIsLoading(false);
|
125
|
+
}
|
121
126
|
}
|
122
|
-
}, [value]);
|
127
|
+
}, [value, isDirty, sessionMode, dataSource, isSearchable, items]);
|
123
128
|
|
124
129
|
if (isLoading) {
|
125
130
|
return <DropdownSkeleton />;
|
126
131
|
}
|
127
132
|
|
128
|
-
return sessionMode
|
133
|
+
return isViewMode(sessionMode) || isTrue(field.readonly) ? (
|
129
134
|
<FieldValueView
|
130
135
|
label={t(field.label)}
|
131
136
|
value={value ? items.find((item) => item.uuid == value)?.display : value}
|
@@ -142,6 +147,7 @@ const UiSelectExtended: React.FC<FormFieldInputProps> = ({ field, errors, warnin
|
|
142
147
|
items={items}
|
143
148
|
itemToString={(item) => item?.display}
|
144
149
|
selectedItem={selectedItem}
|
150
|
+
placeholder={isSearchable ? t('search', 'Search') + '...' : null}
|
145
151
|
shouldFilterItem={({ item, inputValue }) => {
|
146
152
|
if (!inputValue) {
|
147
153
|
// Carbon's initial call at component mount
|
@@ -165,7 +171,7 @@ const UiSelectExtended: React.FC<FormFieldInputProps> = ({ field, errors, warnin
|
|
165
171
|
isProcessingSelection.current = false;
|
166
172
|
return;
|
167
173
|
}
|
168
|
-
if (field.questionOptions
|
174
|
+
if (field.questionOptions.isSearchable) {
|
169
175
|
setSearchTerm(value);
|
170
176
|
}
|
171
177
|
}}
|
@@ -178,6 +184,7 @@ const UiSelectExtended: React.FC<FormFieldInputProps> = ({ field, errors, warnin
|
|
178
184
|
}
|
179
185
|
}}
|
180
186
|
/>
|
187
|
+
{isSearching && <InlineLoading className={styles.loader} description={t('searching', 'Searching') + '...'} />}
|
181
188
|
</Layer>
|
182
189
|
</div>
|
183
190
|
)
|
@@ -1,211 +1,281 @@
|
|
1
1
|
import React from 'react';
|
2
|
-
import { act,
|
3
|
-
import
|
4
|
-
import {
|
5
|
-
import {
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
rendering: 'ui-select-extended',
|
13
|
-
concept: '160540AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
|
14
|
-
datasource: {
|
15
|
-
name: 'location_datasource',
|
16
|
-
config: {
|
17
|
-
tag: 'test-tag',
|
18
|
-
},
|
19
|
-
},
|
20
|
-
},
|
21
|
-
meta: {},
|
22
|
-
id: 'patient_transfer_location',
|
23
|
-
},
|
24
|
-
{
|
25
|
-
label: 'Select criteria for new WHO stage:',
|
26
|
-
type: 'obs',
|
27
|
-
questionOptions: {
|
28
|
-
concept: '250e87b6-beb7-44a1-93a1-d3dd74d7e372',
|
29
|
-
rendering: 'select-concept-answers',
|
30
|
-
datasource: {
|
31
|
-
name: 'select_concept_answers_datasource',
|
32
|
-
config: {
|
33
|
-
concept: '250e87b6-beb7-44a1-93a1-d3dd74d7e372',
|
34
|
-
},
|
35
|
-
},
|
36
|
-
},
|
37
|
-
validators: [],
|
38
|
-
id: '__sq5ELJr7p',
|
39
|
-
},
|
40
|
-
];
|
41
|
-
|
42
|
-
const encounterContext: EncounterContext = {
|
43
|
-
patient: {
|
44
|
-
id: '833db896-c1f0-11eb-8529-0242ac130003',
|
45
|
-
},
|
46
|
-
location: {
|
47
|
-
uuid: '41e6e516-c1f0-11eb-8529-0242ac130003',
|
48
|
-
},
|
49
|
-
encounter: {
|
50
|
-
uuid: '873455da-3ec4-453c-b565-7c1fe35426be',
|
51
|
-
obs: [],
|
52
|
-
},
|
53
|
-
sessionMode: 'enter',
|
54
|
-
encounterDate: new Date(2023, 8, 29),
|
55
|
-
setEncounterDate: (value) => {},
|
56
|
-
encounterProvider: '2c95f6f5-788e-4e73-9079-5626911231fa',
|
57
|
-
setEncounterProvider: jest.fn,
|
58
|
-
setEncounterLocation: jest.fn,
|
59
|
-
encounterRole: '8cb3a399-d18b-4b62-aefb-5a0f948a3809',
|
60
|
-
setEncounterRole: jest.fn,
|
61
|
-
};
|
2
|
+
import { act, render, screen } from '@testing-library/react';
|
3
|
+
import { type FormSchema, type SessionMode, type OpenmrsEncounter } from '../../../types';
|
4
|
+
import { usePatient, useSession } from '@openmrs/esm-framework';
|
5
|
+
import { mockPatient } from '../../../../__mocks__/patient.mock';
|
6
|
+
import { mockSessionDataResponse } from '../../../../__mocks__/session.mock';
|
7
|
+
import { FormEngine } from '../../..';
|
8
|
+
import uiSelectExtForm from '../../../../__mocks__/forms/rfe-forms/sample_ui-select-ext.json';
|
9
|
+
import { assertFormHasAllFields, findSelectInput } from '../../../utils/test-utils';
|
10
|
+
import userEvent from '@testing-library/user-event';
|
11
|
+
import * as api from '../../../api';
|
62
12
|
|
63
|
-
const
|
64
|
-
|
65
|
-
|
13
|
+
const mockUsePatient = jest.mocked(usePatient);
|
14
|
+
const mockUseSession = jest.mocked(useSession);
|
15
|
+
global.ResizeObserver = require('resize-observer-polyfill');
|
16
|
+
|
17
|
+
jest.mock('../../../hooks/useRestMaxResultsCount', () => jest.fn().mockReturnValue({ systemSetting: { value: '50' } }));
|
18
|
+
jest.mock('lodash-es/debounce', () => jest.fn((fn) => fn));
|
19
|
+
|
20
|
+
jest.mock('../../../api', () => {
|
21
|
+
const originalModule = jest.requireActual('../../../api');
|
22
|
+
return {
|
23
|
+
...originalModule,
|
24
|
+
getPreviousEncounter: jest.fn().mockImplementation(() => Promise.resolve(null)),
|
25
|
+
getConcept: jest.fn().mockImplementation(() => Promise.resolve(null)),
|
26
|
+
saveEncounter: jest.fn(),
|
27
|
+
};
|
28
|
+
});
|
29
|
+
|
30
|
+
jest.mock('../../../hooks/useEncounterRole', () => ({
|
31
|
+
useEncounterRole: jest.fn().mockReturnValue({
|
32
|
+
isLoading: false,
|
33
|
+
encounterRole: { name: 'Clinician', uuid: 'clinician-uuid' },
|
34
|
+
error: undefined,
|
35
|
+
}),
|
36
|
+
}));
|
37
|
+
|
38
|
+
jest.mock('../../../hooks/useEncounter', () => ({
|
39
|
+
useEncounter: jest.fn().mockImplementation((formJson: FormSchema) => {
|
40
|
+
return {
|
41
|
+
encounter: formJson.encounter ? (encounter as OpenmrsEncounter) : null,
|
42
|
+
isLoading: false,
|
43
|
+
error: undefined,
|
44
|
+
};
|
45
|
+
}),
|
46
|
+
}));
|
66
47
|
|
67
|
-
|
68
|
-
jest.
|
69
|
-
|
70
|
-
|
71
|
-
|
48
|
+
jest.mock('../../../hooks/useConcepts', () => ({
|
49
|
+
useConcepts: jest.fn().mockImplementation((references: Set<string>) => {
|
50
|
+
return {
|
51
|
+
isLoading: false,
|
52
|
+
concepts: [],
|
53
|
+
error: undefined,
|
54
|
+
};
|
55
|
+
}),
|
56
|
+
}));
|
57
|
+
|
58
|
+
jest.mock('../../../registry/registry', () => {
|
59
|
+
const originalModule = jest.requireActual('../../../registry/registry');
|
60
|
+
return {
|
61
|
+
...originalModule,
|
62
|
+
getRegisteredDataSource: jest.fn().mockResolvedValue({
|
63
|
+
fetchData: jest.fn().mockImplementation((...args) => {
|
64
|
+
if (args[1].class?.length) {
|
65
|
+
// concept DS
|
66
|
+
return Promise.resolve([
|
67
|
+
{
|
68
|
+
uuid: 'stage-1-uuid',
|
69
|
+
display: 'stage 1',
|
70
|
+
},
|
71
|
+
{
|
72
|
+
uuid: 'stage-2-uuid',
|
73
|
+
display: 'stage 2',
|
74
|
+
},
|
75
|
+
]);
|
76
|
+
}
|
77
|
+
|
78
|
+
// location DS
|
72
79
|
return Promise.resolve([
|
73
80
|
{
|
74
|
-
uuid: '
|
75
|
-
display: '
|
81
|
+
uuid: 'aaa-1',
|
82
|
+
display: 'Kololo',
|
83
|
+
},
|
84
|
+
{
|
85
|
+
uuid: 'aaa-2',
|
86
|
+
display: 'Naguru',
|
76
87
|
},
|
77
88
|
{
|
78
|
-
uuid: '
|
79
|
-
display: '
|
89
|
+
uuid: 'aaa-3',
|
90
|
+
display: 'Muyenga',
|
80
91
|
},
|
81
92
|
]);
|
82
|
-
}
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
uuid: 'aaa-2',
|
91
|
-
display: 'Naguru',
|
92
|
-
},
|
93
|
-
{
|
94
|
-
uuid: 'aaa-3',
|
95
|
-
display: 'Muyenga',
|
96
|
-
},
|
97
|
-
]);
|
93
|
+
}),
|
94
|
+
fetchSingleItem: jest.fn().mockImplementation((uuid: string) => {
|
95
|
+
return Promise.resolve({
|
96
|
+
uuid,
|
97
|
+
display: 'stage 1',
|
98
|
+
});
|
99
|
+
}),
|
100
|
+
toUuidAndDisplay: (data) => data,
|
98
101
|
}),
|
99
|
-
|
100
|
-
|
101
|
-
}));
|
102
|
+
};
|
103
|
+
});
|
102
104
|
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
105
|
+
const encounter = {
|
106
|
+
uuid: 'encounter-uuid',
|
107
|
+
obs: [
|
108
|
+
{
|
109
|
+
concept: {
|
110
|
+
uuid: '160540AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
|
111
|
+
},
|
112
|
+
value: 'aaa-2',
|
113
|
+
formFieldNamespace: 'rfe-forms',
|
114
|
+
formFieldPath: 'rfe-forms-patient_transfer_location',
|
115
|
+
uuid: 'obs-uuid-1',
|
116
|
+
},
|
117
|
+
{
|
118
|
+
concept: {
|
119
|
+
uuid: '4b59ac07-cf72-4f46-b8c0-4f62b1779f7e',
|
120
|
+
},
|
121
|
+
value: 'stage-1-uuid',
|
122
|
+
formFieldNamespace: 'rfe-forms',
|
123
|
+
formFieldPath: 'rfe-forms-problem',
|
124
|
+
uuid: 'obs-uuid-2',
|
125
|
+
},
|
126
|
+
],
|
127
|
+
};
|
108
128
|
|
109
|
-
|
110
|
-
|
129
|
+
const renderForm = (mode: SessionMode = 'enter') => {
|
130
|
+
render(
|
131
|
+
<FormEngine
|
132
|
+
formJson={uiSelectExtForm as FormSchema}
|
133
|
+
patientUUID="8673ee4f-e2ab-4077-ba55-4980f408773e"
|
134
|
+
mode={mode}
|
135
|
+
encounterUUID={mode === 'edit' ? 'encounter-uuid' : null}
|
136
|
+
/>,
|
137
|
+
);
|
138
|
+
};
|
139
|
+
|
140
|
+
describe('UiSelectExtended', () => {
|
141
|
+
const user = userEvent.setup();
|
111
142
|
|
112
|
-
|
113
|
-
|
143
|
+
beforeEach(() => {
|
144
|
+
Object.defineProperty(window, 'i18next', {
|
145
|
+
writable: true,
|
146
|
+
configurable: true,
|
147
|
+
value: {
|
148
|
+
language: 'en',
|
149
|
+
t: jest.fn(),
|
150
|
+
},
|
151
|
+
});
|
114
152
|
|
115
|
-
|
116
|
-
|
153
|
+
mockUsePatient.mockImplementation(() => ({
|
154
|
+
patient: mockPatient,
|
155
|
+
isLoading: false,
|
156
|
+
error: undefined,
|
157
|
+
patientUuid: mockPatient.id,
|
158
|
+
}));
|
117
159
|
|
118
|
-
|
119
|
-
expect(screen.getByText('Kololo')).toBeInTheDocument();
|
120
|
-
expect(screen.getByText('Naguru')).toBeInTheDocument();
|
121
|
-
expect(screen.getByText('Muyenga')).toBeInTheDocument();
|
160
|
+
mockUseSession.mockImplementation(() => mockSessionDataResponse.data);
|
122
161
|
});
|
123
162
|
|
124
|
-
|
125
|
-
|
126
|
-
await
|
127
|
-
|
163
|
+
describe('Enter/New mode', () => {
|
164
|
+
it('should render comboboxes correctly for both "non-searchable" and "searchable" instances', async () => {
|
165
|
+
await act(async () => {
|
166
|
+
renderForm();
|
167
|
+
});
|
128
168
|
|
129
|
-
|
130
|
-
|
169
|
+
await assertFormHasAllFields(screen, [
|
170
|
+
{ fieldName: 'Transfer Location', fieldType: 'select' },
|
171
|
+
{ fieldName: 'Problem', fieldType: 'select' },
|
172
|
+
]);
|
131
173
|
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
174
|
+
// Test for "non-searchable" instance
|
175
|
+
const transferLocationSelect = await findSelectInput(screen, 'Transfer Location');
|
176
|
+
await user.click(transferLocationSelect);
|
177
|
+
expect(screen.getByText('Kololo')).toBeInTheDocument();
|
178
|
+
expect(screen.getByText('Naguru')).toBeInTheDocument();
|
179
|
+
expect(screen.getByText('Muyenga')).toBeInTheDocument();
|
136
180
|
|
137
|
-
|
138
|
-
|
139
|
-
|
181
|
+
// Test for "searchable" instance
|
182
|
+
const problemSelect = await findSelectInput(screen, 'Problem');
|
183
|
+
expect(problemSelect).toHaveAttribute('placeholder', 'Search...');
|
140
184
|
});
|
141
185
|
|
142
|
-
|
143
|
-
|
186
|
+
it('should be possible to select an item from the combobox and submit the form', async () => {
|
187
|
+
const mockSaveEncounter = jest.spyOn(api, 'saveEncounter');
|
144
188
|
|
145
|
-
|
146
|
-
|
189
|
+
await act(async () => {
|
190
|
+
renderForm();
|
191
|
+
});
|
147
192
|
|
148
|
-
|
149
|
-
|
150
|
-
|
193
|
+
const transferLocationSelect = await findSelectInput(screen, 'Transfer Location');
|
194
|
+
await user.click(transferLocationSelect);
|
195
|
+
const naguruOption = screen.getByText('Naguru');
|
196
|
+
await user.click(naguruOption);
|
151
197
|
|
152
|
-
|
153
|
-
|
154
|
-
expect(questions[0].meta.submission.newValue).toEqual({
|
155
|
-
concept: '160540AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
|
156
|
-
formFieldNamespace: 'rfe-forms',
|
157
|
-
formFieldPath: 'rfe-forms-patient_transfer_location',
|
158
|
-
value: 'aaa-2',
|
159
|
-
});
|
160
|
-
});
|
161
|
-
});
|
198
|
+
// submit the form
|
199
|
+
await user.click(screen.getByRole('button', { name: /save/i }));
|
162
200
|
|
163
|
-
|
164
|
-
|
165
|
-
|
201
|
+
expect(mockSaveEncounter).toHaveBeenCalledWith(
|
202
|
+
expect.any(AbortController),
|
203
|
+
expect.objectContaining({
|
204
|
+
obs: [
|
205
|
+
{
|
206
|
+
concept: '160540AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
|
207
|
+
formFieldNamespace: 'rfe-forms',
|
208
|
+
formFieldPath: 'rfe-forms-patient_transfer_location',
|
209
|
+
value: 'aaa-2',
|
210
|
+
},
|
211
|
+
],
|
212
|
+
}),
|
213
|
+
undefined,
|
214
|
+
);
|
166
215
|
});
|
167
216
|
|
168
|
-
|
169
|
-
|
217
|
+
it('should be possible to search and select an item from the search-box and submit the form', async () => {
|
218
|
+
const mockSaveEncounter = jest.spyOn(api, 'saveEncounter');
|
170
219
|
|
171
|
-
|
172
|
-
|
220
|
+
await act(async () => {
|
221
|
+
renderForm();
|
222
|
+
});
|
173
223
|
|
174
|
-
|
175
|
-
|
224
|
+
const problemSelect = await findSelectInput(screen, 'Problem');
|
225
|
+
await user.click(problemSelect);
|
226
|
+
await user.type(problemSelect, 'stage');
|
227
|
+
expect(screen.getByText('stage 1')).toBeInTheDocument();
|
228
|
+
expect(screen.getByText('stage 2')).toBeInTheDocument();
|
229
|
+
// select the first option
|
230
|
+
await user.click(screen.getByText('stage 1'));
|
176
231
|
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
expect(
|
232
|
+
// submit the form
|
233
|
+
await user.click(screen.getByRole('button', { name: /save/i }));
|
234
|
+
|
235
|
+
expect(mockSaveEncounter).toHaveBeenCalledWith(
|
236
|
+
expect.any(AbortController),
|
237
|
+
expect.objectContaining({
|
238
|
+
obs: [
|
239
|
+
{
|
240
|
+
concept: '4b59ac07-cf72-4f46-b8c0-4f62b1779f7e',
|
241
|
+
formFieldNamespace: 'rfe-forms',
|
242
|
+
formFieldPath: 'rfe-forms-problem',
|
243
|
+
value: 'stage-1-uuid',
|
244
|
+
},
|
245
|
+
],
|
246
|
+
}),
|
247
|
+
undefined,
|
248
|
+
);
|
249
|
+
});
|
250
|
+
|
251
|
+
it('should filter items based on user input', async () => {
|
252
|
+
await act(async () => {
|
253
|
+
renderForm();
|
254
|
+
});
|
181
255
|
|
182
|
-
|
256
|
+
const transferLocationSelect = await findSelectInput(screen, 'Transfer Location');
|
257
|
+
await user.click(transferLocationSelect);
|
258
|
+
await user.type(transferLocationSelect, 'Nag');
|
259
|
+
|
260
|
+
expect(screen.getByText('Naguru')).toBeInTheDocument();
|
183
261
|
expect(screen.queryByText('Kololo')).not.toBeInTheDocument();
|
184
262
|
expect(screen.queryByText('Muyenga')).not.toBeInTheDocument();
|
185
263
|
});
|
186
264
|
});
|
187
265
|
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
266
|
+
describe('Edit mode', () => {
|
267
|
+
it('should initialize with the current value for both "non-searchable" and "searchable" instances', async () => {
|
268
|
+
await act(async () => {
|
269
|
+
renderForm('edit');
|
270
|
+
});
|
193
271
|
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
fetchData: jest.fn().mockResolvedValue([]),
|
198
|
-
toUuidAndDisplay: (data) => data,
|
199
|
-
config: expectedConfigValue,
|
200
|
-
}),
|
201
|
-
}));
|
272
|
+
// Non-searchable instance
|
273
|
+
const nonSearchableInstance = await findSelectInput(screen, 'Transfer Location');
|
274
|
+
expect(nonSearchableInstance).toHaveValue('Naguru');
|
202
275
|
|
203
|
-
|
204
|
-
await
|
276
|
+
// Searchable instance
|
277
|
+
const searchableInstance = await findSelectInput(screen, 'Problem');
|
278
|
+
expect(searchableInstance).toHaveValue('stage 1');
|
205
279
|
});
|
206
|
-
const config = questions[0].questionOptions.datasource.config;
|
207
|
-
|
208
|
-
// Assert that the config is set with the expected configuration value
|
209
|
-
expect(config).toEqual(expectedConfigValue);
|
210
280
|
});
|
211
281
|
});
|
@@ -2,7 +2,7 @@ import React from 'react';
|
|
2
2
|
import dayjs from 'dayjs';
|
3
3
|
import { fireEvent, render, screen } from '@testing-library/react';
|
4
4
|
import { OpenmrsDatePicker } from '@openmrs/esm-framework';
|
5
|
-
import { type FormField
|
5
|
+
import { type FormField } from '../../../types';
|
6
6
|
import { findTextOrDateInput } from '../../../utils/test-utils';
|
7
7
|
|
8
8
|
const mockOpenmrsDatePicker = jest.mocked(OpenmrsDatePicker);
|
@@ -31,27 +31,6 @@ const question: FormField = {
|
|
31
31
|
id: 'visit-date',
|
32
32
|
};
|
33
33
|
|
34
|
-
const encounterContext: EncounterContext = {
|
35
|
-
patient: {
|
36
|
-
id: '833db896-c1f0-11eb-8529-0242ac130003',
|
37
|
-
},
|
38
|
-
location: {
|
39
|
-
uuid: '41e6e516-c1f0-11eb-8529-0242ac130003',
|
40
|
-
},
|
41
|
-
encounter: {
|
42
|
-
uuid: '873455da-3ec4-453c-b565-7c1fe35426be',
|
43
|
-
obs: [],
|
44
|
-
},
|
45
|
-
sessionMode: 'enter',
|
46
|
-
encounterDate: new Date(2020, 11, 29),
|
47
|
-
setEncounterDate: (value) => {},
|
48
|
-
encounterProvider: '2c95f6f5-788e-4e73-9079-5626911231fa',
|
49
|
-
setEncounterProvider: jest.fn,
|
50
|
-
setEncounterLocation: jest.fn,
|
51
|
-
encounterRole: '8cb3a399-d18b-4b62-aefb-5a0f948a3809',
|
52
|
-
setEncounterRole: jest.fn,
|
53
|
-
};
|
54
|
-
|
55
34
|
const renderForm = (initialValues) => {
|
56
35
|
render(<></>);
|
57
36
|
};
|
@@ -4,21 +4,21 @@ import { isEmpty } from '../validators/form-validator';
|
|
4
4
|
|
5
5
|
export class ConceptDataSource extends BaseOpenMRSDataSource {
|
6
6
|
constructor() {
|
7
|
-
super(`${restBaseUrl}/concept?
|
7
|
+
super(`${restBaseUrl}/concept?v=custom:(uuid,display,conceptClass:(uuid,display))`);
|
8
8
|
}
|
9
9
|
|
10
|
-
fetchData(searchTerm: string, config?: Record<string, any
|
10
|
+
fetchData(searchTerm: string, config?: Record<string, any>): Promise<any[]> {
|
11
11
|
if (isEmpty(config?.class) && isEmpty(config?.concept) && !config?.useSetMembersByConcept && isEmpty(searchTerm)) {
|
12
12
|
return Promise.resolve([]);
|
13
13
|
}
|
14
14
|
|
15
|
-
let
|
15
|
+
let searchUrl = `${restBaseUrl}/concept?name=&searchType=fuzzy&v=custom:(uuid,display,conceptClass:(uuid,display))`;
|
16
16
|
if (config?.class) {
|
17
17
|
if (typeof config.class == 'string') {
|
18
|
-
const urlParts =
|
19
|
-
|
18
|
+
const urlParts = searchUrl.split('searchType=fuzzy');
|
19
|
+
searchUrl = `${urlParts[0]}searchType=fuzzy&class=${config.class}&${urlParts[1]}`;
|
20
20
|
} else {
|
21
|
-
return openmrsFetch(searchTerm ? `${
|
21
|
+
return openmrsFetch(searchTerm ? `${searchUrl}&q=${searchTerm}` : searchUrl).then(({ data }) => {
|
22
22
|
return data.results.filter(
|
23
23
|
(concept) => concept.conceptClass && config.class.includes(concept.conceptClass.uuid),
|
24
24
|
);
|
@@ -27,15 +27,15 @@ export class ConceptDataSource extends BaseOpenMRSDataSource {
|
|
27
27
|
}
|
28
28
|
|
29
29
|
if (config?.concept && config?.useSetMembersByConcept) {
|
30
|
-
let urlParts =
|
31
|
-
|
32
|
-
return openmrsFetch(searchTerm ? `${
|
30
|
+
let urlParts = searchUrl.split('?name=&searchType=fuzzy&v=');
|
31
|
+
searchUrl = `${urlParts[0]}/${config.concept}?v=custom:(uuid,setMembers:(uuid,display))`;
|
32
|
+
return openmrsFetch(searchTerm ? `${searchUrl}&q=${searchTerm}` : searchUrl).then(({ data }) => {
|
33
33
|
// return the setMembers from the retrieved concept object
|
34
34
|
return data['setMembers'];
|
35
35
|
});
|
36
36
|
}
|
37
37
|
|
38
|
-
return openmrsFetch(searchTerm ? `${
|
38
|
+
return openmrsFetch(searchTerm ? `${searchUrl}&q=${searchTerm}` : searchUrl).then(({ data }) => {
|
39
39
|
return data.results;
|
40
40
|
});
|
41
41
|
}
|
@@ -14,6 +14,17 @@ export class BaseOpenMRSDataSource implements DataSource<OpenmrsResource> {
|
|
14
14
|
});
|
15
15
|
}
|
16
16
|
|
17
|
+
fetchSingleItem(uuid: string): Promise<OpenmrsResource | null> {
|
18
|
+
let apiUrl = this.url;
|
19
|
+
if (apiUrl.includes('?')) {
|
20
|
+
const urlParts = apiUrl.split('?');
|
21
|
+
apiUrl = `${urlParts[0]}/${uuid}?${urlParts[1]}`;
|
22
|
+
} else {
|
23
|
+
apiUrl = `${apiUrl}/${uuid}`;
|
24
|
+
}
|
25
|
+
return openmrsFetch(apiUrl).then(({ data }) => data);
|
26
|
+
}
|
27
|
+
|
17
28
|
toUuidAndDisplay(data: OpenmrsResource): OpenmrsResource {
|
18
29
|
if (typeof data.uuid === 'undefined' || typeof data.display === 'undefined') {
|
19
30
|
throw new Error("'uuid' or 'display' not found in the OpenMRS object.");
|
package/src/index.ts
CHANGED
@@ -5,7 +5,6 @@ export * from './constants';
|
|
5
5
|
export * from './utils/boolean-utils';
|
6
6
|
export * from './validators/form-validator';
|
7
7
|
export * from './utils/form-helper';
|
8
|
-
export * from './form-context';
|
9
8
|
export * from './components/value/view/field-value-view.component';
|
10
9
|
export * from './components/previous-value-review/previous-value-review.component';
|
11
10
|
export * from './hooks/useFormJson';
|
@@ -164,6 +164,10 @@ function transformByRendering(question: FormField) {
|
|
164
164
|
case 'markdown':
|
165
165
|
question.type = 'control';
|
166
166
|
break;
|
167
|
+
case 'drug':
|
168
|
+
case 'problem':
|
169
|
+
question.questionOptions.isSearchable = true;
|
170
|
+
break;
|
167
171
|
}
|
168
172
|
return question;
|
169
173
|
}
|
@@ -193,6 +197,7 @@ function handleLabOrders(question: FormField) {
|
|
193
197
|
}
|
194
198
|
|
195
199
|
function handleSelectConceptAnswers(question: FormField) {
|
200
|
+
question.questionOptions.isSearchable = true;
|
196
201
|
if (!question.questionOptions.datasource?.config) {
|
197
202
|
question.questionOptions.datasource = {
|
198
203
|
name: 'select_concept_answers_datasource',
|
package/src/types/index.ts
CHANGED
@@ -69,7 +69,14 @@ export interface DataSource<T> {
|
|
69
69
|
/**
|
70
70
|
* Fetches arbitrary data from a data source
|
71
71
|
*/
|
72
|
-
fetchData(searchTerm?: string, config?: Record<string, any
|
72
|
+
fetchData(searchTerm?: string, config?: Record<string, any>): Promise<Array<T>>;
|
73
|
+
|
74
|
+
/**
|
75
|
+
* Fetches a single item from the data source based on its UUID.
|
76
|
+
* This is used for value binding with previously selected values.
|
77
|
+
*/
|
78
|
+
fetchSingleItem(uuid: string): Promise<T | null>;
|
79
|
+
|
73
80
|
/**
|
74
81
|
* Maps a data source item to an object with a uuid and display property
|
75
82
|
*/
|
package/src/types/schema.ts
CHANGED
@@ -198,6 +198,7 @@ export type RenderType =
|
|
198
198
|
| 'content-switcher'
|
199
199
|
| 'date'
|
200
200
|
| 'datetime'
|
201
|
+
| 'drug'
|
201
202
|
| 'encounter-location'
|
202
203
|
| 'encounter-provider'
|
203
204
|
| 'encounter-role'
|
@@ -205,6 +206,7 @@ export type RenderType =
|
|
205
206
|
| 'file'
|
206
207
|
| 'group'
|
207
208
|
| 'number'
|
209
|
+
| 'problem'
|
208
210
|
| 'radio'
|
209
211
|
| 'repeating'
|
210
212
|
| 'select'
|
@@ -8,7 +8,6 @@ import { DefaultValueValidator } from '../validators/default-value-validator';
|
|
8
8
|
import { type LayoutType } from '@openmrs/esm-framework';
|
9
9
|
import { ConceptTrue } from '../constants';
|
10
10
|
import { type FormField, type OpenmrsEncounter, type SessionMode } from '../types';
|
11
|
-
import { type EncounterContext } from '../form-context';
|
12
11
|
|
13
12
|
jest.mock('../validators/default-value-validator');
|
14
13
|
|
package/src/form-context.tsx
DELETED
@@ -1,42 +0,0 @@
|
|
1
|
-
import React from 'react';
|
2
|
-
import { type LayoutType } from '@openmrs/esm-framework';
|
3
|
-
import {
|
4
|
-
type PatientProgram,
|
5
|
-
type FormField,
|
6
|
-
type OpenmrsEncounter,
|
7
|
-
type PatientIdentifier,
|
8
|
-
type SessionMode,
|
9
|
-
} from './types';
|
10
|
-
|
11
|
-
type FormContextProps = {
|
12
|
-
values: Record<string, any>;
|
13
|
-
setFieldValue: (field: string, value: any, shouldValidate?: boolean) => void;
|
14
|
-
setEncounterLocation: (value: any) => void;
|
15
|
-
encounterContext: EncounterContext;
|
16
|
-
fields: FormField[];
|
17
|
-
isFieldInitializationComplete: boolean;
|
18
|
-
isSubmitting: boolean;
|
19
|
-
layoutType?: LayoutType;
|
20
|
-
workspaceLayout?: 'minimized' | 'maximized';
|
21
|
-
};
|
22
|
-
|
23
|
-
export interface EncounterContext {
|
24
|
-
patient: fhir.Patient;
|
25
|
-
encounter: OpenmrsEncounter;
|
26
|
-
previousEncounter?: OpenmrsEncounter;
|
27
|
-
location: any;
|
28
|
-
sessionMode: SessionMode;
|
29
|
-
encounterDate: Date;
|
30
|
-
setEncounterDate(value: Date): void;
|
31
|
-
encounterProvider: string;
|
32
|
-
setEncounterProvider(value: string): void;
|
33
|
-
setEncounterLocation(value: any): void;
|
34
|
-
encounterRole: string;
|
35
|
-
setEncounterRole(value: string): void;
|
36
|
-
initValues?: Record<string, any>;
|
37
|
-
patientIdentifier?: PatientIdentifier;
|
38
|
-
patientPrograms?: Array<PatientProgram>;
|
39
|
-
getFormField?: (id: string) => FormField;
|
40
|
-
}
|
41
|
-
|
42
|
-
export const FormContext = React.createContext<FormContextProps | undefined>(undefined);
|
File without changes
|