Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/smart-forms-app/public/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -32,6 +38,7 @@ function PrePopulateMenuItem(props: PrePopulateMenuItemProps) {
} = props;

const sourceQuestionnaire = useQuestionnaireStore.use.sourceQuestionnaire();
const { enqueueSnackbar } = useSnackbar();

const populateEnabled = sourceFhirServerUrl !== null && patient !== null;

Expand Down Expand Up @@ -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: <CloseSnackbar variant="warning" />
});
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: <CloseSnackbar variant="warning" />
});
console.warn('Pre-population issues:', issues);
} else {
rendererConfigStore
.getState()
.setRendererConfig({ prepopulationWarningLinkIds: new Set() });
enqueueSnackbar('Form pre-populated.', { action: <CloseSnackbar /> });
}
})
.catch(() => {
onSpinnerChange({ isSpinning: false, status: null, message: '' });
enqueueSnackbar('Form could not be pre-populated.', {
variant: 'warning',
action: <CloseSnackbar variant="warning" />
});
});
});
}

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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;
Expand All @@ -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') {
Expand Down Expand Up @@ -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: <CloseSnackbar /> }
);
} else {
enqueueSnackbar(formatPopulateIssuesForUser(issues), {
variant: 'warning',
persist: true,
action: <CloseSnackbar variant="warning" />
});
}
// 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: <CloseSnackbar />
Expand Down
Original file line number Diff line number Diff line change
@@ -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<string> {
const linkIds = new Set<string>();
for (const issue of issues.issue ?? []) {
if (issue.code === 'invalid' && issue.expression) {
for (const expr of issue.expression) {
linkIds.add(expr);
}
}
}
return linkIds;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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';

Expand Down Expand Up @@ -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: <CloseSnackbar /> }
);
console.warn(issues);
rendererConfigStore
.getState()
.setRendererConfig({ prepopulationWarningLinkIds: extractWarningLinkIds(issues) });
enqueueSnackbar(formatPopulateIssuesForUser(issues), {
variant: 'warning',
persist: true,
action: <CloseSnackbar variant="warning" />
});
console.warn('Re-population issues:', issues);
return;
}
rendererConfigStore.getState().setRendererConfig({ prepopulationWarningLinkIds: new Set() });
},
onError: () => {
onSpinnerChange({ isSpinning: false, status: null, message: '' });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand All @@ -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;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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] })
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand All @@ -71,6 +75,11 @@ function ItemFieldGrid(props: ItemFieldGridProps) {
{displayInstructions}
</DisplayInstructions>
)}
{hasPrepopWarning && (
<Typography variant="caption" color="warning.main" display="block" mt={0.5}>
This field could not be pre-populated.
</Typography>
)}
</Grid>
</Grid>
);
Expand Down
Loading
Loading