Skip to content

feat(ux): resilience & feedback — error boundaries, no-flash lists, skeletons#617

Merged
SoundMindsAI merged 2 commits into
mainfrom
feat_ux_resilience_feedback
Jul 11, 2026
Merged

feat(ux): resilience & feedback — error boundaries, no-flash lists, skeletons#617
SoundMindsAI merged 2 commits into
mainfrom
feat_ux_resilience_feedback

Conversation

@SoundMindsAI

Copy link
Copy Markdown
Owner

Second UX-audit fix PR. Scope: the "states, feedback & errors" findings.

Changes

  • Route boundaries (previously absent → blank white screen on any render error/bad route):
    • app/not-found.tsx — branded 404 with a Dashboard link.
    • app/error.tsx — recoverable render-error card with reset() + Dashboard fallback.
    • app/global-error.tsx — root boundary (renders its own html/body).
  • No-flash lists — added placeholderData: keepPreviousData to all 11 paginated list hooks (studies, trials, proposals, judgments, clusters, config-repos, conversations, documents, query-sets, query-set queries, query-templates). Sorting/filtering/searching/paginating now keeps the prior page visible instead of flashing to a loading state and resetting scroll. By-id hooks intentionally excluded (showing stale wrong data during a detail navigation would be worse).
  • Skeletons — new ui/skeleton.tsx primitive (reduced-motion aware). DataTable's loading branch and DetailPageShell's loading branch now render shaped skeletons instead of bare "Loading…" text, holding layout stable.
  • Honest table error state — DataTable's error branch shows a real message (optional errorMessage prop) + an always-present Retry button (optional onRetry, falls back to page reload) instead of a static "Try again" with no affordance.
  • PR-open success toast — the proposal page now toasts "Pull request opened" on the polled status transition to pr_opened (the product's key success event was previously silent). Fires once, and not when merely viewing an already-opened proposal.

Deferred to later PRs (tracked)

Per-page <title>s and empty-state CTA parity fit the IA pass and land in the navigation PR; per-list errorMessage/onRetry wiring is now possible via the new optional props (reload fallback works everywhere today).

Testing

New error-boundary + Skeleton render tests; DataTable error-state test rewritten for the new UI (real message + retry callback). Full suite green (1304); tsc + eslint clean.

🤖 Generated with Claude Code

…keletons

UX-audit "states, feedback & errors" pass:

- Route boundaries: app/not-found.tsx (branded 404 + Dashboard link),
  app/error.tsx (recoverable render-error card with reset()), and
  app/global-error.tsx (root boundary) replace Next's blank-white defaults.
- No-flash lists: add placeholderData: keepPreviousData to all 11 paginated
  list hooks (studies, trials, proposals, judgments, clusters, config-repos,
  conversations, documents, query-sets, query-set queries, query-templates)
  so sorting/filtering/searching/paginating keeps the prior page visible
  instead of flashing to a loading state and resetting scroll. By-id hooks
  intentionally excluded (stale wrong data would be worse).
- Skeletons: new ui/skeleton.tsx primitive (reduced-motion aware). DataTable's
  loading branch and DetailPageShell's loading branch now render shaped
  skeletons instead of bare "Loading…" text, holding layout stable.
- Honest table error state: DataTable's error branch shows a real message
  (optional errorMessage prop) + an always-present Retry button (optional
  onRetry, falls back to page reload) instead of a static "Try again" with no
  affordance.
- PR-open success toast: the proposal page now toasts "Pull request opened" on
  the polled status transition to pr_opened (the money path was previously
  silent). Fires once, and not when merely viewing an already-opened proposal.

Tests: error-boundaries + Skeleton render tests; DataTable error-state test
rewritten for the new UI (message + retry callback). Full suite green
(1304); tsc + lint clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: SoundMindsAI <eric.starr@soundminds.ai>

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Review

This pull request introduces robust error boundaries, skeleton loaders, and enhanced error-handling states across the application, including a new Skeleton component and a global 404 page. It also integrates keepPreviousData across multiple TanStack Query hooks to prevent layout flashes during pagination, sorting, or filtering. Feedback focuses on aligning the button label with its action in the root error boundary and guarding the proposal status transition ref against temporary undefined states to ensure the success toast fires reliably.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +45 to +56
<button
type="button"
onClick={reset}
style={{
padding: '0.5rem 1rem',
borderRadius: '0.375rem',
border: '1px solid #d1d5db',
cursor: 'pointer',
}}
>
Reload
</button>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The button is labeled Reload but it calls reset(), which only triggers a React-level re-render of the route segment rather than a full browser reload. To avoid user confusion and align with Next.js conventions (and error.tsx on line 39), consider labeling the button Try again. Alternatively, if a full browser reload is intended (which is often useful for root-level crashes like chunk load errors), you can use window.location.reload().

Suggested change
<button
type="button"
onClick={reset}
style={{
padding: '0.5rem 1rem',
borderRadius: '0.375rem',
border: '1px solid #d1d5db',
cursor: 'pointer',
}}
>
Reload
</button>
<button
type="button"
onClick={reset}
style={{
padding: '0.5rem 1rem',
borderRadius: '0.375rem',
border: '1px solid #d1d5db',
cursor: 'pointer',
}}
>
Try again
</button>

Comment on lines +157 to +163
useEffect(() => {
const prev = prevStatusRef.current;
if (prev && prev !== 'pr_opened' && proposalStatus === 'pr_opened') {
toast.success('Pull request opened');
}
prevStatusRef.current = proposalStatus;
}, [proposalStatus]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

If proposalStatus temporarily becomes undefined (for example, during a hard query reload or a temporary network error), prevStatusRef.current will be overwritten with undefined. If the status then recovers and flips directly to 'pr_opened', the success toast will not fire because prev is undefined instead of 'pending'. Guarding the ref update to only store defined statuses ensures the transition is tracked reliably across temporary loading states.

Suggested change
useEffect(() => {
const prev = prevStatusRef.current;
if (prev && prev !== 'pr_opened' && proposalStatus === 'pr_opened') {
toast.success('Pull request opened');
}
prevStatusRef.current = proposalStatus;
}, [proposalStatus]);
useEffect(() => {
const prev = prevStatusRef.current;
if (prev && prev !== 'pr_opened' && proposalStatus === 'pr_opened') {
toast.success('Pull request opened');
}
if (proposalStatus !== undefined) {
prevStatusRef.current = proposalStatus;
}
}, [proposalStatus]);

- global-error: the "Reload" button now does a real window.location.reload()
  (robust recovery for a root-layout/chunk crash) instead of React reset(),
  so the action matches the label.
- proposals: only store DEFINED proposal statuses in the transition ref, so a
  transient undefined (hard reload / brief network error) can't erase the
  prior status and swallow the "Pull request opened" toast on recovery.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: SoundMindsAI <eric.starr@soundminds.ai>
@SoundMindsAI

Copy link
Copy Markdown
Owner Author

Review adjudication

Gemini — 2 findings, both accepted + fixed in 87418b12:

Finding Verdict Action
global-error.tsx — button labeled "Reload" but calls React reset() (segment re-render, not a reload) Accept Switched to window.location.reload() — the robust recovery for a root-layout/chunk crash, and now matches the label.
proposals/[id] — a transient undefined status overwrites the transition ref and could swallow the PR-opened toast Accept Guard the ref update to only store defined statuses.

Pre-existing red check (not this PR): the backend (contract+integration) UBI test documented in #610.

@SoundMindsAI
SoundMindsAI merged commit 9f66266 into main Jul 11, 2026
17 of 20 checks passed
@SoundMindsAI
SoundMindsAI deleted the feat_ux_resilience_feedback branch July 11, 2026 12:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant