diff --git a/apps/smart-forms-app/src/features/playground/components/Playground.tsx b/apps/smart-forms-app/src/features/playground/components/Playground.tsx index 0a042cad6..de7469834 100644 --- a/apps/smart-forms-app/src/features/playground/components/Playground.tsx +++ b/apps/smart-forms-app/src/features/playground/components/Playground.tsx @@ -107,7 +107,7 @@ function Playground() { } return ( - + {buildingState === 'built' ? ( diff --git a/apps/smart-forms-app/src/features/renderer/components/FormPage/FormRenderer/FormWrapper.tsx b/apps/smart-forms-app/src/features/renderer/components/FormPage/FormRenderer/FormWrapper.tsx index c2fb19e06..58ccf3bde 100644 --- a/apps/smart-forms-app/src/features/renderer/components/FormPage/FormRenderer/FormWrapper.tsx +++ b/apps/smart-forms-app/src/features/renderer/components/FormPage/FormRenderer/FormWrapper.tsx @@ -38,7 +38,7 @@ function FormWrapper() { return ; } - if (topLevelQItems.length === 0 || topLevelQRItems.length === 0) { + if (topLevelQItems.length === 0) { return ; } diff --git a/package-lock.json b/package-lock.json index 86a19fc5b..6539c0ec4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11231,8 +11231,9 @@ } }, "node_modules/@types/react-redux": { - "version": "7.1.31", - "license": "MIT", + "version": "7.1.33", + "resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.33.tgz", + "integrity": "sha512-NF8m5AjWCkert+fosDsN3hAlHzpjSiXlVy9EgQEmLoBhaNXbmyeGs/aj5dQzKuF+/q+S7JQagorGDW8pJ28Hmg==", "dependencies": { "@types/hoist-non-react-statics": "^3.3.0", "@types/react": "*", @@ -14502,7 +14503,8 @@ }, "node_modules/css-box-model": { "version": "1.2.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/css-box-model/-/css-box-model-1.2.1.tgz", + "integrity": "sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw==", "dependencies": { "tiny-invariant": "^1.0.6" } @@ -20468,7 +20470,8 @@ }, "node_modules/memoize-one": { "version": "5.2.1", - "license": "MIT" + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz", + "integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==" }, "node_modules/memoizerific": { "version": "1.11.3", @@ -22287,7 +22290,8 @@ }, "node_modules/raf-schd": { "version": "4.0.3", - "license": "MIT" + "resolved": "https://registry.npmjs.org/raf-schd/-/raf-schd-4.0.3.tgz", + "integrity": "sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ==" }, "node_modules/ramda": { "version": "0.29.0", @@ -22356,7 +22360,8 @@ }, "node_modules/react-beautiful-dnd": { "version": "13.1.1", - "license": "Apache-2.0", + "resolved": "https://registry.npmjs.org/react-beautiful-dnd/-/react-beautiful-dnd-13.1.1.tgz", + "integrity": "sha512-0Lvs4tq2VcrEjEgDXHjT98r+63drkKEgqyxdA7qD3mvKwga6a5SscbdLPO2IExotU1jW8L0Ksdl0Cj2AF67nPQ==", "dependencies": { "@babel/runtime": "^7.9.2", "css-box-model": "^1.2.0", @@ -22657,7 +22662,8 @@ }, "node_modules/react-redux": { "version": "7.2.9", - "license": "MIT", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.9.tgz", + "integrity": "sha512-Gx4L3uM182jEEayZfRbI/G11ZpYdNAnBs70lFVMNdHJI76XYtR+7m0MN+eAs7UHBPhWXcnFPaS+9owSCJQHNpQ==", "dependencies": { "@babel/runtime": "^7.15.4", "@types/react-redux": "^7.1.20", @@ -22680,7 +22686,8 @@ }, "node_modules/react-redux/node_modules/react-is": { "version": "17.0.2", - "license": "MIT" + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" }, "node_modules/react-refresh": { "version": "0.14.0", @@ -25661,7 +25668,8 @@ }, "node_modules/use-memo-one": { "version": "1.1.3", - "license": "MIT", + "resolved": "https://registry.npmjs.org/use-memo-one/-/use-memo-one-1.1.3.tgz", + "integrity": "sha512-g66/K7ZQGYrI6dy8GLpVcMsBp4s17xNkYJVSMvTEevGy3nDxHOfE6z8BVE22+5G5x7t3+bhzrlTDB7ObrEE0cQ==", "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0" } @@ -26519,6 +26527,8 @@ "lodash.debounce": "^4.0.8", "nanoid": "^5.0.1", "react-beautiful-dnd": "^13.1.1", + "react-dnd": "^16.0.1", + "react-dnd-html5-backend": "^16.0.1", "react-markdown": "^8.0.7", "zustand": "^4.4.6" }, @@ -26604,6 +26614,21 @@ "inline-style-parser": "0.2.2" } }, + "packages/structure-definition-transform": { + "version": "0.1.0", + "extraneous": true, + "license": "Apache-2.0", + "dependencies": { + "fhirpath": "^3.7.1" + }, + "devDependencies": { + "@jest/globals": "^29.3.1", + "@types/fhir": "^0.0.38", + "@types/jest": "^29.5.7", + "jest": "^29.7.0", + "ts-jest": "^29.1.1" + } + }, "services/assemble-express": { "version": "1.2.0", "license": "ISC", diff --git a/packages/smart-forms-renderer/package.json b/packages/smart-forms-renderer/package.json index 6a51e3c92..e5243cfab 100644 --- a/packages/smart-forms-renderer/package.json +++ b/packages/smart-forms-renderer/package.json @@ -35,6 +35,8 @@ "lodash.debounce": "^4.0.8", "nanoid": "^5.0.1", "react-beautiful-dnd": "^13.1.1", + "react-dnd": "^16.0.1", + "react-dnd-html5-backend": "^16.0.1", "react-markdown": "^8.0.7", "zustand": "^4.4.6" }, diff --git a/packages/smart-forms-renderer/src/components/FormComponents/AttachmentItem/AttachmentField.tsx b/packages/smart-forms-renderer/src/components/FormComponents/AttachmentItem/AttachmentField.tsx new file mode 100644 index 000000000..d31c00bc0 --- /dev/null +++ b/packages/smart-forms-renderer/src/components/FormComponents/AttachmentItem/AttachmentField.tsx @@ -0,0 +1,96 @@ +/* + * Copyright 2023 Commonwealth Scientific and Industrial Research + * Organisation (CSIRO) ABN 41 687 119 230. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; +import type { PropsWithIsTabledAttribute } from '../../../interfaces/renderProps.interface'; +import { StandardTextField } from '../Textfield.styles'; +import AttachmentFileCollector from './AttachmentFileCollector'; +import Typography from '@mui/material/Typography'; +import Box from '@mui/material/Box'; +import Stack from '@mui/material/Stack'; +import type { AttachmentValues } from './AttachmentItem'; +import AttachmentUrlField from './AttachmentUrlField'; + +interface AttachmentFieldProps extends PropsWithIsTabledAttribute { + linkId: string; + attachmentValues: AttachmentValues; + readOnly: boolean; + onUploadFile: (file: File | null) => void; + onUrlChange: (url: string) => void; + onFileNameChange: (fileName: string) => void; +} + +function AttachmentField(props: AttachmentFieldProps) { + const { + linkId, + attachmentValues, + readOnly, + isTabled, + onUploadFile, + onUrlChange, + onFileNameChange + } = props; + + const { uploadedFile, url, fileName } = attachmentValues; + + return ( + <> + + + An attachment must either have a file or a URL, or both. + + + + + + + + + File name (optional) + onFileNameChange(event.target.value)} + disabled={readOnly} + size="small" + data-test="q-item-attachment-field" + /> + + + {uploadedFile && url ? ( + + Ensure that the attached file and URL has the same content. + + ) : null} + + + ); +} + +export default AttachmentField; diff --git a/packages/smart-forms-renderer/src/components/FormComponents/AttachmentItem/AttachmentFieldWrapper.tsx b/packages/smart-forms-renderer/src/components/FormComponents/AttachmentItem/AttachmentFieldWrapper.tsx new file mode 100644 index 000000000..15bfa4fb5 --- /dev/null +++ b/packages/smart-forms-renderer/src/components/FormComponents/AttachmentItem/AttachmentFieldWrapper.tsx @@ -0,0 +1,87 @@ +/* + * Copyright 2023 Commonwealth Scientific and Industrial Research + * Organisation (CSIRO) ABN 41 687 119 230. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// import { HTML5Backend } from 'react-dnd-html5-backend'; +// +// + +import React from 'react'; +import AttachmentField from './AttachmentField'; +import { FullWidthFormComponentBox } from '../../Box.styles'; +import ItemFieldGrid from '../ItemParts/ItemFieldGrid'; +import type { + PropsWithIsRepeatedAttribute, + PropsWithIsTabledAttribute +} from '../../../interfaces/renderProps.interface'; +import type { QuestionnaireItem } from 'fhir/r4'; +import type { AttachmentValues } from './AttachmentItem'; + +interface AttachmentFieldWrapperProps + extends PropsWithIsRepeatedAttribute, + PropsWithIsTabledAttribute { + qItem: QuestionnaireItem; + attachmentValues: AttachmentValues; + readOnly: boolean; + onUploadFile: (file: File | null) => void; + onUrlChange: (url: string) => void; + onFileNameChange: (fileName: string) => void; +} + +function AttachmentFieldWrapper(props: AttachmentFieldWrapperProps) { + const { + qItem, + attachmentValues, + readOnly, + isRepeated, + isTabled, + onUploadFile, + onUrlChange, + onFileNameChange + } = props; + + if (isRepeated) { + return ( + + ); + } + + return ( + + + + + + ); +} + +export default AttachmentFieldWrapper; diff --git a/packages/smart-forms-renderer/src/components/FormComponents/AttachmentItem/AttachmentFileCollector.tsx b/packages/smart-forms-renderer/src/components/FormComponents/AttachmentItem/AttachmentFileCollector.tsx new file mode 100644 index 000000000..047a75437 --- /dev/null +++ b/packages/smart-forms-renderer/src/components/FormComponents/AttachmentItem/AttachmentFileCollector.tsx @@ -0,0 +1,101 @@ +/* + * Copyright 2024 Commonwealth Scientific and Industrial Research + * Organisation (CSIRO) ABN 41 687 119 230. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { ChangeEvent } from 'react'; +import React, { memo, useCallback } from 'react'; +import { Box, IconButton, Stack, Tooltip } from '@mui/material'; +import { useSnackbar } from 'notistack'; +import AttachmentFileDropBox from './AttachmentFileDropBox'; +import Iconify from '../../Iconify/Iconify'; +import type { PropsWithIsTabledAttribute } from '../../../interfaces/renderProps.interface'; + +interface AttachmentFileCollectorProps extends PropsWithIsTabledAttribute { + uploadedFile: File | null; + onUploadFile: (file: File | null) => void; +} + +const AttachmentFileCollector = memo(function AttachmentFileCollector( + props: AttachmentFileCollectorProps +) { + const { uploadedFile, isTabled, onUploadFile } = props; + + const { enqueueSnackbar } = useSnackbar(); + + const handleFileDrop = useCallback( + (item: { files: any[] }) => { + if (item) { + const files = item.files; + + if (files.length > 1) { + enqueueSnackbar('Only one file allowed', { + variant: 'warning', + preventDuplicate: true + }); + } + + if (files[0] instanceof File) { + const file = files[0]; + + onUploadFile(file); + } + } + }, + [onUploadFile, enqueueSnackbar] + ); + + function handleAttachFile(event: ChangeEvent) { + const file = event.target.files?.[0]; + + if (file instanceof File) { + onUploadFile(file); + } + } + + return ( + <> + + + + + + + + + + + + onUploadFile(null)}> + + + + + + + + ); +}); + +export default AttachmentFileCollector; diff --git a/packages/smart-forms-renderer/src/components/FormComponents/AttachmentItem/AttachmentFileDropBox.styles.ts b/packages/smart-forms-renderer/src/components/FormComponents/AttachmentItem/AttachmentFileDropBox.styles.ts new file mode 100644 index 000000000..ff3813356 --- /dev/null +++ b/packages/smart-forms-renderer/src/components/FormComponents/AttachmentItem/AttachmentFileDropBox.styles.ts @@ -0,0 +1,31 @@ +/* + * Copyright 2024 Commonwealth Scientific and Industrial Research + * Organisation (CSIRO) ABN 41 687 119 230. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { styled } from '@mui/material/styles'; +import { Box } from '@mui/material'; +import { TEXT_FIELD_WIDTH } from '../Textfield.styles'; + +export const AttachmentFileDropBoxWrapper = styled(Box, { + shouldForwardProp: (prop) => prop !== 'isActive' && prop !== 'isTabled' +})<{ isActive: boolean; isTabled: boolean }>(({ theme, isActive, isTabled }) => ({ + backgroundColor: theme.palette.background.paper, + border: '2px dashed', + borderColor: isActive ? theme.palette.secondary.main : theme.palette.primary.main, + borderRadius: '4px', + maxWidth: !isTabled ? TEXT_FIELD_WIDTH : 3000, + minWidth: 160 +})); diff --git a/packages/smart-forms-renderer/src/components/FormComponents/AttachmentItem/AttachmentFileDropBox.tsx b/packages/smart-forms-renderer/src/components/FormComponents/AttachmentItem/AttachmentFileDropBox.tsx new file mode 100644 index 000000000..0b4465f1f --- /dev/null +++ b/packages/smart-forms-renderer/src/components/FormComponents/AttachmentItem/AttachmentFileDropBox.tsx @@ -0,0 +1,66 @@ +/* + * Copyright 2023 Commonwealth Scientific and Industrial Research + * Organisation (CSIRO) ABN 41 687 119 230. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Box, Typography } from '@mui/material'; +import useFileDrop from '../../../hooks/UseFileDrop'; +import { AttachmentFileDropBoxWrapper } from './AttachmentFileDropBox.styles'; +import React from 'react'; +import type { PropsWithIsTabledAttribute } from '../../../interfaces/renderProps.interface'; +import { getFileSize } from '../../../utils/fileUtils'; + +export interface AttachmentFileDropBoxProps extends PropsWithIsTabledAttribute { + file: File | null; + onDrop: (item: { files: any[] }) => void; + errorMessage: string; +} + +function AttachmentFileDropBox(props: AttachmentFileDropBoxProps) { + const { file, onDrop, errorMessage, isTabled } = props; + + const { canDrop, isOver, dropTarget } = useFileDrop(onDrop); + + const isActive = canDrop && isOver; + + let boxMessage = 'No file selected'; + if (isActive) { + boxMessage = 'Release to drop file'; + } else if (errorMessage) { + boxMessage = errorMessage; + } else if (file) { + boxMessage = file.name; + } + + return ( + + + {boxMessage} + {file ? ( + + Size: {getFileSize(file.size.toString() ?? '0')} + Type: {file.type} + + ) : null} + + + ); +} + +export default AttachmentFileDropBox; diff --git a/packages/smart-forms-renderer/src/components/FormComponents/AttachmentItem/AttachmentItem.tsx b/packages/smart-forms-renderer/src/components/FormComponents/AttachmentItem/AttachmentItem.tsx new file mode 100644 index 000000000..c3b3c07bf --- /dev/null +++ b/packages/smart-forms-renderer/src/components/FormComponents/AttachmentItem/AttachmentItem.tsx @@ -0,0 +1,123 @@ +/* + * Copyright 2023 Commonwealth Scientific and Industrial Research + * Organisation (CSIRO) ABN 41 687 119 230. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { useCallback, useState } from 'react'; +import type { + PropsWithIsRepeatedAttribute, + PropsWithIsTabledAttribute, + PropsWithParentIsReadOnlyAttribute, + PropsWithQrItemChangeHandler +} from '../../../interfaces/renderProps.interface'; +import type { QuestionnaireItem, QuestionnaireResponseItem } from 'fhir/r4'; +import debounce from 'lodash.debounce'; +import { createEmptyQrItem } from '../../../utils/qrItem'; +import { DEBOUNCE_DURATION } from '../../../utils/debounce'; +import useStringInput from '../../../hooks/useStringInput'; +import useReadOnly from '../../../hooks/useReadOnly'; +import AttachmentFieldWrapper from './AttachmentFieldWrapper'; +import { HTML5Backend } from 'react-dnd-html5-backend'; +import { DndProvider } from 'react-dnd'; +import { createAttachmentAnswer } from '../../../utils/fileUtils'; + +export interface AttachmentValues { + uploadedFile: File | null; + url: string; + fileName: string; +} + +interface AttachmentItemProps + extends PropsWithQrItemChangeHandler, + PropsWithIsRepeatedAttribute, + PropsWithIsTabledAttribute, + PropsWithParentIsReadOnlyAttribute { + qItem: QuestionnaireItem; + qrItem: QuestionnaireResponseItem | null; +} + +function AttachmentItem(props: AttachmentItemProps) { + const { qItem, qrItem, isRepeated, isTabled, parentIsReadOnly, onQrItemChange } = props; + + const readOnly = useReadOnly(qItem, parentIsReadOnly); + + // Init input value + let valueString = ''; + if (qrItem?.answer && qrItem?.answer[0].valueString) { + valueString = qrItem.answer[0].valueString; + } + const [uploadedFile, setUploadedFile] = useState(null); + const [url, setUrl] = useStringInput(valueString); + const [fileName, setFileName] = useStringInput(valueString); + + // Event handlers + async function handleUploadFile(newUploadedFile: File | null) { + setUploadedFile(newUploadedFile); + + const attachment = await createAttachmentAnswer(newUploadedFile, url, fileName); + if (attachment) { + onQrItemChange({ + ...createEmptyQrItem(qItem), + answer: [{ valueAttachment: attachment }] + }); + } else { + onQrItemChange(createEmptyQrItem(qItem)); + } + } + + async function handleUrlChange(newUrl: string) { + setUrl(newUrl); + await updateQrItemWithDebounce(uploadedFile, newUrl, fileName); + } + + async function handleFileNameChange(newFileName: string) { + setFileName(newFileName); + await updateQrItemWithDebounce(uploadedFile, url, newFileName); + } + + // eslint-disable-next-line react-hooks/exhaustive-deps + const updateQrItemWithDebounce = useCallback( + debounce(async (file: File | null, url: string, fileName: string) => { + const attachment = await createAttachmentAnswer(file, url, fileName); + + if (attachment) { + onQrItemChange({ + ...createEmptyQrItem(qItem), + answer: [{ valueAttachment: attachment }] + }); + } else { + onQrItemChange(createEmptyQrItem(qItem)); + } + }, DEBOUNCE_DURATION), + [onQrItemChange, qItem] + ); // Dependencies are tested, debounce is causing eslint to not recognise dependencies + + return ( + + + + ); +} + +export default AttachmentItem; diff --git a/packages/smart-forms-renderer/src/components/FormComponents/AttachmentItem/AttachmentUrlField.tsx b/packages/smart-forms-renderer/src/components/FormComponents/AttachmentItem/AttachmentUrlField.tsx new file mode 100644 index 000000000..4251e6648 --- /dev/null +++ b/packages/smart-forms-renderer/src/components/FormComponents/AttachmentItem/AttachmentUrlField.tsx @@ -0,0 +1,78 @@ +/* + * Copyright 2023 Commonwealth Scientific and Industrial Research + * Organisation (CSIRO) ABN 41 687 119 230. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; +import type { PropsWithIsTabledAttribute } from '../../../interfaces/renderProps.interface'; +import { StandardTextField } from '../Textfield.styles'; +import Typography from '@mui/material/Typography'; +import Box from '@mui/material/Box'; +import Stack from '@mui/material/Stack'; +import useAttachmentUrlValidation from '../../../hooks/useAttachmentUrlValidation'; +import InputAdornment from '@mui/material/InputAdornment'; +import Tooltip from '@mui/material/Tooltip'; +import CheckIcon from '@mui/icons-material/Check'; +import DangerousIcon from '@mui/icons-material/Dangerous'; + +interface AttachmentUrlFieldProps extends PropsWithIsTabledAttribute { + linkId: string; + url: string; + readOnly: boolean; + onUrlChange: (url: string) => void; +} + +function AttachmentUrlField(props: AttachmentUrlFieldProps) { + const { linkId, url, readOnly, isTabled, onUrlChange } = props; + + const urlIsValid = useAttachmentUrlValidation(url); + + return ( + + URL + + onUrlChange(event.target.value)} + disabled={readOnly} + size="small" + data-test="q-item-attachment-field" + InputProps={{ + endAdornment: ( + + {url != '' ? ( + + + {urlIsValid ? ( + + ) : ( + + )} + + + ) : null} + + ) + }} + /> + + + ); +} + +export default AttachmentUrlField; diff --git a/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceAutocompleteField.tsx b/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceAutocompleteField.tsx index 5dfc31654..7701f9aa1 100644 --- a/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceAutocompleteField.tsx +++ b/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceAutocompleteField.tsx @@ -21,7 +21,7 @@ import CircularProgress from '@mui/material/CircularProgress'; import Fade from '@mui/material/Fade'; import Tooltip from '@mui/material/Tooltip'; import type { Coding, QuestionnaireItem } from 'fhir/r4'; -import { StandardTextField } from '../Textfield.styles'; +import { StandardTextField, TEXT_FIELD_WIDTH } from '../Textfield.styles'; import SearchIcon from '@mui/icons-material/Search'; import InfoIcon from '@mui/icons-material/Info'; import WarningAmberIcon from '@mui/icons-material/WarningAmber'; @@ -32,7 +32,7 @@ import type { PropsWithIsTabledAttribute, PropsWithParentIsReadOnlyAttribute } from '../../../interfaces/renderProps.interface'; -import { AlertColor } from '@mui/material/Alert'; +import type { AlertColor } from '@mui/material/Alert'; interface ChoiceAutocompleteFieldsProps extends PropsWithIsTabledAttribute, @@ -75,7 +75,7 @@ function ChoiceAutocompleteField(props: ChoiceAutocompleteFieldsProps) { clearOnEscape autoHighlight onChange={(_, newValue) => onValueChange(newValue)} - sx={{ maxWidth: !isTabled ? 280 : 3000, minWidth: 160, flexGrow: 1 }} + sx={{ maxWidth: !isTabled ? TEXT_FIELD_WIDTH : 3000, minWidth: 160, flexGrow: 1 }} filterOptions={(x) => x} renderInput={(params) => ( {displayUnit}} - sx={{ maxWidth: !isTabled ? 280 : 3000, minWidth: 160 }} + sx={{ maxWidth: !isTabled ? TEXT_FIELD_WIDTH : 3000, minWidth: 160 }} size="small" onChange={(e) => onSelectChange(e.target.value)}> {qItem.answerOption?.map((option, index) => { diff --git a/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceSelectAnswerValueSetFields.tsx b/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceSelectAnswerValueSetFields.tsx index 4ea88bfde..5eefee717 100644 --- a/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceSelectAnswerValueSetFields.tsx +++ b/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceSelectAnswerValueSetFields.tsx @@ -17,7 +17,7 @@ import React from 'react'; import Autocomplete from '@mui/material/Autocomplete'; -import { StandardTextField } from '../Textfield.styles'; +import { StandardTextField, TEXT_FIELD_WIDTH } from '../Textfield.styles'; import { StyledAlert } from '../../Alert.styles'; import ErrorOutlineIcon from '@mui/icons-material/ErrorOutline'; import Typography from '@mui/material/Typography'; @@ -49,7 +49,7 @@ function ChoiceSelectAnswerValueSetFields(props: ChoiceSelectAnswerValueSetField onChange={(_, newValue) => onSelectChange(newValue)} openOnFocus autoHighlight - sx={{ maxWidth: !isTabled ? 280 : 3000, minWidth: 160, flexGrow: 1 }} + sx={{ maxWidth: !isTabled ? TEXT_FIELD_WIDTH : 3000, minWidth: 160, flexGrow: 1 }} size="small" disabled={readOnly} renderInput={(params) => ( diff --git a/packages/smart-forms-renderer/src/components/FormComponents/DateItem/DateField.tsx b/packages/smart-forms-renderer/src/components/FormComponents/DateItem/DateField.tsx index 04b382c2b..0322bc500 100644 --- a/packages/smart-forms-renderer/src/components/FormComponents/DateItem/DateField.tsx +++ b/packages/smart-forms-renderer/src/components/FormComponents/DateItem/DateField.tsx @@ -20,6 +20,7 @@ import type { PropsWithIsTabledAttribute } from '../../../interfaces/renderProps import type { Dayjs } from 'dayjs'; import { DatePicker as MuiDatePicker, LocalizationProvider } from '@mui/x-date-pickers'; import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; +import { TEXT_FIELD_WIDTH } from '../Textfield.styles'; interface DateFieldProps extends PropsWithIsTabledAttribute { value: Dayjs | null; @@ -39,7 +40,7 @@ function DateField(props: DateFieldProps) { value={value} disabled={readOnly} label={displayPrompt} - sx={{ maxWidth: !isTabled ? 280 : 3000, minWidth: 160 }} + sx={{ maxWidth: !isTabled ? TEXT_FIELD_WIDTH : 3000, minWidth: 160 }} onChange={onDateChange} slotProps={{ textField: { diff --git a/packages/smart-forms-renderer/src/components/FormComponents/DateTimeItem/DateTimeField.tsx b/packages/smart-forms-renderer/src/components/FormComponents/DateTimeItem/DateTimeField.tsx index 014c98e1f..678a0ee21 100644 --- a/packages/smart-forms-renderer/src/components/FormComponents/DateTimeItem/DateTimeField.tsx +++ b/packages/smart-forms-renderer/src/components/FormComponents/DateTimeItem/DateTimeField.tsx @@ -21,6 +21,7 @@ import type { Dayjs } from 'dayjs'; import { DateTimePicker as MuiDateTimePicker, LocalizationProvider } from '@mui/x-date-pickers'; import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; import Box from '@mui/material/Box'; +import { TEXT_FIELD_WIDTH } from '../Textfield.styles'; interface DateTimeFieldProps extends PropsWithIsTabledAttribute { value: Dayjs | null; @@ -41,7 +42,7 @@ function DateTimeField(props: DateTimeFieldProps) { value={value} disabled={readOnly} label={displayPrompt} - sx={{ maxWidth: !isTabled ? 280 : 3000, minWidth: 160 }} + sx={{ maxWidth: !isTabled ? TEXT_FIELD_WIDTH : 3000, minWidth: 160 }} onChange={onDateTimeChange} slotProps={{ textField: { diff --git a/packages/smart-forms-renderer/src/components/FormComponents/OpenChoiceItems/OpenChoiceAutocompleteField.tsx b/packages/smart-forms-renderer/src/components/FormComponents/OpenChoiceItems/OpenChoiceAutocompleteField.tsx index 3669b04ae..c32c8d5b1 100644 --- a/packages/smart-forms-renderer/src/components/FormComponents/OpenChoiceItems/OpenChoiceAutocompleteField.tsx +++ b/packages/smart-forms-renderer/src/components/FormComponents/OpenChoiceItems/OpenChoiceAutocompleteField.tsx @@ -18,7 +18,7 @@ import React from 'react'; import Box from '@mui/material/Box'; import Autocomplete from '@mui/material/Autocomplete'; -import { StandardTextField } from '../Textfield.styles'; +import { StandardTextField, TEXT_FIELD_WIDTH } from '../Textfield.styles'; import SearchIcon from '@mui/icons-material/Search'; import CircularProgress from '@mui/material/CircularProgress'; import Fade from '@mui/material/Fade'; @@ -80,7 +80,7 @@ function OpenChoiceAutocompleteField(props: OpenChoiceAutocompleteFieldProps) { clearOnEscape freeSolo autoHighlight - sx={{ maxWidth: !isTabled ? 280 : 3000, minWidth: 220, flexGrow: 1 }} + sx={{ maxWidth: !isTabled ? TEXT_FIELD_WIDTH : 3000, minWidth: 220, flexGrow: 1 }} onChange={(_, newValue) => onValueChange(newValue)} filterOptions={(x) => x} renderInput={(params) => ( diff --git a/packages/smart-forms-renderer/src/components/FormComponents/OpenChoiceItems/OpenChoiceSelectAnswerOptionField.tsx b/packages/smart-forms-renderer/src/components/FormComponents/OpenChoiceItems/OpenChoiceSelectAnswerOptionField.tsx index 7ea0d95a7..5c08442db 100644 --- a/packages/smart-forms-renderer/src/components/FormComponents/OpenChoiceItems/OpenChoiceSelectAnswerOptionField.tsx +++ b/packages/smart-forms-renderer/src/components/FormComponents/OpenChoiceItems/OpenChoiceSelectAnswerOptionField.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { getAnswerOptionLabel } from '../../../utils/openChoice'; -import { StandardTextField } from '../Textfield.styles'; +import { StandardTextField, TEXT_FIELD_WIDTH } from '../Textfield.styles'; import Autocomplete from '@mui/material/Autocomplete'; import type { QuestionnaireItem, QuestionnaireItemAnswerOption } from 'fhir/r4'; import type { @@ -33,7 +33,7 @@ function OpenChoiceSelectAnswerOptionField(props: OpenChoiceSelectAnswerOptionFi onChange={(_, newValue) => onChange(newValue)} freeSolo autoHighlight - sx={{ maxWidth: !isTabled ? 280 : 3000, minWidth: 160, flexGrow: 1 }} + sx={{ maxWidth: !isTabled ? TEXT_FIELD_WIDTH : 3000, minWidth: 160, flexGrow: 1 }} disabled={readOnly} size="small" renderInput={(params) => ( diff --git a/packages/smart-forms-renderer/src/components/FormComponents/OpenChoiceItems/OpenChoiceSelectAnswerValueSetField.tsx b/packages/smart-forms-renderer/src/components/FormComponents/OpenChoiceItems/OpenChoiceSelectAnswerValueSetField.tsx index edf2edfa2..2dc6c831a 100644 --- a/packages/smart-forms-renderer/src/components/FormComponents/OpenChoiceItems/OpenChoiceSelectAnswerValueSetField.tsx +++ b/packages/smart-forms-renderer/src/components/FormComponents/OpenChoiceItems/OpenChoiceSelectAnswerValueSetField.tsx @@ -1,6 +1,6 @@ import React from 'react'; import Autocomplete from '@mui/material/Autocomplete'; -import { StandardTextField } from '../Textfield.styles'; +import { StandardTextField, TEXT_FIELD_WIDTH } from '../Textfield.styles'; import Typography from '@mui/material/Typography'; import type { PropsWithIsTabledAttribute, @@ -36,7 +36,7 @@ function OpenChoiceSelectAnswerValueSetField(props: OpenChoiceSelectAnswerValueS onInputChange={(_, newValue) => onValueChange(newValue)} freeSolo autoHighlight - sx={{ maxWidth: !isTabled ? 280 : 3000, minWidth: 160, flexGrow: 1 }} + sx={{ maxWidth: !isTabled ? TEXT_FIELD_WIDTH : 3000, minWidth: 160, flexGrow: 1 }} disabled={readOnly} size="small" renderInput={(params) => ( diff --git a/packages/smart-forms-renderer/src/components/FormComponents/SingleItem/SingleItemSwitcher.tsx b/packages/smart-forms-renderer/src/components/FormComponents/SingleItem/SingleItemSwitcher.tsx index 3d27d1502..4db902af5 100644 --- a/packages/smart-forms-renderer/src/components/FormComponents/SingleItem/SingleItemSwitcher.tsx +++ b/packages/smart-forms-renderer/src/components/FormComponents/SingleItem/SingleItemSwitcher.tsx @@ -39,6 +39,7 @@ import CustomDateItem from '../CustomDateItem/CustomDateItem'; import { isSpecificItemControl } from '../../../utils'; import SliderItem from '../SliderItem/SliderItem'; import IntegerItem from '../IntegerItem/IntegerItem'; +import AttachmentItem from '../AttachmentItem/AttachmentItem'; interface SingleItemSwitcherProps extends PropsWithQrItemChangeHandler, @@ -55,9 +56,11 @@ function SingleItemSwitcher(props: SingleItemSwitcherProps) { props; switch (qItem.type) { - case 'string': + case 'display': + return ; + case 'boolean': return ( - ); - case 'boolean': + case 'decimal': return ( - ); - case 'time': + case 'integer': + if (isSpecificItemControl(qItem, 'slider')) { + return ( + + ); + } + return ( - ); - case 'text': + case 'time': return ( - ); - case 'display': - return ; - case 'integer': - if (isSpecificItemControl(qItem, 'slider')) { - return ( - - ); - } - + case 'string': return ( - ); - case 'decimal': + case 'text': return ( - + ); + case 'url': + return ( + ); - case 'url': + case 'attachment': return ( - + ); + case 'reference': + // FIXME reference item uses the same component as string item currently + return ( + + ); + case 'quantity': + // FIXME quantity item uses the same component as decimal item currently + return ( + - Item type not supported yet, or something has went wrong. If your questionnnaire is not a - FHIR R4 resource, there might be issues rendering it. + Item type {qItem.type} not supported yet, or something has went wrong. If your + questionnnaire is not a FHIR R4 resource, there might be issues rendering it. ); } diff --git a/packages/smart-forms-renderer/src/components/FormComponents/SliderItem/SliderField.tsx b/packages/smart-forms-renderer/src/components/FormComponents/SliderItem/SliderField.tsx index c90614869..3f9d93ff6 100644 --- a/packages/smart-forms-renderer/src/components/FormComponents/SliderItem/SliderField.tsx +++ b/packages/smart-forms-renderer/src/components/FormComponents/SliderItem/SliderField.tsx @@ -22,6 +22,7 @@ import { getSliderMarks } from '../../../utils/slider'; import Stack from '@mui/material/Stack'; import SliderLabels from './SliderLabels'; import SliderDisplayValue from './SliderDisplayValue'; +import { TEXT_FIELD_WIDTH } from '../Textfield.styles'; interface SliderFieldProps extends PropsWithIsTabledAttribute { linkId: string; @@ -54,7 +55,7 @@ function SliderField(props: SliderFieldProps) { const sliderMarks = getSliderMarks(minValue, maxValue, minLabel, maxLabel, stepValue); const sliderSx = { - maxWidth: !isTabled ? 280 : 3000, + maxWidth: !isTabled ? TEXT_FIELD_WIDTH : 3000, minWidth: 160 }; diff --git a/packages/smart-forms-renderer/src/components/FormComponents/Textfield.styles.ts b/packages/smart-forms-renderer/src/components/FormComponents/Textfield.styles.ts index a9414e52c..4da4b47be 100644 --- a/packages/smart-forms-renderer/src/components/FormComponents/Textfield.styles.ts +++ b/packages/smart-forms-renderer/src/components/FormComponents/Textfield.styles.ts @@ -18,12 +18,14 @@ import { styled } from '@mui/material/styles'; import TextField from '@mui/material/TextField'; +export const TEXT_FIELD_WIDTH = 300; + // Always use this accompanied by the TextField prop fullWidth export const StandardTextField = styled(TextField, { shouldForwardProp: (prop) => prop !== 'isTabled' })<{ isTabled: boolean }>(({ isTabled }) => ({ // Set 280 as the standard width for a field // Set a theoretical infinite maxWidth if field is within a table to fill the table row - maxWidth: !isTabled ? 280 : 3000, + maxWidth: !isTabled ? TEXT_FIELD_WIDTH : 3000, minWidth: 160 })); diff --git a/packages/smart-forms-renderer/src/components/FormComponents/TimeItem/TimeField.tsx b/packages/smart-forms-renderer/src/components/FormComponents/TimeItem/TimeField.tsx index ff4fde0bc..e8f4a1313 100644 --- a/packages/smart-forms-renderer/src/components/FormComponents/TimeItem/TimeField.tsx +++ b/packages/smart-forms-renderer/src/components/FormComponents/TimeItem/TimeField.tsx @@ -20,6 +20,7 @@ import type { PropsWithIsTabledAttribute } from '../../../interfaces/renderProps import type { Dayjs } from 'dayjs'; import { LocalizationProvider, TimePicker as MuiTimePicker } from '@mui/x-date-pickers'; import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; +import { TEXT_FIELD_WIDTH } from '../Textfield.styles'; interface TimeFieldProps extends PropsWithIsTabledAttribute { value: Dayjs | null; @@ -39,7 +40,7 @@ function TimeField(props: TimeFieldProps) { value={value} disabled={readOnly} label={displayPrompt} - sx={{ maxWidth: !isTabled ? 280 : 3000, minWidth: 160 }} + sx={{ maxWidth: !isTabled ? TEXT_FIELD_WIDTH : 3000, minWidth: 160 }} slotProps={{ textField: { fullWidth: true diff --git a/packages/smart-forms-renderer/src/hooks/UseFileDrop.ts b/packages/smart-forms-renderer/src/hooks/UseFileDrop.ts new file mode 100644 index 000000000..2b6cd7b47 --- /dev/null +++ b/packages/smart-forms-renderer/src/hooks/UseFileDrop.ts @@ -0,0 +1,53 @@ +/* + * Copyright 2024 Commonwealth Scientific and Industrial Research + * Organisation (CSIRO) ABN 41 687 119 230. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { ConnectDropTarget, DropTargetMonitor } from 'react-dnd'; +import { useDrop } from 'react-dnd'; +import { NativeTypes } from 'react-dnd-html5-backend'; + +interface UseFileDrop { + canDrop: boolean; + isOver: boolean; + dropTarget: ConnectDropTarget; +} + +function UseFileDrop(onDrop: (item: { files: any[] }) => void): UseFileDrop { + const [{ canDrop, isOver }, drop] = useDrop( + () => ({ + accept: [NativeTypes.FILE], + drop(item: { files: any[] }) { + if (onDrop) { + onDrop(item); + } + }, + canDrop() { + return true; + }, + collect: (monitor: DropTargetMonitor) => { + return { + isOver: monitor.isOver(), + canDrop: monitor.canDrop() + }; + } + }), + [onDrop] + ); + + return { canDrop, isOver, dropTarget: drop }; +} + +export default UseFileDrop; diff --git a/packages/smart-forms-renderer/src/hooks/useAttachmentUrlValidation.ts b/packages/smart-forms-renderer/src/hooks/useAttachmentUrlValidation.ts new file mode 100644 index 000000000..dd0c4271c --- /dev/null +++ b/packages/smart-forms-renderer/src/hooks/useAttachmentUrlValidation.ts @@ -0,0 +1,27 @@ +/* + * Copyright 2023 Commonwealth Scientific and Industrial Research + * Organisation (CSIRO) ABN 41 687 119 230. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +function useAttachmentUrlValidation(url: string): boolean { + try { + new URL(url); + return true; + } catch (error) { + return false; + } +} + +export default useAttachmentUrlValidation; diff --git a/packages/smart-forms-renderer/src/utils/fileUtils.ts b/packages/smart-forms-renderer/src/utils/fileUtils.ts new file mode 100644 index 000000000..a6065faff --- /dev/null +++ b/packages/smart-forms-renderer/src/utils/fileUtils.ts @@ -0,0 +1,66 @@ +/* + * Copyright 2024 Commonwealth Scientific and Industrial Research + * Organisation (CSIRO) ABN 41 687 119 230. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { Attachment } from 'fhir/r4'; + +export function getFileSize(fileSizeString: string) { + if (fileSizeString.length < 7) { + return `${Math.round(+fileSizeString / 1024)}kb`; + } + + return `${Math.round(+fileSizeString / 1024) / 1000}MB`; +} + +const fileToBase64 = (file: File) => + new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.readAsDataURL(file); + reader.onload = () => resolve(reader.result); + reader.onerror = reject; + }); + +export async function createAttachmentAnswer( + file: File | null, + url: string, + fileName: string +): Promise { + if (!file || url === '') { + return null; + } + + try { + const base64Data = (await fileToBase64(file)) as string; + const attachment: Attachment = { + contentType: file.type, + data: base64Data, + size: file.size + }; + + if (url) { + attachment.url = url; + } + + if (fileName) { + attachment.title = fileName; + } + + return attachment; + } catch (error) { + console.error(error); + return null; + } +} diff --git a/packages/smart-forms-renderer/src/utils/validateQuestionnaire.ts b/packages/smart-forms-renderer/src/utils/validateQuestionnaire.ts index 88d12d8c3..f1d97b5cb 100644 --- a/packages/smart-forms-renderer/src/utils/validateQuestionnaire.ts +++ b/packages/smart-forms-renderer/src/utils/validateQuestionnaire.ts @@ -15,14 +15,19 @@ * limitations under the License. */ -import type { QuestionnaireResponse, QuestionnaireResponseItemAnswer } from 'fhir/r4'; -import { Questionnaire, QuestionnaireItem, QuestionnaireResponseItem } from 'fhir/r4'; +import type { + Questionnaire, + QuestionnaireItem, + QuestionnaireResponse, + QuestionnaireResponseItem, + QuestionnaireResponseItemAnswer +} from 'fhir/r4'; import { getQrItemsIndex, mapQItemsIndex } from './mapItem'; -import { EnableWhenExpression, EnableWhenItems } from '../interfaces/enableWhen.interface'; +import type { EnableWhenExpression, EnableWhenItems } from '../interfaces/enableWhen.interface'; import { isHidden } from './qItem'; import { getRegexValidation } from './itemControl'; import { structuredDataCapture } from 'fhir-sdc-helpers'; -import { RegexValidation } from '../interfaces/regex.interface'; +import type { RegexValidation } from '../interfaces/regex.interface'; export type InvalidType = 'regex' | 'minLength' | 'maxLength' | 'required'; @@ -38,7 +43,6 @@ interface ValidateQuestionnaireParams { /** * Recursively go through the questionnaireResponse and check for un-filled required qItems * At the moment item.required for group items are not checked - * FIXME will eventually be renamed to validate questionnaire * * @author Sean Fong */ @@ -129,7 +133,7 @@ function validateItemRecursive(params: ValidateItemRecursiveParams) { // FIXME repeat groups not working if (qItem.type === 'group' && qItem.repeats) { - return validateRepeatGroup(qItem, qrItem, invalidItems); + return; } const childQItems = qItem.item; @@ -227,13 +231,13 @@ function validateSingleItem( return invalidItems; } -function validateRepeatGroup( - qItem: QuestionnaireItem, - qrItems: QuestionnaireResponseItem, - invalidLinkIds: Record -) { - return; -} +// function validateRepeatGroup( +// qItem: QuestionnaireItem, +// qrItems: QuestionnaireResponseItem, +// invalidLinkIds: Record +// ) { +// return; +// } function getInputInString(answer: QuestionnaireResponseItemAnswer) { if (answer.valueString) {