Create useMigration helper

This commit is contained in:
Zoe Roux 2025-06-18 12:05:10 +02:00
parent 36abadc2cc
commit 3a9cb262f8
No known key found for this signature in database
4 changed files with 71 additions and 41 deletions

View File

@ -23,7 +23,6 @@ import Refresh from "@material-symbols/svg-400/rounded/autorenew.svg";
import Info from "@material-symbols/svg-400/rounded/info.svg"; import Info from "@material-symbols/svg-400/rounded/info.svg";
import MoreVert from "@material-symbols/svg-400/rounded/more_vert.svg"; import MoreVert from "@material-symbols/svg-400/rounded/more_vert.svg";
import MovieInfo from "@material-symbols/svg-400/rounded/movie_info.svg"; import MovieInfo from "@material-symbols/svg-400/rounded/movie_info.svg";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import type { ComponentProps } from "react"; import type { ComponentProps } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Platform } from "react-native"; import { Platform } from "react-native";
@ -31,7 +30,7 @@ import { useYoshiki } from "yoshiki/native";
import { WatchStatusV } from "~/models"; import { WatchStatusV } from "~/models";
import { HR, IconButton, Menu, tooltip } from "~/primitives"; import { HR, IconButton, Menu, tooltip } from "~/primitives";
import { useAccount } from "~/providers/account-context"; import { useAccount } from "~/providers/account-context";
import { queryFn } from "~/query"; import { useMutation } from "~/query";
// import { useDownloader } from "../../packages/ui/src/downloadses/ui/src/downloads"; // import { useDownloader } from "../../packages/ui/src/downloadses/ui/src/downloads";
export const EpisodesContext = ({ export const EpisodesContext = ({
@ -53,22 +52,19 @@ export const EpisodesContext = ({
const { css } = useYoshiki(); const { css } = useYoshiki();
const { t } = useTranslation(); const { t } = useTranslation();
const queryClient = useQueryClient();
const mutation = useMutation({ const mutation = useMutation({
mutationFn: (newStatus: WatchStatusV | null) => path: [type, slug, "watchStatus"],
queryFn({ compute: (newStatus: WatchStatusV | null) => ({
path: [type, slug, "watchStatus", newStatus && `?status=${newStatus}`], method: newStatus ? "POST" : "DELETE",
method: newStatus ? "POST" : "DELETE", params: newStatus ? { status: newStatus } : undefined,
}), }),
onSettled: async () => await queryClient.invalidateQueries({ queryKey: [type, slug] }), invalidate: [type, slug],
}); });
const metadataRefreshMutation = useMutation({ const metadataRefreshMutation = useMutation({
mutationFn: () => method: "POST",
queryFn({ path: [type, slug, "refresh"],
path: [type, slug, "refresh"], invalidate: null,
method: "POST",
}),
}); });
return ( return (

View File

@ -1,10 +1,10 @@
import { createContext, useContext } from "react"; import { createContext, useContext } from "react";
import { ServerInfoP, type Account, type Token } from "~/models"; import { type Account, ServerInfoP, type Token } from "~/models";
import { useFetch } from "~/query"; import { useFetch } from "~/query";
export const AccountContext = createContext<{ export const AccountContext = createContext<{
apiUrl: string; apiUrl: string;
authToken: Token | null; authToken: string | null; //Token | null;
selectedAccount: Account | null; selectedAccount: Account | null;
accounts: (Account & { select: () => void; remove: () => void })[]; accounts: (Account & { select: () => void; remove: () => void })[];
}>({ apiUrl: "api", authToken: null, selectedAccount: null, accounts: [] }); }>({ apiUrl: "api", authToken: null, selectedAccount: null, accounts: [] });

View File

@ -1,4 +1,11 @@
import { QueryClient, dehydrate, useInfiniteQuery, useQuery } from "@tanstack/react-query"; import {
QueryClient,
dehydrate,
useInfiniteQuery,
useQuery,
useQueryClient,
useMutation as useRQMutation,
} from "@tanstack/react-query";
import { useContext } from "react"; import { useContext } from "react";
import { Platform } from "react-native"; import { Platform } from "react-native";
import type { z } from "zod"; import type { z } from "zod";
@ -14,7 +21,7 @@ const cleanSlash = (str: string | null, keepFirst = false) => {
return str.replace(/^\/|\/$/g, ""); return str.replace(/^\/|\/$/g, "");
}; };
export const queryFn = async <Parser extends z.ZodTypeAny>(context: { const queryFn = async <Parser extends z.ZodTypeAny>(context: {
method?: "GET" | "POST" | "PUT" | "PATCH" | "DELETE"; method?: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
url: string; url: string;
body?: object; body?: object;
@ -22,7 +29,7 @@ export const queryFn = async <Parser extends z.ZodTypeAny>(context: {
plainText?: boolean; plainText?: boolean;
authToken: string | null; authToken: string | null;
parser?: Parser; parser?: Parser;
signal: AbortSignal; signal?: AbortSignal;
}): Promise<z.infer<Parser>> => { }): Promise<z.infer<Parser>> => {
if (Platform.OS === "web" && typeof window === "undefined" && context.url.startsWith("/api")) if (Platform.OS === "web" && typeof window === "undefined" && context.url.startsWith("/api"))
context.url = `${ssrApiUrl}/${context.url.substring(4)}`; context.url = `${ssrApiUrl}/${context.url.substring(4)}`;
@ -88,13 +95,6 @@ export const queryFn = async <Parser extends z.ZodTypeAny>(context: {
return parsed.data; return parsed.data;
}; };
export type MutationParam = {
params?: Record<string, number | string>;
body?: object;
path: string[];
method: "POST" | "DELETE";
};
export const createQueryClient = () => export const createQueryClient = () =>
new QueryClient({ new QueryClient({
defaultOptions: { defaultOptions: {
@ -105,15 +105,6 @@ export const createQueryClient = () =>
refetchOnReconnect: false, refetchOnReconnect: false,
retry: false, retry: false,
}, },
mutations: {
// mutationFn: (({ method, path, body, params }: MutationParam) => {
// return queryFn({
// method,
// url: keyToUrl(toQueryKey({ path, params })),
// body,
// });
// }) as any,
},
}, },
}); });
@ -130,7 +121,7 @@ export type QueryIdentifier<T = unknown, Ret = T> = {
}; };
}; };
export const toQueryKey = (query: { const toQueryKey = (query: {
apiUrl: string; apiUrl: string;
path: (string | undefined)[]; path: (string | undefined)[];
params?: { [query: string]: boolean | number | string | string[] | undefined }; params?: { [query: string]: boolean | number | string | string[] | undefined };
@ -162,7 +153,7 @@ export const useFetch = <Data,>(query: QueryIdentifier<Data>) => {
url: keyToUrl(key), url: keyToUrl(key),
parser: query.parser, parser: query.parser,
signal: ctx.signal, signal: ctx.signal,
authToken: authToken?.access_token ?? null, authToken: authToken ?? null,
...query.options, ...query.options,
}), }),
placeholderData: query.placeholderData as any, placeholderData: query.placeholderData as any,
@ -181,7 +172,7 @@ export const useInfiniteFetch = <Data, Ret>(query: QueryIdentifier<Data, Ret>) =
url: (ctx.pageParam as string) ?? keyToUrl(key), url: (ctx.pageParam as string) ?? keyToUrl(key),
parser: Paged(query.parser), parser: Paged(query.parser),
signal: ctx.signal, signal: ctx.signal,
authToken: authToken?.access_token ?? null, authToken: authToken ?? null,
...query.options, ...query.options,
}), }),
getNextPageParam: (page: Page<Data>) => page?.next || undefined, getNextPageParam: (page: Page<Data>) => page?.next || undefined,
@ -221,7 +212,7 @@ export const prefetch = async (...queries: QueryIdentifier[]) => {
url: keyToUrl(key), url: keyToUrl(key),
parser: Paged(query.parser), parser: Paged(query.parser),
signal: ctx.signal, signal: ctx.signal,
authToken: authToken?.access_token ?? null, authToken: authToken ?? null,
...query.options, ...query.options,
}), }),
initialPageParam: undefined, initialPageParam: undefined,
@ -234,7 +225,7 @@ export const prefetch = async (...queries: QueryIdentifier[]) => {
url: keyToUrl(key), url: keyToUrl(key),
parser: query.parser, parser: query.parser,
signal: ctx.signal, signal: ctx.signal,
authToken: authToken?.access_token ?? null, authToken: authToken ?? null,
...query.options, ...query.options,
}), }),
}); });
@ -243,3 +234,46 @@ export const prefetch = async (...queries: QueryIdentifier[]) => {
setServerData("queryState", dehydrate(client)); setServerData("queryState", dehydrate(client));
return client; return client;
}; };
type MutationParams = {
method?: "POST" | "PUT" | "DELETE";
path?: string[];
params?: { [query: string]: boolean | number | string | string[] | undefined };
body?: object;
};
export const useMutation = <T = void,>({
compute,
invalidate,
...queryParams
}: MutationParams & {
compute?: (param: T) => MutationParams;
invalidate: string[] | null;
}) => {
const { apiUrl, authToken } = useContext(AccountContext);
const queryClient = useQueryClient();
const mutation = useRQMutation({
mutationFn: (param: T) => {
const { method, path, params, body } = {
...queryParams,
...compute?.(param),
} as Required<MutationParams>;
return queryFn({
method,
url: keyToUrl(toQueryKey({ apiUrl, path, params })),
body,
authToken,
});
},
onSuccess: invalidate
? async () =>
await queryClient.invalidateQueries({
queryKey: toQueryKey({ apiUrl, path: invalidate }),
})
: undefined,
// TODO: Do something
// onError: () => {}
});
return mutation;
};

View File

@ -3,7 +3,7 @@ import { useTranslation } from "react-i18next";
import { View } from "react-native"; import { View } from "react-native";
import { useYoshiki } from "yoshiki/native"; import { useYoshiki } from "yoshiki/native";
import { Button, Icon, Link, P, ts } from "~/primitives"; import { Button, Icon, Link, P, ts } from "~/primitives";
import { useAccount } from "~/providers/account-provider"; import { useAccount } from "~/providers/account-context";
export const Unauthorized = ({ missing }: { missing: string[] }) => { export const Unauthorized = ({ missing }: { missing: string[] }) => {
const { t } = useTranslation(); const { t } = useTranslation();