diff --git a/packages/smart-forms-renderer/src/components/FormComponents/RepeatGroup/RepeatGroup.tsx b/packages/smart-forms-renderer/src/components/FormComponents/RepeatGroup/RepeatGroup.tsx index 51e248c2c..76cd8c179 100644 --- a/packages/smart-forms-renderer/src/components/FormComponents/RepeatGroup/RepeatGroup.tsx +++ b/packages/smart-forms-renderer/src/components/FormComponents/RepeatGroup/RepeatGroup.tsx @@ -58,7 +58,7 @@ function RepeatGroup(props: RepeatGroupProps) { const mutateRepeatEnableWhenItems = useQuestionnaireStore.use.mutateRepeatEnableWhenItems(); - const initialRepeatGroups = useInitialiseRepeatGroups(qItem, qrItems); + const initialRepeatGroups = useInitialiseRepeatGroups(qItem.linkId, qrItems); const [repeatGroups, setRepeatGroups] = useRepeatGroups(initialRepeatGroups); @@ -104,7 +104,7 @@ function RepeatGroup(props: RepeatGroupProps) { setRepeatGroups([ ...repeatGroups, { - nanoId: generateNewRepeatId(qItem.linkId), + id: generateNewRepeatId(qItem.linkId), qrItem: null } ]); diff --git a/packages/smart-forms-renderer/src/components/FormComponents/RepeatGroup/RepeatGroupView.tsx b/packages/smart-forms-renderer/src/components/FormComponents/RepeatGroup/RepeatGroupView.tsx index ade78ff7a..e8572b6ff 100644 --- a/packages/smart-forms-renderer/src/components/FormComponents/RepeatGroup/RepeatGroupView.tsx +++ b/packages/smart-forms-renderer/src/components/FormComponents/RepeatGroup/RepeatGroupView.tsx @@ -76,7 +76,7 @@ function RepeatGroupView(props: RepeatGroupViewProps) { return ( - {repeatGroups.map(({ nanoId, qrItem: nullableQrItem }, index) => { + {repeatGroups.map(({ id, qrItem: nullableQrItem }, index) => { const answeredQrItem = createEmptyQrItem(qItem, undefined); if (nullableQrItem) { answeredQrItem.item = nullableQrItem.item; @@ -84,7 +84,7 @@ function RepeatGroupView(props: RepeatGroupViewProps) { return ( {qItem.text ? : null} - {repeatGroups.map(({ nanoId, qrItem: nullableQrItem }, index) => { + {repeatGroups.map(({ id, qrItem: nullableQrItem }, index) => { const answeredQrItem = createEmptyQrItem(qItem, undefined); if (nullableQrItem) { answeredQrItem.item = nullableQrItem.item; } return ( - + ) : null} - {repeatGroups.map(({ nanoId, qrItem: nullableQrItem }, index) => { + {repeatGroups.map(({ id, qrItem: nullableQrItem }, index) => { const answeredQrItem = createEmptyQrItem(qItem, undefined); if (nullableQrItem) { answeredQrItem.item = nullableQrItem.item; } return ( - + id !== rowToRemove.nanoId); + const updatedSelectedIds = selectedIds.filter((id) => id !== rowToRemove.id); updatedTableRows.splice(index, 1); @@ -117,21 +117,21 @@ function GroupTable(props: GroupTableProps) { } function handleAddRow() { - const newRowNanoId = generateNewRepeatId(qItem.linkId); + const newRowId = generateNewRepeatId(qItem.linkId); setTableRows([ ...tableRows, { - nanoId: newRowNanoId, + id: newRowId, qrItem: null } ]); - setSelectedIds([...selectedIds, newRowNanoId]); + setSelectedIds([...selectedIds, newRowId]); } function handleSelectAll() { // deselect all if all are selected, otherwise select all const updatedTableIds = - selectedIds.length === tableRows.length ? [] : tableRows.map((tableRow) => tableRow.nanoId); + selectedIds.length === tableRows.length ? [] : tableRows.map((tableRow) => tableRow.id); setSelectedIds(updatedTableIds); onQrRepeatGroupChange({ linkId: qItem.linkId, @@ -139,12 +139,12 @@ function GroupTable(props: GroupTableProps) { }); } - function handleSelectRow(nanoId: string) { + function handleSelectRow(rowId: string) { const updatedSelectedIds = [...selectedIds]; - const index = updatedSelectedIds.indexOf(nanoId); + const index = updatedSelectedIds.indexOf(rowId); if (index === -1) { - updatedSelectedIds.push(nanoId); + updatedSelectedIds.push(rowId); } else { updatedSelectedIds.splice(index, 1); } diff --git a/packages/smart-forms-renderer/src/components/FormComponents/Tables/GroupTableBody.tsx b/packages/smart-forms-renderer/src/components/FormComponents/Tables/GroupTableBody.tsx index e65ec3c24..c030afaef 100644 --- a/packages/smart-forms-renderer/src/components/FormComponents/Tables/GroupTableBody.tsx +++ b/packages/smart-forms-renderer/src/components/FormComponents/Tables/GroupTableBody.tsx @@ -41,7 +41,7 @@ interface GroupTableBodyProps qItemsIndexMap: Record; onRowChange: (newQrRow: QuestionnaireResponseItem, index: number) => void; onRemoveRow: (index: number) => void; - onSelectRow: (nanoId: string) => void; + onSelectRow: (rowId: string) => void; onReorderRows: (newTableRows: GroupTableRowModel[]) => void; } @@ -80,8 +80,8 @@ function GroupTableBody(props: GroupTableBodyProps) { {(droppableProvided, snapshot) => ( - {tableRows.map(({ nanoId, qrItem: nullableQrItem }, index) => { - const itemIsSelected = selectedIds.indexOf(nanoId) !== -1; + {tableRows.map(({ id, qrItem: nullableQrItem }, index) => { + const itemIsSelected = selectedIds.indexOf(id) !== -1; const answeredQrItem = createEmptyQrItem(tableQItem, undefined); if (nullableQrItem) { answeredQrItem.item = nullableQrItem.item; @@ -89,9 +89,9 @@ function GroupTableBody(props: GroupTableBodyProps) { return ( + {(draggableProvided, snapshot) => ( onSelectRow(nanoId)} + onSelectItem={() => onSelectRow(rowId)} /> )} @@ -150,7 +150,7 @@ function GroupTableRow(props: GroupTableRowProps) { onSelectRow(nanoId)} + onSelectItem={() => onSelectRow(rowId)} /> )} diff --git a/packages/smart-forms-renderer/src/components/FormComponents/Tables/GroupTableView.tsx b/packages/smart-forms-renderer/src/components/FormComponents/Tables/GroupTableView.tsx index 632b7d582..3f2b9ce1a 100644 --- a/packages/smart-forms-renderer/src/components/FormComponents/Tables/GroupTableView.tsx +++ b/packages/smart-forms-renderer/src/components/FormComponents/Tables/GroupTableView.tsx @@ -54,7 +54,7 @@ interface GroupTableViewProps onAddRow: () => void; onRowChange: (newQrRow: QuestionnaireResponseItem, index: number) => void; onRemoveRow: (index: number) => void; - onSelectRow: (nanoId: string) => void; + onSelectRow: (rowId: string) => void; onSelectAll: () => void; onReorderRows: (newTableRows: GroupTableRowModel[]) => void; } diff --git a/packages/smart-forms-renderer/src/hooks/useGroupTableRows.ts b/packages/smart-forms-renderer/src/hooks/useGroupTableRows.ts index e3ffc63a5..c7e1a3cb8 100644 --- a/packages/smart-forms-renderer/src/hooks/useGroupTableRows.ts +++ b/packages/smart-forms-renderer/src/hooks/useGroupTableRows.ts @@ -24,7 +24,7 @@ function useGroupTableRows(linkId: string, qrItems: QuestionnaireResponseItem[]) const [tableRows, setTableRows] = useState(initialisedGroupTableRows); const [selectedIds, setSelectedIds] = useState( - initialisedGroupTableRows.map((row) => row.nanoId) + initialisedGroupTableRows.map((row) => row.id) ); return { tableRows, selectedIds, setTableRows, setSelectedIds }; diff --git a/packages/smart-forms-renderer/src/hooks/useInitialiseGroupTable.ts b/packages/smart-forms-renderer/src/hooks/useInitialiseGroupTable.ts index 3936ecd6c..2934b9cd5 100644 --- a/packages/smart-forms-renderer/src/hooks/useInitialiseGroupTable.ts +++ b/packages/smart-forms-renderer/src/hooks/useInitialiseGroupTable.ts @@ -23,23 +23,16 @@ function useInitialiseGroupTable( linkId: string, qrItems: QuestionnaireResponseItem[] ): GroupTableRowModel[] { - let initialGroupTableRows: GroupTableRowModel[] = [ - { - nanoId: generateNewRepeatId(linkId), - qrItem: null - } - ]; - - if (qrItems.length > 0) { - initialGroupTableRows = qrItems.map((qrItem, index) => { - return { - nanoId: generateExistingRepeatId(linkId, index), - qrItem - }; - }); + if (qrItems.length === 0) { + return [{ id: generateNewRepeatId(linkId), qrItem: null }]; } - return initialGroupTableRows; + return qrItems.map((qrItem, index) => { + return { + id: generateExistingRepeatId(linkId, index), + qrItem + }; + }); } export default useInitialiseGroupTable; diff --git a/packages/smart-forms-renderer/src/hooks/useInitialiseRepeatGroups.ts b/packages/smart-forms-renderer/src/hooks/useInitialiseRepeatGroups.ts index dec11307f..4b710bee8 100644 --- a/packages/smart-forms-renderer/src/hooks/useInitialiseRepeatGroups.ts +++ b/packages/smart-forms-renderer/src/hooks/useInitialiseRepeatGroups.ts @@ -15,38 +15,27 @@ * limitations under the License. */ -import type { QuestionnaireItem, QuestionnaireResponseItem } from 'fhir/r4'; +import type { QuestionnaireResponseItem } from 'fhir/r4'; import type { RepeatGroupSingle } from '../interfaces/repeatGroup.interface'; import { useMemo } from 'react'; import { generateExistingRepeatId, generateNewRepeatId } from '../utils/repeatId'; function useInitialiseRepeatGroups( - qItem: QuestionnaireItem, + linkId: string, qrItems: QuestionnaireResponseItem[] ): RepeatGroupSingle[] { - return useMemo( - () => { - let initialRepeatGroupAnswers: RepeatGroupSingle[] = [ - { - nanoId: generateNewRepeatId(qItem.linkId), - qrItem: null - } - ]; + return useMemo(() => { + if (qrItems.length === 0) { + return [{ id: generateNewRepeatId(linkId), qrItem: null }]; + } - if (qrItems.length > 0) { - initialRepeatGroupAnswers = qrItems.map((qrItem, index) => { - return { - nanoId: generateExistingRepeatId(qItem.linkId, index), - qrItem - }; - }); - } - return initialRepeatGroupAnswers; - }, - // Requires checking of both qItem and qrItems - // eslint-disable-next-line react-hooks/exhaustive-deps - [qItem, qrItems] - ); + return qrItems.map((qrItem, index) => { + return { + id: generateExistingRepeatId(linkId, index), + qrItem + }; + }); + }, [linkId, qrItems]); } export default useInitialiseRepeatGroups; diff --git a/packages/smart-forms-renderer/src/hooks/useRepeatGroups.ts b/packages/smart-forms-renderer/src/hooks/useRepeatGroups.ts index a49be1a09..f13daf542 100644 --- a/packages/smart-forms-renderer/src/hooks/useRepeatGroups.ts +++ b/packages/smart-forms-renderer/src/hooks/useRepeatGroups.ts @@ -19,6 +19,7 @@ import type { Dispatch, SetStateAction } from 'react'; import { useEffect, useState } from 'react'; import type { RepeatGroupSingle } from '../interfaces/repeatGroup.interface'; import _isEqual from 'lodash/isEqual'; +import type { QuestionnaireResponseItem } from 'fhir/r4'; function useRepeatGroups( valueFromProps: RepeatGroupSingle[] @@ -27,10 +28,13 @@ function useRepeatGroups( useEffect( () => { - const valueFromPropsQRItems = valueFromProps.map( - (repeatGroupSingle) => repeatGroupSingle.qrItem - ); - const repeatGroupsQRItems = repeatGroups.map((repeatGroupSingle) => repeatGroupSingle.qrItem); + const valueFromPropsQRItems = valueFromProps + .map((repeatGroupSingle) => repeatGroupSingle.qrItem) + .filter((qrItem): qrItem is QuestionnaireResponseItem => qrItem !== null); + + const repeatGroupsQRItems = repeatGroups + .map((repeatGroupSingle) => repeatGroupSingle.qrItem) + .filter((qrItem): qrItem is QuestionnaireResponseItem => qrItem !== null); if (!_isEqual(valueFromPropsQRItems, repeatGroupsQRItems)) { setRepeatGroups(valueFromProps); diff --git a/packages/smart-forms-renderer/src/index.ts b/packages/smart-forms-renderer/src/index.ts index 7f9f8948e..e57d97e7a 100644 --- a/packages/smart-forms-renderer/src/index.ts +++ b/packages/smart-forms-renderer/src/index.ts @@ -42,6 +42,7 @@ export { destroyForm, getResponse, removeEmptyAnswersFromResponse, + removeInternalIdsFromResponse, isSpecificItemControl, isRepeatItemAndNotCheckbox, initialiseQuestionnaireResponse, diff --git a/packages/smart-forms-renderer/src/interfaces/groupTable.interface.ts b/packages/smart-forms-renderer/src/interfaces/groupTable.interface.ts index 38c55a057..78022b601 100644 --- a/packages/smart-forms-renderer/src/interfaces/groupTable.interface.ts +++ b/packages/smart-forms-renderer/src/interfaces/groupTable.interface.ts @@ -18,6 +18,6 @@ import type { QuestionnaireResponseItem } from 'fhir/r4'; export interface GroupTableRowModel { - nanoId: string; + id: string; qrItem: QuestionnaireResponseItem | null; } diff --git a/packages/smart-forms-renderer/src/interfaces/repeatGroup.interface.ts b/packages/smart-forms-renderer/src/interfaces/repeatGroup.interface.ts index 43c65afc1..cefa83407 100644 --- a/packages/smart-forms-renderer/src/interfaces/repeatGroup.interface.ts +++ b/packages/smart-forms-renderer/src/interfaces/repeatGroup.interface.ts @@ -23,6 +23,6 @@ export interface QrRepeatGroup { } export interface RepeatGroupSingle { - nanoId: string; + id: string; qrItem: QuestionnaireResponseItem | null; } diff --git a/packages/smart-forms-renderer/src/stories/assets/questionnaires/QIdRemoverDebugger.ts b/packages/smart-forms-renderer/src/stories/assets/questionnaires/QIdRemoverDebugger.ts new file mode 100644 index 000000000..768db7985 --- /dev/null +++ b/packages/smart-forms-renderer/src/stories/assets/questionnaires/QIdRemoverDebugger.ts @@ -0,0 +1,161 @@ +/* + * 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 { Questionnaire } from 'fhir/r4'; + +export const qMyPatient: Questionnaire = { + resourceType: 'Questionnaire', + id: 'canshare-myPatient1', + meta: { + versionId: '9', + lastUpdated: '2024-09-18T07:23:35.7317908+00:00' + }, + extension: [ + { + extension: [ + { + url: 'name', + valueId: 'LaunchPatient' + }, + { + url: 'type', + valueCode: 'Patient' + }, + { + url: 'description', + valueString: 'The patient that is to be used to pre-populate the form' + } + ], + url: 'http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-launchContext' + }, + { + extension: [ + { + url: 'name', + valueId: 'LaunchPractitioner' + }, + { + url: 'type', + valueCode: 'Practitioner' + }, + { + url: 'description', + valueString: 'The practitioner that is to be used to pre-populate the form' + } + ], + url: 'http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-launchContext' + }, + { + url: 'http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-itemExtractionContext', + valueCode: 'Patient' + } + ], + url: 'http://canshare.co.nz/questionnaire/myPatient1', + name: 'myPatient1', + status: 'active', + publisher: 'DEMO: David Hay', + useContext: [ + { + code: { + system: 'http://terminology.hl7.org/CodeSystem/usage-context-type', + code: 'user', + display: 'User Type' + }, + valueCodeableConcept: { + coding: [ + { + code: 'extract', + display: 'Demo Extract' + } + ] + } + } + ], + item: [ + { + linkId: 'myPatient1', + text: 'myPatient1', + type: 'group', + item: [ + { + linkId: 'myPatient1.name', + definition: 'http://hl7.org/fhir/StructureDefinition/Patient#Patient.name', + text: 'name *', + type: 'group', + repeats: true, + item: [ + { + linkId: 'myPatient1.name.first', + definition: 'http://hl7.org/fhir/StructureDefinition/Patient#Patient.name.given', + text: 'firstName *', + type: 'string', + repeats: true + }, + { + linkId: 'myPatient1.name.lastName', + definition: 'http://hl7.org/fhir/StructureDefinition/Patient#Patient.name.family', + text: 'lastName', + type: 'string' + } + ] + }, + { + linkId: 'myPatient1.hair', + definition: 'http://hl7.org/fhir/StructureDefinition/Patient#Patient.extension', + text: 'hair', + type: 'group', + item: [ + { + linkId: 'myPatient1.hair.colour', + definition: + 'http://hl7.org/fhir/StructureDefinition/Patient#Patient.extension.valueString', + text: 'colour', + type: 'string' + }, + { + linkId: 'myPatient1.hair.url', + definition: 'http://hl7.org/fhir/StructureDefinition/Patient#Patient.extension.url', + text: 'url', + type: 'string' + } + ] + }, + { + linkId: 'myPatient1.religion', + definition: 'http://hl7.org/fhir/StructureDefinition/Patient#Patient.extension', + text: 'religion', + type: 'group', + item: [ + { + linkId: 'myPatient1.religion.brand', + definition: + 'http://hl7.org/fhir/StructureDefinition/Patient#Patient.extension.valueString', + text: 'brand', + type: 'string' + }, + { + linkId: 'myPatient1.religion.url1', + definition: 'http://hl7.org/fhir/StructureDefinition/Patient#Patient.extension.url', + text: 'url', + type: 'string' + } + ] + } + ] + } + ] +}; diff --git a/packages/smart-forms-renderer/src/stories/storybookWrappers/IdRemoverButtonForStorybook.tsx b/packages/smart-forms-renderer/src/stories/storybookWrappers/IdRemoverButtonForStorybook.tsx new file mode 100644 index 000000000..61a1e5f2e --- /dev/null +++ b/packages/smart-forms-renderer/src/stories/storybookWrappers/IdRemoverButtonForStorybook.tsx @@ -0,0 +1,51 @@ +/* + * 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. + */ + +// @ts-ignore +import React from 'react'; +import type { Questionnaire, QuestionnaireResponse } from 'fhir/r4'; +import { Box, IconButton, Tooltip } from '@mui/material'; +import ContentCutIcon from '@mui/icons-material/ContentCut'; +import { useQuestionnaireResponseStore } from '../../stores'; +import { removeInternalIdsFromResponse } from '../../utils/manageForm'; + +interface IdRemoverButtonProps { + questionnaire: Questionnaire; + questionnaireResponse: QuestionnaireResponse; +} + +function IdRemoverButtonForStorybook(props: IdRemoverButtonProps) { + const { questionnaire, questionnaireResponse } = props; + + const updateResponse = useQuestionnaireResponseStore.use.updateResponse(); + + async function handleRemoveIds() { + updateResponse(removeInternalIdsFromResponse(questionnaire, questionnaireResponse)); + } + + return ( + + + + + + + + ); +} + +export default IdRemoverButtonForStorybook; diff --git a/packages/smart-forms-renderer/src/stories/storybookWrappers/IdRemoverDebuggerWrapperForStorybook.tsx b/packages/smart-forms-renderer/src/stories/storybookWrappers/IdRemoverDebuggerWrapperForStorybook.tsx new file mode 100644 index 000000000..563d71722 --- /dev/null +++ b/packages/smart-forms-renderer/src/stories/storybookWrappers/IdRemoverDebuggerWrapperForStorybook.tsx @@ -0,0 +1,84 @@ +/* + * 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. + */ + +// @ts-ignore +import React from 'react'; +import type { Questionnaire, QuestionnaireResponse } from 'fhir/r4'; +import { BaseRenderer } from '../../components'; +import { QueryClientProvider } from '@tanstack/react-query'; +import { RendererThemeProvider } from '../../theme'; +import { useBuildForm, useRendererQueryClient } from '../../hooks'; +import { STORYBOOK_TERMINOLOGY_SERVER_URL } from './globals'; +import IdRemoverButtonForStorybook from './IdRemoverButtonForStorybook'; +import { Grid } from '@mui/material'; +import { useQuestionnaireResponseStore, useQuestionnaireStore } from '../../stores'; + +interface IdRemoverDebuggerWrapperForStorybookProps { + questionnaire: Questionnaire; + questionnaireResponse?: QuestionnaireResponse; +} + +/** + * This is a wrapper which for debugging answer/item IDs in repeating items and groups. + * It features a button to remove answer/item IDs from the QuestionnaireResponse. + * + * @author Sean Fong + */ +function IdRemoverDebuggerWrapperForStorybook(props: IdRemoverDebuggerWrapperForStorybookProps) { + const { questionnaire, questionnaireResponse } = props; + + const queryClient = useRendererQueryClient(); + + const focusedLinkId = useQuestionnaireStore.use.focusedLinkId(); + const updatableResponse = useQuestionnaireResponseStore.use.updatableResponse(); + + const isBuilding = useBuildForm( + questionnaire, + questionnaireResponse, + undefined, + STORYBOOK_TERMINOLOGY_SERVER_URL + ); + + if (isBuilding) { + return
Loading...
; + } + + return ( + + +
+ + + + + + +
{JSON.stringify(focusedLinkId, null, 2)}
+ ---- +
{JSON.stringify(updatableResponse, null, 2)}
+
+
+
+
+
+ ); +} + +export default IdRemoverDebuggerWrapperForStorybook; diff --git a/packages/smart-forms-renderer/src/stories/testing/IdRemoverDebuggerWrapper.stories.tsx b/packages/smart-forms-renderer/src/stories/testing/IdRemoverDebuggerWrapper.stories.tsx new file mode 100644 index 000000000..33716f750 --- /dev/null +++ b/packages/smart-forms-renderer/src/stories/testing/IdRemoverDebuggerWrapper.stories.tsx @@ -0,0 +1,39 @@ +/* + * 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 { Meta, StoryObj } from '@storybook/react'; +import IdRemoverDebuggerWrapperForStorybook from '../storybookWrappers/IdRemoverDebuggerWrapperForStorybook'; +import { qMyPatient } from '../assets/questionnaires/QIdRemoverDebugger'; + +// More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction#default-export +const meta = { + title: 'Component/Testing/ID Remover Debugger', + component: IdRemoverDebuggerWrapperForStorybook, + // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/react/writing-docs/autodocs + tags: [] +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +// More on writing stories with args: https://storybook.js.org/docs/react/writing-stories/args + +export const MyPatient: Story = { + args: { + questionnaire: qMyPatient + } +}; diff --git a/packages/smart-forms-renderer/src/utils/groupTable.ts b/packages/smart-forms-renderer/src/utils/groupTable.ts index 0f2111322..c9557880e 100644 --- a/packages/smart-forms-renderer/src/utils/groupTable.ts +++ b/packages/smart-forms-renderer/src/utils/groupTable.ts @@ -32,6 +32,6 @@ export function reorderRows( export function getGroupTableItemsToUpdate(tableRows: GroupTableRowModel[], selectedIds: string[]) { return tableRows - .filter((row) => selectedIds.includes(row.nanoId)) + .filter((row) => selectedIds.includes(row.id)) .flatMap((singleRow) => (singleRow.qrItem ? [cloneDeep(singleRow.qrItem)] : [])); } diff --git a/packages/smart-forms-renderer/src/utils/index.ts b/packages/smart-forms-renderer/src/utils/index.ts index d69332fe2..8198c0a17 100644 --- a/packages/smart-forms-renderer/src/utils/index.ts +++ b/packages/smart-forms-renderer/src/utils/index.ts @@ -15,7 +15,13 @@ * limitations under the License. */ -export { buildForm, destroyForm, getResponse, removeEmptyAnswersFromResponse } from './manageForm'; +export { + buildForm, + destroyForm, + getResponse, + removeEmptyAnswersFromResponse, + removeInternalIdsFromResponse +} from './manageForm'; export { initialiseQuestionnaireResponse } from './initialise'; export { isSpecificItemControl } from './itemControl'; export { isRepeatItemAndNotCheckbox } from './qItem'; diff --git a/packages/smart-forms-renderer/src/utils/manageForm.ts b/packages/smart-forms-renderer/src/utils/manageForm.ts index 9a4d26b89..e6404f0f5 100644 --- a/packages/smart-forms-renderer/src/utils/manageForm.ts +++ b/packages/smart-forms-renderer/src/utils/manageForm.ts @@ -10,6 +10,8 @@ import { removeEmptyAnswers } from './removeEmptyAnswers'; import { readEncounter, readPatient, readUser } from '../api/smartClient'; import type Client from 'fhirclient/lib/Client'; import cloneDeep from 'lodash.clonedeep'; +import { updateQuestionnaireResponse } from './genericRecursive'; +import { removeInternalRepeatIdsRecursive } from './repeatId'; /** * Build the form with an initial Questionnaire and an optional filled QuestionnaireResponse. @@ -96,7 +98,11 @@ export async function initialiseFhirClient(fhirClient: Client): Promise { * @author Sean Fong */ export function getResponse(): QuestionnaireResponse { - return cloneDeep(questionnaireResponseStore.getState().updatableResponse); + const cleanResponse = removeInternalIdsFromResponse( + questionnaireStore.getState().sourceQuestionnaire, + questionnaireResponseStore.getState().updatableResponse + ); + return cloneDeep(cleanResponse); } /** @@ -123,6 +129,26 @@ export function removeEmptyAnswersFromResponse( }); } +/** + * Remove all instances of item.answer.id from the filled QuestionnaireResponse. + * These IDs are used internally for rendering repeating items, and can be safely left out of the final response. + * + * @author Sean Fong + */ +export function removeInternalIdsFromResponse( + questionnaire: Questionnaire, + questionnaireResponse: QuestionnaireResponse +): QuestionnaireResponse { + const questionnaireResponseToUpdate = cloneDeep(questionnaireResponse); + + return updateQuestionnaireResponse( + questionnaire, + questionnaireResponseToUpdate, + removeInternalRepeatIdsRecursive, + undefined + ); +} + /** * Check if a QuestionnaireResponseItem has either an item or an answer property. * diff --git a/packages/smart-forms-renderer/src/utils/repeatId.ts b/packages/smart-forms-renderer/src/utils/repeatId.ts index ff9e7c4bd..2f4209cd6 100644 --- a/packages/smart-forms-renderer/src/utils/repeatId.ts +++ b/packages/smart-forms-renderer/src/utils/repeatId.ts @@ -16,12 +16,108 @@ */ import { nanoid } from 'nanoid'; +import type { + QuestionnaireItem, + QuestionnaireResponseItem, + QuestionnaireResponseItemAnswer +} from 'fhir/r4'; +import { getQrItemsIndex, mapQItemsIndex } from './mapItem'; export function generateNewRepeatId(linkId: string): string { - return `${linkId}-${nanoid()}`; + return `${linkId}-repeat-${nanoid()}`; } export function generateExistingRepeatId(linkId: string, index: number): string { const paddedIndex = index.toString().padStart(6, '0'); - return `${linkId}-${paddedIndex}`; + return `${linkId}-repeat-${paddedIndex}`; +} + +export function removeInternalRepeatIdsRecursive( + qItem: QuestionnaireItem, + qrItemOrItems: QuestionnaireResponseItem | QuestionnaireResponseItem[] | null +): QuestionnaireResponseItem | QuestionnaireResponseItem[] | null { + // Process repeating group items separately + const hasMultipleAnswers = Array.isArray(qrItemOrItems); + if (hasMultipleAnswers) { + return removeInternalRepeatIdsFromRepeatGroup(qItem, qrItemOrItems); + } + + // At this point qrItemOrItems is a single QuestionnaireResponseItem + const qrItem = qrItemOrItems; + + // Process items with child items + const childQItems = qItem.item ?? []; + const childQrItems = qrItem?.item ?? []; + const updatedChildQrItems: QuestionnaireResponseItem[] = []; + if (childQItems.length > 0) { + const indexMap = mapQItemsIndex(qItem); + const qrItemsByIndex = getQrItemsIndex(childQItems, childQrItems, indexMap); + + // Iterate child items + for (const [index, childQItem] of childQItems.entries()) { + const childQRItemOrItems = qrItemsByIndex[index]; + + const updatedChildQRItemOrItems = removeInternalRepeatIdsRecursive( + childQItem, + childQRItemOrItems ?? null + ); + + if (Array.isArray(updatedChildQRItemOrItems)) { + if (updatedChildQRItemOrItems.length > 0) { + updatedChildQrItems.push(...updatedChildQRItemOrItems); + } + continue; + } + + if (updatedChildQRItemOrItems) { + updatedChildQrItems.push(updatedChildQRItemOrItems); + } + } + } + + // Construct updated qrItem + return removeInternalRepeatIdsFromItem(qItem, qrItem, updatedChildQrItems); +} + +function removeInternalRepeatIdsFromRepeatGroup( + qItem: QuestionnaireItem, + qrItems: QuestionnaireResponseItem[] +) { + if (!qItem.item) { + return []; + } + + return qrItems + .flatMap((childQrItem) => removeInternalRepeatIdsRecursive(qItem, childQrItem)) + .filter((childQRItem): childQRItem is QuestionnaireResponseItem => !!childQRItem); +} + +function removeInternalRepeatIdsFromItem( + qItem: QuestionnaireItem, + qrItem: QuestionnaireResponseItem | null, + childQrItems: QuestionnaireResponseItem[] +): QuestionnaireResponseItem | null { + if (!qrItem) { + return null; + } + + // Remove internal repeatId from all answers + const updatedAnswers: QuestionnaireResponseItemAnswer[] = + qrItem.answer + ?.map( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + ({ id, ...rest }) => { + return { + ...rest + }; + } + ) + .filter((answer) => !!answer && Object.keys(answer).length > 0) ?? []; + + return { + linkId: qItem.linkId, + ...(qItem.text && { text: qItem.text }), + ...(childQrItems.length > 0 && { item: childQrItems }), + ...(updatedAnswers.length > 0 && { answer: updatedAnswers }) + }; }