From 3b9e0445a7a0f48f4e75a7215545e7f357767a1b Mon Sep 17 00:00:00 2001 From: Sean Fong Date: Mon, 18 Dec 2023 13:29:03 +1030 Subject: [PATCH] Fix remove empty answers, save and form preview for facade compatibility --- apps/smart-forms-app/.env | 2 +- apps/smart-forms-app/.env.production | 2 +- apps/smart-forms-app/src/api/headers.ts | 4 +- apps/smart-forms-app/src/api/saveQr.ts | 11 ++-- .../src/features/preview/utils/preview.ts | 24 +++---- .../FormPreviewPage/FormPreview.tsx | 4 +- .../RendererActions/SaveProgressAction.tsx | 10 ++- .../RendererNav/BlockerUnsavedFormDialog.tsx | 4 +- .../tokenTimer/components/AutoSaveDialog.tsx | 4 +- .../components/TokenTimerDialog.tsx | 4 +- .../src/features/viewer/ResponsePreview.tsx | 4 +- ...{removeHidden.ts => removeEmptyAnswers.ts} | 64 ++++++++++++++----- 12 files changed, 87 insertions(+), 50 deletions(-) rename packages/smart-forms-renderer/src/utils/{removeHidden.ts => removeEmptyAnswers.ts} (76%) diff --git a/apps/smart-forms-app/.env b/apps/smart-forms-app/.env index 7049859fd..35b5a6487 100644 --- a/apps/smart-forms-app/.env +++ b/apps/smart-forms-app/.env @@ -2,7 +2,7 @@ VITE_ONTOSERVER_URL=https://r4.ontoserver.csiro.au/fhir VITE_FORMS_SERVER_URL=https://smartforms.csiro.au/api/fhir VITE_LAUNCH_SCOPE=fhirUser online_access openid profile patient/Condition.rs patient/Observation.rs launch patient/Encounter.rs patient/QuestionnaireResponse.cruds patient/Patient.rs -VITE_LAUNCH_CLIENT_ID=a57d90e3-5f69-4b92-aa2e-2992180863c0 +VITE_LAUNCH_CLIENT_ID=a57d90e3-5f69-4b92-aa2e-2992180863c1 VITE_IN_APP_POPULATE=true diff --git a/apps/smart-forms-app/.env.production b/apps/smart-forms-app/.env.production index d88f057dc..363b4474d 100644 --- a/apps/smart-forms-app/.env.production +++ b/apps/smart-forms-app/.env.production @@ -2,7 +2,7 @@ VITE_ONTOSERVER_URL=https://r4.ontoserver.csiro.au/fhir VITE_FORMS_SERVER_URL=https://smartforms.csiro.au/api/fhir VITE_LAUNCH_SCOPE=fhirUser online_access openid profile patient/Condition.rs patient/Observation.rs launch patient/Encounter.rs patient/QuestionnaireResponse.cruds patient/Patient.rs -VITE_LAUNCH_CLIENT_ID=a57d90e3-5f69-4b92-aa2e-2992180863c0 +VITE_LAUNCH_CLIENT_ID=a57d90e3-5f69-4b92-aa2e-2992180863c1 VITE_IN_APP_POPULATE=true diff --git a/apps/smart-forms-app/src/api/headers.ts b/apps/smart-forms-app/src/api/headers.ts index c94c45b6e..c7e182199 100644 --- a/apps/smart-forms-app/src/api/headers.ts +++ b/apps/smart-forms-app/src/api/headers.ts @@ -1,5 +1,5 @@ export const HEADERS = { 'Cache-Control': 'no-cache', - 'Content-Type': 'application/json+fhir;charset=utf-8', - Accept: 'application/json+fhir;charset=utf-8' + 'Content-Type': 'application/json;charset=utf-8', + Accept: 'application/json;charset=utf-8' }; diff --git a/apps/smart-forms-app/src/api/saveQr.ts b/apps/smart-forms-app/src/api/saveQr.ts index 4900ac20c..38382d024 100644 --- a/apps/smart-forms-app/src/api/saveQr.ts +++ b/apps/smart-forms-app/src/api/saveQr.ts @@ -23,7 +23,7 @@ import { qrToHTML } from '../features/preview/utils/preview.ts'; import { fetchQuestionnaireById } from './client.ts'; import cloneDeep from 'lodash.clonedeep'; import { HEADERS } from './headers.ts'; -import { removeHiddenAnswersFromResponse } from '@aehrc/smart-forms-renderer'; +import { removeEmptyAnswersFromResponse } from '@aehrc/smart-forms-renderer'; /** * POST questionnaire to SMART Health IT when opening it to ensure response-saving can be performed @@ -55,7 +55,7 @@ export async function saveProgress( questionnaireResponse: QuestionnaireResponse, saveStatus: 'in-progress' | 'completed' ) { - const responseToSave = removeHiddenAnswersFromResponse( + const responseToSave = removeEmptyAnswersFromResponse( questionnaire, cloneDeep(questionnaireResponse) ); @@ -87,7 +87,7 @@ export async function saveQuestionnaireResponse( user: Practitioner, questionnaire: Questionnaire, questionnaireResponse: QuestionnaireResponse -): Promise { +) { let requestUrl = 'QuestionnaireResponse'; let method = 'POST'; let questionnaireResponseToSave: QuestionnaireResponse = cloneDeep(questionnaireResponse); @@ -111,6 +111,7 @@ export async function saveQuestionnaireResponse( authored: dayjs().format() }; + // TODO pre-pop should filter out all empty strings really // Add additional attributes depending on whether questionnaire has been saved before if (questionnaireResponseToSave.id) { requestUrl += '/' + questionnaireResponseToSave.id; @@ -124,11 +125,13 @@ export async function saveQuestionnaireResponse( ); } + const modifiedHeaders = { ...HEADERS, prefer: 'return=representation' }; + return client.request({ url: requestUrl, method: method, body: JSON.stringify(questionnaireResponseToSave), - headers: HEADERS + headers: modifiedHeaders }); } diff --git a/apps/smart-forms-app/src/features/preview/utils/preview.ts b/apps/smart-forms-app/src/features/preview/utils/preview.ts index 757ed5a69..6bb92298a 100644 --- a/apps/smart-forms-app/src/features/preview/utils/preview.ts +++ b/apps/smart-forms-app/src/features/preview/utils/preview.ts @@ -30,7 +30,7 @@ export function qrToHTML( ): string { if (!questionnaireResponse.item || questionnaireResponse.item.length === 0) return ''; - let QrHtml = `
${questionnaire.title}

`; + let QrHtml = `
${questionnaire.title}

`; for (const topLevelQRItem of questionnaireResponse.item) { const topLevelQRItemHTML = qrItemToHTML(topLevelQRItem); @@ -38,8 +38,7 @@ export function qrToHTML( QrHtml += topLevelQRItemHTML; } } - - return `
${QrHtml}
`; + return `
${QrHtml}
`; } export function qrItemToHTML(topLevelQRItem: QuestionnaireResponseItem) { @@ -125,29 +124,26 @@ function renderItemDiv(item: QuestionnaireResponseItem, nestedLevel: number) { item.answer.forEach((answer) => { const answerValueInString = he.encode(qrItemAnswerValueTypeSwitcher(answer)); if (answerValueInString === '') { - qrItemAnswer += - '
Undefined answer
'; + qrItemAnswer += '
Undefined answer
'; } else { - qrItemAnswer += `
${ + qrItemAnswer += `
${ answerValueInString[0].toUpperCase() + answerValueInString.slice(1) }
`; } }); - const qrItemRender = `
${item.text}
-
-
${qrItemAnswer}
`; + const qrItemRender = `
${item.text}
${qrItemAnswer}
`; return `
${qrItemRender}
`; + }; display: flex; flex-wrap: wrap">${qrItemRender}
`; } function renderGroupHeadingDiv(item: QuestionnaireResponseItem, nestedLevel: number) { const fontSize = nestedLevel === 0 ? '18px' : '15px'; const headingText = he.encode(item.text ?? ''); - return `
${headingText}
`; + return `
${headingText}
`; } function renderRepeatGroupItemHeadingDiv() { @@ -202,7 +198,7 @@ function qrItemAnswerValueTypeSwitcher(answer: QuestionnaireResponseItemAnswer): function renderGroupBottomMargin(repeatGroupItemStatus: RepeatGroupItemStatus) { if (repeatGroupItemStatus === 'first' || repeatGroupItemStatus === 'middle') { - return `
`; + return `
`; } else { return `
`; } @@ -212,8 +208,8 @@ function renderGeneralBottomMargin( nestedLevel: number, nextItem: QuestionnaireResponseItem | undefined ) { - const smallMarginDiv = `
`; - const largeMarginDiv = `
`; + const smallMarginDiv = `
`; + const largeMarginDiv = `
`; if (nestedLevel !== 0) { return ''; diff --git a/apps/smart-forms-app/src/features/renderer/components/FormPreviewPage/FormPreview.tsx b/apps/smart-forms-app/src/features/renderer/components/FormPreviewPage/FormPreview.tsx index a939b1c78..c06d4c8ed 100644 --- a/apps/smart-forms-app/src/features/renderer/components/FormPreviewPage/FormPreview.tsx +++ b/apps/smart-forms-app/src/features/renderer/components/FormPreviewPage/FormPreview.tsx @@ -22,7 +22,7 @@ import { qrToHTML } from '../../../preview/utils/preview.ts'; import { Helmet } from 'react-helmet'; import PageHeading from '../../../dashboard/components/DashboardPages/PageHeading.tsx'; import { - removeHiddenAnswersFromResponse, + removeEmptyAnswersFromResponse, useQuestionnaireResponseStore, useQuestionnaireStore } from '@aehrc/smart-forms-renderer'; @@ -39,7 +39,7 @@ function FormPreview() { return ; } - const cleanResponse = removeHiddenAnswersFromResponse(sourceQuestionnaire, updatableResponse); + const cleanResponse = removeEmptyAnswersFromResponse(sourceQuestionnaire, updatableResponse); const parsedHTML = parse(qrToHTML(sourceQuestionnaire, cleanResponse)); return ( diff --git a/apps/smart-forms-app/src/features/renderer/components/RendererActions/SaveProgressAction.tsx b/apps/smart-forms-app/src/features/renderer/components/RendererActions/SaveProgressAction.tsx index 4655649f2..1301a0087 100644 --- a/apps/smart-forms-app/src/features/renderer/components/RendererActions/SaveProgressAction.tsx +++ b/apps/smart-forms-app/src/features/renderer/components/RendererActions/SaveProgressAction.tsx @@ -77,14 +77,20 @@ function SaveProgressAction(props: SaveProgressSpeedDialActionProps) { 'in-progress' ); - if (!savedResponse) { + // If the response is null or undefined, then the save has failed + if (savedResponse === null || savedResponse === undefined) { enqueueSnackbar(saveErrorMessage, { variant: 'error' }); return; } - setUpdatableResponseAsSaved(savedResponse); + // Use saved validated response as the new updatable response + if (savedResponse && savedResponse.resourceType === 'QuestionnaireResponse') { + setUpdatableResponseAsSaved(savedResponse); + } else { + setUpdatableResponseAsSaved(updatableResponse); + } // Refetch existing responses if prop is provided if (refetchResponses) { diff --git a/apps/smart-forms-app/src/features/renderer/components/RendererNav/BlockerUnsavedFormDialog.tsx b/apps/smart-forms-app/src/features/renderer/components/RendererNav/BlockerUnsavedFormDialog.tsx index 4b755f090..78097c3bb 100644 --- a/apps/smart-forms-app/src/features/renderer/components/RendererNav/BlockerUnsavedFormDialog.tsx +++ b/apps/smart-forms-app/src/features/renderer/components/RendererNav/BlockerUnsavedFormDialog.tsx @@ -30,7 +30,7 @@ import { saveQuestionnaireResponse } from '../../../../api/saveQr.ts'; import cloneDeep from 'lodash.clonedeep'; import { LoadingButton } from '@mui/lab'; import { - removeHiddenAnswersFromResponse, + removeEmptyAnswersFromResponse, useQuestionnaireResponseStore, useQuestionnaireStore } from '@aehrc/smart-forms-renderer'; @@ -79,7 +79,7 @@ function BlockerUnsavedFormDialog(props: Props) { setIsSaving(true); let responseToSave = cloneDeep(updatableResponse); - responseToSave = removeHiddenAnswersFromResponse(sourceQuestionnaire, responseToSave); + responseToSave = removeEmptyAnswersFromResponse(sourceQuestionnaire, responseToSave); setIsSaving(true); responseToSave.status = 'in-progress'; diff --git a/apps/smart-forms-app/src/features/tokenTimer/components/AutoSaveDialog.tsx b/apps/smart-forms-app/src/features/tokenTimer/components/AutoSaveDialog.tsx index 6f1adfb00..a8e9e8ae6 100644 --- a/apps/smart-forms-app/src/features/tokenTimer/components/AutoSaveDialog.tsx +++ b/apps/smart-forms-app/src/features/tokenTimer/components/AutoSaveDialog.tsx @@ -18,7 +18,7 @@ import { useEffect } from 'react'; import { Dialog, DialogContent, DialogContentText, DialogTitle } from '@mui/material'; import { - removeHiddenAnswersFromResponse, + removeEmptyAnswersFromResponse, useQuestionnaireResponseStore, useQuestionnaireStore } from '@aehrc/smart-forms-renderer'; @@ -54,7 +54,7 @@ function AutoSaveDialog(props: AutoSaveDialogProps) { return; } - const responseToSave = removeHiddenAnswersFromResponse( + const responseToSave = removeEmptyAnswersFromResponse( sourceQuestionnaire, cloneDeep(updatableResponse) ); diff --git a/apps/smart-forms-app/src/features/tokenTimer/components/TokenTimerDialog.tsx b/apps/smart-forms-app/src/features/tokenTimer/components/TokenTimerDialog.tsx index 1a6e591fe..ee006d370 100644 --- a/apps/smart-forms-app/src/features/tokenTimer/components/TokenTimerDialog.tsx +++ b/apps/smart-forms-app/src/features/tokenTimer/components/TokenTimerDialog.tsx @@ -31,7 +31,7 @@ import { } from '@mui/material'; import { LoadingButton } from '@mui/lab'; import { - removeHiddenAnswersFromResponse, + removeEmptyAnswersFromResponse, useQuestionnaireResponseStore, useQuestionnaireStore } from '@aehrc/smart-forms-renderer'; @@ -75,7 +75,7 @@ function TokenTimerDialog(props: TokenTimerDialogProps) { } setIsSaving(true); - const responseToSave = removeHiddenAnswersFromResponse( + const responseToSave = removeEmptyAnswersFromResponse( sourceQuestionnaire, cloneDeep(updatableResponse) ); diff --git a/apps/smart-forms-app/src/features/viewer/ResponsePreview.tsx b/apps/smart-forms-app/src/features/viewer/ResponsePreview.tsx index 3dfab06ef..e94d0c648 100644 --- a/apps/smart-forms-app/src/features/viewer/ResponsePreview.tsx +++ b/apps/smart-forms-app/src/features/viewer/ResponsePreview.tsx @@ -24,7 +24,7 @@ import { qrToHTML } from '../preview/utils/preview.ts'; import { Helmet } from 'react-helmet'; import PageHeading from '../dashboard/components/DashboardPages/PageHeading.tsx'; import { - removeHiddenAnswersFromResponse, + removeEmptyAnswersFromResponse, useQuestionnaireResponseStore, useQuestionnaireStore } from '@aehrc/smart-forms-renderer'; @@ -49,7 +49,7 @@ function ResponsePreview() { return ; } - const responseCleaned = removeHiddenAnswersFromResponse(sourceQuestionnaire, sourceResponse); + const responseCleaned = removeEmptyAnswersFromResponse(sourceQuestionnaire, sourceResponse); const parsedHTML = parse(qrToHTML(sourceQuestionnaire, responseCleaned)); return ( diff --git a/packages/smart-forms-renderer/src/utils/removeHidden.ts b/packages/smart-forms-renderer/src/utils/removeEmptyAnswers.ts similarity index 76% rename from packages/smart-forms-renderer/src/utils/removeHidden.ts rename to packages/smart-forms-renderer/src/utils/removeEmptyAnswers.ts index f5d48bf6c..39f9dfa4b 100644 --- a/packages/smart-forms-renderer/src/utils/removeHidden.ts +++ b/packages/smart-forms-renderer/src/utils/removeEmptyAnswers.ts @@ -24,7 +24,7 @@ import type { import type { EnableWhenExpression, EnableWhenItems } from '../interfaces/enableWhen.interface'; import { isHidden } from './qItem'; -interface removeHiddenAnswersParams { +interface removeEmptyAnswersParams { questionnaire: Questionnaire; questionnaireResponse: QuestionnaireResponse; enableWhenIsActivated: boolean; @@ -33,11 +33,11 @@ interface removeHiddenAnswersParams { } /** - * Recursively go through the questionnaireResponse and remove qrItems whose qItems are hidden in the form + * Recursively go through the questionnaireResponse and remove qrItems whose qItems are empty in the form * * @author Sean Fong */ -export function removeHiddenAnswers(params: removeHiddenAnswersParams): QuestionnaireResponse { +export function removeEmptyAnswers(params: removeEmptyAnswersParams): QuestionnaireResponse { const { questionnaire, questionnaireResponse, @@ -59,7 +59,7 @@ export function removeHiddenAnswers(params: removeHiddenAnswersParams): Question topLevelQRItems.forEach((qrItem, i) => { const qItem = topLevelQItems[i]; - const newTopLevelQRItem = readQuestionnaireResponseItemRecursive({ + const newTopLevelQRItem = removeEmptyAnswersFromItemRecursive({ qItem, qrItem, enableWhenIsActivated, @@ -74,7 +74,7 @@ export function removeHiddenAnswers(params: removeHiddenAnswersParams): Question return questionnaireResponse; } -interface readQuestionnaireResponseItemRecursiveParams { +interface removeEmptyAnswersFromItemRecursiveParams { qItem: QuestionnaireItem; qrItem: QuestionnaireResponseItem; enableWhenIsActivated: boolean; @@ -82,8 +82,8 @@ interface readQuestionnaireResponseItemRecursiveParams { enableWhenExpressions: Record; } -function readQuestionnaireResponseItemRecursive( - params: readQuestionnaireResponseItemRecursiveParams +function removeEmptyAnswersFromItemRecursive( + params: removeEmptyAnswersFromItemRecursiveParams ): QuestionnaireResponseItem | null { const { qItem, qrItem, enableWhenIsActivated, enableWhenItems, enableWhenExpressions } = params; @@ -116,7 +116,7 @@ function readQuestionnaireResponseItemRecursive( ) { // Save qrItem if linkIds of current qItem and qrItem are the same if (qrItems[qrItemIndex] && qItems[qItemIndex].linkId === qrItems[qrItemIndex].linkId) { - const newQrItem = readQuestionnaireResponseItemRecursive({ + const newQrItem = removeEmptyAnswersFromItemRecursive({ qItem: qItems[qItemIndex], qrItem: qrItems[qrItemIndex], enableWhenIsActivated, @@ -144,17 +144,49 @@ function readQuestionnaireResponseItemRecursive( return { ...qrItem, item: newQrItems }; } - // Also perform checking if answer exists - return qrItem['answer'] ? qrItem : null; + // Also perform checks if answer exists + return answerIsEmpty( + qItem, + qrItem, + enableWhenIsActivated, + enableWhenItems, + enableWhenExpressions + ) + ? null + : qrItem; } // Process non-group items - return isHidden({ - questionnaireItem: qItem, - enableWhenIsActivated, - enableWhenItems, - enableWhenExpressions - }) + return answerIsEmpty(qItem, qrItem, enableWhenIsActivated, enableWhenItems, enableWhenExpressions) ? null : { ...qrItem }; } + +function answerIsEmpty( + qItem: QuestionnaireItem, + qrItem: QuestionnaireResponseItem, + enableWhenIsActivated: boolean, + enableWhenItems: EnableWhenItems, + enableWhenExpressions: Record +) { + if ( + isHidden({ + questionnaireItem: qItem, + enableWhenIsActivated, + enableWhenItems, + enableWhenExpressions + }) + ) { + return true; + } + + if (!qrItem.answer) { + return true; + } + + if (qrItem.answer[0]?.valueString === '') { + return true; + } + + return false; +}