mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-06-05 20:54:14 +00:00
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:
parent
dff509b394
commit
bbb7af65f6
4 changed files with 73 additions and 71 deletions
67
frontend/src/api/queries/useAllSettings.ts
Normal file
67
frontend/src/api/queries/useAllSettings.ts
Normal 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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
@ -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('');
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue