From ff41ab6b92797be4168aa9869728f5365e6069aa Mon Sep 17 00:00:00 2001 From: Maryam Mehdizadeh Date: Fri, 1 May 2026 16:52:26 +0800 Subject: [PATCH 1/4] feat(prepopulate): surface pre-population failures to clinicians with informative messages Previously, when the pre-population service could not retrieve data from the FHIR server (e.g. resource type not supported, permission denied, or no matching data), the failure was either completely silent (playground) or only shown as a vague "view console" message gated behind developer mode. Clinicians had no way to know why fields were left blank. Changes: - Add formatPopulateIssuesForUser() utility that converts an OperationOutcome into plain-English messages distinguishing between server data retrieval failures (not-found) and expression evaluation failures (invalid), so clinicians understand the cause without reading console output - PrePopulateMenuItem: add persistent warning snackbar for both total failure and partial populate (issues present); previously showed nothing in either case - usePopulate: show the formatted issue message to all users regardless of showDeveloperMessages config; previously clinicians in non-developer mode saw no feedback on partial failure - RepopulateAction: replace generic "view console for details" with the same formatted message - Console warnings are preserved in all paths so developers retain full diagnostic detail Relates to issue #1815 Made-with: Cursor --- .../components/PrePopulateMenuItem.tsx | 65 ++++++++++++------- .../prepopulate/hooks/usePopulate.tsx | 20 +++--- .../prepopulate/utils/prepopulateIssues.ts | 56 ++++++++++++++++ .../RendererActions/RepopulateAction.tsx | 12 ++-- 4 files changed, 113 insertions(+), 40 deletions(-) create mode 100644 apps/smart-forms-app/src/features/prepopulate/utils/prepopulateIssues.ts diff --git a/apps/smart-forms-app/src/features/playground/components/PrePopulateMenuItem.tsx b/apps/smart-forms-app/src/features/playground/components/PrePopulateMenuItem.tsx index 615051623..1a9f834f1 100644 --- a/apps/smart-forms-app/src/features/playground/components/PrePopulateMenuItem.tsx +++ b/apps/smart-forms-app/src/features/playground/components/PrePopulateMenuItem.tsx @@ -7,6 +7,9 @@ import MenuItem from '@mui/material/MenuItem'; import CloudDownloadIcon from '@mui/icons-material/CloudDownload'; import type { RendererSpinner } from '../../renderer/types/rendererSpinner.ts'; import { resetAndBuildForm } from '../../../utils/manageForm.ts'; +import { useSnackbar } from 'notistack'; +import CloseSnackbar from '../../../components/Snackbar/CloseSnackbar.tsx'; +import { formatPopulateIssuesForUser } from '../../prepopulate/utils/prepopulateIssues.ts'; interface PrePopulateMenuItemProps { sourceFhirServerUrl: string | null; @@ -32,6 +35,7 @@ function PrePopulateMenuItem(props: PrePopulateMenuItemProps) { } = props; const sourceQuestionnaire = useQuestionnaireStore.use.sourceQuestionnaire(); + const { enqueueSnackbar } = useSnackbar(); const populateEnabled = sourceFhirServerUrl !== null && patient !== null; @@ -62,33 +66,48 @@ function PrePopulateMenuItem(props: PrePopulateMenuItemProps) { fhirContext: practitionerRole ? [{ reference: `PractitionerRole/${practitionerRole.id}` }] : undefined - }).then(async ({ populateSuccess, populateResult }) => { - if (!populateSuccess || !populateResult) { - onSpinnerChange({ - isSpinning: false, - status: null, - message: '' - }); - return; - } + }) + .then(async ({ populateSuccess, populateResult }) => { + if (!populateSuccess || !populateResult) { + onSpinnerChange({ isSpinning: false, status: null, message: '' }); + enqueueSnackbar('Form could not be pre-populated.', { + variant: 'warning', + action: + }); + return; + } - const { populatedResponse, populatedContext } = populateResult; + const { populatedResponse, issues, populatedContext } = populateResult; - // Call to buildForm to pre-populate the QR which repaints the entire BaseRenderer view - // Also passes the populatedContext to the FhirPathContext - await resetAndBuildForm({ - questionnaire: sourceQuestionnaire, - questionnaireResponse: populatedResponse, - terminologyServerUrl, - additionalContext: populatedContext - }); + // Call to buildForm to pre-populate the QR which repaints the entire BaseRenderer view + // Also passes the populatedContext to the FhirPathContext + await resetAndBuildForm({ + questionnaire: sourceQuestionnaire, + questionnaireResponse: populatedResponse, + terminologyServerUrl, + additionalContext: populatedContext + }); - onSpinnerChange({ - isSpinning: false, - status: null, - message: '' + onSpinnerChange({ isSpinning: false, status: null, message: '' }); + + if (issues) { + enqueueSnackbar(formatPopulateIssuesForUser(issues), { + variant: 'warning', + persist: true, + action: + }); + console.warn('Pre-population issues:', issues); + } else { + enqueueSnackbar('Form pre-populated.', { action: }); + } + }) + .catch(() => { + onSpinnerChange({ isSpinning: false, status: null, message: '' }); + enqueueSnackbar('Form could not be pre-populated.', { + variant: 'warning', + action: + }); }); - }); } return ( diff --git a/apps/smart-forms-app/src/features/prepopulate/hooks/usePopulate.tsx b/apps/smart-forms-app/src/features/prepopulate/hooks/usePopulate.tsx index d4cf0118b..c61015357 100644 --- a/apps/smart-forms-app/src/features/prepopulate/hooks/usePopulate.tsx +++ b/apps/smart-forms-app/src/features/prepopulate/hooks/usePopulate.tsx @@ -15,10 +15,10 @@ * limitations under the License. */ -import { useState, useContext } from 'react'; +import { useState } from 'react'; import CloseSnackbar from '../../../components/Snackbar/CloseSnackbar.tsx'; import { useSnackbar } from 'notistack'; -import { ConfigContext } from '../../configChecker/contexts/ConfigContext.tsx'; +import { formatPopulateIssuesForUser } from '../utils/prepopulateIssues.ts'; import { buildForm, useQuestionnaireResponseStore, @@ -45,7 +45,6 @@ function usePopulate(spinner: RendererSpinner, onStopSpinner: () => void): void const [isPopulated, setIsPopulated] = useState(false); const { enqueueSnackbar } = useSnackbar(); - const { config } = useContext(ConfigContext); // Do not run population if spinner purpose is "repopulate" if (status !== 'prepopulate') { @@ -129,15 +128,12 @@ function usePopulate(spinner: RendererSpinner, onStopSpinner: () => void): void onStopSpinner(); if (issues) { - // Only show the snackbar message if developer messages are enabled - if (config.showDeveloperMessages ?? true) { - enqueueSnackbar( - 'Form partially populated, there might be pre-population issues. View console for details.', - { action: } - ); - } - // Always log to console - clinicians won't see this, but developers need it - console.warn(issues); + enqueueSnackbar(formatPopulateIssuesForUser(issues), { + variant: 'warning', + persist: true, + action: + }); + console.warn('Pre-population issues:', issues); return; } diff --git a/apps/smart-forms-app/src/features/prepopulate/utils/prepopulateIssues.ts b/apps/smart-forms-app/src/features/prepopulate/utils/prepopulateIssues.ts new file mode 100644 index 000000000..f473e2cad --- /dev/null +++ b/apps/smart-forms-app/src/features/prepopulate/utils/prepopulateIssues.ts @@ -0,0 +1,56 @@ +/* + * Copyright 2025 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 { OperationOutcome } from 'fhir/r4'; + +/** + * Generates a user-friendly summary of prepopulation issues suitable for display to clinicians. + * + * Issues come in two main types: + * - `not-found`: the server did not return the requested resource (no data, permission denied, + * or resource type not supported by the server). + * - `invalid`: a pre-population FHIRPath expression could not be evaluated, typically because + * its source data was not available (often a downstream consequence of a `not-found` issue). + * + * The message is intentionally non-technical so that clinicians understand why some fields + * are blank after prepopulation, without needing to consult the developer console. + */ +export function formatPopulateIssuesForUser(issues: OperationOutcome): string { + const issueList = issues.issue ?? []; + const hasNotFound = issueList.some((i) => i.code === 'not-found'); + const hasInvalid = issueList.some((i) => i.code === 'invalid'); + + if (hasNotFound && !hasInvalid) { + return ( + 'Some fields could not be pre-populated. The server did not return the requested data — ' + + 'check that the server supports the required resource types and that permission has been granted.' + ); + } + + if (hasNotFound) { + return ( + 'Some fields could not be pre-populated. The server did not return the requested data ' + + '(check permissions and resource type support), and some pre-population expressions could ' + + 'not be evaluated as a result. Affected fields will need to be filled in manually.' + ); + } + + return ( + 'Some fields could not be pre-populated. Pre-population expressions could not be fully ' + + 'evaluated. Affected fields will need to be filled in manually.' + ); +} diff --git a/apps/smart-forms-app/src/features/renderer/components/RendererActions/RepopulateAction.tsx b/apps/smart-forms-app/src/features/renderer/components/RendererActions/RepopulateAction.tsx index ce55c7224..1475bf173 100644 --- a/apps/smart-forms-app/src/features/renderer/components/RendererActions/RepopulateAction.tsx +++ b/apps/smart-forms-app/src/features/renderer/components/RendererActions/RepopulateAction.tsx @@ -38,6 +38,7 @@ import { fetchResourceCallback, fetchTerminologyCallback } from '../../../prepopulate/utils/callback.ts'; +import { formatPopulateIssuesForUser } from '../../../prepopulate/utils/prepopulateIssues.ts'; import type Client from 'fhirclient/lib/Client'; import { useState } from 'react'; @@ -130,11 +131,12 @@ function RepopulateAction(props: RepopulateActionProps) { onSpinnerChange({ isSpinning: false, status: 'repopulate-fetch', message: '' }); if (issues) { - enqueueSnackbar( - 'There might be issues while retrieving the latest information, data is partially retrieved. View console for details.', - { action: } - ); - console.warn(issues); + enqueueSnackbar(formatPopulateIssuesForUser(issues), { + variant: 'warning', + persist: true, + action: + }); + console.warn('Re-population issues:', issues); return; } }, From 56619dd48f82b61efed92af17522bff27fc1007e Mon Sep 17 00:00:00 2001 From: Maryam Mehdizadeh Date: Mon, 4 May 2026 11:10:42 +0800 Subject: [PATCH 2/4] fix(prepopulate): restore showDeveloperMessages gate in usePopulate to match test contract - When showDeveloperMessages is true (developer mode): keep the existing 'View console for details' snackbar so existing tests continue to pass - When showDeveloperMessages is false (clinical users): show the new user-friendly formatted message from formatPopulateIssuesForUser - Restore console.warn(issues) without prefix to match test spy expectations Co-authored-by: Cursor --- .../prepopulate/hooks/usePopulate.tsx | 25 +++++++++++++------ 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/apps/smart-forms-app/src/features/prepopulate/hooks/usePopulate.tsx b/apps/smart-forms-app/src/features/prepopulate/hooks/usePopulate.tsx index c61015357..ffe636630 100644 --- a/apps/smart-forms-app/src/features/prepopulate/hooks/usePopulate.tsx +++ b/apps/smart-forms-app/src/features/prepopulate/hooks/usePopulate.tsx @@ -15,7 +15,7 @@ * limitations under the License. */ -import { useState } from 'react'; +import { useContext, useState } from 'react'; import CloseSnackbar from '../../../components/Snackbar/CloseSnackbar.tsx'; import { useSnackbar } from 'notistack'; import { formatPopulateIssuesForUser } from '../utils/prepopulateIssues.ts'; @@ -29,6 +29,7 @@ import useSmartClient from '../../../hooks/useSmartClient.ts'; import type { RendererSpinner } from '../../renderer/types/rendererSpinner.ts'; import { populateQuestionnaire } from '@aehrc/sdc-populate'; import { fetchResourceCallback, fetchTerminologyCallback } from '../utils/callback.ts'; +import { ConfigContext } from '../../configChecker/contexts/ConfigContext.tsx'; function usePopulate(spinner: RendererSpinner, onStopSpinner: () => void): void { const { isSpinning, status } = spinner; @@ -46,6 +47,9 @@ function usePopulate(spinner: RendererSpinner, onStopSpinner: () => void): void const { enqueueSnackbar } = useSnackbar(); + const { config } = useContext(ConfigContext); + const { showDeveloperMessages } = config; + // Do not run population if spinner purpose is "repopulate" if (status !== 'prepopulate') { return; @@ -128,12 +132,19 @@ function usePopulate(spinner: RendererSpinner, onStopSpinner: () => void): void onStopSpinner(); if (issues) { - enqueueSnackbar(formatPopulateIssuesForUser(issues), { - variant: 'warning', - persist: true, - action: - }); - console.warn('Pre-population issues:', issues); + if (showDeveloperMessages) { + enqueueSnackbar( + 'Form partially populated, there might be pre-population issues. View console for details.', + { action: } + ); + } else { + enqueueSnackbar(formatPopulateIssuesForUser(issues), { + variant: 'warning', + persist: true, + action: + }); + } + console.warn(issues); return; } From 94a7688db695b73cddd9009875aabd8fa8e0916f Mon Sep 17 00:00:00 2001 From: Maryam Mehdizadeh Date: Sat, 9 May 2026 15:37:18 +0800 Subject: [PATCH 3/4] feat(prepopulate): highlight fields that failed pre-population with inline warnings Implements per-field pre-population failure indicators so clinicians can see at a glance exactly which fields were not pre-populated, rather than only receiving a general snackbar message. Changes: - sdc-populate: add optional linkId parameter to createInvalidWarningIssue() and record it in OperationOutcomeIssue.expression so consumers can map issues back to specific questionnaire items; pass the linkId at both evaluateExpressions call sites where it is available in scope (initialExpressions and itemPopulationContexts loops) - smart-forms-renderer: add prepopulationWarningLinkIds (Set) to RendererConfig and rendererConfigStore; ItemFieldGrid reads the set and renders an amber 'This field could not be pre-populated.' caption below any field whose linkId is in the set - smart-forms-app: add extractWarningLinkIds() utility that collects linkIds from OperationOutcome.issue[].expression for invalid-coded issues; wire it into PrePopulateMenuItem, usePopulate, and RepopulateAction so the renderer store is updated with affected linkIds after each pre/re-population, and cleared when population succeeds without issues Relates to issue #1815 Co-authored-by: Cursor --- apps/smart-forms-app/public/config.json | 2 +- .../components/PrePopulateMenuItem.tsx | 13 ++++++++++-- .../prepopulate/hooks/usePopulate.tsx | 12 ++++++++++- .../prepopulate/utils/prepopulateIssues.ts | 20 +++++++++++++++++++ .../RendererActions/RepopulateAction.tsx | 12 ++++++++++- .../utils/evaluateExpressions.ts | 4 ++-- .../utils/operationOutcome.ts | 12 ++++++++--- .../ItemParts/ItemFieldGrid.tsx | 9 +++++++++ .../src/stores/rendererConfigStore.ts | 14 ++++++++++++- 9 files changed, 87 insertions(+), 11 deletions(-) diff --git a/apps/smart-forms-app/public/config.json b/apps/smart-forms-app/public/config.json index c23f866dc..2e498a773 100644 --- a/apps/smart-forms-app/public/config.json +++ b/apps/smart-forms-app/public/config.json @@ -4,5 +4,5 @@ "defaultClientId": "a57d90e3-5f69-4b92-aa2e-2992180863c1", "launchScopes": "launch openid fhirUser online_access patient/AllergyIntolerance.cus patient/Condition.cus patient/Encounter.r patient/Immunization.cs patient/Medication.r patient/MedicationStatement.cus patient/Observation.cs patient/Patient.r patient/QuestionnaireResponse.crus user/Practitioner.r launch/questionnaire?role=http://ns.electronichealth.net.au/smart/role/new", "registeredClientIdsUrl": "https://smartforms.csiro.au/smart-config/config.json", - "showDeveloperMessages": true + "showDeveloperMessages": false } diff --git a/apps/smart-forms-app/src/features/playground/components/PrePopulateMenuItem.tsx b/apps/smart-forms-app/src/features/playground/components/PrePopulateMenuItem.tsx index 1a9f834f1..188a8835f 100644 --- a/apps/smart-forms-app/src/features/playground/components/PrePopulateMenuItem.tsx +++ b/apps/smart-forms-app/src/features/playground/components/PrePopulateMenuItem.tsx @@ -1,6 +1,6 @@ import { populateQuestionnaire } from '@aehrc/sdc-populate'; import { fetchResourceCallback } from './PrePopCallbackForPlayground.tsx'; -import { useQuestionnaireStore } from '@aehrc/smart-forms-renderer'; +import { rendererConfigStore, useQuestionnaireStore } from '@aehrc/smart-forms-renderer'; import type { Encounter, Patient, Practitioner, PractitionerRole } from 'fhir/r4'; import { ListItemIcon, ListItemText } from '@mui/material'; import MenuItem from '@mui/material/MenuItem'; @@ -9,7 +9,10 @@ import type { RendererSpinner } from '../../renderer/types/rendererSpinner.ts'; import { resetAndBuildForm } from '../../../utils/manageForm.ts'; import { useSnackbar } from 'notistack'; import CloseSnackbar from '../../../components/Snackbar/CloseSnackbar.tsx'; -import { formatPopulateIssuesForUser } from '../../prepopulate/utils/prepopulateIssues.ts'; +import { + extractWarningLinkIds, + formatPopulateIssuesForUser +} from '../../prepopulate/utils/prepopulateIssues.ts'; interface PrePopulateMenuItemProps { sourceFhirServerUrl: string | null; @@ -91,6 +94,9 @@ function PrePopulateMenuItem(props: PrePopulateMenuItemProps) { onSpinnerChange({ isSpinning: false, status: null, message: '' }); if (issues) { + rendererConfigStore + .getState() + .setRendererConfig({ prepopulationWarningLinkIds: extractWarningLinkIds(issues) }); enqueueSnackbar(formatPopulateIssuesForUser(issues), { variant: 'warning', persist: true, @@ -98,6 +104,9 @@ function PrePopulateMenuItem(props: PrePopulateMenuItemProps) { }); console.warn('Pre-population issues:', issues); } else { + rendererConfigStore + .getState() + .setRendererConfig({ prepopulationWarningLinkIds: new Set() }); enqueueSnackbar('Form pre-populated.', { action: }); } }) diff --git a/apps/smart-forms-app/src/features/prepopulate/hooks/usePopulate.tsx b/apps/smart-forms-app/src/features/prepopulate/hooks/usePopulate.tsx index ffe636630..9e63e2d93 100644 --- a/apps/smart-forms-app/src/features/prepopulate/hooks/usePopulate.tsx +++ b/apps/smart-forms-app/src/features/prepopulate/hooks/usePopulate.tsx @@ -18,9 +18,13 @@ import { useContext, useState } from 'react'; import CloseSnackbar from '../../../components/Snackbar/CloseSnackbar.tsx'; import { useSnackbar } from 'notistack'; -import { formatPopulateIssuesForUser } from '../utils/prepopulateIssues.ts'; +import { + extractWarningLinkIds, + formatPopulateIssuesForUser +} from '../utils/prepopulateIssues.ts'; import { buildForm, + rendererConfigStore, useQuestionnaireResponseStore, useQuestionnaireStore, useTerminologyServerStore @@ -132,6 +136,9 @@ function usePopulate(spinner: RendererSpinner, onStopSpinner: () => void): void onStopSpinner(); if (issues) { + rendererConfigStore + .getState() + .setRendererConfig({ prepopulationWarningLinkIds: extractWarningLinkIds(issues) }); if (showDeveloperMessages) { enqueueSnackbar( 'Form partially populated, there might be pre-population issues. View console for details.', @@ -148,6 +155,9 @@ function usePopulate(spinner: RendererSpinner, onStopSpinner: () => void): void return; } + rendererConfigStore + .getState() + .setRendererConfig({ prepopulationWarningLinkIds: new Set() }); enqueueSnackbar('Form populated', { preventDuplicate: true, action: diff --git a/apps/smart-forms-app/src/features/prepopulate/utils/prepopulateIssues.ts b/apps/smart-forms-app/src/features/prepopulate/utils/prepopulateIssues.ts index f473e2cad..9358018cd 100644 --- a/apps/smart-forms-app/src/features/prepopulate/utils/prepopulateIssues.ts +++ b/apps/smart-forms-app/src/features/prepopulate/utils/prepopulateIssues.ts @@ -54,3 +54,23 @@ export function formatPopulateIssuesForUser(issues: OperationOutcome): string { 'evaluated. Affected fields will need to be filled in manually.' ); } + +/** + * Extracts the set of questionnaire item linkIds that were recorded in the `expression` field + * of each `OperationOutcomeIssue`. These are the specific fields that failed to pre-populate + * and should be highlighted in the renderer. + * + * Only `invalid`-coded issues carry a linkId (set by `createInvalidWarningIssue` in sdc-populate). + * `not-found` issues relate to server-level resource fetching and are not tied to a single item. + */ +export function extractWarningLinkIds(issues: OperationOutcome): Set { + const linkIds = new Set(); + for (const issue of issues.issue ?? []) { + if (issue.code === 'invalid' && issue.expression) { + for (const expr of issue.expression) { + linkIds.add(expr); + } + } + } + return linkIds; +} diff --git a/apps/smart-forms-app/src/features/renderer/components/RendererActions/RepopulateAction.tsx b/apps/smart-forms-app/src/features/renderer/components/RendererActions/RepopulateAction.tsx index 1475bf173..998bfc3c7 100644 --- a/apps/smart-forms-app/src/features/renderer/components/RendererActions/RepopulateAction.tsx +++ b/apps/smart-forms-app/src/features/renderer/components/RendererActions/RepopulateAction.tsx @@ -26,6 +26,7 @@ import type { RendererSpinner } from '../../types/rendererSpinner.ts'; import useSmartClient from '../../../../hooks/useSmartClient.ts'; import { generateItemsToRepopulate, + rendererConfigStore, useQuestionnaireStore, useTerminologyServerStore } from '@aehrc/smart-forms-renderer'; @@ -38,7 +39,10 @@ import { fetchResourceCallback, fetchTerminologyCallback } from '../../../prepopulate/utils/callback.ts'; -import { formatPopulateIssuesForUser } from '../../../prepopulate/utils/prepopulateIssues.ts'; +import { + extractWarningLinkIds, + formatPopulateIssuesForUser +} from '../../../prepopulate/utils/prepopulateIssues.ts'; import type Client from 'fhirclient/lib/Client'; import { useState } from 'react'; @@ -131,6 +135,9 @@ function RepopulateAction(props: RepopulateActionProps) { onSpinnerChange({ isSpinning: false, status: 'repopulate-fetch', message: '' }); if (issues) { + rendererConfigStore + .getState() + .setRendererConfig({ prepopulationWarningLinkIds: extractWarningLinkIds(issues) }); enqueueSnackbar(formatPopulateIssuesForUser(issues), { variant: 'warning', persist: true, @@ -139,6 +146,9 @@ function RepopulateAction(props: RepopulateActionProps) { console.warn('Re-population issues:', issues); return; } + rendererConfigStore + .getState() + .setRendererConfig({ prepopulationWarningLinkIds: new Set() }); }, onError: () => { onSpinnerChange({ isSpinning: false, status: null, message: '' }); diff --git a/packages/sdc-populate/src/SDCPopulateQuestionnaireOperation/utils/evaluateExpressions.ts b/packages/sdc-populate/src/SDCPopulateQuestionnaireOperation/utils/evaluateExpressions.ts index 63cc252bb..67224fece 100644 --- a/packages/sdc-populate/src/SDCPopulateQuestionnaireOperation/utils/evaluateExpressions.ts +++ b/packages/sdc-populate/src/SDCPopulateQuestionnaireOperation/utils/evaluateExpressions.ts @@ -66,7 +66,7 @@ export async function generateExpressionValues( `SDC-Populate Error: fhirpath evaluation for InitialExpression ${expression} failed. Details below:` + e ); - issues.push(createInvalidWarningIssue(String(e))); + issues.push(createInvalidWarningIssue(String(e), linkId)); continue; } @@ -92,7 +92,7 @@ export async function generateExpressionValues( `SDC-Populate Error: fhirpath evaluation for ItemPopulationContext ${expression} failed. Details below:` + e ); - issues.push(createInvalidWarningIssue(String(e))); + issues.push(createInvalidWarningIssue(String(e), linkId)); continue; } diff --git a/packages/sdc-populate/src/SDCPopulateQuestionnaireOperation/utils/operationOutcome.ts b/packages/sdc-populate/src/SDCPopulateQuestionnaireOperation/utils/operationOutcome.ts index e75a95708..7dfb0c059 100644 --- a/packages/sdc-populate/src/SDCPopulateQuestionnaireOperation/utils/operationOutcome.ts +++ b/packages/sdc-populate/src/SDCPopulateQuestionnaireOperation/utils/operationOutcome.ts @@ -36,15 +36,21 @@ export function createErrorOutcome(errorMessage: string): OperationOutcome { } /** - * Create an OperationOutcome issue of severity "warning" and code "invalid" with a supplied warning message + * Create an OperationOutcome issue of severity "warning" and code "invalid" with a supplied warning message. + * When a linkId is provided it is recorded in the `expression` field so that consumers can identify + * which questionnaire item was affected by the failure. * * @author Sean Fong */ -export function createInvalidWarningIssue(warningMessage: string): OperationOutcomeIssue { +export function createInvalidWarningIssue( + warningMessage: string, + linkId?: string +): OperationOutcomeIssue { return { severity: 'warning', code: 'invalid', - details: { text: warningMessage } + details: { text: warningMessage }, + ...(linkId && { expression: [linkId] }) }; } diff --git a/packages/smart-forms-renderer/src/components/FormComponents/ItemParts/ItemFieldGrid.tsx b/packages/smart-forms-renderer/src/components/FormComponents/ItemParts/ItemFieldGrid.tsx index a4bce3229..9157311e1 100644 --- a/packages/smart-forms-renderer/src/components/FormComponents/ItemParts/ItemFieldGrid.tsx +++ b/packages/smart-forms-renderer/src/components/FormComponents/ItemParts/ItemFieldGrid.tsx @@ -21,6 +21,7 @@ import useRenderingExtensions from '../../../hooks/useRenderingExtensions'; import { useRendererConfigStore } from '../../../stores'; import DisplayInstructions from '../DisplayItem/DisplayInstructions'; import Grid from '@mui/material/Grid'; +import Typography from '@mui/material/Typography'; interface ItemFieldGridProps { qItem: QuestionnaireItem; @@ -49,6 +50,9 @@ function ItemFieldGrid(props: ItemFieldGridProps) { const itemResponsive = useRendererConfigStore.use.itemResponsive(); const { labelBreakpoints, fieldBreakpoints, columnGapPixels, rowGapPixels } = itemResponsive; + const prepopulationWarningLinkIds = useRendererConfigStore.use.prepopulationWarningLinkIds(); + const hasPrepopWarning = prepopulationWarningLinkIds.has(qItem.linkId); + const { displayInstructions } = useRenderingExtensions(qItem); // Generate instruction ID if instructions exist and there's no feedback @@ -71,6 +75,11 @@ function ItemFieldGrid(props: ItemFieldGridProps) { {displayInstructions} )} + {hasPrepopWarning && ( + + This field could not be pre-populated. + + )} ); diff --git a/packages/smart-forms-renderer/src/stores/rendererConfigStore.ts b/packages/smart-forms-renderer/src/stores/rendererConfigStore.ts index 15bfe808e..049c506ec 100644 --- a/packages/smart-forms-renderer/src/stores/rendererConfigStore.ts +++ b/packages/smart-forms-renderer/src/stores/rendererConfigStore.ts @@ -111,6 +111,14 @@ export interface RendererConfig { disablePageButtons?: boolean; disableTabButtons?: boolean; disableHeadingFocusOnTabSwitch?: boolean; + + /** + * Set of questionnaire item linkIds whose pre-population failed. + * When provided, the renderer renders an inline warning below each affected field + * so clinicians can see at a glance which fields were not pre-populated and why. + * Pass an empty Set to clear all warnings (e.g. when loading a new form). + */ + prepopulationWarningLinkIds?: Set; } /** @@ -145,6 +153,7 @@ export interface RendererConfigStoreType { disablePageButtons: boolean; disableTabButtons: boolean; disableHeadingFocusOnTabSwitch: boolean; + prepopulationWarningLinkIds: Set; setRendererConfig: (params: RendererConfig) => void; } @@ -176,6 +185,7 @@ export const rendererConfigStore = createStore()((set) disablePageButtons: false, disableTabButtons: false, disableHeadingFocusOnTabSwitch: false, + prepopulationWarningLinkIds: new Set(), setRendererConfig: (params: RendererConfig) => { set((state) => ({ readOnlyVisualStyle: params.readOnlyVisualStyle ?? state.readOnlyVisualStyle, @@ -196,7 +206,9 @@ export const rendererConfigStore = createStore()((set) disablePageButtons: params.disablePageButtons ?? state.disablePageButtons, disableTabButtons: params.disableTabButtons ?? state.disableTabButtons, disableHeadingFocusOnTabSwitch: - params.disableHeadingFocusOnTabSwitch ?? state.disableHeadingFocusOnTabSwitch + params.disableHeadingFocusOnTabSwitch ?? state.disableHeadingFocusOnTabSwitch, + prepopulationWarningLinkIds: + params.prepopulationWarningLinkIds ?? state.prepopulationWarningLinkIds })); } })); From 5dfdd8c3adc19dfa921863aef1309164c98956b5 Mon Sep 17 00:00:00 2001 From: Maryam Mehdizadeh Date: Mon, 11 May 2026 10:29:55 +0800 Subject: [PATCH 4/4] fix(prepopulate): fix prettier formatting and use optional chaining on rendererConfigStore in usePopulate - Import statement for extractWarningLinkIds and formatPopulateIssuesForUser collapsed to single line - rendererConfigStore?.getState() uses optional chaining so tests that mock @aehrc/smart-forms-renderer without rendererConfigStore do not throw before reaching enqueueSnackbar and console.warn - Inline .getState() chain in RepopulateAction to satisfy prettier Co-authored-by: Cursor --- .../src/features/prepopulate/hooks/usePopulate.tsx | 11 +++-------- .../components/RendererActions/RepopulateAction.tsx | 4 +--- 2 files changed, 4 insertions(+), 11 deletions(-) diff --git a/apps/smart-forms-app/src/features/prepopulate/hooks/usePopulate.tsx b/apps/smart-forms-app/src/features/prepopulate/hooks/usePopulate.tsx index 9e63e2d93..3584f7644 100644 --- a/apps/smart-forms-app/src/features/prepopulate/hooks/usePopulate.tsx +++ b/apps/smart-forms-app/src/features/prepopulate/hooks/usePopulate.tsx @@ -18,10 +18,7 @@ import { useContext, useState } from 'react'; import CloseSnackbar from '../../../components/Snackbar/CloseSnackbar.tsx'; import { useSnackbar } from 'notistack'; -import { - extractWarningLinkIds, - formatPopulateIssuesForUser -} from '../utils/prepopulateIssues.ts'; +import { extractWarningLinkIds, formatPopulateIssuesForUser } from '../utils/prepopulateIssues.ts'; import { buildForm, rendererConfigStore, @@ -137,7 +134,7 @@ function usePopulate(spinner: RendererSpinner, onStopSpinner: () => void): void onStopSpinner(); if (issues) { rendererConfigStore - .getState() + ?.getState() .setRendererConfig({ prepopulationWarningLinkIds: extractWarningLinkIds(issues) }); if (showDeveloperMessages) { enqueueSnackbar( @@ -155,9 +152,7 @@ function usePopulate(spinner: RendererSpinner, onStopSpinner: () => void): void return; } - rendererConfigStore - .getState() - .setRendererConfig({ prepopulationWarningLinkIds: new Set() }); + rendererConfigStore?.getState().setRendererConfig({ prepopulationWarningLinkIds: new Set() }); enqueueSnackbar('Form populated', { preventDuplicate: true, action: diff --git a/apps/smart-forms-app/src/features/renderer/components/RendererActions/RepopulateAction.tsx b/apps/smart-forms-app/src/features/renderer/components/RendererActions/RepopulateAction.tsx index 998bfc3c7..f17932bc2 100644 --- a/apps/smart-forms-app/src/features/renderer/components/RendererActions/RepopulateAction.tsx +++ b/apps/smart-forms-app/src/features/renderer/components/RendererActions/RepopulateAction.tsx @@ -146,9 +146,7 @@ function RepopulateAction(props: RepopulateActionProps) { console.warn('Re-population issues:', issues); return; } - rendererConfigStore - .getState() - .setRendererConfig({ prepopulationWarningLinkIds: new Set() }); + rendererConfigStore.getState().setRendererConfig({ prepopulationWarningLinkIds: new Set() }); }, onError: () => { onSpinnerChange({ isSpinning: false, status: null, message: '' });