feat(CodeLookup): memory picklist — frequently-used codes with device-trust storage - #351
feat(CodeLookup): memory picklist — frequently-used codes with device-trust storage#351horner wants to merge 14 commits into
Conversation
…mory Memory now needs two gates open: a real signed-in userId (placeholders like 'anonymous' no longer pool every kiosk visitor into one bucket) and a storage mode declared by the app. Storage defaults to 'session' (RAM, dies with the tab), so a public kiosk that forgets to configure leaks nothing; 'local' opts into the IndexedDB cache and asserts a login-secured machine. The server becomes the source of truth: syncFromServer revalidates on a TTL instead of seeding once, and its counts are authoritative on top of unflushed local picks, so a count can move down without losing a pick.
'Signed in as' and 'Device' drive the memory picklist's two gates through the ambient CodeLookup provider, so every Healthcare story demonstrates them and the decorator doubles as the reference wiring. Both default to the safe end (not signed in, public kiosk), leaving existing story baselines unchanged.
dynamicTitle defaults to true, so every global widened the bar with its selected label; the labels still show in each dropdown and the button's aria-label keeps carrying the current value.
Emoji move into `title` so the button is one glyph wide while still reporting its setting, and the wording moves to the dropdown's `right` column. Theme, density and device use per-item icons instead (sun/moon, grow/collapse, unlock/lock), so the lock icon alone says whether picks are cached.
Each role gets its own memory bucket, so the picklist can be shown filling differently for a prescriber, a rooming nurse and a patient portal.
Deploying ui with
|
| Latest commit: |
b1d4b9c
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://923b1782.ui-6d0.pages.dev |
| Branch Preview URL: | https://code-memory.ui-6d0.pages.dev |
There was a problem hiding this comment.
Pull request overview
Adds an opt-in “Frequently used” memory picklist to CodeLookup, backed by a device-trust-aware local cache (session/IndexedDB) with optional server revalidation + debounced delta flush, plus YAML import/export tooling and Storybook globals to demo the gating behavior.
Changes:
- Introduces a new memory storage backend + store (usage counts per
{userId, context}) with optional server sync and flush. - Wires memory picklist behavior into
CodeLookup(empty-query focus shows remembered codes; remembered matches rank ahead while typing). - Adds YAML import/export utilities + tests and updates Storybook/README to document and demonstrate the feature.
Reviewed changes
Copilot reviewed 14 out of 15 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| src/components/CodeLookup/README.md | Documents the new memory prop, gating rules, server protocol, and YAML import/export. |
| src/components/CodeLookup/memoryYaml.ts | Implements YAML export/import (lazy js-yaml loading) for memory buckets. |
| src/components/CodeLookup/memoryYaml.test.ts | Adds unit tests for YAML round-tripping, merging, and validation behavior. |
| src/components/CodeLookup/memoryStore.ts | Implements the memory store: record/getTop/sync/flush/match/clear APIs. |
| src/components/CodeLookup/memoryStore.test.ts | Adds unit tests covering isolation, sync authority, flushing, gates, and modes. |
| src/components/CodeLookup/memoryBackend.ts | Adds storage backends (IndexedDB/session/none) and dispatch logic. |
| src/components/CodeLookup/index.ts | Re-exports memory APIs/types and YAML APIs from the CodeLookup barrel. |
| src/components/CodeLookup/engine.ts | Adds viaMemory marker on result rows. |
| src/components/CodeLookup/context.tsx | Adds provider-level memory defaults and a provider-wide disable switch (memory={false}). |
| src/components/CodeLookup/CodeLookup.tsx | Integrates memory picklist UI/behavior and recording/flush wiring. |
| src/components/CodeLookup/CodeLookup.stories.tsx | Adds a Storybook demo with YAML import/export controls. |
| .storybook/preview.tsx | Adds Storybook toolbar globals for “Signed in as” and “Device” and wires provider memory defaults. |
| eslint.config.js | Adds IDB globals needed by the new backend code/tests. |
| pnpm-lock.yaml | Updates lockfile entries (unrelated to CodeLookup functionality). |
| code-memory-plan.md | Adds an implementation plan/notes document for the feature. |
Files not reviewed (1)
- pnpm-lock.yaml: Generated file
- export ties break on lastUsed like the picklist, not label - a missing js-yaml now says so, and the cached import resets so a later install works without a reload - clearMemory drops the scope's revalidation TTL, otherwise a cleared bucket stayed empty until it expired - safelist text-primary-600 for Tailwind 3 consumers
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 15 out of 16 changed files in this pull request and generated no new comments.
Files not reviewed (1)
- pnpm-lock.yaml: Generated file
Suppressed comments (2)
src/components/CodeLookup/memoryStore.ts:249
syncFromServerbuilds the GET URL by string-concatenating?user=…&context=…. IfserverUrlalready contains a query string (or a trailing?), this will produce an invalid URL (double?) and the sync will fail.
Build the URL with new URL() + searchParams so existing query params are preserved and encoding is handled correctly.
const url = `${serverUrl}?user=${encodeURIComponent(scope.userId)}&context=${encodeURIComponent(scope.context)}`;
const res = await fetch(url, { credentials: 'include' });
src/components/CodeLookup/CodeLookup.tsx:304
setMemoryStorage(memStorage)runs unconditionally (andmemStoragecurrently defaults to'session'). Because the storage mode inmemoryBackendis a module-level singleton, rendering aCodeLookupwithout a provider (or without memory enabled) can still force global storage back to'session', disabling persistence for otherCodeLookupinstances that expect'local'.
While touching this block, also prefer ?? for memCfg.limit so a caller-provided 0 isn’t silently replaced by the default.
// Declared first so the store knows the device's trust level before the
// effect below reads or writes anything.
React.useEffect(() => {
setMemoryStorage(memStorage);
}, [memStorage]);
The picklist needed two things wired per instance (a memory config and a
context), so most lookups silently had no memory. Now a provider that names
a signed-in memory.userId turns it on everywhere below it, and the bucket
defaults to the box's own domains so a med picker and a problem picker still
never share a list.
memory={false} remains the opt-out, on the provider (kiosk) or one box.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 15 out of 16 changed files in this pull request and generated no new comments.
Files not reviewed (1)
- pnpm-lock.yaml: Generated file
Suppressed comments (1)
src/components/CodeLookup/memoryStore.ts:153
- recordUse() does a read-modify-write (memGet → memSet). If recordUse is called concurrently for the same (userId, context, fullid) key, both calls can read the same previous value and one increment can be lost. Serializing updates per entry key avoids undercounting and keeps ordering stable under rapid selections.
const key = entryKey(scope, result.fullid);
const prev = await memGet(key);
const entry: MemoryEntry = {
userId: scope.userId,
context: scope.context,
CodeLookup memory — frequently-used codes, safe on shared machines
Adds a "Frequently used" picklist to
CodeLookup: focus the empty search box andthe codes you actually pick are right there, ranked by how often you use them.
Memory is off unless the deployment turns it on, and it stays off on any
machine that isn't trusted.
Focus on the field shows a dropdown:
Searching
What's in it
Memory store (
memoryStore.ts) — usage counts per{ userId, context }bucket, so a prescriber's med list and a rooming nurse's are separate.
Picklist — empty-query focus shows the top N; while typing, remembered
codes rank ahead of index hits.
Server is the source of truth — the local store is a cache. Counts
revalidate on a TTL, the server's number is authoritative (it can move a count
down), and unflushed local picks are added on top so the list never goes
backwards while you work. Picks flush debounced with
credentials: 'include'.Two gates, both fail closed — a real signed-in
userId(anonymous,guest,kiosk, empty… never remember) and a storage mode:storage'session'(default)'local''none'memory={false}on the provider is the one-line kiosk switch and beats anyper-instance opt-in.
YAML import/export — buckets round-trip as YAML for seeding, backup and
moving a clinician's list between systems.
Storybook changes
Storybook toolbar globals — "Signed in as" (not signed in / two doctors /
nurse / reception / patient) and "Device" (public kiosk vs trusted
workstation) drive the gates live, so the behaviour is demonstrable rather
than described.
PHI note
Nothing is written to disk unless the deployment declares the machine trusted
and a real user is signed in. The server must take identity from the session —
the
?user=in the sync URL is a hint for the cache key, never an authorization(documented in the README).
Verified
writes no IndexedDB row and forgets on reload; a trusted workstation survives
reload; each identity sees its own bucket.
Not in this PR
Forwarding
memory={{ context }}from the Healthcare wrappers (MedicationList,OrderEditor,ProblemList,AllergyList, …) — the plumbing is ready, thewiring is a follow-up.