@openmrs/esm-fast-data-entry-app 1.0.1-pre.10 → 1.0.1-pre.15
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/.yarn/versions/{45b499b6.yml → c1451405.yml} +0 -0
- package/dist/openmrs-esm-fast-data-entry-app.js +1 -1
- package/package.json +5 -3
- package/src/add-group-modal/AddGroupModal.tsx +80 -27
- package/src/add-group-modal/styles.scss +14 -4
- package/src/context/GroupFormWorkflowContext.tsx +2 -0
- package/src/context/GroupFormWorkflowReducer.ts +26 -2
- package/src/group-form-entry-workflow/GroupFormEntryWorkflow.tsx +2 -2
- package/src/group-form-entry-workflow/{group-banner/GroupBanner.test.tsx → group-display-header/GroupDisplayHeader.test.tsx} +2 -2
- package/src/group-form-entry-workflow/{group-banner/GroupBanner.tsx → group-display-header/GroupDisplayHeader.tsx} +31 -5
- package/src/group-form-entry-workflow/group-display-header/index.ts +3 -0
- package/src/group-form-entry-workflow/{group-banner → group-display-header}/styles.scss +0 -0
- package/src/group-form-entry-workflow/group-search/CompactGroupResults.tsx +61 -28
- package/src/group-form-entry-workflow/group-search/CompactGroupSearch.tsx +5 -0
- package/src/group-form-entry-workflow/group-search/GroupSearch.tsx +65 -8
- package/src/group-form-entry-workflow/group-search/group-search.scss +8 -6
- package/src/group-form-entry-workflow/group-search-header/GroupSearchHeader.tsx +14 -7
- package/src/group-form-entry-workflow/styles.scss +10 -0
- package/src/hooks/index.ts +1 -0
- package/src/hooks/usePostEndpoint.ts +70 -0
- package/src/hooks/useSearchEndpoint.ts +120 -0
- package/translations/en.json +6 -1
- package/src/group-form-entry-workflow/group-banner/index.ts +0 -3
- package/src/group-form-entry-workflow/group-search/mock-group-data.ts +0 -79
- package/src/group-form-entry-workflow/group-search/useGroupSearch.ts +0 -14
- package/src/hooks/usePostCohort.ts +0 -18
|
File without changes
|
|
@@ -1 +1 @@
|
|
|
1
|
-
var _openmrs_esm_fast_data_entry_app;(()=>{function b(a){var c=h[a];if(void 0!==c)return c.exports;var d=h[a]={id:a,loaded:!1,exports:{}};return G[a].call(d.exports,d,d.exports,b),d.loaded=!0,d.exports}var j,k,q,v,w,x,y,z,A,B,C,D,E,F,G={7140:(b,c,d)=>{var f={"./start":()=>d.e(132).then(()=>()=>d(8132))},e=(a,b)=>(d.R=b,b=d.o(f,a)?f[a]():Promise.resolve().then(()=>{throw new Error("Module \""+a+"\" does not exist in container.")}),d.R=void 0,b),a=(b,c)=>{if(d.S){var e=d.S["default"];if(e&&e!==b)throw new Error("Container initialization failed as it has already been initialized with a different share scope");return d.S["default"]=b,d.I("default",c)}};d.d(c,{get:()=>e,init:()=>a})}},h={};b.m=G,b.c=h,b.n=a=>{var c=a&&a.__esModule?()=>a.default:()=>a;return b.d(c,{a:c}),c},b.d=(a,c)=>{for(var d in c)b.o(c,d)&&!b.o(a,d)&&Object.defineProperty(a,d,{enumerable:!0,get:c[d]})},b.f={},b.e=a=>Promise.all(Object.keys(b.f).reduce((c,d)=>(b.f[d](a,c),c),[])),b.u=a=>a+".js",b.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(a){if("object"==typeof window)return window}}(),b.o=(a,b)=>Object.prototype.hasOwnProperty.call(a,b),j={},k="@openmrs/esm-fast-data-entry-app:",b.l=(c,e,g)=>{if(j[c])j[c].push(e);else{var h,a;if(void 0!==g)for(var m,n=document.getElementsByTagName("script"),o=0;o<n.length;o++)if(m=n[o],m.getAttribute("src")==c||m.getAttribute("data-webpack")==k+g){h=m;break}h||(a=!0,(h=document.createElement("script")).charset="utf-8",h.timeout=120,b.nc&&h.setAttribute("nonce",b.nc),h.setAttribute("data-webpack",k+g),h.src=c),j[c]=[e];var q=(b,f)=>{h.onerror=h.onload=null,clearTimeout(d);var e=j[c];if(delete j[c],h.parentNode&&h.parentNode.removeChild(h),e&&e.forEach(a=>a(f)),b)return b(f)},d=setTimeout(q.bind(null,void 0,{type:"timeout",target:h}),12e4);h.onerror=q.bind(null,h.onerror),h.onload=q.bind(null,h.onload),a&&document.head.appendChild(h)}},b.r=a=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(a,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(a,"__esModule",{value:!0})},b.nmd=a=>(a.paths=[],a.children||(a.children=[]),a),(()=>{b.S={};var c={},d={};b.I=(e,f)=>{f||(f=[]);var g=d[e];if(g||(g=d[e]={}),!(0<=f.indexOf(g))){if(f.push(g),c[e])return c[e];b.o(b.S,e)||(b.S[e]={});var h=b.S[e],i=(b,c,d,e)=>{var f=h[b]=h[b]||{},a=f[c];a&&(a.loaded||(!e==!a.eager?!("@openmrs/esm-fast-data-entry-app">a.from):!e))||(f[c]={get:d,from:"@openmrs/esm-fast-data-entry-app",eager:!!e})},j=[];return"default"===e&&(i("@carbon/react","1.11.0",()=>Promise.all([b.e(820),b.e(327),b.e(569),b.e(672),b.e(183)]).then(()=>()=>b(8569))),i("@openmrs/esm-framework","4.0.2-pre.
|
|
1
|
+
var _openmrs_esm_fast_data_entry_app;(()=>{function b(a){var c=h[a];if(void 0!==c)return c.exports;var d=h[a]={id:a,loaded:!1,exports:{}};return G[a].call(d.exports,d,d.exports,b),d.loaded=!0,d.exports}var j,k,q,v,w,x,y,z,A,B,C,D,E,F,G={7140:(b,c,d)=>{var f={"./start":()=>d.e(132).then(()=>()=>d(8132))},e=(a,b)=>(d.R=b,b=d.o(f,a)?f[a]():Promise.resolve().then(()=>{throw new Error("Module \""+a+"\" does not exist in container.")}),d.R=void 0,b),a=(b,c)=>{if(d.S){var e=d.S["default"];if(e&&e!==b)throw new Error("Container initialization failed as it has already been initialized with a different share scope");return d.S["default"]=b,d.I("default",c)}};d.d(c,{get:()=>e,init:()=>a})}},h={};b.m=G,b.c=h,b.n=a=>{var c=a&&a.__esModule?()=>a.default:()=>a;return b.d(c,{a:c}),c},b.d=(a,c)=>{for(var d in c)b.o(c,d)&&!b.o(a,d)&&Object.defineProperty(a,d,{enumerable:!0,get:c[d]})},b.f={},b.e=a=>Promise.all(Object.keys(b.f).reduce((c,d)=>(b.f[d](a,c),c),[])),b.u=a=>a+".js",b.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(a){if("object"==typeof window)return window}}(),b.o=(a,b)=>Object.prototype.hasOwnProperty.call(a,b),j={},k="@openmrs/esm-fast-data-entry-app:",b.l=(c,e,g)=>{if(j[c])j[c].push(e);else{var h,a;if(void 0!==g)for(var m,n=document.getElementsByTagName("script"),o=0;o<n.length;o++)if(m=n[o],m.getAttribute("src")==c||m.getAttribute("data-webpack")==k+g){h=m;break}h||(a=!0,(h=document.createElement("script")).charset="utf-8",h.timeout=120,b.nc&&h.setAttribute("nonce",b.nc),h.setAttribute("data-webpack",k+g),h.src=c),j[c]=[e];var q=(b,f)=>{h.onerror=h.onload=null,clearTimeout(d);var e=j[c];if(delete j[c],h.parentNode&&h.parentNode.removeChild(h),e&&e.forEach(a=>a(f)),b)return b(f)},d=setTimeout(q.bind(null,void 0,{type:"timeout",target:h}),12e4);h.onerror=q.bind(null,h.onerror),h.onload=q.bind(null,h.onload),a&&document.head.appendChild(h)}},b.r=a=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(a,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(a,"__esModule",{value:!0})},b.nmd=a=>(a.paths=[],a.children||(a.children=[]),a),(()=>{b.S={};var c={},d={};b.I=(e,f)=>{f||(f=[]);var g=d[e];if(g||(g=d[e]={}),!(0<=f.indexOf(g))){if(f.push(g),c[e])return c[e];b.o(b.S,e)||(b.S[e]={});var h=b.S[e],i=(b,c,d,e)=>{var f=h[b]=h[b]||{},a=f[c];a&&(a.loaded||(!e==!a.eager?!("@openmrs/esm-fast-data-entry-app">a.from):!e))||(f[c]={get:d,from:"@openmrs/esm-fast-data-entry-app",eager:!!e})},j=[];return"default"===e&&(i("@carbon/react","1.11.0",()=>Promise.all([b.e(820),b.e(327),b.e(569),b.e(672),b.e(183)]).then(()=>()=>b(8569))),i("@openmrs/esm-framework","4.0.2-pre.328",()=>Promise.all([b.e(595),b.e(672)]).then(()=>()=>b(595))),i("react-dom","18.2.0",()=>Promise.all([b.e(935),b.e(672)]).then(()=>()=>b(3935))),i("react-i18next","11.18.5",()=>Promise.all([b.e(247),b.e(672)]).then(()=>()=>b(8247))),i("react-router-dom","6.3.0",()=>Promise.all([b.e(68),b.e(672)]).then(()=>()=>b(6068))),i("react","18.2.0",()=>b.e(294).then(()=>()=>b(7294)))),c[e]=j.length?Promise.all(j).then(()=>c[e]=1):1}}})(),(()=>{var a;b.g.importScripts&&(a=b.g.location+"");var c=b.g.document;if(!a&&c&&(c.currentScript&&(a=c.currentScript.src),!a)){var d=c.getElementsByTagName("script");d.length&&(a=d[d.length-1].src)}if(!a)throw new Error("Automatic publicPath is not supported in this browser");a=a.replace(/#.*$/,"").replace(/\?.*$/,"").replace(/\/[^\/]+$/,"/"),b.p=a})(),q=a=>{var b=a=>a.split(".").map(a=>+a==a?+a:a),c=/^([^-+]+)?(?:-([^+]+))?(?:\+(.+))?$/.exec(a),d=c[1]?b(c[1]):[];return c[2]&&(d.length++,d.push.apply(d,b(c[2]))),c[3]&&(d.push([]),d.push.apply(d,b(c[3]))),d},v=(b,c)=>{b=q(b),c=q(c);for(var d=0;;){if(d>=b.length)return d<c.length&&"u"!=(typeof c[d])[0];var f=b[d],a=(typeof f)[0];if(d>=c.length)return"u"==a;var g=c[d],h=(typeof g)[0];if(a!=h)return"o"==a&&"n"==h||"s"==h||"u"==a;if("o"!=a&&"u"!=a&&f!=g)return f<g;d++}},w=a=>{function b(){return g.pop().replace(/^\((.+)\)$/,"$1")}var c=a[0],d="";if(1===a.length)return"*";if(c+.5){d+=0==c?">=":-1==c?"<":1==c?"^":2==c?"~":0<c?"=":"!=";for(var e=1,f=1;f<a.length;f++)e--,d+="u"==(typeof(h=a[f]))[0]?"-":(0<e?".":"")+(e=2,h);return d}var g=[];for(f=1;f<a.length;f++){var h=a[f];g.push(0===h?"not("+b()+")":1===h?"("+b()+" || "+b()+")":2===h?g.pop()+" "+g.pop():w(h))}return b()},x=(b,e)=>{if(0 in b){e=q(e);var g=b[0],j=0>g;j&&(g=-g-1);for(var a=0,k=1,m=!0;;k++,a++){var o,t,v=k<b.length?(typeof b[k])[0]:"";if(a>=e.length||"o"==(t=(typeof(o=e[a]))[0]))return!m||("u"==v?k>g&&!j:""==v!=j);if("u"==t){if(!m||"u"!=v)return!1;}else if(!m)"s"!=v&&"n"!=v&&(m=!1,k--);else if(v==t){if(!(k<=g)){if(j?o>b[k]:o<b[k])return!1;o!=b[k]&&(m=!1)}else if(o!=b[k])return!1;}else if("s"!=v&&"n"!=v){if(j||k<=g)return!1;m=!1,k--}else{if(k<=g||t<v!=j)return!1;m=!1}}}var d=[],p=d.pop.bind(d);for(a=1;a<b.length;a++){var c=b[a];d.push(1==c?p()|p():2==c?p()&p():c?x(c,e):!p())}return!!p()},y=(a,b)=>{var c=a[b];return Object.keys(c).reduce((a,b)=>a&&(c[a].loaded||!v(a,b))?a:b,0)},z=(a,b,c,d)=>"Unsatisfied version "+c+" from "+(c&&a[b][c].from)+" of shared singleton module "+b+" (required "+w(d)+")",A=(b,c,d,e)=>{var f=y(b,d);return x(e,f)||"undefined"!=typeof console&&console.warn&&console.warn(z(b,d,f,e)),B(b[d][f])},B=a=>(a.loaded=1,a.get()),C=(c=>function(d,e,f,g){var a=b.I(d);return a&&a.then?a.then(c.bind(c,d,b.S[d],e,f,g)):c(0,b.S[d],e,f,g)})((c,d,e,f,g)=>d&&b.o(d,e)?A(d,0,e,f):g()),D={},E={1672:()=>C("default","react",[1,18],()=>b.e(294).then(()=>()=>b(7294))),5183:()=>C("default","react-dom",[1,18],()=>b.e(935).then(()=>()=>b(3935))),1132:()=>C("default","@openmrs/esm-framework",[0],()=>Promise.all([b.e(595),b.e(672)]).then(()=>()=>b(595))),1338:()=>C("default","react-router-dom",[1,6],()=>b.e(68).then(()=>()=>b(6068))),3397:()=>C("default","react-i18next",[1,11],()=>b.e(247).then(()=>()=>b(8247))),4422:()=>C("default","@carbon/react",[1,1,9,0],()=>Promise.all([b.e(820),b.e(327),b.e(569),b.e(183)]).then(()=>()=>b(8569)))},F={132:[1132],183:[5183],397:[3397],617:[4422],672:[1672],804:[1338]},b.f.consumes=(a,c)=>{b.o(F,a)&&F[a].forEach(d=>{if(b.o(D,d))return c.push(D[d]);var e=a=>{D[d]=0,b.m[d]=c=>{delete b.c[d],c.exports=a()}},f=a=>{delete D[d],b.m[d]=()=>{throw delete b.c[d],a}};try{var g=E[d]();g.then?c.push(D[d]=g.then(e).catch(f)):e(g)}catch(a){f(a)}})},(()=>{var c={447:0};b.f.j=(d,e)=>{var f=b.o(c,d)?c[d]:void 0;if(0!==f)if(f)e.push(f[2]);else if(/^(183|397|672)$/.test(d))c[d]=0;else{var g=new Promise((b,e)=>f=c[d]=[b,e]);e.push(f[2]=g);var a=b.p+b.u(d),h=new Error;b.l(a,e=>{if(b.o(c,d)&&(0!==(f=c[d])&&(c[d]=void 0),f)){var g=e&&("load"===e.type?"missing":e.type),a=e&&e.target&&e.target.src;h.message="Loading chunk "+d+" failed.\n("+g+": "+a+")",h.name="ChunkLoadError",h.type=g,h.request=a,f[1](h)}},"chunk-"+d,d)}};var a=(d,e)=>{var f,g,[h,j,i]=e,k=0;if(h.some(a=>0!==c[a])){for(f in j)b.o(j,f)&&(b.m[f]=j[f]);i&&i(b)}for(d&&d(e);k<h.length;k++)g=h[k],b.o(c,g)&&c[g]&&c[g][0](),c[g]=0},d=self.webpackChunk_openmrs_esm_fast_data_entry_app=self.webpackChunk_openmrs_esm_fast_data_entry_app||[];d.forEach(a.bind(null,0)),d.push=a.bind(null,d.push.bind(d))})(),b.nc=void 0;var m=b(7140);_openmrs_esm_fast_data_entry_app=m})();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openmrs/esm-fast-data-entry-app",
|
|
3
|
-
"version": "1.0.1-pre.
|
|
3
|
+
"version": "1.0.1-pre.15",
|
|
4
4
|
"license": "MPL-2.0",
|
|
5
5
|
"description": "An OpenMRS 3.x microfrontend",
|
|
6
6
|
"browser": "dist/openmrs-esm-fast-data-entry-app.js",
|
|
@@ -45,6 +45,7 @@
|
|
|
45
45
|
"peerDependencies": {
|
|
46
46
|
"@carbon/react": "^1.9.0",
|
|
47
47
|
"@openmrs/esm-framework": "*",
|
|
48
|
+
"lodash-es": "4.x",
|
|
48
49
|
"react": "18.x",
|
|
49
50
|
"react-dom": "18.x",
|
|
50
51
|
"react-i18next": "11.x",
|
|
@@ -76,6 +77,7 @@
|
|
|
76
77
|
"jest": "^28.1.3",
|
|
77
78
|
"jest-cli": "^28.1.3",
|
|
78
79
|
"jest-environment-jsdom": "^28.1.3",
|
|
80
|
+
"lodash-es": "^4.17.21",
|
|
79
81
|
"openmrs": "next",
|
|
80
82
|
"prettier": "^2.3.0",
|
|
81
83
|
"pretty-quick": "^3.1.0",
|
|
@@ -85,13 +87,13 @@
|
|
|
85
87
|
"react-router-dom": "^6.3.0",
|
|
86
88
|
"semver": "^7.3.7",
|
|
87
89
|
"swc-loader": "^0.2.3",
|
|
88
|
-
"swr": "^1.3.0",
|
|
89
90
|
"typescript": "^4.7.3",
|
|
90
91
|
"webpack": "^5.73.0"
|
|
91
92
|
},
|
|
92
93
|
"packageManager": "yarn@3.2.2",
|
|
93
94
|
"dependencies": {
|
|
94
|
-
"react-hook-form": "^7.34.2"
|
|
95
|
+
"react-hook-form": "^7.34.2",
|
|
96
|
+
"swr": "1.1.2"
|
|
95
97
|
},
|
|
96
98
|
"stableVersion": "1.0.0"
|
|
97
99
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { useCallback, useContext, useState } from "react";
|
|
1
|
+
import React, { useCallback, useContext, useEffect, useState } from "react";
|
|
2
2
|
import {
|
|
3
3
|
ComposedModal,
|
|
4
4
|
Button,
|
|
@@ -7,12 +7,14 @@ import {
|
|
|
7
7
|
ModalBody,
|
|
8
8
|
TextInput,
|
|
9
9
|
FormLabel,
|
|
10
|
+
Loading,
|
|
10
11
|
} from "@carbon/react";
|
|
11
|
-
import { Add,
|
|
12
|
+
import { Add, TrashCan } from "@carbon/react/icons";
|
|
12
13
|
import { useTranslation } from "react-i18next";
|
|
13
|
-
import { ExtensionSlot } from "@openmrs/esm-framework";
|
|
14
|
+
import { ExtensionSlot, showToast } from "@openmrs/esm-framework";
|
|
14
15
|
import styles from "./styles.scss";
|
|
15
16
|
import GroupFormWorkflowContext from "../context/GroupFormWorkflowContext";
|
|
17
|
+
import { usePostCohort } from "../hooks";
|
|
16
18
|
|
|
17
19
|
const MemExtension = React.memo(ExtensionSlot);
|
|
18
20
|
|
|
@@ -20,18 +22,19 @@ const PatientRow = ({ patient, removePatient }) => {
|
|
|
20
22
|
const { t } = useTranslation();
|
|
21
23
|
return (
|
|
22
24
|
<li key={patient.uuid} className={styles.patientRow}>
|
|
23
|
-
<span className={styles.patientName}>{patient?.display}</span>
|
|
24
25
|
<span>
|
|
25
26
|
<Button
|
|
26
27
|
kind="tertiary"
|
|
27
28
|
size="sm"
|
|
29
|
+
hasIconOnly
|
|
28
30
|
onClick={() => removePatient(patient.uuid)}
|
|
29
|
-
renderIcon={
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
{t("remove", "Remove")}
|
|
33
|
-
|
|
31
|
+
renderIcon={TrashCan}
|
|
32
|
+
tooltipAlignment="start"
|
|
33
|
+
tooltipPosition="top"
|
|
34
|
+
iconDescription={t("remove", "Remove")}
|
|
35
|
+
/>
|
|
34
36
|
</span>
|
|
37
|
+
<span className={styles.patientName}>{patient?.display}</span>
|
|
35
38
|
</li>
|
|
36
39
|
);
|
|
37
40
|
};
|
|
@@ -64,17 +67,21 @@ const NewGroupForm = (props) => {
|
|
|
64
67
|
/>
|
|
65
68
|
{errors?.name && (
|
|
66
69
|
<p className={styles.formError}>
|
|
67
|
-
{
|
|
70
|
+
{errors.name === "required"
|
|
71
|
+
? t("groupNameError", "Please enter a group name.")
|
|
72
|
+
: errors.name}
|
|
68
73
|
</p>
|
|
69
74
|
)}
|
|
70
|
-
<FormLabel>
|
|
75
|
+
<FormLabel>
|
|
76
|
+
{patientList.length} {t("patientsInGroup", "Patients in group")}
|
|
77
|
+
</FormLabel>
|
|
71
78
|
{errors?.patientList && (
|
|
72
79
|
<p className={styles.formError}>
|
|
73
80
|
{t("noPatientError", "Please enter at least one patient.")}
|
|
74
81
|
</p>
|
|
75
82
|
)}
|
|
76
83
|
{!errors?.patientList && (
|
|
77
|
-
<ul>
|
|
84
|
+
<ul className={styles.patientList}>
|
|
78
85
|
{patientList?.map((patient, index) => (
|
|
79
86
|
<PatientRow
|
|
80
87
|
patient={patient}
|
|
@@ -92,7 +99,7 @@ const NewGroupForm = (props) => {
|
|
|
92
99
|
state={{
|
|
93
100
|
selectPatientAction: updatePatientList,
|
|
94
101
|
buttonProps: {
|
|
95
|
-
kind: "
|
|
102
|
+
kind: "secondary",
|
|
96
103
|
},
|
|
97
104
|
}}
|
|
98
105
|
/>
|
|
@@ -108,6 +115,7 @@ const AddGroupModal = () => {
|
|
|
108
115
|
const [errors, setErrors] = useState({});
|
|
109
116
|
const [name, setName] = useState("");
|
|
110
117
|
const [patientList, setPatientList] = useState([]);
|
|
118
|
+
const { post, result, isPosting, error } = usePostCohort();
|
|
111
119
|
|
|
112
120
|
const handleCancel = () => {
|
|
113
121
|
setOpen(false);
|
|
@@ -165,10 +173,46 @@ const AddGroupModal = () => {
|
|
|
165
173
|
|
|
166
174
|
const handleSubmit = () => {
|
|
167
175
|
if (validate()) {
|
|
168
|
-
|
|
176
|
+
post({
|
|
177
|
+
name: name,
|
|
178
|
+
cohortMembers: patientList.map((p) => ({ patient: p.uuid })),
|
|
179
|
+
});
|
|
169
180
|
}
|
|
170
181
|
};
|
|
171
182
|
|
|
183
|
+
useEffect(() => {
|
|
184
|
+
if (result) {
|
|
185
|
+
setGroup({
|
|
186
|
+
...result,
|
|
187
|
+
// the result doesn't come with cohortMembers.
|
|
188
|
+
// need to add it in based on our local state
|
|
189
|
+
cohortMembers: patientList.map((p) => ({ patient: { uuid: p.uuid } })),
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
}, [result, setGroup, patientList]);
|
|
193
|
+
|
|
194
|
+
useEffect(() => {
|
|
195
|
+
if (error) {
|
|
196
|
+
showToast({
|
|
197
|
+
kind: "error",
|
|
198
|
+
title: t("postError", "POST Error"),
|
|
199
|
+
description:
|
|
200
|
+
error.message ??
|
|
201
|
+
t("unknownPostError", "An unknown error occured while saving data"),
|
|
202
|
+
});
|
|
203
|
+
if (error.fieldErrors) {
|
|
204
|
+
setErrors(
|
|
205
|
+
Object.fromEntries(
|
|
206
|
+
Object.entries(error.fieldErrors).map(([key, value]) => [
|
|
207
|
+
key,
|
|
208
|
+
value?.[0]?.message,
|
|
209
|
+
])
|
|
210
|
+
)
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}, [error, t]);
|
|
215
|
+
|
|
172
216
|
return (
|
|
173
217
|
<div className={styles.modal}>
|
|
174
218
|
<Button
|
|
@@ -181,23 +225,32 @@ const AddGroupModal = () => {
|
|
|
181
225
|
<ComposedModal open={open} onClose={() => setOpen(false)}>
|
|
182
226
|
<ModalHeader>{t("createNewGroup", "Create New Group")}</ModalHeader>
|
|
183
227
|
<ModalBody>
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
228
|
+
{result ? (
|
|
229
|
+
<p>Group saved succesfully</p>
|
|
230
|
+
) : isPosting ? (
|
|
231
|
+
<div className={styles.loading}>
|
|
232
|
+
<Loading withOverlay={false} />
|
|
233
|
+
<span>Saving new group...</span>
|
|
234
|
+
</div>
|
|
235
|
+
) : (
|
|
236
|
+
<NewGroupForm
|
|
237
|
+
{...{
|
|
238
|
+
name,
|
|
239
|
+
setName,
|
|
240
|
+
patientList,
|
|
241
|
+
updatePatientList,
|
|
242
|
+
errors,
|
|
243
|
+
validate,
|
|
244
|
+
removePatient,
|
|
245
|
+
}}
|
|
246
|
+
/>
|
|
247
|
+
)}
|
|
195
248
|
</ModalBody>
|
|
196
249
|
<ModalFooter>
|
|
197
|
-
<Button kind="secondary" onClick={handleCancel}>
|
|
250
|
+
<Button kind="secondary" onClick={handleCancel} disabled={isPosting}>
|
|
198
251
|
{t("cancel", "Cancel")}
|
|
199
252
|
</Button>
|
|
200
|
-
<Button kind="primary" onClick={handleSubmit}>
|
|
253
|
+
<Button kind="primary" onClick={handleSubmit} disabled={isPosting}>
|
|
201
254
|
{t("createGroup", "Create Group")}
|
|
202
255
|
</Button>
|
|
203
256
|
</ModalFooter>
|
|
@@ -23,13 +23,23 @@
|
|
|
23
23
|
|
|
24
24
|
.patientRow {
|
|
25
25
|
display: flex;
|
|
26
|
+
align-items: center;
|
|
26
27
|
width: "100%";
|
|
28
|
+
&:nth-child(odd) {
|
|
29
|
+
background-color: colors.$gray-20;
|
|
30
|
+
}
|
|
27
31
|
}
|
|
28
32
|
|
|
29
33
|
.patientName {
|
|
30
34
|
flex-grow: 1;
|
|
31
|
-
padding: spacing.$spacing-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
+
padding-left: spacing.$spacing-05;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.loading {
|
|
39
|
+
display: flex;
|
|
40
|
+
height: 100%;
|
|
41
|
+
flex-direction: column;
|
|
42
|
+
justify-content: center;
|
|
43
|
+
align-items: center;
|
|
44
|
+
row-gap: spacing.$spacing-05;
|
|
35
45
|
}
|
|
@@ -20,6 +20,7 @@ export interface MetaType {
|
|
|
20
20
|
|
|
21
21
|
const initialActions = {
|
|
22
22
|
setGroup: (group: GroupType) => undefined,
|
|
23
|
+
unsetGroup: () => undefined,
|
|
23
24
|
setSessionMeta: (meta: MetaType) => undefined,
|
|
24
25
|
openPatientSearch: () => undefined,
|
|
25
26
|
saveEncounter: (encounterUuid: string | number) => undefined,
|
|
@@ -75,6 +76,7 @@ const GroupFormWorkflowProvider = ({ children }) => {
|
|
|
75
76
|
activeFormUuid,
|
|
76
77
|
}),
|
|
77
78
|
setGroup: (group) => dispatch({ type: "SET_GROUP", group }),
|
|
79
|
+
unsetGroup: () => dispatch({ type: "UNSET_GROUP" }),
|
|
78
80
|
setSessionMeta: (meta) => dispatch({ type: "SET_SESSION_META", meta }),
|
|
79
81
|
openPatientSearch: () => dispatch({ type: "OPEN_PATIENT_SEARCH" }),
|
|
80
82
|
saveEncounter: (encounterUuid) =>
|
|
@@ -75,9 +75,33 @@ const reducer = (state, action) => {
|
|
|
75
75
|
...state.forms,
|
|
76
76
|
[state.activeFormUuid]: {
|
|
77
77
|
...state.forms[state.activeFormUuid],
|
|
78
|
-
groupUuid: action.group.
|
|
78
|
+
groupUuid: action.group.uuid,
|
|
79
79
|
groupName: action.group.name,
|
|
80
|
-
patientUuids:
|
|
80
|
+
patientUuids:
|
|
81
|
+
// this translation is not preferred
|
|
82
|
+
// the only reason we tollerate it here is beause it should be the only time
|
|
83
|
+
// we add cohort information to state
|
|
84
|
+
action.group.cohortMembers?.map(
|
|
85
|
+
(member) => member?.patient?.uuid
|
|
86
|
+
) ?? [],
|
|
87
|
+
activePatientUuid: null,
|
|
88
|
+
activeEncounterUuid: null,
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
persistData(newState);
|
|
93
|
+
return newState;
|
|
94
|
+
}
|
|
95
|
+
case "UNSET_GROUP": {
|
|
96
|
+
const newState = {
|
|
97
|
+
...state,
|
|
98
|
+
forms: {
|
|
99
|
+
...state.forms,
|
|
100
|
+
[state.activeFormUuid]: {
|
|
101
|
+
...state.forms[state.activeFormUuid],
|
|
102
|
+
groupUuid: null,
|
|
103
|
+
groupName: null,
|
|
104
|
+
patientUuids: [],
|
|
81
105
|
activePatientUuid: null,
|
|
82
106
|
activeEncounterUuid: null,
|
|
83
107
|
},
|
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
import React, { useContext, useEffect, useState } from "react";
|
|
20
20
|
import { useNavigate } from "react-router-dom";
|
|
21
21
|
import PatientCard from "../patient-card/PatientCard";
|
|
22
|
-
import
|
|
22
|
+
import GroupDisplayHeader from "./group-display-header";
|
|
23
23
|
import styles from "./styles.scss";
|
|
24
24
|
import { useTranslation } from "react-i18next";
|
|
25
25
|
import GroupFormWorkflowContext, {
|
|
@@ -387,7 +387,7 @@ const GroupFormEntryWorkflow = () => {
|
|
|
387
387
|
<ExtensionSlot extensionSlotName="breadcrumbs-slot" />
|
|
388
388
|
</div>
|
|
389
389
|
<GroupSearchHeader />
|
|
390
|
-
<
|
|
390
|
+
<GroupDisplayHeader />
|
|
391
391
|
{workflowState === "NEW_GROUP_SESSION" && (
|
|
392
392
|
<div className={styles.workspaceWrapper}>
|
|
393
393
|
<SessionMetaWorkspace />
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import { render } from "@testing-library/react";
|
|
3
|
-
import
|
|
3
|
+
import GroupDisplayHeader from "./GroupDisplayHeader";
|
|
4
4
|
|
|
5
5
|
describe("PatientBanner", () => {
|
|
6
6
|
it("renders placeholder information when no data is present", () => {
|
|
7
|
-
render(<
|
|
7
|
+
render(<GroupDisplayHeader />);
|
|
8
8
|
});
|
|
9
9
|
});
|
|
@@ -1,12 +1,20 @@
|
|
|
1
1
|
import React, { useContext } from "react";
|
|
2
|
-
import {
|
|
2
|
+
import { Button } from "@carbon/react";
|
|
3
|
+
import { Events, Close } from "@carbon/react/icons";
|
|
3
4
|
import styles from "./styles.scss";
|
|
4
5
|
import { useTranslation } from "react-i18next";
|
|
5
6
|
import GroupFormWorkflowContext from "../../context/GroupFormWorkflowContext";
|
|
7
|
+
import { navigate } from "@openmrs/esm-framework";
|
|
6
8
|
|
|
7
|
-
const
|
|
8
|
-
const {
|
|
9
|
-
|
|
9
|
+
const GroupDisplayHeader = () => {
|
|
10
|
+
const {
|
|
11
|
+
activeGroupName,
|
|
12
|
+
activeGroupUuid,
|
|
13
|
+
patientUuids,
|
|
14
|
+
activeSessionMeta,
|
|
15
|
+
unsetGroup,
|
|
16
|
+
destroySession,
|
|
17
|
+
} = useContext(GroupFormWorkflowContext);
|
|
10
18
|
const { t } = useTranslation();
|
|
11
19
|
|
|
12
20
|
if (!activeGroupUuid) {
|
|
@@ -38,8 +46,26 @@ const GroupBanner = () => {
|
|
|
38
46
|
</div>
|
|
39
47
|
</div>
|
|
40
48
|
)}
|
|
49
|
+
<span style={{ flexGrow: 1 }} />
|
|
50
|
+
<span>
|
|
51
|
+
<Button kind="ghost" onClick={() => unsetGroup()}>
|
|
52
|
+
{t("changeGroup", "Choose a different group")} <Close size={20} />
|
|
53
|
+
</Button>
|
|
54
|
+
</span>
|
|
55
|
+
<span>
|
|
56
|
+
<Button
|
|
57
|
+
kind="ghost"
|
|
58
|
+
onClick={() => {
|
|
59
|
+
destroySession();
|
|
60
|
+
// eslint-disable-next-line
|
|
61
|
+
navigate({ to: "${openmrsSpaBase}/forms" });
|
|
62
|
+
}}
|
|
63
|
+
>
|
|
64
|
+
{t("cancel", "Cancel")} <Close size={20} />
|
|
65
|
+
</Button>
|
|
66
|
+
</span>
|
|
41
67
|
</div>
|
|
42
68
|
);
|
|
43
69
|
};
|
|
44
70
|
|
|
45
|
-
export default
|
|
71
|
+
export default GroupDisplayHeader;
|
|
File without changes
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { useEffect, useReducer } from "react";
|
|
1
|
+
import React, { useEffect, useReducer, useRef } from "react";
|
|
2
2
|
import { SkeletonIcon, SkeletonText } from "@carbon/react";
|
|
3
3
|
import { Events } from "@carbon/react/icons";
|
|
4
4
|
import styles from "./compact-group-result.scss";
|
|
@@ -20,11 +20,64 @@ const reducer = (state, action) => {
|
|
|
20
20
|
}
|
|
21
21
|
};
|
|
22
22
|
|
|
23
|
-
const
|
|
23
|
+
const scrollingOptions = {
|
|
24
|
+
behavior: "smooth",
|
|
25
|
+
block: "nearest",
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const ResultItem = ({
|
|
29
|
+
index,
|
|
30
|
+
selectGroupAction,
|
|
31
|
+
group,
|
|
32
|
+
dispatch,
|
|
33
|
+
state,
|
|
34
|
+
totalGroups,
|
|
35
|
+
lastRef,
|
|
36
|
+
}) => {
|
|
37
|
+
const ref = useRef(null);
|
|
38
|
+
const { t } = useTranslation();
|
|
39
|
+
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
if (state.selectedIndex === totalGroups - 1) {
|
|
42
|
+
lastRef.current.scrollIntoView(scrollingOptions);
|
|
43
|
+
} else if (state.selectedIndex === index) {
|
|
44
|
+
ref.current.scrollIntoView(scrollingOptions);
|
|
45
|
+
}
|
|
46
|
+
}, [state, index, totalGroups, lastRef]);
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<div
|
|
50
|
+
onClick={() => {
|
|
51
|
+
dispatch({ type: "select", payload: index });
|
|
52
|
+
selectGroupAction(group);
|
|
53
|
+
}}
|
|
54
|
+
className={`${styles.patientSearchResult} ${
|
|
55
|
+
index === state.selectedIndex && styles.patientSearchResultSelected
|
|
56
|
+
}`}
|
|
57
|
+
role="button"
|
|
58
|
+
aria-pressed={index === state.selectedIndex}
|
|
59
|
+
tabIndex={0}
|
|
60
|
+
ref={ref}
|
|
61
|
+
>
|
|
62
|
+
<div className={styles.patientAvatar} role="img">
|
|
63
|
+
<Events size={24} />
|
|
64
|
+
</div>
|
|
65
|
+
<div>
|
|
66
|
+
<h2 className={styles.patientName}>{group.name}</h2>
|
|
67
|
+
<p className={styles.demographics}>
|
|
68
|
+
{group.cohortMembers?.length ?? 0} {t("members", "members")}
|
|
69
|
+
<span className={styles.middot}>·</span> {group.description}
|
|
70
|
+
</p>
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
);
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const CompactGroupResults = ({ groups, selectGroupAction, lastRef }) => {
|
|
24
77
|
const arrowUpPressed = useKeyPress("ArrowUp");
|
|
25
78
|
const arrowDownPressed = useKeyPress("ArrowDown");
|
|
26
79
|
const enterPressed = useKeyPress("Enter");
|
|
27
|
-
|
|
80
|
+
|
|
28
81
|
const [state, dispatch] = useReducer(reducer, { selectedIndex: 0 });
|
|
29
82
|
|
|
30
83
|
useEffect(() => {
|
|
@@ -48,31 +101,11 @@ const CompactGroupResults = ({ groups, selectGroupAction }) => {
|
|
|
48
101
|
return (
|
|
49
102
|
<>
|
|
50
103
|
{groups.map((group, index) => (
|
|
51
|
-
<
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
key={group.id}
|
|
57
|
-
className={`${styles.patientSearchResult} ${
|
|
58
|
-
index === state.selectedIndex && styles.patientSearchResultSelected
|
|
59
|
-
}`}
|
|
60
|
-
role="button"
|
|
61
|
-
aria-pressed={index === state.selectedIndex}
|
|
62
|
-
tabIndex={0}
|
|
63
|
-
>
|
|
64
|
-
<div className={styles.patientAvatar} role="img">
|
|
65
|
-
<Events size={24} />
|
|
66
|
-
</div>
|
|
67
|
-
<div>
|
|
68
|
-
<h2 className={styles.patientName}>{group.name}</h2>
|
|
69
|
-
<p className={styles.demographics}>
|
|
70
|
-
{group.members.length} {t("members", "members")}
|
|
71
|
-
<span className={styles.middot}>·</span>{" "}
|
|
72
|
-
{group.description}
|
|
73
|
-
</p>
|
|
74
|
-
</div>
|
|
75
|
-
</div>
|
|
104
|
+
<ResultItem
|
|
105
|
+
key={index}
|
|
106
|
+
totalGroups={groups.length}
|
|
107
|
+
{...{ lastRef, index, selectGroupAction, group, dispatch, state }}
|
|
108
|
+
/>
|
|
76
109
|
))}
|
|
77
110
|
</>
|
|
78
111
|
);
|
|
@@ -4,6 +4,7 @@ import styles from "./compact-group-search.scss";
|
|
|
4
4
|
import GroupSearch from "./GroupSearch";
|
|
5
5
|
import { Button, Search } from "@carbon/react";
|
|
6
6
|
import { useTranslation } from "react-i18next";
|
|
7
|
+
import debounce from "lodash-es/debounce";
|
|
7
8
|
|
|
8
9
|
interface CompactGroupSearchProps {
|
|
9
10
|
selectGroupAction?: (group: GroupType) => void;
|
|
@@ -23,6 +24,10 @@ const CompactGroupSearch: React.FC<CompactGroupSearchProps> = ({
|
|
|
23
24
|
};
|
|
24
25
|
|
|
25
26
|
const handleSearchChange = (e) => {
|
|
27
|
+
debounce((q) => {
|
|
28
|
+
setDropdownShown(!!e.length);
|
|
29
|
+
setQuery(q);
|
|
30
|
+
}, 300);
|
|
26
31
|
setQuery(e);
|
|
27
32
|
if (e.length) {
|
|
28
33
|
setDropdownShown(true);
|
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import React from "react";
|
|
1
|
+
import React, { useCallback, useRef } from "react";
|
|
2
2
|
import { useTranslation } from "react-i18next";
|
|
3
|
-
import { Layer, Tile } from "@carbon/react";
|
|
3
|
+
import { Layer, Tile, Loading } from "@carbon/react";
|
|
4
4
|
import styles from "./group-search.scss";
|
|
5
5
|
import { EmptyDataIllustration } from "../../empty-state/EmptyDataIllustration";
|
|
6
|
-
import { useGroupSearch } from "./useGroupSearch";
|
|
7
6
|
import CompactGroupResults, {
|
|
8
7
|
SearchResultSkeleton,
|
|
9
8
|
} from "./CompactGroupResults";
|
|
10
9
|
import { GroupType } from "../../context/GroupFormWorkflowContext";
|
|
10
|
+
import { useSearchCohortInfinite } from "../../hooks/useSearchEndpoint";
|
|
11
11
|
|
|
12
12
|
interface GroupSearchProps {
|
|
13
13
|
query: string;
|
|
@@ -19,8 +19,48 @@ const GroupSearch: React.FC<GroupSearchProps> = ({
|
|
|
19
19
|
selectGroupAction,
|
|
20
20
|
}) => {
|
|
21
21
|
const { t } = useTranslation();
|
|
22
|
-
const
|
|
23
|
-
|
|
22
|
+
const {
|
|
23
|
+
isLoading,
|
|
24
|
+
data: results,
|
|
25
|
+
error,
|
|
26
|
+
loadingNewData,
|
|
27
|
+
setPage,
|
|
28
|
+
hasMore,
|
|
29
|
+
totalResults,
|
|
30
|
+
} = useSearchCohortInfinite({
|
|
31
|
+
searchTerm: query,
|
|
32
|
+
searching: !!query,
|
|
33
|
+
parameters: {
|
|
34
|
+
v: "full",
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const lastItem = useRef(null);
|
|
39
|
+
const observer = useRef(null);
|
|
40
|
+
const loadingRef = useCallback(
|
|
41
|
+
(node) => {
|
|
42
|
+
if (loadingNewData) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
if (observer.current) {
|
|
46
|
+
observer.current.disconnect();
|
|
47
|
+
}
|
|
48
|
+
observer.current = new IntersectionObserver(
|
|
49
|
+
(entries) => {
|
|
50
|
+
if (entries[0].isIntersecting && hasMore) {
|
|
51
|
+
setPage((page) => page + 1);
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
threshold: 0.75,
|
|
56
|
+
}
|
|
57
|
+
);
|
|
58
|
+
if (node) {
|
|
59
|
+
observer.current.observe(node);
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
[loadingNewData, hasMore, setPage]
|
|
63
|
+
);
|
|
24
64
|
|
|
25
65
|
if (error) {
|
|
26
66
|
return (
|
|
@@ -43,9 +83,19 @@ const GroupSearch: React.FC<GroupSearchProps> = ({
|
|
|
43
83
|
);
|
|
44
84
|
}
|
|
45
85
|
|
|
46
|
-
if (
|
|
86
|
+
if (isLoading) {
|
|
87
|
+
return (
|
|
88
|
+
<div className={styles.searchResultsContainer}>
|
|
89
|
+
<SearchResultSkeleton />
|
|
90
|
+
<SearchResultSkeleton />
|
|
91
|
+
<SearchResultSkeleton />
|
|
92
|
+
<SearchResultSkeleton />
|
|
93
|
+
<SearchResultSkeleton />
|
|
94
|
+
</div>
|
|
95
|
+
);
|
|
96
|
+
}
|
|
47
97
|
|
|
48
|
-
if (results
|
|
98
|
+
if (results?.length === 0) {
|
|
49
99
|
return (
|
|
50
100
|
<div className={styles.searchResults}>
|
|
51
101
|
<Layer>
|
|
@@ -79,12 +129,19 @@ const GroupSearch: React.FC<GroupSearchProps> = ({
|
|
|
79
129
|
}}
|
|
80
130
|
>
|
|
81
131
|
<p className={styles.resultsText}>
|
|
82
|
-
{
|
|
132
|
+
{totalResults} {t("searchResultsText", "search result(s)")}
|
|
83
133
|
</p>
|
|
84
134
|
<CompactGroupResults
|
|
85
135
|
groups={results}
|
|
86
136
|
selectGroupAction={selectGroupAction}
|
|
137
|
+
lastRef={lastItem}
|
|
87
138
|
/>
|
|
139
|
+
<div ref={lastItem}>
|
|
140
|
+
<div className={styles.lastItem} ref={loadingRef}>
|
|
141
|
+
{hasMore && <Loading withOverlay={false} small />}
|
|
142
|
+
{!hasMore && <p>{t("noMoreResults", "End of search results")}</p>}
|
|
143
|
+
</div>
|
|
144
|
+
</div>
|
|
88
145
|
</div>
|
|
89
146
|
</div>
|
|
90
147
|
);
|
|
@@ -29,12 +29,7 @@
|
|
|
29
29
|
width: 100%;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
|
|
33
|
-
padding: spacing.$spacing-05 0;
|
|
34
|
-
display: flex;
|
|
35
|
-
justify-content: center;
|
|
36
|
-
align-items: center;
|
|
37
|
-
}
|
|
32
|
+
|
|
38
33
|
|
|
39
34
|
.searchTerm {
|
|
40
35
|
@include type.type-style('heading-03');
|
|
@@ -92,3 +87,10 @@
|
|
|
92
87
|
@include type.type-style('body-01');
|
|
93
88
|
color: $text-02;
|
|
94
89
|
}
|
|
90
|
+
|
|
91
|
+
.lastItem {
|
|
92
|
+
padding: spacing.$spacing-05;
|
|
93
|
+
display: flex;
|
|
94
|
+
justify-content: center;
|
|
95
|
+
align-items: center;
|
|
96
|
+
}
|
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
import { Close } from "@carbon/react/icons";
|
|
2
2
|
import { Button } from "@carbon/react";
|
|
3
3
|
import React, { useContext } from "react";
|
|
4
|
-
import { Link } from "react-router-dom";
|
|
5
4
|
import GroupFormWorkflowContext from "../../context/GroupFormWorkflowContext";
|
|
6
5
|
import styles from "./styles.scss";
|
|
7
6
|
import { useTranslation } from "react-i18next";
|
|
8
7
|
import CompactGroupSearch from "../group-search/CompactGroupSearch";
|
|
9
8
|
import AddGroupModal from "../../add-group-modal/AddGroupModal";
|
|
9
|
+
import { navigate } from "@openmrs/esm-framework";
|
|
10
10
|
|
|
11
11
|
const GroupSearchHeader = () => {
|
|
12
12
|
const { t } = useTranslation();
|
|
13
|
-
const { activeGroupUuid, setGroup } = useContext(
|
|
13
|
+
const { activeGroupUuid, setGroup, destroySession } = useContext(
|
|
14
|
+
GroupFormWorkflowContext
|
|
15
|
+
);
|
|
14
16
|
const handleSelectGroup = (group) => {
|
|
15
17
|
setGroup(group);
|
|
16
18
|
};
|
|
@@ -29,11 +31,16 @@ const GroupSearchHeader = () => {
|
|
|
29
31
|
</span>
|
|
30
32
|
<span style={{ flexGrow: 1 }} />
|
|
31
33
|
<span>
|
|
32
|
-
<
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
34
|
+
<Button
|
|
35
|
+
kind="ghost"
|
|
36
|
+
onClick={() => {
|
|
37
|
+
destroySession();
|
|
38
|
+
// eslint-disable-next-line
|
|
39
|
+
navigate({ to: "${openmrsSpaBase}/forms" });
|
|
40
|
+
}}
|
|
41
|
+
>
|
|
42
|
+
{t("cancel", "Cancel")} <Close size={20} />
|
|
43
|
+
</Button>
|
|
37
44
|
</span>
|
|
38
45
|
</div>
|
|
39
46
|
);
|
|
@@ -19,6 +19,16 @@
|
|
|
19
19
|
width: 1100px;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
:global(.omrs-breakpoint-lt-large-desktop) .workspace {
|
|
23
|
+
width: 1000px;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
:global(.omrs-breakpoint-lt-small-desktop) .workspace {
|
|
27
|
+
// there's only so much we can do here. Currenlty the design does not support tablet
|
|
28
|
+
width: 100vw;
|
|
29
|
+
padding: 0 spacing.$spacing-04;
|
|
30
|
+
}
|
|
31
|
+
|
|
22
32
|
.selectPatientMessage {
|
|
23
33
|
@include type.type-style('productive-heading-03');
|
|
24
34
|
margin: spacing.$spacing-07;
|
package/src/hooks/index.ts
CHANGED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { openmrsFetch } from "@openmrs/esm-framework";
|
|
2
|
+
import { useCallback, useState } from "react";
|
|
3
|
+
|
|
4
|
+
const usePostEndpoint = ({ endpointUrl }) => {
|
|
5
|
+
const [submissionInProgress, setSubmissionInProgress] = useState(null);
|
|
6
|
+
const [result, setResult] = useState(null);
|
|
7
|
+
const [error, setError] = useState(null);
|
|
8
|
+
|
|
9
|
+
const onFormPosted = useCallback(
|
|
10
|
+
(result) => {
|
|
11
|
+
setSubmissionInProgress(false);
|
|
12
|
+
if (error) {
|
|
13
|
+
setError(null);
|
|
14
|
+
}
|
|
15
|
+
setResult(result.data);
|
|
16
|
+
},
|
|
17
|
+
[error]
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
const onError = useCallback(
|
|
21
|
+
(error) => {
|
|
22
|
+
setSubmissionInProgress(false);
|
|
23
|
+
if (result) {
|
|
24
|
+
setResult(null);
|
|
25
|
+
}
|
|
26
|
+
setError(error?.responseBody?.error ?? error?.responseBody ?? error);
|
|
27
|
+
},
|
|
28
|
+
[result]
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
const post = useCallback(
|
|
32
|
+
async (data) => {
|
|
33
|
+
setSubmissionInProgress(true);
|
|
34
|
+
return openmrsFetch(endpointUrl, {
|
|
35
|
+
method: "POST",
|
|
36
|
+
headers: {
|
|
37
|
+
"Content-Type": "application/json",
|
|
38
|
+
},
|
|
39
|
+
body: data,
|
|
40
|
+
})
|
|
41
|
+
.then(onFormPosted)
|
|
42
|
+
.catch(onError);
|
|
43
|
+
},
|
|
44
|
+
[endpointUrl, onError, onFormPosted]
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
const reset = () => {
|
|
48
|
+
setSubmissionInProgress(null);
|
|
49
|
+
setResult(null);
|
|
50
|
+
setError(null);
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
post,
|
|
55
|
+
isPosting: submissionInProgress,
|
|
56
|
+
result,
|
|
57
|
+
error,
|
|
58
|
+
reset,
|
|
59
|
+
};
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const usePostVisit = () => {
|
|
63
|
+
return usePostEndpoint({ endpointUrl: "/ws/rest/v1/visit" });
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const usePostCohort = () => {
|
|
67
|
+
return usePostEndpoint({ endpointUrl: "/ws/rest/v1/cohortm/cohort" });
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
export { usePostEndpoint, usePostVisit, usePostCohort };
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { openmrsFetch, FetchResponse } from "@openmrs/esm-framework";
|
|
2
|
+
import { useCallback, useMemo } from "react";
|
|
3
|
+
import useSWRInfinite from "swr/infinite";
|
|
4
|
+
|
|
5
|
+
export interface SearchResponse {
|
|
6
|
+
data: Array<Record<string, unknown>> | null;
|
|
7
|
+
isLoading: boolean;
|
|
8
|
+
error: Error;
|
|
9
|
+
loadingNewData: boolean;
|
|
10
|
+
hasMore: boolean;
|
|
11
|
+
currentPage: number;
|
|
12
|
+
totalResults: number;
|
|
13
|
+
setPage: (size: number | ((_size: number) => number)) => Promise<
|
|
14
|
+
FetchResponse<{
|
|
15
|
+
results: Array<Record<string, unknown>>;
|
|
16
|
+
links: Array<{
|
|
17
|
+
rel: "prev" | "next";
|
|
18
|
+
}>;
|
|
19
|
+
}>[]
|
|
20
|
+
>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface SearchInfiniteProps {
|
|
24
|
+
baseUrl?: string;
|
|
25
|
+
searchTerm: string;
|
|
26
|
+
parameters?: Record<string, unknown> | undefined;
|
|
27
|
+
searching: boolean;
|
|
28
|
+
resultsToFetch?: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const useSearchEndpointInfinite = (
|
|
32
|
+
arg0: SearchInfiniteProps
|
|
33
|
+
): SearchResponse => {
|
|
34
|
+
const {
|
|
35
|
+
baseUrl,
|
|
36
|
+
searchTerm,
|
|
37
|
+
parameters,
|
|
38
|
+
searching = true,
|
|
39
|
+
resultsToFetch = 10,
|
|
40
|
+
} = arg0;
|
|
41
|
+
|
|
42
|
+
const getUrl = useCallback(
|
|
43
|
+
(
|
|
44
|
+
page: number,
|
|
45
|
+
prevPageData: FetchResponse<{
|
|
46
|
+
results: Array<Record<string, unknown>>;
|
|
47
|
+
links: Array<{ rel: "prev" | "next" }>;
|
|
48
|
+
}>
|
|
49
|
+
) => {
|
|
50
|
+
if (
|
|
51
|
+
prevPageData &&
|
|
52
|
+
!prevPageData?.data?.links.some((link) => link.rel === "next")
|
|
53
|
+
) {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
let url = `${baseUrl}?q=${searchTerm}`;
|
|
57
|
+
const params = {
|
|
58
|
+
// merge passed parameters and default parameters
|
|
59
|
+
// this way the defaults can be overriden if needed
|
|
60
|
+
totalCount: true,
|
|
61
|
+
limit: resultsToFetch,
|
|
62
|
+
...parameters,
|
|
63
|
+
};
|
|
64
|
+
Object.entries(params).forEach(([key, value]) => {
|
|
65
|
+
// don't send null parmeters
|
|
66
|
+
if (value !== null && value !== undefined) {
|
|
67
|
+
url += `&${key}=${value}`;
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
if (page) {
|
|
71
|
+
url += `&startIndex=${page * resultsToFetch}`;
|
|
72
|
+
}
|
|
73
|
+
return url;
|
|
74
|
+
},
|
|
75
|
+
[baseUrl, searchTerm, parameters, resultsToFetch]
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
const { data, isValidating, setSize, error, size } = useSWRInfinite<
|
|
79
|
+
FetchResponse<{
|
|
80
|
+
results: Array<Record<string, unknown>>;
|
|
81
|
+
links: Array<{ rel: "prev" | "next" }>;
|
|
82
|
+
totalCount: number;
|
|
83
|
+
}>,
|
|
84
|
+
Error
|
|
85
|
+
>(searching ? getUrl : null, openmrsFetch);
|
|
86
|
+
|
|
87
|
+
const results = useMemo(
|
|
88
|
+
() => ({
|
|
89
|
+
data: data
|
|
90
|
+
? [].concat(...(data?.map((resp) => resp?.data?.results) ?? []))
|
|
91
|
+
: null,
|
|
92
|
+
isLoading: !data && !error,
|
|
93
|
+
error,
|
|
94
|
+
hasMore: data?.length
|
|
95
|
+
? !!data[data.length - 1].data?.links?.some(
|
|
96
|
+
(link) => link.rel === "next"
|
|
97
|
+
)
|
|
98
|
+
: false,
|
|
99
|
+
loadingNewData: isValidating,
|
|
100
|
+
setPage: setSize,
|
|
101
|
+
currentPage: size,
|
|
102
|
+
totalResults: data?.[0]?.data?.totalCount,
|
|
103
|
+
}),
|
|
104
|
+
[data, isValidating, error, setSize, size]
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
return results;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const useSearchCohortInfinite = ({
|
|
111
|
+
...props
|
|
112
|
+
}: SearchInfiniteProps): SearchResponse => {
|
|
113
|
+
return useSearchEndpointInfinite({
|
|
114
|
+
baseUrl: "/ws/rest/v1/cohortm/cohort",
|
|
115
|
+
resultsToFetch: 10,
|
|
116
|
+
...props,
|
|
117
|
+
});
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
export { useSearchEndpointInfinite, useSearchCohortInfinite };
|
package/translations/en.json
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
"areYouSure": "Are you sure?",
|
|
5
5
|
"cancel": "Cancel",
|
|
6
6
|
"cancelExplanation": "You will lose any unsaved changes on the current form. Do you want to discard the current session?",
|
|
7
|
+
"changeGroup": "Choose a different group",
|
|
7
8
|
"chooseGroupError": "Please choose a group.",
|
|
8
9
|
"createNewPatient": "Create new patient",
|
|
9
10
|
"createGroup": "Create Group",
|
|
@@ -27,10 +28,13 @@
|
|
|
27
28
|
"nextPatient": "Next Patient",
|
|
28
29
|
"noFormsFound": "No Forms To Show",
|
|
29
30
|
"noFormsFoundMessage": "No forms could be found for this category. Please double check the form concept uuids and access permissions.",
|
|
31
|
+
"noMoreResults": "End of search results",
|
|
30
32
|
"noGroupsFoundMessage": "Sorry, no groups have been found",
|
|
31
33
|
"noPatientError": "Please enter at least one patient.",
|
|
32
34
|
"or": "or",
|
|
33
35
|
"orLabelName": "OR label name",
|
|
36
|
+
"patientsInGroup": "Patients in group",
|
|
37
|
+
"postError": "POST Error",
|
|
34
38
|
"practitionerName": "Practitioner Name",
|
|
35
39
|
"remove": "Remove",
|
|
36
40
|
"resumeSession": "Resume Session",
|
|
@@ -45,5 +49,6 @@
|
|
|
45
49
|
"sessionName": "Session Name",
|
|
46
50
|
"sessionNotes": "Session Notes",
|
|
47
51
|
"startGroupSession": "Start Group Session",
|
|
48
|
-
"trySearchWithPatientUniqueID": "Try searching with the cohort's description"
|
|
52
|
+
"trySearchWithPatientUniqueID": "Try searching with the cohort's description",
|
|
53
|
+
"unknownPostError": "An unknown error occured while saving data"
|
|
49
54
|
}
|
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
export const mockGroupData = [
|
|
2
|
-
{
|
|
3
|
-
id: 1,
|
|
4
|
-
name: "Teal Group",
|
|
5
|
-
description: "Coping skills group - Calgary",
|
|
6
|
-
members: [
|
|
7
|
-
{
|
|
8
|
-
name: "Donna Campbell",
|
|
9
|
-
uuid: "f87cae5e-6b64-4f64-884d-d46043a3b7da",
|
|
10
|
-
},
|
|
11
|
-
{
|
|
12
|
-
name: "Agnes Lewis",
|
|
13
|
-
uuid: "8af48d72-b155-4b41-ba9c-39e017fcf452",
|
|
14
|
-
},
|
|
15
|
-
{
|
|
16
|
-
name: "Daniel Mitchell",
|
|
17
|
-
uuid: "63665703-de71-4a2b-baa4-1a3acf995b7f",
|
|
18
|
-
},
|
|
19
|
-
],
|
|
20
|
-
},
|
|
21
|
-
{
|
|
22
|
-
id: 2,
|
|
23
|
-
name: "Orange Group",
|
|
24
|
-
description: "Thursday night depression session",
|
|
25
|
-
members: [
|
|
26
|
-
{
|
|
27
|
-
name: "Mark Martin",
|
|
28
|
-
uuid: "eaef5256-ce7c-402c-9e56-f8e5d1fba22f",
|
|
29
|
-
},
|
|
30
|
-
{
|
|
31
|
-
name: "Robert Evans",
|
|
32
|
-
uuid: "8648a68d-1d68-4293-9d1a-a7a188b91418",
|
|
33
|
-
},
|
|
34
|
-
{
|
|
35
|
-
name: "Linda Johnson",
|
|
36
|
-
uuid: "8078cc5f-3cd7-4ebb-9ade-515c0dd5f85f",
|
|
37
|
-
},
|
|
38
|
-
{
|
|
39
|
-
name: "Daniel Green",
|
|
40
|
-
uuid: "4ff33dfa-e4ad-4cb5-bbad-8936f9d162df",
|
|
41
|
-
},
|
|
42
|
-
{
|
|
43
|
-
name: "Kimberly Adams",
|
|
44
|
-
uuid: "65bb2ebd-fef9-48c4-9c64-16445fa4fe7c",
|
|
45
|
-
},
|
|
46
|
-
{
|
|
47
|
-
name: "Daniel Lee",
|
|
48
|
-
uuid: "7d46336f-4b6e-4daa-a122-77a7efeb7a2e",
|
|
49
|
-
},
|
|
50
|
-
{
|
|
51
|
-
name: "Nancy Hernández",
|
|
52
|
-
uuid: "89ee2163-4dd4-4d4b-9882-f6be19c15b8f",
|
|
53
|
-
},
|
|
54
|
-
],
|
|
55
|
-
},
|
|
56
|
-
{
|
|
57
|
-
id: 3,
|
|
58
|
-
name: "Blue Group",
|
|
59
|
-
description: "Thursday night depression session",
|
|
60
|
-
members: [
|
|
61
|
-
{
|
|
62
|
-
name: "Patty Patterson",
|
|
63
|
-
uuid: "9bf3adf4-3a5e-48b2-9adf-e117ce2ecdc5",
|
|
64
|
-
},
|
|
65
|
-
{
|
|
66
|
-
name: "Johnny Cash",
|
|
67
|
-
uuid: "f43172b8-a6fc-4edb-b92b-8d759ed9da50",
|
|
68
|
-
},
|
|
69
|
-
{
|
|
70
|
-
name: "John Doe Two",
|
|
71
|
-
uuid: "fa918efe-a4a4-4ac8-8381-3ba14ac61953",
|
|
72
|
-
},
|
|
73
|
-
{
|
|
74
|
-
name: "Foo Bazzi Bar",
|
|
75
|
-
uuid: "791c9475-8396-4519-a8ab-905cee8cccd3",
|
|
76
|
-
},
|
|
77
|
-
],
|
|
78
|
-
},
|
|
79
|
-
];
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import { mockGroupData } from "./mock-group-data";
|
|
2
|
-
|
|
3
|
-
export function useGroupSearch(filter) {
|
|
4
|
-
const searchHistory = mockGroupData || [];
|
|
5
|
-
// if (filter.length <= 2) return [];
|
|
6
|
-
|
|
7
|
-
return filter
|
|
8
|
-
? searchHistory?.filter(
|
|
9
|
-
(item) =>
|
|
10
|
-
item?.description?.toLowerCase()?.includes(filter?.toLowerCase()) ||
|
|
11
|
-
item?.name?.toLowerCase()?.includes(filter?.toLowerCase())
|
|
12
|
-
)
|
|
13
|
-
: searchHistory;
|
|
14
|
-
}
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import { openmrsFetch } from "@openmrs/esm-framework";
|
|
2
|
-
|
|
3
|
-
const usePostCohort = () => {
|
|
4
|
-
const cohortURL = `/ws/rest/v1/cohortm/cohort`;
|
|
5
|
-
const fetcher = openmrsFetch(cohortURL, {
|
|
6
|
-
method: "POST",
|
|
7
|
-
headers: {
|
|
8
|
-
"Content-Type": "application/json",
|
|
9
|
-
},
|
|
10
|
-
body: {
|
|
11
|
-
name: "Magenta",
|
|
12
|
-
cohortType: "hello",
|
|
13
|
-
},
|
|
14
|
-
});
|
|
15
|
-
return fetcher;
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
export default usePostCohort;
|