Skip to content

[ENG-761] Draft support for Questionnaires with Structured Questions - #16573

Open
gigincg wants to merge 3 commits into
developfrom
ENG-761-structured-questionnaire-drafts-context-snapshot-recovery-reconciliation
Open

[ENG-761] Draft support for Questionnaires with Structured Questions#16573
gigincg wants to merge 3 commits into
developfrom
ENG-761-structured-questionnaire-drafts-context-snapshot-recovery-reconciliation

Conversation

@gigincg

@gigincg gigincg commented Jul 15, 2026

Copy link
Copy Markdown
Member

Proposed Changes

Tagging: @ohcnetwork/care-fe-code-reviewers

Merge Checklist

  • Add specs that demonstrate the bug or test the new feature.
  • Update product documentation.
  • Ensure that UI text is placed in I18n files.
  • Prepare a screenshot or demo video for the changelog entry and attach it to the issue.
  • Request peer reviews.
  • Complete QA on mobile devices.
  • Complete QA on desktop devices.
  • Add or update Playwright tests for related changes

Summary by CodeRabbit

  • New Features
    • Added structured questionnaire draft recovery that reconciles saved draft data with the latest server records.
    • Shows a “data changed” confirmation view listing added, removed, and updated records, with localized status labels and messaging.
  • Bug Fixes
    • Prevents server-managed audit/metadata changes from being treated as meaningful draft differences.
    • Better preserves user edits while incorporating current server data during reconciliation, including improved handling of newly added and removed records.

@gigincg
gigincg requested review from a team and Copilot July 15, 2026 16:11
@gigincg
gigincg requested a review from a team as a code owner July 15, 2026 16:11
@github-actions

Copy link
Copy Markdown

⚠️ Merge Checklist Incomplete

Thank you for your contribution! To help us review your PR efficiently, please complete the merge checklist in your PR description.

Your PR will be reviewed once you have marked the appropriate checklist items.

To update the checklist:

  • Change - [ ] to - [x] for completed items
  • Only check items that are relevant to your PR
  • Leave items unchecked if they don't apply

The checklist helps ensure code quality, testing coverage, and documentation are properly addressed.

@github-actions github-actions Bot added the Type Changes Contains changes in typescript types label Jul 15, 2026
@coderabbitai

coderabbitai Bot commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Structured questionnaire responses now preserve fetched record context, reconcile restored drafts with current server data, identify added, removed, and updated records, and show localized changes before continuation.

Changes

Structured draft reconciliation

Layer / File(s) Summary
Context contracts and reconciliation utilities
src/types/questionnaire/form.ts, src/components/Questionnaire/structured/*
Adds structured response context types, id-based diffing, draft-value merging, and tests for comparison and merging behavior.
Context propagation through question inputs
src/components/Questionnaire/QuestionRenderer.tsx, src/components/Questionnaire/QuestionTypes/*, src/pages/Encounters/tabs/overview/FormSubmissionDrafts.tsx
Threads setResponseContext through question rendering and populates context from fetched structured records.
Draft restoration and reconciliation flow
src/components/Questionnaire/QuestionnaireForm.tsx
Snapshots structured draft context, restores fresh context, merges records, computes server changes, and updates derived context without marking the form dirty.
Draft change notification UI
src/components/Questionnaire/DraftContextChangesScreen.tsx, public/locale/en.json
Adds a localized overlay showing added, removed, and updated records with a continue action.

Possibly related PRs

Suggested reviewers: amjithtitus09, jacobjeevan

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description includes tagging and a checklist, but it omits the required Proposed Changes details and issue reference. Add a Proposed Changes section with Fixes #issue_number and 1-3 concrete change bullets, plus any missing context.
Docstring Coverage ⚠️ Warning Docstring coverage is 18.75% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately describes the main change: draft support for questionnaires with structured questions.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ENG-761-structured-questionnaire-drafts-context-snapshot-recovery-reconciliation

Comment @coderabbitai help to get the list of available commands.

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 15, 2026

Copy link
Copy Markdown

Deploying care-preview with  Cloudflare Pages  Cloudflare Pages

Latest commit: 59cd45c
Status: ✅  Deploy successful!
Preview URL: https://7a3eeb6f.care-preview-a7w.pages.dev
Branch Preview URL: https://eng-761-structured-questionn.care-preview-a7w.pages.dev

View logs

@gigincg gigincg changed the title ENG761 Draft support for Questionnaires with Structured Questions [ENG-761] Draft support for Questionnaires with Structured Questions Jul 15, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/components/Questionnaire/DraftContextChangesScreen.tsx`:
- Around line 33-76: Update ChangeGroup’s dynamic t(meta.labelKey) call to use
explicit branches for each ChangeKind, with statically detectable
t("draft_changes_added"), t("draft_changes_removed"), and
t("draft_changes_updated") calls. Preserve the existing KIND_META styling and
label behavior while ensuring all three translation keys remain discoverable by
i18n cleanup.
- Around line 105-143: Update the blocking screen around the component rendering
this questionnaire change content to use the Shadcn/Radix Dialog primitive
instead of a standalone fixed layout. Render the existing title and description
through DialogTitle and DialogDescription, keep the changes and Continue action
inside DialogContent, and configure it as a non-dismissible modal so focus is
trapped and the background questionnaire is inert while preserving onContinue
behavior.

In `@src/components/Questionnaire/QuestionnaireForm.tsx`:
- Around line 694-700: Update the reconciliation flow in QuestionnaireForm so a
fetch timeout with any applicable response still lacking context does not
complete reconciliation. Do not allow the existing changes/finalization logic
around the responses mapping and lines 735-738 to clear the pending state;
instead retain the blocked state and expose the existing retry or error state
until every applicable context has been reconciled.

In `@src/components/Questionnaire/structured/contextMatch.test.ts`:
- Around line 92-108: Remove the performance.now timing setup and the 16ms
assertion from the “scales to 100 large records well under a frame” test, while
retaining the large-record inputs and the functional d.matches assertion. Leave
timing validation to a separate benchmark.
- Around line 8-9: Convert the pure fixture helpers rec and big in the
questionnaire context-match tests from arrow-function assignments to function
declarations, preserving their parameters, return values, and behavior.

In `@src/components/Questionnaire/structured/contextMatch.ts`:
- Around line 3-8: Replace the RecordLike type alias with an interface while
preserving its optional id and dirty properties and its string-indexed unknown
values. Keep the existing RecordLike name and documentation unchanged.
- Around line 10-19: Remove "recorded_date" from VOLATILE_FIELDS so changes to
this clinical date are treated as content drift, while continuing to ignore the
remaining server-managed audit fields. Add a regression test for the
context-matching flow where only recorded_date differs and assert that
changed.length equals 1.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 05015638-ecc5-4b46-9e1b-2c781bf7d28b

📥 Commits

Reviewing files that changed from the base of the PR and between dd3299d and d1c9343.

📒 Files selected for processing (16)
  • public/locale/en.json
  • src/components/Questionnaire/DraftContextChangesScreen.tsx
  • src/components/Questionnaire/QuestionRenderer.tsx
  • src/components/Questionnaire/QuestionTypes/AllergyQuestion.tsx
  • src/components/Questionnaire/QuestionTypes/DiagnosisQuestion.tsx
  • src/components/Questionnaire/QuestionTypes/EncounterQuestion.tsx
  • src/components/Questionnaire/QuestionTypes/MedicationRequestQuestion.tsx
  • src/components/Questionnaire/QuestionTypes/MedicationStatementQuestion.tsx
  • src/components/Questionnaire/QuestionTypes/QuestionGroup.tsx
  • src/components/Questionnaire/QuestionTypes/QuestionInput.tsx
  • src/components/Questionnaire/QuestionTypes/SymptomQuestion.tsx
  • src/components/Questionnaire/QuestionnaireForm.tsx
  • src/components/Questionnaire/structured/contextMatch.test.ts
  • src/components/Questionnaire/structured/contextMatch.ts
  • src/pages/Encounters/tabs/overview/FormSubmissionDrafts.tsx
  • src/types/questionnaire/form.ts

Comment on lines +33 to +76
const KIND_META: Record<
ChangeKind,
{ labelKey: string; icon: IconName; dot: string; label: string }
> = {
added: {
labelKey: "draft_changes_added",
icon: "l-plus-circle",
dot: "bg-green-500",
label: "text-green-700",
},
removed: {
labelKey: "draft_changes_removed",
icon: "l-minus-circle",
dot: "bg-red-500",
label: "text-red-700",
},
changed: {
labelKey: "draft_changes_updated",
icon: "l-sync",
dot: "bg-amber-500",
label: "text-amber-700",
},
};

function ChangeGroup({
kind,
records,
}: {
kind: ChangeKind;
records: RecordLike[];
}) {
const { t } = useTranslation();
if (!records.length) return null;
const meta = KIND_META[kind];
return (
<div className="space-y-1.5">
<div
className={cn(
"flex items-center gap-1.5 text-xs font-semibold uppercase tracking-wide",
meta.label,
)}
>
<CareIcon icon={meta.icon} className="size-3.5" />
{t(meta.labelKey)}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Use literal t() calls so these keys survive i18n cleanup.

t(meta.labelKey) prevents remove-unused-i18n.js from statically discovering the three translation keys. Resolve the label with explicit branches containing t("draft_changes_added"), t("draft_changes_removed"), and t("draft_changes_updated").

Based on learnings, translation keys must be passed through statically detectable t() calls.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/Questionnaire/DraftContextChangesScreen.tsx` around lines 33 -
76, Update ChangeGroup’s dynamic t(meta.labelKey) call to use explicit branches
for each ChangeKind, with statically detectable t("draft_changes_added"),
t("draft_changes_removed"), and t("draft_changes_updated") calls. Preserve the
existing KIND_META styling and label behavior while ensuring all three
translation keys remain discoverable by i18n cleanup.

Source: Learnings

Comment on lines +105 to +143
return (
<div className="flex min-h-screen items-center justify-center p-4">
<div className="w-full max-w-lg space-y-6 rounded-xl border border-gray-200 bg-white p-6 shadow-lg sm:p-8">
<div className="space-y-3 text-center">
<div className="mx-auto flex size-12 items-center justify-center rounded-full bg-amber-100">
<CareIcon
icon="l-exclamation-triangle"
className="size-6 text-amber-600"
/>
</div>
<h2 className="text-lg font-semibold text-gray-900">
{t("draft_data_changed_title")}
</h2>
<p className="text-sm text-gray-500">
{t("draft_data_changed_description")}
</p>
</div>

<div className="space-y-3">
{changes.map((change) => (
<div
key={change.questionId}
className="space-y-3 rounded-lg border border-gray-200 bg-gray-50 p-4"
>
<h3 className="text-sm font-semibold text-gray-900">
{change.title}
</h3>
<ChangeGroup kind="added" records={change.added} />
<ChangeGroup kind="removed" records={change.removed} />
<ChangeGroup kind="changed" records={change.changed} />
</div>
))}
</div>

<Button onClick={onContinue} className="w-full">
{t("continue")}
</Button>
</div>
</div>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Implement this blocking screen as an accessible modal.

The fixed overlay has no dialog semantics, initial focus, focus trap, or background inertness. Keyboard users can tab into and activate the questionnaire underneath. Use the Shadcn/Radix Dialog primitive with an accessible title and description.

As per coding guidelines, components must use Shadcn primitives and provide proper ARIA attributes and keyboard navigation.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/Questionnaire/DraftContextChangesScreen.tsx` around lines 105
- 143, Update the blocking screen around the component rendering this
questionnaire change content to use the Shadcn/Radix Dialog primitive instead of
a standalone fixed layout. Render the existing title and description through
DialogTitle and DialogDescription, keep the changes and Continue action inside
DialogContent, and configure it as a non-dismissible modal so focus is trapped
and the background questionnaire is inert while preserving onContinue behavior.

Source: Coding guidelines

Comment on lines +694 to +700
if (!allPopulated && !reconcileTimedOut) return;

const changes: DraftContextChange[] = [];
const responses = form.responses.map((r) => {
const snap = snapshot[r.question_id];
// Skip if not applicable or fresh context never arrived (keep draft as-is).
if (!snap || r.context === undefined) return r;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Do not silently complete reconciliation after a fetch timeout.

When context remains undefined, Line 700 retains the stale draft records, but Lines 735-738 still end reconciliation. The user can then submit records that were removed or changed on the server without any warning. Keep the flow blocked and expose a retry/error state until every applicable context is reconciled.

Also applies to: 735-738

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/Questionnaire/QuestionnaireForm.tsx` around lines 694 - 700,
Update the reconciliation flow in QuestionnaireForm so a fetch timeout with any
applicable response still lacking context does not complete reconciliation. Do
not allow the existing changes/finalization logic around the responses mapping
and lines 735-738 to clear the pending state; instead retain the blocked state
and expose the existing retry or error state until every applicable context has
been reconciled.

Comment on lines +8 to +9
const rec = (id: string, extra: Record<string, unknown> = {}) =>
({ id, code: { display: id }, ...extra }) as unknown as ResponseContext;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Use function declarations for pure fixtures.

rec and big are pure helpers and should use the required declaration style.

As per coding guidelines: “Use the function keyword for pure functions.”

Also applies to: 93-101

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/Questionnaire/structured/contextMatch.test.ts` around lines 8
- 9, Convert the pure fixture helpers rec and big in the questionnaire
context-match tests from arrow-function assignments to function declarations,
preserving their parameters, return values, and behavior.

Source: Coding guidelines

Comment on lines +92 to +108
test("scales to 100 large records well under a frame", () => {
const big = (id: string) =>
rec(id, {
code: { display: id, system: "http://x", code: id },
created_by: { id: "u", username: "u", first_name: "a", last_name: "b" },
created_date: "2020",
modified_date: "2020",
onset: { onset_datetime: "2020", note: "x".repeat(50) },
nested: { a: [1, 2, 3], b: { c: { d: id } } },
});
const draft = Array.from({ length: 100 }, (_, i) => big(String(i)));
const fresh = Array.from({ length: 100 }, (_, i) => big(String(i)));
const start = performance.now();
const d = diffResponseContext(draft, fresh);
const ms = performance.now() - start;
assert.equal(d.matches, true);
assert.ok(ms < 16, `diff took ${ms.toFixed(2)}ms (expected < 16ms)`);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Remove the wall-clock assertion from this unit test.

The 16ms limit can fail under CI contention or GC despite identical behavior. Keep the functional assertion and move timing checks to a dedicated benchmark.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/Questionnaire/structured/contextMatch.test.ts` around lines 92
- 108, Remove the performance.now timing setup and the 16ms assertion from the
“scales to 100 large records well under a frame” test, while retaining the
large-record inputs and the functional d.matches assertion. Leave timing
validation to a separate benchmark.

Comment on lines +3 to +8
/** A structured value record (`*Request`) or read record — both carry these. */
export type RecordLike = {
id?: string;
dirty?: boolean;
[key: string]: unknown;
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Use an interface for RecordLike.

This is an object shape and should follow the repository convention.

Proposed fix
-export type RecordLike = {
+export interface RecordLike {
   id?: string;
   dirty?: boolean;
   [key: string]: unknown;
-};
+}

As per coding guidelines: “Use interface for object type definitions.”

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/** A structured value record (`*Request`) or read record — both carry these. */
export type RecordLike = {
id?: string;
dirty?: boolean;
[key: string]: unknown;
};
/** A structured value record (`*Request`) or read record — both carry these. */
export interface RecordLike {
id?: string;
dirty?: boolean;
[key: string]: unknown;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/Questionnaire/structured/contextMatch.ts` around lines 3 - 8,
Replace the RecordLike type alias with an interface while preserving its
optional id and dirty properties and its string-indexed unknown values. Keep the
existing RecordLike name and documentation unchanged.

Source: Coding guidelines

Comment on lines +10 to +19
// Server-managed audit fields (who/when) that must not count as content drift.
const VOLATILE_FIELDS = new Set([
"modified_date",
"created_date",
"updated_date",
"recorded_date",
"created_by",
"updated_by",
"modified_by",
]);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Do not ignore recorded_date changes.

recorded_date is present on diagnosis and symptom records. Excluding it makes a server-side clinical-date change return matches: true, so draft recovery will not notify the user.

Proposed fix
   "updated_date",
-  "recorded_date",
   "created_by",

Add a regression test where only recorded_date differs and assert changed.length === 1.

Based on PR objective: server-side record changes must be surfaced during draft recovery.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Server-managed audit fields (who/when) that must not count as content drift.
const VOLATILE_FIELDS = new Set([
"modified_date",
"created_date",
"updated_date",
"recorded_date",
"created_by",
"updated_by",
"modified_by",
]);
// Server-managed audit fields (who/when) that must not count as content drift.
const VOLATILE_FIELDS = new Set([
"modified_date",
"created_date",
"updated_date",
"created_by",
"updated_by",
"modified_by",
]);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/Questionnaire/structured/contextMatch.ts` around lines 10 -
19, Remove "recorded_date" from VOLATILE_FIELDS so changes to this clinical date
are treated as content drift, while continuing to ignore the remaining
server-managed audit fields. Add a regression test for the context-matching flow
where only recorded_date differs and assert that changed.length equals 1.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds draft-recovery support for structured questionnaire questions by snapshotting “fresh” server context into drafts, diffing it on recovery, and reconciling draft edits against current server records (with a user-facing warning screen when drift is detected).

Changes:

  • Extend questionnaire response typing to include a draft-only context snapshot and a canonical list of context-bearing structured types.
  • Wire setResponseContext through the questionnaire renderer so structured question components can populate draft context from their fetches.
  • Add context diff/merge helpers plus a recovery overlay screen that summarizes server-side changes; add i18n strings for the new UI.

Reviewed changes

Copilot reviewed 16 out of 16 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
src/types/questionnaire/form.ts Adds ResponseContext, CONTEXT_STRUCTURED_TYPES, and optional QuestionnaireResponse.context.
src/pages/Encounters/tabs/overview/FormSubmissionDrafts.tsx Passes the new required setResponseContext prop to QuestionRenderer (noop for read-only draft preview).
src/components/Questionnaire/structured/contextMatch.ts Introduces context diffing (ignoring volatile audit fields) and value-merge logic for draft recovery.
src/components/Questionnaire/structured/contextMatch.test.ts Adds unit tests for the context diff/merge behavior.
src/components/Questionnaire/QuestionTypes/SymptomQuestion.tsx Populates response context from fetched symptom records.
src/components/Questionnaire/QuestionTypes/QuestionInput.tsx Threads setResponseContext down to question components via shared props.
src/components/Questionnaire/QuestionTypes/QuestionGroup.tsx Propagates setResponseContext through grouped/sub questions.
src/components/Questionnaire/QuestionTypes/MedicationStatementQuestion.tsx Populates response context from fetched medication statement records.
src/components/Questionnaire/QuestionTypes/MedicationRequestQuestion.tsx Populates response context; explicitly sets empty context when no prescription exists.
src/components/Questionnaire/QuestionTypes/EncounterQuestion.tsx Stores fetched encounter record into response context.
src/components/Questionnaire/QuestionTypes/DiagnosisQuestion.tsx Populates response context from fetched diagnosis records.
src/components/Questionnaire/QuestionTypes/AllergyQuestion.tsx Populates response context from fetched allergy intolerance records.
src/components/Questionnaire/QuestionRenderer.tsx Adds and forwards setResponseContext prop.
src/components/Questionnaire/QuestionnaireForm.tsx Implements draft recovery reconciliation flow, timeout handling, and change-warning overlay UI.
src/components/Questionnaire/DraftContextChangesScreen.tsx New UI for summarizing “added/removed/changed” server-side drift when recovering drafts.
public/locale/en.json Adds English strings for the draft-context change screen.

Comment on lines +1 to +7
import assert from "node:assert/strict";
import { test } from "node:test";

import { ResponseContext } from "@/types/questionnaire/form";

import { diffResponseContext, mergeRecoveredValues } from "./contextMatch";

const d = diffResponseContext(draft, fresh);
const ms = performance.now() - start;
assert.equal(d.matches, true);
assert.ok(ms < 16, `diff took ${ms.toFixed(2)}ms (expected < 16ms)`);
@github-actions

github-actions Bot commented Jul 15, 2026

Copy link
Copy Markdown

🎭 Playwright Test Results

Status: ✅ Passed
Test Shards: 3

Metric Count
Total Tests 334
✅ Passed 333
❌ Failed 0
⏭️ Skipped 1

📊 Detailed results are available in the playwright-final-report artifact.

Run: #10487

@greptile-apps

greptile-apps Bot commented Jul 15, 2026

Copy link
Copy Markdown

Greptile Summary

This PR adds draft-save support for questionnaires that contain structured questions (diagnosis, symptom, allergy, medication request/statement, encounter). Previously these forms were excluded from drafts entirely. The new approach saves a context snapshot alongside the draft, and on recovery mounts the form underneath a loading overlay, lets each structured component re-fetch fresh server data, then diffs the saved snapshot against live data — showing a change-summary screen if server-side records drifted while the draft was held.

  • Context reconciliation state machine (QuestionnaireForm.tsx): a three-phase state ("done", "reconciling", "changed") orchestrates the recovery flow, with an 8-second timeout so a stalled fetch never blocks the user indefinitely.
  • Pure utility layer (contextMatch.ts / .test.ts): diffResponseContext and mergeRecoveredValues are well-isolated, tested for order-independence, volatile-field filtering, and the no-duplication edge case, with a performance assertion under 16 ms for 100 records.
  • isDraftSaveable simplification: the previous recursive walk to detect any structured question is replaced by a single FIXED_QUESTIONNAIRES slug check, enabling single-questionnaire structured forms to save as drafts.

Confidence Score: 4/5

Safe to merge with the noted caveats addressed; the core reconciliation logic is correct and well-tested, and the changes are isolated to the draft-recovery path.

The reconciliation state machine and the pure utility functions in contextMatch.ts are well-reasoned and covered by unit tests. The main concerns are: the inline setResponseContext callback defeats QuestionGroup's memo causing excess re-renders during recovery, EncounterQuestion never signals context on a failed/absent fetch causing an 8-second penalty, and the context field on QuestionnaireResponse lacks an explicit strip before form submission. None of these break correctness for the happy path, but the submission concern warrants verification before shipping to production.

QuestionnaireForm.tsx (inline callback and reconcile effect deps) and EncounterQuestion.tsx (missing context signal on failed fetch) deserve a second look before merge.

Important Files Changed

Filename Overview
src/components/Questionnaire/QuestionnaireForm.tsx Core of the draft-recovery feature; adds reconcile state machine, snapshot logic, and inline setResponseContext callback. The reconcile effect including questionnaireForms in deps causes redundant re-runs, and the inline callback defeats QuestionGroup memo — both worth addressing.
src/components/Questionnaire/structured/contextMatch.ts New diffResponseContext and mergeRecoveredValues utilities with a well-reasoned deepEqualIgnoringVolatile helper; logic is correct and well-tested.
src/components/Questionnaire/structured/contextMatch.test.ts Good unit-test coverage: covers order-independence, add/remove/change buckets, volatile-field exclusion, and the no-duplication edge case for MedicationRequest without a prescription.
src/components/Questionnaire/DraftContextChangesScreen.tsx New UI screen that displays server-side drift after draft recovery; clean component, uses i18n correctly, and gracefully falls back to id or dash for unlabeled records.
src/types/questionnaire/form.ts Adds ResponseContext union type, CONTEXT_STRUCTURED_TYPES constant, and context field to QuestionnaireResponse; the field is flagged 'draft-only, never sent on submit' but no stripping logic is visible in the diff.
src/components/Questionnaire/QuestionTypes/EncounterQuestion.tsx Adds setResponseContext call, but only when encounterData is truthy — if the fetch fails or encounterId is absent, context is never set and reconciliation waits for the full 8-second timeout.
src/components/Questionnaire/QuestionTypes/MedicationRequestQuestion.tsx Handles no-prescription case by immediately calling setResponseContext with [] so reconciliation isn't blocked, matching the test coverage in contextMatch.test.ts.
src/components/Questionnaire/QuestionTypes/AllergyQuestion.tsx Correctly adds setResponseContext call alongside updateQuestionnaireResponseCB in the same effect, so values and context are always committed together.
src/components/Questionnaire/QuestionTypes/DiagnosisQuestion.tsx Adds setResponseContext call in the existing patientDiagnoses effect, consistent with other structured question components.
src/components/Questionnaire/QuestionTypes/SymptomQuestion.tsx Adds setResponseContext call alongside updateQuestionnaireResponseCB in the same effect, correctly batching both state updates together.
src/components/Questionnaire/QuestionTypes/MedicationStatementQuestion.tsx Adds setResponseContext call in the existing patientMedications effect; consistent with other structured question components.
src/components/Questionnaire/QuestionTypes/QuestionGroup.tsx Threads setResponseContext through QuestionGroup and its sub-question rendering, passing it to both QuestionInput and nested QuestionGroup instances.
src/components/Questionnaire/QuestionTypes/QuestionInput.tsx Adds setResponseContext to the shared props object passed to structured question components; non-structured inputs are unaffected.
src/components/Questionnaire/QuestionRenderer.tsx Threads the new setResponseContext prop down to QuestionGroup; minimal, correct change.
src/pages/Encounters/tabs/overview/FormSubmissionDrafts.tsx Adds no-op setResponseContext={() => {}} for the read-only draft preview — correct, since reconciliation is not needed in this context.
public/locale/en.json Adds five new i18n keys for the draft-changes UI; all strings are clear and follow existing patterns.

Comments Outside Diff (2)

  1. src/types/questionnaire/form.ts, line 1147 (link)

    P2 Verify context is stripped from the form-submission payload

    The context field is documented as "draft-only, never sent on submit," but the diff shows no stripping logic before submission. If the questionnaire batch endpoint receives QuestionnaireResponse objects that include context, the backend may reject the request (400/422) or persist unexpected data. The submit path should explicitly omit context from each response before it is sent — e.g., by mapping responses to a {...r, context: undefined} shape, or by picking only the fields the API expects.

  2. src/components/Questionnaire/QuestionTypes/EncounterQuestion.tsx, line 182-191 (link)

    P2 setResponseContext not called when encounterData never resolves

    setResponseContext is called only inside if (encounterData). If the fetch fails (network error, 404) or encounterId is absent (enabled: false), context for the encounter question is never populated. During draft recovery the reconcile phase stays stuck in "reconciling" for the full 8-second timeout, showing a full-screen loading spinner. Calling setResponseContext(questionnaireResponse.question_id, []) on a failed query or when encounterId is falsy would let reconciliation proceed immediately — matching the pattern MedicationRequestQuestion already uses for the no-prescription case.

Reviews (1): Last reviewed commit: "Disable Drafts for Structured Editors" | Re-trigger Greptile

Comment on lines 1193 to +1207
setIsDirty(true);
}
}}
setResponseContext={(
questionId: string,
context: ResponseContext[],
) => {
// Derived-from-fetch snapshot: preserve values/note, and don't
// mark the form dirty (would trip the unsaved-changes prompt).
setQuestionnaireForms((existingForms) =>
existingForms.map((formItem) =>
formItem.questionnaire.id === form.questionnaire.id
? {
...formItem,
responses: formItem.responses.map((r) =>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Inline setResponseContext defeats QuestionGroup memoization

The callback is defined inline without useCallback, so a new function reference is created on every render of QuestionnaireForm. During the reconcile phase, each setResponseContext call triggers setQuestionnaireForms, which re-renders QuestionnaireForm, which produces a fresh reference — passed all the way down to QuestionGroup (which is wrapped in memo), causing every group to re-render. For N structured questions this means O(N²) unnecessary question-group re-renders. Wrap the callback in useCallback with a stable functional-update helper that doesn't close over questionnaireForms at all.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

setReconcile(
changes.length ? { phase: "changed", changes } : { phase: "done" },
);
}, [reconcile.phase, questionnaireForms, reconcileTimedOut]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 questionnaireForms in reconcile effect deps causes redundant intermediate runs

The effect depends on questionnaireForms, which is updated by every setResponseContext call. This means the effect runs once per structured question as each fetch completes, hitting the allPopulated check and returning early N-1 times before the final run does real work. After the final run calls both setQuestionnaireForms and setReconcile, React 18 batches those updates, so the effect fires one extra time with reconcile.phase === "done" where it early-returns. A useRef flag (hasReconciled) set before the state updates would eliminate the extra run and make the intent explicit.

Copilot AI review requested due to automatic review settings July 29, 2026 13:07

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
public/locale/en.json (1)

2052-2056: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Append new locale keys to the end of the file.

These newly added draft keys are inserted in the middle of public/locale/en.json. Move them to the end of the English locale object; do not edit non-English locale files.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@public/locale/en.json` around lines 2052 - 2056, Move the draft-related
locale keys draft_changes_added, draft_changes_removed, draft_changes_updated,
draft_data_changed_description, and draft_data_changed_title from their current
position to the end of the English locale object in public/locale/en.json.
Preserve their keys and values exactly, and do not modify any non-English locale
files.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@public/locale/en.json`:
- Around line 2052-2056: Move the draft-related locale keys draft_changes_added,
draft_changes_removed, draft_changes_updated, draft_data_changed_description,
and draft_data_changed_title from their current position to the end of the
English locale object in public/locale/en.json. Preserve their keys and values
exactly, and do not modify any non-English locale files.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: c85e91b7-a123-479c-9473-c21c7531dde7

📥 Commits

Reviewing files that changed from the base of the PR and between d1c9343 and 59cd45c.

📒 Files selected for processing (1)
  • public/locale/en.json

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 16 out of 16 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (3)

src/components/Questionnaire/structured/contextMatch.test.ts:6

  • This unit test file won’t be executed by the current unit test runner (npm run test:unit only runs node --test "plugins/**/*.test.ts"). As-is, it won’t provide CI coverage for the new reconciliation logic.
import assert from "node:assert/strict";
import { test } from "node:test";

import { ResponseContext } from "@/types/questionnaire/form";

import { diffResponseContext, mergeRecoveredValues } from "./contextMatch";

src/components/Questionnaire/structured/contextMatch.ts:19

  • diffResponseContext is intended to ignore server-managed audit fields, but AllergyIntolerance read records include edited_by (not present in the request type) and it currently counts as a meaningful change. This can cause false “Updated on the server” warnings when only edited_by changes.
// Server-managed audit fields (who/when) that must not count as content drift.
const VOLATILE_FIELDS = new Set([
  "modified_date",
  "created_date",
  "updated_date",
  "recorded_date",
  "created_by",
  "updated_by",
  "modified_by",
]);

src/components/Questionnaire/QuestionnaireForm.tsx:588

  • Now that drafts can include structured-question context snapshots, the “continue draft → submit” path will upload/persist that context unless it’s explicitly stripped when converting the draft to a submitted form_submission. Since context is meant for draft reconciliation only, consider omitting it (and any other draft-only fields) when building the submitted payload/response_dump to avoid transmitting/storing raw fetched records.
  // Single-questionnaire drafts (structured questions supported via context
  // reconciliation on recovery). Structured edit questionnaires (diagnosis,
  // service_request, allergy_intolerance, …) are frontend-only — their slug is
  // not a real backend questionnaire — so they can't be saved as drafts.
  const isDraftSaveable = useMemo(() => {
    if (!careConfig.enableQuestionnaireDraft) {
      return false;
    }
    if (!questionnaireSlug || FIXED_QUESTIONNAIRES[questionnaireSlug]) {
      return false;
    }
    return questionnaireForms.length <= 1;

@nihal467 nihal467 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.

@nihal467 nihal467 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.

@nihal467 nihal467 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Image

When a user adds Common Cold to the symptoms and saves the form as a draft, then adds Common Cold again directly in the Symptoms section, reopening the draft triggers the duplicate detection pop-up indicating that the symptoms have changed.

After clicking Continue, the Symptoms section displays Common Cold twice. The user can then submit the form successfully, allowing the frontend duplicate validation for symptoms to be bypassed.

@nihal467 nihal467 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Image image
  • disable the interaction with the file component & Appointment & history button (Medication, symtpoms, etc...) in the draft state on the overview page

@coderabbitai coderabbitai Bot mentioned this pull request Aug 1, 2026
8 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Changes Required Type Changes Contains changes in typescript types

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants