fix(case-page): resolve full-page review findings (bugs + RTL + resilience)
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 4s
Lint — undefined names / undefined-names (pull_request) Successful in 10s

Full code review of the case-detail page (14 components) surfaced these,
all fixed here:

Logic bugs
- case-edit-dialog: form.reset ran on the 5s useCase refetch while the dialog
  was open, clobbering in-progress edits. Now resets only on open→true.
- status-changer: `selected` never synced to async/external `currentStatus`
  (stale dropdown). Reworked to track currentStatus until an explicit pick;
  resets to tracking after save.
- decision-blocks-panel: `block.content`/`word_count` accessed without null
  guards (endpoint has no response model) → potential render crash. Coerced
  with `?? ""` / `?? 0`. `STATUS_LABELS[status]` now falls back to the raw
  status instead of rendering literal "undefined".
- document-type-editor: `await mutateAsync()` in async click handlers without
  try/catch → unhandled promise rejection. Wrapped (errors still surface via
  isError).

Resilience / hygiene
- page.tsx: a transient 5xx on the background poll flipped the WHOLE page to
  the error card and discarded loaded data. Now gated on `!data`, plus a
  "נסה שוב" retry.
- cases.ts useUpdateCase: invalidated casesKeys.all, which re-invalidated the
  detail it had just optimistically patched. Scoped to the list prefix.

RTL correctness (logical properties)
- agent-status-widget `mr-auto`→`ms-auto`; agent-activity-feed `mr-auto`→
  `me-auto`, icon `ml-1/ml-2`→`me-1/me-2`, required `*` `mr-1`→`ms-1`;
  document-type-editor list `pr-4`→`ps-4`.

Minors
- drafts-panel: `<a href>`→`next/link` (operations + citation links) for SPA
  nav. agent-activity-feed: issueMap memoized; comment Textarea aria-label.
  upload-sheet: `unknown` status no longer shown as green success (neutral
  icon + "רענן לאישור"). citations.ts: case_name typed `string | null`.

Design gate: visual-touching items (RTL gap side, retry button, neutral
upload icon) were chair-authorized via the reviewed-findings approval ("fix
all"); none alter an approved page layout — they are correctness fixes.
tsc clean; eslint clean (1 pre-existing form.watch warning, untouched line).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-20 18:57:43 +00:00
parent 148b4b9bf6
commit 2b591f5018
11 changed files with 115 additions and 74 deletions

View File

@@ -1,6 +1,6 @@
"use client";
import { useRef, useState, useEffect } from "react";
import { useRef, useState, useEffect, useMemo } from "react";
import { Button } from "@/components/ui/button";
import { Textarea } from "@/components/ui/textarea";
import { Badge } from "@/components/ui/badge";
@@ -151,7 +151,7 @@ function CommentCard({
{identifier}
</Badge>
)}
<span className="text-[11px] text-ink-faint mr-auto flex items-center gap-1">
<span className="text-[11px] text-ink-faint me-auto flex items-center gap-1">
<Clock className="w-3 h-3" />
{timeAgo(comment.created_at)}
</span>
@@ -267,7 +267,7 @@ function AskUserQuestionsForm({
<fieldset key={q.id} className="space-y-2">
<legend className="text-sm font-semibold text-navy mb-1">
{q.prompt}
{(q.required ?? true) && <span className="text-rose-600 mr-1">*</span>}
{(q.required ?? true) && <span className="text-rose-600 ms-1">*</span>}
</legend>
<div className="space-y-1.5">
{q.options.map((opt) => {
@@ -316,7 +316,7 @@ function AskUserQuestionsForm({
{pending ? (
<Loader2 className="w-4 h-4 animate-spin" />
) : (
<Send className="w-4 h-4 ml-1" />
<Send className="w-4 h-4 me-1" />
)}
{interaction.payload.submitLabel || "שלח תשובה"}
</Button>
@@ -397,14 +397,14 @@ function RequestConfirmationForm({
onClick={handleReject}
disabled={pending || (requireReason && !reason.trim())}
>
<XCircle className="w-4 h-4 ml-1" />
<XCircle className="w-4 h-4 me-1" />
{rejectLabel}
</Button>
<Button size="sm" onClick={onAccept} disabled={pending}>
{pending ? (
<Loader2 className="w-4 h-4 animate-spin" />
) : (
<CheckCircle2 className="w-4 h-4 ml-1" />
<CheckCircle2 className="w-4 h-4 me-1" />
)}
{acceptLabel}
</Button>
@@ -489,7 +489,7 @@ function SuggestTasksForm({
onClick={() => (showReason ? onReject(reason.trim()) : setShowReason(true))}
disabled={pending}
>
<XCircle className="w-4 h-4 ml-1" />
<XCircle className="w-4 h-4 me-1" />
{showReason ? "אישור דחייה" : "דחייה"}
</Button>
<Button
@@ -500,7 +500,7 @@ function SuggestTasksForm({
{pending ? (
<Loader2 className="w-4 h-4 animate-spin" />
) : (
<CheckCircle2 className="w-4 h-4 ml-1" />
<CheckCircle2 className="w-4 h-4 me-1" />
)}
אישור משימות נבחרות ({selected.size})
</Button>
@@ -575,7 +575,7 @@ function InteractionCard({
{identifier}
</Badge>
)}
<span className="text-[11px] text-ink-faint mr-auto flex items-center gap-1">
<span className="text-[11px] text-ink-faint me-auto flex items-center gap-1">
<Clock className="w-3 h-3" />
{timeAgo(interaction.resolved_at ?? interaction.created_at)}
</span>
@@ -635,13 +635,13 @@ export function AgentActivityFeed({
const [body, setBody] = useState("");
const endRef = useRef<HTMLDivElement>(null);
// Build issue_id → identifier map
const issueMap = new Map<string, string>();
if (data?.issues) {
for (const iss of data.issues) {
issueMap.set(iss.id, iss.identifier);
}
}
// Build issue_id → identifier map (memoized — the feed refetches every 10s,
// and a fresh Map each render would defeat the child cards' memoization).
const issueMap = useMemo(() => {
const m = new Map<string, string>();
for (const iss of data?.issues ?? []) m.set(iss.id, iss.identifier);
return m;
}, [data?.issues]);
// Auto-scroll on new comments or interactions
const commentCount = data?.comments?.length ?? 0;
@@ -669,7 +669,7 @@ export function AgentActivityFeed({
if (isLoading) {
return (
<div className="flex items-center justify-center py-12 text-ink-faint">
<Loader2 className="w-5 h-5 animate-spin ml-2" />
<Loader2 className="w-5 h-5 animate-spin me-2" />
<span>טוען פעילות סוכנים...</span>
</div>
);
@@ -777,6 +777,7 @@ export function AgentActivityFeed({
value={body}
onChange={(e) => setBody(e.target.value)}
placeholder="כתוב הוראה לסוכנים..."
aria-label="הוראה לסוכנים"
className="min-h-[60px] resize-none text-sm"
dir="rtl"
onKeyDown={(e) => {
@@ -798,7 +799,7 @@ export function AgentActivityFeed({
{sendComment.isPending ? (
<Loader2 className="w-4 h-4 animate-spin" />
) : (
<Send className="w-4 h-4 ml-1" />
<Send className="w-4 h-4 me-1" />
)}
שלח
</Button>