feat(frontend): migrate useAllSetting to TanStack Query

Replaces the hand-rolled fetch + dirty-tracking hook with useAllSettings
backed by useQuery + useMutation. The draft (current edits) is kept in
local state and reset whenever query.data lands. saveAll posts the
draft via a mutation; on success, invalidating ['settings'] refetches
and the useEffect resets the draft so saveDisabled flips back to true.

staleTime: Infinity prevents refetchOnWindowFocus from clobbering
in-flight edits — settings only change in response to this user's own
save.

setSpinning stays as a pass-through to a local flag so the existing
restartPanel flow in SettingsPage keeps showing its spinner.
This commit is contained in:
MHSanaei 2026-05-24 18:53:05 +02:00
parent dff509b394
commit bbb7af65f6
No known key found for this signature in database
GPG key ID: 7E4060F2FBE5AB7A
4 changed files with 73 additions and 71 deletions

View file

@ -0,0 +1,67 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { HttpUtil } from '@/utils';
import { AllSetting } from '@/models/setting';
import { keys } from '@/api/queryKeys';
interface ApiMsg<T = unknown> {
success?: boolean;
obj?: T;
msg?: string;
}
async function fetchAllSetting(): Promise<unknown> {
const msg = await HttpUtil.post('/panel/setting/all', undefined, { silent: true }) as ApiMsg;
if (!msg?.success) throw new Error(msg?.msg || 'Failed to fetch settings');
return msg.obj;
}
export function useAllSettings() {
const queryClient = useQueryClient();
const [draft, setDraft] = useState<AllSetting>(() => new AllSetting());
const [extraSpinning, setExtraSpinning] = useState(false);
const query = useQuery({
queryKey: keys.settings.all(),
queryFn: fetchAllSetting,
staleTime: Infinity,
});
const server = useMemo(() => new AllSetting(query.data), [query.data]);
useEffect(() => {
if (query.data !== undefined) {
setDraft(new AllSetting(query.data));
}
}, [query.data]);
const updateSetting = useCallback((patch: Partial<AllSetting>) => {
setDraft((prev) => {
const next = new AllSetting(prev);
Object.assign(next, patch);
return next;
});
}, []);
const saveMut = useMutation({
mutationFn: async (next: AllSetting) =>
HttpUtil.post('/panel/setting/update', next) as Promise<ApiMsg>,
onSuccess: (msg) => {
if (msg?.success) queryClient.invalidateQueries({ queryKey: keys.settings.all() });
},
});
const saveAll = useCallback(() => saveMut.mutateAsync(draft), [saveMut, draft]);
const saveDisabled = useMemo(() => server.equals(draft), [server, draft]);
return {
allSetting: draft,
updateSetting,
fetched: query.data !== undefined,
spinning: extraSpinning || saveMut.isPending,
setSpinning: setExtraSpinning,
saveDisabled,
saveAll,
};
}

View file

@ -6,4 +6,8 @@ export const keys = {
root: () => ['nodes'] as const, root: () => ['nodes'] as const,
list: () => ['nodes', 'list'] as const, list: () => ['nodes', 'list'] as const,
}, },
settings: {
root: () => ['settings'] as const,
all: () => ['settings', 'all'] as const,
},
} as const; } as const;

View file

@ -1,69 +0,0 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { HttpUtil } from '@/utils';
import { AllSetting } from '@/models/setting';
interface ApiMsg<T = unknown> {
success?: boolean;
obj?: T;
}
export function useAllSetting() {
const [allSetting, setAllSetting] = useState<AllSetting>(() => new AllSetting());
const [oldAllSetting, setOldAllSetting] = useState<AllSetting>(() => new AllSetting());
const [fetched, setFetched] = useState(false);
const [spinning, setSpinning] = useState(false);
const fetchedRef = useRef(false);
const applyServerState = useCallback((obj: unknown) => {
setAllSetting(new AllSetting(obj));
setOldAllSetting(new AllSetting(obj));
}, []);
const fetchAll = useCallback(async () => {
const msg = await HttpUtil.post('/panel/setting/all') as ApiMsg;
if (msg?.success) {
applyServerState(msg.obj);
fetchedRef.current = true;
setFetched(true);
}
}, [applyServerState]);
const saveAll = useCallback(async () => {
setSpinning(true);
try {
const msg = await HttpUtil.post('/panel/setting/update', allSetting) as ApiMsg;
if (msg?.success) await fetchAll();
} finally {
setSpinning(false);
}
}, [allSetting, fetchAll]);
const updateSetting = useCallback((patch: Partial<AllSetting>) => {
setAllSetting((prev) => {
const next = new AllSetting(prev);
Object.assign(next, patch);
return next;
});
}, []);
const saveDisabled = useMemo(
() => allSetting.equals(oldAllSetting),
[allSetting, oldAllSetting],
);
useEffect(() => {
fetchAll();
}, [fetchAll]);
return {
allSetting,
updateSetting,
fetched,
spinning,
setSpinning,
saveDisabled,
fetchAll,
saveAll,
};
}

View file

@ -28,7 +28,7 @@ import { HttpUtil, PromiseUtil } from '@/utils';
import { setMessageInstance } from '@/utils/messageBus'; import { setMessageInstance } from '@/utils/messageBus';
import { useTheme } from '@/hooks/useTheme'; import { useTheme } from '@/hooks/useTheme';
import { useMediaQuery } from '@/hooks/useMediaQuery'; import { useMediaQuery } from '@/hooks/useMediaQuery';
import { useAllSetting } from '@/hooks/useAllSetting'; import { useAllSettings } from '@/api/queries/useAllSettings';
import AppSidebar from '@/components/AppSidebar'; import AppSidebar from '@/components/AppSidebar';
import GeneralTab from './GeneralTab'; import GeneralTab from './GeneralTab';
import SecurityTab from './SecurityTab'; import SecurityTab from './SecurityTab';
@ -92,7 +92,7 @@ export default function SettingsPage() {
setSpinning, setSpinning,
saveDisabled, saveDisabled,
saveAll, saveAll,
} = useAllSetting(); } = useAllSettings();
const [entryHost, setEntryHost] = useState(''); const [entryHost, setEntryHost] = useState('');
const [entryPort, setEntryPort] = useState(''); const [entryPort, setEntryPort] = useState('');