@openmrs/esm-form-engine-lib 3.1.5-pre.2085 → 3.1.5-pre.2086
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/openmrs-esm-form-engine-lib.js +1 -1
- package/package.json +1 -1
- package/src/adapters/obs-adapter.test.ts +117 -0
- package/src/adapters/obs-adapter.ts +54 -27
- package/src/api/index.ts +17 -24
- package/src/components/inputs/file/file-thumbnail.component.tsx +55 -0
- package/src/components/inputs/file/file-thumbnail.scss +42 -0
- package/src/components/inputs/file/file.component.tsx +66 -130
- package/src/components/inputs/file/file.scss +8 -88
- package/src/components/inputs/workspace-launcher/workspace-launcher.component.tsx +4 -2
- package/src/processors/encounter/encounter-form-processor.ts +4 -3
- package/src/processors/encounter/encounter-processor-helper.ts +17 -15
- package/src/registry/registry.ts +2 -1
- package/src/types/domain.ts +9 -9
- package/src/types/index.ts +3 -3
- package/src/components/inputs/file/camera/camera.component.tsx +0 -34
- package/src/components/inputs/file/camera/camera.scss +0 -3
|
@@ -1 +1 @@
|
|
|
1
|
-
var _openmrs_esm_form_engine_lib;(()=>{"use strict";var e,r,t,n,o,i,a,l,s,u,f,d,p,c,h,m,v,g,b,y,w,_={78008:(e,r,t)=>{var n={"./start":()=>Promise.all([t.e(177),t.e(
|
|
1
|
+
var _openmrs_esm_form_engine_lib;(()=>{"use strict";var e,r,t,n,o,i,a,l,s,u,f,d,p,c,h,m,v,g,b,y,w,_={78008:(e,r,t)=>{var n={"./start":()=>Promise.all([t.e(177),t.e(899),t.e(759),t.e(72),t.e(456),t.e(306)]).then((()=>()=>t(4306)))},o=(e,r)=>(t.R=r,r=t.o(n,e)?n[e]():Promise.resolve().then((()=>{throw new Error('Module "'+e+'" does not exist in container.')})),t.R=void 0,r),i=(e,r)=>{if(t.S){var n="default",o=t.S[n];if(o&&o!==e)throw new Error("Container initialization failed as it has already been initialized with a different share scope");return t.S[n]=e,t.I(n,r)}};t.d(r,{get:()=>o,init:()=>i})}},P={};function j(e){var r=P[e];if(void 0!==r)return r.exports;var t=P[e]={id:e,loaded:!1,exports:{}};return _[e].call(t.exports,t,t.exports,j),t.loaded=!0,t.exports}j.m=_,j.c=P,j.n=e=>{var r=e&&e.__esModule?()=>e.default:()=>e;return j.d(r,{a:r}),r},r=Object.getPrototypeOf?e=>Object.getPrototypeOf(e):e=>e.__proto__,j.t=function(t,n){if(1&n&&(t=this(t)),8&n)return t;if("object"==typeof t&&t){if(4&n&&t.__esModule)return t;if(16&n&&"function"==typeof t.then)return t}var o=Object.create(null);j.r(o);var i={};e=e||[null,r({}),r([]),r(r)];for(var a=2&n&&t;"object"==typeof a&&!~e.indexOf(a);a=r(a))Object.getOwnPropertyNames(a).forEach((e=>i[e]=()=>t[e]));return i.default=()=>t,j.d(o,i),o},j.d=(e,r)=>{for(var t in r)j.o(r,t)&&!j.o(e,t)&&Object.defineProperty(e,t,{enumerable:!0,get:r[t]})},j.f={},j.e=e=>Promise.all(Object.keys(j.f).reduce(((r,t)=>(j.f[t](e,r),r)),[])),j.u=e=>e+".js",j.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(e){if("object"==typeof window)return window}}(),j.o=(e,r)=>Object.prototype.hasOwnProperty.call(e,r),t={},n="@openmrs/esm-form-engine-lib:",j.l=(e,r,o,i)=>{if(t[e])t[e].push(r);else{var a,l;if(void 0!==o)for(var s=document.getElementsByTagName("script"),u=0;u<s.length;u++){var f=s[u];if(f.getAttribute("src")==e||f.getAttribute("data-webpack")==n+o){a=f;break}}a||(l=!0,(a=document.createElement("script")).charset="utf-8",a.timeout=120,j.nc&&a.setAttribute("nonce",j.nc),a.setAttribute("data-webpack",n+o),a.src=e),t[e]=[r];var d=(r,n)=>{a.onerror=a.onload=null,clearTimeout(p);var o=t[e];if(delete t[e],a.parentNode&&a.parentNode.removeChild(a),o&&o.forEach((e=>e(n))),r)return r(n)},p=setTimeout(d.bind(null,void 0,{type:"timeout",target:a}),12e4);a.onerror=d.bind(null,a.onerror),a.onload=d.bind(null,a.onload),l&&document.head.appendChild(a)}},j.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},j.nmd=e=>(e.paths=[],e.children||(e.children=[]),e),j.j=719,(()=>{j.S={};var e={},r={};j.I=(t,n)=>{n||(n=[]);var o=r[t];if(o||(o=r[t]={}),!(n.indexOf(o)>=0)){if(n.push(o),e[t])return e[t];j.o(j.S,t)||(j.S[t]={});var i=j.S[t],a="@openmrs/esm-form-engine-lib",l=(e,r,t,n)=>{var o=i[e]=i[e]||{},l=o[r];(!l||!l.loaded&&(!n!=!l.eager?n:a>l.from))&&(o[r]={get:t,from:a,eager:!!n})},s=[];return"default"===t&&(l("@openmrs/esm-framework","7.0.1-pre.3291",(()=>Promise.all([j.e(177),j.e(387),j.e(899),j.e(72),j.e(456),j.e(766),j.e(310)]).then((()=>()=>j(96387))))),l("dayjs","1.11.13",(()=>j.e(353).then((()=>()=>j(74353))))),l("i18next","23.16.0",(()=>j.e(635).then((()=>()=>j(72635))))),l("react-i18next","11.18.6",(()=>Promise.all([j.e(72),j.e(414)]).then((()=>()=>j(93414))))),l("react","18.3.1",(()=>j.e(540).then((()=>()=>j(96540))))),l("swr/immutable","2.3.3",(()=>Promise.all([j.e(177),j.e(72),j.e(606)]).then((()=>()=>j(54225))))),l("swr/infinite","2.3.3",(()=>Promise.all([j.e(177),j.e(72),j.e(422)]).then((()=>()=>j(23041)))))),e[t]=s.length?Promise.all(s).then((()=>e[t]=1)):1}}})(),(()=>{var e;j.g.importScripts&&(e=j.g.location+"");var r=j.g.document;if(!e&&r&&(r.currentScript&&"SCRIPT"===r.currentScript.tagName.toUpperCase()&&(e=r.currentScript.src),!e)){var t=r.getElementsByTagName("script");if(t.length)for(var n=t.length-1;n>-1&&(!e||!/^http(s?):/.test(e));)e=t[n--].src}if(!e)throw new Error("Automatic publicPath is not supported in this browser");e=e.replace(/^blob:/,"").replace(/#.*$/,"").replace(/\?.*$/,"").replace(/\/[^\/]+$/,"/"),j.p=e})(),o=e=>{var r=e=>e.split(".").map((e=>+e==e?+e:e)),t=/^([^-+]+)?(?:-([^+]+))?(?:\+(.+))?$/.exec(e),n=t[1]?r(t[1]):[];return t[2]&&(n.length++,n.push.apply(n,r(t[2]))),t[3]&&(n.push([]),n.push.apply(n,r(t[3]))),n},i=(e,r)=>{e=o(e),r=o(r);for(var t=0;;){if(t>=e.length)return t<r.length&&"u"!=(typeof r[t])[0];var n=e[t],i=(typeof n)[0];if(t>=r.length)return"u"==i;var a=r[t],l=(typeof a)[0];if(i!=l)return"o"==i&&"n"==l||"s"==l||"u"==i;if("o"!=i&&"u"!=i&&n!=a)return n<a;t++}},a=e=>{var r=e[0],t="";if(1===e.length)return"*";if(r+.5){t+=0==r?">=":-1==r?"<":1==r?"^":2==r?"~":r>0?"=":"!=";for(var n=1,o=1;o<e.length;o++)n--,t+="u"==(typeof(l=e[o]))[0]?"-":(n>0?".":"")+(n=2,l);return t}var i=[];for(o=1;o<e.length;o++){var l=e[o];i.push(0===l?"not("+s()+")":1===l?"("+s()+" || "+s()+")":2===l?i.pop()+" "+i.pop():a(l))}return s();function s(){return i.pop().replace(/^\((.+)\)$/,"$1")}},l=(e,r)=>{if(0 in e){r=o(r);var t=e[0],n=t<0;n&&(t=-t-1);for(var i=0,a=1,s=!0;;a++,i++){var u,f,d=a<e.length?(typeof e[a])[0]:"";if(i>=r.length||"o"==(f=(typeof(u=r[i]))[0]))return!s||("u"==d?a>t&&!n:""==d!=n);if("u"==f){if(!s||"u"!=d)return!1}else if(s)if(d==f)if(a<=t){if(u!=e[a])return!1}else{if(n?u>e[a]:u<e[a])return!1;u!=e[a]&&(s=!1)}else if("s"!=d&&"n"!=d){if(n||a<=t)return!1;s=!1,a--}else{if(a<=t||f<d!=n)return!1;s=!1}else"s"!=d&&"n"!=d&&(s=!1,a--)}}var p=[],c=p.pop.bind(p);for(i=1;i<e.length;i++){var h=e[i];p.push(1==h?c()|c():2==h?c()&c():h?l(h,r):!c())}return!!c()},s=(e,r)=>e&&j.o(e,r),u=e=>(e.loaded=1,e.get()),f=e=>Object.keys(e).reduce(((r,t)=>(e[t].eager&&(r[t]=e[t]),r)),{}),d=(e,r,t)=>{var n=t?f(e[r]):e[r];return Object.keys(n).reduce(((e,r)=>!e||!n[e].loaded&&i(e,r)?r:e),0)},p=(e,r,t,n)=>"Unsatisfied version "+t+" from "+(t&&e[r][t].from)+" of shared singleton module "+r+" (required "+a(n)+")",c=e=>{throw new Error(e)},h=e=>{"undefined"!=typeof console&&console.warn&&console.warn(e)},m=(e,r,t)=>t?t():((e,r)=>c("Shared module "+r+" doesn't exist in shared scope "+e))(e,r),v=(e=>function(r,t,n,o,i){var a=j.I(r);return a&&a.then&&!n?a.then(e.bind(e,r,j.S[r],t,!1,o,i)):e(r,j.S[r],t,n,o,i)})(((e,r,t,n,o,i)=>{if(!s(r,t))return m(e,t,i);var a=d(r,t,n);return l(o,a)||h(p(r,t,a,o)),u(r[t][a])})),g={},b={16072:()=>v("default","react",!1,[1,18],(()=>j.e(540).then((()=>()=>j(96540))))),53941:()=>v("default","react-i18next",!1,[1,11],(()=>j.e(33).then((()=>()=>j(93414))))),66838:()=>v("default","@openmrs/esm-framework",!1,[1,7],(()=>Promise.all([j.e(177),j.e(387),j.e(766)]).then((()=>()=>j(96387))))),76766:()=>v("default","i18next",!1,[1,23],(()=>j.e(635).then((()=>()=>j(72635))))),44209:()=>v("default","swr/immutable",!1,[1,2],(()=>Promise.all([j.e(177),j.e(225)]).then((()=>()=>j(54225))))),56339:()=>v("default","swr/infinite",!1,[1,2],(()=>Promise.all([j.e(177),j.e(41)]).then((()=>()=>j(23041))))),70231:()=>v("default","dayjs",!1,[1,1],(()=>j.e(353).then((()=>()=>j(74353)))))},y={72:[16072],306:[44209,56339,70231],310:[44209,56339,70231],456:[53941,66838],766:[76766]},w={},j.f.consumes=(e,r)=>{j.o(y,e)&&y[e].forEach((e=>{if(j.o(g,e))return r.push(g[e]);if(!w[e]){var t=r=>{g[e]=0,j.m[e]=t=>{delete j.c[e],t.exports=r()}};w[e]=!0;var n=r=>{delete g[e],j.m[e]=t=>{throw delete j.c[e],r}};try{var o=b[e]();o.then?r.push(g[e]=o.then(t).catch(n)):t(o)}catch(e){n(e)}}}))},(()=>{var e={719:0};j.f.j=(r,t)=>{var n=j.o(e,r)?e[r]:void 0;if(0!==n)if(n)t.push(n[2]);else if(/^(456|72|766)$/.test(r))e[r]=0;else{var o=new Promise(((t,o)=>n=e[r]=[t,o]));t.push(n[2]=o);var i=j.p+j.u(r),a=new Error;j.l(i,(t=>{if(j.o(e,r)&&(0!==(n=e[r])&&(e[r]=void 0),n)){var o=t&&("load"===t.type?"missing":t.type),i=t&&t.target&&t.target.src;a.message="Loading chunk "+r+" failed.\n("+o+": "+i+")",a.name="ChunkLoadError",a.type=o,a.request=i,n[1](a)}}),"chunk-"+r,r)}};var r=(r,t)=>{var n,o,[i,a,l]=t,s=0;if(i.some((r=>0!==e[r]))){for(n in a)j.o(a,n)&&(j.m[n]=a[n]);l&&l(j)}for(r&&r(t);s<i.length;s++)o=i[s],j.o(e,o)&&e[o]&&e[o][0](),e[o]=0},t=globalThis.webpackChunk_openmrs_esm_form_engine_lib=globalThis.webpackChunk_openmrs_esm_form_engine_lib||[];t.forEach(r.bind(null,0)),t.push=r.bind(null,t.push.bind(t))})(),j.nc=void 0;var S=j(78008);_openmrs_esm_form_engine_lib=S})();
|
package/package.json
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { getAttachmentByUuid } from '@openmrs/esm-framework';
|
|
1
2
|
import { type FormContextProps } from '../provider/form-provider';
|
|
2
3
|
import { type FormField } from '../types';
|
|
3
4
|
import { findObsByFormField, hasPreviousObsValueChanged, ObsAdapter } from './obs-adapter';
|
|
@@ -40,6 +41,9 @@ const formContext = {
|
|
|
40
41
|
setDeletedFields: jest.fn(),
|
|
41
42
|
} as FormContextProps;
|
|
42
43
|
|
|
44
|
+
window.openmrsBase = 'openmrs';
|
|
45
|
+
const mockGetAttachmentByUuid = jest.mocked(getAttachmentByUuid);
|
|
46
|
+
|
|
43
47
|
describe('ObsAdapter - transformFieldValue', () => {
|
|
44
48
|
// new submission (enter mode)
|
|
45
49
|
it('should handle submission for text input', () => {
|
|
@@ -188,6 +192,42 @@ describe('ObsAdapter - transformFieldValue', () => {
|
|
|
188
192
|
});
|
|
189
193
|
});
|
|
190
194
|
|
|
195
|
+
it('should handle value transformation for file input', () => {
|
|
196
|
+
// setup
|
|
197
|
+
const field: FormField = {
|
|
198
|
+
label: "Upload an image or use this device's camera to capture an image",
|
|
199
|
+
type: 'obs',
|
|
200
|
+
questionOptions: {
|
|
201
|
+
rendering: 'file',
|
|
202
|
+
},
|
|
203
|
+
meta: {},
|
|
204
|
+
id: 'demoFile',
|
|
205
|
+
};
|
|
206
|
+
// value
|
|
207
|
+
const image = {
|
|
208
|
+
base64Content: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAoA',
|
|
209
|
+
capturedFromWebcam: true,
|
|
210
|
+
fileDescription: '',
|
|
211
|
+
fileName: 'Image taken from camera.png',
|
|
212
|
+
fileType: 'image',
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
// replay
|
|
216
|
+
const obs = ObsAdapter.transformFieldValue(field, [image], formContext);
|
|
217
|
+
// verify
|
|
218
|
+
expect(obs).toEqual([
|
|
219
|
+
{
|
|
220
|
+
base64Content: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAoA',
|
|
221
|
+
capturedFromWebcam: true,
|
|
222
|
+
fileDescription: '',
|
|
223
|
+
fileName: 'Image taken from camera.png',
|
|
224
|
+
fileType: 'image',
|
|
225
|
+
formFieldNamespace: 'rfe-forms',
|
|
226
|
+
formFieldPath: 'rfe-forms-demoFile',
|
|
227
|
+
},
|
|
228
|
+
]);
|
|
229
|
+
});
|
|
230
|
+
|
|
191
231
|
// editing existing values (edit mode)
|
|
192
232
|
it('should edit obs text/number value in edit mode', () => {
|
|
193
233
|
// setup
|
|
@@ -519,6 +559,39 @@ describe('ObsAdapter - transformFieldValue', () => {
|
|
|
519
559
|
});
|
|
520
560
|
expect(field.meta.submission.newValue).toBe(null);
|
|
521
561
|
});
|
|
562
|
+
|
|
563
|
+
it('should void deleted files', () => {
|
|
564
|
+
// setup
|
|
565
|
+
const field: FormField = {
|
|
566
|
+
label: "Upload an image or use this device's camera to capture an image",
|
|
567
|
+
type: 'obs',
|
|
568
|
+
questionOptions: {
|
|
569
|
+
rendering: 'file',
|
|
570
|
+
},
|
|
571
|
+
meta: {
|
|
572
|
+
initialValue: {
|
|
573
|
+
omrsObject: {
|
|
574
|
+
uuid: '6ff51289-b334-4050-8a0e-28cb2034bfc3',
|
|
575
|
+
formFieldPath: 'rfe-forms-demoFile',
|
|
576
|
+
},
|
|
577
|
+
},
|
|
578
|
+
},
|
|
579
|
+
id: 'demoFile',
|
|
580
|
+
};
|
|
581
|
+
// replay
|
|
582
|
+
ObsAdapter.transformFieldValue(
|
|
583
|
+
field,
|
|
584
|
+
[{ uuid: '6ff51289-b334-4050-8a0e-28cb2034bfc3', voided: true }],
|
|
585
|
+
formContext,
|
|
586
|
+
);
|
|
587
|
+
// verify
|
|
588
|
+
expect(field.meta.submission.voidedValue).toEqual([
|
|
589
|
+
{
|
|
590
|
+
uuid: '6ff51289-b334-4050-8a0e-28cb2034bfc3',
|
|
591
|
+
voided: true,
|
|
592
|
+
},
|
|
593
|
+
]);
|
|
594
|
+
});
|
|
522
595
|
});
|
|
523
596
|
|
|
524
597
|
describe('ObsAdapter - getInitialValue', () => {
|
|
@@ -663,6 +736,49 @@ describe('ObsAdapter - getInitialValue', () => {
|
|
|
663
736
|
expect(initialValue).toEqual('12f7be3d-fb5d-47dc-b5e3-56c501be80a6');
|
|
664
737
|
});
|
|
665
738
|
|
|
739
|
+
it('should get initial value for file rendering', async () => {
|
|
740
|
+
// setup
|
|
741
|
+
const field: FormField = {
|
|
742
|
+
label: "Upload an image or use this device's camera to capture an image",
|
|
743
|
+
type: 'obs',
|
|
744
|
+
questionOptions: {
|
|
745
|
+
rendering: 'file',
|
|
746
|
+
},
|
|
747
|
+
meta: {},
|
|
748
|
+
id: 'demoFile',
|
|
749
|
+
};
|
|
750
|
+
|
|
751
|
+
const obs = {
|
|
752
|
+
uuid: '6ff51289-b334-4050-8a0e-28cb2034bfc3',
|
|
753
|
+
formFieldPath: 'rfe-forms-demoFile',
|
|
754
|
+
};
|
|
755
|
+
formContext.domainObjectValue['obs'].push(obs);
|
|
756
|
+
mockGetAttachmentByUuid.mockReturnValue(
|
|
757
|
+
Promise.resolve({
|
|
758
|
+
data: {
|
|
759
|
+
uuid: '6ff51289-b334-4050-8a0e-28cb2034bfc3',
|
|
760
|
+
dateTime: '2025-08-21T19:31:39.000+0000',
|
|
761
|
+
filename: 'image.png',
|
|
762
|
+
comment: 'An image captured for test purposes',
|
|
763
|
+
bytesMimeType: 'image/png',
|
|
764
|
+
bytesContentFamily: 'IMAGE',
|
|
765
|
+
},
|
|
766
|
+
} as any),
|
|
767
|
+
);
|
|
768
|
+
|
|
769
|
+
// replay
|
|
770
|
+
const initialValue = await ObsAdapter.getInitialValue(field, formContext.domainObjectValue, formContext);
|
|
771
|
+
expect(initialValue).toEqual([
|
|
772
|
+
{
|
|
773
|
+
uuid: '6ff51289-b334-4050-8a0e-28cb2034bfc3',
|
|
774
|
+
base64Content: 'openmrs/ws/rest/v1/attachment/6ff51289-b334-4050-8a0e-28cb2034bfc3/bytes',
|
|
775
|
+
fileName: 'image.png',
|
|
776
|
+
fileDescription: 'An image captured for test purposes',
|
|
777
|
+
fileType: 'image',
|
|
778
|
+
},
|
|
779
|
+
]);
|
|
780
|
+
});
|
|
781
|
+
|
|
666
782
|
it('should get initial values for obs-group members', async () => {
|
|
667
783
|
// setup
|
|
668
784
|
const basePath = 'rfe-forms-';
|
|
@@ -952,6 +1068,7 @@ describe('findObsByFormField', () => {
|
|
|
952
1068
|
it('Should find observation by field path', () => {
|
|
953
1069
|
// do find
|
|
954
1070
|
let matchedObs = findObsByFormField(obsList, [], fields[0]);
|
|
1071
|
+
|
|
955
1072
|
// verify
|
|
956
1073
|
expect(matchedObs.length).toBe(1);
|
|
957
1074
|
expect(matchedObs[0]).toBe(obsList[0]);
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import dayjs from 'dayjs';
|
|
2
2
|
import { ConceptTrue, codedTypes } from '../constants';
|
|
3
3
|
import {
|
|
4
|
+
type Attachment,
|
|
4
5
|
type OpenmrsObs,
|
|
5
6
|
type FormField,
|
|
6
7
|
type OpenmrsEncounter,
|
|
7
|
-
type AttachmentResponse,
|
|
8
|
-
type Attachment,
|
|
9
8
|
type ValueAndDisplay,
|
|
10
9
|
type FormFieldValueAdapter,
|
|
11
10
|
} from '../types';
|
|
@@ -19,8 +18,7 @@ import {
|
|
|
19
18
|
} from '../utils/common-utils';
|
|
20
19
|
import { type FormContextProps } from '../provider/form-provider';
|
|
21
20
|
import { isEmpty } from '../validators/form-validator';
|
|
22
|
-
import { getAttachmentByUuid } from '
|
|
23
|
-
import { formatDate, type OpenmrsResource, restBaseUrl } from '@openmrs/esm-framework';
|
|
21
|
+
import { attachmentUrl, getAttachmentByUuid, type OpenmrsResource } from '@openmrs/esm-framework';
|
|
24
22
|
|
|
25
23
|
// Temporarily holds observations that have already been bound with matching fields
|
|
26
24
|
export let assignedObsIds: string[] = [];
|
|
@@ -28,15 +26,11 @@ export let assignedObsIds: string[] = [];
|
|
|
28
26
|
export const ObsAdapter: FormFieldValueAdapter = {
|
|
29
27
|
async getInitialValue(field: FormField, sourceObject: any, context: FormContextProps) {
|
|
30
28
|
const encounter = sourceObject ?? (context.domainObjectValue as OpenmrsEncounter);
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
// TODO: This seems like a violation of the data model.
|
|
35
|
-
// I think we should instead use something like `formFieldPath` to do the mapping.
|
|
36
|
-
const rawAttachment = attachmentsResponse.results?.find((attachment) => attachment.comment === field.id);
|
|
37
|
-
return rawAttachment ? generateAttachment(rawAttachment) : null;
|
|
29
|
+
const matchingObs = findObsByFormField(flattenObsList(encounter.obs), assignedObsIds, field);
|
|
30
|
+
if (hasRendering(field, 'file') && matchingObs?.length) {
|
|
31
|
+
return resolveAttachmentsFromObs(field, matchingObs);
|
|
38
32
|
}
|
|
39
|
-
return extractFieldValue(field,
|
|
33
|
+
return extractFieldValue(field, matchingObs, true);
|
|
40
34
|
},
|
|
41
35
|
async getPreviousValue(field: FormField, sourceObject: any, context: FormContextProps): Promise<ValueAndDisplay> {
|
|
42
36
|
const encounter = sourceObject ?? (context.previousDomainObjectValue as OpenmrsEncounter);
|
|
@@ -85,6 +79,9 @@ export const ObsAdapter: FormFieldValueAdapter = {
|
|
|
85
79
|
if (hasRendering(field, 'checkbox')) {
|
|
86
80
|
return handleMultiSelect(field, Array.isArray(value) ? value : [value]);
|
|
87
81
|
}
|
|
82
|
+
if (hasRendering(field, 'file')) {
|
|
83
|
+
return handleAttachments(field, value);
|
|
84
|
+
}
|
|
88
85
|
if (!isEmpty(value) && hasPreviousObsValueChanged(field, value)) {
|
|
89
86
|
return gracefullySetSubmission(field, editObs(field, value), undefined);
|
|
90
87
|
}
|
|
@@ -251,6 +248,20 @@ function handleMultiSelect(field: FormField, values: Array<string> = []) {
|
|
|
251
248
|
);
|
|
252
249
|
}
|
|
253
250
|
|
|
251
|
+
function handleAttachments(field: FormField, attachments: Attachment[] = []) {
|
|
252
|
+
const voided = attachments
|
|
253
|
+
.filter((attachment) => attachment.uuid && attachment.voided)
|
|
254
|
+
.map((voided) => ({ uuid: voided.uuid, voided: true }));
|
|
255
|
+
const newAttachments = (field.meta.submission.newValue = attachments
|
|
256
|
+
.filter((attachment) => !attachment.uuid)
|
|
257
|
+
.map((newAttachment) => ({
|
|
258
|
+
formFieldNamespace: 'rfe-forms',
|
|
259
|
+
formFieldPath: `rfe-forms-${field.id}`,
|
|
260
|
+
...newAttachment,
|
|
261
|
+
})));
|
|
262
|
+
return gracefullySetSubmission(field, newAttachments, voided);
|
|
263
|
+
}
|
|
264
|
+
|
|
254
265
|
/**
|
|
255
266
|
* Retrieves a list of observations from a given `obsList` that correspond to the specified field.
|
|
256
267
|
*
|
|
@@ -263,9 +274,15 @@ export function findObsByFormField(
|
|
|
263
274
|
claimedObsIds: string[],
|
|
264
275
|
field: FormField,
|
|
265
276
|
): OpenmrsObs[] {
|
|
266
|
-
const obs = obsList.filter(
|
|
267
|
-
|
|
268
|
-
|
|
277
|
+
const obs = obsList.filter((candidate) => {
|
|
278
|
+
// we ignore the concept for attachments because they're managed from the backend
|
|
279
|
+
if (hasRendering(field, 'file') && candidate.formFieldPath == `rfe-forms-${field.id}`) {
|
|
280
|
+
return true;
|
|
281
|
+
}
|
|
282
|
+
return (
|
|
283
|
+
candidate.formFieldPath == `rfe-forms-${field.id}` && candidate.concept.uuid == field.questionOptions.concept
|
|
284
|
+
);
|
|
285
|
+
});
|
|
269
286
|
|
|
270
287
|
// We shall fall back to mapping by the associated concept
|
|
271
288
|
// That being said, we shall find all matching obs and pick the one that wasn't previously claimed.
|
|
@@ -277,17 +294,27 @@ export function findObsByFormField(
|
|
|
277
294
|
return obs;
|
|
278
295
|
}
|
|
279
296
|
|
|
280
|
-
function
|
|
281
|
-
const
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
297
|
+
async function resolveAttachmentsFromObs(field: FormField, obs: OpenmrsObs[]) {
|
|
298
|
+
const abortController = new AbortController();
|
|
299
|
+
const attachments = await Promise.all(
|
|
300
|
+
obs.map((obs) =>
|
|
301
|
+
getAttachmentByUuid(obs.uuid, abortController)
|
|
302
|
+
.then((response) => response.data)
|
|
303
|
+
.catch((error) => {
|
|
304
|
+
console.error(`Failed to fetch attachment ${obs.uuid}:`, error);
|
|
305
|
+
return null;
|
|
306
|
+
}),
|
|
307
|
+
),
|
|
308
|
+
);
|
|
309
|
+
|
|
310
|
+
field.meta.initialValue = {
|
|
311
|
+
omrsObject: obs,
|
|
292
312
|
};
|
|
313
|
+
return attachments.filter(Boolean).map((attachment) => ({
|
|
314
|
+
uuid: attachment.uuid,
|
|
315
|
+
base64Content: `${window.openmrsBase}${attachmentUrl}/${attachment.uuid}/bytes`,
|
|
316
|
+
fileName: attachment.filename,
|
|
317
|
+
fileDescription: attachment.comment,
|
|
318
|
+
fileType: attachment.bytesContentFamily?.toLowerCase(),
|
|
319
|
+
})) as Attachment[];
|
|
293
320
|
}
|
package/src/api/index.ts
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
|
-
import { fhirBaseUrl, openmrsFetch, restBaseUrl } from '@openmrs/esm-framework';
|
|
1
|
+
import { attachmentUrl, fhirBaseUrl, openmrsFetch, restBaseUrl, type UploadedFile } from '@openmrs/esm-framework';
|
|
2
2
|
import { encounterRepresentation } from '../constants';
|
|
3
|
-
import type {
|
|
3
|
+
import type {
|
|
4
|
+
AttachmentFieldValue,
|
|
5
|
+
FHIRObsResource,
|
|
6
|
+
OpenmrsForm,
|
|
7
|
+
PatientIdentifier,
|
|
8
|
+
PatientProgramPayload,
|
|
9
|
+
} from '../types';
|
|
4
10
|
import { isUuid } from '../utils/boolean-utils';
|
|
5
11
|
|
|
6
12
|
export function saveEncounter(abortController: AbortController, payload, encounterUuid?: string) {
|
|
@@ -18,41 +24,28 @@ export function saveEncounter(abortController: AbortController, payload, encount
|
|
|
18
24
|
});
|
|
19
25
|
}
|
|
20
26
|
|
|
21
|
-
export function
|
|
22
|
-
const url = `${restBaseUrl}/attachment`;
|
|
23
|
-
|
|
24
|
-
const content = field.meta.submission?.newValue?.value;
|
|
25
|
-
const cameraUploadType = typeof content === 'string' && content?.split(';')[0].split(':')[1].split('/')[1];
|
|
26
|
-
|
|
27
|
+
export async function createAttachment(patientUuid: string, encounterUUID: string, attachment: AttachmentFieldValue) {
|
|
27
28
|
const formData = new FormData();
|
|
28
|
-
const fileCaption = field.id;
|
|
29
29
|
|
|
30
|
-
formData.append('fileCaption',
|
|
30
|
+
formData.append('fileCaption', attachment.fileDescription);
|
|
31
31
|
formData.append('patient', patientUuid);
|
|
32
32
|
|
|
33
|
-
if (
|
|
34
|
-
formData.append('file',
|
|
33
|
+
if (attachment.file) {
|
|
34
|
+
formData.append('file', attachment.file, attachment.fileName);
|
|
35
35
|
} else {
|
|
36
|
-
formData.append('file', new File([''],
|
|
37
|
-
formData.append('base64Content',
|
|
36
|
+
formData.append('file', new File([''], attachment.fileName), attachment.fileName);
|
|
37
|
+
formData.append('base64Content', attachment.base64Content);
|
|
38
38
|
}
|
|
39
39
|
formData.append('encounter', encounterUUID);
|
|
40
|
-
formData.append('
|
|
40
|
+
formData.append('formFieldNamespace', attachment.formFieldNamespace);
|
|
41
|
+
formData.append('formFieldPath', attachment.formFieldPath);
|
|
41
42
|
|
|
42
|
-
return openmrsFetch(
|
|
43
|
+
return openmrsFetch(`${attachmentUrl}`, {
|
|
43
44
|
method: 'POST',
|
|
44
|
-
signal: abortController.signal,
|
|
45
45
|
body: formData,
|
|
46
46
|
});
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
export function getAttachmentByUuid(patientUuid: string, encounterUuid: string, abortController: AbortController) {
|
|
50
|
-
const attachmentUrl = `${restBaseUrl}/attachment`;
|
|
51
|
-
return openmrsFetch(`${attachmentUrl}?patient=${patientUuid}&encounter=${encounterUuid}`, {
|
|
52
|
-
signal: abortController.signal,
|
|
53
|
-
}).then((response) => response.data);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
49
|
export function getConcept(conceptUuid: string, v: string) {
|
|
57
50
|
return openmrsFetch(`${restBaseUrl}/concept/${conceptUuid}?v=${v}`).then(({ data }) => data.results);
|
|
58
51
|
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import React, { useMemo } from 'react';
|
|
2
|
+
import styles from './file-thumbnail.scss';
|
|
3
|
+
import { CloseFilled, DocumentPdf, DocumentUnknown } from '@carbon/react/icons';
|
|
4
|
+
import { Button } from '@carbon/react';
|
|
5
|
+
|
|
6
|
+
interface FileThumbnailProps {
|
|
7
|
+
src: string;
|
|
8
|
+
title: string;
|
|
9
|
+
bytesContentFamily: string;
|
|
10
|
+
removeFileCb: () => void;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
type ThumbnailProps = Omit<FileThumbnailProps, 'bytesContentFamily' | 'removeFileCb'>;
|
|
14
|
+
|
|
15
|
+
export function FileThumbnail({ bytesContentFamily, removeFileCb, ...thumbnailProps }: FileThumbnailProps) {
|
|
16
|
+
const Thumbnail = useMemo(() => {
|
|
17
|
+
switch (bytesContentFamily) {
|
|
18
|
+
case 'image':
|
|
19
|
+
return ImageThumbnail;
|
|
20
|
+
case 'pdf':
|
|
21
|
+
return PDFThumbnail;
|
|
22
|
+
default:
|
|
23
|
+
return OtherThumbnail;
|
|
24
|
+
}
|
|
25
|
+
}, []);
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<div className={styles.thumbnail}>
|
|
29
|
+
<Thumbnail {...thumbnailProps} />
|
|
30
|
+
<Button kind="ghost" className={styles.removeButton} onClick={removeFileCb}>
|
|
31
|
+
<CloseFilled size={16} className={styles.closeIcon} />
|
|
32
|
+
</Button>
|
|
33
|
+
</div>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function ImageThumbnail(props: ThumbnailProps) {
|
|
38
|
+
return <img className={styles.imageThumbnail} src={props.src} alt={props.title} />;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function PDFThumbnail(props: ThumbnailProps) {
|
|
42
|
+
return (
|
|
43
|
+
<div className={styles.pdfThumbnail} role="button" tabIndex={0}>
|
|
44
|
+
<DocumentPdf size={24} />
|
|
45
|
+
</div>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function OtherThumbnail(props: ThumbnailProps) {
|
|
50
|
+
return (
|
|
51
|
+
<div className={styles.pdfThumbnail} role="button" tabIndex={0}>
|
|
52
|
+
<DocumentUnknown size={24} />
|
|
53
|
+
</div>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
@use '@carbon/layout';
|
|
2
|
+
@use '@openmrs/esm-styleguide/src/vars' as vars;
|
|
3
|
+
|
|
4
|
+
.thumbnail {
|
|
5
|
+
height: 7rem;
|
|
6
|
+
width: 7rem;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
.imageThumbnail {
|
|
10
|
+
max-width: 100%;
|
|
11
|
+
max-height: 100%;
|
|
12
|
+
object-fit: cover;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
.pdfThumbnail {
|
|
16
|
+
background-color: vars.$color-gray-30;
|
|
17
|
+
display: flex;
|
|
18
|
+
justify-content: center;
|
|
19
|
+
align-items: center;
|
|
20
|
+
width: 100%;
|
|
21
|
+
height: 100%;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.removeButton {
|
|
25
|
+
position: absolute;
|
|
26
|
+
top: layout.$spacing-02;
|
|
27
|
+
right: layout.$spacing-02;
|
|
28
|
+
background-color: transparent;
|
|
29
|
+
border: none;
|
|
30
|
+
padding: 0;
|
|
31
|
+
cursor: pointer;
|
|
32
|
+
padding-block-start: 0;
|
|
33
|
+
--cds-layout-size-height-local: 0;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.removeButton:hover {
|
|
37
|
+
background-color: transparent;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.closeIcon {
|
|
41
|
+
fill: white !important;
|
|
42
|
+
}
|
|
@@ -1,56 +1,57 @@
|
|
|
1
|
-
import React, {
|
|
2
|
-
import {
|
|
1
|
+
import React, { useCallback } from 'react';
|
|
2
|
+
import { Button } from '@carbon/react';
|
|
3
3
|
import { useTranslation } from 'react-i18next';
|
|
4
|
-
import {
|
|
5
|
-
import Camera from './camera/camera.component';
|
|
6
|
-
import { Close, DocumentPdf } from '@carbon/react/icons';
|
|
4
|
+
import { Add } from '@carbon/react/icons';
|
|
7
5
|
import styles from './file.scss';
|
|
8
|
-
import { type FormFieldInputProps } from '../../../types';
|
|
6
|
+
import { type Attachment, type FormFieldInputProps } from '../../../types';
|
|
9
7
|
import { useFormProviderContext } from '../../../provider/form-provider';
|
|
10
8
|
import { isViewMode } from '../../../utils/common-utils';
|
|
11
9
|
import FieldValueView from '../../value/view/field-value-view.component';
|
|
12
10
|
import FieldLabel from '../../field-label/field-label.component';
|
|
11
|
+
import { showModal, type UploadedFile, useLayoutType } from '@openmrs/esm-framework';
|
|
12
|
+
import { FileThumbnail } from './file-thumbnail.component';
|
|
13
|
+
import classNames from 'classnames';
|
|
13
14
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
const File: React.FC<FormFieldInputProps> = ({ field, value, setFieldValue }) => {
|
|
15
|
+
const File: React.FC<FormFieldInputProps<Array<Attachment>>> = ({ field, value, setFieldValue }) => {
|
|
17
16
|
const { t } = useTranslation();
|
|
18
|
-
const [cameraWidgetVisible, setCameraWidgetVisible] = useState(false);
|
|
19
|
-
const [imagePreview, setImagePreview] = useState(null);
|
|
20
|
-
const [dataSource, setDataSource] = useState<DataSourceType>(null);
|
|
21
17
|
const { sessionMode } = useFormProviderContext();
|
|
18
|
+
const isTablet = useLayoutType() === 'tablet';
|
|
22
19
|
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
:
|
|
35
|
-
|
|
20
|
+
const showImageCaptureModal = useCallback(() => {
|
|
21
|
+
const close = showModal('capture-photo-modal', {
|
|
22
|
+
saveFile: (file: UploadedFile) => {
|
|
23
|
+
if (file.capturedFromWebcam && !file.fileName.includes('.')) {
|
|
24
|
+
file.fileName = `${file.fileName}.png`;
|
|
25
|
+
}
|
|
26
|
+
const currentFiles = value ? value : [];
|
|
27
|
+
setFieldValue([...currentFiles, file]);
|
|
28
|
+
close();
|
|
29
|
+
return Promise.resolve();
|
|
30
|
+
},
|
|
31
|
+
closeModal: () => {
|
|
32
|
+
close();
|
|
33
|
+
},
|
|
34
|
+
allowedExtensions: field.questionOptions.allowedFileTypes,
|
|
35
|
+
multipleFiles: field.questionOptions.allowMultiple,
|
|
36
|
+
collectDescription: true,
|
|
37
|
+
});
|
|
38
|
+
}, [value, field]);
|
|
36
39
|
|
|
37
|
-
const
|
|
38
|
-
(
|
|
39
|
-
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
40
|
+
const handleRemoveFile = useCallback(
|
|
41
|
+
(index: number) => {
|
|
42
|
+
const buffer = [...value];
|
|
43
|
+
const attachment = buffer[index];
|
|
44
|
+
if (attachment.uuid) {
|
|
45
|
+
buffer[index] = {
|
|
46
|
+
...attachment,
|
|
47
|
+
voided: true,
|
|
48
|
+
};
|
|
49
|
+
} else {
|
|
50
|
+
buffer.splice(index, 1);
|
|
51
|
+
}
|
|
52
|
+
setFieldValue(buffer);
|
|
43
53
|
},
|
|
44
|
-
[
|
|
45
|
-
);
|
|
46
|
-
|
|
47
|
-
const handleCameraImageChange = useCallback(
|
|
48
|
-
(newImage) => {
|
|
49
|
-
setImagePreview(newImage);
|
|
50
|
-
setCameraWidgetVisible(false);
|
|
51
|
-
setFieldValue(newImage);
|
|
52
|
-
},
|
|
53
|
-
[setFieldValue],
|
|
54
|
+
[value],
|
|
54
55
|
);
|
|
55
56
|
|
|
56
57
|
if (isViewMode(sessionMode) && !value) {
|
|
@@ -59,102 +60,37 @@ const File: React.FC<FormFieldInputProps> = ({ field, value, setFieldValue }) =>
|
|
|
59
60
|
);
|
|
60
61
|
}
|
|
61
62
|
|
|
62
|
-
return
|
|
63
|
+
return (
|
|
63
64
|
<div>
|
|
64
|
-
<div className={styles.label
|
|
65
|
-
<div className={styles.editModeImage}>
|
|
66
|
-
<div className={styles.imageContent}>
|
|
67
|
-
{value.bytesContentFamily === 'PDF' ? (
|
|
68
|
-
<div className={styles.pdfThumbnail} role="button" tabIndex={0}>
|
|
69
|
-
<DocumentPdf size={24} />
|
|
70
|
-
</div>
|
|
71
|
-
) : (
|
|
72
|
-
<img src={value.src} alt={t('preview', 'Preview')} width="200px" />
|
|
73
|
-
)}
|
|
74
|
-
</div>
|
|
75
|
-
</div>
|
|
76
|
-
</div>
|
|
77
|
-
) : (
|
|
78
|
-
<div>
|
|
79
|
-
<div className={styles.label}>
|
|
65
|
+
<div className={classNames(styles.label, 'cds--label')}>
|
|
80
66
|
<FieldLabel field={field} />
|
|
81
67
|
</div>
|
|
82
|
-
|
|
83
|
-
<div
|
|
84
|
-
<Button
|
|
85
|
-
{
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
{t('cameraCapture', 'Camera capture')}
|
|
68
|
+
{!isViewMode(sessionMode) && (
|
|
69
|
+
<div>
|
|
70
|
+
<Button
|
|
71
|
+
className={styles.uploadButton}
|
|
72
|
+
kind={isTablet ? 'ghost' : 'tertiary'}
|
|
73
|
+
onClick={showImageCaptureModal}
|
|
74
|
+
renderIcon={(props) => <Add size={16} {...props} />}>
|
|
75
|
+
{field.questionOptions.buttonLabel ? t(field.questionOptions.buttonLabel) : t('addFile', 'Add file')}
|
|
91
76
|
</Button>
|
|
92
77
|
</div>
|
|
93
|
-
</div>
|
|
94
|
-
{!dataSource && value && (
|
|
95
|
-
<div className={styles.editModeImage}>
|
|
96
|
-
<div className={styles.imageContent}>
|
|
97
|
-
{value.bytesContentFamily === 'PDF' ? (
|
|
98
|
-
<div className={styles.pdfThumbnail} role="button" tabIndex={0}>
|
|
99
|
-
<DocumentPdf size={24} />
|
|
100
|
-
</div>
|
|
101
|
-
) : (
|
|
102
|
-
<img src={value.src} alt="Preview" width="200px" />
|
|
103
|
-
)}
|
|
104
|
-
</div>
|
|
105
|
-
</div>
|
|
106
78
|
)}
|
|
107
|
-
{
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
onChange={handleFilePickerChange}
|
|
120
|
-
/>
|
|
121
|
-
</div>
|
|
122
|
-
)}
|
|
123
|
-
{dataSource === 'camera' && (
|
|
124
|
-
<div className={styles.cameraUploader}>
|
|
125
|
-
<div className={styles.camButton}>
|
|
126
|
-
<p className={styles.titleStyles}>Camera</p>
|
|
127
|
-
<p className={styles.descriptionStyles}>Capture image via camera</p>
|
|
128
|
-
<Button onClick={() => setCameraWidgetVisible((prevState) => !prevState)} size="md">
|
|
129
|
-
{cameraWidgetVisible ? t('closeCamera', 'Close camera') : t('addCameraImage', 'Add camera image')}
|
|
130
|
-
</Button>
|
|
131
|
-
</div>
|
|
132
|
-
{cameraWidgetVisible && (
|
|
133
|
-
<div className={styles.cameraPreview}>
|
|
134
|
-
<Camera handleImages={handleCameraImageChange} />
|
|
135
|
-
</div>
|
|
136
|
-
)}
|
|
137
|
-
{imagePreview && (
|
|
138
|
-
<div className={styles.capturedImage}>
|
|
139
|
-
<div className={styles.imageContent}>
|
|
140
|
-
<img src={imagePreview} alt={t('preview', 'Preview')} width="200px" />
|
|
141
|
-
<div className={styles.caption}>
|
|
142
|
-
<p>{t('uploadedPhoto', 'Uploaded photo')}</p>
|
|
143
|
-
<div
|
|
144
|
-
tabIndex={0}
|
|
145
|
-
role="button"
|
|
146
|
-
onClick={() => {
|
|
147
|
-
setImagePreview(null);
|
|
148
|
-
}}
|
|
149
|
-
className={styles.closeIcon}>
|
|
150
|
-
<Close />
|
|
151
|
-
</div>
|
|
152
|
-
</div>
|
|
79
|
+
<div className={styles.thumbnailGrid}>
|
|
80
|
+
{value &&
|
|
81
|
+
value
|
|
82
|
+
.filter((file) => !file.voided)
|
|
83
|
+
.map((file, index) => (
|
|
84
|
+
<div className={styles.thumbnailContainer}>
|
|
85
|
+
<FileThumbnail
|
|
86
|
+
title={file.fileName}
|
|
87
|
+
src={file.base64Content}
|
|
88
|
+
bytesContentFamily={file.fileType}
|
|
89
|
+
removeFileCb={() => handleRemoveFile(index)}
|
|
90
|
+
/>
|
|
153
91
|
</div>
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
</div>
|
|
157
|
-
)}
|
|
92
|
+
))}
|
|
93
|
+
</div>
|
|
158
94
|
</div>
|
|
159
95
|
);
|
|
160
96
|
};
|
|
@@ -1,101 +1,21 @@
|
|
|
1
1
|
@use '@carbon/layout';
|
|
2
2
|
|
|
3
3
|
.label {
|
|
4
|
-
font-family: IBM Plex Sans;
|
|
5
|
-
font-size: 14px;
|
|
6
|
-
font-style: normal;
|
|
7
4
|
font-weight: 600;
|
|
8
|
-
line-height: 30px;
|
|
9
|
-
letter-spacing: 0.16px;
|
|
5
|
+
line-height: 30px;
|
|
10
6
|
color: #000000;
|
|
11
7
|
}
|
|
12
8
|
|
|
13
|
-
.
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
.capturedImage {
|
|
18
|
-
width: 100%;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
.caption {
|
|
22
|
-
display: flex;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
.closeIcon {
|
|
26
|
-
margin: 0 1rem;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
.uploadSelector {
|
|
30
|
-
display: flex;
|
|
31
|
-
align-items: center;
|
|
32
|
-
margin-bottom: 1rem;
|
|
33
|
-
flex-wrap: wrap;
|
|
9
|
+
.thumbnailGrid {
|
|
10
|
+
display: grid;
|
|
11
|
+
grid-template-columns: repeat(auto-fill, 7rem);
|
|
34
12
|
gap: layout.$spacing-05;
|
|
13
|
+
margin-top: layout.$spacing-05;
|
|
35
14
|
}
|
|
36
15
|
|
|
37
|
-
.
|
|
38
|
-
display: flex;
|
|
39
|
-
margin-top: 5px;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
.imageDescription {
|
|
43
|
-
margin-top: 5px;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
.fileUploader {
|
|
47
|
-
background-color: rgb(255, 255, 255);
|
|
48
|
-
padding: 1rem;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
.cameraUploader {
|
|
52
|
-
margin: 1rem 0;
|
|
53
|
-
background-color: white;
|
|
54
|
-
padding: 1rem;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
.camButton {
|
|
58
|
-
margin-bottom: 1rem;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
.cameraPreview {
|
|
62
|
-
margin-top: 1rem;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
.imageContent {
|
|
66
|
-
padding: 0;
|
|
67
|
-
margin: 0;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
.titleStyles {
|
|
71
|
-
color: #161616;
|
|
72
|
-
font-size: 0.875rem;
|
|
73
|
-
font-weight: 600;
|
|
74
|
-
letter-spacing: 0.16px;
|
|
75
|
-
line-height: 1.2857;
|
|
76
|
-
margin-bottom: 0.5rem;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
.descriptionStyles {
|
|
80
|
-
color: #525252;
|
|
81
|
-
font-size: 0.875rem;
|
|
82
|
-
font-weight: 400;
|
|
83
|
-
letter-spacing: 0.16px;
|
|
84
|
-
line-height: 1.28572;
|
|
85
|
-
margin-bottom: 1rem;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
.editModeImage {
|
|
89
|
-
background-color: white;
|
|
90
|
-
padding: 1rem;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
.pdfThumbnail {
|
|
94
|
-
cursor: pointer;
|
|
95
|
-
background-color: gray;
|
|
16
|
+
.thumbnailContainer {
|
|
96
17
|
display: flex;
|
|
97
|
-
|
|
18
|
+
flex-direction: column;
|
|
98
19
|
align-items: center;
|
|
99
|
-
|
|
100
|
-
height: 5rem;
|
|
20
|
+
position: relative;
|
|
101
21
|
}
|
|
@@ -15,7 +15,7 @@ const WorkspaceLauncher: React.FC<FormFieldInputProps> = ({ field }) => {
|
|
|
15
15
|
|
|
16
16
|
const handleLaunchWorkspace = () => {
|
|
17
17
|
const workspaceName = field.questionOptions?.workspaceName;
|
|
18
|
-
// TODO: properly check if workspace name is valid
|
|
18
|
+
// TODO: properly check if workspace name is valid
|
|
19
19
|
// https://openmrs.atlassian.net/browse/O3-4976
|
|
20
20
|
const isWorkspaceNameValid = true;
|
|
21
21
|
if (!isWorkspaceNameValid) {
|
|
@@ -38,7 +38,9 @@ const WorkspaceLauncher: React.FC<FormFieldInputProps> = ({ field }) => {
|
|
|
38
38
|
<div className={styles.label}>{t(field.label)}</div>
|
|
39
39
|
<div className={styles.workspaceButton}>
|
|
40
40
|
<Button disabled={isTrue(field.readonly)} onClick={handleLaunchWorkspace}>
|
|
41
|
-
{
|
|
41
|
+
{field.questionOptions.buttonLabel
|
|
42
|
+
? t(field.questionOptions.buttonLabel)
|
|
43
|
+
: t('launchWorkspace', 'Launch Workspace')}
|
|
42
44
|
</Button>
|
|
43
45
|
</div>
|
|
44
46
|
</div>
|
|
@@ -185,9 +185,8 @@ export class EncounterFormProcessor extends FormProcessor {
|
|
|
185
185
|
}
|
|
186
186
|
// handle attachments
|
|
187
187
|
try {
|
|
188
|
-
const attachmentsResponse = await
|
|
189
|
-
|
|
190
|
-
);
|
|
188
|
+
const attachmentsResponse = await saveAttachments(context.formFields, savedEncounter, abortController);
|
|
189
|
+
|
|
191
190
|
if (attachmentsResponse?.length) {
|
|
192
191
|
showSnackbar({
|
|
193
192
|
title: t('attachmentsSaved', 'Attachment(s) saved successfully'),
|
|
@@ -196,6 +195,7 @@ export class EncounterFormProcessor extends FormProcessor {
|
|
|
196
195
|
});
|
|
197
196
|
}
|
|
198
197
|
} catch (error) {
|
|
198
|
+
console.error('Error saving attachments', error);
|
|
199
199
|
const errorMessages = extractErrorMessagesFromResponse(error);
|
|
200
200
|
return Promise.reject({
|
|
201
201
|
title: t('errorSavingAttachments', 'Error saving attachment(s)'),
|
|
@@ -206,6 +206,7 @@ export class EncounterFormProcessor extends FormProcessor {
|
|
|
206
206
|
}
|
|
207
207
|
return savedEncounter;
|
|
208
208
|
} catch (error) {
|
|
209
|
+
console.error('Error saving encounter', error);
|
|
209
210
|
const errorMessages = extractErrorMessagesFromResponse(error);
|
|
210
211
|
return Promise.reject({
|
|
211
212
|
title: t('errorSavingEncounter', 'Error saving encounter'),
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
|
+
type AttachmentFieldValue,
|
|
2
3
|
type FormField,
|
|
3
4
|
type FormProcessorContextProps,
|
|
4
5
|
type OpenmrsEncounter,
|
|
@@ -7,7 +8,7 @@ import {
|
|
|
7
8
|
type PatientProgram,
|
|
8
9
|
type PatientProgramPayload,
|
|
9
10
|
} from '../../types';
|
|
10
|
-
import {
|
|
11
|
+
import { createAttachment, savePatientIdentifier, saveProgramEnrollment } from '../../api';
|
|
11
12
|
import { hasRendering, hasSubmission } from '../../utils/common-utils';
|
|
12
13
|
import dayjs from 'dayjs';
|
|
13
14
|
import { assignedObsIds, constructObs, voidObs } from '../../adapters/obs-adapter';
|
|
@@ -144,19 +145,17 @@ export function savePatientPrograms(patientPrograms: PatientProgramPayload[]) {
|
|
|
144
145
|
export function saveAttachments(fields: FormField[], encounter: OpenmrsEncounter, abortController: AbortController) {
|
|
145
146
|
const complexFields = fields?.filter((field) => field?.questionOptions.rendering === 'file' && hasSubmission(field));
|
|
146
147
|
|
|
147
|
-
if (!complexFields?.length)
|
|
148
|
+
if (!complexFields?.length) {
|
|
149
|
+
return [];
|
|
150
|
+
}
|
|
148
151
|
|
|
149
|
-
|
|
152
|
+
const allPromises = complexFields.flatMap((field) => {
|
|
150
153
|
const patientUuid = typeof encounter?.patient === 'string' ? encounter?.patient : encounter?.patient?.uuid;
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
field,
|
|
154
|
-
field?.questionOptions.concept,
|
|
155
|
-
new Date().toISOString(),
|
|
156
|
-
encounter?.uuid,
|
|
157
|
-
abortController,
|
|
158
|
-
);
|
|
154
|
+
const attachments = (field.meta.submission.newValue as AttachmentFieldValue[]) ?? [];
|
|
155
|
+
return attachments.map((attachment) => createAttachment(patientUuid, encounter.uuid, attachment));
|
|
159
156
|
});
|
|
157
|
+
|
|
158
|
+
return Promise.all(allPromises);
|
|
160
159
|
}
|
|
161
160
|
|
|
162
161
|
export function getMutableSessionProps(context: FormContextProps) {
|
|
@@ -199,11 +198,14 @@ function processObsField(obsForSubmission: OpenmrsObs[], field: FormField) {
|
|
|
199
198
|
|
|
200
199
|
if (field.type === 'obsGroup') {
|
|
201
200
|
processObsGroup(obsForSubmission, field);
|
|
202
|
-
|
|
203
|
-
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// new attachments will be processed later
|
|
205
|
+
if (!hasRendering(field, 'file')) {
|
|
204
206
|
addObsToList(obsForSubmission, field.meta.submission.newValue);
|
|
205
|
-
addObsToList(obsForSubmission, field.meta.submission.voidedValue);
|
|
206
207
|
}
|
|
208
|
+
addObsToList(obsForSubmission, field.meta.submission.voidedValue);
|
|
207
209
|
}
|
|
208
210
|
|
|
209
211
|
function processObsGroup(obsForSubmission: OpenmrsObs[], groupField: FormField) {
|
|
@@ -257,7 +259,7 @@ function hasSubmittableObs(field: FormField) {
|
|
|
257
259
|
type,
|
|
258
260
|
} = field;
|
|
259
261
|
|
|
260
|
-
if (isTransient || !['obs', 'obsGroup'].includes(type) ||
|
|
262
|
+
if (isTransient || !['obs', 'obsGroup'].includes(type) || field.meta.groupId) {
|
|
261
263
|
return false;
|
|
262
264
|
}
|
|
263
265
|
if ((field.isHidden || field.isParentHidden) && field.meta.initialValue?.omrsObject) {
|
package/src/registry/registry.ts
CHANGED
|
@@ -16,6 +16,7 @@ import { getControlTemplate } from './inbuilt-components/control-templates';
|
|
|
16
16
|
import { inbuiltPostSubmissionActions } from './inbuilt-components/InbuiltPostSubmissionActions';
|
|
17
17
|
import { inbuiltFormTransformers } from './inbuilt-components/inbuiltTransformers';
|
|
18
18
|
import { inbuiltFieldValueAdapters } from './inbuilt-components/inbuiltFieldValueAdapters';
|
|
19
|
+
import { hasRendering } from '../utils/common-utils';
|
|
19
20
|
|
|
20
21
|
/**
|
|
21
22
|
* @internal
|
|
@@ -143,7 +144,7 @@ export async function getRegisteredControl(renderType: string) {
|
|
|
143
144
|
*/
|
|
144
145
|
export function getFieldControlWithFallback(question: FormField) {
|
|
145
146
|
// Check if the question has a missing concept
|
|
146
|
-
if (hasMissingConcept(question)) {
|
|
147
|
+
if (hasMissingConcept(question) && !hasRendering(question, 'file')) {
|
|
147
148
|
// If so, render a disabled text input
|
|
148
149
|
question.disabled = true;
|
|
149
150
|
question.isDisabled = true;
|
package/src/types/domain.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type OpenmrsResource } from '@openmrs/esm-framework';
|
|
1
|
+
import { type UploadedFile, type OpenmrsResource } from '@openmrs/esm-framework';
|
|
2
2
|
|
|
3
3
|
export interface OpenmrsEncounter {
|
|
4
4
|
uuid?: string;
|
|
@@ -108,14 +108,14 @@ export interface OpenmrsFormResource extends OpenmrsResource {
|
|
|
108
108
|
valueReference?: string;
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
-
export interface Attachment {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
111
|
+
export interface Attachment extends UploadedFile {
|
|
112
|
+
uuid?: string;
|
|
113
|
+
voided?: boolean;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export interface AttachmentFieldValue extends Attachment {
|
|
117
|
+
formFieldNamespace: string;
|
|
118
|
+
formFieldPath: string;
|
|
119
119
|
}
|
|
120
120
|
|
|
121
121
|
export interface AttachmentResponse {
|
package/src/types/index.ts
CHANGED
|
@@ -118,8 +118,8 @@ export interface PostSubmissionAction {
|
|
|
118
118
|
): void;
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
-
export interface FormFieldInputProps {
|
|
122
|
-
value:
|
|
121
|
+
export interface FormFieldInputProps<TValue = any> {
|
|
122
|
+
value: TValue;
|
|
123
123
|
field: FormField;
|
|
124
124
|
errors: ValidationResult[];
|
|
125
125
|
warnings: ValidationResult[];
|
|
@@ -128,7 +128,7 @@ export interface FormFieldInputProps {
|
|
|
128
128
|
*
|
|
129
129
|
* @param value - The new value of the field.
|
|
130
130
|
*/
|
|
131
|
-
setFieldValue: (value:
|
|
131
|
+
setFieldValue: (value: TValue) => void;
|
|
132
132
|
}
|
|
133
133
|
|
|
134
134
|
/**
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import Webcam from 'react-webcam';
|
|
3
|
-
import { Button } from '@carbon/react';
|
|
4
|
-
import { Camera as CameraIcon } from '@carbon/react/icons';
|
|
5
|
-
|
|
6
|
-
import styles from './camera.scss';
|
|
7
|
-
|
|
8
|
-
interface CameraProps {
|
|
9
|
-
handleImages: (state: any) => void;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
const Camera: React.FC<CameraProps> = ({ handleImages }) => {
|
|
13
|
-
const webcamRef = React.useRef(null);
|
|
14
|
-
|
|
15
|
-
const capture = React.useCallback(() => {
|
|
16
|
-
const imageSrc = webcamRef.current.getScreenshot();
|
|
17
|
-
handleImages(imageSrc);
|
|
18
|
-
}, [webcamRef]);
|
|
19
|
-
|
|
20
|
-
const videoConstraints = {
|
|
21
|
-
facingMode: 'user',
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
return (
|
|
25
|
-
<div>
|
|
26
|
-
<Webcam audio={false} ref={webcamRef} screenshotFormat="image/png" videoConstraints={videoConstraints} />
|
|
27
|
-
<div className={styles.captureButton}>
|
|
28
|
-
<Button onClick={capture} type="button" hasIconOnly renderIcon={() => <CameraIcon size={24} />}></Button>
|
|
29
|
-
</div>
|
|
30
|
-
</div>
|
|
31
|
-
);
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
export default Camera;
|