Skip to content

Commit a6ded8c

Browse files
authored
Fix: Ensure close all properly closes all open files and handles multiple saved files (#2964)
1 parent d1d2329 commit a6ded8c

File tree

3 files changed

+73
-30
lines changed

3 files changed

+73
-30
lines changed

apps/web/client/src/app/project/[id]/_components/right-panel/code-tab/file-content/index.tsx

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,11 @@ interface CodeEditorAreaProps {
1414
navigationTarget: CodeNavigationTarget | null;
1515
editorViewsRef: RefObject<Map<string, EditorView>>;
1616
onSaveFile: () => Promise<void>;
17+
onSaveAndCloseFiles: () => Promise<void>;
1718
onUpdateFileContent: (fileId: string, content: string) => void;
18-
onDiscardChanges: (fileId: string) => void;
19+
onDiscardChanges: () => void;
1920
onCancelUnsaved: () => void;
21+
fileCountToClose?: number;
2022
}
2123

2224
export const CodeEditorArea = ({
@@ -26,9 +28,11 @@ export const CodeEditorArea = ({
2628
navigationTarget,
2729
editorViewsRef,
2830
onSaveFile,
31+
onSaveAndCloseFiles,
2932
onUpdateFileContent,
3033
onDiscardChanges,
3134
onCancelUnsaved,
35+
fileCountToClose,
3236
}: CodeEditorAreaProps) => {
3337
const [activeFileIsDirty, setActiveFileIsDirty] = useState(false);
3438

@@ -82,9 +86,10 @@ export const CodeEditorArea = ({
8286
</div>
8387
{activeFileIsDirty && showUnsavedDialog && (
8488
<UnsavedChangesDialog
85-
onSave={onSaveFile}
86-
onDiscard={() => onDiscardChanges(activeFile?.path || '')}
89+
onSave={onSaveAndCloseFiles}
90+
onDiscard={onDiscardChanges}
8791
onCancel={() => { onCancelUnsaved(); }}
92+
fileCount={fileCountToClose}
8893
/>
8994
)}
9095
</div>

apps/web/client/src/app/project/[id]/_components/right-panel/code-tab/file-content/unsaved-changes-dialog.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,15 @@ interface UnsavedChangesDialogProps {
44
onSave: () => Promise<void>;
55
onDiscard: () => void;
66
onCancel: () => void;
7+
fileCount?: number;
78
}
89

9-
export function UnsavedChangesDialog({ onSave, onDiscard, onCancel }: UnsavedChangesDialogProps) {
10+
export function UnsavedChangesDialog({ onSave, onDiscard, onCancel, fileCount = 1 }: UnsavedChangesDialogProps) {
11+
const isMultiple = fileCount > 1;
1012
return (
1113
<div className="absolute top-4 left-1/2 z-50 -translate-x-1/2 bg-white dark:bg-zinc-800 border dark:border-zinc-700 shadow-lg rounded-lg p-4 w-[320px]">
1214
<div className="text-sm text-gray-800 dark:text-gray-100 mb-4">
13-
You have unsaved changes. Are you sure you want to close this file?
15+
You have unsaved changes. Are you sure you want to close {isMultiple ? `${fileCount} files` : 'this file'}?
1416
</div>
1517
<div className="flex justify-end gap-1">
1618
<Button

apps/web/client/src/app/project/[id]/_components/right-panel/code-tab/index.tsx

Lines changed: 61 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ export const CodeTab = memo(forwardRef<CodeTabRef, CodeTabProps>(({ projectId, b
6363
const [activeEditorFile, setActiveEditorFile] = useState<EditorFile | null>(null);
6464
const [openedEditorFiles, setOpenedEditorFiles] = useState<EditorFile[]>([]);
6565
const [showLocalUnsavedDialog, setShowLocalUnsavedDialog] = useState(false);
66+
const [filesToClose, setFilesToClose] = useState<string[]>([]);
6667

6768
// This is a workaround to allow code controls to access the hasUnsavedChanges state
6869
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
@@ -185,38 +186,64 @@ export const CodeTab = memo(forwardRef<CodeTabRef, CodeTabProps>(({ projectId, b
185186
}
186187
};
187188

189+
const saveFileWithHash = async (filePath: string, file: EditorFile): Promise<EditorFile> => {
190+
if (!branchData) {
191+
throw new Error('Branch data not found');
192+
}
193+
194+
await branchData.codeEditor.writeFile(filePath, file.content || '');
195+
196+
if (file.type === 'text') {
197+
const newHash = await hashContent(file.content);
198+
return { ...file, originalHash: newHash };
199+
}
200+
201+
return file;
202+
};
203+
188204
const handleSaveFile = async () => {
189205
if (!selectedFilePath || !activeEditorFile) return;
190206
try {
191-
if (!branchData) {
192-
throw new Error('Branch data not found');
193-
}
194-
195-
await branchData.codeEditor.writeFile(selectedFilePath, activeEditorFile.content || '');
207+
const updatedFile = await saveFileWithHash(selectedFilePath, activeEditorFile);
196208

197-
// Update originalHash to mark file as clean after successful save
198-
if (activeEditorFile.type === 'text') {
199-
const newHash = await hashContent(activeEditorFile.content);
200-
const updatedFile = { ...activeEditorFile, originalHash: newHash };
201-
202-
// Update in opened files list
203-
const updatedFiles = openedEditorFiles.map(file =>
204-
pathsEqual(file.path, selectedFilePath) ? updatedFile : file
205-
);
206-
setOpenedEditorFiles(updatedFiles);
207-
setActiveEditorFile(updatedFile);
208-
}
209+
// Update in opened files list
210+
const updatedFiles = openedEditorFiles.map(file =>
211+
pathsEqual(file.path, selectedFilePath) ? updatedFile : file
212+
);
213+
setOpenedEditorFiles(updatedFiles);
214+
setActiveEditorFile(updatedFile);
209215
} catch (error) {
210216
console.error('Failed to save file:', error);
211217
alert(`Failed to save: ${error instanceof Error ? error.message : 'Unknown error'}`);
212218
}
213219
};
214220

221+
const handleSaveAndCloseFiles = async () => {
222+
try {
223+
// Save all files in filesToClose
224+
await Promise.all(filesToClose.map(async (filePath) => {
225+
const fileToSave = openedEditorFiles.find(f => pathsEqual(f.path, filePath));
226+
if (!fileToSave) return;
227+
228+
await saveFileWithHash(filePath, fileToSave);
229+
}));
230+
231+
// Close the files (no need to update hashes since we're closing them)
232+
filesToClose.forEach(filePath => closeFileInternal(filePath));
233+
setFilesToClose([]);
234+
setShowLocalUnsavedDialog(false);
235+
} catch (error) {
236+
console.error('Failed to save files:', error);
237+
alert(`Failed to save: ${error instanceof Error ? error.message : 'Unknown error'}`);
238+
}
239+
};
240+
215241
const closeLocalFile = useCallback((filePath: string) => {
216242
const fileToClose = openedEditorFiles.find(f => pathsEqual(f.path, filePath));
217243
if (fileToClose) {
218244
isDirty(fileToClose).then(dirty => {
219245
if (dirty) {
246+
setFilesToClose([filePath]);
220247
setShowLocalUnsavedDialog(true);
221248
return;
222249
}
@@ -238,6 +265,7 @@ export const CodeTab = memo(forwardRef<CodeTabRef, CodeTabProps>(({ projectId, b
238265
// Check if any dirty files remain
239266
const dirtyFiles = fileStatuses.filter(status => status.dirty);
240267
if (dirtyFiles.length > 0) {
268+
setFilesToClose(dirtyFiles.map(status => status.file.path));
241269
setShowLocalUnsavedDialog(true);
242270
return;
243271
}
@@ -272,22 +300,27 @@ export const CodeTab = memo(forwardRef<CodeTabRef, CodeTabProps>(({ projectId, b
272300
editorViewsRef.current.delete(filePath);
273301
}
274302

275-
const updatedFiles = openedEditorFiles.filter(f => !pathsEqual(f.path, filePath));
276-
setOpenedEditorFiles(updatedFiles);
303+
setOpenedEditorFiles(prev => {
304+
const updatedFiles = prev.filter(f => !pathsEqual(f.path, filePath));
277305

278-
if (activeEditorFile && pathsEqual(activeEditorFile.path, filePath)) {
279-
const newActiveFile = updatedFiles.length > 0 ? updatedFiles[updatedFiles.length - 1] || null : null;
280-
setActiveEditorFile(newActiveFile);
281-
}
306+
// Update active file if we're closing it
307+
if (activeEditorFile && pathsEqual(activeEditorFile.path, filePath)) {
308+
const newActiveFile = updatedFiles.length > 0 ? updatedFiles[updatedFiles.length - 1] || null : null;
309+
setActiveEditorFile(newActiveFile);
310+
}
311+
312+
return updatedFiles;
313+
});
282314

283315
// Clear selected file path if the closed file was selected
284316
if (selectedFilePath && pathsEqual(selectedFilePath, filePath)) {
285317
setSelectedFilePath(null);
286318
}
287319
};
288320

289-
const discardLocalFileChanges = (filePath: string) => {
290-
closeFileInternal(filePath);
321+
const discardLocalFileChanges = () => {
322+
filesToClose.forEach(filePath => closeFileInternal(filePath));
323+
setFilesToClose([]);
291324
setShowLocalUnsavedDialog(false);
292325
};
293326

@@ -396,11 +429,14 @@ export const CodeTab = memo(forwardRef<CodeTabRef, CodeTabProps>(({ projectId, b
396429
showUnsavedDialog={showLocalUnsavedDialog}
397430
navigationTarget={navigationTarget}
398431
onSaveFile={handleSaveFile}
432+
onSaveAndCloseFiles={handleSaveAndCloseFiles}
399433
onUpdateFileContent={updateLocalFileContent}
400434
onDiscardChanges={discardLocalFileChanges}
401435
onCancelUnsaved={() => {
436+
setFilesToClose([]);
402437
setShowLocalUnsavedDialog(false);
403438
}}
439+
fileCountToClose={filesToClose.length}
404440
/>
405441
</div>
406442
</div>

0 commit comments

Comments
 (0)