From d3d3edfbd736fe2327c2d79a05b363360049e51d Mon Sep 17 00:00:00 2001 From: GhaZy Date: Thu, 29 Oct 2026 15:09:29 +0300 Subject: [PATCH 1/2] fix(web): disable transcript editor during pending saves to prevent discarding later typing (#3502) When saving a transcript cue correction, the editor previously remained writable while the request was in flight. Any edits typed during the save were discarded when the earlier request resolved and closed the editor. Additionally, triggering an edit on another cue during a pending save could cause its draft to be cleared. - Disable the active cue textarea while isSaving is true - Hide and disable cue edit actions across other transcript rows while isSaving is true - Guard startEditing, cancelEditing, and handleTranscriptClick against running during pending saves - Capture entryIdToSave in saveEdit and only close the editor if the active entry still matches the saved entry Closes #3502 --- .../[videoId]/_components/tabs/Transcript.tsx | 41 ++++++++++++------- .../_components/tabs/transcript-row.tsx | 3 +- 2 files changed, 29 insertions(+), 15 deletions(-) diff --git a/apps/web/app/s/[videoId]/_components/tabs/Transcript.tsx b/apps/web/app/s/[videoId]/_components/tabs/Transcript.tsx index 5bfb575bc18..88f52a0b46f 100644 --- a/apps/web/app/s/[videoId]/_components/tabs/Transcript.tsx +++ b/apps/web/app/s/[videoId]/_components/tabs/Transcript.tsx @@ -250,52 +250,65 @@ export const Transcript: React.FC = ({ data, onSeek }) => { const handleTranscriptClick = useCallback( (entry: TranscriptEntry) => { - if (editingEntry === entry.id) { + if (isSaving || editingEntry === entry.id) { return; } setSelectedEntry(entry.id); onSeek?.(entry.startTime); }, - [editingEntry, onSeek], + [editingEntry, isSaving, onSeek], ); - const startEditing = useCallback((entry: TranscriptEntry) => { - setEditingEntry(entry.id); - }, []); + const startEditing = useCallback( + (entry: TranscriptEntry) => { + if (isSaving) { + return; + } + setEditingEntry(entry.id); + }, + [isSaving], + ); - const cancelEditing = () => { + const cancelEditing = useCallback(() => { + if (isSaving) { + return; + } setEditingEntry(null); - }; + }, [isSaving]); const saveEdit = async (editText: string) => { const normalizedEditText = normalizeTranscriptCueText(editText); - if (!editingEntry || !normalizedEditText) { + if (!editingEntry || !normalizedEditText || isSaving) { return; } + const entryIdToSave = editingEntry; + setIsSaving(true); try { const result = await editTranscriptEntry( data.id, - editingEntry, + entryIdToSave, normalizedEditText, ); if (result.success) { setTranscriptData((prev) => prev.map((entry) => - entry.id === editingEntry + entry.id === entryIdToSave ? { ...entry, text: normalizedEditText } : entry, ), ); - setEditingEntry(null); + setEditingEntry((current) => + current === entryIdToSave ? null : current, + ); invalidateTranscript(data.id); } else { console.error("[Transcript] Failed to save transcript edit:", { - entryId: editingEntry, + entryId: entryIdToSave, videoId: data.id, errorMessage: result.message, result, @@ -303,7 +316,7 @@ export const Transcript: React.FC = ({ data, onSeek }) => { } } catch (error) { console.error("[Transcript] Error saving transcript edit:", { - entryId: editingEntry, + entryId: entryIdToSave, videoId: data.id, error: error instanceof Error ? error.message : error, stack: error instanceof Error ? error.stack : undefined, @@ -808,7 +821,7 @@ export const Transcript: React.FC = ({ data, onSeek }) => { key={entry.id} entry={entry} selected={selectedEntry === entry.id} - canEdit={canEdit} + canEdit={canEdit && !isSaving} onSelect={handleTranscriptClick} onEdit={startEditing} /> diff --git a/apps/web/app/s/[videoId]/_components/tabs/transcript-row.tsx b/apps/web/app/s/[videoId]/_components/tabs/transcript-row.tsx index c0bb2d4eb47..f044b876966 100644 --- a/apps/web/app/s/[videoId]/_components/tabs/transcript-row.tsx +++ b/apps/web/app/s/[videoId]/_components/tabs/transcript-row.tsx @@ -69,7 +69,8 @@ export function TranscriptEditor({