docs(uat): SHIPPED annotations for PR4 (a11y primitives + click-to-preview)
Annotate 4 finding entries: - em-dash lint guard (sweep parked) - DocumentList Download in kebab - WatchersCard empty-state padding - EOI empty-state Mark Signed button - Platform-wide click-to-preview (FileGrid + DocumentList; 2 remaining surfaces parked) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -84,8 +84,9 @@ _Copy tweaks, alignment, single-prop edits, obvious typos._
|
||||
> - **Form validation never sets `aria-invalid` / `role="alert"` / `aria-live`** across every react-hook-form caller. SR users get zero feedback on validation failure. Build a shared `<FieldError>` component emitting both visible text + ARIA. Sweep all forms. ~2h. **Bundles with the Bucket 2 form-error UX finding** — same surfaces, same primitive.
|
||||
> - **Icon-only buttons inconsistent — ~50% have `aria-label`, rest have nothing or only `sr-only` text.** Add `jsx-a11y/control-has-associated-label` lint rule + sweep. ~1h.
|
||||
> - **Sweep: remove em-dashes from all user-facing copy (toast messages, button labels, helper text, banners, dialog descriptions, empty states)** — em-dashes (`—`) feel AI-generated and add visual noise; user reads them as "Claude wrote this." Replace with periods, commas, colons, or simple hyphens depending on context. **Scope:** _src/components_ (every UI string), _src/lib/email/templates_ (email body copy), _src/lib/templates_ (merge-field labels + EOI body), _src/app_ (page-level copy), public form copy, error messages from `src/lib/errors`. **Out of scope (keep em-dashes):** code comments, JSDoc, audit-log entries, structured logging, this UAT findings doc itself (internal docs are fine). **Method:** grep `—` across `src/`, manually triage each match (some are inside JSX, some inside string literals); replace per context. Heuristic: if a user could see the character, replace it. **Effort:** ~2-3h depending on hit count (rough estimate 200-400 instances). Captured 2026-05-21 from UAT. **Going forward:** add an ESLint rule banning `—` in JSX text + string literals inside `src/components` so new code doesn't reintroduce them.
|
||||
> - **SHIPPED (lint guard only) in 52342ee:** `no-restricted-syntax` rule on `JSXText[value=/—/]` scoped to `src/components` + `src/app`, set to `warn`. 111 existing instances flagged as warnings — sweep remains parked.
|
||||
> - **Custom-field form: "Sort Order" needs an explainer tooltip — example of a broader gap** — _src/components/admin/custom-fields/custom-field-form.tsx:298-308_ — surfaces a specific instance of a platform-wide gap: see the next finding for the full sweep.
|
||||
> - **DocumentList DocRow kebab: add "Download" action** — _src/components/documents/document-list.tsx:86-109_ — current kebab has Send-for-Signing (draft only), Move-to-folder, Delete. No Download. Reps reviewing a signed doc from the interest's documents tab have to navigate into the document detail to download. Add a `<DropdownMenuItem>` at the top of the menu when `doc.signedFileId` is set (or `doc.fileId` for non-Documenso docs like manual uploads), wired to the same `apiFetch('/api/v1/files/[id]/download')` + anchor-click pattern used elsewhere. Permission-gate by `files.download` if that perm exists. ~10 min. Captured 2026-05-21 from UAT.
|
||||
> - **DocumentList DocRow kebab: add "Download" action** — _src/components/documents/document-list.tsx:86-109_ — current kebab has Send-for-Signing (draft only), Move-to-folder, Delete. No Download. Reps reviewing a signed doc from the interest's documents tab have to navigate into the document detail to download. Add a `<DropdownMenuItem>` at the top of the menu when `doc.signedFileId` is set (or `doc.fileId` for non-Documenso docs like manual uploads), wired to the same `apiFetch('/api/v1/files/[id]/download')` + anchor-click pattern used elsewhere. Permission-gate by `files.download` if that perm exists. ~10 min. Captured 2026-05-21 from UAT. **SHIPPED in 52342ee:** DocRow now renders Download at the top of the kebab when `signedFileId` is set; wired via the existing `triggerUrlDownload` helper from PR1.
|
||||
> - **InterestEoiTab "Open" link too ambiguous — relabel to "Open in Documents"** — _src/components/interests/interest-eoi-tab.tsx:163_ — the link in the EOI history list goes to `/${portSlug}/documents/${d.id}` (Documents Hub doc detail) but the label just says "Open" + an external-link icon. Rep can't tell where it goes until they hover. Change to `Open in Documents` (or `View in Documents`). Apply the same idiom anywhere else a cross-section navigation link uses bare "Open" — quick grep + sweep. ~5 min. Captured 2026-05-21 from UAT.
|
||||
> - **PaymentsSection: deprioritize layout — move below milestones + collapse-by-default at Reservation** — _src/components/interests/interest-tabs.tsx:633 + 846-852_ (current `showPaymentsSection = reservationStageReached` + renders at line 847, ABOVE the milestone strip at line 859+) + _src/components/interests/payments-section.tsx_ (the section component itself). Today: hidden pre-Reservation (correct ✓), shows as a full expanded card at Reservation+ sitting above the milestone strip. Section is reference/history once expected — milestone work (active step) should be the rep's primary visual focus, not deposits-tracking.
|
||||
> - **Fix (three states):**
|
||||
@@ -95,7 +96,7 @@ _Copy tweaks, alignment, single-prop edits, obvious typos._
|
||||
> - **Render order change in interest-tabs.tsx:** lift the PaymentsSection mount from its current position (line 846-852, above milestones) to AFTER the milestone strip + AFTER the OverviewTab grid (below "Latest note", Tags, Berth requirements). It becomes the last visual element on the OverviewTab.
|
||||
> - **Collapse state:** persist per-interest via Zustand or react-query cache (so re-opening the same deal remembers the rep's last expand/collapse). Default collapsed unless a deposit was added in this session.
|
||||
> - **Effort:** ~1-1.5h (layout reorder + collapsed-bar state + summary chip + render-order verification). Captured 2026-05-21 from UAT.
|
||||
> - **WatchersCard empty state missing bottom padding** — _src/components/documents/document-detail.tsx:546_ — `<p className="text-xs text-muted-foreground">No one is watching this document yet.</p>` has no margin while the sibling populated `<ul>` at line 548 has `mb-3 space-y-1`. Empty state text sits flush against the add-watcher form below. Add `mb-3` to the empty-state `<p>` to match. ~30s. Captured 2026-05-21 from UAT.
|
||||
> - **WatchersCard empty state missing bottom padding** — _src/components/documents/document-detail.tsx:546_ — `<p className="text-xs text-muted-foreground">No one is watching this document yet.</p>` has no margin while the sibling populated `<ul>` at line 548 has `mb-3 space-y-1`. Empty state text sits flush against the add-watcher form below. Add `mb-3` to the empty-state `<p>` to match. ~30s. Captured 2026-05-21 from UAT. **SHIPPED in 52342ee.**
|
||||
> - **DocumentDetail Interest link should show berth(s), not duplicate the client name** — _src/components/documents/document-detail.tsx:96 (type) + 237-241 (linked-entity row builder)_ + the document-detail API service that hydrates `linked.interest`. Today renders `Client: Matthew Ciaccio · Interest: Matthew Ciaccio` — visually redundant, and the Interest link carries no distinct information. Should be `Client: Matthew Ciaccio · Interest: A1-A3, B5-B7` (berth range via the existing `formatBerthRange()` helper from `src/lib/templates/berth-range.ts`, same idiom as the locked folder-naming convention and the external-EOI default title).
|
||||
> - **Backend:** swap the response payload's `interest: { id, clientName }` → `interest: { id, berthLabel }` where `berthLabel` is derived in the service layer from the interest's primary or in-bundle berths. Falls back to "No berths linked" when no berths are attached.
|
||||
> - **Frontend:** change line 241 from `sub: linked.interest.clientName` → `sub: linked.interest.berthLabel ?? 'No berths linked'`.
|
||||
@@ -106,7 +107,7 @@ _Copy tweaks, alignment, single-prop edits, obvious typos._
|
||||
> - **Sweep:** drop-in replacement at each of the 7 sites. Pair with the platform-wide file-preview work (Bucket 3) so picker-then-preview becomes consistent everywhere.
|
||||
> - **Effort:** ~10 min for the primitive; ~30-45 min for the 7-site sweep. Total ~1h. Captured 2026-05-21 from UAT.
|
||||
> - **SHIPPED (primitive) in 8f42940:** `src/components/ui/file-input-button.tsx` lands with the shape the queue asked for + an optional `showSelectedFilename` mode. external-eoi-upload-dialog migrated. The 5 other queued sites were re-audited — they already use the hidden-input + Button-trigger pattern (no browser-default UI visible), so no migration was needed; the primitive is in place for any new caller.
|
||||
> - **EOI empty state: add "Mark as signed without file" button (parity with Reservation + Contract tabs)** — _src/components/interests/interest-eoi-tab.tsx:553-562_ (`EmptyEoiState` only renders Generate + Upload paper-signed) — `MarkExternallySignedDialog` already supports `docType: 'eoi'` (mark-externally-signed-dialog.tsx:37-41) with full copy ("Flips the EOI sub-status to 'signed' without uploading a file…"); the reservation tab uses the same dialog via a third ghost-button row (interest-reservation-tab.tsx:378-380). EOI tab's empty state just never grew the button. Add it as a third ghost-variant Button, wired to a `setMarkExternalOpen(true)` state hook + the existing dialog. ~5-10 min. Captured 2026-05-21 from UAT.
|
||||
> - **EOI empty state: add "Mark as signed without file" button (parity with Reservation + Contract tabs)** — _src/components/interests/interest-eoi-tab.tsx:553-562_ (`EmptyEoiState` only renders Generate + Upload paper-signed) — `MarkExternallySignedDialog` already supports `docType: 'eoi'` (mark-externally-signed-dialog.tsx:37-41) with full copy ("Flips the EOI sub-status to 'signed' without uploading a file…"); the reservation tab uses the same dialog via a third ghost-button row (interest-reservation-tab.tsx:378-380). EOI tab's empty state just never grew the button. Add it as a third ghost-variant Button, wired to a `setMarkExternalOpen(true)` state hook + the existing dialog. ~5-10 min. Captured 2026-05-21 from UAT. **SHIPPED in 52342ee.**
|
||||
> - **Activity feed: "See all" link to the full audit log** — _src/components/dashboard/activity-feed.tsx_ (ActivityFeedInner, around line 175) — the card lists the most recent audit events but has no jump-off to the full audit-log page. Add a "See all" link in the card header (or as a trailing row underneath the list). Confirm the target route (likely `/{portSlug}/admin/audit-log`) and permission-gate the link by the same `audit_log.view` perm the admin sidebar uses, so non-admin reps see the card but not the link. ~10 min.
|
||||
|
||||
1. **Dev-mode banner dismissible** — _src/components/shared/dev-mode-banner.tsx:23_ — added X close button + localStorage persistence keyed by redirect address. Fixed in this session.
|
||||
@@ -772,6 +773,7 @@ _Functional defects. Tag each with `[critical|high|medium|low]` prefix._
|
||||
- **Title cell specifically:** wrap the filename cell in a button-styled span with `onClick={() => onPreview(row)}` so the rep's natural click target works. Keep the row's other action cells (View, Download, kebab) untouched — they're secondary affordances.
|
||||
- **Bundle with Bucket 3 #000 (universal preview)** — pointless to make every row click-to-preview if half the file types render a blank dialog. Ship the two together: file-row surfaces all click-to-preview AND `FilePreviewDialog` handles every mime type (or shows a graceful fallback).
|
||||
- **Effort:** ~1-1.5h for the click-target sweep across 4-5 surfaces; ~5-7h with the universal-preview piece bundled. Captured 2026-05-21 from UAT (FileGrid surfaced specifically; Recent Files captured earlier).
|
||||
- **SHIPPED (FileGrid + DocumentList) in 52342ee:** FileGrid card body is now a `<button onClick={onPreview}>`. DocumentList title cell on rows with `signedFileId` opens `FilePreviewDialog`; kebab keeps More Actions, gains Download. Remaining: aggregated-section.tsx Recent Files + entity-folder-view.tsx — parked for next wave (~30-45min each).
|
||||
11. **[high] Supplemental-info form blocked by portal kill-switch (route nested under `(portal)` group)** — _src/app/(portal)/public/supplemental-info/[token]/page.tsx_ (current location) + _src/app/(portal)/layout.tsx:25-37_ (`isPortalDisabledGlobally()` short-circuit returns "Client portal unavailable" screen for ALL children). The supplemental-info form is token-protected and conceptually independent of the portal login concept — it's a one-shot URL emailed to a client to fill in extra info for an EOI, and should always work as long as the token is valid. But because the route lives inside the `(portal)` route group, it inherits the layout's "portal disabled?" gate. Net effect: any port that hasn't opted into the client portal (the default state for most ports right now) cannot use the supplemental-info flow at all — clients see the "Client portal unavailable" screen when they click the emailed link, even though the rep just sent it successfully.
|
||||
- **Fix:** move the file from `src/app/(portal)/public/supplemental-info/[token]/page.tsx` → `src/app/public/supplemental-info/[token]/page.tsx` (out of the route group). URL stays identical (`/public/supplemental-info/<token>`) because Next route groups don't affect URLs — the route group's only effect was layout inheritance, and moving it drops the portal gate. Verify the API route at `/api/public/supplemental-info/[token]` doesn't have a similar nesting issue (likely fine — `/api/` paths don't share the `(portal)` layout).
|
||||
- **Sweep:** audit `src/app/(portal)/` for any other anonymous token routes that should be outside the group. Currently `find` only returns the one file, but worth verifying as new public flows are added (password-reset tokens, magic-link tokens for non-portal flows, etc.).
|
||||
|
||||
Reference in New Issue
Block a user