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 615051623..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,12 +1,18 @@
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';
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 {
+ extractWarningLinkIds,
+ formatPopulateIssuesForUser
+} from '../../prepopulate/utils/prepopulateIssues.ts';
interface PrePopulateMenuItemProps {
sourceFhirServerUrl: string | null;
@@ -32,6 +38,7 @@ function PrePopulateMenuItem(props: PrePopulateMenuItemProps) {
} = props;
const sourceQuestionnaire = useQuestionnaireStore.use.sourceQuestionnaire();
+ const { enqueueSnackbar } = useSnackbar();
const populateEnabled = sourceFhirServerUrl !== null && patient !== null;
@@ -62,33 +69,54 @@ 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) {
+ rendererConfigStore
+ .getState()
+ .setRendererConfig({ prepopulationWarningLinkIds: extractWarningLinkIds(issues) });
+ enqueueSnackbar(formatPopulateIssuesForUser(issues), {
+ variant: 'warning',
+ persist: true,
+ action:
+ });
+ console.warn('Pre-population issues:', issues);
+ } else {
+ rendererConfigStore
+ .getState()
+ .setRendererConfig({ prepopulationWarningLinkIds: new Set() });
+ 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..3584f7644 100644
--- a/apps/smart-forms-app/src/features/prepopulate/hooks/usePopulate.tsx
+++ b/apps/smart-forms-app/src/features/prepopulate/hooks/usePopulate.tsx
@@ -15,12 +15,13 @@
* limitations under the License.
*/
-import { useState, useContext } from 'react';
+import { useContext, useState } from 'react';
import CloseSnackbar from '../../../components/Snackbar/CloseSnackbar.tsx';
import { useSnackbar } from 'notistack';
-import { ConfigContext } from '../../configChecker/contexts/ConfigContext.tsx';
+import { extractWarningLinkIds, formatPopulateIssuesForUser } from '../utils/prepopulateIssues.ts';
import {
buildForm,
+ rendererConfigStore,
useQuestionnaireResponseStore,
useQuestionnaireStore,
useTerminologyServerStore
@@ -29,6 +30,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;
@@ -45,7 +47,9 @@ function usePopulate(spinner: RendererSpinner, onStopSpinner: () => void): void
const [isPopulated, setIsPopulated] = useState(false);
const { enqueueSnackbar } = useSnackbar();
+
const { config } = useContext(ConfigContext);
+ const { showDeveloperMessages } = config;
// Do not run population if spinner purpose is "repopulate"
if (status !== 'prepopulate') {
@@ -129,18 +133,26 @@ function usePopulate(spinner: RendererSpinner, onStopSpinner: () => void): void
onStopSpinner();
if (issues) {
- // Only show the snackbar message if developer messages are enabled
- if (config.showDeveloperMessages ?? true) {
+ rendererConfigStore
+ ?.getState()
+ .setRendererConfig({ prepopulationWarningLinkIds: extractWarningLinkIds(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:
+ });
}
- // Always log to console - clinicians won't see this, but developers need it
console.warn(issues);
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
new file mode 100644
index 000000000..9358018cd
--- /dev/null
+++ b/apps/smart-forms-app/src/features/prepopulate/utils/prepopulateIssues.ts
@@ -0,0 +1,76 @@
+/*
+ * 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.'
+ );
+}
+
+/**
+ * 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 ce55c7224..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
@@ -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,6 +39,10 @@ import {
fetchResourceCallback,
fetchTerminologyCallback
} from '../../../prepopulate/utils/callback.ts';
+import {
+ extractWarningLinkIds,
+ formatPopulateIssuesForUser
+} from '../../../prepopulate/utils/prepopulateIssues.ts';
import type Client from 'fhirclient/lib/Client';
import { useState } from 'react';
@@ -130,13 +135,18 @@ 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);
+ rendererConfigStore
+ .getState()
+ .setRendererConfig({ prepopulationWarningLinkIds: extractWarningLinkIds(issues) });
+ enqueueSnackbar(formatPopulateIssuesForUser(issues), {
+ variant: 'warning',
+ persist: true,
+ action:
+ });
+ 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
}));
}
}));