feat(tickets): persistance de l'édition d'un ticket côté frontend
Ticket #9 — l'édition d'un ticket (titre, corps, champs) est désormais persistée depuis le détail : useTicketDetail porte l'état d'édition et le flux de sauvegarde, TicketDetail expose l'UI d'édition/validation. Tests Vitest tickets verts (13/13), typecheck et build OK. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -23,6 +23,14 @@ export interface TicketDetailViewModel {
|
||||
/** Set when the last write lost an optimistic-concurrency race (F3). */
|
||||
conflict: boolean;
|
||||
busy: boolean;
|
||||
/**
|
||||
* Monotonic counter bumped **only** when the ticket is (re)loaded from the
|
||||
* gateway — initial load, an external-event refresh, or a conflict reload. It
|
||||
* is deliberately **not** bumped by an ordinary optimistic mutation (e.g. a
|
||||
* priority change), so a consumer can re-seed its local edit draft on genuine
|
||||
* reloads without clobbering an in-progress edit on every version bump (#9).
|
||||
*/
|
||||
reloadCount: number;
|
||||
refresh: () => Promise<void>;
|
||||
updateFields: (input: {
|
||||
title?: string;
|
||||
@ -62,6 +70,7 @@ export function useTicketDetail(
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [conflict, setConflict] = useState(false);
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [reloadCount, setReloadCount] = useState(0);
|
||||
|
||||
const refresh = useCallback(async () => {
|
||||
setBusy(true);
|
||||
@ -69,6 +78,11 @@ export function useTicketDetail(
|
||||
try {
|
||||
setTicket(await gateway.read(projectId, ref, true));
|
||||
setConflict(false);
|
||||
// Signal a benign (re)load so consumers can refresh their edit draft; an
|
||||
// optimistic mutation below never bumps this (#9). A benign reload leaves
|
||||
// `conflict` false, so the draft is preserved field-by-field; a conflict
|
||||
// reload (see `run`) keeps `conflict` true to force a clean re-apply.
|
||||
setReloadCount((c) => c + 1);
|
||||
} catch (e) {
|
||||
setError(describe(e));
|
||||
} finally {
|
||||
@ -119,11 +133,19 @@ export function useTicketDetail(
|
||||
return true;
|
||||
} catch (e) {
|
||||
if (isVersionConflict(e)) {
|
||||
setConflict(true);
|
||||
// Conflict reload: reload the fresh ticket but keep `conflict` true so
|
||||
// the consumer force-re-seeds the draft (re-apply cleanly) rather than
|
||||
// preserving the now-stale edit. Bump `reloadCount` to trigger it.
|
||||
setError(
|
||||
"This ticket was modified elsewhere. It has been reloaded — re-apply your change.",
|
||||
);
|
||||
await refresh();
|
||||
try {
|
||||
setTicket(await gateway.read(projectId, ref, true));
|
||||
} catch (reloadErr) {
|
||||
setError(describe(reloadErr));
|
||||
}
|
||||
setConflict(true);
|
||||
setReloadCount((c) => c + 1);
|
||||
} else {
|
||||
setError(describe(e));
|
||||
}
|
||||
@ -132,7 +154,7 @@ export function useTicketDetail(
|
||||
setBusy(false);
|
||||
}
|
||||
},
|
||||
[ticket, gateway, refresh],
|
||||
[ticket, gateway, projectId, ref],
|
||||
);
|
||||
|
||||
const updateFields: TicketDetailViewModel["updateFields"] = useCallback(
|
||||
@ -169,6 +191,7 @@ export function useTicketDetail(
|
||||
error,
|
||||
conflict,
|
||||
busy,
|
||||
reloadCount,
|
||||
refresh,
|
||||
updateFields,
|
||||
saveCarnet,
|
||||
|
||||
Reference in New Issue
Block a user