fix(case-page): resolve full-page review findings (bugs + RTL + resilience)
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:
@@ -56,7 +56,7 @@ export function DecisionBlocksPanel({ caseNumber }: { caseNumber: string }) {
|
||||
}
|
||||
if (!data) return null;
|
||||
|
||||
const written = data.blocks.filter((b) => b.word_count > 0).length;
|
||||
const written = data.blocks.filter((b) => (b.word_count ?? 0) > 0).length;
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
@@ -101,11 +101,11 @@ export function DecisionBlocksPanel({ caseNumber }: { caseNumber: string }) {
|
||||
{blockLabel(block)}
|
||||
</span>
|
||||
<Badge
|
||||
className={`text-[0.65rem] border ${STATUS_CLASSES[block.status]}`}
|
||||
className={`text-[0.65rem] border ${STATUS_CLASSES[block.status] ?? ""}`}
|
||||
>
|
||||
{STATUS_LABELS[block.status]}
|
||||
{STATUS_LABELS[block.status] ?? block.status}
|
||||
</Badge>
|
||||
{block.word_count > 0 && (
|
||||
{(block.word_count ?? 0) > 0 && (
|
||||
<span className="text-[0.7rem] text-ink-muted tabular-nums">
|
||||
{block.word_count} מילים
|
||||
</span>
|
||||
@@ -137,19 +137,22 @@ function BlockEditor({
|
||||
caseNumber: string;
|
||||
block: DecisionBlock;
|
||||
}) {
|
||||
// The endpoint has no FastAPI response model, so `content` may arrive null
|
||||
// for an empty block — coerce to "" to keep `.trim()` / state safe.
|
||||
const content = block.content ?? "";
|
||||
const [editing, setEditing] = useState(false);
|
||||
const [value, setValue] = useState(block.content);
|
||||
const [value, setValue] = useState(content);
|
||||
const [state, setState] = useState<SaveState>({ kind: "idle" });
|
||||
/* The last content known to be persisted — used to skip no-op saves. */
|
||||
const [baseline, setBaseline] = useState(block.content);
|
||||
const [baseline, setBaseline] = useState(content);
|
||||
const save = useSaveBlock(caseNumber);
|
||||
|
||||
/* Re-sync when the upstream query refetches (e.g. after another save) while
|
||||
* not actively editing. Adjusting state during render — the documented React
|
||||
* pattern for derived-from-props — avoids a setState-in-effect cascade. */
|
||||
if (!editing && block.content !== baseline) {
|
||||
setBaseline(block.content);
|
||||
setValue(block.content);
|
||||
if (!editing && content !== baseline) {
|
||||
setBaseline(content);
|
||||
setValue(content);
|
||||
}
|
||||
|
||||
async function handleSave() {
|
||||
@@ -171,8 +174,8 @@ function BlockEditor({
|
||||
if (!editing) {
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
{block.content.trim() ? (
|
||||
<Markdown content={block.content} />
|
||||
{content.trim() ? (
|
||||
<Markdown content={content} />
|
||||
) : (
|
||||
<p className="text-sm text-ink-muted italic">בלוק ריק.</p>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user