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
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.
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.
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.json | Config | +7 | -3 |
| messages/hi.json | Config | +7 | -3 |
| public/brand/google-drive.svg | Source | +8 | -0 |
| src/components/matters/base-sheet.tsx | Component | +13 | -1 |
| src/components/matters/import-into-matter-sheet-v2.stories.tsx | Component | +36 | -3 |
| src/components/matters/import-into-matter-sheet-v2.tsx | Component | +62 | -13 |
| src/lib/google/drive-fetch.test.ts | Library | +51 | -1 |
| src/lib/google/drive-fetch.ts | Library | +23 | -5 |
| src/lib/google/drive-picker.test.ts | Library | +93 | -0 |
| src/lib/google/drive-picker.ts | Library | +85 | -5 |