- Fix React hooks violations: ref updates during render (useDraftPersistence, useChipDraftPersistence, useAttachments), setState in effects across 15+ components, useCallback self-reference TDZ in useResizableColumns - Fix TypeScript lint: remove unnecessary type assertions, replace inline import() annotations with direct imports, remove unused variables/imports - Fix SonarJS issues: prefer-regexp-exec, slow-regex in SubagentResolver, no-misleading-array-reverse in TeamProvisioningService, use-type-alias in ClaudeLogsSection, variable shadowing in ChangeExtractorService - Fix accessibility: associate labels with controls in filter popovers - Fix template expression safety: wrap unknown errors with String() - Fix flaky FileWatcher test: floor instanceCreatedAt to second granularity to match filesystem birthtimeMs resolution on Linux - Replace TODO comments with NOTE where features are intentionally disabled - Remove unused leadContextByTeam from TeamDetailView store selector 62 files changed across main process, renderer, shared types, and hooks. All 1646 tests pass, typecheck clean, 0 lint errors.
140 lines
3.7 KiB
TypeScript
140 lines
3.7 KiB
TypeScript
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
|
|
import { draftStorage } from '@renderer/services/draftStorage';
|
|
|
|
interface UseDraftPersistenceOptions {
|
|
key: string;
|
|
initialValue?: string;
|
|
enabled?: boolean;
|
|
debounceMs?: number;
|
|
}
|
|
|
|
interface UseDraftPersistenceResult {
|
|
value: string;
|
|
setValue: (v: string) => void;
|
|
isSaved: boolean;
|
|
clearDraft: () => void;
|
|
}
|
|
|
|
export function useDraftPersistence({
|
|
key,
|
|
initialValue,
|
|
enabled = true,
|
|
debounceMs = 500,
|
|
}: UseDraftPersistenceOptions): UseDraftPersistenceResult {
|
|
const [value, setValueState] = useState(initialValue ?? '');
|
|
const [isSaved, setIsSaved] = useState(false);
|
|
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
const pendingValueRef = useRef<{ key: string; value: string } | null>(null);
|
|
const keyRef = useRef(key);
|
|
const mountedRef = useRef(true);
|
|
|
|
useEffect(() => {
|
|
keyRef.current = key;
|
|
}, [key]);
|
|
|
|
useEffect(() => {
|
|
mountedRef.current = true;
|
|
return () => {
|
|
mountedRef.current = false;
|
|
};
|
|
}, []);
|
|
|
|
const flushPending = useCallback(() => {
|
|
if (timerRef.current != null) {
|
|
clearTimeout(timerRef.current);
|
|
timerRef.current = null;
|
|
}
|
|
if (pendingValueRef.current != null) {
|
|
const pending = pendingValueRef.current;
|
|
pendingValueRef.current = null;
|
|
if (pending.value.length === 0) {
|
|
void draftStorage.deleteDraft(pending.key);
|
|
} else {
|
|
void draftStorage.saveDraft(pending.key, pending.value);
|
|
}
|
|
}
|
|
}, []);
|
|
|
|
// Load draft on mount / key change
|
|
useEffect(() => {
|
|
let cancelled = false;
|
|
// Prevent debounced saves for the previous key from landing under the new key.
|
|
flushPending();
|
|
// Reset local state for the new key immediately. If a draft exists, it will overwrite below.
|
|
// eslint-disable-next-line react-hooks/set-state-in-effect -- intentional reset on key change before async load
|
|
setValueState(initialValue ?? '');
|
|
|
|
setIsSaved(false);
|
|
|
|
if (!enabled)
|
|
return () => {
|
|
cancelled = true;
|
|
};
|
|
void (async () => {
|
|
const draft = await draftStorage.loadDraft(key);
|
|
if (cancelled) return;
|
|
if (draft != null && initialValue == null) {
|
|
setValueState(draft);
|
|
setIsSaved(true);
|
|
}
|
|
})();
|
|
|
|
return () => {
|
|
cancelled = true;
|
|
};
|
|
}, [key, enabled, initialValue, flushPending]);
|
|
|
|
// Flush on unmount
|
|
useEffect(() => {
|
|
return () => {
|
|
flushPending();
|
|
};
|
|
}, [flushPending]);
|
|
|
|
const setValue = useCallback(
|
|
(v: string) => {
|
|
setValueState(v);
|
|
setIsSaved(false);
|
|
|
|
if (!enabled) return;
|
|
|
|
pendingValueRef.current = { key: keyRef.current, value: v };
|
|
|
|
if (timerRef.current != null) {
|
|
clearTimeout(timerRef.current);
|
|
}
|
|
|
|
timerRef.current = setTimeout(() => {
|
|
timerRef.current = null;
|
|
const pending = pendingValueRef.current;
|
|
pendingValueRef.current = null;
|
|
if (pending == null) return;
|
|
|
|
if (pending.value.length === 0) {
|
|
void draftStorage.deleteDraft(pending.key);
|
|
} else {
|
|
void draftStorage.saveDraft(pending.key, pending.value).then(() => {
|
|
if (mountedRef.current) setIsSaved(true);
|
|
});
|
|
}
|
|
}, debounceMs);
|
|
},
|
|
[enabled, debounceMs]
|
|
);
|
|
|
|
const clearDraft = useCallback(() => {
|
|
if (timerRef.current != null) {
|
|
clearTimeout(timerRef.current);
|
|
timerRef.current = null;
|
|
}
|
|
pendingValueRef.current = null;
|
|
setValueState('');
|
|
setIsSaved(false);
|
|
if (enabled) {
|
|
void draftStorage.deleteDraft(keyRef.current);
|
|
}
|
|
}, [enabled]);
|
|
|
|
return { value, setValue, isSaved, clearDraft };
|
|
}
|