Skip to content
Open
Show file tree
Hide file tree
Changes from 105 commits
Commits
Show all changes
135 commits
Select commit Hold shift + click to select a range
565cee6
feat(questionnaire-v2): builder model groundwork for the studio
bodhish Aug 1, 2026
e0edbde
feat(questionnaire-v2): full renderer (form/) — one-scroll canvas wit…
bodhish Aug 1, 2026
048e994
feat(questionnaire-v2): WYSIWYG studio — 3-pane builder with realtime…
bodhish Aug 1, 2026
38262b7
fix(questionnaire-v2): studio review fixes + one-scroll spec alignment
bodhish Aug 1, 2026
c544723
test(questionnaire-v2): scope the protected-note unlock assertion to …
bodhish Aug 1, 2026
dac7148
feat(questionnaire-v2): studio reference-gap pass — inspector tabs, l…
bodhish Aug 2, 2026
49f0d22
feat(questionnaire-v2): inspector Question tab styled per the reference
bodhish Aug 2, 2026
7f38019
feat(questionnaire-v2): revision viewer on the full renderer, save-ve…
bodhish Aug 2, 2026
e51c166
fix(questionnaire-v2): one border per input on the canvas
bodhish Aug 2, 2026
dcf96ec
feat(questionnaire-v2): fullscreen studio — no app sidebar, viewport …
bodhish Aug 2, 2026
0497e4f
refactor(questionnaire-v2): deep-review pass — perf, a11y, dedup, con…
bodhish Aug 2, 2026
22d0b61
refactor(questionnaire-v2): close out deep-review stragglers
bodhish Aug 2, 2026
84922e9
feat(questionnaire-v2): surface the selected structured type everywhe…
bodhish Aug 2, 2026
d4a8df7
refactor(questionnaire): canonical StructuredQuestionType in the type…
bodhish Aug 2, 2026
734bbb6
feat(questionnaire-v2): structured-type contract — one definition per…
bodhish Aug 2, 2026
6141bb7
feat(questionnaire-v2): fill-mode engine seams — draft seed, real cle…
bodhish Aug 2, 2026
e5d42bb
feat(questionnaire-v2): fill submit pipeline — validate, compose one …
bodhish Aug 2, 2026
cbad1c3
feat(questionnaire-v2): local fill drafts — debounced autosave with s…
bodhish Aug 2, 2026
b967515
feat(questionnaire-v2): the fill page — two-tab shell, outline, autos…
bodhish Aug 2, 2026
28a6859
feat(questionnaire-v2): mount the fill page on the questionnaire routes
bodhish Aug 2, 2026
60987a8
test(questionnaire-v2): fill-page specs — validation, autosave, shell…
bodhish Aug 2, 2026
98c0ce4
fix(questionnaire-v2): verification-round fixes — pristine-before-nav…
bodhish Aug 2, 2026
0544321
docs(questionnaire-v2): tighten draft-layer comments
bodhish Aug 2, 2026
b321629
feat(questionnaire-v2): explicit resume/discard prompt for local fill…
bodhish Aug 2, 2026
30b9066
feat(questionnaire-v2): multi-questionnaire fill sessions — per-form …
bodhish Aug 2, 2026
b116298
fix(questionnaire-v2): persist the fill session when its shape changes
bodhish Aug 2, 2026
1328e27
feat(questionnaire-v2): subject-generic fill — FillSubject union, sub…
bodhish Aug 2, 2026
d2184c9
feat(questionnaire-v2): restore the /consultation picker filter, hard…
bodhish Aug 2, 2026
16f5b74
feat(questionnaire-v2): structured types declare their subjects — pic…
bodhish Aug 2, 2026
a06d855
feat(questionnaire-v2): plugin-extensible structured types — runtime …
bodhish Aug 2, 2026
44c4e44
fix(questionnaire-v2): re-resolve structured types when a plugin regi…
bodhish Aug 2, 2026
c1ead36
feat(questionnaire-v2): action registry + descriptor/invoke Scribe co…
bodhish Aug 2, 2026
33f29cd
fix(questionnaire-v2): tighten the action choke point after review
bodhish Aug 2, 2026
e00388e
refactor(questionnaire-v2): preview encounter drafts through the v2 r…
bodhish Aug 2, 2026
4d5b5f7
refactor(questionnaire-v2): delete the legacy fill stack and the v2 s…
bodhish Aug 2, 2026
8b141c5
refactor(questionnaire-v2): relocate the engine under form/engine
bodhish Aug 2, 2026
4616fb5
docs(questionnaire-v2): rewrite the module map for one renderer, one …
bodhish Aug 2, 2026
f4c933f
feat(questionnaire-v2): restore the explicit server draft on the fill…
bodhish Aug 2, 2026
251b2e1
fix(questionnaire-v2): encounter-only server drafts, non-mutating dra…
bodhish Aug 2, 2026
3aecd14
fix(questionnaire-v2): give the fixed encounter form an encounter sub…
bodhish Aug 2, 2026
bd93fb4
test(questionnaire-v2): drive the legacy clinical flows through the v…
bodhish Aug 2, 2026
b03b7bc
fix(questionnaire-v2): correct the local-draft lifecycle around struc…
bodhish Aug 3, 2026
1643171
fix(questionnaire-v2): submit time answers as HH:mm:ss and drop clear…
bodhish Aug 3, 2026
d15894a
fix(questionnaire-v2): contain structured failures and stop them bloc…
bodhish Aug 3, 2026
36d433f
fix(questionnaire-v2): enforce plugin namespace ownership and tidy th…
bodhish Aug 3, 2026
04ebeb6
docs(questionnaire-v2): state the server-draft gate and compose diver…
bodhish Aug 3, 2026
8b70bb8
test(questionnaire-v2): pin the draft lifecycle, time format and pati…
bodhish Aug 3, 2026
f321cb3
fix(questionnaire-v2): keep un-fetchable added forms in the draft on …
bodhish Aug 3, 2026
e13430d
Questionnaire v2: submit-path hardening and render-failure containmen…
bodhish Aug 3, 2026
47e70ff
test(questionnaire-v2): paginate fixture slug lookup past the first 1…
bodhish Aug 3, 2026
468a6f0
fix(questionnaire-v2): evaluate exists enable-when like the backend, …
bodhish Aug 3, 2026
374e188
fix(questionnaire-v2): clear fill drafts only after successful auth; …
bodhish Aug 3, 2026
a74983b
fix(questionnaire-v2): scope post-auth draft clear to other users; dr…
bodhish Aug 3, 2026
0d65959
fix(questionnaire-v2): studio save race, type-change residue, valuese…
bodhish Aug 3, 2026
018827a
fix(questionnaire-v2): metadata-side save race, choice-precedence gat…
bodhish Aug 3, 2026
be716d9
fix(questionnaire-v2): plugin boundary resets on re-register; namespa…
bodhish Aug 3, 2026
bf2ffa9
fix(questionnaire-v2): valueset quantity answers submit their coding
bodhish Aug 3, 2026
02b9dc1
fix(questionnaire-v2): fall back a quantity's coding to its default unit
bodhish Aug 3, 2026
065e5b0
fix(questionnaire-v2): submit guards — draft linkage, batch cap prefl…
bodhish Aug 3, 2026
0f9ed9e
fix(questionnaire-v2): isolate the P1-9 spec, in-memory batch-cap fil…
bodhish Aug 3, 2026
9a2d1a8
fix(questionnaire-v2): required structured sections in a broken state…
bodhish Aug 3, 2026
6973ae9
fix(questionnaire-v2): notices stop contradicting the required hard-b…
bodhish Aug 3, 2026
b0c68c0
fix(questionnaire-v2): fill mounts enforce subject family and encount…
bodhish Aug 3, 2026
9c4393d
fix(questionnaire-v2): freeze the session during submit; extract sess…
bodhish Aug 3, 2026
9d956c1
fix(questionnaire-v2): gate the note popover and draft-resume control…
bodhish Aug 3, 2026
8467e50
refactor(questionnaire-v2): submit bodies typed to the real backend c…
bodhish Aug 3, 2026
4216cc9
perf(questionnaire-v2): per-form autosave signatures; deferred studio…
bodhish Aug 3, 2026
f1688b5
fix(questionnaire-v2): drop dead session signature, seed primary form…
bodhish Aug 3, 2026
a82110d
fix(questionnaire-v2): checkpoint fixes — server-draft cache revert, …
bodhish Aug 4, 2026
3cbed9a
feat(questionnaire-v2): edit-log vocabulary on the questionnaire resp…
bodhish Aug 4, 2026
6a56d69
feat(questionnaire-v2): row identity and structural equality for the …
bodhish Aug 4, 2026
97c336d
fix(questionnaire-v2): reconcile RowEdit with Task 1's shipped edit v…
bodhish Aug 4, 2026
5e3155e
fix(questionnaire-v2): close the Partial<TRow> gap in the vocabulary-…
bodhish Aug 4, 2026
6cf9f8f
feat(questionnaire-v2): edit-log reducer with one-edit-per-row coales…
bodhish Aug 4, 2026
792e60c
fix(questionnaire-v2): resolve edit-log resurrection ops by baseline …
bodhish Aug 4, 2026
9f07ba0
feat(questionnaire-v2): baseline+edits projection with display-only o…
bodhish Aug 4, 2026
f91c660
fix(questionnaire-v2): resolve projectRows' add-loop by the same last…
bodhish Aug 4, 2026
8684ea7
feat(questionnaire-v2): resolve an edit log into create/update/remove…
bodhish Aug 4, 2026
0a1de95
fix(questionnaire-v2): resolveChanges consults baseline for orphans a…
bodhish Aug 4, 2026
a711b5a
fix(questionnaire-v2): reclassify an add colliding with baseline to u…
bodhish Aug 4, 2026
9f9f8f5
feat(questionnaire-v2): dual-contract structured definitions with a r…
bodhish Aug 4, 2026
3b41fe4
feat(questionnaire-v2): useStructuredRows — the baseline/edits/projec…
bodhish Aug 4, 2026
0c8ffd3
fix(questionnaire-v2): refuse contract-v2 plugin registrations for th…
bodhish Aug 4, 2026
f2342dd
feat(questionnaire-v2): dual-contract shim across submit, validation …
bodhish Aug 4, 2026
c540a56
fix(questionnaire-v2): fix the initialEdits seed latch and extract th…
bodhish Aug 4, 2026
94940e1
fix(questionnaire-v2): restore mergePatch's normalizePatch-undefined …
bodhish Aug 4, 2026
7154d11
fix(questionnaire-v2): pin the untyped-structured draft gate and subs…
bodhish Aug 4, 2026
0658eec
test(questionnaire-v2): pin the structured fixture questionnaires beh…
bodhish Aug 4, 2026
dc442ea
feat(questionnaire-v2): port time_of_death to contract v2 and make it…
bodhish Aug 4, 2026
ee4c2fd
feat(questionnaire-v2): appointment differ and slot rule for contract…
bodhish Aug 4, 2026
f7fea04
test(questionnaire-v2): pin appointment's PROJECTION AND SUBMIT AGREE…
bodhish Aug 4, 2026
626a3c4
fix(questionnaire-v2): retain dropped orphan edits, pin the prune's w…
bodhish Aug 4, 2026
87536d5
fix(questionnaire-v2): agree projection/differ on empty rows, fix the…
bodhish Aug 4, 2026
5fe1772
fix(questionnaire-v2): filter blanks before the singleton pick and co…
bodhish Aug 4, 2026
00799c8
fix(questionnaire-v2): agree the singleton's projection and differ un…
bodhish Aug 4, 2026
e8d7c0d
fix(questionnaire-v2): dedupe droppedEdits under StrictMode, pin the …
bodhish Aug 4, 2026
80c54bd
feat(questionnaire-v2): one structured field error primitive; port ap…
bodhish Aug 4, 2026
4231f3d
fix(questionnaire-v2): correct the ordering claim in resolveSingleton…
bodhish Aug 4, 2026
c489b21
feat(questionnaire-v2): response-backed charge item rows with one dis…
bodhish Aug 4, 2026
fdd08c0
fix(questionnaire-v2): stop the appointment slot-sheet render loop an…
bodhish Aug 4, 2026
1ff76c4
feat(questionnaire-v2): responsive structured list primitive; port ch…
bodhish Aug 4, 2026
966020d
fix(questionnaire-v2): correct the inline-field-error allow-list and …
bodhish Aug 4, 2026
5a4814c
feat(questionnaire-v2): encounter row derivation and differ as pure f…
bodhish Aug 4, 2026
89fa7fd
fix(questionnaire-v2): make StructuredList's mobile error unreachable…
bodhish Aug 4, 2026
f9e1bdc
feat(questionnaire-v2): port encounter to single-row contract v2 with…
bodhish Aug 4, 2026
2134d26
fix(questionnaire-v2): render and force-expand on an unmatched field_…
bodhish Aug 4, 2026
d46bb35
feat(questionnaire-v2): port files to contract v2, the sole draft-exc…
bodhish Aug 4, 2026
dd4283f
docs(questionnaire-v2): correct encounter's discharge-disposition doc…
bodhish Aug 4, 2026
cdf211b
fix(questionnaire-v2): stop the files editor from committing rejected…
bodhish Aug 4, 2026
caf5e5d
fix(questionnaire-v2): allow-list files for inline field errors
bodhish Aug 4, 2026
25d07a5
test(questionnaire-v2): per-type matrix for the structured singletons
bodhish Aug 4, 2026
b7e8028
fix(questionnaire-v2): defer the initialEdits seed's commit past the …
bodhish Aug 4, 2026
56ce670
test(questionnaire-v2): per-type matrix for structured lists, includi…
bodhish Aug 4, 2026
5fe3395
test(questionnaire-v2): pin zero-upsert, refetch-safety and structure…
bodhish Aug 4, 2026
d8bb0f5
fix(questionnaire-v2): add liveness guard to the deferred seed commit…
bodhish Aug 4, 2026
e06643c
feat(questionnaire-v2): port allergy_intolerance to contract v2, add …
bodhish Aug 4, 2026
0e1bcb7
feat(questionnaire-v2): port symptom to contract v2
bodhish Aug 4, 2026
f425c16
feat(questionnaire-v2): port diagnosis to contract v2
bodhish Aug 4, 2026
63e44ef
feat(questionnaire-v2): rebuild plugin fill capability as a generic a…
bodhish Aug 4, 2026
c6184a2
feat(questionnaire-v2): port medication_statement to contract v2
bodhish Aug 4, 2026
e3f6d99
feat(questionnaire-v2): port medication_request to contract v2
bodhish Aug 4, 2026
300ed55
feat(questionnaire-v2): port service_request to contract v2, add shar…
bodhish Aug 4, 2026
f288c76
fix(questionnaire-v2): close the three Phase-1 structured-core carry-…
bodhish Aug 4, 2026
52586cf
feat(questionnaire-v2): compatibility-aware draft merge (spec amendme…
bodhish Aug 4, 2026
63cb2ad
fix(questionnaire-v2): remove the dual-contract shim now that every t…
bodhish Aug 4, 2026
dbdee23
feat(questionnaire-v2): delete the legacy QuestionTypes stack (Batch …
bodhish Aug 4, 2026
651fd3a
feat(questionnaire-v2): medication_request templates, per-row actions…
bodhish Aug 5, 2026
a3156e6
fix(questionnaire-v2): repair nine post-rearchitecture spec failures,…
bodhish Aug 5, 2026
c10e8cc
refactor(questionnaire-v2): quality pass — comment cleanup, dead code…
bodhish Aug 5, 2026
bb803bc
fix(questionnaire-v2): add the locale keys the fill and builder surfa…
bodhish Aug 5, 2026
110a826
fix(questionnaire-v2): stop the fill session losing a clinician's work
bodhish Aug 5, 2026
af9f5ea
fix(questionnaire-v2): correct the structured row core and the per-ty…
bodhish Aug 5, 2026
000cd3a
fix(questionnaire-v2): correct quantity, integer and note handling in…
bodhish Aug 5, 2026
e622f76
fix(questionnaire-v2): correct enable_when authoring and gate the rea…
bodhish Aug 5, 2026
54b84d7
test(questionnaire-v2): close coverage gaps, de-flake the E2E suite, …
bodhish Aug 5, 2026
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 package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
"build": "npm run build:meta && npm run supported-browsers && npm run build:react",
"postinstall": "tsx scripts/install-platform-deps.ts && tsx scripts/generate-headers.ts",
"test": "snyk test",
"test:unit": "node --import tsx --test \"plugins/**/*.test.ts\"",
"test:unit": "node --import tsx --test \"plugins/**/*.test.ts\" \"src/**/*.test.ts\"",
"playwright:test": "playwright test",
"playwright:test:ui": "playwright test --ui",
"playwright:test:headed": "playwright test --headed",
Expand Down
85 changes: 83 additions & 2 deletions public/locale/en.json

Large diffs are not rendered by default.

41 changes: 37 additions & 4 deletions src/PluginEngine.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import React, { Suspense, useEffect, useMemo, useRef } from "react";
import ErrorBoundary from "@/components/Common/ErrorBoundary";
import Loading from "@/components/Common/Loading";
import { PluginErrorBoundary } from "@/components/Common/PluginErrorBoundary";
import { registerPluginStructuredType } from "@/components/QuestionnaireV2/structured/pluginRegistry";
import { addOverride } from "@/lib/override";
import { PlugConfig, PlugConfigMeta } from "@/types/plugConfig";
import plugConfigApi from "@/types/plugConfig/plugConfigApi";
Expand Down Expand Up @@ -96,7 +97,17 @@ export default function PluginEngine({
return { ...config, isLoading: true as const };
}

return { ...config, isLoading: false as const, ...data! };
// `data` is the remote's OWN manifest — untrusted. `slug` spreads
// in AFTER `...data!` so nothing the manifest declares (a stray
// `slug` field, or reusing `plugin.plugin`) can shadow the
// backend-issued `config.slug` this query was actually fetched
// for. That's the identity namespace ownership checks rely on.
return {
...config,
isLoading: false as const,
...data!,
slug: config.slug,
};
}),
});

Expand All @@ -116,7 +127,7 @@ export default function PluginEngine({
window.__CARE_PLUGIN_RUNTIME__ = deepFreeze({ meta: pluginMeta });
}, [pluginMeta]);

// Register plugin overrides
// Register plugin overrides and structured question types
const overrideCleanupRef = useRef<(() => void)[]>([]);

useEffect(() => {
Expand All @@ -126,9 +137,9 @@ export default function PluginEngine({

// Register new overrides from all loaded plugins
for (const plugin of pluginsQuery) {
if (plugin.isLoading || !plugin.overrides) continue;
if (plugin.isLoading) continue;

for (const override of plugin.overrides) {
for (const override of plugin.overrides ?? []) {
const cleanup = addOverride(override.component, {
component: override.replacement,
condition: override.condition,
Expand All @@ -138,6 +149,28 @@ export default function PluginEngine({
});
overrideCleanupRef.current.push(cleanup);
}

// Structured question types the plugin contributes. Registration
// throws on a non-namespaced id — one malformed definition is logged
// and skipped, never fatal to the app. The registering plugin's slug
// goes in so the registry can verify the `{plugin_slug}.` half of the
// id actually belongs to it: namespacing is the isolation guarantee,
// and it is only a guarantee if someone checks. `plugin.slug` is the
// trusted, backend-issued identity (see the combine above) — never
// `plugin.plugin`, which is a field the remote manifest declares
// about itself and could spoof another plugin's namespace with.
for (const definition of plugin.structuredQuestionTypes ?? []) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The error log here uses plugin.plugin — the value the remote manifest declared about itself, explicitly called out two comments above as untrusted and potentially spoofed. The backend-issued, trusted identity for the plugin is plugin.slug. Logging an attacker-controlled string in the error message is at best misleading, at worst used to obfuscate which plugin actually misbehaved.

console.error(`Invalid structured type from plugin ${plugin.slug}`, error);

try {
overrideCleanupRef.current.push(
registerPluginStructuredType(definition, plugin.slug),
);
} catch (error) {
console.error(
`Invalid structured type from plugin ${plugin.plugin}`,
error,
);
}
}
}

// Cleanup on unmount
Expand Down
50 changes: 49 additions & 1 deletion src/Providers/AuthUserProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,18 @@ import { navigate, usePath } from "raviger";
import { useCallback, useEffect, useState } from "react";

import Loading from "@/components/Common/Loading";
import {
clearOtherUsersFillDrafts,
clearQuestionnaireFillDrafts,
sweepExpiredFillDrafts,
} from "@/components/QuestionnaireV2/fill/draft/fillDraftCache";

import { AuthUserContext } from "@/hooks/useAuthUser";

import { LocalStorageKeys } from "@/common/constants";

import mutate from "@/Utils/request/mutate";
import query from "@/Utils/request/query";
import query, { callApi } from "@/Utils/request/query";
import { userAtom } from "@/atoms/user-atom";
import {
JwtTokenObtainPair,
Expand Down Expand Up @@ -65,6 +70,14 @@ export default function AuthUserProvider({
useEffect(() => {
setUser(user);
}, [user, setUser]);

// Boot-time housekeeping — drop any fill drafts that outlived their TTL,
// regardless of which user (or logged-out session) wrote them. Fresh
// drafts (e.g. one saved from the login form) are left alone.
useEffect(() => {
sweepExpiredFillDrafts();
}, []);

const refreshToken = localStorage.getItem(LocalStorageKeys.refreshToken);

const tokenRefreshQuery = useQuery({
Expand All @@ -79,6 +92,11 @@ export default function AuthUserProvider({

useEffect(() => {
if (tokenRefreshQuery.isError) {
// Tokens only — this query has retry:false, so a single transient
// network blip trips this branch. Clearing drafts here would
// destroy exactly the recovery scenario this feature exists for
// (a phone call blocking the network mid-refresh); signOut() is the
// deliberate, user-initiated place drafts get wiped on session end.
localStorage.removeItem(LocalStorageKeys.accessToken);
localStorage.removeItem(LocalStorageKeys.refreshToken);
return;
Expand All @@ -91,6 +109,25 @@ export default function AuthUserProvider({
}
}, [tokenRefreshQuery.data, tokenRefreshQuery.isError]);

// The freshly-issued tokens are in localStorage by the time this runs,
// but the `user` in scope here is still last render's (possibly none) —
// fetch the identity behind the NEW tokens directly rather than wait on
// the query hook's next render, so the draft clear below can be scoped
// to this exact account at the earliest point it is known, strictly
// after credentials were accepted.
const clearOtherUsersDrafts = useCallback(async () => {
try {
const currentUser = await callApi(userApi.currentUser, {
silent: true,
});
clearOtherUsersFillDrafts(currentUser.id);
} catch {
// Best-effort — an identity lookup failing here must not block
// sign-in. Worst case a different user's stale draft lingers until
// the next successful login or the boot-time expiry sweep.
}
}, []);

const { mutateAsync: signIn, isPending: isAuthenticating } = useMutation({
mutationFn: mutate(authApi.login),
onSuccess: async (data: LoginResponse) => {
Expand All @@ -113,6 +150,12 @@ export default function AuthUserProvider({
setAccessToken(data.access);
localStorage.setItem(LocalStorageKeys.accessToken, data.access);
localStorage.setItem(LocalStorageKeys.refreshToken, data.refresh);
// Credentials are accepted — a draft left at the login form by a
// DIFFERENT account (e.g. a session expiry on a shared machine)
// must not survive into this session. A draft belonging to the
// account that just signed in is the recovery case this feature
// exists for, so it must survive.
await clearOtherUsersDrafts();

await queryClient.invalidateQueries({ queryKey: ["currentUser"] });
if (path === "/" || path === "/login") {
Expand All @@ -130,6 +173,10 @@ export default function AuthUserProvider({
setAccessToken(data.access);
localStorage.setItem(LocalStorageKeys.accessToken, data.access);
localStorage.setItem(LocalStorageKeys.refreshToken, data.refresh);
// Same rule as the direct JWT success branch above — the 2FA step
// just completed, so this is the first point credentials are fully
// accepted.
await clearOtherUsersDrafts();

await queryClient.invalidateQueries({ queryKey: ["currentUser"] });
navigate(getRedirectOr("/"));
Expand Down Expand Up @@ -163,6 +210,7 @@ export default function AuthUserProvider({
localStorage.removeItem(LocalStorageKeys.accessToken);
localStorage.removeItem(LocalStorageKeys.refreshToken);
localStorage.removeItem(LocalStorageKeys.patientTokenKey);
clearQuestionnaireFillDrafts();
setAccessToken(null);
setPatientToken(null);

Expand Down
10 changes: 9 additions & 1 deletion src/Routers/AppRouter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,16 @@ const PATHS_WITHOUT_SIDEBAR = [
/^\/facility\/[^/]+\/locations\/[^/]+\/external_supply\/purchase_orders\/[^/]+\/edit$/,
/^\/facility\/[^/]+\/locations\/[^/]+\/external_supply\/deliveries\/[^/]+$/,
/^\/facility\/[^/]+\/queues\/[^/]+\/tokens\/[^/]+$/,
// Questionnaire form routes
// Questionnaire fill routes (fullscreen v2 fill experience)
/^\/facility\/[^/]+\/patient\/[^/]+\/encounter\/[^/]+\/questionnaire(\/[^/]+)?$/,
/^\/facility\/[^/]+\/patient\/[^/]+\/questionnaire(\/[^/]+)?$/,
/^\/facility\/[^/]+\/patient\/[^/]+\/consultation$/,
/^\/patient\/[^/]+\/questionnaire(\/[^/]+)?$/,
/^\/facility\/[^/]+\/locations\/[^/]+\/questionnaire(\/[^/]+)?$/,
/^\/facility\/[^/]+\/settings\/devices\/[^/]+\/questionnaire(\/[^/]+)?$/,
// Questionnaire studio (fullscreen builder) routes
/^\/facility\/[^/]+\/settings\/questionnaires\/[^/]+\/edit$/,
/^\/admin\/questionnaires\/[^/]+\/edit$/,
];

export type RouteParams<T extends string> =
Expand Down
59 changes: 39 additions & 20 deletions src/Routers/routes/ConsultationRoutes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { PrintAllQuestionnaireResponses } from "@/components/Facility/Consultati
import { PrintQuestionnaireResponse } from "@/components/Facility/ConsultationDetails/PrintQuestionnaireResponse";
import QuestionnaireResponseView from "@/components/Facility/ConsultationDetails/QuestionnaireResponseView";
import { PrintMedicationAdministration } from "@/components/Medicine/MedicationAdministration/PrintMedicationAdministration";
import EncounterQuestionnaire from "@/components/Patient/EncounterQuestionnaire";
import QuestionnaireFillPage from "@/components/QuestionnaireV2/fill/QuestionnaireFillPage";

import { AppRoutes } from "@/Routers/AppRouter";
import { EncounterShow } from "@/pages/Encounters/EncounterShow";
Expand Down Expand Up @@ -95,22 +95,16 @@ const consultationRoutes: AppRoutes = {
),
"/facility/:facilityId/patient/:patientId/encounter/:encounterId/questionnaire":
({ facilityId, encounterId, patientId }) => (
<EncounterQuestionnaire
facilityId={facilityId}
encounterId={encounterId}
patientId={patientId}
subjectType="encounter"
<QuestionnaireFillPage
subject={{ type: "encounter", facilityId, patientId, encounterId }}
/>
),

"/facility/:facilityId/patient/:patientId/encounter/:encounterId/questionnaire/:questionnaireId":
({ facilityId, encounterId, questionnaireId, patientId }) => (
<EncounterQuestionnaire
facilityId={facilityId}
encounterId={encounterId}
<QuestionnaireFillPage
subject={{ type: "encounter", facilityId, patientId, encounterId }}
questionnaireId={questionnaireId}
patientId={patientId}
subjectType="encounter"
/>
),

Expand All @@ -131,29 +125,54 @@ const consultationRoutes: AppRoutes = {
);
return acc;
}, {}),
// Legacy pre-encounter URL, kept so old links resolve. Nothing in the app
// links here any more — encounters are created by CreateEncounterForm, not
// by this questionnaire — so the SUBJECT is the patient and the fixed
// "encounter" question reports that it needs an encounter in scope. The
// picker keeps offering encounter-subject forms, as it did before the
// subject union landed.
"/facility/:facilityId/patient/:patientId/consultation": ({
facilityId,
patientId,
}) => (
<EncounterQuestionnaire
facilityId={facilityId}
patientId={patientId}
<QuestionnaireFillPage
subject={{ type: "patient", facilityId, patientId }}
questionnaireId="encounter"
subjectType="encounter"
pickerSubjectType="encounter"
/>
),
"/facility/:facilityId/patient/:patientId/questionnaire": ({
facilityId,
patientId,
}) => (
<EncounterQuestionnaire
facilityId={facilityId}
patientId={patientId}
subjectType="patient"
<QuestionnaireFillPage
subject={{ type: "patient", facilityId, patientId }}
/>
),
// The patient-subject picker needs an id route to land on — the legacy
// form appended picked questionnaires in-session instead of navigating,
// so these two shapes are new alongside the v2 single-questionnaire fill.
"/facility/:facilityId/patient/:patientId/questionnaire/:questionnaireId": ({
facilityId,
patientId,
questionnaireId,
}) => (
<QuestionnaireFillPage
subject={{ type: "patient", facilityId, patientId }}
questionnaireId={questionnaireId}
/>
),
"/patient/:patientId/questionnaire": ({ patientId }) => (
<EncounterQuestionnaire patientId={patientId} subjectType="patient" />
<QuestionnaireFillPage subject={{ type: "patient", patientId }} />
),
"/patient/:patientId/questionnaire/:questionnaireId": ({
patientId,
questionnaireId,
}) => (
<QuestionnaireFillPage
subject={{ type: "patient", patientId }}
questionnaireId={questionnaireId}
/>
),
};

Expand Down
4 changes: 2 additions & 2 deletions src/Routers/routes/adminRoutes.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { navigate } from "raviger";

import { QuestionnaireBuilderPage } from "@/components/QuestionnaireV2/builder/QuestionnaireBuilderPage";
import { QuestionnaireCreatePage } from "@/components/QuestionnaireV2/manage/QuestionnaireCreatePage";
import { QuestionnaireDetailPage } from "@/components/QuestionnaireV2/manage/QuestionnaireDetailPage";
import { QuestionnaireListPage } from "@/components/QuestionnaireV2/manage/QuestionnaireListPage";
import { QuestionnaireRevisionPage } from "@/components/QuestionnaireV2/manage/QuestionnaireRevisionPage";
import { QuestionnaireStudioPage } from "@/components/QuestionnaireV2/studio/QuestionnaireStudioPage";
import { ValueSetEditor } from "@/components/ValueSet/ValueSetEditor";
import { ValueSetList } from "@/components/ValueSet/ValueSetList";

Expand Down Expand Up @@ -36,7 +36,7 @@ const AdminRoutes: AppRoutes = {
// Must be registered before "/admin/questionnaires/:id" for the same
// reason — otherwise "edit" would be captured as an :id.
"/admin/questionnaires/:id/edit": ({ id }) => (
<QuestionnaireBuilderPage scope={INSTANCE_SCOPE} id={id} />
<QuestionnaireStudioPage scope={INSTANCE_SCOPE} id={id} />
),
// Registered before "/admin/questionnaires/:id" like the routes above.
"/admin/questionnaires/:id/versions/:revisionId": ({ id, revisionId }) => (
Expand Down
18 changes: 18 additions & 0 deletions src/atoms/scheduleServiceTypeAtom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,30 @@ import { SchedulableResourceType } from "@/types/scheduling/schedule";
* Atom for caching the last selected schedule service type (practitioner/healthservice/location)
* Uses localStorage to persist across sessions and logouts
* Only clears on cache/localStorage clear
*
* `getOnInit: true` — REVIEW FIX. Without it, `atomWithStorage` resolves to
* `initialValue` (Practitioner) on the very FIRST render and only reads
* `localStorage` in its `onMount`, which runs after mount. Any consumer
* that reads this atom to seed its OWN first-render state (`AppointmentEditor`'s
* `useState(() => initialResource(serviceType, currentUser))`,
* `AppointmentQuestion.tsx`'s identical pattern) captures the pre-hydration
* default and never sees the real persisted value unless it also runs a
* reconciling effect after the atom updates. `getOnInit: true` makes
* `atomWithStorage` read `localStorage` synchronously ONCE, right here,
* when this module loads and constructs the atom — not per store, per
* mount, or per consumer. That one read becomes the atom's resolved
* initial value in the jotai store; every `useAtom(scheduleServiceTypeAtom)`
* call anywhere thereafter just reads THAT value like any other atom
* state, so every consumer's first render already reflects the real
* preference — no race, no reconciling effect required.
*/
export const SCHEDULE_SERVICE_TYPE_KEY = "care_schedule_service_type";

export const scheduleServiceTypeAtom = atomWithStorage<SchedulableResourceType>(
SCHEDULE_SERVICE_TYPE_KEY,
SchedulableResourceType.Practitioner,
undefined,
{ getOnInit: true },
);

/**
Expand Down
Loading
Loading