@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.
Files changed (26) hide show
  1. package/.yarn/versions/{45b499b6.yml → c1451405.yml} +0 -0
  2. package/dist/openmrs-esm-fast-data-entry-app.js +1 -1
  3. package/package.json +5 -3
  4. package/src/add-group-modal/AddGroupModal.tsx +80 -27
  5. package/src/add-group-modal/styles.scss +14 -4
  6. package/src/context/GroupFormWorkflowContext.tsx +2 -0
  7. package/src/context/GroupFormWorkflowReducer.ts +26 -2
  8. package/src/group-form-entry-workflow/GroupFormEntryWorkflow.tsx +2 -2
  9. package/src/group-form-entry-workflow/{group-banner/GroupBanner.test.tsx → group-display-header/GroupDisplayHeader.test.tsx} +2 -2
  10. package/src/group-form-entry-workflow/{group-banner/GroupBanner.tsx → group-display-header/GroupDisplayHeader.tsx} +31 -5
  11. package/src/group-form-entry-workflow/group-display-header/index.ts +3 -0
  12. package/src/group-form-entry-workflow/{group-banner → group-display-header}/styles.scss +0 -0
  13. package/src/group-form-entry-workflow/group-search/CompactGroupResults.tsx +61 -28
  14. package/src/group-form-entry-workflow/group-search/CompactGroupSearch.tsx +5 -0
  15. package/src/group-form-entry-workflow/group-search/GroupSearch.tsx +65 -8
  16. package/src/group-form-entry-workflow/group-search/group-search.scss +8 -6
  17. package/src/group-form-entry-workflow/group-search-header/GroupSearchHeader.tsx +14 -7
  18. package/src/group-form-entry-workflow/styles.scss +10 -0
  19. package/src/hooks/index.ts +1 -0
  20. package/src/hooks/usePostEndpoint.ts +70 -0
  21. package/src/hooks/useSearchEndpoint.ts +120 -0
  22. package/translations/en.json +6 -1
  23. package/src/group-form-entry-workflow/group-banner/index.ts +0 -3
  24. package/src/group-form-entry-workflow/group-search/mock-group-data.ts +0 -79
  25. package/src/group-form-entry-workflow/group-search/useGroupSearch.ts +0 -14
  26. 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.275",()=>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],672:[1672],804:[1338],877:[4422]},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})();
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.10",
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, Close } from "@carbon/react/icons";
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={Close}
30
- tooltipPosition="right"
31
- >
32
- {t("remove", "Remove")}
33
- </Button>
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
- {t("groupNameError", "Please enter a group name.")}
70
+ {errors.name === "required"
71
+ ? t("groupNameError", "Please enter a group name.")
72
+ : errors.name}
68
73
  </p>
69
74
  )}
70
- <FormLabel>Patients in group</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: "primary",
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
- setGroup({ id: "1234", name: name, members: patientList });
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
- <NewGroupForm
185
- {...{
186
- name,
187
- setName,
188
- patientList,
189
- updatePatientList,
190
- errors,
191
- validate,
192
- removePatient,
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-02;
32
- &:hover {
33
- background-color: colors.$gray-20;
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.id,
78
+ groupUuid: action.group.uuid,
79
79
  groupName: action.group.name,
80
- patientUuids: action.group.members.map((member) => member.uuid),
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 GroupBanner from "./group-banner";
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
- <GroupBanner />
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 GroupBanner from "./GroupBanner";
3
+ import GroupDisplayHeader from "./GroupDisplayHeader";
4
4
 
5
5
  describe("PatientBanner", () => {
6
6
  it("renders placeholder information when no data is present", () => {
7
- render(<GroupBanner />);
7
+ render(<GroupDisplayHeader />);
8
8
  });
9
9
  });
@@ -1,12 +1,20 @@
1
1
  import React, { useContext } from "react";
2
- import { Events } from "@carbon/react/icons";
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 GroupBanner = () => {
8
- const { activeGroupName, activeGroupUuid, patientUuids, activeSessionMeta } =
9
- useContext(GroupFormWorkflowContext);
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 GroupBanner;
71
+ export default GroupDisplayHeader;
@@ -0,0 +1,3 @@
1
+ import GroupDisplayHeader from "./GroupDisplayHeader";
2
+
3
+ export default GroupDisplayHeader;
@@ -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 CompactGroupResults = ({ groups, selectGroupAction }) => {
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}>&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
- const { t } = useTranslation();
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
- <div
52
- onClick={() => {
53
- dispatch({ type: "select", payload: index });
54
- selectGroupAction(group);
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}>&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 results = useGroupSearch(query);
23
- const error = false;
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 (query.length <= 2) return <SearchResultSkeleton />;
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.length === 0) {
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
- {results.length} {t("searchResultsText", "search result(s)")}
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
- .loadingIcon {
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(GroupFormWorkflowContext);
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
- <Link to="..">
33
- <Button kind="ghost">
34
- {t("cancel", "Cancel")} <Close size={20} />
35
- </Button>
36
- </Link>
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;
@@ -4,3 +4,4 @@ import useFormState from "./useFormState";
4
4
  import useGetEncounter from "./useGetEncounter";
5
5
 
6
6
  export { useGetAllForms, useGetPatient, useFormState, useGetEncounter };
7
+ export * from "./usePostEndpoint";
@@ -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 };
@@ -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,3 +0,0 @@
1
- import GroupBanner from "./GroupBanner";
2
-
3
- export default GroupBanner;
@@ -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;