Files
fusion/packages/dashboard/app/hooks/useAutoSavePreference.ts
gsxdsm 06ec0e606e FN-7866: add auto-save toggle for the workspace file editor (default on)
Adds a shared, persisted auto-save preference for workspace text-file editing, defaulted to on, surfaced as a toolbar toggle in both the Files modal and right-dock Files view.

- Add useAutoSavePreference hook: persists the fn-file-editor-auto-save localStorage preference, broadcasts same-window changes via a custom event (storage events only reach other documents), and defaults to true.
- Extend useWorkspaceFileEditor with an autoSave flag that debounces (800ms) and triggers save() for a loaded, editable file with real pending changes, keyed by workspace+file+content to avoid re-firing on failed writes.
- Add an Auto-save toggle button to FileEditor's toolbar (autoSaveEnabled/onToggleAutoSave/canToggleAutoSave props), hidden for read-only/preview/binary files.
- Wire the shared preference into FileBrowserModal and DockFilesView, disabling auto-save for binary files in the modal.
- Add fileEditor.autoSave / fileEditor.toggleAutoSave i18n strings and document the new default behavior in docs/dashboard-guide.md.
- Add/extend tests covering the new hook, debounced auto-save behavior, and toolbar toggle wiring across FileEditor, FileBrowserModal, and DockFilesView.

Files changed:
 docs/dashboard-guide.md                            |   3 +
 .../dashboard/app/components/DockFilesView.tsx     |   6 +-
 .../dashboard/app/components/FileBrowserModal.tsx  |  20 ++--
 packages/dashboard/app/components/FileEditor.tsx   |  17 +++-
 .../components/__tests__/DockFilesView.test.tsx    |  37 ++++++-
 .../components/__tests__/FileBrowserModal.test.tsx |  86 +++++++++++++---
 .../app/components/__tests__/FileEditor.test.tsx   |  56 +++++++++++
 .../hooks/__tests__/useAutoSavePreference.test.ts  |  66 +++++++++++++
 .../hooks/__tests__/useWorkspaceFileEditor.test.ts | 108 +++++++++++++++++++++
 .../dashboard/app/hooks/useAutoSavePreference.ts   |  79 +++++++++++++++
 .../dashboard/app/hooks/useWorkspaceFileEditor.ts  |  47 ++++++++-
 packages/i18n/locales/en/app.json                  |   2 +
 12 files changed, 500 insertions(+), 27 deletions(-)

Fusion-Task-Id: FN-7866

Fusion-Task-Lineage: 0604de18-666d-4872-abce-2a3886c9ea55

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-12 12:49:37 -07:00

80 lines
3.0 KiB
TypeScript

import { useCallback, useEffect, useState } from "react";
export const FILE_EDITOR_AUTO_SAVE_STORAGE_KEY = "fn-file-editor-auto-save";
const FILE_EDITOR_AUTO_SAVE_CHANGED_EVENT = "fn:file-editor-auto-save-changed";
function readBooleanPref(key: string, defaultValue: boolean): boolean {
if (typeof window === "undefined") return defaultValue;
try {
const raw = window.localStorage.getItem(key);
if (raw === null) return defaultValue;
return raw === "true";
} catch {
return defaultValue;
}
}
function writeBooleanPref(key: string, value: boolean): void {
if (typeof window === "undefined") return;
try {
window.localStorage.setItem(key, value ? "true" : "false");
} catch {
// Ignore storage failures (quota, private mode, etc.).
}
}
export interface UseAutoSavePreferenceReturn {
autoSaveEnabled: boolean;
toggleAutoSave: () => void;
setAutoSaveEnabled: (value: boolean) => void;
}
/*
FNXC:FileEditor 2026-07-12-00:00:
Workspace file-editor auto-save defaults ON and is toggled from the shared toolbar. Persist one preference key for every workspace editor surface, and broadcast same-window changes because the native storage event only reaches other documents.
*/
export function useAutoSavePreference(): UseAutoSavePreferenceReturn {
const [autoSaveEnabled, setAutoSaveEnabledState] = useState(() => readBooleanPref(FILE_EDITOR_AUTO_SAVE_STORAGE_KEY, true));
const setAutoSaveEnabled = useCallback((value: boolean) => {
setAutoSaveEnabledState(value);
writeBooleanPref(FILE_EDITOR_AUTO_SAVE_STORAGE_KEY, value);
if (typeof window !== "undefined") {
window.dispatchEvent(new CustomEvent(FILE_EDITOR_AUTO_SAVE_CHANGED_EVENT, { detail: value }));
}
}, []);
const toggleAutoSave = useCallback(() => {
setAutoSaveEnabledState((current) => {
const next = !current;
writeBooleanPref(FILE_EDITOR_AUTO_SAVE_STORAGE_KEY, next);
if (typeof window !== "undefined") {
window.dispatchEvent(new CustomEvent(FILE_EDITOR_AUTO_SAVE_CHANGED_EVENT, { detail: next }));
}
return next;
});
}, []);
useEffect(() => {
if (typeof window === "undefined") return;
const syncFromStorage = (event: StorageEvent) => {
if (event.key !== FILE_EDITOR_AUTO_SAVE_STORAGE_KEY) return;
setAutoSaveEnabledState(readBooleanPref(FILE_EDITOR_AUTO_SAVE_STORAGE_KEY, true));
};
const syncFromLocalEvent = (event: Event) => {
const nextValue = (event as CustomEvent<boolean>).detail;
setAutoSaveEnabledState(typeof nextValue === "boolean" ? nextValue : readBooleanPref(FILE_EDITOR_AUTO_SAVE_STORAGE_KEY, true));
};
window.addEventListener("storage", syncFromStorage);
window.addEventListener(FILE_EDITOR_AUTO_SAVE_CHANGED_EVENT, syncFromLocalEvent);
return () => {
window.removeEventListener("storage", syncFromStorage);
window.removeEventListener(FILE_EDITOR_AUTO_SAVE_CHANGED_EVENT, syncFromLocalEvent);
};
}, []);
return { autoSaveEnabled, toggleAutoSave, setAutoSaveEnabled };
}