PR: #612: feat(import): drawer polish — icon buttons, source instruction copy, outside-click close, Drive token reuse [Story-2393] by @ankitmaheshwari-com
View PR →

Story-2393 · pramaan-app · 14 Jul 2026

The drawer stops making counsel guess

Story-2386 opened the Drive door; staging UAT then showed the four places the drawer still asked counsel to work things out alone. Buttons that read as plain text. A grey dropzone that refused files without saying why. An outside click that did nothing. And Google asking for consent again on every single pick. This Story answers all four — no new pipeline, no new scope, no backend change.

The one idea

The drawer teaches the model at the moment the model binds. Counsel is asked exactly one irreversible-feeling question — whose documents are these? — so the drawer now states the question, states what the answer does, and states that it can be corrected. Everything else here is the same principle applied to gestures: a click outside means “I’m done” only when nothing is in flight, and a consent popup that has already been given is not asked for twice.

What shipped

Piece What it does
Buttons read as buttons FileUp + “Browse files”, FolderOpen + “Choose folder”, and Google’s own tri-colour Drive mark + “Google Drive”. One line each (whitespace-nowrap), equal height, one gap. The mark lives in public/brand/google-drive.svg — a third party’s brand hexes are not PRAMAAN tokens and must not be inlined into src/**.
The unarmed dropzone teaches “Drag files here” (an instruction counsel cannot follow — the zone is disabled) becomes “Whose documents are these?” plus the line that Strategy and privileged papers never enter the Record. The disabled state is no longer a dead end; it is the lesson.
The armed dropzone reassures Under the buttons: “Everything added here is marked Client’s. Any file’s side can be corrected after it lands.” (Strategy says marked for Strategy, outside the Record.) The stamp is stated, and so is its reversibility — which is what stops counsel hesitating over the chip.
Outside click closes — with two vetoes An idle drawer dismisses on an outside click (the X still works, always). It does not close while the Google Picker is open, nor while any row is fetching / uploading / reading. Busy plus outside click = no-op.
Drive folders: a picker gesture, not a feature One hint line: open the folder in the picker and select all its files (⌘A). See below for why this is the honest answer and not a shortcut.
In-session token reuse The GIS access token and its expiry are cached in module memory. The second pick within the hour opens the picker straight away, no popup. Refreshed five minutes early, silently (prompt: ""). No localStorage, no cookie, no server copy — a reload forgets it.

Why folder import is a gesture, not a button

The obvious feature is: pick a folder, we fetch its children. We cannot, and the reason is the scope we deliberately chose. PRAMAAN asks Google for drive.file — the narrowest scope that exists: it grants access to the individual files counsel hands us in our own picker, and nothing else. A folder’s children were never handed to us, so they are not reachable, by design.

The alternative is drive.readonly: a restricted scope that reads counsel’s entire Drive, gated behind Google’s CASA security assessment. For a litigation platform holding privileged material, asking a law firm for blanket read access to everything they own is the wrong trade at any price. So we keep the narrow grant and say the true thing out loud: open the folder in the picker, select all (⌘A). Multi-select was already enabled; the picker already navigates folders. Nothing to build — only something to say.

Why an outside click needs guards at all

A drawer that closes on an outside click is table stakes — until you notice what counts as “outside”. Google’s picker renders its own dialog into <body>, outside our Radix content. Every click counsel makes inside the picker — every file they select — is an outside click on the drawer behind it. Without a guard, choosing a file would close the very drawer waiting to receive it.

The second guard is about bytes, not pixels. Rows that are fetching, uploading or reading own live requests. Unmounting the drawer kills them. So the drawer vetoes its own dismissal while any row is in flight — and only while: the X still closes it at any time, because a deliberate close means “I trust the background”, exactly what the footer’s trust line promises.

outside click (pointer down / focus outside) │ ▼ onInteractOutside │ ├── Google Picker open (drive.pending) ──▶ preventDefault (drawer stays) ├── any row fetching/uploading/reading ──▶ preventDefault (drawer stays) └── idle ────────────────────────────────▶ close

The token: consent once, not once per pick

Before this Story every “Choose from Drive” minted a brand-new token, so Google’s consent popup appeared on every single pick — a firm importing eight bundles saw it eight times. The token was always good for an hour; we simply threw it away.

Now it is held in module memory with its expiry, reused until five minutes before it dies, and re-issued silently after that. And because a token can die ahead of its own clock (revoked, rotated), a Drive fetch that comes back 401 drops the cache, re-issues once with prompt: "", and repeats the call. One retry, then the row fails honestly. Counsel sees a popup once per page life — and a reload forgets everything, which is the point of module memory rather than storage.

Choose from Drive │ ▼ cached token alive (> 5 min left)? ──yes──▶ picker opens, NO popup │ no ▼ GIS token client (silent if we held one before, else consent) │ ▼ fetch bytes ──401──▶ drop cache ──▶ silent re-issue ──▶ retry ONCE ──▶ bytes │ 401 again ▼ row fails, retryable

Where it lives

File Change
src/components/matters/import-into-matter-sheet-v2.tsx Icon buttons, tri-colour Drive mark, unarmed / armed instruction copy, the folder hint, and the two outside-click vetoes.
src/components/matters/base-sheet.tsx Opt-in closeOnInteractOutside + an onInteractOutside the caller can veto. Other sheets keep X-only, unchanged.
src/lib/google/drive-picker.ts Module-memory token cache, 5-minute skew, silent re-issue, refreshDriveAccessToken().
src/lib/google/drive-fetch.ts A single 401 retry with a freshly re-issued token.
public/brand/google-drive.svg Google’s tri-colour mark, kept out of src/** so no brand hex enters the token-disciplined source tree.
messages/{en,hi}.json Six new keys in both locales; dragFilesHere and chooseFromDrive retired.

How to see it

Storybook → Matters/Import/ImportIntoMatterSheetV2: Empty (the unarmed instruction; click the scrim — it closes), ArmedClient (the armed line), DrivePickerLoading and BusyOutsideClickNoClose (click the scrim — nothing happens; the X still works). Token reuse is live-only: pick from Drive twice in one page life and the second pick shows no Google popup.

Files changed

10 file(s), +385 / -34 lines

Path Role + -
messages/en.jsonConfig+7-3
messages/hi.jsonConfig+7-3
public/brand/google-drive.svgSource+8-0
src/components/matters/base-sheet.tsxComponent+13-1
src/components/matters/import-into-matter-sheet-v2.stories.tsxComponent+36-3
src/components/matters/import-into-matter-sheet-v2.tsxComponent+62-13
src/lib/google/drive-fetch.test.tsLibrary+51-1
src/lib/google/drive-fetch.tsLibrary+23-5
src/lib/google/drive-picker.test.tsLibrary+93-0
src/lib/google/drive-picker.tsLibrary+85-5