@openmrs/esm-form-engine-lib 3.1.5-pre.2001 → 3.1.5-pre.2008
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/components/renderer/field/fieldRenderUtils.test.ts +39 -0
- package/src/components/renderer/field/fieldRenderUtils.ts +10 -0
- package/src/components/renderer/field/form-field-renderer.component.tsx +8 -0
- package/src/components/sidebar/sidebar.component.tsx +28 -24
- package/src/form-engine.component.tsx +9 -3
- package/src/hooks/useExternalFormAction.ts +94 -0
- package/src/hooks/useFormJson.ts +15 -5
- package/src/provider/form-factory-provider.tsx +30 -4
- package/src/transformers/default-schema-transformer.test.ts +97 -1
- package/src/transformers/default-schema-transformer.ts +32 -2
- package/src/types/index.ts +4 -1
- package/src/types/schema.ts +8 -1
@@ -1 +1 @@
|
|
1
|
-
var _openmrs_esm_form_engine_lib;(()=>{"use strict";var e,r,t,n,o,i,a,l,s,u,f,p,d,c,h,m,v,g,b,y,w,_={78008:(e,r,t)=>{var n={"./start":()=>Promise.all([t.e(177),t.e(381),t.e(58),t.e(72),t.e(985),t.e(
|
1
|
+
var _openmrs_esm_form_engine_lib;(()=>{"use strict";var e,r,t,n,o,i,a,l,s,u,f,p,d,c,h,m,v,g,b,y,w,_={78008:(e,r,t)=>{var n={"./start":()=>Promise.all([t.e(177),t.e(381),t.e(58),t.e(72),t.e(985),t.e(677)]).then((()=>()=>t(5677)))},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 p=(r,n)=>{a.onerror=a.onload=null,clearTimeout(d);var o=t[e];if(delete t[e],a.parentNode&&a.parentNode.removeChild(a),o&&o.forEach((e=>e(n))),r)return r(n)},d=setTimeout(p.bind(null,void 0,{type:"timeout",target:a}),12e4);a.onerror=p.bind(null,a.onerror),a.onload=p.bind(null,a.onload),l&&document.head.appendChild(a)}},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","6.3.1-pre.3218",(()=>Promise.all([j.e(177),j.e(657),j.e(381),j.e(43),j.e(72),j.e(766),j.e(985),j.e(561),j.e(942)]).then((()=>()=>j(68043))))),l("@openmrs/esm-patient-common-lib","10.2.1-pre.8354",(()=>Promise.all([j.e(177),j.e(657),j.e(381),j.e(356),j.e(72),j.e(766),j.e(985),j.e(561),j.e(704)]).then((()=>()=>j(5356))))),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,p=a<e.length?(typeof e[a])[0]:"";if(i>=r.length||"o"==(f=(typeof(u=r[i]))[0]))return!s||("u"==p?a>t&&!n:""==p!=n);if("u"==f){if(!s||"u"!=p)return!1}else if(s)if(p==f)if(a<=t){if(u!=e[a])return!1}else{if(n?u>e[a]:u<e[a])return!1;u!=e[a]&&(s=!1)}else if("s"!=p&&"n"!=p){if(n||a<=t)return!1;s=!1,a--}else{if(a<=t||f<p!=n)return!1;s=!1}else"s"!=p&&"n"!=p&&(s=!1,a--)}}var d=[],c=d.pop.bind(d);for(i=1;i<e.length;i++){var h=e[i];d.push(1==h?c()|c():2==h?c()&c():h?l(h,r):!c())}return!!c()},s=(e,r)=>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)),{}),p=(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)},d=(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=p(r,t,n);return l(o,a)||h(d(r,t,a,o)),u(r[t][a])})),g={},b={16072:()=>v("default","react",!1,[1,18],(()=>j.e(540).then((()=>()=>j(96540))))),76766:()=>v("default","i18next",!1,[1,23],(()=>j.e(635).then((()=>()=>j(72635))))),29197:()=>v("default","@openmrs/esm-framework",!1,[1,6],(()=>Promise.all([j.e(177),j.e(657),j.e(43),j.e(766)]).then((()=>()=>j(68043))))),53941:()=>v("default","react-i18next",!1,[1,11],(()=>j.e(33).then((()=>()=>j(93414))))),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))))),80879:()=>v("default","@openmrs/esm-patient-common-lib",!1,[1,10],(()=>Promise.all([j.e(177),j.e(657),j.e(356),j.e(766)]).then((()=>()=>j(5356)))))},y={72:[16072],561:[44209,56339,70231],677:[44209,56339,70231,80879],766:[76766],985:[29197,53941]},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(/^(561|72|766|985)$/.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
@@ -0,0 +1,39 @@
|
|
1
|
+
import { shouldRenderField } from './fieldRenderUtils';
|
2
|
+
|
3
|
+
describe('shouldRenderField', () => {
|
4
|
+
it('should return false for transient fields with no value in embedded-view mode', () => {
|
5
|
+
const sessionMode = 'embedded-view';
|
6
|
+
const isTransient = true;
|
7
|
+
const isEmpty = true;
|
8
|
+
|
9
|
+
const result = shouldRenderField(sessionMode, isTransient, isEmpty);
|
10
|
+
expect(result).toBe(false);
|
11
|
+
});
|
12
|
+
|
13
|
+
it('should return true for transient fields with a value in embedded-view mode', () => {
|
14
|
+
const sessionMode = 'embedded-view';
|
15
|
+
const isTransient = true;
|
16
|
+
const isEmpty = false;
|
17
|
+
|
18
|
+
const result = shouldRenderField(sessionMode, isTransient, isEmpty);
|
19
|
+
expect(result).toBe(true);
|
20
|
+
});
|
21
|
+
|
22
|
+
it('should return true for non-transient fields in embedded-view mode', () => {
|
23
|
+
const sessionMode = 'embedded-view';
|
24
|
+
const isTransient = false;
|
25
|
+
const isEmpty = true;
|
26
|
+
|
27
|
+
const result = shouldRenderField(sessionMode, isTransient, isEmpty);
|
28
|
+
expect(result).toBe(true);
|
29
|
+
});
|
30
|
+
|
31
|
+
it('should return true for any field in non-embedded modes', () => {
|
32
|
+
const sessionMode = 'edit';
|
33
|
+
const isTransient = true;
|
34
|
+
const isEmpty = true;
|
35
|
+
|
36
|
+
const result = shouldRenderField(sessionMode, isTransient, isEmpty);
|
37
|
+
expect(result).toBe(true);
|
38
|
+
});
|
39
|
+
});
|
@@ -0,0 +1,10 @@
|
|
1
|
+
import { type SessionMode } from '../../../types';
|
2
|
+
|
3
|
+
/**
|
4
|
+
* @name shouldRenderField
|
5
|
+
* @description Determines if a field should be rendered based on the session mode, whether it is transient, and if it is empty.
|
6
|
+
* - A field will not be rendered in 'embedded-view' mode if it is transient and has no value.
|
7
|
+
*/
|
8
|
+
export function shouldRenderField(sessionMode: SessionMode, isTransient: boolean, isEmpty: boolean): boolean {
|
9
|
+
return !(sessionMode === 'embedded-view' && isTransient && isEmpty);
|
10
|
+
}
|
@@ -19,6 +19,7 @@ import { isTrue } from '../../../utils/boolean-utils';
|
|
19
19
|
import { useFormProviderContext } from '../../../provider/form-provider';
|
20
20
|
import PreviousValueReview from '../../previous-value-review/previous-value-review.component';
|
21
21
|
import UnspecifiedField from '../../inputs/unspecified/unspecified.component';
|
22
|
+
import { shouldRenderField } from './fieldRenderUtils';
|
22
23
|
import styles from './form-field-renderer.scss';
|
23
24
|
|
24
25
|
export interface FormFieldRendererProps {
|
@@ -172,6 +173,13 @@ export const FormFieldRenderer = ({ fieldId, valueAdapter, repeatOptions }: Form
|
|
172
173
|
/>
|
173
174
|
);
|
174
175
|
}
|
176
|
+
|
177
|
+
// If the field is transient and has no value, we do not render it in embedded view mode.
|
178
|
+
// This is to prevent transient fields from being displayed in the form when they are not necessarily needed.
|
179
|
+
if (!shouldRenderField(sessionMode, !!field.questionOptions.isTransient, isEmpty(fieldValue))) {
|
180
|
+
return null;
|
181
|
+
}
|
182
|
+
|
175
183
|
return (
|
176
184
|
<ErrorBoundary FallbackComponent={ErrorFallback} onReset={noop}>
|
177
185
|
<Controller
|
@@ -16,6 +16,7 @@ interface SidebarProps {
|
|
16
16
|
onCancel: () => void;
|
17
17
|
handleClose: () => void;
|
18
18
|
hideFormCollapseToggle: () => void;
|
19
|
+
hideControls?: boolean;
|
19
20
|
}
|
20
21
|
|
21
22
|
const Sidebar: React.FC<SidebarProps> = ({
|
@@ -25,6 +26,7 @@ const Sidebar: React.FC<SidebarProps> = ({
|
|
25
26
|
onCancel,
|
26
27
|
handleClose,
|
27
28
|
hideFormCollapseToggle,
|
29
|
+
hideControls,
|
28
30
|
}) => {
|
29
31
|
const { t } = useTranslation();
|
30
32
|
const { pages, pagesWithErrors, activePages, evaluatedPagesVisibility } = usePageObserver();
|
@@ -53,32 +55,34 @@ const Sidebar: React.FC<SidebarProps> = ({
|
|
53
55
|
requestPage={requestPage}
|
54
56
|
/>
|
55
57
|
))}
|
56
|
-
{sessionMode !== 'view' && <hr className={styles.divider} />}
|
58
|
+
{sessionMode !== 'view' && !hideControls && <hr className={styles.divider} />}
|
57
59
|
|
58
|
-
|
59
|
-
{
|
60
|
-
|
61
|
-
{isFormSubmitting
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
60
|
+
{!hideControls && (
|
61
|
+
<div className={styles.sideNavActions}>
|
62
|
+
{sessionMode !== 'view' && (
|
63
|
+
<Button className={styles.saveButton} disabled={isFormSubmitting} type="submit" size={responsiveSize}>
|
64
|
+
{isFormSubmitting ? (
|
65
|
+
<InlineLoading description={t('submitting', 'Submitting') + '...'} />
|
66
|
+
) : (
|
67
|
+
<span>{`${t('save', 'Save')}`}</span>
|
68
|
+
)}
|
69
|
+
</Button>
|
70
|
+
)}
|
71
|
+
<Button
|
72
|
+
className={classNames(styles.closeButton, {
|
73
|
+
[styles.topMargin]: sessionMode === 'view',
|
74
|
+
})}
|
75
|
+
kind="tertiary"
|
76
|
+
onClick={() => {
|
77
|
+
onCancel?.();
|
78
|
+
handleClose?.();
|
79
|
+
hideFormCollapseToggle();
|
80
|
+
}}
|
81
|
+
size={responsiveSize}>
|
82
|
+
{sessionMode === 'view' ? t('close', 'Close') : t('cancel', 'Cancel')}
|
66
83
|
</Button>
|
67
|
-
|
68
|
-
|
69
|
-
className={classNames(styles.closeButton, {
|
70
|
-
[styles.topMargin]: sessionMode === 'view',
|
71
|
-
})}
|
72
|
-
kind="tertiary"
|
73
|
-
onClick={() => {
|
74
|
-
onCancel?.();
|
75
|
-
handleClose?.();
|
76
|
-
hideFormCollapseToggle();
|
77
|
-
}}
|
78
|
-
size={responsiveSize}>
|
79
|
-
{sessionMode === 'view' ? t('close', 'Close') : t('cancel', 'Cancel')}
|
80
|
-
</Button>
|
81
|
-
</div>
|
84
|
+
</div>
|
85
|
+
)}
|
82
86
|
</div>
|
83
87
|
);
|
84
88
|
};
|
@@ -12,7 +12,7 @@ import { useFormCollapse } from './hooks/useFormCollapse';
|
|
12
12
|
import { useFormWorkspaceSize } from './hooks/useFormWorkspaceSize';
|
13
13
|
import { usePageObserver } from './components/sidebar/usePageObserver';
|
14
14
|
import { usePatientData } from './hooks/usePatientData';
|
15
|
-
import type { FormField, FormSchema, SessionMode } from './types';
|
15
|
+
import type { FormField, FormSchema, SessionMode, PreFilledQuestions } from './types';
|
16
16
|
import FormProcessorFactory from './components/processor-factory/form-processor-factory.component';
|
17
17
|
import Loader from './components/loaders/loader.component';
|
18
18
|
import MarkdownWrapper from './components/inputs/markdown/markdown-wrapper.component';
|
@@ -33,6 +33,8 @@ interface FormEngineProps {
|
|
33
33
|
handleClose?: () => void;
|
34
34
|
handleConfirmQuestionDeletion?: (question: Readonly<FormField>) => Promise<void>;
|
35
35
|
markFormAsDirty?: (isDirty: boolean) => void;
|
36
|
+
hideControls?: boolean;
|
37
|
+
preFilledQuestions?: PreFilledQuestions;
|
36
38
|
}
|
37
39
|
|
38
40
|
const FormEngine = ({
|
@@ -48,6 +50,8 @@ const FormEngine = ({
|
|
48
50
|
handleClose,
|
49
51
|
handleConfirmQuestionDeletion,
|
50
52
|
markFormAsDirty,
|
53
|
+
hideControls = false,
|
54
|
+
preFilledQuestions,
|
51
55
|
}: FormEngineProps) => {
|
52
56
|
const { t } = useTranslation();
|
53
57
|
const session = useSession();
|
@@ -68,7 +72,7 @@ const FormEngine = ({
|
|
68
72
|
formJson: refinedFormJson,
|
69
73
|
isLoading: isLoadingFormJson,
|
70
74
|
formError,
|
71
|
-
} = useFormJson(formUUID, formJson, encounterUUID, formSessionIntent);
|
75
|
+
} = useFormJson(formUUID, formJson, encounterUUID, formSessionIntent, preFilledQuestions);
|
72
76
|
|
73
77
|
const showPatientBanner = useMemo(() => {
|
74
78
|
return patient && workspaceSize === 'ultra-wide' && mode !== 'embedded-view';
|
@@ -119,6 +123,7 @@ const FormEngine = ({
|
|
119
123
|
) : (
|
120
124
|
<FormFactoryProvider
|
121
125
|
patient={patient}
|
126
|
+
patientUUID={patientUUID}
|
122
127
|
sessionMode={sessionMode}
|
123
128
|
sessionDate={sessionDate}
|
124
129
|
formJson={refinedFormJson}
|
@@ -152,6 +157,7 @@ const FormEngine = ({
|
|
152
157
|
onCancel={onCancel}
|
153
158
|
handleClose={handleClose}
|
154
159
|
hideFormCollapseToggle={hideFormCollapseToggle}
|
160
|
+
hideControls={hideControls}
|
155
161
|
/>
|
156
162
|
)}
|
157
163
|
<div className={styles.formContentInner}>
|
@@ -167,7 +173,7 @@ const FormEngine = ({
|
|
167
173
|
setIsLoadingFormDependencies={setIsLoadingDependencies}
|
168
174
|
/>
|
169
175
|
</div>
|
170
|
-
{showBottomButtonSet && (
|
176
|
+
{showBottomButtonSet && !hideControls && (
|
171
177
|
<ButtonSet className={styles.minifiedButtons}>
|
172
178
|
<Button
|
173
179
|
kind="secondary"
|
@@ -0,0 +1,94 @@
|
|
1
|
+
import { useEffect } from 'react';
|
2
|
+
import { reportError } from '../utils/error-utils';
|
3
|
+
import { useTranslation } from 'react-i18next';
|
4
|
+
|
5
|
+
interface SubmitEventDetail {
|
6
|
+
formUuid: string;
|
7
|
+
patientUuid: string;
|
8
|
+
action: string;
|
9
|
+
}
|
10
|
+
|
11
|
+
interface UseExternalFormActionProps {
|
12
|
+
patientUuid: string;
|
13
|
+
formUuid: string;
|
14
|
+
setIsSubmitting: (boolean) => void;
|
15
|
+
setIsValidating: (boolean) => void;
|
16
|
+
}
|
17
|
+
|
18
|
+
/**
|
19
|
+
* useExternalFormAction
|
20
|
+
*
|
21
|
+
* A custom React hook that listens for a global `CustomEvent` (`ampath-form-action`) and triggers
|
22
|
+
* specific actions (such as form submission or validation) by updating external state via provided handlers.
|
23
|
+
*
|
24
|
+
* This is especially useful in scenarios where the Form Engine is embedded inside another application
|
25
|
+
* or UI shell and form behavior (e.g. validation or submission) needs to be triggered programmatically,
|
26
|
+
* such as from a toolbar button, modal footer, or an iframe parent.
|
27
|
+
*
|
28
|
+
* The hook ensures that the received event matches the current `formUuid` and `patientUuid` before executing any action.
|
29
|
+
*
|
30
|
+
* ### Supported Actions
|
31
|
+
* - `"onSubmit"` — Triggers form submission
|
32
|
+
* - `"validateForm"` — Triggers form validation
|
33
|
+
*
|
34
|
+
* @param patientUuid - The UUID of the current patient, used to verify that the action is intended for this instance.
|
35
|
+
* @param formUuid - The UUID of the current form, used to validate the target of the action.
|
36
|
+
* @param setIsSubmitting - A `setState` handler that initiates form submission when set to `true`.
|
37
|
+
* @param setIsValidating - A `setState` handler that initiates form validation when set to `true`.
|
38
|
+
*
|
39
|
+
* @example
|
40
|
+
*
|
41
|
+
* // Elsewhere in the app, dispatching the event:
|
42
|
+
* window.dispatchEvent(
|
43
|
+
* new CustomEvent('ampath-form-action', {
|
44
|
+
* detail: {
|
45
|
+
* formUuid: '289417aa-31d5-3a06-bae8-a22d870bcf1d',
|
46
|
+
* patientUuid: '9ee7a509-d639-4d91-979a-cd605b4d0ad1/chart',
|
47
|
+
* action: 'onSubmit',
|
48
|
+
* },
|
49
|
+
* })
|
50
|
+
* );
|
51
|
+
*/
|
52
|
+
|
53
|
+
export function useExternalFormAction({
|
54
|
+
patientUuid,
|
55
|
+
formUuid,
|
56
|
+
setIsSubmitting,
|
57
|
+
setIsValidating,
|
58
|
+
}: UseExternalFormActionProps) {
|
59
|
+
const { t } = useTranslation();
|
60
|
+
|
61
|
+
useEffect(() => {
|
62
|
+
const handleSubmit = (event: Event) => {
|
63
|
+
const customEvent = event as CustomEvent<SubmitEventDetail>;
|
64
|
+
const { formUuid: targetFormUuid, patientUuid: targetPatientUuid, action } = customEvent.detail;
|
65
|
+
|
66
|
+
if (!action || !targetFormUuid || !targetPatientUuid) {
|
67
|
+
reportError(
|
68
|
+
new Error('The form action event is missing required details (formUuid, patientUuid, or action).'),
|
69
|
+
t('formActionFailed', 'Form action failed'),
|
70
|
+
);
|
71
|
+
return;
|
72
|
+
}
|
73
|
+
|
74
|
+
if (targetFormUuid === formUuid && targetPatientUuid === patientUuid) {
|
75
|
+
switch (action) {
|
76
|
+
case 'onSubmit':
|
77
|
+
setIsSubmitting(true);
|
78
|
+
break;
|
79
|
+
case 'validateForm':
|
80
|
+
setIsValidating(true);
|
81
|
+
break;
|
82
|
+
default:
|
83
|
+
reportError(new Error(`Unsupported form action: "${action}"`), t('formActionFailed', 'Form action failed'));
|
84
|
+
break;
|
85
|
+
}
|
86
|
+
}
|
87
|
+
};
|
88
|
+
|
89
|
+
window.addEventListener('ampath-form-action', handleSubmit);
|
90
|
+
return () => {
|
91
|
+
window.removeEventListener('ampath-form-action', handleSubmit);
|
92
|
+
};
|
93
|
+
}, [setIsSubmitting, setIsValidating, formUuid, patientUuid]);
|
94
|
+
}
|
package/src/hooks/useFormJson.ts
CHANGED
@@ -1,12 +1,18 @@
|
|
1
1
|
import { useEffect, useState } from 'react';
|
2
|
-
import { type FormSchemaTransformer, type FormSchema, type FormSection, type ReferencedForm } from '../types';
|
2
|
+
import { type FormSchemaTransformer, type FormSchema, type FormSection, type ReferencedForm , type PreFilledQuestions } from '../types';
|
3
3
|
import { isTrue } from '../utils/boolean-utils';
|
4
4
|
import { applyFormIntent } from '../utils/forms-loader';
|
5
5
|
import { fetchOpenMRSForm, fetchClobData } from '../api';
|
6
6
|
import { getRegisteredFormSchemaTransformers } from '../registry/registry';
|
7
7
|
import { formEngineAppName } from '../globals';
|
8
8
|
|
9
|
-
export function useFormJson(
|
9
|
+
export function useFormJson(
|
10
|
+
formUuid: string,
|
11
|
+
rawFormJson: any,
|
12
|
+
encounterUuid: string,
|
13
|
+
formSessionIntent: string,
|
14
|
+
preFilledQuestions?: PreFilledQuestions,
|
15
|
+
) {
|
10
16
|
const [formJson, setFormJson] = useState<FormSchema>(null);
|
11
17
|
const [error, setError] = useState(validateFormsArgs(formUuid, rawFormJson));
|
12
18
|
|
@@ -21,7 +27,7 @@ export function useFormJson(formUuid: string, rawFormJson: any, encounterUuid: s
|
|
21
27
|
setFormJson(formJson);
|
22
28
|
};
|
23
29
|
|
24
|
-
loadFormJson(formUuid, rawFormJson, formSessionIntent)
|
30
|
+
loadFormJson(formUuid, rawFormJson, formSessionIntent, preFilledQuestions)
|
25
31
|
.then((formJson) => {
|
26
32
|
setFormJsonWithTranslations({ ...formJson, encounter: encounterUuid });
|
27
33
|
})
|
@@ -52,12 +58,14 @@ export function useFormJson(formUuid: string, rawFormJson: any, encounterUuid: s
|
|
52
58
|
* @param rawFormJson The raw form JSON object to be used if `formIdentifier` is not provided.
|
53
59
|
* @param formIdentifier The UUID or name of the form to be fetched from OpenMRS if `rawFormJson` is not provided.
|
54
60
|
* @param formSessionIntent An optional parameter that represents the current intent.
|
61
|
+
* @param preFilledQuestions An optional parameter that represents the pre-filled questions.
|
55
62
|
* @returns A well-built form object that might include subForms.
|
56
63
|
*/
|
57
64
|
export async function loadFormJson(
|
58
65
|
formIdentifier: string,
|
59
66
|
rawFormJson?: FormSchema,
|
60
67
|
formSessionIntent?: string,
|
68
|
+
preFilledQuestions?: PreFilledQuestions,
|
61
69
|
): Promise<FormSchema> {
|
62
70
|
const openmrsFormResponse = await fetchOpenMRSForm(formIdentifier);
|
63
71
|
const clobDataResponse = await fetchClobData(openmrsFormResponse);
|
@@ -81,7 +89,7 @@ export async function loadFormJson(
|
|
81
89
|
|
82
90
|
const formComponents = mapFormComponents(resolvedFormComponents);
|
83
91
|
updateFormJsonWithComponents(formJson, formComponents, formNameToAliasMap);
|
84
|
-
return refineFormJson(formJson, transformers, formSessionIntent);
|
92
|
+
return refineFormJson(formJson, transformers, formSessionIntent, preFilledQuestions);
|
85
93
|
}
|
86
94
|
|
87
95
|
function extractSubFormRefs(formJson: FormSchema): string[] {
|
@@ -116,16 +124,18 @@ function validateFormsArgs(formUuid: string, rawFormJson: any): Error {
|
|
116
124
|
* Refines the input form JSON object by parsing it, removing inline sub forms, applying form schema transformers, setting the encounter type, and applying form intents if provided.
|
117
125
|
* @param {any} formJson - The input form JSON object or string.
|
118
126
|
* @param {string} [formSessionIntent] - The optional form session intent.
|
127
|
+
* @param {PreFilledQuestions} [preFilledQuestions] - The optional pre-filled questions.
|
119
128
|
* @returns {FormSchema} - The refined form JSON object of type FormSchema.
|
120
129
|
*/
|
121
130
|
function refineFormJson(
|
122
131
|
formJson: any,
|
123
132
|
schemaTransformers: FormSchemaTransformer[] = [],
|
124
133
|
formSessionIntent?: string,
|
134
|
+
preFilledQuestions?: PreFilledQuestions,
|
125
135
|
): FormSchema {
|
126
136
|
removeInlineSubForms(formJson, formSessionIntent);
|
127
137
|
// apply form schema transformers
|
128
|
-
schemaTransformers.reduce((draftForm, transformer) => transformer.transform(draftForm), formJson);
|
138
|
+
schemaTransformers.reduce((draftForm, transformer) => transformer.transform(draftForm, preFilledQuestions), formJson);
|
129
139
|
setEncounterType(formJson);
|
130
140
|
return applyFormIntent(formSessionIntent, formJson);
|
131
141
|
}
|
@@ -1,4 +1,4 @@
|
|
1
|
-
import React, { createContext, useCallback, useContext, useEffect, useRef } from 'react';
|
1
|
+
import React, { createContext, useCallback, useContext, useEffect, useRef, useState } from 'react';
|
2
2
|
import { type FormField, type FormSchema, type SessionMode } from '../types';
|
3
3
|
import { EncounterFormProcessor } from '../processors/encounter/encounter-form-processor';
|
4
4
|
import {
|
@@ -14,6 +14,7 @@ import { type FormContextProps } from './form-provider';
|
|
14
14
|
import { processPostSubmissionActions, validateForm } from './form-factory-helper';
|
15
15
|
import { useTranslation } from 'react-i18next';
|
16
16
|
import { usePostSubmissionActions } from '../hooks/usePostSubmissionActions';
|
17
|
+
import { useExternalFormAction } from '../hooks/useExternalFormAction';
|
17
18
|
|
18
19
|
interface FormFactoryProviderContextProps {
|
19
20
|
patient: fhir.Patient;
|
@@ -34,6 +35,7 @@ interface FormFactoryProviderContextProps {
|
|
34
35
|
|
35
36
|
interface FormFactoryProviderProps {
|
36
37
|
patient: fhir.Patient;
|
38
|
+
patientUUID: string;
|
37
39
|
sessionMode: SessionMode;
|
38
40
|
sessionDate: Date;
|
39
41
|
formJson: FormSchema;
|
@@ -59,6 +61,7 @@ const FormFactoryProviderContext = createContext<FormFactoryProviderContextProps
|
|
59
61
|
|
60
62
|
export const FormFactoryProvider: React.FC<FormFactoryProviderProps> = ({
|
61
63
|
patient,
|
64
|
+
patientUUID,
|
62
65
|
sessionMode,
|
63
66
|
sessionDate,
|
64
67
|
formJson,
|
@@ -78,6 +81,7 @@ export const FormFactoryProvider: React.FC<FormFactoryProviderProps> = ({
|
|
78
81
|
const subForms = useRef<Record<string, FormContextProps>>({});
|
79
82
|
const layoutType = useLayoutType();
|
80
83
|
const { isSubmitting, setIsSubmitting, onSubmit, onError, handleClose } = formSubmissionProps;
|
84
|
+
const [isValidating, setIsValidating] = useState(false);
|
81
85
|
const postSubmissionHandlers = usePostSubmissionActions(formJson.postSubmissionActions);
|
82
86
|
|
83
87
|
const abortController = new AbortController();
|
@@ -95,12 +99,34 @@ export const FormFactoryProvider: React.FC<FormFactoryProviderProps> = ({
|
|
95
99
|
EncounterFormProcessor: EncounterFormProcessor,
|
96
100
|
});
|
97
101
|
|
102
|
+
const validateAllForms = useCallback(() => {
|
103
|
+
const forms = [rootForm.current, ...Object.values(subForms.current)];
|
104
|
+
const isValid = forms.every((formContext) => validateForm(formContext));
|
105
|
+
return {
|
106
|
+
forms: forms,
|
107
|
+
isValid: isValid,
|
108
|
+
};
|
109
|
+
}, []);
|
110
|
+
|
111
|
+
useExternalFormAction({
|
112
|
+
patientUuid: patientUUID,
|
113
|
+
formUuid: formJson?.uuid,
|
114
|
+
setIsSubmitting: setIsSubmitting,
|
115
|
+
setIsValidating: setIsValidating,
|
116
|
+
});
|
117
|
+
|
118
|
+
useEffect(() => {
|
119
|
+
if (isValidating) {
|
120
|
+
validateAllForms();
|
121
|
+
setIsValidating(false);
|
122
|
+
}
|
123
|
+
}, [isValidating, validateAllForms]);
|
124
|
+
|
98
125
|
useEffect(() => {
|
99
126
|
if (isSubmitting) {
|
100
127
|
// TODO: find a dynamic way of managing the form processing order
|
101
|
-
const forms = [rootForm.current, ...Object.values(subForms.current)];
|
102
128
|
// validate all forms
|
103
|
-
const isValid =
|
129
|
+
const { forms, isValid } = validateAllForms();
|
104
130
|
if (isValid) {
|
105
131
|
Promise.all(forms.map((formContext) => formContext.processor.processSubmission(formContext, abortController)))
|
106
132
|
.then(async (results) => {
|
@@ -150,7 +176,7 @@ export const FormFactoryProvider: React.FC<FormFactoryProviderProps> = ({
|
|
150
176
|
return () => {
|
151
177
|
abortController.abort();
|
152
178
|
};
|
153
|
-
}, [isSubmitting]);
|
179
|
+
}, [isSubmitting, validateAllForms]);
|
154
180
|
|
155
181
|
return (
|
156
182
|
<FormFactoryProviderContext.Provider
|
@@ -1,4 +1,4 @@
|
|
1
|
-
import { type FormSchema } from '../types';
|
1
|
+
import { type FormSchema , type PreFilledQuestions } from '../types';
|
2
2
|
import { DefaultFormSchemaTransformer } from './default-schema-transformer';
|
3
3
|
import { testForm } from '__mocks__/forms';
|
4
4
|
|
@@ -239,4 +239,100 @@ describe('Default form schema transformer', () => {
|
|
239
239
|
],
|
240
240
|
};
|
241
241
|
});
|
242
|
+
|
243
|
+
it('should handle pre-filled questions', () => {
|
244
|
+
const form = {
|
245
|
+
pages: [
|
246
|
+
{
|
247
|
+
sections: [
|
248
|
+
{
|
249
|
+
questions: [
|
250
|
+
{
|
251
|
+
id: 'question1',
|
252
|
+
type: 'obs',
|
253
|
+
questionOptions: {},
|
254
|
+
},
|
255
|
+
{
|
256
|
+
id: 'nestedGroup',
|
257
|
+
type: 'obsGroup',
|
258
|
+
questionOptions: {
|
259
|
+
rendering: 'group',
|
260
|
+
},
|
261
|
+
questions: [
|
262
|
+
{
|
263
|
+
id: 'nestedQuestion1',
|
264
|
+
type: 'obs',
|
265
|
+
questionOptions: {},
|
266
|
+
},
|
267
|
+
],
|
268
|
+
},
|
269
|
+
],
|
270
|
+
},
|
271
|
+
],
|
272
|
+
},
|
273
|
+
],
|
274
|
+
};
|
275
|
+
const preFilledQuestions = {
|
276
|
+
question1: 'prefilledValue1',
|
277
|
+
nestedQuestion1: 'prefilledValue2',
|
278
|
+
};
|
279
|
+
|
280
|
+
const transformedForm = DefaultFormSchemaTransformer.transform(
|
281
|
+
form as FormSchema,
|
282
|
+
preFilledQuestions as PreFilledQuestions,
|
283
|
+
);
|
284
|
+
|
285
|
+
expect(transformedForm.pages[0].sections[0].questions[0].questionOptions.defaultValue).toEqual('prefilledValue1');
|
286
|
+
expect(transformedForm.pages[0].sections[0].questions[1].questions[0].questionOptions.defaultValue).toEqual(
|
287
|
+
'prefilledValue2',
|
288
|
+
);
|
289
|
+
});
|
290
|
+
|
291
|
+
it('should not modify questions when no pre-filled questions are provided', () => {
|
292
|
+
const form = {
|
293
|
+
pages: [
|
294
|
+
{
|
295
|
+
sections: [
|
296
|
+
{
|
297
|
+
questions: [
|
298
|
+
{
|
299
|
+
id: 'question1',
|
300
|
+
type: 'obs',
|
301
|
+
questionOptions: {},
|
302
|
+
},
|
303
|
+
],
|
304
|
+
},
|
305
|
+
],
|
306
|
+
},
|
307
|
+
],
|
308
|
+
};
|
309
|
+
|
310
|
+
const transformedForm = DefaultFormSchemaTransformer.transform(form as FormSchema);
|
311
|
+
|
312
|
+
expect(transformedForm.pages[0].sections[0].questions[0].questionOptions.defaultValue).toBeUndefined();
|
313
|
+
});
|
314
|
+
|
315
|
+
it('should handle empty pre-filled questions object', () => {
|
316
|
+
const form = {
|
317
|
+
pages: [
|
318
|
+
{
|
319
|
+
sections: [
|
320
|
+
{
|
321
|
+
questions: [
|
322
|
+
{
|
323
|
+
id: 'question1',
|
324
|
+
type: 'obs',
|
325
|
+
questionOptions: {},
|
326
|
+
},
|
327
|
+
],
|
328
|
+
},
|
329
|
+
],
|
330
|
+
},
|
331
|
+
],
|
332
|
+
};
|
333
|
+
|
334
|
+
const transformedForm = DefaultFormSchemaTransformer.transform(form as FormSchema, {} as PreFilledQuestions);
|
335
|
+
|
336
|
+
expect(transformedForm.pages[0].sections[0].questions[0].questionOptions.defaultValue).toBeUndefined();
|
337
|
+
});
|
242
338
|
});
|
@@ -1,12 +1,19 @@
|
|
1
1
|
import { type OpenmrsResource } from '@openmrs/esm-framework';
|
2
|
-
import {
|
2
|
+
import {
|
3
|
+
type FormField,
|
4
|
+
type FormSchema,
|
5
|
+
type FormSchemaTransformer,
|
6
|
+
type RenderType,
|
7
|
+
type FormPage,
|
8
|
+
type PreFilledQuestions,
|
9
|
+
} from '../types';
|
3
10
|
import { isTrue } from '../utils/boolean-utils';
|
4
11
|
import { hasRendering } from '../utils/common-utils';
|
5
12
|
|
6
13
|
export type RenderTypeExtended = 'multiCheckbox' | 'numeric' | RenderType;
|
7
14
|
|
8
15
|
export const DefaultFormSchemaTransformer: FormSchemaTransformer = {
|
9
|
-
transform: (form: FormSchema) => {
|
16
|
+
transform: (form: FormSchema, preFilledQuestions?: PreFilledQuestions) => {
|
10
17
|
parseBooleanTokenIfPresent(form, 'readonly');
|
11
18
|
form.pages.forEach((page, index) => {
|
12
19
|
const label = page.label ?? '';
|
@@ -22,6 +29,9 @@ export const DefaultFormSchemaTransformer: FormSchemaTransformer = {
|
|
22
29
|
});
|
23
30
|
}
|
24
31
|
});
|
32
|
+
if (preFilledQuestions && typeof preFilledQuestions === 'object') {
|
33
|
+
handlePreFilledQuestions(form, preFilledQuestions);
|
34
|
+
}
|
25
35
|
if (form.meta?.programs) {
|
26
36
|
handleProgramMetaTags(form);
|
27
37
|
}
|
@@ -308,3 +318,23 @@ function handleDiagnosis(question: FormField) {
|
|
308
318
|
delete question.questionOptions['dataSource'];
|
309
319
|
}
|
310
320
|
}
|
321
|
+
|
322
|
+
function handlePreFilledQuestions(form: FormSchema, preFilledQuestions: PreFilledQuestions) {
|
323
|
+
Object.entries(preFilledQuestions).forEach(([preFilledQnId, preFilledValue]) => {
|
324
|
+
form?.pages.forEach((page) => {
|
325
|
+
page.sections.forEach((section) => {
|
326
|
+
section.questions.forEach((question) => {
|
327
|
+
if (question.id === preFilledQnId) {
|
328
|
+
question.questionOptions.defaultValue = preFilledValue;
|
329
|
+
} else if (Array.isArray(question?.questions) && question.questions.length > 0) {
|
330
|
+
question.questions.forEach((question) => {
|
331
|
+
if (question.id === preFilledQnId) {
|
332
|
+
question.questionOptions.defaultValue = preFilledValue;
|
333
|
+
}
|
334
|
+
});
|
335
|
+
}
|
336
|
+
});
|
337
|
+
});
|
338
|
+
});
|
339
|
+
});
|
340
|
+
}
|
package/src/types/index.ts
CHANGED
@@ -101,8 +101,9 @@ export interface DataSourceParameters {
|
|
101
101
|
export interface FormSchemaTransformer {
|
102
102
|
/**
|
103
103
|
* Transforms the raw schema to be compatible with the React Form Engine.
|
104
|
+
* Adds default values to questions based on the preFilledQuestions object.
|
104
105
|
*/
|
105
|
-
transform: (form: FormSchema) => FormSchema;
|
106
|
+
transform: (form: FormSchema, preFilledQuestions?: PreFilledQuestions) => FormSchema;
|
106
107
|
}
|
107
108
|
|
108
109
|
export interface PostSubmissionAction {
|
@@ -146,5 +147,7 @@ export interface ValidationResult {
|
|
146
147
|
message: string;
|
147
148
|
}
|
148
149
|
|
150
|
+
export type PreFilledQuestions = Record<string, string | number | Date | boolean | Array<string>>;
|
151
|
+
|
149
152
|
export * from './schema';
|
150
153
|
export * from './domain';
|
package/src/types/schema.ts
CHANGED
@@ -149,9 +149,16 @@ export interface FormQuestionOptions {
|
|
149
149
|
*/
|
150
150
|
step?: number;
|
151
151
|
/**
|
152
|
-
*
|
152
|
+
* @description
|
153
|
+
* Indicates whether the field is transient.
|
154
|
+
* - Transient fields are ignored on form submission.
|
155
|
+
* - If set to __true__, the field is omitted from the O3 Form in `embedded-view` mode.
|
156
|
+
* @default false
|
153
157
|
*/
|
154
158
|
isTransient?: boolean;
|
159
|
+
/**
|
160
|
+
* maxLength and maxLength are used to validate text field length
|
161
|
+
*/
|
155
162
|
maxLength?: string;
|
156
163
|
minLength?: string;
|
157
164
|
showDate?: string;
|