The prop names were backwards. `canEdit` meant "may delete" and `canWrite` meant "may rewrite the text" — which is the opposite of how either word reads, on a component where the distinction is the entire permission model. They are now canDelete and canRewrite. An open edit draft never re-synced with its entry, so a refetch after someone else's edit left a stale draft and a Cancel that restored the wrong text. It now resyncs when the entry changes underneath, but only while not editing, so a background refetch can't clobber what's being typed — the version guard is what catches a genuine collision. A 409 from an edit had no handling, so the component kept its stale version and every retry conflicted again: a button that just doesn't work. A conflict now invalidates, so the fresh version arrives. Saving an empty edit returned silently, which also reads as a broken button. It says why nothing happened, and points at Delete if that's what was meant. A failed delete's error now clears on retry rather than making a second attempt look like a second failure. formatObservedAt let out-of-range parts roll over — 2026-13-45 rendering as February 2027, confidently wrong. It shows the raw string instead. Also: the scope select guards against a non-numeric value becoming a NaN objectId and a cryptic 400; the journal total is memoized rather than spread-and-reduced every render; EditorRail's doc comment no longer calls the journal "later" now that it's here; and a static class list stopped being split across two cn() arguments for no reason. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01H3zbym8Doka2d7D48maSgZ
This commit is contained in:
+10
-2
@@ -6,7 +6,7 @@
|
||||
|
||||
import { useInfiniteQuery, useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import { z } from 'zod'
|
||||
import { api } from './api'
|
||||
import { ApiError, api } from './api'
|
||||
|
||||
export const journalEntrySchema = z.object({
|
||||
id: z.number(),
|
||||
@@ -108,6 +108,12 @@ export function useUpdateJournalEntry(gardenId: number) {
|
||||
return journalEntrySchema.parse(await api.patch(`/journal/${id}`, rest))
|
||||
},
|
||||
onSuccess: () => invalidateJournal(qc, gardenId),
|
||||
onError: (err) => {
|
||||
// A 409 means the row moved on. Refetch so the component receives the
|
||||
// current version — otherwise a retry resends the stale one and conflicts
|
||||
// forever, which reads as a button that simply doesn't work.
|
||||
if (err instanceof ApiError && err.isConflict) invalidateJournal(qc, gardenId)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -133,6 +139,8 @@ export function today(): string {
|
||||
* through a Date (which would shift it by the timezone offset). */
|
||||
export function formatObservedAt(iso: string): string {
|
||||
const [y, m, d] = iso.split('-').map(Number)
|
||||
if (!y || !m || !d) return iso
|
||||
// Out-of-range parts would silently roll over — 2026-13-45 becoming February
|
||||
// 2027 — so show the raw string instead of a confidently wrong date.
|
||||
if (!y || !m || !d || m < 1 || m > 12 || d < 1 || d > 31) return iso
|
||||
return new Date(y, m - 1, d).toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' })
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user