Files
3x-ui/frontend/src/api/queries/useAllSettings.ts
dawn b73ceae081 fix(frontend): refresh subscription settings after save (#6287)
The derived defaults query is cached indefinitely, so subscription links kept using the old path after settings saves. Invalidate it only after successful saves so inbounds and clients refetch generated subscription URLs.
2026-08-22 20:11:51 +02:00

109 lines
3.3 KiB
TypeScript

import { useCallback, useMemo, useState } from 'react';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { HttpUtil, Msg } from '@/utils';
import { parseMsg } from '@/utils/zodValidate';
import { AllSetting } from '@/models/setting';
import {
AllSettingResponseSchema,
AllSettingSchema,
type AllSettingInput,
} from '@/schemas/setting';
import { keys } from '@/api/queryKeys';
import { useServerDraft } from '@/hooks/useServerDraft';
type SettingSavePayload = Partial<AllSetting> & Record<string, unknown>;
type SettingSaveResult = {
msg: Msg<unknown>;
saved?: AllSetting;
};
async function fetchAllSetting(): Promise<AllSettingInput | null> {
const msg = await HttpUtil.post('/panel/api/setting/all', undefined, { silent: true });
if (!msg?.success) throw new Error(msg?.msg || 'Failed to fetch settings');
const validated = parseMsg(msg, AllSettingResponseSchema, 'setting/all');
return validated.obj;
}
export function useAllSettings() {
const queryClient = useQueryClient();
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]);
const { draft, setDraft, isDirty, markSaved } = useServerDraft(
query.data === undefined ? undefined : server,
(setting) => new AllSetting(setting),
(left, right) => left.equals(right),
);
const allSetting = draft ?? server;
const updateSetting = useCallback(
(patch: Partial<AllSetting>) => {
setDraft((prev) => {
const next = new AllSetting(prev ?? server);
Object.assign(next, patch);
return next;
});
},
[server, setDraft],
);
const saveMut = useMutation({
mutationFn: async ({
payload,
saved,
}: {
payload: SettingSavePayload;
saved?: AllSetting;
}): Promise<SettingSaveResult> => {
const next = { ...payload };
const body = AllSettingSchema.partial().safeParse(next);
if (!body.success) {
console.warn('[zod] setting/update body failed validation', body.error.issues);
}
const msg = await HttpUtil.post(
'/panel/api/setting/update',
body.success ? { ...next, ...body.data } : next,
);
return { msg, saved };
},
onSuccess: ({ msg, saved }) => {
if (!msg?.success) return;
if (saved) markSaved(saved);
queryClient.invalidateQueries({ queryKey: keys.settings.all() });
queryClient.invalidateQueries({ queryKey: keys.settings.defaults() });
},
});
const saveAll = useCallback(async () => {
const saved = new AllSetting(allSetting);
return (await saveMut.mutateAsync({ payload: { ...saved }, saved })).msg;
}, [allSetting, saveMut]);
const savePayload = useCallback(
async (payload: SettingSavePayload) => {
const saved = new AllSetting(allSetting);
Object.assign(saved, payload);
return (await saveMut.mutateAsync({ payload, saved })).msg;
},
[allSetting, saveMut],
);
const saveDisabled = !isDirty;
return {
allSetting,
updateSetting,
fetched: query.data !== undefined,
spinning: extraSpinning || saveMut.isPending,
setSpinning: setExtraSpinning,
saveDisabled,
saveAll,
savePayload,
};
}