@aehrc/smart-forms-renderer 0.36.1 → 0.38.2

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 (164) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/lib/components/FormComponents/Button.styles.d.ts +4 -0
  3. package/lib/components/FormComponents/Button.styles.js +10 -0
  4. package/lib/components/FormComponents/Button.styles.js.map +1 -0
  5. package/lib/components/FormComponents/GroupItem/GroupHeading.d.ts +1 -0
  6. package/lib/components/FormComponents/GroupItem/GroupHeading.js +3 -2
  7. package/lib/components/FormComponents/GroupItem/GroupHeading.js.map +1 -1
  8. package/lib/components/FormComponents/GroupItem/GroupItem.d.ts +4 -0
  9. package/lib/components/FormComponents/GroupItem/GroupItem.js +3 -3
  10. package/lib/components/FormComponents/GroupItem/GroupItem.js.map +1 -1
  11. package/lib/components/FormComponents/GroupItem/GroupItemView.d.ts +4 -0
  12. package/lib/components/FormComponents/GroupItem/GroupItemView.js +8 -5
  13. package/lib/components/FormComponents/GroupItem/GroupItemView.js.map +1 -1
  14. package/lib/components/FormComponents/GroupItem/NextPageButton.d.ts +7 -0
  15. package/lib/{hooks/useQueryClient.js → components/FormComponents/GroupItem/NextPageButton.js} +9 -12
  16. package/lib/components/FormComponents/GroupItem/NextPageButton.js.map +1 -0
  17. package/lib/components/FormComponents/GroupItem/PageButtonWrapper.d.ts +8 -0
  18. package/lib/components/FormComponents/GroupItem/PageButtonWrapper.js +46 -0
  19. package/lib/components/FormComponents/GroupItem/PageButtonWrapper.js.map +1 -0
  20. package/lib/components/FormComponents/GroupItem/PreviousPageButton.d.ts +7 -0
  21. package/lib/components/FormComponents/GroupItem/PreviousPageButton.js +26 -0
  22. package/lib/components/FormComponents/GroupItem/PreviousPageButton.js.map +1 -0
  23. package/lib/components/FormComponents/QuantityItem/QuantityComparatorField.d.ts +12 -0
  24. package/lib/components/FormComponents/QuantityItem/QuantityComparatorField.js +13 -0
  25. package/lib/components/FormComponents/QuantityItem/QuantityComparatorField.js.map +1 -0
  26. package/lib/components/FormComponents/QuantityItem/QuantityField.d.ts +15 -0
  27. package/lib/components/FormComponents/QuantityItem/QuantityField.js +14 -0
  28. package/lib/components/FormComponents/QuantityItem/QuantityField.js.map +1 -0
  29. package/lib/components/FormComponents/{DateTimeItem/DateTimeItem.d.ts → QuantityItem/QuantityItem.d.ts} +3 -3
  30. package/lib/components/FormComponents/QuantityItem/QuantityItem.js +144 -0
  31. package/lib/components/FormComponents/QuantityItem/QuantityItem.js.map +1 -0
  32. package/lib/components/FormComponents/QuantityItem/QuantityUnitField.d.ts +12 -0
  33. package/lib/components/FormComponents/QuantityItem/QuantityUnitField.js +10 -0
  34. package/lib/components/FormComponents/QuantityItem/QuantityUnitField.js.map +1 -0
  35. package/lib/components/FormComponents/SingleItem/SingleItemSwitcher.js +2 -1
  36. package/lib/components/FormComponents/SingleItem/SingleItemSwitcher.js.map +1 -1
  37. package/lib/components/Renderer/BaseRenderer.js +8 -0
  38. package/lib/components/Renderer/BaseRenderer.js.map +1 -1
  39. package/lib/components/Renderer/FormBodyPage.d.ts +9 -0
  40. package/lib/components/Renderer/FormBodyPage.js +43 -0
  41. package/lib/components/Renderer/FormBodyPage.js.map +1 -0
  42. package/lib/components/Renderer/FormTopLevelItem.js +7 -0
  43. package/lib/components/Renderer/FormTopLevelItem.js.map +1 -1
  44. package/lib/components/Renderer/FormTopLevelPage.d.ts +9 -0
  45. package/lib/components/Renderer/FormTopLevelPage.js +29 -0
  46. package/lib/components/Renderer/FormTopLevelPage.js.map +1 -0
  47. package/lib/hooks/useDecimalCalculatedExpression.d.ts +2 -2
  48. package/lib/hooks/useNextAndPreviousVisiblePages.d.ts +7 -0
  49. package/lib/hooks/useNextAndPreviousVisiblePages.js +47 -0
  50. package/lib/hooks/useNextAndPreviousVisiblePages.js.map +1 -0
  51. package/lib/hooks/useQuantityCalculatedExpression.d.ts +14 -0
  52. package/lib/hooks/useQuantityCalculatedExpression.js +105 -0
  53. package/lib/hooks/useQuantityCalculatedExpression.js.map +1 -0
  54. package/lib/hooks/useRenderingExtensions.d.ts +2 -1
  55. package/lib/hooks/useRenderingExtensions.js +3 -2
  56. package/lib/hooks/useRenderingExtensions.js.map +1 -1
  57. package/lib/hooks/useStringInput.js +1 -0
  58. package/lib/hooks/useStringInput.js.map +1 -1
  59. package/lib/interfaces/page.interface.d.ts +16 -0
  60. package/lib/interfaces/page.interface.js +2 -0
  61. package/lib/interfaces/page.interface.js.map +1 -0
  62. package/lib/interfaces/questionnaireStore.interface.d.ts +2 -0
  63. package/lib/interfaces/valueSet.interface.d.ts +15 -0
  64. package/lib/stores/questionnaireStore.d.ts +13 -0
  65. package/lib/stores/questionnaireStore.js +19 -3
  66. package/lib/stores/questionnaireStore.js.map +1 -1
  67. package/lib/utils/calculatedExpression.js +4 -1
  68. package/lib/utils/calculatedExpression.js.map +1 -1
  69. package/lib/utils/initialise.d.ts +3 -0
  70. package/lib/utils/initialise.js +6 -1
  71. package/lib/utils/initialise.js.map +1 -1
  72. package/lib/utils/itemControl.d.ts +7 -1
  73. package/lib/utils/itemControl.js +14 -0
  74. package/lib/utils/itemControl.js.map +1 -1
  75. package/lib/utils/page.d.ts +43 -0
  76. package/lib/utils/page.js +101 -0
  77. package/lib/utils/page.js.map +1 -0
  78. package/lib/utils/quantity.d.ts +4 -0
  79. package/lib/utils/quantity.js +49 -0
  80. package/lib/utils/quantity.js.map +1 -0
  81. package/lib/utils/questionnaireStoreUtils/createQuestionaireModel.js +4 -0
  82. package/lib/utils/questionnaireStoreUtils/createQuestionaireModel.js.map +1 -1
  83. package/lib/utils/questionnaireStoreUtils/extractPages.d.ts +3 -0
  84. package/lib/utils/questionnaireStoreUtils/extractPages.js +18 -0
  85. package/lib/utils/questionnaireStoreUtils/extractPages.js.map +1 -0
  86. package/lib/utils/valueSet.d.ts +2 -1
  87. package/lib/utils/valueSet.js +22 -0
  88. package/lib/utils/valueSet.js.map +1 -1
  89. package/package.json +4 -4
  90. package/src/components/FormComponents/Button.styles.ts +10 -0
  91. package/src/components/FormComponents/GroupItem/GroupHeading.tsx +5 -3
  92. package/src/components/FormComponents/GroupItem/GroupItem.tsx +11 -1
  93. package/src/components/FormComponents/GroupItem/GroupItemView.tsx +12 -0
  94. package/src/components/FormComponents/GroupItem/NextPageButton.tsx +37 -0
  95. package/src/components/FormComponents/GroupItem/PageButtonWrapper.tsx +78 -0
  96. package/src/components/FormComponents/GroupItem/PreviousPageButton.tsx +41 -0
  97. package/src/components/FormComponents/QuantityItem/QuantityComparatorField.tsx +40 -0
  98. package/src/components/FormComponents/QuantityItem/QuantityField.tsx +60 -0
  99. package/src/components/FormComponents/QuantityItem/QuantityItem.tsx +286 -0
  100. package/src/components/FormComponents/QuantityItem/QuantityUnitField.tsx +38 -0
  101. package/src/components/FormComponents/SingleItem/SingleItemSwitcher.tsx +2 -1
  102. package/src/components/Renderer/BaseRenderer.tsx +21 -0
  103. package/src/components/Renderer/FormBodyPage.tsx +93 -0
  104. package/src/components/Renderer/FormTopLevelItem.tsx +17 -0
  105. package/src/components/Renderer/FormTopLevelPage.tsx +70 -0
  106. package/src/hooks/useDecimalCalculatedExpression.ts +2 -2
  107. package/src/hooks/useNextAndPreviousVisiblePages.ts +69 -0
  108. package/src/hooks/useQuantityCalculatedExpression.ts +177 -0
  109. package/src/hooks/useRenderingExtensions.ts +5 -2
  110. package/src/hooks/useStringInput.ts +1 -0
  111. package/src/interfaces/page.interface.ts +13 -0
  112. package/src/interfaces/questionnaireStore.interface.ts +2 -0
  113. package/src/interfaces/valueSet.interface.ts +19 -0
  114. package/src/stores/questionnaireStore.ts +33 -2
  115. package/src/stories/assets/questionnaires/AuCoreTestingJson/AuCoreAllergyIntolerance.json +1 -1
  116. package/src/stories/assets/questionnaires/AuCoreTestingJson/AuCoreCondition.json +1 -1
  117. package/src/stories/assets/questionnaires/AuCoreTestingJson/AuCoreEncounter.json +137 -58
  118. package/src/stories/assets/questionnaires/AuCoreTestingJson/AuCoreImmunization.json +175 -0
  119. package/src/stories/assets/questionnaires/AuCoreTestingJson/AuCoreMedicationRequest.json +229 -0
  120. package/src/stories/assets/questionnaires/AuCoreTestingJson/AuCoreObservationBP.json +359 -0
  121. package/src/stories/assets/questionnaires/AuCoreTestingJson/AuCoreObservationBodyHeight.json +195 -0
  122. package/src/stories/assets/questionnaires/AuCoreTestingJson/AuCoreObservationBodyWeight.json +195 -0
  123. package/src/stories/assets/questionnaires/AuCoreTestingJson/AuCoreObservationHeartRate.json +195 -0
  124. package/src/stories/assets/questionnaires/AuCoreTestingJson/AuCoreObservationSmokingStatus.json +174 -0
  125. package/src/stories/assets/questionnaires/AuCoreTestingJson/AuCorePatient.json +495 -0
  126. package/src/stories/assets/questionnaires/AuCoreTestingJson/AuCorePractitioner.json +139 -0
  127. package/src/stories/assets/questionnaires/AuCoreTestingJson/AuCorePractitionerRole.json +216 -0
  128. package/src/stories/assets/questionnaires/AuCoreTestingJson/AuCoreProcedure.json +199 -0
  129. package/src/stories/assets/questionnaires/AuCoreTestingJson/AuCoreRespirationRate.json +195 -0
  130. package/src/stories/assets/questionnaires/AuCoreTestingJson/AuCoreWaistCircumference.json +195 -0
  131. package/src/stories/assets/questionnaires/QAuCoreTesting.ts +3342 -1
  132. package/src/stories/assets/questionnaires/QItemControlGroup.ts +673 -0
  133. package/src/stories/assets/questionnaires/QPrePopTester.ts +30 -0
  134. package/src/stories/assets/questionnaires/QQuantity.ts +283 -1
  135. package/src/stories/itemTypes/Quantity.stories.tsx +33 -1
  136. package/src/stories/sdc/ItemControlGroup.stories.tsx +22 -1
  137. package/src/stories/testing/AuCoreTester.stories.tsx +140 -1
  138. package/src/utils/calculatedExpression.ts +5 -1
  139. package/src/utils/initialise.ts +11 -0
  140. package/src/utils/itemControl.ts +19 -1
  141. package/src/utils/page.ts +134 -0
  142. package/src/utils/quantity.ts +62 -0
  143. package/src/utils/questionnaireStoreUtils/createQuestionaireModel.ts +5 -0
  144. package/src/utils/questionnaireStoreUtils/extractPages.ts +24 -0
  145. package/src/utils/valueSet.ts +32 -1
  146. package/lib/components/FormComponents/DateTimeItem/DateTimeField.d.ts +0 -12
  147. package/lib/components/FormComponents/DateTimeItem/DateTimeField.js +0 -34
  148. package/lib/components/FormComponents/DateTimeItem/DateTimeField.js.map +0 -1
  149. package/lib/components/FormComponents/DateTimeItem/DateTimeItem.js +0 -60
  150. package/lib/components/FormComponents/DateTimeItem/DateTimeItem.js.map +0 -1
  151. package/lib/hooks/useDisplayCalculatedExpression.d.ts +0 -3
  152. package/lib/hooks/useDisplayCalculatedExpression.js +0 -40
  153. package/lib/hooks/useDisplayCalculatedExpression.js.map +0 -1
  154. package/lib/hooks/useInitialiseRenderer.d.ts +0 -4
  155. package/lib/hooks/useInitialiseRenderer.js +0 -85
  156. package/lib/hooks/useInitialiseRenderer.js.map +0 -1
  157. package/lib/hooks/useQueryClient.d.ts +0 -3
  158. package/lib/hooks/useQueryClient.js.map +0 -1
  159. package/lib/utils/buildForm.d.ts +0 -8
  160. package/lib/utils/buildForm.js +0 -26
  161. package/lib/utils/buildForm.js.map +0 -1
  162. package/stats.html +0 -4842
  163. package/stats1.html +0 -4842
  164. package/stats3.html +0 -4842
@@ -0,0 +1,78 @@
1
+ import React, { memo } from 'react';
2
+ import Box from '@mui/material/Box';
3
+ import Typography from '@mui/material/Typography';
4
+ import type { Pages } from '../../../interfaces/page.interface';
5
+ import { useQuestionnaireStore } from '../../../stores';
6
+ import NextPageButton from './NextPageButton';
7
+ import PreviousPageButton from './PreviousPageButton';
8
+ import useNextAndPreviousVisiblePages from '../../../hooks/useNextAndPreviousVisiblePages';
9
+
10
+ interface PageButtonsWrapperProps {
11
+ currentPageIndex?: number;
12
+ pages?: Pages;
13
+ }
14
+
15
+ const PageButtonsWrapper = memo(function PageButtonsWrapper(props: PageButtonsWrapperProps) {
16
+ const { currentPageIndex, pages } = props;
17
+
18
+ const switchPage = useQuestionnaireStore.use.switchPage();
19
+
20
+ const { previousPageIndex, nextPageIndex, numOfVisiblePages } = useNextAndPreviousVisiblePages(
21
+ currentPageIndex,
22
+ pages
23
+ );
24
+
25
+ const pagesNotDefined = currentPageIndex === undefined || pages === undefined;
26
+
27
+ // Event handlers
28
+ function handlePreviousPageButtonClick() {
29
+ if (previousPageIndex === null) {
30
+ return;
31
+ }
32
+
33
+ switchPage(previousPageIndex);
34
+
35
+ // Scroll to top of page
36
+ window.scrollTo(0, 0);
37
+ }
38
+
39
+ function handleNextPageButtonClick() {
40
+ if (nextPageIndex === null) {
41
+ return;
42
+ }
43
+
44
+ switchPage(nextPageIndex);
45
+
46
+ // Scroll to top of page
47
+ window.scrollTo(0, 0);
48
+ }
49
+
50
+ if (pagesNotDefined) {
51
+ return null;
52
+ }
53
+
54
+ const previousPageButtonHidden = previousPageIndex === null;
55
+ const nextPageButtonHidden = nextPageIndex === null;
56
+
57
+ // This is more of a fallback check to prevent the user from navigating to an invisble page if buttons are visble for some reason
58
+ const pageButtonsDisabled = numOfVisiblePages <= 1;
59
+
60
+ return (
61
+ <Box display="flex" mt={3} gap={2} alignItems="center">
62
+ <Box flexGrow={1} />
63
+ <Typography variant="subtitle2" color="text.secondary">
64
+ Page {`${currentPageIndex + 1} / ${numOfVisiblePages}`}
65
+ </Typography>
66
+ <PreviousPageButton
67
+ isDisabled={pageButtonsDisabled || previousPageButtonHidden}
68
+ onPreviousPageClick={handlePreviousPageButtonClick}
69
+ />
70
+ <NextPageButton
71
+ isDisabled={pageButtonsDisabled || nextPageButtonHidden}
72
+ onNextPageClick={handleNextPageButtonClick}
73
+ />
74
+ </Box>
75
+ );
76
+ });
77
+
78
+ export default PageButtonsWrapper;
@@ -0,0 +1,41 @@
1
+ /*
2
+ * Copyright 2024 Commonwealth Scientific and Industrial Research
3
+ * Organisation (CSIRO) ABN 41 687 119 230.
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+
18
+ import React from 'react';
19
+ import Iconify from '../../Iconify/Iconify';
20
+ import { SecondaryFab } from '../Button.styles';
21
+
22
+ interface PreviousPageButtonProps {
23
+ isDisabled: boolean;
24
+ onPreviousPageClick: () => void;
25
+ }
26
+
27
+ function PreviousPageButton(props: PreviousPageButtonProps) {
28
+ const { isDisabled, onPreviousPageClick } = props;
29
+
30
+ return (
31
+ <SecondaryFab
32
+ size="small"
33
+ aria-label="back"
34
+ disabled={isDisabled}
35
+ onClick={onPreviousPageClick}>
36
+ <Iconify icon="material-symbols:chevron-left-rounded" />
37
+ </SecondaryFab>
38
+ );
39
+ }
40
+
41
+ export default PreviousPageButton;
@@ -0,0 +1,40 @@
1
+ import React from 'react';
2
+ import Autocomplete from '@mui/material/Autocomplete';
3
+ import type { PropsWithIsTabledAttribute } from '../../../interfaces/renderProps.interface';
4
+ import MuiTextField from '../TextItem/MuiTextField';
5
+ import Box from '@mui/material/Box';
6
+ import Typography from '@mui/material/Typography';
7
+ import type { Quantity } from 'fhir/r4';
8
+
9
+ interface QuantityComparatorFieldProps extends PropsWithIsTabledAttribute {
10
+ linkId: string;
11
+ options: Quantity['comparator'][];
12
+ valueSelect: Quantity['comparator'] | null;
13
+ readOnly: boolean;
14
+ onChange: (newValue: Quantity['comparator'] | null) => void;
15
+ }
16
+
17
+ function QuantityComparatorField(props: QuantityComparatorFieldProps) {
18
+ const { linkId, options, valueSelect, readOnly, onChange } = props;
19
+
20
+ return (
21
+ <Box>
22
+ <Autocomplete
23
+ id={linkId + '-comparator'}
24
+ value={valueSelect ?? null}
25
+ options={options}
26
+ onChange={(_, newValue) => onChange(newValue as Quantity['comparator'])}
27
+ autoHighlight
28
+ sx={{ width: 88 }}
29
+ disabled={readOnly}
30
+ size="small"
31
+ renderInput={(params) => <MuiTextField sx={{ width: 88 }} {...params} />}
32
+ />
33
+ <Typography variant="caption" color="text.secondary">
34
+ Symbol (optional)
35
+ </Typography>
36
+ </Box>
37
+ );
38
+ }
39
+
40
+ export default QuantityComparatorField;
@@ -0,0 +1,60 @@
1
+ import React from 'react';
2
+ import InputAdornment from '@mui/material/InputAdornment';
3
+ import FadingCheckIcon from '../ItemParts/FadingCheckIcon';
4
+ import { StandardTextField } from '../Textfield.styles';
5
+ import type { PropsWithIsTabledAttribute } from '../../../interfaces/renderProps.interface';
6
+
7
+ interface QuantityFieldProps extends PropsWithIsTabledAttribute {
8
+ linkId: string;
9
+ input: string;
10
+ feedback: string;
11
+ displayPrompt: string;
12
+ displayUnit: string;
13
+ entryFormat: string;
14
+ readOnly: boolean;
15
+ calcExpUpdated: boolean;
16
+ onInputChange: (value: string) => void;
17
+ }
18
+
19
+ function QuantityField(props: QuantityFieldProps) {
20
+ const {
21
+ linkId,
22
+ input,
23
+ feedback,
24
+ displayPrompt,
25
+ displayUnit,
26
+ entryFormat,
27
+ readOnly,
28
+ calcExpUpdated,
29
+ isTabled,
30
+ onInputChange
31
+ } = props;
32
+
33
+ return (
34
+ <StandardTextField
35
+ id={linkId}
36
+ value={input}
37
+ error={!!feedback}
38
+ onChange={(event) => onInputChange(event.target.value)}
39
+ disabled={readOnly}
40
+ label={displayPrompt}
41
+ placeholder={entryFormat === '' ? '0.0' : entryFormat}
42
+ fullWidth
43
+ isTabled={isTabled}
44
+ size="small"
45
+ inputProps={{ inputMode: 'numeric', pattern: '[0-9]*' }}
46
+ InputProps={{
47
+ endAdornment: (
48
+ <InputAdornment position={'end'}>
49
+ <FadingCheckIcon fadeIn={calcExpUpdated} disabled={readOnly} />
50
+ {displayUnit}
51
+ </InputAdornment>
52
+ )
53
+ }}
54
+ helperText={feedback}
55
+ data-test="q-item-quantity-field"
56
+ />
57
+ );
58
+ }
59
+
60
+ export default QuantityField;
@@ -0,0 +1,286 @@
1
+ import React, { useCallback, useMemo, useState } from 'react';
2
+ import type {
3
+ PropsWithIsRepeatedAttribute,
4
+ PropsWithIsTabledAttribute,
5
+ PropsWithParentIsReadOnlyAttribute,
6
+ PropsWithQrItemChangeHandler
7
+ } from '../../../interfaces/renderProps.interface';
8
+ import type {
9
+ Quantity,
10
+ QuestionnaireItem,
11
+ QuestionnaireItemAnswerOption,
12
+ QuestionnaireResponseItem
13
+ } from 'fhir/r4';
14
+ import useRenderingExtensions from '../../../hooks/useRenderingExtensions';
15
+ import { FullWidthFormComponentBox } from '../../Box.styles';
16
+ import useValidationFeedback from '../../../hooks/useValidationFeedback';
17
+ import debounce from 'lodash.debounce';
18
+ import { DEBOUNCE_DURATION } from '../../../utils/debounce';
19
+ import { createEmptyQrItem } from '../../../utils/qrItem';
20
+ import ItemFieldGrid from '../ItemParts/ItemFieldGrid';
21
+ import { parseDecimalStringWithPrecision } from '../../../utils/parseInputs';
22
+ import { getDecimalPrecision } from '../../../utils/itemControl';
23
+ import useStringInput from '../../../hooks/useStringInput';
24
+ import useReadOnly from '../../../hooks/useReadOnly';
25
+ import { useQuestionnaireStore } from '../../../stores';
26
+ import Box from '@mui/material/Box';
27
+ import QuantityField from './QuantityField';
28
+ import QuantityUnitField from './QuantityUnitField';
29
+ import {
30
+ createQuantityItemAnswer,
31
+ quantityComparators,
32
+ stringIsComparator
33
+ } from '../../../utils/quantity';
34
+ import QuantityComparatorField from './QuantityComparatorField';
35
+ import useQuantityCalculatedExpression from '../../../hooks/useQuantityCalculatedExpression';
36
+
37
+ interface QuantityItemProps
38
+ extends PropsWithQrItemChangeHandler,
39
+ PropsWithIsRepeatedAttribute,
40
+ PropsWithIsTabledAttribute,
41
+ PropsWithParentIsReadOnlyAttribute {
42
+ qItem: QuestionnaireItem;
43
+ qrItem: QuestionnaireResponseItem | null;
44
+ }
45
+
46
+ function QuantityItem(props: QuantityItemProps) {
47
+ const { qItem, qrItem, isRepeated, isTabled, parentIsReadOnly, onQrItemChange } = props;
48
+
49
+ const onFocusLinkId = useQuestionnaireStore.use.onFocusLinkId();
50
+
51
+ const readOnly = useReadOnly(qItem, parentIsReadOnly);
52
+ const precision = getDecimalPrecision(qItem);
53
+ const { displayUnit, displayPrompt, entryFormat, quantityUnit } = useRenderingExtensions(qItem);
54
+
55
+ // Get units options if present
56
+ const unitOptions = useMemo(
57
+ () =>
58
+ qItem.extension?.filter(
59
+ (f) => f.url === 'http://hl7.org/fhir/StructureDefinition/questionnaire-unitOption'
60
+ ) ?? [],
61
+ [qItem]
62
+ );
63
+
64
+ // Init inputs
65
+ let valueQuantity: Quantity = {};
66
+ let initialValueInput = '';
67
+ let initialComparatorInput: Quantity['comparator'] | null = null;
68
+ let initialUnitInput: QuestionnaireItemAnswerOption | null =
69
+ quantityUnit ?? unitOptions?.at(0) ?? null;
70
+ if (qrItem?.answer) {
71
+ if (qrItem?.answer[0].valueQuantity) {
72
+ valueQuantity = qrItem.answer[0].valueQuantity;
73
+ }
74
+
75
+ initialValueInput =
76
+ (precision ? valueQuantity.value?.toFixed(precision) : valueQuantity.value?.toString()) || '';
77
+
78
+ if (valueQuantity.comparator && stringIsComparator(valueQuantity.comparator)) {
79
+ initialComparatorInput = valueQuantity.comparator;
80
+ }
81
+
82
+ if (valueQuantity.code && valueQuantity.system) {
83
+ initialUnitInput = {
84
+ valueCoding: {
85
+ code: valueQuantity.code,
86
+ system: valueQuantity.system,
87
+ display: valueQuantity.unit
88
+ }
89
+ };
90
+ }
91
+ }
92
+
93
+ // input states
94
+ const [valueInput, setValueInput] = useStringInput(initialValueInput);
95
+ const [comparatorInput, setComparatorInput] = useState<Quantity['comparator'] | null>(
96
+ initialComparatorInput
97
+ );
98
+ const [unitInput, setUnitInput] = useState<QuestionnaireItemAnswerOption | null>(
99
+ initialUnitInput
100
+ );
101
+
102
+ // Perform validation checks
103
+ const feedback = useValidationFeedback(qItem, valueInput);
104
+
105
+ // Process calculated expressions
106
+ const { calcExpUpdated } = useQuantityCalculatedExpression({
107
+ qItem: qItem,
108
+ inputValue: valueInput,
109
+ precision: precision,
110
+ onChangeByCalcExpressionDecimal: (newValueDecimal: number) => {
111
+ setValueInput(
112
+ typeof precision === 'number'
113
+ ? newValueDecimal.toFixed(precision)
114
+ : newValueDecimal.toString()
115
+ );
116
+ onQrItemChange({
117
+ ...createEmptyQrItem(qItem),
118
+ answer: [
119
+ {
120
+ valueQuantity: {
121
+ value: newValueDecimal,
122
+ unit: unitInput?.valueCoding?.display,
123
+ system: unitInput?.valueCoding?.system,
124
+ code: unitInput?.valueCoding?.code
125
+ }
126
+ }
127
+ ]
128
+ });
129
+ },
130
+ onChangeByCalcExpressionQuantity: (
131
+ newValueDecimal: number,
132
+ newUnitSystem,
133
+ newUnitCode,
134
+ newUnitDisplay
135
+ ) => {
136
+ setValueInput(
137
+ typeof precision === 'number'
138
+ ? newValueDecimal.toFixed(precision)
139
+ : newValueDecimal.toString()
140
+ );
141
+ onQrItemChange({
142
+ ...createEmptyQrItem(qItem),
143
+ answer: [
144
+ {
145
+ valueQuantity: {
146
+ value: newValueDecimal,
147
+ unit: newUnitDisplay,
148
+ system: newUnitSystem,
149
+ code: newUnitCode
150
+ }
151
+ }
152
+ ]
153
+ });
154
+ },
155
+ onChangeByCalcExpressionNull: () => {
156
+ setValueInput('');
157
+ onQrItemChange(createEmptyQrItem(qItem));
158
+ }
159
+ });
160
+
161
+ // Event handlers
162
+ function handleComparatorInputChange(newComparatorInput: Quantity['comparator'] | null) {
163
+ setComparatorInput(newComparatorInput);
164
+
165
+ if (!valueInput) return;
166
+
167
+ onQrItemChange({
168
+ ...createEmptyQrItem(qItem),
169
+ answer: createQuantityItemAnswer(precision, valueInput, newComparatorInput, unitInput)
170
+ });
171
+ }
172
+
173
+ function handleUnitInputChange(newUnitInput: QuestionnaireItemAnswerOption | null) {
174
+ setUnitInput(newUnitInput);
175
+
176
+ if (!valueInput) return;
177
+
178
+ onQrItemChange({
179
+ ...createEmptyQrItem(qItem),
180
+ answer: createQuantityItemAnswer(precision, valueInput, comparatorInput, newUnitInput)
181
+ });
182
+ }
183
+
184
+ function handleValueInputChange(newInput: string) {
185
+ const parsedNewInput: string = parseDecimalStringWithPrecision(newInput, precision);
186
+
187
+ setValueInput(parsedNewInput);
188
+ updateQrItemWithDebounce(parsedNewInput);
189
+ }
190
+
191
+ // eslint-disable-next-line react-hooks/exhaustive-deps
192
+ const updateQrItemWithDebounce = useCallback(
193
+ debounce((parsedNewInput: string) => {
194
+ if (parsedNewInput === '') {
195
+ onQrItemChange(createEmptyQrItem(qItem));
196
+ } else {
197
+ onQrItemChange({
198
+ ...createEmptyQrItem(qItem),
199
+ answer: createQuantityItemAnswer(precision, parsedNewInput, comparatorInput, unitInput)
200
+ });
201
+ }
202
+ }, DEBOUNCE_DURATION),
203
+ [onQrItemChange, qItem, displayUnit, precision, comparatorInput, unitInput]
204
+ ); // Dependencies are tested, debounce is causing eslint to not recognise dependencies
205
+
206
+ if (isRepeated) {
207
+ return (
208
+ <Box data-test="q-item-quantity-box" display="flex" gap={1}>
209
+ <QuantityComparatorField
210
+ linkId={qItem.linkId}
211
+ options={quantityComparators}
212
+ valueSelect={comparatorInput}
213
+ readOnly={readOnly}
214
+ isTabled={isTabled}
215
+ onChange={handleComparatorInputChange}
216
+ />
217
+ <QuantityField
218
+ linkId={qItem.linkId}
219
+ input={valueInput}
220
+ feedback={feedback}
221
+ displayPrompt={displayPrompt}
222
+ displayUnit={displayUnit}
223
+ entryFormat={entryFormat}
224
+ readOnly={readOnly}
225
+ calcExpUpdated={calcExpUpdated}
226
+ isTabled={isTabled}
227
+ onInputChange={handleValueInputChange}
228
+ />
229
+ {unitOptions.length > 0 ? (
230
+ <QuantityUnitField
231
+ linkId={qItem.linkId}
232
+ options={unitOptions}
233
+ valueSelect={unitInput}
234
+ readOnly={readOnly}
235
+ isTabled={isTabled}
236
+ onChange={handleUnitInputChange}
237
+ />
238
+ ) : null}
239
+ </Box>
240
+ );
241
+ }
242
+
243
+ return (
244
+ <FullWidthFormComponentBox
245
+ data-test="q-item-quantity-box"
246
+ data-linkid={qItem.linkId}
247
+ onClick={() => onFocusLinkId(qItem.linkId)}>
248
+ <ItemFieldGrid qItem={qItem} readOnly={readOnly}>
249
+ <Box display="flex" gap={1}>
250
+ <QuantityComparatorField
251
+ linkId={qItem.linkId}
252
+ options={quantityComparators}
253
+ valueSelect={comparatorInput}
254
+ readOnly={readOnly}
255
+ isTabled={isTabled}
256
+ onChange={handleComparatorInputChange}
257
+ />
258
+ <QuantityField
259
+ linkId={qItem.linkId}
260
+ input={valueInput}
261
+ feedback={feedback}
262
+ displayPrompt={displayPrompt}
263
+ displayUnit={displayUnit}
264
+ entryFormat={entryFormat}
265
+ readOnly={readOnly}
266
+ calcExpUpdated={calcExpUpdated}
267
+ isTabled={isTabled}
268
+ onInputChange={handleValueInputChange}
269
+ />
270
+ {unitOptions.length > 0 ? (
271
+ <QuantityUnitField
272
+ linkId={qItem.linkId}
273
+ options={unitOptions}
274
+ valueSelect={unitInput}
275
+ readOnly={readOnly}
276
+ isTabled={isTabled}
277
+ onChange={handleUnitInputChange}
278
+ />
279
+ ) : null}
280
+ </Box>
281
+ </ItemFieldGrid>
282
+ </FullWidthFormComponentBox>
283
+ );
284
+ }
285
+
286
+ export default QuantityItem;
@@ -0,0 +1,38 @@
1
+ import React from 'react';
2
+ import { getAnswerOptionLabel } from '../../../utils/openChoice';
3
+ import { StandardTextField, TEXT_FIELD_WIDTH } from '../Textfield.styles';
4
+ import Autocomplete from '@mui/material/Autocomplete';
5
+ import type { QuestionnaireItemAnswerOption } from 'fhir/r4';
6
+ import type { PropsWithIsTabledAttribute } from '../../../interfaces/renderProps.interface';
7
+
8
+ interface QuantityUnitFieldProps extends PropsWithIsTabledAttribute {
9
+ linkId: string;
10
+ options: QuestionnaireItemAnswerOption[];
11
+ valueSelect: QuestionnaireItemAnswerOption | null;
12
+ readOnly: boolean;
13
+ onChange: (newValue: QuestionnaireItemAnswerOption | null) => void;
14
+ }
15
+
16
+ function QuantityUnitField(props: QuantityUnitFieldProps) {
17
+ const { linkId, options, valueSelect, readOnly, isTabled, onChange } = props;
18
+
19
+ return (
20
+ <Autocomplete
21
+ id={linkId + '-unit'}
22
+ value={valueSelect ?? null}
23
+ isOptionEqualToValue={(option, value) =>
24
+ option.valueCoding?.code === value?.valueCoding?.code
25
+ }
26
+ options={options}
27
+ getOptionLabel={(option) => getAnswerOptionLabel(option)}
28
+ onChange={(_, newValue) => onChange(newValue as QuestionnaireItemAnswerOption | null)}
29
+ autoHighlight
30
+ sx={{ maxWidth: !isTabled ? TEXT_FIELD_WIDTH : 3000, minWidth: 160, flexGrow: 1 }}
31
+ disabled={readOnly}
32
+ size="small"
33
+ renderInput={(params) => <StandardTextField isTabled={isTabled} {...params} />}
34
+ />
35
+ );
36
+ }
37
+
38
+ export default QuantityUnitField;
@@ -40,6 +40,7 @@ import SliderItem from '../SliderItem/SliderItem';
40
40
  import IntegerItem from '../IntegerItem/IntegerItem';
41
41
  import AttachmentItem from '../AttachmentItem/AttachmentItem';
42
42
  import CustomDateTimeItem from '../DateTimeItems/CustomDateTimeItem/CustomDateTimeItem';
43
+ import QuantityItem from '../QuantityItem/QuantityItem';
43
44
 
44
45
  interface SingleItemSwitcherProps
45
46
  extends PropsWithQrItemChangeHandler,
@@ -219,7 +220,7 @@ function SingleItemSwitcher(props: SingleItemSwitcherProps) {
219
220
  case 'quantity':
220
221
  // FIXME quantity item uses the same component as decimal item currently
221
222
  return (
222
- <DecimalItem
223
+ <QuantityItem
223
224
  qItem={qItem}
224
225
  qrItem={qrItem}
225
226
  isRepeated={isRepeated}
@@ -24,7 +24,9 @@ import { useQuestionnaireResponseStore, useQuestionnaireStore } from '../../stor
24
24
  import cloneDeep from 'lodash.clonedeep';
25
25
  import { getQrItemsIndex, mapQItemsIndex } from '../../utils/mapItem';
26
26
  import { updateQrItemsInGroup } from '../../utils/qrItem';
27
+ import { everyIsPages } from '../../utils/page';
27
28
  import type { QrRepeatGroup } from '../../interfaces/repeatGroup.interface';
29
+ import FormTopLevelPage from './FormTopLevelPage';
28
30
 
29
31
  /**
30
32
  * Main component of the form-rendering engine.
@@ -74,6 +76,25 @@ function BaseRenderer() {
74
76
  // If an item has multiple answers, it is a repeat group
75
77
  const topLevelQRItemsByIndex = getQrItemsIndex(topLevelQItems, topLevelQRItems, qItemsIndexMap);
76
78
 
79
+ const everyItemIsPage = everyIsPages(topLevelQItems);
80
+
81
+ if (everyItemIsPage) {
82
+ return (
83
+ <Fade in={true} timeout={500}>
84
+ <Container maxWidth="xl">
85
+ <FormTopLevelPage
86
+ topLevelQItems={topLevelQItems}
87
+ topLevelQRItems={topLevelQRItemsByIndex}
88
+ parentIsReadOnly={readOnly}
89
+ onQrItemChange={(newTopLevelQRItem) =>
90
+ handleTopLevelQRItemSingleChange(newTopLevelQRItem)
91
+ }
92
+ />
93
+ </Container>
94
+ </Fade>
95
+ );
96
+ }
97
+
77
98
  return (
78
99
  <Fade in={true} timeout={500}>
79
100
  <Container maxWidth="xl">
@@ -0,0 +1,93 @@
1
+ import React, { useMemo } from 'react';
2
+ import Grid from '@mui/material/Grid';
3
+ import type { QuestionnaireItem, QuestionnaireResponseItem } from 'fhir/r4';
4
+ import TabContext from '@mui/lab/TabContext';
5
+ import TabPanel from '@mui/lab/TabPanel';
6
+ import GroupItem from '../FormComponents/GroupItem/GroupItem';
7
+ import type {
8
+ PropsWithParentIsReadOnlyAttribute,
9
+ PropsWithQrItemChangeHandler
10
+ } from '../../interfaces/renderProps.interface';
11
+ import { useQuestionnaireStore } from '../../stores';
12
+ import { getQrItemsIndex, mapQItemsIndex } from '../../utils/mapItem';
13
+ import { createEmptyQrGroup, updateQrItemsInGroup } from '../../utils/qrItem';
14
+
15
+ interface FormBodyPageProps
16
+ extends PropsWithQrItemChangeHandler,
17
+ PropsWithParentIsReadOnlyAttribute {
18
+ topLevelQItem: QuestionnaireItem;
19
+ topLevelQRItem: QuestionnaireResponseItem | null;
20
+ }
21
+
22
+ function FormBodyPage(props: FormBodyPageProps) {
23
+ const { topLevelQItem, topLevelQRItem, parentIsReadOnly, onQrItemChange } = props;
24
+
25
+ const pages = useQuestionnaireStore.use.pages();
26
+ const currentPage = useQuestionnaireStore.use.currentPageIndex();
27
+
28
+ const indexMap: Record<string, number> = useMemo(
29
+ () => mapQItemsIndex(topLevelQItem),
30
+ [topLevelQItem]
31
+ );
32
+
33
+ const nonNullTopLevelQRItem = topLevelQRItem ?? createEmptyQrGroup(topLevelQItem);
34
+
35
+ const qItems = topLevelQItem.item;
36
+ const qrItems = nonNullTopLevelQRItem.item;
37
+
38
+ function handleQrGroupChange(qrItem: QuestionnaireResponseItem) {
39
+ updateQrItemsInGroup(qrItem, null, nonNullTopLevelQRItem, indexMap);
40
+ onQrItemChange(nonNullTopLevelQRItem);
41
+ }
42
+
43
+ if (!qItems || !qrItems) {
44
+ return <>Unable to load form</>;
45
+ }
46
+
47
+ const qrItemsByIndex = getQrItemsIndex(qItems, qrItems, indexMap);
48
+
49
+ return (
50
+ <Grid container spacing={1.5}>
51
+ <TabContext value={currentPage.toString()}>
52
+ <Grid item xs={12} md={12} lg={12}>
53
+ {qItems.map((qItem, i) => {
54
+ const qrItem = qrItemsByIndex[i];
55
+
56
+ const isNotRepeatGroup = !Array.isArray(qrItem);
57
+ const isPage = !!pages[qItem.linkId];
58
+
59
+ if (!isPage || !isNotRepeatGroup) {
60
+ // Something has gone horribly wrong
61
+ return null;
62
+ }
63
+
64
+ const isRepeated = qItem.repeats ?? false;
65
+ const pageIsMarkedAsComplete = pages[qItem.linkId].isComplete ?? false;
66
+
67
+ return (
68
+ <TabPanel
69
+ key={qItem.linkId}
70
+ sx={{ p: 0 }}
71
+ value={i.toString()}
72
+ data-test="renderer-page-panel">
73
+ <GroupItem
74
+ qItem={qItem}
75
+ qrItem={qrItem ?? null}
76
+ isRepeated={isRepeated}
77
+ groupCardElevation={1}
78
+ pageIsMarkedAsComplete={pageIsMarkedAsComplete}
79
+ pages={pages}
80
+ currentPageIndex={currentPage}
81
+ parentIsReadOnly={parentIsReadOnly}
82
+ onQrItemChange={handleQrGroupChange}
83
+ />
84
+ </TabPanel>
85
+ );
86
+ })}
87
+ </Grid>
88
+ </TabContext>
89
+ </Grid>
90
+ );
91
+ }
92
+
93
+ export default FormBodyPage;