diff options
| author | Mohamed Bassem <me@mbassem.com> | 2026-02-01 12:29:54 +0000 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-02-01 12:29:54 +0000 |
| commit | 65f6e83f11c82b0ec762e11f3392a80e614ee69a (patch) | |
| tree | 945d8d73122f07fe6a77c2bd3ac9db566939ba3b | |
| parent | e516a525bca6f319a2f003e9677624e968b277bf (diff) | |
| download | karakeep-65f6e83f11c82b0ec762e11f3392a80e614ee69a.tar.zst | |
refactor: migrate trpc to the new react query integration mode (#2438)
* refactor: migrate trpc to the new react query integration mode
* more fixes
* more migrations
* upgrade trpc client
112 files changed, 2527 insertions, 1862 deletions
diff --git a/apps/browser-extension/package.json b/apps/browser-extension/package.json index d2a699c3..575bb8b7 100644 --- a/apps/browser-extension/package.json +++ b/apps/browser-extension/package.json @@ -24,9 +24,9 @@ "@tanstack/query-async-storage-persister": "5.90.2", "@tanstack/react-query": "5.90.2", "@tanstack/react-query-persist-client": "5.90.2", - "@trpc/client": "^11.4.3", - "@trpc/react-query": "^11.4.3", - "@trpc/server": "^11.4.3", + "@trpc/client": "^11.9.0", + "@trpc/server": "^11.9.0", + "@trpc/tanstack-react-query": "^11.9.0", "@uidotdev/usehooks": "^2.4.1", "class-variance-authority": "^0.7.0", "clsx": "^2.1.0", diff --git a/apps/browser-extension/src/OptionsPage.tsx b/apps/browser-extension/src/OptionsPage.tsx index cac32eff..1b1dc8b6 100644 --- a/apps/browser-extension/src/OptionsPage.tsx +++ b/apps/browser-extension/src/OptionsPage.tsx @@ -1,4 +1,5 @@ import React, { useEffect } from "react"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { useNavigate } from "react-router-dom"; import { Button } from "./components/ui/button"; @@ -17,27 +18,32 @@ import usePluginSettings, { DEFAULT_BADGE_CACHE_EXPIRE_MS, } from "./utils/settings"; import { useTheme } from "./utils/ThemeProvider"; -import { api } from "./utils/trpc"; +import { useTRPC } from "./utils/trpc"; export default function OptionsPage() { + const api = useTRPC(); + const queryClient = useQueryClient(); const navigate = useNavigate(); const { settings, setSettings } = usePluginSettings(); const { setTheme, theme } = useTheme(); - const { data: whoami, error: whoAmIError } = api.users.whoami.useQuery( - undefined, - { + const { data: whoami, error: whoAmIError } = useQuery( + api.users.whoami.queryOptions(undefined, { enabled: settings.address != "", - }, + }), ); - const { mutate: deleteKey } = api.apiKeys.revoke.useMutation(); + const { mutate: deleteKey } = useMutation( + api.apiKeys.revoke.mutationOptions(), + ); - const invalidateWhoami = api.useUtils().users.whoami.refetch; + const invalidateWhoami = () => { + queryClient.refetchQueries(api.users.whoami.queryFilter()); + }; useEffect(() => { invalidateWhoami(); - }, [settings, invalidateWhoami]); + }, [settings]); let loggedInMessage: React.ReactNode; if (whoAmIError) { diff --git a/apps/browser-extension/src/SavePage.tsx b/apps/browser-extension/src/SavePage.tsx index b4b9ce95..5f55e164 100644 --- a/apps/browser-extension/src/SavePage.tsx +++ b/apps/browser-extension/src/SavePage.tsx @@ -1,4 +1,5 @@ import { useEffect, useState } from "react"; +import { useMutation } from "@tanstack/react-query"; import { Navigate } from "react-router-dom"; import { @@ -9,33 +10,36 @@ import { import { NEW_BOOKMARK_REQUEST_KEY_NAME } from "./background/protocol"; import Spinner from "./Spinner"; -import { api } from "./utils/trpc"; +import { useTRPC } from "./utils/trpc"; import { MessageType } from "./utils/type"; import { isHttpUrl } from "./utils/url"; export default function SavePage() { + const api = useTRPC(); const [error, setError] = useState<string | undefined>(undefined); const { data, mutate: createBookmark, status, - } = api.bookmarks.createBookmark.useMutation({ - onError: (e) => { - setError("Something went wrong: " + e.message); - }, - onSuccess: async () => { - // After successful creation, update badge cache and notify background - const [currentTab] = await chrome.tabs.query({ - active: true, - lastFocusedWindow: true, - }); - await chrome.runtime.sendMessage({ - type: MessageType.BOOKMARK_REFRESH_BADGE, - currentTab: currentTab, - }); - }, - }); + } = useMutation( + api.bookmarks.createBookmark.mutationOptions({ + onError: (e) => { + setError("Something went wrong: " + e.message); + }, + onSuccess: async () => { + // After successful creation, update badge cache and notify background + const [currentTab] = await chrome.tabs.query({ + active: true, + lastFocusedWindow: true, + }); + await chrome.runtime.sendMessage({ + type: MessageType.BOOKMARK_REFRESH_BADGE, + currentTab: currentTab, + }); + }, + }), + ); useEffect(() => { async function getNewBookmarkRequestFromBackgroundScriptIfAny(): Promise<ZNewBookmarkRequest | null> { const { [NEW_BOOKMARK_REQUEST_KEY_NAME]: req } = diff --git a/apps/browser-extension/src/SignInPage.tsx b/apps/browser-extension/src/SignInPage.tsx index 6cf8b35d..8a7229b6 100644 --- a/apps/browser-extension/src/SignInPage.tsx +++ b/apps/browser-extension/src/SignInPage.tsx @@ -1,11 +1,12 @@ import { useState } from "react"; +import { useMutation } from "@tanstack/react-query"; import { useNavigate } from "react-router-dom"; import { Button } from "./components/ui/button"; import { Input } from "./components/ui/input"; import Logo from "./Logo"; import usePluginSettings from "./utils/settings"; -import { api } from "./utils/trpc"; +import { useTRPC } from "./utils/trpc"; const enum LoginState { NONE = "NONE", @@ -14,6 +15,7 @@ const enum LoginState { } export default function SignInPage() { + const api = useTRPC(); const navigate = useNavigate(); const { settings, setSettings } = usePluginSettings(); @@ -21,23 +23,27 @@ export default function SignInPage() { mutate: login, error: usernamePasswordError, isPending: userNamePasswordRequestIsPending, - } = api.apiKeys.exchange.useMutation({ - onSuccess: (resp) => { - setSettings((s) => ({ ...s, apiKey: resp.key, apiKeyId: resp.id })); - navigate("/options"); - }, - }); + } = useMutation( + api.apiKeys.exchange.mutationOptions({ + onSuccess: (resp) => { + setSettings((s) => ({ ...s, apiKey: resp.key, apiKeyId: resp.id })); + navigate("/options"); + }, + }), + ); const { mutate: validateApiKey, error: apiKeyValidationError, isPending: apiKeyValueRequestIsPending, - } = api.apiKeys.validate.useMutation({ - onSuccess: () => { - setSettings((s) => ({ ...s, apiKey: apiKeyFormData.apiKey })); - navigate("/options"); - }, - }); + } = useMutation( + api.apiKeys.validate.mutationOptions({ + onSuccess: () => { + setSettings((s) => ({ ...s, apiKey: apiKeyFormData.apiKey })); + navigate("/options"); + }, + }), + ); const [lastLoginAttemptSource, setLastLoginAttemptSource] = useState<LoginState>(LoginState.NONE); diff --git a/apps/browser-extension/src/components/BookmarkLists.tsx b/apps/browser-extension/src/components/BookmarkLists.tsx index 1d70d257..8debef5c 100644 --- a/apps/browser-extension/src/components/BookmarkLists.tsx +++ b/apps/browser-extension/src/components/BookmarkLists.tsx @@ -1,3 +1,4 @@ +import { useQuery } from "@tanstack/react-query"; import { X } from "lucide-react"; import { @@ -5,15 +6,18 @@ import { useRemoveBookmarkFromList, } from "@karakeep/shared-react/hooks/lists"; -import { api } from "../utils/trpc"; +import { useTRPC } from "../utils/trpc"; import { Button } from "./ui/button"; export default function BookmarkLists({ bookmarkId }: { bookmarkId: string }) { + const api = useTRPC(); const { data: allLists } = useBookmarkLists(); const { mutate: deleteFromList } = useRemoveBookmarkFromList(); - const { data: lists } = api.lists.getListsOfBookmark.useQuery({ bookmarkId }); + const { data: lists } = useQuery( + api.lists.getListsOfBookmark.queryOptions({ bookmarkId }), + ); if (!lists || !allLists) { return null; } diff --git a/apps/browser-extension/src/components/ListsSelector.tsx b/apps/browser-extension/src/components/ListsSelector.tsx index 86c151d1..b27e866a 100644 --- a/apps/browser-extension/src/components/ListsSelector.tsx +++ b/apps/browser-extension/src/components/ListsSelector.tsx @@ -1,4 +1,5 @@ import * as React from "react"; +import { useQuery } from "@tanstack/react-query"; import { useSet } from "@uidotdev/usehooks"; import { Check, ChevronsUpDown } from "lucide-react"; @@ -9,7 +10,7 @@ import { } from "@karakeep/shared-react/hooks/lists"; import { cn } from "../utils/css"; -import { api } from "../utils/trpc"; +import { useTRPC } from "../utils/trpc"; import { Button } from "./ui/button"; import { Command, @@ -23,14 +24,17 @@ import { DynamicPopoverContent } from "./ui/dynamic-popover"; import { Popover, PopoverTrigger } from "./ui/popover"; export function ListsSelector({ bookmarkId }: { bookmarkId: string }) { + const api = useTRPC(); const currentlyUpdating = useSet<string>(); const [open, setOpen] = React.useState(false); const { mutate: addToList } = useAddBookmarkToList(); const { mutate: removeFromList } = useRemoveBookmarkFromList(); - const { data: existingLists } = api.lists.getListsOfBookmark.useQuery({ - bookmarkId, - }); + const { data: existingLists } = useQuery( + api.lists.getListsOfBookmark.queryOptions({ + bookmarkId, + }), + ); const { data: allLists } = useBookmarkLists(); diff --git a/apps/browser-extension/src/components/TagsSelector.tsx b/apps/browser-extension/src/components/TagsSelector.tsx index ce404ac8..30cdcafc 100644 --- a/apps/browser-extension/src/components/TagsSelector.tsx +++ b/apps/browser-extension/src/components/TagsSelector.tsx @@ -1,4 +1,5 @@ import * as React from "react"; +import { useQuery } from "@tanstack/react-query"; import { useSet } from "@uidotdev/usehooks"; import { Check, ChevronsUpDown, Plus } from "lucide-react"; @@ -8,7 +9,7 @@ import { } from "@karakeep/shared-react/hooks/bookmarks"; import { cn } from "../utils/css"; -import { api } from "../utils/trpc"; +import { useTRPC } from "../utils/trpc"; import { Button } from "./ui/button"; import { Command, @@ -22,7 +23,8 @@ import { DynamicPopoverContent } from "./ui/dynamic-popover"; import { Popover, PopoverTrigger } from "./ui/popover"; export function TagsSelector({ bookmarkId }: { bookmarkId: string }) { - const { data: allTags } = api.tags.list.useQuery({}); + const api = useTRPC(); + const { data: allTags } = useQuery(api.tags.list.queryOptions({})); const { data: bookmark } = useAutoRefreshingBookmarkQuery({ bookmarkId }); const existingTagIds = new Set(bookmark?.tags.map((t) => t.id) ?? []); diff --git a/apps/browser-extension/src/utils/providers.tsx b/apps/browser-extension/src/utils/providers.tsx index 86489d6d..4c09084d 100644 --- a/apps/browser-extension/src/utils/providers.tsx +++ b/apps/browser-extension/src/utils/providers.tsx @@ -1,4 +1,4 @@ -import { TRPCProvider } from "@karakeep/shared-react/providers/trpc-provider"; +import { TRPCSettingsProvider } from "@karakeep/shared-react/providers/trpc-provider"; import usePluginSettings from "./settings"; import { ThemeProvider } from "./ThemeProvider"; @@ -7,8 +7,8 @@ export function Providers({ children }: { children: React.ReactNode }) { const { settings } = usePluginSettings(); return ( - <TRPCProvider settings={settings}> + <TRPCSettingsProvider settings={settings}> <ThemeProvider>{children}</ThemeProvider> - </TRPCProvider> + </TRPCSettingsProvider> ); } diff --git a/apps/browser-extension/src/utils/trpc.ts b/apps/browser-extension/src/utils/trpc.ts index b3215d9d..73fe68c5 100644 --- a/apps/browser-extension/src/utils/trpc.ts +++ b/apps/browser-extension/src/utils/trpc.ts @@ -1,7 +1,7 @@ import { QueryClient } from "@tanstack/react-query"; import { persistQueryClient } from "@tanstack/react-query-persist-client"; import { createTRPCClient, httpBatchLink } from "@trpc/client"; -import { createTRPCReact } from "@trpc/react-query"; +import { createTRPCContext } from "@trpc/tanstack-react-query"; import superjson from "superjson"; import type { AppRouter } from "@karakeep/trpc/routers/_app"; @@ -9,7 +9,7 @@ import type { AppRouter } from "@karakeep/trpc/routers/_app"; import { getPluginSettings } from "./settings"; import { createChromeStorage } from "./storagePersister"; -export const api = createTRPCReact<AppRouter>(); +export const { TRPCProvider, useTRPC } = createTRPCContext<AppRouter>(); let apiClient: ReturnType<typeof createTRPCClient<AppRouter>> | null = null; let queryClient: QueryClient | null = null; diff --git a/apps/cli/package.json b/apps/cli/package.json index ca085997..04c72f81 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -20,8 +20,8 @@ "@karakeep/shared": "workspace:^0.1.0", "@karakeep/trpc": "workspace:^0.1.0", "@karakeep/tsconfig": "workspace:^0.1.0", - "@trpc/client": "^11.4.3", - "@trpc/server": "^11.4.3", + "@trpc/client": "^11.9.0", + "@trpc/server": "^11.9.0", "@tsconfig/node22": "^22.0.0", "chalk": "^5.3.0", "commander": "^12.0.0", diff --git a/apps/mobile/app/dashboard/(tabs)/highlights.tsx b/apps/mobile/app/dashboard/(tabs)/highlights.tsx index 7879081b..8a0a8ae3 100644 --- a/apps/mobile/app/dashboard/(tabs)/highlights.tsx +++ b/apps/mobile/app/dashboard/(tabs)/highlights.tsx @@ -4,11 +4,13 @@ import HighlightList from "@/components/highlights/HighlightList"; import CustomSafeAreaView from "@/components/ui/CustomSafeAreaView"; import FullPageSpinner from "@/components/ui/FullPageSpinner"; import PageTitle from "@/components/ui/PageTitle"; +import { useInfiniteQuery, useQueryClient } from "@tanstack/react-query"; -import { api } from "@karakeep/shared-react/trpc"; +import { useTRPC } from "@karakeep/shared-react/trpc"; export default function Highlights() { - const apiUtils = api.useUtils(); + const api = useTRPC(); + const queryClient = useQueryClient(); const { data, isPending, @@ -17,12 +19,14 @@ export default function Highlights() { fetchNextPage, isFetchingNextPage, refetch, - } = api.highlights.getAll.useInfiniteQuery( - {}, - { - initialCursor: null, - getNextPageParam: (lastPage) => lastPage.nextCursor, - }, + } = useInfiniteQuery( + api.highlights.getAll.infiniteQueryOptions( + {}, + { + initialCursor: null, + getNextPageParam: (lastPage) => lastPage.nextCursor, + }, + ), ); if (error) { @@ -34,7 +38,7 @@ export default function Highlights() { } const onRefresh = () => { - apiUtils.highlights.getAll.invalidate(); + queryClient.invalidateQueries(api.highlights.getAll.pathFilter()); }; return ( diff --git a/apps/mobile/app/dashboard/(tabs)/lists.tsx b/apps/mobile/app/dashboard/(tabs)/lists.tsx index 45c23a28..5719c67c 100644 --- a/apps/mobile/app/dashboard/(tabs)/lists.tsx +++ b/apps/mobile/app/dashboard/(tabs)/lists.tsx @@ -8,9 +8,10 @@ import CustomSafeAreaView from "@/components/ui/CustomSafeAreaView"; import FullPageSpinner from "@/components/ui/FullPageSpinner"; import PageTitle from "@/components/ui/PageTitle"; import { Text } from "@/components/ui/Text"; -import { api } from "@/lib/trpc"; +import { useTRPC } from "@/lib/trpc"; import { useColorScheme } from "@/lib/useColorScheme"; import { condProps } from "@/lib/utils"; +import { useQuery, useQueryClient } from "@tanstack/react-query"; import { Plus } from "lucide-react-native"; import { useBookmarkLists } from "@karakeep/shared-react/hooks/lists"; @@ -84,8 +85,9 @@ export default function Lists() { const [showChildrenOf, setShowChildrenOf] = useState<Record<string, boolean>>( {}, ); - const apiUtils = api.useUtils(); - const { data: listStats } = api.lists.stats.useQuery(); + const api = useTRPC(); + const queryClient = useQueryClient(); + const { data: listStats } = useQuery(api.lists.stats.queryOptions()); // Check if there are any shared lists const hasSharedLists = useMemo(() => { @@ -116,8 +118,8 @@ export default function Lists() { } const onRefresh = () => { - apiUtils.lists.list.invalidate(); - apiUtils.lists.stats.invalidate(); + queryClient.invalidateQueries(api.lists.list.pathFilter()); + queryClient.invalidateQueries(api.lists.stats.pathFilter()); }; const links: ListLink[] = [ diff --git a/apps/mobile/app/dashboard/(tabs)/settings.tsx b/apps/mobile/app/dashboard/(tabs)/settings.tsx index 106baec5..2610aa37 100644 --- a/apps/mobile/app/dashboard/(tabs)/settings.tsx +++ b/apps/mobile/app/dashboard/(tabs)/settings.tsx @@ -13,7 +13,8 @@ import { Text } from "@/components/ui/Text"; import { useServerVersion } from "@/lib/hooks"; import { useSession } from "@/lib/session"; import useAppSettings from "@/lib/settings"; -import { api } from "@/lib/trpc"; +import { useTRPC } from "@/lib/trpc"; +import { useQuery } from "@tanstack/react-query"; export default function Dashboard() { const { logout } = useSession(); @@ -22,6 +23,7 @@ export default function Dashboard() { setSettings, isLoading: isSettingsLoading, } = useAppSettings(); + const api = useTRPC(); const imageQuality = useSharedValue(0); const imageQualityMin = useSharedValue(0); @@ -31,7 +33,7 @@ export default function Dashboard() { imageQuality.value = settings.imageQuality * 100; }, [settings]); - const { data, error } = api.users.whoami.useQuery(); + const { data, error } = useQuery(api.users.whoami.queryOptions()); const { data: serverVersion, isLoading: isServerVersionLoading, diff --git a/apps/mobile/app/dashboard/(tabs)/tags.tsx b/apps/mobile/app/dashboard/(tabs)/tags.tsx index c0ac2d49..470ff3f3 100644 --- a/apps/mobile/app/dashboard/(tabs)/tags.tsx +++ b/apps/mobile/app/dashboard/(tabs)/tags.tsx @@ -8,7 +8,8 @@ import FullPageSpinner from "@/components/ui/FullPageSpinner"; import PageTitle from "@/components/ui/PageTitle"; import { SearchInput } from "@/components/ui/SearchInput"; import { Text } from "@/components/ui/Text"; -import { api } from "@/lib/trpc"; +import { useTRPC } from "@/lib/trpc"; +import { useQueryClient } from "@tanstack/react-query"; import { usePaginatedSearchTags } from "@karakeep/shared-react/hooks/tags"; import { useDebounce } from "@karakeep/shared-react/hooks/use-debounce"; @@ -23,7 +24,8 @@ interface TagItem { export default function Tags() { const [refreshing, setRefreshing] = useState(false); const [searchQuery, setSearchQuery] = useState(""); - const apiUtils = api.useUtils(); + const api = useTRPC(); + const queryClient = useQueryClient(); // Debounce search query to avoid too many API calls const debouncedSearch = useDebounce(searchQuery, 300); @@ -56,7 +58,7 @@ export default function Tags() { } const onRefresh = () => { - apiUtils.tags.list.invalidate(); + queryClient.invalidateQueries(api.tags.list.pathFilter()); }; const tags: TagItem[] = data.tags.map((tag) => ({ diff --git a/apps/mobile/app/dashboard/bookmarks/[slug]/index.tsx b/apps/mobile/app/dashboard/bookmarks/[slug]/index.tsx index 8fd04115..567ac605 100644 --- a/apps/mobile/app/dashboard/bookmarks/[slug]/index.tsx +++ b/apps/mobile/app/dashboard/bookmarks/[slug]/index.tsx @@ -12,7 +12,8 @@ import BottomActions from "@/components/bookmarks/BottomActions"; import FullPageError from "@/components/FullPageError"; import FullPageSpinner from "@/components/ui/FullPageSpinner"; import useAppSettings from "@/lib/settings"; -import { api } from "@/lib/trpc"; +import { useTRPC } from "@/lib/trpc"; +import { useQuery } from "@tanstack/react-query"; import { Settings } from "lucide-react-native"; import { useColorScheme } from "nativewind"; @@ -25,6 +26,7 @@ export default function BookmarkView() { const { colorScheme } = useColorScheme(); const isDark = colorScheme === "dark"; const { settings } = useAppSettings(); + const api = useTRPC(); const [bookmarkLinkType, setBookmarkLinkType] = useState<BookmarkLinkType>( settings.defaultBookmarkView, @@ -38,10 +40,12 @@ export default function BookmarkView() { data: bookmark, error, refetch, - } = api.bookmarks.getBookmark.useQuery({ - bookmarkId: slug, - includeContent: false, - }); + } = useQuery( + api.bookmarks.getBookmark.queryOptions({ + bookmarkId: slug, + includeContent: false, + }), + ); if (error) { return <FullPageError error={error.message} onRetry={refetch} />; diff --git a/apps/mobile/app/dashboard/bookmarks/[slug]/manage_lists.tsx b/apps/mobile/app/dashboard/bookmarks/[slug]/manage_lists.tsx index 8402bb0b..1070207b 100644 --- a/apps/mobile/app/dashboard/bookmarks/[slug]/manage_lists.tsx +++ b/apps/mobile/app/dashboard/bookmarks/[slug]/manage_lists.tsx @@ -5,15 +5,18 @@ import { useLocalSearchParams } from "expo-router"; import CustomSafeAreaView from "@/components/ui/CustomSafeAreaView"; import { Text } from "@/components/ui/Text"; import { useToast } from "@/components/ui/Toast"; +import { useQuery } from "@tanstack/react-query"; +import type { ZBookmarkList } from "@karakeep/shared/types/lists"; import { useAddBookmarkToList, useBookmarkLists, useRemoveBookmarkFromList, } from "@karakeep/shared-react/hooks/lists"; -import { api } from "@karakeep/shared-react/trpc"; +import { useTRPC } from "@karakeep/shared-react/trpc"; const ListPickerPage = () => { + const api = useTRPC(); const { slug: bookmarkId } = useLocalSearchParams(); if (typeof bookmarkId !== "string") { throw new Error("Unexpected param type"); @@ -26,13 +29,16 @@ const ListPickerPage = () => { showProgress: false, }); }; - const { data: existingLists } = api.lists.getListsOfBookmark.useQuery( - { - bookmarkId, - }, - { - select: (data) => new Set(data.lists.map((l) => l.id)), - }, + const { data: existingLists } = useQuery( + api.lists.getListsOfBookmark.queryOptions( + { + bookmarkId, + }, + { + select: (data: { lists: ZBookmarkList[] }) => + new Set(data.lists.map((l) => l.id)), + }, + ), ); const { data } = useBookmarkLists(); diff --git a/apps/mobile/app/dashboard/bookmarks/[slug]/manage_tags.tsx b/apps/mobile/app/dashboard/bookmarks/[slug]/manage_tags.tsx index 984bc224..64d057f2 100644 --- a/apps/mobile/app/dashboard/bookmarks/[slug]/manage_tags.tsx +++ b/apps/mobile/app/dashboard/bookmarks/[slug]/manage_tags.tsx @@ -6,17 +6,19 @@ import FullPageSpinner from "@/components/ui/FullPageSpinner"; import { Text } from "@/components/ui/Text"; import { useToast } from "@/components/ui/Toast"; import { useColorScheme } from "@/lib/useColorScheme"; +import { useQuery } from "@tanstack/react-query"; import { Check, Plus } from "lucide-react-native"; import { useAutoRefreshingBookmarkQuery, useUpdateBookmarkTags, } from "@karakeep/shared-react/hooks/bookmarks"; -import { api } from "@karakeep/shared-react/trpc"; +import { useTRPC } from "@karakeep/shared-react/trpc"; const NEW_TAG_ID = "new-tag"; const ListPickerPage = () => { + const api = useTRPC(); const { colors } = useColorScheme(); const { slug: bookmarkId } = useLocalSearchParams(); @@ -34,22 +36,24 @@ const ListPickerPage = () => { }); }; - const { data: allTags, isPending: isAllTagsPending } = api.tags.list.useQuery( - {}, - { - select: React.useCallback( - (data: { tags: { id: string; name: string }[] }) => { - return data.tags - .map((t) => ({ - id: t.id, - name: t.name, - lowered: t.name.toLowerCase(), - })) - .sort((a, b) => a.lowered.localeCompare(b.lowered)); - }, - [], - ), - }, + const { data: allTags, isPending: isAllTagsPending } = useQuery( + api.tags.list.queryOptions( + {}, + { + select: React.useCallback( + (data: { tags: { id: string; name: string }[] }) => { + return data.tags + .map((t) => ({ + id: t.id, + name: t.name, + lowered: t.name.toLowerCase(), + })) + .sort((a, b) => a.lowered.localeCompare(b.lowered)); + }, + [], + ), + }, + ), ); const { data: existingTags } = useAutoRefreshingBookmarkQuery({ bookmarkId, diff --git a/apps/mobile/app/dashboard/lists/[slug]/edit.tsx b/apps/mobile/app/dashboard/lists/[slug]/edit.tsx index 6ccc2f26..e0654722 100644 --- a/apps/mobile/app/dashboard/lists/[slug]/edit.tsx +++ b/apps/mobile/app/dashboard/lists/[slug]/edit.tsx @@ -7,7 +7,8 @@ import FullPageSpinner from "@/components/ui/FullPageSpinner"; import { Input } from "@/components/ui/Input"; import { Text } from "@/components/ui/Text"; import { useToast } from "@/components/ui/Toast"; -import { api } from "@/lib/trpc"; +import { useTRPC } from "@/lib/trpc"; +import { useQuery } from "@tanstack/react-query"; import { useEditBookmarkList } from "@karakeep/shared-react/hooks/lists"; @@ -16,6 +17,7 @@ const EditListPage = () => { const [text, setText] = useState(""); const [query, setQuery] = useState(""); const { toast } = useToast(); + const api = useTRPC(); const { mutate, isPending: editIsPending } = useEditBookmarkList({ onSuccess: () => { dismiss(); @@ -41,9 +43,11 @@ const EditListPage = () => { throw new Error("Unexpected param type"); } - const { data: list, isLoading: fetchIsPending } = api.lists.get.useQuery({ - listId, - }); + const { data: list, isLoading: fetchIsPending } = useQuery( + api.lists.get.queryOptions({ + listId, + }), + ); const dismiss = () => { router.back(); diff --git a/apps/mobile/app/dashboard/lists/[slug]/index.tsx b/apps/mobile/app/dashboard/lists/[slug]/index.tsx index 11379588..97f797c6 100644 --- a/apps/mobile/app/dashboard/lists/[slug]/index.tsx +++ b/apps/mobile/app/dashboard/lists/[slug]/index.tsx @@ -5,14 +5,16 @@ import UpdatingBookmarkList from "@/components/bookmarks/UpdatingBookmarkList"; import FullPageError from "@/components/FullPageError"; import CustomSafeAreaView from "@/components/ui/CustomSafeAreaView"; import FullPageSpinner from "@/components/ui/FullPageSpinner"; -import { api } from "@/lib/trpc"; +import { useTRPC } from "@/lib/trpc"; import { MenuView } from "@react-native-menu/menu"; +import { useMutation, useQuery } from "@tanstack/react-query"; import { Ellipsis } from "lucide-react-native"; import { ZBookmarkList } from "@karakeep/shared/types/lists"; export default function ListView() { const { slug } = useLocalSearchParams(); + const api = useTRPC(); if (typeof slug !== "string") { throw new Error("Unexpected param type"); } @@ -20,7 +22,7 @@ export default function ListView() { data: list, error, refetch, - } = api.lists.get.useQuery({ listId: slug }); + } = useQuery(api.lists.get.queryOptions({ listId: slug })); return ( <CustomSafeAreaView> @@ -58,17 +60,22 @@ function ListActionsMenu({ listId: string; role: ZBookmarkList["userRole"]; }) { - const { mutate: deleteList } = api.lists.delete.useMutation({ - onSuccess: () => { - router.replace("/dashboard/lists"); - }, - }); + const api = useTRPC(); + const { mutate: deleteList } = useMutation( + api.lists.delete.mutationOptions({ + onSuccess: () => { + router.replace("/dashboard/lists"); + }, + }), + ); - const { mutate: leaveList } = api.lists.leaveList.useMutation({ - onSuccess: () => { - router.replace("/dashboard/lists"); - }, - }); + const { mutate: leaveList } = useMutation( + api.lists.leaveList.mutationOptions({ + onSuccess: () => { + router.replace("/dashboard/lists"); + }, + }), + ); const handleDelete = () => { Alert.alert("Delete List", "Are you sure you want to delete this list?", [ diff --git a/apps/mobile/app/dashboard/search.tsx b/apps/mobile/app/dashboard/search.tsx index ab89ce8d..0e59c607 100644 --- a/apps/mobile/app/dashboard/search.tsx +++ b/apps/mobile/app/dashboard/search.tsx @@ -7,9 +7,13 @@ import CustomSafeAreaView from "@/components/ui/CustomSafeAreaView"; import FullPageSpinner from "@/components/ui/FullPageSpinner"; import { SearchInput } from "@/components/ui/SearchInput"; import { Text } from "@/components/ui/Text"; -import { api } from "@/lib/trpc"; +import { useTRPC } from "@/lib/trpc"; import AsyncStorage from "@react-native-async-storage/async-storage"; -import { keepPreviousData } from "@tanstack/react-query"; +import { + keepPreviousData, + useInfiniteQuery, + useQueryClient, +} from "@tanstack/react-query"; import { useSearchHistory } from "@karakeep/shared-react/hooks/search-history"; import { useDebounce } from "@karakeep/shared-react/hooks/use-debounce"; @@ -29,7 +33,12 @@ export default function Search() { removeItem: (k: string) => AsyncStorage.removeItem(k), }); - const onRefresh = api.useUtils().bookmarks.searchBookmarks.invalidate; + const api = useTRPC(); + const queryClient = useQueryClient(); + + const onRefresh = () => { + queryClient.invalidateQueries(api.bookmarks.searchBookmarks.pathFilter()); + }; const { data, @@ -39,14 +48,16 @@ export default function Search() { isFetching, fetchNextPage, isFetchingNextPage, - } = api.bookmarks.searchBookmarks.useInfiniteQuery( - { text: query }, - { - placeholderData: keepPreviousData, - gcTime: 0, - initialCursor: null, - getNextPageParam: (lastPage) => lastPage.nextCursor, - }, + } = useInfiniteQuery( + api.bookmarks.searchBookmarks.infiniteQueryOptions( + { text: query }, + { + placeholderData: keepPreviousData, + gcTime: 0, + initialCursor: null, + getNextPageParam: (lastPage) => lastPage.nextCursor, + }, + ), ); const filteredHistory = useMemo(() => { diff --git a/apps/mobile/app/dashboard/tags/[slug].tsx b/apps/mobile/app/dashboard/tags/[slug].tsx index 3f294328..170cb04d 100644 --- a/apps/mobile/app/dashboard/tags/[slug].tsx +++ b/apps/mobile/app/dashboard/tags/[slug].tsx @@ -4,15 +4,21 @@ import UpdatingBookmarkList from "@/components/bookmarks/UpdatingBookmarkList"; import FullPageError from "@/components/FullPageError"; import CustomSafeAreaView from "@/components/ui/CustomSafeAreaView"; import FullPageSpinner from "@/components/ui/FullPageSpinner"; -import { api } from "@/lib/trpc"; +import { useTRPC } from "@/lib/trpc"; +import { useQuery } from "@tanstack/react-query"; export default function TagView() { const { slug } = useLocalSearchParams(); + const api = useTRPC(); if (typeof slug !== "string") { throw new Error("Unexpected param type"); } - const { data: tag, error, refetch } = api.tags.get.useQuery({ tagId: slug }); + const { + data: tag, + error, + refetch, + } = useQuery(api.tags.get.queryOptions({ tagId: slug })); return ( <CustomSafeAreaView> diff --git a/apps/mobile/app/sharing.tsx b/apps/mobile/app/sharing.tsx index 3e2b6bfb..6d9167db 100644 --- a/apps/mobile/app/sharing.tsx +++ b/apps/mobile/app/sharing.tsx @@ -5,8 +5,9 @@ import { useShareIntentContext } from "expo-share-intent"; import { Button } from "@/components/ui/Button"; import { Text } from "@/components/ui/Text"; import useAppSettings from "@/lib/settings"; -import { api } from "@/lib/trpc"; +import { useTRPC } from "@/lib/trpc"; import { useUploadAsset } from "@/lib/upload"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; import { z } from "zod"; import { BookmarkTypes, ZBookmark } from "@karakeep/shared/types/bookmarks"; @@ -18,8 +19,11 @@ type Mode = | { type: "error" }; function SaveBookmark({ setMode }: { setMode: (mode: Mode) => void }) { + const api = useTRPC(); + const queryClient = useQueryClient(); + const onSaved = (d: ZBookmark & { alreadyExists: boolean }) => { - invalidateAllBookmarks(); + queryClient.invalidateQueries(api.bookmarks.getBookmarks.pathFilter()); setMode({ type: d.alreadyExists ? "alreadyExists" : "success", bookmarkId: d.id, @@ -36,9 +40,6 @@ function SaveBookmark({ setMode }: { setMode: (mode: Mode) => void }) { }, }); - const invalidateAllBookmarks = - api.useUtils().bookmarks.getBookmarks.invalidate; - useEffect(() => { if (isLoading) { return; @@ -77,12 +78,14 @@ function SaveBookmark({ setMode }: { setMode: (mode: Mode) => void }) { } }, [isLoading]); - const { mutate, isPending } = api.bookmarks.createBookmark.useMutation({ - onSuccess: onSaved, - onError: () => { - setMode({ type: "error" }); - }, - }); + const { mutate, isPending } = useMutation( + api.bookmarks.createBookmark.mutationOptions({ + onSuccess: onSaved, + onError: () => { + setMode({ type: "error" }); + }, + }), + ); return ( <View className="flex flex-row gap-3"> diff --git a/apps/mobile/app/signin.tsx b/apps/mobile/app/signin.tsx index 03cbba5a..5c6370ca 100644 --- a/apps/mobile/app/signin.tsx +++ b/apps/mobile/app/signin.tsx @@ -13,7 +13,8 @@ import { Button } from "@/components/ui/Button"; import { Input } from "@/components/ui/Input"; import { Text } from "@/components/ui/Text"; import useAppSettings from "@/lib/settings"; -import { api } from "@/lib/trpc"; +import { useTRPC } from "@/lib/trpc"; +import { useMutation } from "@tanstack/react-query"; import { Bug, Edit3 } from "lucide-react-native"; enum LoginType { @@ -24,6 +25,7 @@ enum LoginType { export default function Signin() { const { settings, setSettings } = useAppSettings(); const router = useRouter(); + const api = useTRPC(); const [error, setError] = useState<string | undefined>(); const [loginType, setLoginType] = useState<LoginType>(LoginType.Password); @@ -43,33 +45,37 @@ export default function Signin() { }; const { mutate: login, isPending: userNamePasswordRequestIsPending } = - api.apiKeys.exchange.useMutation({ - onSuccess: (resp) => { - setSettings({ ...settings, apiKey: resp.key, apiKeyId: resp.id }); - }, - onError: (e) => { - if (e.data?.code === "UNAUTHORIZED") { - setError("Wrong username or password"); - } else { - setError(`${e.message}`); - } - }, - }); + useMutation( + api.apiKeys.exchange.mutationOptions({ + onSuccess: (resp) => { + setSettings({ ...settings, apiKey: resp.key, apiKeyId: resp.id }); + }, + onError: (e) => { + if (e.data?.code === "UNAUTHORIZED") { + setError("Wrong username or password"); + } else { + setError(`${e.message}`); + } + }, + }), + ); const { mutate: validateApiKey, isPending: apiKeyValueRequestIsPending } = - api.apiKeys.validate.useMutation({ - onSuccess: () => { - const apiKey = apiKeyRef.current; - setSettings({ ...settings, apiKey: apiKey }); - }, - onError: (e) => { - if (e.data?.code === "UNAUTHORIZED") { - setError("Invalid API key"); - } else { - setError(`${e.message}`); - } - }, - }); + useMutation( + api.apiKeys.validate.mutationOptions({ + onSuccess: () => { + const apiKey = apiKeyRef.current; + setSettings({ ...settings, apiKey: apiKey }); + }, + onError: (e) => { + if (e.data?.code === "UNAUTHORIZED") { + setError("Invalid API key"); + } else { + setError(`${e.message}`); + } + }, + }), + ); if (settings.apiKey) { return <Redirect href="dashboard" />; diff --git a/apps/mobile/components/bookmarks/BookmarkCard.tsx b/apps/mobile/components/bookmarks/BookmarkCard.tsx index 6c3ef070..38fed737 100644 --- a/apps/mobile/components/bookmarks/BookmarkCard.tsx +++ b/apps/mobile/components/bookmarks/BookmarkCard.tsx @@ -15,9 +15,10 @@ import { router, useRouter } from "expo-router"; import * as Sharing from "expo-sharing"; import { Text } from "@/components/ui/Text"; import useAppSettings from "@/lib/settings"; -import { api } from "@/lib/trpc"; +import { useTRPC } from "@/lib/trpc"; import { buildApiHeaders } from "@/lib/utils"; import { MenuView } from "@react-native-menu/menu"; +import { useQuery } from "@tanstack/react-query"; import { Ellipsis, ShareIcon, Star } from "lucide-react-native"; import type { ZBookmark } from "@karakeep/shared/types/bookmarks"; @@ -477,20 +478,23 @@ export default function BookmarkCard({ }: { bookmark: ZBookmark; }) { - const { data: bookmark } = api.bookmarks.getBookmark.useQuery( - { - bookmarkId: initialData.id, - }, - { - initialData, - refetchInterval: (query) => { - const data = query.state.data; - if (!data) { - return false; - } - return getBookmarkRefreshInterval(data); + const api = useTRPC(); + const { data: bookmark } = useQuery( + api.bookmarks.getBookmark.queryOptions( + { + bookmarkId: initialData.id, + }, + { + initialData, + refetchInterval: (query) => { + const data = query.state.data; + if (!data) { + return false; + } + return getBookmarkRefreshInterval(data); + }, }, - }, + ), ); const router = useRouter(); diff --git a/apps/mobile/components/bookmarks/BookmarkLinkPreview.tsx b/apps/mobile/components/bookmarks/BookmarkLinkPreview.tsx index 4478bdda..aa073c69 100644 --- a/apps/mobile/components/bookmarks/BookmarkLinkPreview.tsx +++ b/apps/mobile/components/bookmarks/BookmarkLinkPreview.tsx @@ -6,8 +6,9 @@ import { WebViewSourceUri } from "react-native-webview/lib/WebViewTypes"; import { Text } from "@/components/ui/Text"; import { useAssetUrl } from "@/lib/hooks"; import { useReaderSettings, WEBVIEW_FONT_FAMILIES } from "@/lib/readerSettings"; -import { api } from "@/lib/trpc"; +import { useTRPC } from "@/lib/trpc"; import { useColorScheme } from "@/lib/useColorScheme"; +import { useQuery } from "@tanstack/react-query"; import { BookmarkTypes, ZBookmark } from "@karakeep/shared/types/bookmarks"; @@ -65,16 +66,19 @@ export function BookmarkLinkReaderPreview({ }) { const { isDarkColorScheme: isDark } = useColorScheme(); const { settings: readerSettings } = useReaderSettings(); + const api = useTRPC(); const { data: bookmarkWithContent, error, isLoading, refetch, - } = api.bookmarks.getBookmark.useQuery({ - bookmarkId: bookmark.id, - includeContent: true, - }); + } = useQuery( + api.bookmarks.getBookmark.queryOptions({ + bookmarkId: bookmark.id, + includeContent: true, + }), + ); if (isLoading) { return <FullPageSpinner />; diff --git a/apps/mobile/components/bookmarks/UpdatingBookmarkList.tsx b/apps/mobile/components/bookmarks/UpdatingBookmarkList.tsx index e627ee16..b5aeaa40 100644 --- a/apps/mobile/components/bookmarks/UpdatingBookmarkList.tsx +++ b/apps/mobile/components/bookmarks/UpdatingBookmarkList.tsx @@ -1,4 +1,5 @@ -import { api } from "@/lib/trpc"; +import { useTRPC } from "@/lib/trpc"; +import { useInfiniteQuery, useQueryClient } from "@tanstack/react-query"; import type { ZGetBookmarksRequest } from "@karakeep/shared/types/bookmarks"; import { BookmarkTypes } from "@karakeep/shared/types/bookmarks"; @@ -14,7 +15,8 @@ export default function UpdatingBookmarkList({ query: Omit<ZGetBookmarksRequest, "sortOrder" | "includeContent">; // Sort order is not supported in mobile yet header?: React.ReactElement; }) { - const apiUtils = api.useUtils(); + const api = useTRPC(); + const queryClient = useQueryClient(); const { data, isPending, @@ -23,12 +25,14 @@ export default function UpdatingBookmarkList({ fetchNextPage, isFetchingNextPage, refetch, - } = api.bookmarks.getBookmarks.useInfiniteQuery( - { ...query, useCursorV2: true, includeContent: false }, - { - initialCursor: null, - getNextPageParam: (lastPage) => lastPage.nextCursor, - }, + } = useInfiniteQuery( + api.bookmarks.getBookmarks.infiniteQueryOptions( + { ...query, useCursorV2: true, includeContent: false }, + { + initialCursor: null, + getNextPageParam: (lastPage) => lastPage.nextCursor, + }, + ), ); if (error) { @@ -40,8 +44,8 @@ export default function UpdatingBookmarkList({ } const onRefresh = () => { - apiUtils.bookmarks.getBookmarks.invalidate(); - apiUtils.bookmarks.getBookmark.invalidate(); + queryClient.invalidateQueries(api.bookmarks.getBookmarks.pathFilter()); + queryClient.invalidateQueries(api.bookmarks.getBookmark.pathFilter()); }; return ( diff --git a/apps/mobile/components/highlights/HighlightCard.tsx b/apps/mobile/components/highlights/HighlightCard.tsx index 7e0b4a2b..c8f97e32 100644 --- a/apps/mobile/components/highlights/HighlightCard.tsx +++ b/apps/mobile/components/highlights/HighlightCard.tsx @@ -2,7 +2,8 @@ import { ActivityIndicator, Alert, Pressable, View } from "react-native"; import * as Haptics from "expo-haptics"; import { useRouter } from "expo-router"; import { Text } from "@/components/ui/Text"; -import { api } from "@/lib/trpc"; +import { useTRPC } from "@/lib/trpc"; +import { useQuery } from "@tanstack/react-query"; import dayjs from "dayjs"; import relativeTime from "dayjs/plugin/relativeTime"; import { ExternalLink, Trash2 } from "lucide-react-native"; @@ -29,6 +30,7 @@ export default function HighlightCard({ }) { const { toast } = useToast(); const router = useRouter(); + const api = useTRPC(); const onError = () => { toast({ @@ -64,13 +66,15 @@ export default function HighlightCard({ ], ); - const { data: bookmark } = api.bookmarks.getBookmark.useQuery( - { - bookmarkId: highlight.bookmarkId, - }, - { - retry: false, - }, + const { data: bookmark } = useQuery( + api.bookmarks.getBookmark.queryOptions( + { + bookmarkId: highlight.bookmarkId, + }, + { + retry: false, + }, + ), ); const handleBookmarkPress = () => { diff --git a/apps/mobile/lib/providers.tsx b/apps/mobile/lib/providers.tsx index 01d2d5b5..4a7def1d 100644 --- a/apps/mobile/lib/providers.tsx +++ b/apps/mobile/lib/providers.tsx @@ -2,7 +2,7 @@ import { useEffect } from "react"; import FullPageSpinner from "@/components/ui/FullPageSpinner"; import { Toaster } from "sonner-native"; -import { TRPCProvider } from "@karakeep/shared-react/providers/trpc-provider"; +import { TRPCSettingsProvider } from "@karakeep/shared-react/providers/trpc-provider"; import { ReaderSettingsProvider } from "./readerSettings"; import useAppSettings from "./settings"; @@ -20,11 +20,11 @@ export function Providers({ children }: { children: React.ReactNode }) { } return ( - <TRPCProvider settings={settings}> + <TRPCSettingsProvider settings={settings}> <ReaderSettingsProvider> {children} <Toaster /> </ReaderSettingsProvider> - </TRPCProvider> + </TRPCSettingsProvider> ); } diff --git a/apps/mobile/lib/session.ts b/apps/mobile/lib/session.ts index 8eb646cb..9f539693 100644 --- a/apps/mobile/lib/session.ts +++ b/apps/mobile/lib/session.ts @@ -1,12 +1,16 @@ import { useCallback } from "react"; +import { useMutation } from "@tanstack/react-query"; import useAppSettings from "./settings"; -import { api } from "./trpc"; +import { useTRPC } from "./trpc"; export function useSession() { const { settings, setSettings } = useAppSettings(); + const api = useTRPC(); - const { mutate: deleteKey } = api.apiKeys.revoke.useMutation(); + const { mutate: deleteKey } = useMutation( + api.apiKeys.revoke.mutationOptions(), + ); const logout = useCallback(() => { if (settings.apiKeyId) { diff --git a/apps/mobile/lib/trpc.ts b/apps/mobile/lib/trpc.ts index e56968b8..915c265d 100644 --- a/apps/mobile/lib/trpc.ts +++ b/apps/mobile/lib/trpc.ts @@ -1,5 +1,3 @@ -import { createTRPCReact } from "@trpc/react-query"; - -import type { AppRouter } from "@karakeep/trpc/routers/_app"; - -export const api = createTRPCReact<AppRouter>(); +// Re-export from shared-react to ensure there's only one TRPCProvider context +// This is necessary because the hooks in shared-react use useTRPC from shared-react +export { TRPCProvider, useTRPC } from "@karakeep/shared-react/trpc"; diff --git a/apps/mobile/lib/upload.ts b/apps/mobile/lib/upload.ts index 06f007f7..13abae16 100644 --- a/apps/mobile/lib/upload.ts +++ b/apps/mobile/lib/upload.ts @@ -1,5 +1,5 @@ import ReactNativeBlobUtil from "react-native-blob-util"; -import { useMutation } from "@tanstack/react-query"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; import { BookmarkTypes, ZBookmark } from "@karakeep/shared/types/bookmarks"; import { @@ -8,7 +8,7 @@ import { } from "@karakeep/shared/types/uploads"; import type { Settings } from "./settings"; -import { api } from "./trpc"; +import { useTRPC } from "./trpc"; import { buildApiHeaders } from "./utils"; export function useUploadAsset( @@ -18,13 +18,13 @@ export function useUploadAsset( onError?: (e: string) => void; }, ) { - const invalidateAllBookmarks = - api.useUtils().bookmarks.getBookmarks.invalidate; + const api = useTRPC(); + const queryClient = useQueryClient(); - const { mutate: createBookmark, isPending: isCreatingBookmark } = - api.bookmarks.createBookmark.useMutation({ + const { mutate: createBookmark, isPending: isCreatingBookmark } = useMutation( + api.bookmarks.createBookmark.mutationOptions({ onSuccess: (d) => { - invalidateAllBookmarks(); + queryClient.invalidateQueries(api.bookmarks.getBookmarks.pathFilter()); if (options.onSuccess) { options.onSuccess(d); } @@ -34,7 +34,8 @@ export function useUploadAsset( options.onError(e.message); } }, - }); + }), + ); const { mutate: uploadAsset, isPending: isUploading } = useMutation({ mutationFn: async (file: { type: string; name: string; uri: string }) => { diff --git a/apps/web/app/check-email/page.tsx b/apps/web/app/check-email/page.tsx index 227e116c..9e6a37b8 100644 --- a/apps/web/app/check-email/page.tsx +++ b/apps/web/app/check-email/page.tsx @@ -11,26 +11,30 @@ import { CardHeader, CardTitle, } from "@/components/ui/card"; -import { api } from "@/lib/trpc"; +import { useTRPC } from "@/lib/trpc"; +import { useMutation } from "@tanstack/react-query"; import { Loader2, Mail } from "lucide-react"; export default function CheckEmailPage() { + const api = useTRPC(); const searchParams = useSearchParams(); const router = useRouter(); const [message, setMessage] = useState(""); const email = searchParams.get("email"); - const resendEmailMutation = api.users.resendVerificationEmail.useMutation({ - onSuccess: () => { - setMessage( - "A new verification email has been sent to your email address.", - ); - }, - onError: (error) => { - setMessage(error.message || "Failed to resend verification email."); - }, - }); + const resendEmailMutation = useMutation( + api.users.resendVerificationEmail.mutationOptions({ + onSuccess: () => { + setMessage( + "A new verification email has been sent to your email address.", + ); + }, + onError: (error) => { + setMessage(error.message || "Failed to resend verification email."); + }, + }), + ); const handleResendEmail = () => { if (email) { diff --git a/apps/web/app/reader/[bookmarkId]/page.tsx b/apps/web/app/reader/[bookmarkId]/page.tsx index 3eba7c7a..0ba72016 100644 --- a/apps/web/app/reader/[bookmarkId]/page.tsx +++ b/apps/web/app/reader/[bookmarkId]/page.tsx @@ -10,22 +10,28 @@ import { FullPageSpinner } from "@/components/ui/full-page-spinner"; import { Separator } from "@/components/ui/separator"; import { useSession } from "@/lib/auth/client"; import { useReaderSettings } from "@/lib/readerSettings"; +import { useQuery } from "@tanstack/react-query"; import { HighlighterIcon as Highlight, Printer, X } from "lucide-react"; -import { api } from "@karakeep/shared-react/trpc"; +import { useTRPC } from "@karakeep/shared-react/trpc"; import { BookmarkTypes } from "@karakeep/shared/types/bookmarks"; import { READER_FONT_FAMILIES } from "@karakeep/shared/types/readers"; import { getBookmarkTitle } from "@karakeep/shared/utils/bookmarkUtils"; export default function ReaderViewPage() { + const api = useTRPC(); const params = useParams<{ bookmarkId: string }>(); const bookmarkId = params.bookmarkId; - const { data: highlights } = api.highlights.getForBookmark.useQuery({ - bookmarkId, - }); - const { data: bookmark } = api.bookmarks.getBookmark.useQuery({ - bookmarkId, - }); + const { data: highlights } = useQuery( + api.highlights.getForBookmark.queryOptions({ + bookmarkId, + }), + ); + const { data: bookmark } = useQuery( + api.bookmarks.getBookmark.queryOptions({ + bookmarkId, + }), + ); const { data: session } = useSession(); const router = useRouter(); diff --git a/apps/web/app/settings/assets/page.tsx b/apps/web/app/settings/assets/page.tsx index a2d2c9ab..0991816c 100644 --- a/apps/web/app/settings/assets/page.tsx +++ b/apps/web/app/settings/assets/page.tsx @@ -16,8 +16,9 @@ import { } from "@/components/ui/table"; import { ASSET_TYPE_TO_ICON } from "@/lib/attachments"; import { useTranslation } from "@/lib/i18n/client"; -import { api } from "@/lib/trpc"; +import { useTRPC } from "@/lib/trpc"; import { formatBytes } from "@/lib/utils"; +import { useInfiniteQuery } from "@tanstack/react-query"; import { ExternalLink, Trash2 } from "lucide-react"; import { useDetachBookmarkAsset } from "@karakeep/shared-react/hooks/assets"; @@ -28,6 +29,7 @@ import { } from "@karakeep/trpc/lib/attachments"; export default function AssetsSettingsPage() { + const api = useTRPC(); const { t } = useTranslation(); const { mutate: detachAsset, isPending: isDetaching } = useDetachBookmarkAsset({ @@ -49,13 +51,15 @@ export default function AssetsSettingsPage() { fetchNextPage, hasNextPage, isFetchingNextPage, - } = api.assets.list.useInfiniteQuery( - { - limit: 20, - }, - { - getNextPageParam: (lastPage) => lastPage.nextCursor, - }, + } = useInfiniteQuery( + api.assets.list.infiniteQueryOptions( + { + limit: 20, + }, + { + getNextPageParam: (lastPage) => lastPage.nextCursor, + }, + ), ); const assets = data?.pages.flatMap((page) => page.assets) ?? []; diff --git a/apps/web/app/settings/broken-links/page.tsx b/apps/web/app/settings/broken-links/page.tsx index 139e8f91..4197d62e 100644 --- a/apps/web/app/settings/broken-links/page.tsx +++ b/apps/web/app/settings/broken-links/page.tsx @@ -11,6 +11,7 @@ import { TableHeader, TableRow, } from "@/components/ui/table"; +import { useQuery, useQueryClient } from "@tanstack/react-query"; import { RefreshCw, Trash2 } from "lucide-react"; import { useTranslation } from "react-i18next"; @@ -18,20 +19,23 @@ import { useDeleteBookmark, useRecrawlBookmark, } from "@karakeep/shared-react/hooks/bookmarks"; -import { api } from "@karakeep/shared-react/trpc"; +import { useTRPC } from "@karakeep/shared-react/trpc"; export default function BrokenLinksPage() { + const api = useTRPC(); const { t } = useTranslation(); - const apiUtils = api.useUtils(); - const { data, isPending } = api.bookmarks.getBrokenLinks.useQuery(); + const queryClient = useQueryClient(); + const { data, isPending } = useQuery( + api.bookmarks.getBrokenLinks.queryOptions(), + ); const { mutate: deleteBookmark, isPending: isDeleting } = useDeleteBookmark({ onSuccess: () => { toast({ description: t("toasts.bookmarks.deleted"), }); - apiUtils.bookmarks.getBrokenLinks.invalidate(); + queryClient.invalidateQueries(api.bookmarks.getBrokenLinks.pathFilter()); }, onError: () => { toast({ @@ -47,7 +51,9 @@ export default function BrokenLinksPage() { toast({ description: t("toasts.bookmarks.refetch"), }); - apiUtils.bookmarks.getBrokenLinks.invalidate(); + queryClient.invalidateQueries( + api.bookmarks.getBrokenLinks.pathFilter(), + ); }, onError: () => { toast({ diff --git a/apps/web/app/settings/rules/page.tsx b/apps/web/app/settings/rules/page.tsx index 17f5b388..6d0b6522 100644 --- a/apps/web/app/settings/rules/page.tsx +++ b/apps/web/app/settings/rules/page.tsx @@ -6,21 +6,25 @@ import RuleList from "@/components/dashboard/rules/RuleEngineRuleList"; import { Button } from "@/components/ui/button"; import { FullPageSpinner } from "@/components/ui/full-page-spinner"; import { useTranslation } from "@/lib/i18n/client"; -import { api } from "@/lib/trpc"; +import { useTRPC } from "@/lib/trpc"; +import { useQuery } from "@tanstack/react-query"; import { PlusCircle } from "lucide-react"; import { RuleEngineRule } from "@karakeep/shared/types/rules"; export default function RulesSettingsPage() { + const api = useTRPC(); const { t } = useTranslation(); const [editingRule, setEditingRule] = useState< (Omit<RuleEngineRule, "id"> & { id: string | null }) | null >(null); - const { data: rules, isLoading } = api.rules.list.useQuery(undefined, { - refetchOnWindowFocus: true, - refetchOnMount: true, - }); + const { data: rules, isLoading } = useQuery( + api.rules.list.queryOptions(undefined, { + refetchOnWindowFocus: true, + refetchOnMount: true, + }), + ); const handleCreateRule = () => { const newRule = { diff --git a/apps/web/app/settings/stats/page.tsx b/apps/web/app/settings/stats/page.tsx index 28c017f5..06076376 100644 --- a/apps/web/app/settings/stats/page.tsx +++ b/apps/web/app/settings/stats/page.tsx @@ -6,7 +6,8 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Progress } from "@/components/ui/progress"; import { Skeleton } from "@/components/ui/skeleton"; import { useTranslation } from "@/lib/i18n/client"; -import { api } from "@/lib/trpc"; +import { useTRPC } from "@/lib/trpc"; +import { useQuery } from "@tanstack/react-query"; import { Archive, BarChart3, @@ -159,9 +160,10 @@ function StatCard({ } export default function StatsPage() { + const api = useTRPC(); const { t } = useTranslation(); - const { data: stats, isLoading } = api.users.stats.useQuery(); - const { data: userSettings } = api.users.settings.useQuery(); + const { data: stats, isLoading } = useQuery(api.users.stats.queryOptions()); + const { data: userSettings } = useQuery(api.users.settings.queryOptions()); const maxHourlyActivity = useMemo(() => { if (!stats) return 0; @@ -237,7 +239,6 @@ export default function StatsPage() { </p> </div> </div> - {/* Overview Stats */} <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4"> <StatCard @@ -289,7 +290,6 @@ export default function StatsPage() { description={t("settings.stats.overview.bookmarks_added")} /> </div> - <div className="grid gap-6 md:grid-cols-2"> {/* Bookmark Types */} <Card> @@ -532,7 +532,6 @@ export default function StatsPage() { </CardContent> </Card> </div> - {/* Activity Patterns */} <div className="grid gap-6 md:grid-cols-2"> {/* Hourly Activity */} @@ -583,7 +582,6 @@ export default function StatsPage() { </CardContent> </Card> </div> - {/* Asset Storage */} {stats.assetsByType.length > 0 && ( <Card> diff --git a/apps/web/app/verify-email/page.tsx b/apps/web/app/verify-email/page.tsx index da9b8b6b..7da96761 100644 --- a/apps/web/app/verify-email/page.tsx +++ b/apps/web/app/verify-email/page.tsx @@ -11,10 +11,12 @@ import { CardHeader, CardTitle, } from "@/components/ui/card"; -import { api } from "@/lib/trpc"; +import { useTRPC } from "@/lib/trpc"; +import { useMutation } from "@tanstack/react-query"; import { CheckCircle, Loader2, XCircle } from "lucide-react"; export default function VerifyEmailPage() { + const api = useTRPC(); const searchParams = useSearchParams(); const router = useRouter(); const [status, setStatus] = useState<"loading" | "success" | "error">( @@ -25,32 +27,36 @@ export default function VerifyEmailPage() { const token = searchParams.get("token"); const email = searchParams.get("email"); - const verifyEmailMutation = api.users.verifyEmail.useMutation({ - onSuccess: () => { - setStatus("success"); - setMessage( - "Your email has been successfully verified! You can now sign in.", - ); - }, - onError: (error) => { - setStatus("error"); - setMessage( - error.message || - "Failed to verify email. The link may be invalid or expired.", - ); - }, - }); + const verifyEmailMutation = useMutation( + api.users.verifyEmail.mutationOptions({ + onSuccess: () => { + setStatus("success"); + setMessage( + "Your email has been successfully verified! You can now sign in.", + ); + }, + onError: (error) => { + setStatus("error"); + setMessage( + error.message || + "Failed to verify email. The link may be invalid or expired.", + ); + }, + }), + ); - const resendEmailMutation = api.users.resendVerificationEmail.useMutation({ - onSuccess: () => { - setMessage( - "A new verification email has been sent to your email address.", - ); - }, - onError: (error) => { - setMessage(error.message || "Failed to resend verification email."); - }, - }); + const resendEmailMutation = useMutation( + api.users.resendVerificationEmail.mutationOptions({ + onSuccess: () => { + setMessage( + "A new verification email has been sent to your email address.", + ); + }, + onError: (error) => { + setMessage(error.message || "Failed to resend verification email."); + }, + }), + ); useEffect(() => { if (token && email) { diff --git a/apps/web/components/admin/AddUserDialog.tsx b/apps/web/components/admin/AddUserDialog.tsx index 3c578eca..2e29c6da 100644 --- a/apps/web/components/admin/AddUserDialog.tsx +++ b/apps/web/components/admin/AddUserDialog.tsx @@ -1,213 +1,217 @@ -import { useEffect, useState } from "react";
-import { ActionButton } from "@/components/ui/action-button";
-import { Button } from "@/components/ui/button";
-import {
- Dialog,
- DialogClose,
- DialogContent,
- DialogFooter,
- DialogHeader,
- DialogTitle,
- DialogTrigger,
-} from "@/components/ui/dialog";
-import {
- Form,
- FormControl,
- FormField,
- FormItem,
- FormLabel,
- FormMessage,
-} from "@/components/ui/form";
-import { Input } from "@/components/ui/input";
-import {
- Select,
- SelectContent,
- SelectItem,
- SelectTrigger,
- SelectValue,
-} from "@/components/ui/select";
-import { toast } from "@/components/ui/sonner";
-import { api } from "@/lib/trpc";
-import { zodResolver } from "@hookform/resolvers/zod";
-import { TRPCClientError } from "@trpc/client";
-import { useForm } from "react-hook-form";
-import { z } from "zod";
-
-import { zAdminCreateUserSchema } from "@karakeep/shared/types/admin";
-
-type AdminCreateUserSchema = z.infer<typeof zAdminCreateUserSchema>;
-
-export default function AddUserDialog({
- children,
-}: {
- children?: React.ReactNode;
-}) {
- const apiUtils = api.useUtils();
- const [isOpen, onOpenChange] = useState(false);
- const form = useForm<AdminCreateUserSchema>({
- resolver: zodResolver(zAdminCreateUserSchema),
- defaultValues: {
- name: "",
- email: "",
- password: "",
- confirmPassword: "",
- role: "user",
- },
- });
- const { mutate, isPending } = api.admin.createUser.useMutation({
- onSuccess: () => {
- toast({
- description: "User created successfully",
- });
- onOpenChange(false);
- apiUtils.users.list.invalidate();
- apiUtils.admin.userStats.invalidate();
- },
- onError: (error) => {
- if (error instanceof TRPCClientError) {
- toast({
- variant: "destructive",
- description: error.message,
- });
- } else {
- toast({
- variant: "destructive",
- description: "Failed to create user",
- });
- }
- },
- });
-
- useEffect(() => {
- if (!isOpen) {
- form.reset();
- }
- }, [isOpen, form]);
-
- return (
- <Dialog open={isOpen} onOpenChange={onOpenChange}>
- <DialogTrigger asChild>{children}</DialogTrigger>
- <DialogContent>
- <DialogHeader>
- <DialogTitle>Add User</DialogTitle>
- </DialogHeader>
- <Form {...form}>
- <form onSubmit={form.handleSubmit((val) => mutate(val))}>
- <div className="flex w-full flex-col space-y-2">
- <FormField
- control={form.control}
- name="name"
- render={({ field }) => (
- <FormItem>
- <FormLabel>Name</FormLabel>
- <FormControl>
- <Input
- type="text"
- placeholder="Name"
- {...field}
- className="w-full rounded border p-2"
- />
- </FormControl>
- <FormMessage />
- </FormItem>
- )}
- />
- <FormField
- control={form.control}
- name="email"
- render={({ field }) => (
- <FormItem>
- <FormLabel>Email</FormLabel>
- <FormControl>
- <Input
- type="email"
- placeholder="Email"
- {...field}
- className="w-full rounded border p-2"
- />
- </FormControl>
- <FormMessage />
- </FormItem>
- )}
- />
- <FormField
- control={form.control}
- name="password"
- render={({ field }) => (
- <FormItem>
- <FormLabel>Password</FormLabel>
- <FormControl>
- <Input
- type="password"
- placeholder="Password"
- {...field}
- className="w-full rounded border p-2"
- />
- </FormControl>
- <FormMessage />
- </FormItem>
- )}
- />
- <FormField
- control={form.control}
- name="confirmPassword"
- render={({ field }) => (
- <FormItem>
- <FormLabel>Confirm Password</FormLabel>
- <FormControl>
- <Input
- type="password"
- placeholder="Confirm Password"
- {...field}
- className="w-full rounded border p-2"
- />
- </FormControl>
- <FormMessage />
- </FormItem>
- )}
- />
- <FormField
- control={form.control}
- name="role"
- render={({ field }) => (
- <FormItem>
- <FormLabel>Role</FormLabel>
- <FormControl>
- <Select
- value={field.value}
- onValueChange={field.onChange}
- >
- <SelectTrigger className="w-full">
- <SelectValue />
- </SelectTrigger>
- <SelectContent>
- <SelectItem value="user">User</SelectItem>
- <SelectItem value="admin">Admin</SelectItem>
- </SelectContent>
- </Select>
- </FormControl>
- <FormMessage />
- </FormItem>
- )}
- />
- <DialogFooter className="sm:justify-end">
- <DialogClose asChild>
- <Button type="button" variant="secondary">
- Close
- </Button>
- </DialogClose>
- <ActionButton
- type="submit"
- loading={isPending}
- disabled={isPending}
- >
- Create
- </ActionButton>
- </DialogFooter>
- </div>
- </form>
- </Form>
- </DialogContent>
- </Dialog>
- );
-}
+import { useEffect, useState } from "react"; +import { ActionButton } from "@/components/ui/action-button"; +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogClose, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { toast } from "@/components/ui/sonner"; +import { useTRPC } from "@/lib/trpc"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { TRPCClientError } from "@trpc/client"; +import { useForm } from "react-hook-form"; +import { z } from "zod"; + +import { zAdminCreateUserSchema } from "@karakeep/shared/types/admin"; + +type AdminCreateUserSchema = z.infer<typeof zAdminCreateUserSchema>; + +export default function AddUserDialog({ + children, +}: { + children?: React.ReactNode; +}) { + const api = useTRPC(); + const queryClient = useQueryClient(); + const [isOpen, onOpenChange] = useState(false); + const form = useForm<AdminCreateUserSchema>({ + resolver: zodResolver(zAdminCreateUserSchema), + defaultValues: { + name: "", + email: "", + password: "", + confirmPassword: "", + role: "user", + }, + }); + const { mutate, isPending } = useMutation( + api.admin.createUser.mutationOptions({ + onSuccess: () => { + toast({ + description: "User created successfully", + }); + onOpenChange(false); + queryClient.invalidateQueries(api.users.list.pathFilter()); + queryClient.invalidateQueries(api.admin.userStats.pathFilter()); + }, + onError: (error) => { + if (error instanceof TRPCClientError) { + toast({ + variant: "destructive", + description: error.message, + }); + } else { + toast({ + variant: "destructive", + description: "Failed to create user", + }); + } + }, + }), + ); + + useEffect(() => { + if (!isOpen) { + form.reset(); + } + }, [isOpen, form]); + + return ( + <Dialog open={isOpen} onOpenChange={onOpenChange}> + <DialogTrigger asChild>{children}</DialogTrigger> + <DialogContent> + <DialogHeader> + <DialogTitle>Add User</DialogTitle> + </DialogHeader> + <Form {...form}> + <form onSubmit={form.handleSubmit((val) => mutate(val))}> + <div className="flex w-full flex-col space-y-2"> + <FormField + control={form.control} + name="name" + render={({ field }) => ( + <FormItem> + <FormLabel>Name</FormLabel> + <FormControl> + <Input + type="text" + placeholder="Name" + {...field} + className="w-full rounded border p-2" + /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + <FormField + control={form.control} + name="email" + render={({ field }) => ( + <FormItem> + <FormLabel>Email</FormLabel> + <FormControl> + <Input + type="email" + placeholder="Email" + {...field} + className="w-full rounded border p-2" + /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + <FormField + control={form.control} + name="password" + render={({ field }) => ( + <FormItem> + <FormLabel>Password</FormLabel> + <FormControl> + <Input + type="password" + placeholder="Password" + {...field} + className="w-full rounded border p-2" + /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + <FormField + control={form.control} + name="confirmPassword" + render={({ field }) => ( + <FormItem> + <FormLabel>Confirm Password</FormLabel> + <FormControl> + <Input + type="password" + placeholder="Confirm Password" + {...field} + className="w-full rounded border p-2" + /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + <FormField + control={form.control} + name="role" + render={({ field }) => ( + <FormItem> + <FormLabel>Role</FormLabel> + <FormControl> + <Select + value={field.value} + onValueChange={field.onChange} + > + <SelectTrigger className="w-full"> + <SelectValue /> + </SelectTrigger> + <SelectContent> + <SelectItem value="user">User</SelectItem> + <SelectItem value="admin">Admin</SelectItem> + </SelectContent> + </Select> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + <DialogFooter className="sm:justify-end"> + <DialogClose asChild> + <Button type="button" variant="secondary"> + Close + </Button> + </DialogClose> + <ActionButton + type="submit" + loading={isPending} + disabled={isPending} + > + Create + </ActionButton> + </DialogFooter> + </div> + </form> + </Form> + </DialogContent> + </Dialog> + ); +} diff --git a/apps/web/components/admin/AdminNotices.tsx b/apps/web/components/admin/AdminNotices.tsx index 77b1b481..e9d9a692 100644 --- a/apps/web/components/admin/AdminNotices.tsx +++ b/apps/web/components/admin/AdminNotices.tsx @@ -2,7 +2,8 @@ import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; import { Badge } from "@/components/ui/badge"; -import { api } from "@/lib/trpc"; +import { useTRPC } from "@/lib/trpc"; +import { useQuery } from "@tanstack/react-query"; import { AlertCircle } from "lucide-react"; import { AdminCard } from "./AdminCard"; @@ -14,7 +15,8 @@ interface AdminNotice { } function useAdminNotices() { - const { data } = api.admin.getAdminNoticies.useQuery(); + const api = useTRPC(); + const { data } = useQuery(api.admin.getAdminNoticies.queryOptions()); if (!data) { return []; } diff --git a/apps/web/components/admin/BackgroundJobs.tsx b/apps/web/components/admin/BackgroundJobs.tsx index 382069c8..3dab3c54 100644 --- a/apps/web/components/admin/BackgroundJobs.tsx +++ b/apps/web/components/admin/BackgroundJobs.tsx @@ -13,8 +13,8 @@ import { import { Skeleton } from "@/components/ui/skeleton"; import { toast } from "@/components/ui/sonner"; import { useTranslation } from "@/lib/i18n/client"; -import { api } from "@/lib/trpc"; -import { keepPreviousData } from "@tanstack/react-query"; +import { useTRPC } from "@/lib/trpc"; +import { keepPreviousData, useMutation, useQuery } from "@tanstack/react-query"; import { Activity, AlertTriangle, @@ -254,13 +254,51 @@ function JobCard({ } function useJobActions() { + const api = useTRPC(); const { t } = useTranslation(); const { mutateAsync: recrawlLinks, isPending: isRecrawlPending } = - api.admin.recrawlLinks.useMutation({ + useMutation( + api.admin.recrawlLinks.mutationOptions({ + onSuccess: () => { + toast({ + description: "Recrawl enqueued", + }); + }, + onError: (e) => { + toast({ + variant: "destructive", + description: e.message, + }); + }, + }), + ); + + const { mutateAsync: reindexBookmarks, isPending: isReindexPending } = + useMutation( + api.admin.reindexAllBookmarks.mutationOptions({ + onSuccess: () => { + toast({ + description: "Reindex enqueued", + }); + }, + onError: (e) => { + toast({ + variant: "destructive", + description: e.message, + }); + }, + }), + ); + + const { + mutateAsync: reprocessAssetsFixMode, + isPending: isReprocessingPending, + } = useMutation( + api.admin.reprocessAssetsFixMode.mutationOptions({ onSuccess: () => { toast({ - description: "Recrawl enqueued", + description: "Reprocessing enqueued", }); }, onError: (e) => { @@ -269,13 +307,17 @@ function useJobActions() { description: e.message, }); }, - }); + }), + ); - const { mutateAsync: reindexBookmarks, isPending: isReindexPending } = - api.admin.reindexAllBookmarks.useMutation({ + const { + mutateAsync: reRunInferenceOnAllBookmarks, + isPending: isInferencePending, + } = useMutation( + api.admin.reRunInferenceOnAllBookmarks.mutationOptions({ onSuccess: () => { toast({ - description: "Reindex enqueued", + description: "Inference jobs enqueued", }); }, onError: (e) => { @@ -284,58 +326,27 @@ function useJobActions() { description: e.message, }); }, - }); - - const { - mutateAsync: reprocessAssetsFixMode, - isPending: isReprocessingPending, - } = api.admin.reprocessAssetsFixMode.useMutation({ - onSuccess: () => { - toast({ - description: "Reprocessing enqueued", - }); - }, - onError: (e) => { - toast({ - variant: "destructive", - description: e.message, - }); - }, - }); - - const { - mutateAsync: reRunInferenceOnAllBookmarks, - isPending: isInferencePending, - } = api.admin.reRunInferenceOnAllBookmarks.useMutation({ - onSuccess: () => { - toast({ - description: "Inference jobs enqueued", - }); - }, - onError: (e) => { - toast({ - variant: "destructive", - description: e.message, - }); - }, - }); + }), + ); const { mutateAsync: runAdminMaintenanceTask, isPending: isAdminMaintenancePending, - } = api.admin.runAdminMaintenanceTask.useMutation({ - onSuccess: () => { - toast({ - description: "Admin maintenance request has been enqueued!", - }); - }, - onError: (e) => { - toast({ - variant: "destructive", - description: e.message, - }); - }, - }); + } = useMutation( + api.admin.runAdminMaintenanceTask.mutationOptions({ + onSuccess: () => { + toast({ + description: "Admin maintenance request has been enqueued!", + }); + }, + onError: (e) => { + toast({ + variant: "destructive", + description: e.message, + }); + }, + }), + ); return { crawlActions: [ @@ -466,13 +477,13 @@ function useJobActions() { } export default function BackgroundJobs() { + const api = useTRPC(); const { t } = useTranslation(); - const { data: serverStats } = api.admin.backgroundJobsStats.useQuery( - undefined, - { + const { data: serverStats } = useQuery( + api.admin.backgroundJobsStats.queryOptions(undefined, { refetchInterval: 1000, placeholderData: keepPreviousData, - }, + }), ); const actions = useJobActions(); diff --git a/apps/web/components/admin/BasicStats.tsx b/apps/web/components/admin/BasicStats.tsx index 67352f66..9c88ba83 100644 --- a/apps/web/components/admin/BasicStats.tsx +++ b/apps/web/components/admin/BasicStats.tsx @@ -3,7 +3,7 @@ import { AdminCard } from "@/components/admin/AdminCard"; import { useClientConfig } from "@/lib/clientConfig"; import { useTranslation } from "@/lib/i18n/client"; -import { api } from "@/lib/trpc"; +import { useTRPC } from "@/lib/trpc"; import { useQuery } from "@tanstack/react-query"; const REPO_LATEST_RELEASE_API = @@ -42,7 +42,7 @@ function ReleaseInfo() { rel="noreferrer" title="Update available" > - ({latestRelease} ⬆️) + ({latestRelease}⬆️) </a> ); } @@ -71,10 +71,13 @@ function StatsSkeleton() { } export default function BasicStats() { + const api = useTRPC(); const { t } = useTranslation(); - const { data: serverStats } = api.admin.stats.useQuery(undefined, { - refetchInterval: 5000, - }); + const { data: serverStats } = useQuery( + api.admin.stats.queryOptions(undefined, { + refetchInterval: 5000, + }), + ); if (!serverStats) { return <StatsSkeleton />; diff --git a/apps/web/components/admin/BookmarkDebugger.tsx b/apps/web/components/admin/BookmarkDebugger.tsx index 1628fdcc..78eb2c85 100644 --- a/apps/web/components/admin/BookmarkDebugger.tsx +++ b/apps/web/components/admin/BookmarkDebugger.tsx @@ -8,8 +8,9 @@ import { Button } from "@/components/ui/button"; import InfoTooltip from "@/components/ui/info-tooltip"; import { Input } from "@/components/ui/input"; import { useTranslation } from "@/lib/i18n/client"; -import { api } from "@/lib/trpc"; +import { useTRPC } from "@/lib/trpc"; import { formatBytes } from "@/lib/utils"; +import { useMutation, useQuery } from "@tanstack/react-query"; import { formatDistanceToNow } from "date-fns"; import { AlertCircle, @@ -37,6 +38,7 @@ import { toast } from "sonner"; import { BookmarkTypes } from "@karakeep/shared/types/bookmarks"; export default function BookmarkDebugger() { + const api = useTRPC(); const { t } = useTranslation(); const [inputValue, setInputValue] = useState(""); const [bookmarkId, setBookmarkId] = useQueryState( @@ -56,9 +58,11 @@ export default function BookmarkDebugger() { data: debugInfo, isLoading, error, - } = api.admin.getBookmarkDebugInfo.useQuery( - { bookmarkId: bookmarkId }, - { enabled: !!bookmarkId && bookmarkId.length > 0 }, + } = useQuery( + api.admin.getBookmarkDebugInfo.queryOptions( + { bookmarkId: bookmarkId }, + { enabled: !!bookmarkId && bookmarkId.length > 0 }, + ), ); const handleLookup = () => { @@ -67,57 +71,65 @@ export default function BookmarkDebugger() { } }; - const recrawlMutation = api.admin.adminRecrawlBookmark.useMutation({ - onSuccess: () => { - toast.success(t("admin.admin_tools.action_success"), { - description: t("admin.admin_tools.recrawl_queued"), - }); - }, - onError: (error) => { - toast.error(t("admin.admin_tools.action_failed"), { - description: error.message, - }); - }, - }); + const recrawlMutation = useMutation( + api.admin.adminRecrawlBookmark.mutationOptions({ + onSuccess: () => { + toast.success(t("admin.admin_tools.action_success"), { + description: t("admin.admin_tools.recrawl_queued"), + }); + }, + onError: (error) => { + toast.error(t("admin.admin_tools.action_failed"), { + description: error.message, + }); + }, + }), + ); - const reindexMutation = api.admin.adminReindexBookmark.useMutation({ - onSuccess: () => { - toast.success(t("admin.admin_tools.action_success"), { - description: t("admin.admin_tools.reindex_queued"), - }); - }, - onError: (error) => { - toast.error(t("admin.admin_tools.action_failed"), { - description: error.message, - }); - }, - }); + const reindexMutation = useMutation( + api.admin.adminReindexBookmark.mutationOptions({ + onSuccess: () => { + toast.success(t("admin.admin_tools.action_success"), { + description: t("admin.admin_tools.reindex_queued"), + }); + }, + onError: (error) => { + toast.error(t("admin.admin_tools.action_failed"), { + description: error.message, + }); + }, + }), + ); - const retagMutation = api.admin.adminRetagBookmark.useMutation({ - onSuccess: () => { - toast.success(t("admin.admin_tools.action_success"), { - description: t("admin.admin_tools.retag_queued"), - }); - }, - onError: (error) => { - toast.error(t("admin.admin_tools.action_failed"), { - description: error.message, - }); - }, - }); + const retagMutation = useMutation( + api.admin.adminRetagBookmark.mutationOptions({ + onSuccess: () => { + toast.success(t("admin.admin_tools.action_success"), { + description: t("admin.admin_tools.retag_queued"), + }); + }, + onError: (error) => { + toast.error(t("admin.admin_tools.action_failed"), { + description: error.message, + }); + }, + }), + ); - const resummarizeMutation = api.admin.adminResummarizeBookmark.useMutation({ - onSuccess: () => { - toast.success(t("admin.admin_tools.action_success"), { - description: t("admin.admin_tools.resummarize_queued"), - }); - }, - onError: (error) => { - toast.error(t("admin.admin_tools.action_failed"), { - description: error.message, - }); - }, - }); + const resummarizeMutation = useMutation( + api.admin.adminResummarizeBookmark.mutationOptions({ + onSuccess: () => { + toast.success(t("admin.admin_tools.action_success"), { + description: t("admin.admin_tools.resummarize_queued"), + }); + }, + onError: (error) => { + toast.error(t("admin.admin_tools.action_failed"), { + description: error.message, + }); + }, + }), + ); const handleRecrawl = () => { if (bookmarkId) { diff --git a/apps/web/components/admin/CreateInviteDialog.tsx b/apps/web/components/admin/CreateInviteDialog.tsx index 6738adc9..c8b6be8c 100644 --- a/apps/web/components/admin/CreateInviteDialog.tsx +++ b/apps/web/components/admin/CreateInviteDialog.tsx @@ -20,8 +20,9 @@ import { } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; import { toast } from "@/components/ui/sonner"; -import { api } from "@/lib/trpc"; +import { useTRPC } from "@/lib/trpc"; import { zodResolver } from "@hookform/resolvers/zod"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; import { TRPCClientError } from "@trpc/client"; import { useForm } from "react-hook-form"; import { z } from "zod"; @@ -37,6 +38,8 @@ interface CreateInviteDialogProps { export default function CreateInviteDialog({ children, }: CreateInviteDialogProps) { + const api = useTRPC(); + const queryClient = useQueryClient(); const [open, setOpen] = useState(false); const [errorMessage, setErrorMessage] = useState(""); @@ -47,25 +50,26 @@ export default function CreateInviteDialog({ }, }); - const invalidateInvitesList = api.useUtils().invites.list.invalidate; - const createInviteMutation = api.invites.create.useMutation({ - onSuccess: () => { - toast({ - description: "Invite sent successfully", - }); - invalidateInvitesList(); - setOpen(false); - form.reset(); - setErrorMessage(""); - }, - onError: (e) => { - if (e instanceof TRPCClientError) { - setErrorMessage(e.message); - } else { - setErrorMessage("Failed to send invite"); - } - }, - }); + const createInviteMutation = useMutation( + api.invites.create.mutationOptions({ + onSuccess: () => { + toast({ + description: "Invite sent successfully", + }); + queryClient.invalidateQueries(api.invites.list.pathFilter()); + setOpen(false); + form.reset(); + setErrorMessage(""); + }, + onError: (e) => { + if (e instanceof TRPCClientError) { + setErrorMessage(e.message); + } else { + setErrorMessage("Failed to send invite"); + } + }, + }), + ); return ( <Dialog open={open} onOpenChange={setOpen}> diff --git a/apps/web/components/admin/InvitesList.tsx b/apps/web/components/admin/InvitesList.tsx index 75d29748..9d94f0a0 100644 --- a/apps/web/components/admin/InvitesList.tsx +++ b/apps/web/components/admin/InvitesList.tsx @@ -11,7 +11,12 @@ import { TableHeader, TableRow, } from "@/components/ui/table"; -import { api } from "@/lib/trpc"; +import { useTRPC } from "@/lib/trpc"; +import { + useMutation, + useQueryClient, + useSuspenseQuery, +} from "@tanstack/react-query"; import { formatDistanceToNow } from "date-fns"; import { Mail, MailX, UserPlus } from "lucide-react"; @@ -20,16 +25,17 @@ import { AdminCard } from "./AdminCard"; import CreateInviteDialog from "./CreateInviteDialog"; export default function InvitesList() { - const invalidateInvitesList = api.useUtils().invites.list.invalidate; - const [invites] = api.invites.list.useSuspenseQuery(); + const api = useTRPC(); + const queryClient = useQueryClient(); + const { data: invites } = useSuspenseQuery(api.invites.list.queryOptions()); - const { mutateAsync: revokeInvite, isPending: isRevokePending } = - api.invites.revoke.useMutation({ + const { mutateAsync: revokeInvite, isPending: isRevokePending } = useMutation( + api.invites.revoke.mutationOptions({ onSuccess: () => { toast({ description: "Invite revoked successfully", }); - invalidateInvitesList(); + queryClient.invalidateQueries(api.invites.list.pathFilter()); }, onError: (e) => { toast({ @@ -37,15 +43,16 @@ export default function InvitesList() { description: `Failed to revoke invite: ${e.message}`, }); }, - }); + }), + ); - const { mutateAsync: resendInvite, isPending: isResendPending } = - api.invites.resend.useMutation({ + const { mutateAsync: resendInvite, isPending: isResendPending } = useMutation( + api.invites.resend.mutationOptions({ onSuccess: () => { toast({ description: "Invite resent successfully", }); - invalidateInvitesList(); + queryClient.invalidateQueries(api.invites.list.pathFilter()); }, onError: (e) => { toast({ @@ -53,7 +60,8 @@ export default function InvitesList() { description: `Failed to resend invite: ${e.message}`, }); }, - }); + }), + ); const activeInvites = invites?.invites || []; diff --git a/apps/web/components/admin/ResetPasswordDialog.tsx b/apps/web/components/admin/ResetPasswordDialog.tsx index 4e71d42b..59886d54 100644 --- a/apps/web/components/admin/ResetPasswordDialog.tsx +++ b/apps/web/components/admin/ResetPasswordDialog.tsx @@ -1,145 +1,149 @@ -import { useEffect, useState } from "react";
-import { ActionButton } from "@/components/ui/action-button";
-import { Button } from "@/components/ui/button";
-import {
- Dialog,
- DialogClose,
- DialogContent,
- DialogFooter,
- DialogHeader,
- DialogTitle,
- DialogTrigger,
-} from "@/components/ui/dialog";
-import {
- Form,
- FormControl,
- FormField,
- FormItem,
- FormLabel,
- FormMessage,
-} from "@/components/ui/form";
-import { Input } from "@/components/ui/input";
-import { toast } from "@/components/ui/sonner";
-import { api } from "@/lib/trpc"; // Adjust the import path as needed
-import { zodResolver } from "@hookform/resolvers/zod";
-import { TRPCClientError } from "@trpc/client";
-import { useForm } from "react-hook-form";
-import { z } from "zod";
-
-import { resetPasswordSchema } from "@karakeep/shared/types/admin";
-
-interface ResetPasswordDialogProps {
- userId: string;
- children?: React.ReactNode;
-}
-
-type ResetPasswordSchema = z.infer<typeof resetPasswordSchema>;
-
-export default function ResetPasswordDialog({
- children,
- userId,
-}: ResetPasswordDialogProps) {
- const [isOpen, onOpenChange] = useState(false);
- const form = useForm<ResetPasswordSchema>({
- resolver: zodResolver(resetPasswordSchema),
- defaultValues: {
- userId,
- newPassword: "",
- newPasswordConfirm: "",
- },
- });
- const { mutate, isPending } = api.admin.resetPassword.useMutation({
- onSuccess: () => {
- toast({
- description: "Password reset successfully",
- });
- onOpenChange(false);
- },
- onError: (error) => {
- if (error instanceof TRPCClientError) {
- toast({
- variant: "destructive",
- description: error.message,
- });
- } else {
- toast({
- variant: "destructive",
- description: "Failed to reset password",
- });
- }
- },
- });
-
- useEffect(() => {
- if (isOpen) {
- form.reset();
- }
- }, [isOpen, form]);
-
- return (
- <Dialog open={isOpen} onOpenChange={onOpenChange}>
- <DialogTrigger asChild>{children}</DialogTrigger>
- <DialogContent>
- <DialogHeader>
- <DialogTitle>Reset Password</DialogTitle>
- </DialogHeader>
- <Form {...form}>
- <form onSubmit={form.handleSubmit((val) => mutate(val))}>
- <div className="flex w-full flex-col space-y-2">
- <FormField
- control={form.control}
- name="newPassword"
- render={({ field }) => (
- <FormItem>
- <FormLabel>New Password</FormLabel>
- <FormControl>
- <Input
- type="password"
- placeholder="New Password"
- {...field}
- className="w-full rounded border p-2"
- />
- </FormControl>
- <FormMessage />
- </FormItem>
- )}
- />
- <FormField
- control={form.control}
- name="newPasswordConfirm"
- render={({ field }) => (
- <FormItem>
- <FormLabel>Confirm New Password</FormLabel>
- <FormControl>
- <Input
- type="password"
- placeholder="Confirm New Password"
- {...field}
- className="w-full rounded border p-2"
- />
- </FormControl>
- <FormMessage />
- </FormItem>
- )}
- />
- <DialogFooter className="sm:justify-end">
- <DialogClose asChild>
- <Button type="button" variant="secondary">
- Close
- </Button>
- </DialogClose>
- <ActionButton
- type="submit"
- loading={isPending}
- disabled={isPending}
- >
- Reset
- </ActionButton>
- </DialogFooter>
- </div>
- </form>
- </Form>
- </DialogContent>
- </Dialog>
- );
-}
+import { useEffect, useState } from "react"; +import { ActionButton } from "@/components/ui/action-button"; +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogClose, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { toast } from "@/components/ui/sonner"; +import { useTRPC } from "@/lib/trpc"; // Adjust the import path as needed +import { zodResolver } from "@hookform/resolvers/zod"; +import { useMutation } from "@tanstack/react-query"; +import { TRPCClientError } from "@trpc/client"; +import { useForm } from "react-hook-form"; +import { z } from "zod"; + +import { resetPasswordSchema } from "@karakeep/shared/types/admin"; + +interface ResetPasswordDialogProps { + userId: string; + children?: React.ReactNode; +} + +type ResetPasswordSchema = z.infer<typeof resetPasswordSchema>; + +export default function ResetPasswordDialog({ + children, + userId, +}: ResetPasswordDialogProps) { + const api = useTRPC(); + const [isOpen, onOpenChange] = useState(false); + const form = useForm<ResetPasswordSchema>({ + resolver: zodResolver(resetPasswordSchema), + defaultValues: { + userId, + newPassword: "", + newPasswordConfirm: "", + }, + }); + const { mutate, isPending } = useMutation( + api.admin.resetPassword.mutationOptions({ + onSuccess: () => { + toast({ + description: "Password reset successfully", + }); + onOpenChange(false); + }, + onError: (error) => { + if (error instanceof TRPCClientError) { + toast({ + variant: "destructive", + description: error.message, + }); + } else { + toast({ + variant: "destructive", + description: "Failed to reset password", + }); + } + }, + }), + ); + + useEffect(() => { + if (isOpen) { + form.reset(); + } + }, [isOpen, form]); + + return ( + <Dialog open={isOpen} onOpenChange={onOpenChange}> + <DialogTrigger asChild>{children}</DialogTrigger> + <DialogContent> + <DialogHeader> + <DialogTitle>Reset Password</DialogTitle> + </DialogHeader> + <Form {...form}> + <form onSubmit={form.handleSubmit((val) => mutate(val))}> + <div className="flex w-full flex-col space-y-2"> + <FormField + control={form.control} + name="newPassword" + render={({ field }) => ( + <FormItem> + <FormLabel>New Password</FormLabel> + <FormControl> + <Input + type="password" + placeholder="New Password" + {...field} + className="w-full rounded border p-2" + /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + <FormField + control={form.control} + name="newPasswordConfirm" + render={({ field }) => ( + <FormItem> + <FormLabel>Confirm New Password</FormLabel> + <FormControl> + <Input + type="password" + placeholder="Confirm New Password" + {...field} + className="w-full rounded border p-2" + /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + <DialogFooter className="sm:justify-end"> + <DialogClose asChild> + <Button type="button" variant="secondary"> + Close + </Button> + </DialogClose> + <ActionButton + type="submit" + loading={isPending} + disabled={isPending} + > + Reset + </ActionButton> + </DialogFooter> + </div> + </form> + </Form> + </DialogContent> + </Dialog> + ); +} diff --git a/apps/web/components/admin/ServiceConnections.tsx b/apps/web/components/admin/ServiceConnections.tsx index 8d79d8bb..0509f352 100644 --- a/apps/web/components/admin/ServiceConnections.tsx +++ b/apps/web/components/admin/ServiceConnections.tsx @@ -2,7 +2,8 @@ import { AdminCard } from "@/components/admin/AdminCard"; import { useTranslation } from "@/lib/i18n/client"; -import { api } from "@/lib/trpc"; +import { useTRPC } from "@/lib/trpc"; +import { useQuery } from "@tanstack/react-query"; function ConnectionStatus({ label, @@ -105,10 +106,13 @@ function ConnectionsSkeleton() { } export default function ServiceConnections() { + const api = useTRPC(); const { t } = useTranslation(); - const { data: connections } = api.admin.checkConnections.useQuery(undefined, { - refetchInterval: 10000, - }); + const { data: connections } = useQuery( + api.admin.checkConnections.queryOptions(undefined, { + refetchInterval: 10000, + }), + ); if (!connections) { return <ConnectionsSkeleton />; diff --git a/apps/web/components/admin/UpdateUserDialog.tsx b/apps/web/components/admin/UpdateUserDialog.tsx index 453f4fab..aeec9d4e 100644 --- a/apps/web/components/admin/UpdateUserDialog.tsx +++ b/apps/web/components/admin/UpdateUserDialog.tsx @@ -27,8 +27,9 @@ import { SelectValue, } from "@/components/ui/select"; import { toast } from "@/components/ui/sonner"; -import { api } from "@/lib/trpc"; +import { useTRPC } from "@/lib/trpc"; import { zodResolver } from "@hookform/resolvers/zod"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; import { TRPCClientError } from "@trpc/client"; import { useForm } from "react-hook-form"; import { z } from "zod"; @@ -51,7 +52,8 @@ export default function UpdateUserDialog({ currentStorageQuota, children, }: UpdateUserDialogProps) { - const apiUtils = api.useUtils(); + const api = useTRPC(); + const queryClient = useQueryClient(); const [isOpen, onOpenChange] = useState(false); const defaultValues = { userId, @@ -63,28 +65,30 @@ export default function UpdateUserDialog({ resolver: zodResolver(updateUserSchema), defaultValues, }); - const { mutate, isPending } = api.admin.updateUser.useMutation({ - onSuccess: () => { - toast({ - description: "User updated successfully", - }); - apiUtils.users.list.invalidate(); - onOpenChange(false); - }, - onError: (error) => { - if (error instanceof TRPCClientError) { + const { mutate, isPending } = useMutation( + api.admin.updateUser.mutationOptions({ + onSuccess: () => { toast({ - variant: "destructive", - description: error.message, + description: "User updated successfully", }); - } else { - toast({ - variant: "destructive", - description: "Failed to update user", - }); - } - }, - }); + queryClient.invalidateQueries(api.users.list.pathFilter()); + onOpenChange(false); + }, + onError: (error) => { + if (error instanceof TRPCClientError) { + toast({ + variant: "destructive", + description: error.message, + }); + } else { + toast({ + variant: "destructive", + description: "Failed to update user", + }); + } + }, + }), + ); useEffect(() => { if (isOpen) { diff --git a/apps/web/components/admin/UserList.tsx b/apps/web/components/admin/UserList.tsx index 69f9e3b9..810a945f 100644 --- a/apps/web/components/admin/UserList.tsx +++ b/apps/web/components/admin/UserList.tsx @@ -13,7 +13,12 @@ import { } from "@/components/ui/table"; import { useSession } from "@/lib/auth/client"; import { useTranslation } from "@/lib/i18n/client"; -import { api } from "@/lib/trpc"; +import { useTRPC } from "@/lib/trpc"; +import { + useMutation, + useQueryClient, + useSuspenseQuery, +} from "@tanstack/react-query"; import { Check, KeyRound, Pencil, Trash, UserPlus, X } from "lucide-react"; import ActionConfirmingDialog from "../ui/action-confirming-dialog"; @@ -30,18 +35,23 @@ function toHumanReadableSize(size: number) { } export default function UsersSection() { + const api = useTRPC(); + const queryClient = useQueryClient(); const { t } = useTranslation(); const { data: session } = useSession(); - const invalidateUserList = api.useUtils().users.list.invalidate; - const [{ users }] = api.users.list.useSuspenseQuery(); - const [userStats] = api.admin.userStats.useSuspenseQuery(); - const { mutateAsync: deleteUser, isPending: isDeletionPending } = - api.users.delete.useMutation({ + const { + data: { users }, + } = useSuspenseQuery(api.users.list.queryOptions()); + const { data: userStats } = useSuspenseQuery( + api.admin.userStats.queryOptions(), + ); + const { mutateAsync: deleteUser, isPending: isDeletionPending } = useMutation( + api.users.delete.mutationOptions({ onSuccess: () => { toast({ description: "User deleted", }); - invalidateUserList(); + queryClient.invalidateQueries(api.users.list.pathFilter()); }, onError: (e) => { toast({ @@ -49,7 +59,8 @@ export default function UsersSection() { description: `Something went wrong: ${e.message}`, }); }, - }); + }), + ); return ( <AdminCard> diff --git a/apps/web/components/dashboard/bookmarks/BookmarkCard.tsx b/apps/web/components/dashboard/bookmarks/BookmarkCard.tsx index 595a9e00..4d2b58e7 100644 --- a/apps/web/components/dashboard/bookmarks/BookmarkCard.tsx +++ b/apps/web/components/dashboard/bookmarks/BookmarkCard.tsx @@ -1,4 +1,5 @@ -import { api } from "@/lib/trpc"; +import { useTRPC } from "@/lib/trpc"; +import { useQuery } from "@tanstack/react-query"; import { BookmarkTypes, ZBookmark } from "@karakeep/shared/types/bookmarks"; import { getBookmarkRefreshInterval } from "@karakeep/shared/utils/bookmarkUtils"; @@ -15,20 +16,23 @@ export default function BookmarkCard({ bookmark: ZBookmark; className?: string; }) { - const { data: bookmark } = api.bookmarks.getBookmark.useQuery( - { - bookmarkId: initialData.id, - }, - { - initialData, - refetchInterval: (query) => { - const data = query.state.data; - if (!data) { - return false; - } - return getBookmarkRefreshInterval(data); + const api = useTRPC(); + const { data: bookmark } = useQuery( + api.bookmarks.getBookmark.queryOptions( + { + bookmarkId: initialData.id, }, - }, + { + initialData, + refetchInterval: (query) => { + const data = query.state.data; + if (!data) { + return false; + } + return getBookmarkRefreshInterval(data); + }, + }, + ), ); switch (bookmark.content.type) { diff --git a/apps/web/components/dashboard/bookmarks/BookmarkLayoutAdaptingCard.tsx b/apps/web/components/dashboard/bookmarks/BookmarkLayoutAdaptingCard.tsx index 3e27dbcb..82387325 100644 --- a/apps/web/components/dashboard/bookmarks/BookmarkLayoutAdaptingCard.tsx +++ b/apps/web/components/dashboard/bookmarks/BookmarkLayoutAdaptingCard.tsx @@ -7,13 +7,14 @@ import Image from "next/image"; import Link from "next/link"; import { useSession } from "@/lib/auth/client"; import useBulkActionsStore from "@/lib/bulkActions"; -import { api } from "@/lib/trpc"; +import { useTRPC } from "@/lib/trpc"; import { bookmarkLayoutSwitch, useBookmarkDisplaySettings, useBookmarkLayout, } from "@/lib/userLocalSettings/bookmarksLayout"; import { cn } from "@/lib/utils"; +import { useQuery } from "@tanstack/react-query"; import { Check, Image as ImageIcon, NotebookPen } from "lucide-react"; import { useTheme } from "next-themes"; @@ -64,15 +65,18 @@ function BottomRow({ } function OwnerIndicator({ bookmark }: { bookmark: ZBookmark }) { + const api = useTRPC(); const listContext = useBookmarkListContext(); - const collaborators = api.lists.getCollaborators.useQuery( - { - listId: listContext?.id ?? "", - }, - { - refetchOnWindowFocus: false, - enabled: !!listContext?.hasCollaborators, - }, + const collaborators = useQuery( + api.lists.getCollaborators.queryOptions( + { + listId: listContext?.id ?? "", + }, + { + refetchOnWindowFocus: false, + enabled: !!listContext?.hasCollaborators, + }, + ), ); if (!listContext || listContext.userRole === "owner" || !collaborators.data) { diff --git a/apps/web/components/dashboard/bookmarks/BulkTagModal.tsx b/apps/web/components/dashboard/bookmarks/BulkTagModal.tsx index 53d2d013..96cf1fed 100644 --- a/apps/web/components/dashboard/bookmarks/BulkTagModal.tsx +++ b/apps/web/components/dashboard/bookmarks/BulkTagModal.tsx @@ -8,9 +8,10 @@ import { DialogTitle, } from "@/components/ui/dialog"; import { toast } from "@/components/ui/sonner"; +import { useTRPC } from "@/lib/trpc"; +import { useQueries } from "@tanstack/react-query"; import { useUpdateBookmarkTags } from "@karakeep/shared-react/hooks/bookmarks"; -import { api } from "@karakeep/shared-react/trpc"; import { limitConcurrency } from "@karakeep/shared/concurrency"; import { ZBookmark } from "@karakeep/shared/types/bookmarks"; @@ -25,9 +26,12 @@ export default function BulkTagModal({ open: boolean; setOpen: (open: boolean) => void; }) { - const results = api.useQueries((t) => - bookmarkIds.map((id) => t.bookmarks.getBookmark({ bookmarkId: id })), - ); + const api = useTRPC(); + const results = useQueries({ + queries: bookmarkIds.map((id) => + api.bookmarks.getBookmark.queryOptions({ bookmarkId: id }), + ), + }); const bookmarks = results .map((r) => r.data) diff --git a/apps/web/components/dashboard/bookmarks/EditBookmarkDialog.tsx b/apps/web/components/dashboard/bookmarks/EditBookmarkDialog.tsx index de6c1ff6..922cea2a 100644 --- a/apps/web/components/dashboard/bookmarks/EditBookmarkDialog.tsx +++ b/apps/web/components/dashboard/bookmarks/EditBookmarkDialog.tsx @@ -29,9 +29,10 @@ import { toast } from "@/components/ui/sonner"; import { Textarea } from "@/components/ui/textarea"; import { useDialogFormReset } from "@/lib/hooks/useDialogFormReset"; import { useTranslation } from "@/lib/i18n/client"; -import { api } from "@/lib/trpc"; +import { useTRPC } from "@/lib/trpc"; import { cn } from "@/lib/utils"; import { zodResolver } from "@hookform/resolvers/zod"; +import { useQuery } from "@tanstack/react-query"; import { format } from "date-fns"; import { CalendarIcon } from "lucide-react"; import { useForm } from "react-hook-form"; @@ -60,10 +61,11 @@ export function EditBookmarkDialog({ open: boolean; setOpen: (v: boolean) => void; }) { + const api = useTRPC(); const { t } = useTranslation(); - const { data: assetContent, isLoading: isAssetContentLoading } = - api.bookmarks.getBookmark.useQuery( + const { data: assetContent, isLoading: isAssetContentLoading } = useQuery( + api.bookmarks.getBookmark.queryOptions( { bookmarkId: bookmark.id, includeContent: true, @@ -73,7 +75,8 @@ export function EditBookmarkDialog({ select: (b) => b.content.type == BookmarkTypes.ASSET ? b.content.content : null, }, - ); + ), + ); const bookmarkToDefault = (bookmark: ZBookmark) => ({ bookmarkId: bookmark.id, diff --git a/apps/web/components/dashboard/bookmarks/ManageListsModal.tsx b/apps/web/components/dashboard/bookmarks/ManageListsModal.tsx index 34d797a6..ee92dc5a 100644 --- a/apps/web/components/dashboard/bookmarks/ManageListsModal.tsx +++ b/apps/web/components/dashboard/bookmarks/ManageListsModal.tsx @@ -19,8 +19,9 @@ import { import { toast } from "@/components/ui/sonner"; import LoadingSpinner from "@/components/ui/spinner"; import { useTranslation } from "@/lib/i18n/client"; -import { api } from "@/lib/trpc"; +import { useTRPC } from "@/lib/trpc"; import { zodResolver } from "@hookform/resolvers/zod"; +import { useQuery } from "@tanstack/react-query"; import { Archive, X } from "lucide-react"; import { useForm } from "react-hook-form"; import { z } from "zod"; @@ -43,6 +44,7 @@ export default function ManageListsModal({ open: boolean; setOpen: (open: boolean) => void; }) { + const api = useTRPC(); const { t } = useTranslation(); const formSchema = z.object({ listId: z.string({ @@ -61,13 +63,14 @@ export default function ManageListsModal({ { enabled: open }, ); - const { data: alreadyInList, isPending: isAlreadyInListPending } = - api.lists.getListsOfBookmark.useQuery( + const { data: alreadyInList, isPending: isAlreadyInListPending } = useQuery( + api.lists.getListsOfBookmark.queryOptions( { bookmarkId, }, { enabled: open }, - ); + ), + ); const isLoading = isAllListsPending || isAlreadyInListPending; diff --git a/apps/web/components/dashboard/bookmarks/TagsEditor.tsx b/apps/web/components/dashboard/bookmarks/TagsEditor.tsx index bc06c647..a23f06ed 100644 --- a/apps/web/components/dashboard/bookmarks/TagsEditor.tsx +++ b/apps/web/components/dashboard/bookmarks/TagsEditor.tsx @@ -13,9 +13,9 @@ import { PopoverTrigger, } from "@/components/ui/popover"; import { useClientConfig } from "@/lib/clientConfig"; -import { api } from "@/lib/trpc"; +import { useTRPC } from "@/lib/trpc"; import { cn } from "@/lib/utils"; -import { keepPreviousData } from "@tanstack/react-query"; +import { keepPreviousData, useQuery } from "@tanstack/react-query"; import { Command as CommandPrimitive } from "cmdk"; import { Check, Loader2, Plus, Sparkles, X } from "lucide-react"; @@ -32,6 +32,7 @@ export function TagsEditor({ onDetach: (tag: { tagName: string; tagId: string }) => void; disabled?: boolean; }) { + const api = useTRPC(); const demoMode = !!useClientConfig().demoMode; const isDisabled = demoMode || disabled; const inputRef = React.useRef<HTMLInputElement>(null); @@ -71,8 +72,8 @@ export function TagsEditor({ }); }, [_tags]); - const { data: filteredOptions, isLoading: isExistingTagsLoading } = - api.tags.list.useQuery( + const { data: filteredOptions, isLoading: isExistingTagsLoading } = useQuery( + api.tags.list.queryOptions( { nameContains: inputValue, limit: 50, @@ -91,7 +92,8 @@ export function TagsEditor({ placeholderData: keepPreviousData, gcTime: inputValue.length > 0 ? 60_000 : 3_600_000, }, - ); + ), + ); const selectedValues = optimisticTags.map((tag) => tag.id); diff --git a/apps/web/components/dashboard/bookmarks/UpdatableBookmarksGrid.tsx b/apps/web/components/dashboard/bookmarks/UpdatableBookmarksGrid.tsx index 968d0326..817d975d 100644 --- a/apps/web/components/dashboard/bookmarks/UpdatableBookmarksGrid.tsx +++ b/apps/web/components/dashboard/bookmarks/UpdatableBookmarksGrid.tsx @@ -3,7 +3,8 @@ import { useEffect } from "react"; import UploadDropzone from "@/components/dashboard/UploadDropzone"; import { useSortOrderStore } from "@/lib/store/useSortOrderStore"; -import { api } from "@/lib/trpc"; +import { useTRPC } from "@/lib/trpc"; +import { useInfiniteQuery } from "@tanstack/react-query"; import type { ZGetBookmarksRequest, @@ -23,6 +24,7 @@ export default function UpdatableBookmarksGrid({ showEditorCard?: boolean; itemsPerPage?: number; }) { + const api = useTRPC(); let sortOrder = useSortOrderStore((state) => state.sortOrder); if (sortOrder === "relevance") { // Relevance is not supported in the `getBookmarks` endpoint. @@ -32,17 +34,19 @@ export default function UpdatableBookmarksGrid({ const finalQuery = { ...query, sortOrder, includeContent: false }; const { data, fetchNextPage, hasNextPage, isFetchingNextPage, refetch } = - api.bookmarks.getBookmarks.useInfiniteQuery( - { ...finalQuery, useCursorV2: true }, - { - initialData: () => ({ - pages: [initialBookmarks], - pageParams: [query.cursor], - }), - initialCursor: null, - getNextPageParam: (lastPage) => lastPage.nextCursor, - refetchOnMount: true, - }, + useInfiniteQuery( + api.bookmarks.getBookmarks.infiniteQueryOptions( + { ...finalQuery, useCursorV2: true }, + { + initialData: () => ({ + pages: [initialBookmarks], + pageParams: [query.cursor ?? null], + }), + initialCursor: null, + getNextPageParam: (lastPage) => lastPage.nextCursor, + refetchOnMount: true, + }, + ), ); useEffect(() => { diff --git a/apps/web/components/dashboard/bookmarks/action-buttons/ArchiveBookmarkButton.tsx b/apps/web/components/dashboard/bookmarks/action-buttons/ArchiveBookmarkButton.tsx index fd2780cd..4fd503c1 100644 --- a/apps/web/components/dashboard/bookmarks/action-buttons/ArchiveBookmarkButton.tsx +++ b/apps/web/components/dashboard/bookmarks/action-buttons/ArchiveBookmarkButton.tsx @@ -1,7 +1,8 @@ import React from "react"; import { ActionButton, ActionButtonProps } from "@/components/ui/action-button"; import { toast } from "@/components/ui/sonner"; -import { api } from "@/lib/trpc"; +import { useTRPC } from "@/lib/trpc"; +import { useQuery } from "@tanstack/react-query"; import { useUpdateBookmark } from "@karakeep/shared-react/hooks/bookmarks"; @@ -15,13 +16,16 @@ const ArchiveBookmarkButton = React.forwardRef< HTMLButtonElement, ArchiveBookmarkButtonProps >(({ bookmarkId, onDone, ...props }, ref) => { - const { data } = api.bookmarks.getBookmark.useQuery( - { bookmarkId }, - { - select: (data) => ({ - archived: data.archived, - }), - }, + const api = useTRPC(); + const { data } = useQuery( + api.bookmarks.getBookmark.queryOptions( + { bookmarkId }, + { + select: (data) => ({ + archived: data.archived, + }), + }, + ), ); const { mutate: updateBookmark, isPending: isArchivingBookmark } = diff --git a/apps/web/components/dashboard/cleanups/TagDuplicationDetention.tsx b/apps/web/components/dashboard/cleanups/TagDuplicationDetention.tsx index 89aff598..dde72457 100644 --- a/apps/web/components/dashboard/cleanups/TagDuplicationDetention.tsx +++ b/apps/web/components/dashboard/cleanups/TagDuplicationDetention.tsx @@ -22,8 +22,9 @@ import { TableRow, } from "@/components/ui/table"; import { useTranslation } from "@/lib/i18n/client"; -import { api } from "@/lib/trpc"; +import { useTRPC } from "@/lib/trpc"; import { cn } from "@/lib/utils"; +import { useQuery } from "@tanstack/react-query"; import { distance } from "fastest-levenshtein"; import { Check, Combine, X } from "lucide-react"; @@ -199,12 +200,15 @@ function SuggestionRow({ } export function TagDuplicationDetection() { + const api = useTRPC(); const [expanded, setExpanded] = useState(false); - let { data: allTags } = api.tags.list.useQuery( - {}, - { - refetchOnWindowFocus: false, - }, + let { data: allTags } = useQuery( + api.tags.list.queryOptions( + {}, + { + refetchOnWindowFocus: false, + }, + ), ); const { suggestions, updateMergeInto, setSuggestions, deleteSuggestion } = diff --git a/apps/web/components/dashboard/feeds/FeedSelector.tsx b/apps/web/components/dashboard/feeds/FeedSelector.tsx index db95a042..740dc345 100644 --- a/apps/web/components/dashboard/feeds/FeedSelector.tsx +++ b/apps/web/components/dashboard/feeds/FeedSelector.tsx @@ -7,8 +7,9 @@ import { SelectValue, } from "@/components/ui/select"; import LoadingSpinner from "@/components/ui/spinner"; -import { api } from "@/lib/trpc"; +import { useTRPC } from "@/lib/trpc"; import { cn } from "@/lib/utils"; +import { useQuery } from "@tanstack/react-query"; export function FeedSelector({ value, @@ -21,9 +22,12 @@ export function FeedSelector({ onChange: (value: string) => void; placeholder?: string; }) { - const { data, isPending } = api.feeds.list.useQuery(undefined, { - select: (data) => data.feeds, - }); + const api = useTRPC(); + const { data, isPending } = useQuery( + api.feeds.list.queryOptions(undefined, { + select: (data) => data.feeds, + }), + ); if (isPending) { return <LoadingSpinner />; diff --git a/apps/web/components/dashboard/highlights/AllHighlights.tsx b/apps/web/components/dashboard/highlights/AllHighlights.tsx index 928f4e05..3965d06a 100644 --- a/apps/web/components/dashboard/highlights/AllHighlights.tsx +++ b/apps/web/components/dashboard/highlights/AllHighlights.tsx @@ -5,8 +5,9 @@ import Link from "next/link"; import { ActionButton } from "@/components/ui/action-button"; import { Input } from "@/components/ui/input"; import useRelativeTime from "@/lib/hooks/relative-time"; -import { api } from "@/lib/trpc"; +import { useTRPC } from "@/lib/trpc"; import { Separator } from "@radix-ui/react-dropdown-menu"; +import { useInfiniteQuery } from "@tanstack/react-query"; import dayjs from "dayjs"; import relativeTime from "dayjs/plugin/relativeTime"; import { Dot, LinkIcon, Search, X } from "lucide-react"; @@ -49,6 +50,7 @@ export default function AllHighlights({ }: { highlights: ZGetAllHighlightsResponse; }) { + const api = useTRPC(); const { t } = useTranslation(); const [searchInput, setSearchInput] = useState(""); const debouncedSearch = useDebounce(searchInput, 300); @@ -56,28 +58,32 @@ export default function AllHighlights({ // Use search endpoint if searchQuery is provided, otherwise use getAll const useSearchQuery = debouncedSearch.trim().length > 0; - const getAllQuery = api.highlights.getAll.useInfiniteQuery( - {}, - { - enabled: !useSearchQuery, - initialData: !useSearchQuery - ? () => ({ - pages: [initialHighlights], - pageParams: [null], - }) - : undefined, - initialCursor: null, - getNextPageParam: (lastPage) => lastPage.nextCursor, - }, + const getAllQuery = useInfiniteQuery( + api.highlights.getAll.infiniteQueryOptions( + {}, + { + enabled: !useSearchQuery, + initialData: !useSearchQuery + ? () => ({ + pages: [initialHighlights], + pageParams: [null], + }) + : undefined, + initialCursor: null, + getNextPageParam: (lastPage) => lastPage.nextCursor, + }, + ), ); - const searchQueryResult = api.highlights.search.useInfiniteQuery( - { text: debouncedSearch }, - { - enabled: useSearchQuery, - initialCursor: null, - getNextPageParam: (lastPage) => lastPage.nextCursor, - }, + const searchQueryResult = useInfiniteQuery( + api.highlights.search.infiniteQueryOptions( + { text: debouncedSearch }, + { + enabled: useSearchQuery, + initialCursor: null, + getNextPageParam: (lastPage) => lastPage.nextCursor, + }, + ), ); const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = diff --git a/apps/web/components/dashboard/lists/CollapsibleBookmarkLists.tsx b/apps/web/components/dashboard/lists/CollapsibleBookmarkLists.tsx index 2bb5f41b..626d0757 100644 --- a/apps/web/components/dashboard/lists/CollapsibleBookmarkLists.tsx +++ b/apps/web/components/dashboard/lists/CollapsibleBookmarkLists.tsx @@ -1,8 +1,8 @@ import { useEffect, useState } from "react"; import { Collapsible, CollapsibleContent } from "@/components/ui/collapsible"; import { FullPageSpinner } from "@/components/ui/full-page-spinner"; -import { api } from "@/lib/trpc"; -import { keepPreviousData } from "@tanstack/react-query"; +import { useTRPC } from "@/lib/trpc"; +import { keepPreviousData, useQuery } from "@tanstack/react-query"; import { useBookmarkLists } from "@karakeep/shared-react/hooks/lists"; import { ZBookmarkList } from "@karakeep/shared/types/lists"; @@ -101,6 +101,7 @@ export function CollapsibleBookmarkLists({ filter?: (node: ZBookmarkListTreeNode) => boolean; indentOffset?: number; }) { + const api = useTRPC(); // If listsData is provided, use it directly. Otherwise, fetch it. let { data: fetchedData } = useBookmarkLists(undefined, { initialData: initialData ? { lists: initialData } : undefined, @@ -108,9 +109,11 @@ export function CollapsibleBookmarkLists({ }); const data = listsData || fetchedData; - const { data: listStats } = api.lists.stats.useQuery(undefined, { - placeholderData: keepPreviousData, - }); + const { data: listStats } = useQuery( + api.lists.stats.queryOptions(undefined, { + placeholderData: keepPreviousData, + }), + ); if (!data) { return <FullPageSpinner />; diff --git a/apps/web/components/dashboard/lists/LeaveListConfirmationDialog.tsx b/apps/web/components/dashboard/lists/LeaveListConfirmationDialog.tsx index f2a48062..79d23b6a 100644 --- a/apps/web/components/dashboard/lists/LeaveListConfirmationDialog.tsx +++ b/apps/web/components/dashboard/lists/LeaveListConfirmationDialog.tsx @@ -4,7 +4,8 @@ import { ActionButton } from "@/components/ui/action-button"; import ActionConfirmingDialog from "@/components/ui/action-confirming-dialog"; import { toast } from "@/components/ui/sonner"; import { useTranslation } from "@/lib/i18n/client"; -import { api } from "@/lib/trpc"; +import { useTRPC } from "@/lib/trpc"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; import type { ZBookmarkList } from "@karakeep/shared/types/lists"; @@ -19,34 +20,37 @@ export default function LeaveListConfirmationDialog({ open: boolean; setOpen: (v: boolean) => void; }) { + const api = useTRPC(); const { t } = useTranslation(); const currentPath = usePathname(); const router = useRouter(); - const utils = api.useUtils(); + const queryClient = useQueryClient(); - const { mutate: leaveList, isPending } = api.lists.leaveList.useMutation({ - onSuccess: () => { - toast({ - description: t("lists.leave_list.success", { - icon: list.icon, - name: list.name, - }), - }); - setOpen(false); - // Invalidate the lists cache - utils.lists.list.invalidate(); - // If currently viewing this list, redirect to lists page - if (currentPath.includes(list.id)) { - router.push("/dashboard/lists"); - } - }, - onError: (error) => { - toast({ - variant: "destructive", - description: error.message || t("common.something_went_wrong"), - }); - }, - }); + const { mutate: leaveList, isPending } = useMutation( + api.lists.leaveList.mutationOptions({ + onSuccess: () => { + toast({ + description: t("lists.leave_list.success", { + icon: list.icon, + name: list.name, + }), + }); + setOpen(false); + // Invalidate the lists cache + queryClient.invalidateQueries(api.lists.list.pathFilter()); + // If currently viewing this list, redirect to lists page + if (currentPath.includes(list.id)) { + router.push("/dashboard/lists"); + } + }, + onError: (error) => { + toast({ + variant: "destructive", + description: error.message || t("common.something_went_wrong"), + }); + }, + }), + ); return ( <ActionConfirmingDialog diff --git a/apps/web/components/dashboard/lists/ListHeader.tsx b/apps/web/components/dashboard/lists/ListHeader.tsx index ecbb6431..4176a80e 100644 --- a/apps/web/components/dashboard/lists/ListHeader.tsx +++ b/apps/web/components/dashboard/lists/ListHeader.tsx @@ -10,9 +10,10 @@ import { } from "@/components/ui/tooltip"; import { UserAvatar } from "@/components/ui/user-avatar"; import { useTranslation } from "@/lib/i18n/client"; +import { useQuery } from "@tanstack/react-query"; import { MoreHorizontal, SearchIcon } from "lucide-react"; -import { api } from "@karakeep/shared-react/trpc"; +import { useTRPC } from "@karakeep/shared-react/trpc"; import { parseSearchQuery } from "@karakeep/shared/searchQueryParser"; import { ZBookmarkList } from "@karakeep/shared/types/lists"; @@ -24,25 +25,30 @@ export default function ListHeader({ }: { initialData: ZBookmarkList; }) { + const api = useTRPC(); const { t } = useTranslation(); const router = useRouter(); - const { data: list, error } = api.lists.get.useQuery( - { - listId: initialData.id, - }, - { - initialData, - }, + const { data: list, error } = useQuery( + api.lists.get.queryOptions( + { + listId: initialData.id, + }, + { + initialData, + }, + ), ); - const { data: collaboratorsData } = api.lists.getCollaborators.useQuery( - { - listId: initialData.id, - }, - { - refetchOnWindowFocus: false, - enabled: list.hasCollaborators, - }, + const { data: collaboratorsData } = useQuery( + api.lists.getCollaborators.queryOptions( + { + listId: initialData.id, + }, + { + refetchOnWindowFocus: false, + enabled: list.hasCollaborators, + }, + ), ); const parsedQuery = useMemo(() => { diff --git a/apps/web/components/dashboard/lists/ManageCollaboratorsModal.tsx b/apps/web/components/dashboard/lists/ManageCollaboratorsModal.tsx index 6c5dac1e..354e0dfe 100644 --- a/apps/web/components/dashboard/lists/ManageCollaboratorsModal.tsx +++ b/apps/web/components/dashboard/lists/ManageCollaboratorsModal.tsx @@ -25,7 +25,8 @@ import { import { toast } from "@/components/ui/sonner"; import { UserAvatar } from "@/components/ui/user-avatar"; import { useTranslation } from "@/lib/i18n/client"; -import { api } from "@/lib/trpc"; +import { useTRPC } from "@/lib/trpc"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { Loader2, Trash2, UserPlus, Users } from "lucide-react"; import { ZBookmarkList } from "@karakeep/shared/types/lists"; @@ -43,6 +44,7 @@ export function ManageCollaboratorsModal({ children?: React.ReactNode; readOnly?: boolean; }) { + const api = useTRPC(); if ( (userOpen !== undefined && !userSetOpen) || (userOpen === undefined && userSetOpen) @@ -61,82 +63,102 @@ export function ManageCollaboratorsModal({ >("viewer"); const { t } = useTranslation(); - const utils = api.useUtils(); + const queryClient = useQueryClient(); const invalidateListCaches = () => Promise.all([ - utils.lists.getCollaborators.invalidate({ listId: list.id }), - utils.lists.get.invalidate({ listId: list.id }), - utils.lists.list.invalidate(), - utils.bookmarks.getBookmarks.invalidate({ listId: list.id }), + queryClient.invalidateQueries( + api.lists.getCollaborators.queryFilter({ listId: list.id }), + ), + queryClient.invalidateQueries( + api.lists.get.queryFilter({ listId: list.id }), + ), + queryClient.invalidateQueries(api.lists.list.pathFilter()), + queryClient.invalidateQueries( + api.bookmarks.getBookmarks.queryFilter({ listId: list.id }), + ), ]); // Fetch collaborators - const { data: collaboratorsData, isLoading } = - api.lists.getCollaborators.useQuery({ listId: list.id }, { enabled: open }); + const { data: collaboratorsData, isLoading } = useQuery( + api.lists.getCollaborators.queryOptions( + { listId: list.id }, + { enabled: open }, + ), + ); // Mutations - const addCollaborator = api.lists.addCollaborator.useMutation({ - onSuccess: async () => { - toast({ - description: t("lists.collaborators.invitation_sent"), - }); - setNewCollaboratorEmail(""); - await invalidateListCaches(); - }, - onError: (error) => { - toast({ - variant: "destructive", - description: error.message || t("lists.collaborators.failed_to_add"), - }); - }, - }); + const addCollaborator = useMutation( + api.lists.addCollaborator.mutationOptions({ + onSuccess: async () => { + toast({ + description: t("lists.collaborators.invitation_sent"), + }); + setNewCollaboratorEmail(""); + await invalidateListCaches(); + }, + onError: (error) => { + toast({ + variant: "destructive", + description: error.message || t("lists.collaborators.failed_to_add"), + }); + }, + }), + ); - const removeCollaborator = api.lists.removeCollaborator.useMutation({ - onSuccess: async () => { - toast({ - description: t("lists.collaborators.removed"), - }); - await invalidateListCaches(); - }, - onError: (error) => { - toast({ - variant: "destructive", - description: error.message || t("lists.collaborators.failed_to_remove"), - }); - }, - }); + const removeCollaborator = useMutation( + api.lists.removeCollaborator.mutationOptions({ + onSuccess: async () => { + toast({ + description: t("lists.collaborators.removed"), + }); + await invalidateListCaches(); + }, + onError: (error) => { + toast({ + variant: "destructive", + description: + error.message || t("lists.collaborators.failed_to_remove"), + }); + }, + }), + ); - const updateCollaboratorRole = api.lists.updateCollaboratorRole.useMutation({ - onSuccess: async () => { - toast({ - description: t("lists.collaborators.role_updated"), - }); - await invalidateListCaches(); - }, - onError: (error) => { - toast({ - variant: "destructive", - description: - error.message || t("lists.collaborators.failed_to_update_role"), - }); - }, - }); + const updateCollaboratorRole = useMutation( + api.lists.updateCollaboratorRole.mutationOptions({ + onSuccess: async () => { + toast({ + description: t("lists.collaborators.role_updated"), + }); + await invalidateListCaches(); + }, + onError: (error) => { + toast({ + variant: "destructive", + description: + error.message || t("lists.collaborators.failed_to_update_role"), + }); + }, + }), + ); - const revokeInvitation = api.lists.revokeInvitation.useMutation({ - onSuccess: async () => { - toast({ - description: t("lists.collaborators.invitation_revoked"), - }); - await invalidateListCaches(); - }, - onError: (error) => { - toast({ - variant: "destructive", - description: error.message || t("lists.collaborators.failed_to_revoke"), - }); - }, - }); + const revokeInvitation = useMutation( + api.lists.revokeInvitation.mutationOptions({ + onSuccess: async () => { + toast({ + description: t("lists.collaborators.invitation_revoked"), + }); + await invalidateListCaches(); + }, + onError: (error) => { + toast({ + variant: "destructive", + description: + error.message || t("lists.collaborators.failed_to_revoke"), + }); + }, + }), + ); const handleAddCollaborator = () => { if (!newCollaboratorEmail.trim()) { diff --git a/apps/web/components/dashboard/lists/PendingInvitationsCard.tsx b/apps/web/components/dashboard/lists/PendingInvitationsCard.tsx index 95a916ff..5d70daaf 100644 --- a/apps/web/components/dashboard/lists/PendingInvitationsCard.tsx +++ b/apps/web/components/dashboard/lists/PendingInvitationsCard.tsx @@ -10,7 +10,8 @@ import { } from "@/components/ui/card"; import { toast } from "@/components/ui/sonner"; import { useTranslation } from "@/lib/i18n/client"; -import { api } from "@/lib/trpc"; +import { useTRPC } from "@/lib/trpc"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { Check, Loader2, Mail, X } from "lucide-react"; interface Invitation { @@ -27,41 +28,51 @@ interface Invitation { } function InvitationRow({ invitation }: { invitation: Invitation }) { + const api = useTRPC(); const { t } = useTranslation(); - const utils = api.useUtils(); + const queryClient = useQueryClient(); - const acceptInvitation = api.lists.acceptInvitation.useMutation({ - onSuccess: async () => { - toast({ - description: t("lists.invitations.accepted"), - }); - await Promise.all([ - utils.lists.getPendingInvitations.invalidate(), - utils.lists.list.invalidate(), - ]); - }, - onError: (error) => { - toast({ - variant: "destructive", - description: error.message || t("lists.invitations.failed_to_accept"), - }); - }, - }); + const acceptInvitation = useMutation( + api.lists.acceptInvitation.mutationOptions({ + onSuccess: async () => { + toast({ + description: t("lists.invitations.accepted"), + }); + await Promise.all([ + queryClient.invalidateQueries( + api.lists.getPendingInvitations.pathFilter(), + ), + queryClient.invalidateQueries(api.lists.list.pathFilter()), + ]); + }, + onError: (error) => { + toast({ + variant: "destructive", + description: error.message || t("lists.invitations.failed_to_accept"), + }); + }, + }), + ); - const declineInvitation = api.lists.declineInvitation.useMutation({ - onSuccess: async () => { - toast({ - description: t("lists.invitations.declined"), - }); - await utils.lists.getPendingInvitations.invalidate(); - }, - onError: (error) => { - toast({ - variant: "destructive", - description: error.message || t("lists.invitations.failed_to_decline"), - }); - }, - }); + const declineInvitation = useMutation( + api.lists.declineInvitation.mutationOptions({ + onSuccess: async () => { + toast({ + description: t("lists.invitations.declined"), + }); + await queryClient.invalidateQueries( + api.lists.getPendingInvitations.pathFilter(), + ); + }, + onError: (error) => { + toast({ + variant: "destructive", + description: + error.message || t("lists.invitations.failed_to_decline"), + }); + }, + }), + ); return ( <div className="flex items-center justify-between rounded-lg border p-4"> @@ -126,10 +137,12 @@ function InvitationRow({ invitation }: { invitation: Invitation }) { } export function PendingInvitationsCard() { + const api = useTRPC(); const { t } = useTranslation(); - const { data: invitations, isLoading } = - api.lists.getPendingInvitations.useQuery(); + const { data: invitations, isLoading } = useQuery( + api.lists.getPendingInvitations.queryOptions(), + ); if (isLoading) { return null; diff --git a/apps/web/components/dashboard/lists/RssLink.tsx b/apps/web/components/dashboard/lists/RssLink.tsx index 1be48681..5bc0fdf0 100644 --- a/apps/web/components/dashboard/lists/RssLink.tsx +++ b/apps/web/components/dashboard/lists/RssLink.tsx @@ -7,29 +7,38 @@ import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Switch } from "@/components/ui/switch"; import { useClientConfig } from "@/lib/clientConfig"; -import { api } from "@/lib/trpc"; +import { useTRPC } from "@/lib/trpc"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { Loader2, RotateCcw } from "lucide-react"; import { useTranslation } from "react-i18next"; export default function RssLink({ listId }: { listId: string }) { + const api = useTRPC(); const { t } = useTranslation(); const clientConfig = useClientConfig(); - const apiUtils = api.useUtils(); + const queryClient = useQueryClient(); - const { mutate: regenRssToken, isPending: isRegenPending } = - api.lists.regenRssToken.useMutation({ + const { mutate: regenRssToken, isPending: isRegenPending } = useMutation( + api.lists.regenRssToken.mutationOptions({ onSuccess: () => { - apiUtils.lists.getRssToken.invalidate({ listId }); + queryClient.invalidateQueries( + api.lists.getRssToken.queryFilter({ listId }), + ); }, - }); - const { mutate: clearRssToken, isPending: isClearPending } = - api.lists.clearRssToken.useMutation({ + }), + ); + const { mutate: clearRssToken, isPending: isClearPending } = useMutation( + api.lists.clearRssToken.mutationOptions({ onSuccess: () => { - apiUtils.lists.getRssToken.invalidate({ listId }); + queryClient.invalidateQueries( + api.lists.getRssToken.queryFilter({ listId }), + ); }, - }); - const { data: rssToken, isLoading: isTokenLoading } = - api.lists.getRssToken.useQuery({ listId }); + }), + ); + const { data: rssToken, isLoading: isTokenLoading } = useQuery( + api.lists.getRssToken.queryOptions({ listId }), + ); const rssUrl = useMemo(() => { if (!rssToken || !rssToken.token) { diff --git a/apps/web/components/dashboard/preview/BookmarkPreview.tsx b/apps/web/components/dashboard/preview/BookmarkPreview.tsx index d56bfb6a..b9b8ff81 100644 --- a/apps/web/components/dashboard/preview/BookmarkPreview.tsx +++ b/apps/web/components/dashboard/preview/BookmarkPreview.tsx @@ -16,7 +16,8 @@ import { import { useSession } from "@/lib/auth/client"; import useRelativeTime from "@/lib/hooks/relative-time"; import { useTranslation } from "@/lib/i18n/client"; -import { api } from "@/lib/trpc"; +import { useTRPC } from "@/lib/trpc"; +import { useQuery } from "@tanstack/react-query"; import { Building, CalendarDays, ExternalLink, User } from "lucide-react"; import { BookmarkTypes, ZBookmark } from "@karakeep/shared/types/bookmarks"; @@ -116,24 +117,27 @@ export default function BookmarkPreview({ bookmarkId: string; initialData?: ZBookmark; }) { + const api = useTRPC(); const { t } = useTranslation(); const [activeTab, setActiveTab] = useState<string>("content"); const { data: session } = useSession(); - const { data: bookmark } = api.bookmarks.getBookmark.useQuery( - { - bookmarkId, - }, - { - initialData, - refetchInterval: (query) => { - const data = query.state.data; - if (!data) { - return false; - } - return getBookmarkRefreshInterval(data); + const { data: bookmark } = useQuery( + api.bookmarks.getBookmark.queryOptions( + { + bookmarkId, }, - }, + { + initialData, + refetchInterval: (query) => { + const data = query.state.data; + if (!data) { + return false; + } + return getBookmarkRefreshInterval(data); + }, + }, + ), ); if (!bookmark) { diff --git a/apps/web/components/dashboard/preview/HighlightsBox.tsx b/apps/web/components/dashboard/preview/HighlightsBox.tsx index 41ab7d74..d4821655 100644 --- a/apps/web/components/dashboard/preview/HighlightsBox.tsx +++ b/apps/web/components/dashboard/preview/HighlightsBox.tsx @@ -5,8 +5,9 @@ import { CollapsibleTrigger, } from "@/components/ui/collapsible"; import { useTranslation } from "@/lib/i18n/client"; -import { api } from "@/lib/trpc"; +import { useTRPC } from "@/lib/trpc"; import { Separator } from "@radix-ui/react-dropdown-menu"; +import { useQuery } from "@tanstack/react-query"; import { ChevronsDownUp } from "lucide-react"; import HighlightCard from "../highlights/HighlightCard"; @@ -18,10 +19,12 @@ export default function HighlightsBox({ bookmarkId: string; readOnly: boolean; }) { + const api = useTRPC(); const { t } = useTranslation(); - const { data: highlights, isPending: isLoading } = - api.highlights.getForBookmark.useQuery({ bookmarkId }); + const { data: highlights, isPending: isLoading } = useQuery( + api.highlights.getForBookmark.queryOptions({ bookmarkId }), + ); if (isLoading || !highlights || highlights?.highlights.length === 0) { return null; diff --git a/apps/web/components/dashboard/preview/ReaderView.tsx b/apps/web/components/dashboard/preview/ReaderView.tsx index 9b765d55..4d9bcd6c 100644 --- a/apps/web/components/dashboard/preview/ReaderView.tsx +++ b/apps/web/components/dashboard/preview/ReaderView.tsx @@ -1,6 +1,7 @@ import { FullPageSpinner } from "@/components/ui/full-page-spinner"; import { toast } from "@/components/ui/sonner"; -import { api } from "@/lib/trpc"; +import { useTRPC } from "@/lib/trpc"; +import { useQuery } from "@tanstack/react-query"; import { useCreateHighlight, @@ -22,11 +23,14 @@ export default function ReaderView({ style?: React.CSSProperties; readOnly: boolean; }) { - const { data: highlights } = api.highlights.getForBookmark.useQuery({ - bookmarkId, - }); - const { data: cachedContent, isPending: isCachedContentLoading } = - api.bookmarks.getBookmark.useQuery( + const api = useTRPC(); + const { data: highlights } = useQuery( + api.highlights.getForBookmark.queryOptions({ + bookmarkId, + }), + ); + const { data: cachedContent, isPending: isCachedContentLoading } = useQuery( + api.bookmarks.getBookmark.queryOptions( { bookmarkId, includeContent: true, @@ -37,7 +41,8 @@ export default function ReaderView({ ? data.content.htmlContent : null, }, - ); + ), + ); const { mutate: createHighlight } = useCreateHighlight({ onSuccess: () => { diff --git a/apps/web/components/dashboard/search/useSearchAutocomplete.ts b/apps/web/components/dashboard/search/useSearchAutocomplete.ts index 7ddfa39a..380a79b6 100644 --- a/apps/web/components/dashboard/search/useSearchAutocomplete.ts +++ b/apps/web/components/dashboard/search/useSearchAutocomplete.ts @@ -2,7 +2,8 @@ import type translation from "@/lib/i18n/locales/en/translation.json"; import type { TFunction } from "i18next"; import type { LucideIcon } from "lucide-react"; import { useCallback, useMemo } from "react"; -import { api } from "@/lib/trpc"; +import { useTRPC } from "@/lib/trpc"; +import { useQuery } from "@tanstack/react-query"; import { History, ListTree, @@ -293,6 +294,7 @@ const useTagSuggestions = ( const useFeedSuggestions = ( parsed: ParsedSearchState, ): AutocompleteSuggestionItem[] => { + const api = useTRPC(); const shouldSuggestFeeds = parsed.normalizedTokenWithoutMinus.startsWith("feed:"); const feedSearchTermRaw = shouldSuggestFeeds @@ -300,9 +302,11 @@ const useFeedSuggestions = ( : ""; const feedSearchTerm = stripSurroundingQuotes(feedSearchTermRaw); const normalizedFeedSearchTerm = feedSearchTerm.toLowerCase(); - const { data: feedResults } = api.feeds.list.useQuery(undefined, { - enabled: parsed.activeToken.length > 0, - }); + const { data: feedResults } = useQuery( + api.feeds.list.queryOptions(undefined, { + enabled: parsed.activeToken.length > 0, + }), + ); const feedSuggestions = useMemo<AutocompleteSuggestionItem[]>(() => { if (!shouldSuggestFeeds) { diff --git a/apps/web/components/dashboard/sidebar/InvitationNotificationBadge.tsx b/apps/web/components/dashboard/sidebar/InvitationNotificationBadge.tsx index e4d7b39f..547b8a76 100644 --- a/apps/web/components/dashboard/sidebar/InvitationNotificationBadge.tsx +++ b/apps/web/components/dashboard/sidebar/InvitationNotificationBadge.tsx @@ -1,13 +1,14 @@ "use client"; -import { api } from "@/lib/trpc"; +import { useTRPC } from "@/lib/trpc"; +import { useQuery } from "@tanstack/react-query"; export function InvitationNotificationBadge() { - const { data: pendingInvitations } = api.lists.getPendingInvitations.useQuery( - undefined, - { + const api = useTRPC(); + const { data: pendingInvitations } = useQuery( + api.lists.getPendingInvitations.queryOptions(undefined, { refetchInterval: 1000 * 60 * 5, - }, + }), ); const pendingInvitationsCount = pendingInvitations?.length ?? 0; diff --git a/apps/web/components/dashboard/tags/TagAutocomplete.tsx b/apps/web/components/dashboard/tags/TagAutocomplete.tsx index 8164dc81..656d4c5a 100644 --- a/apps/web/components/dashboard/tags/TagAutocomplete.tsx +++ b/apps/web/components/dashboard/tags/TagAutocomplete.tsx @@ -15,11 +15,12 @@ import { } from "@/components/ui/popover"; import LoadingSpinner from "@/components/ui/spinner"; import { cn } from "@/lib/utils"; +import { useQuery } from "@tanstack/react-query"; import { Check, ChevronsUpDown, X } from "lucide-react"; import { useTagAutocomplete } from "@karakeep/shared-react/hooks/tags"; import { useDebounce } from "@karakeep/shared-react/hooks/use-debounce"; -import { api } from "@karakeep/shared-react/trpc"; +import { useTRPC } from "@karakeep/shared-react/trpc"; interface TagAutocompleteProps { tagId: string; @@ -32,6 +33,7 @@ export function TagAutocomplete({ onChange, className, }: TagAutocompleteProps) { + const api = useTRPC(); const [open, setOpen] = useState(false); const [searchQuery, setSearchQuery] = useState(""); const searchQueryDebounced = useDebounce(searchQuery, 500); @@ -41,8 +43,8 @@ export function TagAutocomplete({ select: (data) => data.tags, }); - const { data: selectedTag, isLoading: isSelectedTagLoading } = - api.tags.get.useQuery( + const { data: selectedTag, isLoading: isSelectedTagLoading } = useQuery( + api.tags.get.queryOptions( { tagId, }, @@ -53,7 +55,8 @@ export function TagAutocomplete({ }), enabled: !!tagId, }, - ); + ), + ); const handleSelect = (currentValue: string) => { setOpen(false); diff --git a/apps/web/components/invite/InviteAcceptForm.tsx b/apps/web/components/invite/InviteAcceptForm.tsx index 5fa166c0..9ad4de1c 100644 --- a/apps/web/components/invite/InviteAcceptForm.tsx +++ b/apps/web/components/invite/InviteAcceptForm.tsx @@ -22,8 +22,9 @@ import { } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; import { signIn } from "@/lib/auth/client"; -import { api } from "@/lib/trpc"; +import { useTRPC } from "@/lib/trpc"; import { zodResolver } from "@hookform/resolvers/zod"; +import { useMutation, useQuery } from "@tanstack/react-query"; import { TRPCClientError } from "@trpc/client"; import { AlertCircle, Clock, Loader2, Mail, UserPlus } from "lucide-react"; import { useForm } from "react-hook-form"; @@ -47,6 +48,7 @@ interface InviteAcceptFormProps { } export default function InviteAcceptForm({ token }: InviteAcceptFormProps) { + const api = useTRPC(); const router = useRouter(); const form = useForm<z.infer<typeof inviteAcceptSchema>>({ @@ -59,7 +61,7 @@ export default function InviteAcceptForm({ token }: InviteAcceptFormProps) { isPending: loading, data: inviteData, error, - } = api.invites.get.useQuery({ token }); + } = useQuery(api.invites.get.queryOptions({ token })); useEffect(() => { if (error) { @@ -67,7 +69,9 @@ export default function InviteAcceptForm({ token }: InviteAcceptFormProps) { } }, [error]); - const acceptInviteMutation = api.invites.accept.useMutation(); + const acceptInviteMutation = useMutation( + api.invites.accept.mutationOptions(), + ); const handleBackToSignIn = () => { router.push("/signin"); diff --git a/apps/web/components/public/lists/PublicBookmarkGrid.tsx b/apps/web/components/public/lists/PublicBookmarkGrid.tsx index 18e42baa..9832c5b1 100644 --- a/apps/web/components/public/lists/PublicBookmarkGrid.tsx +++ b/apps/web/components/public/lists/PublicBookmarkGrid.tsx @@ -9,9 +9,10 @@ import { ActionButton } from "@/components/ui/action-button"; import { badgeVariants } from "@/components/ui/badge"; import { Card, CardContent } from "@/components/ui/card"; import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog"; -import { api } from "@/lib/trpc"; +import { useTRPC } from "@/lib/trpc"; import { cn } from "@/lib/utils"; import tailwindConfig from "@/tailwind.config"; +import { useInfiniteQuery } from "@tanstack/react-query"; import { Expand, FileIcon, ImageIcon } from "lucide-react"; import { useInView } from "react-intersection-observer"; import Masonry from "react-masonry-css"; @@ -199,19 +200,22 @@ export default function PublicBookmarkGrid({ bookmarks: ZPublicBookmark[]; nextCursor: ZCursor | null; }) { + const api = useTRPC(); const { ref: loadMoreRef, inView: loadMoreButtonInView } = useInView(); const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = - api.publicBookmarks.getPublicBookmarksInList.useInfiniteQuery( - { listId: list.id }, - { - initialData: () => ({ - pages: [{ bookmarks: initialBookmarks, nextCursor, list }], - pageParams: [null], - }), - initialCursor: null, - getNextPageParam: (lastPage) => lastPage.nextCursor, - refetchOnMount: true, - }, + useInfiniteQuery( + api.publicBookmarks.getPublicBookmarksInList.infiniteQueryOptions( + { listId: list.id }, + { + initialData: () => ({ + pages: [{ bookmarks: initialBookmarks, nextCursor, list }], + pageParams: [null], + }), + initialCursor: null, + getNextPageParam: (lastPage) => lastPage.nextCursor, + refetchOnMount: true, + }, + ), ); useEffect(() => { diff --git a/apps/web/components/settings/AISettings.tsx b/apps/web/components/settings/AISettings.tsx index 78e3ef56..6d8565da 100644 --- a/apps/web/components/settings/AISettings.tsx +++ b/apps/web/components/settings/AISettings.tsx @@ -40,10 +40,11 @@ import { toast } from "@/components/ui/sonner"; import { Switch } from "@/components/ui/switch"; import { useClientConfig } from "@/lib/clientConfig"; import { useTranslation } from "@/lib/i18n/client"; -import { api } from "@/lib/trpc"; +import { useTRPC } from "@/lib/trpc"; import { useUserSettings } from "@/lib/userSettings"; import { cn } from "@/lib/utils"; import { zodResolver } from "@hookform/resolvers/zod"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { Info, Plus, Save, Trash2 } from "lucide-react"; import { Controller, useForm } from "react-hook-form"; import { z } from "zod"; @@ -340,8 +341,9 @@ export function TagStyleSelector() { } export function PromptEditor() { + const api = useTRPC(); const { t } = useTranslation(); - const apiUtils = api.useUtils(); + const queryClient = useQueryClient(); const form = useForm<z.infer<typeof zNewPromptSchema>>({ resolver: zodResolver(zNewPromptSchema), @@ -351,15 +353,16 @@ export function PromptEditor() { }, }); - const { mutateAsync: createPrompt, isPending: isCreating } = - api.prompts.create.useMutation({ + const { mutateAsync: createPrompt, isPending: isCreating } = useMutation( + api.prompts.create.mutationOptions({ onSuccess: () => { toast({ description: "Prompt has been created!", }); - apiUtils.prompts.list.invalidate(); + queryClient.invalidateQueries(api.prompts.list.pathFilter()); }, - }); + }), + ); return ( <Form {...form}> @@ -441,26 +444,29 @@ export function PromptEditor() { } export function PromptRow({ prompt }: { prompt: ZPrompt }) { + const api = useTRPC(); const { t } = useTranslation(); - const apiUtils = api.useUtils(); - const { mutateAsync: updatePrompt, isPending: isUpdating } = - api.prompts.update.useMutation({ + const queryClient = useQueryClient(); + const { mutateAsync: updatePrompt, isPending: isUpdating } = useMutation( + api.prompts.update.mutationOptions({ onSuccess: () => { toast({ description: "Prompt has been updated!", }); - apiUtils.prompts.list.invalidate(); + queryClient.invalidateQueries(api.prompts.list.pathFilter()); }, - }); - const { mutate: deletePrompt, isPending: isDeleting } = - api.prompts.delete.useMutation({ + }), + ); + const { mutate: deletePrompt, isPending: isDeleting } = useMutation( + api.prompts.delete.mutationOptions({ onSuccess: () => { toast({ description: "Prompt has been deleted!", }); - apiUtils.prompts.list.invalidate(); + queryClient.invalidateQueries(api.prompts.list.pathFilter()); }, - }); + }), + ); const form = useForm<z.infer<typeof zUpdatePromptSchema>>({ resolver: zodResolver(zUpdatePromptSchema), @@ -574,8 +580,11 @@ export function PromptRow({ prompt }: { prompt: ZPrompt }) { } export function TaggingRules() { + const api = useTRPC(); const { t } = useTranslation(); - const { data: prompts, isLoading } = api.prompts.list.useQuery(); + const { data: prompts, isLoading } = useQuery( + api.prompts.list.queryOptions(), + ); return ( <SettingsSection @@ -601,8 +610,9 @@ export function TaggingRules() { } export function PromptDemo() { + const api = useTRPC(); const { t } = useTranslation(); - const { data: prompts } = api.prompts.list.useQuery(); + const { data: prompts } = useQuery(api.prompts.list.queryOptions()); const settings = useUserSettings(); const clientConfig = useClientConfig(); diff --git a/apps/web/components/settings/AddApiKey.tsx b/apps/web/components/settings/AddApiKey.tsx index 9ef9047c..11107333 100644 --- a/apps/web/components/settings/AddApiKey.tsx +++ b/apps/web/components/settings/AddApiKey.tsx @@ -26,8 +26,9 @@ import { import { Input } from "@/components/ui/input"; import { toast } from "@/components/ui/sonner"; import { useTranslation } from "@/lib/i18n/client"; -import { api } from "@/lib/trpc"; +import { useTRPC } from "@/lib/trpc"; import { zodResolver } from "@hookform/resolvers/zod"; +import { useMutation } from "@tanstack/react-query"; import { PlusCircle } from "lucide-react"; import { useForm } from "react-hook-form"; import { z } from "zod"; @@ -35,23 +36,26 @@ import { z } from "zod"; import ApiKeySuccess from "./ApiKeySuccess"; function AddApiKeyForm({ onSuccess }: { onSuccess: (key: string) => void }) { + const api = useTRPC(); const { t } = useTranslation(); const formSchema = z.object({ name: z.string(), }); const router = useRouter(); - const mutator = api.apiKeys.create.useMutation({ - onSuccess: (resp) => { - onSuccess(resp.key); - router.refresh(); - }, - onError: () => { - toast({ - description: t("common.something_went_wrong"), - variant: "destructive", - }); - }, - }); + const mutator = useMutation( + api.apiKeys.create.mutationOptions({ + onSuccess: (resp) => { + onSuccess(resp.key); + router.refresh(); + }, + onError: () => { + toast({ + description: t("common.something_went_wrong"), + variant: "destructive", + }); + }, + }), + ); const form = useForm<z.infer<typeof formSchema>>({ resolver: zodResolver(formSchema), diff --git a/apps/web/components/settings/BackupSettings.tsx b/apps/web/components/settings/BackupSettings.tsx index ad2b66c6..78418f6c 100644 --- a/apps/web/components/settings/BackupSettings.tsx +++ b/apps/web/components/settings/BackupSettings.tsx @@ -24,9 +24,10 @@ import { import { toast } from "@/components/ui/sonner"; import { Switch } from "@/components/ui/switch"; import { useTranslation } from "@/lib/i18n/client"; -import { api } from "@/lib/trpc"; +import { useTRPC } from "@/lib/trpc"; import { useUserSettings } from "@/lib/userSettings"; import { zodResolver } from "@hookform/resolvers/zod"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { CheckCircle, Download, @@ -207,16 +208,17 @@ function BackupConfigurationForm() { } function BackupRow({ backup }: { backup: z.infer<typeof zBackupSchema> }) { + const api = useTRPC(); const { t } = useTranslation(); - const apiUtils = api.useUtils(); + const queryClient = useQueryClient(); - const { mutate: deleteBackup, isPending: isDeleting } = - api.backups.delete.useMutation({ + const { mutate: deleteBackup, isPending: isDeleting } = useMutation( + api.backups.delete.mutationOptions({ onSuccess: () => { toast({ description: t("settings.backups.toasts.backup_deleted"), }); - apiUtils.backups.list.invalidate(); + queryClient.invalidateQueries(api.backups.list.pathFilter()); }, onError: (error) => { toast({ @@ -224,7 +226,8 @@ function BackupRow({ backup }: { backup: z.infer<typeof zBackupSchema> }) { variant: "destructive", }); }, - }); + }), + ); const formatSize = (bytes: number) => { if (bytes < 1024) return `${bytes} B`; @@ -330,25 +333,28 @@ function BackupRow({ backup }: { backup: z.infer<typeof zBackupSchema> }) { } function BackupsList() { + const api = useTRPC(); const { t } = useTranslation(); - const apiUtils = api.useUtils(); - const { data: backups, isLoading } = api.backups.list.useQuery(undefined, { - refetchInterval: (query) => { - const data = query.state.data; - // Poll every 3 seconds if there's a pending backup, otherwise don't poll - return data?.backups.some((backup) => backup.status === "pending") - ? 3000 - : false; - }, - }); + const queryClient = useQueryClient(); + const { data: backups, isLoading } = useQuery( + api.backups.list.queryOptions(undefined, { + refetchInterval: (query) => { + const data = query.state.data; + // Poll every 3 seconds if there's a pending backup, otherwise don't poll + return data?.backups.some((backup) => backup.status === "pending") + ? 3000 + : false; + }, + }), + ); - const { mutate: triggerBackup, isPending: isTriggering } = - api.backups.triggerBackup.useMutation({ + const { mutate: triggerBackup, isPending: isTriggering } = useMutation( + api.backups.triggerBackup.mutationOptions({ onSuccess: () => { toast({ description: t("settings.backups.toasts.backup_queued"), }); - apiUtils.backups.list.invalidate(); + queryClient.invalidateQueries(api.backups.list.pathFilter()); }, onError: (error) => { toast({ @@ -356,7 +362,8 @@ function BackupsList() { variant: "destructive", }); }, - }); + }), + ); return ( <div className="rounded-md border bg-background p-4"> diff --git a/apps/web/components/settings/ChangePassword.tsx b/apps/web/components/settings/ChangePassword.tsx index 1da92267..ae764dd3 100644 --- a/apps/web/components/settings/ChangePassword.tsx +++ b/apps/web/components/settings/ChangePassword.tsx @@ -14,8 +14,9 @@ import { import { Input } from "@/components/ui/input"; import { toast } from "@/components/ui/sonner"; import { useTranslation } from "@/lib/i18n/client"; -import { api } from "@/lib/trpc"; +import { useTRPC } from "@/lib/trpc"; import { zodResolver } from "@hookform/resolvers/zod"; +import { useMutation } from "@tanstack/react-query"; import { Eye, EyeOff, Lock } from "lucide-react"; import { useForm } from "react-hook-form"; @@ -25,6 +26,7 @@ import { Button } from "../ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "../ui/card"; export function ChangePassword() { + const api = useTRPC(); const { t } = useTranslation(); const [showCurrentPassword, setShowCurrentPassword] = useState(false); const [showNewPassword, setShowNewPassword] = useState(false); @@ -38,22 +40,27 @@ export function ChangePassword() { }, }); - const mutator = api.users.changePassword.useMutation({ - onSuccess: () => { - toast({ description: "Password changed successfully" }); - form.reset(); - }, - onError: (e) => { - if (e.data?.code == "UNAUTHORIZED") { - toast({ - description: "Your current password is incorrect", - variant: "destructive", - }); - } else { - toast({ description: "Something went wrong", variant: "destructive" }); - } - }, - }); + const mutator = useMutation( + api.users.changePassword.mutationOptions({ + onSuccess: () => { + toast({ description: "Password changed successfully" }); + form.reset(); + }, + onError: (e) => { + if (e.data?.code == "UNAUTHORIZED") { + toast({ + description: "Your current password is incorrect", + variant: "destructive", + }); + } else { + toast({ + description: "Something went wrong", + variant: "destructive", + }); + } + }, + }), + ); async function onSubmit(value: z.infer<typeof zChangePasswordSchema>) { mutator.mutate({ diff --git a/apps/web/components/settings/DeleteApiKey.tsx b/apps/web/components/settings/DeleteApiKey.tsx index b26b40e6..78a895ac 100644 --- a/apps/web/components/settings/DeleteApiKey.tsx +++ b/apps/web/components/settings/DeleteApiKey.tsx @@ -5,7 +5,8 @@ import { ActionButton } from "@/components/ui/action-button"; import ActionConfirmingDialog from "@/components/ui/action-confirming-dialog"; import { Button } from "@/components/ui/button"; import { useTranslation } from "@/lib/i18n/client"; -import { api } from "@/lib/trpc"; +import { useTRPC } from "@/lib/trpc"; +import { useMutation } from "@tanstack/react-query"; import { Trash } from "lucide-react"; import { toast } from "sonner"; @@ -16,14 +17,17 @@ export default function DeleteApiKey({ name: string; id: string; }) { + const api = useTRPC(); const { t } = useTranslation(); const router = useRouter(); - const mutator = api.apiKeys.revoke.useMutation({ - onSuccess: () => { - toast.success("Key was successfully deleted"); - router.refresh(); - }, - }); + const mutator = useMutation( + api.apiKeys.revoke.mutationOptions({ + onSuccess: () => { + toast.success("Key was successfully deleted"); + router.refresh(); + }, + }), + ); return ( <ActionConfirmingDialog diff --git a/apps/web/components/settings/FeedSettings.tsx b/apps/web/components/settings/FeedSettings.tsx index a49bb0b2..acf947a3 100644 --- a/apps/web/components/settings/FeedSettings.tsx +++ b/apps/web/components/settings/FeedSettings.tsx @@ -16,9 +16,10 @@ import { Input } from "@/components/ui/input"; import { toast } from "@/components/ui/sonner"; import { Switch } from "@/components/ui/switch"; import { useTranslation } from "@/lib/i18n/client"; -import { api } from "@/lib/trpc"; +import { useTRPC } from "@/lib/trpc"; import { cn } from "@/lib/utils"; import { zodResolver } from "@hookform/resolvers/zod"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { ArrowDownToLine, CheckCircle, @@ -61,9 +62,10 @@ import { import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip"; export function FeedsEditorDialog() { + const api = useTRPC(); const { t } = useTranslation(); const [open, setOpen] = React.useState(false); - const apiUtils = api.useUtils(); + const queryClient = useQueryClient(); const form = useForm<z.infer<typeof zNewFeedSchema>>({ resolver: zodResolver(zNewFeedSchema), @@ -81,16 +83,17 @@ export function FeedsEditorDialog() { } }, [open]); - const { mutateAsync: createFeed, isPending: isCreating } = - api.feeds.create.useMutation({ + const { mutateAsync: createFeed, isPending: isCreating } = useMutation( + api.feeds.create.mutationOptions({ onSuccess: () => { toast({ description: "Feed has been created!", }); - apiUtils.feeds.list.invalidate(); + queryClient.invalidateQueries(api.feeds.list.pathFilter()); setOpen(false); }, - }); + }), + ); return ( <Dialog open={open} onOpenChange={setOpen}> @@ -191,8 +194,9 @@ export function FeedsEditorDialog() { } export function EditFeedDialog({ feed }: { feed: ZFeed }) { + const api = useTRPC(); const { t } = useTranslation(); - const apiUtils = api.useUtils(); + const queryClient = useQueryClient(); const [open, setOpen] = React.useState(false); React.useEffect(() => { if (open) { @@ -204,16 +208,17 @@ export function EditFeedDialog({ feed }: { feed: ZFeed }) { }); } }, [open]); - const { mutateAsync: updateFeed, isPending: isUpdating } = - api.feeds.update.useMutation({ + const { mutateAsync: updateFeed, isPending: isUpdating } = useMutation( + api.feeds.update.mutationOptions({ onSuccess: () => { toast({ description: "Feed has been updated!", }); setOpen(false); - apiUtils.feeds.list.invalidate(); + queryClient.invalidateQueries(api.feeds.list.pathFilter()); }, - }); + }), + ); const form = useForm<z.infer<typeof zUpdateFeedSchema>>({ resolver: zodResolver(zUpdateFeedSchema), defaultValues: { @@ -339,44 +344,49 @@ export function EditFeedDialog({ feed }: { feed: ZFeed }) { } export function FeedRow({ feed }: { feed: ZFeed }) { + const api = useTRPC(); const { t } = useTranslation(); - const apiUtils = api.useUtils(); - const { mutate: deleteFeed, isPending: isDeleting } = - api.feeds.delete.useMutation({ + const queryClient = useQueryClient(); + const { mutate: deleteFeed, isPending: isDeleting } = useMutation( + api.feeds.delete.mutationOptions({ onSuccess: () => { toast({ description: "Feed has been deleted!", }); - apiUtils.feeds.list.invalidate(); + queryClient.invalidateQueries(api.feeds.list.pathFilter()); }, - }); + }), + ); - const { mutate: fetchNow, isPending: isFetching } = - api.feeds.fetchNow.useMutation({ + const { mutate: fetchNow, isPending: isFetching } = useMutation( + api.feeds.fetchNow.mutationOptions({ onSuccess: () => { toast({ description: "Feed fetch has been enqueued!", }); - apiUtils.feeds.list.invalidate(); + queryClient.invalidateQueries(api.feeds.list.pathFilter()); }, - }); + }), + ); - const { mutate: updateFeedEnabled } = api.feeds.update.useMutation({ - onSuccess: () => { - toast({ - description: feed.enabled - ? t("settings.feeds.feed_disabled") - : t("settings.feeds.feed_enabled"), - }); - apiUtils.feeds.list.invalidate(); - }, - onError: (error) => { - toast({ - description: `Error: ${error.message}`, - variant: "destructive", - }); - }, - }); + const { mutate: updateFeedEnabled } = useMutation( + api.feeds.update.mutationOptions({ + onSuccess: () => { + toast({ + description: feed.enabled + ? t("settings.feeds.feed_disabled") + : t("settings.feeds.feed_enabled"), + }); + queryClient.invalidateQueries(api.feeds.list.pathFilter()); + }, + onError: (error) => { + toast({ + description: `Error: ${error.message}`, + variant: "destructive", + }); + }, + }), + ); const handleToggle = (checked: boolean) => { updateFeedEnabled({ feedId: feed.id, enabled: checked }); @@ -456,8 +466,9 @@ export function FeedRow({ feed }: { feed: ZFeed }) { } export default function FeedSettings() { + const api = useTRPC(); const { t } = useTranslation(); - const { data: feeds, isLoading } = api.feeds.list.useQuery(); + const { data: feeds, isLoading } = useQuery(api.feeds.list.queryOptions()); return ( <> <div className="rounded-md border bg-background p-4"> diff --git a/apps/web/components/settings/RegenerateApiKey.tsx b/apps/web/components/settings/RegenerateApiKey.tsx index f4914598..9ee0e216 100644 --- a/apps/web/components/settings/RegenerateApiKey.tsx +++ b/apps/web/components/settings/RegenerateApiKey.tsx @@ -16,7 +16,8 @@ import { } from "@/components/ui/dialog"; import { toast } from "@/components/ui/sonner"; import { useTranslation } from "@/lib/i18n/client"; -import { api } from "@/lib/trpc"; +import { useTRPC } from "@/lib/trpc"; +import { useMutation } from "@tanstack/react-query"; import { RefreshCcw } from "lucide-react"; import ApiKeySuccess from "./ApiKeySuccess"; @@ -28,25 +29,28 @@ export default function RegenerateApiKey({ id: string; name: string; }) { + const api = useTRPC(); const { t } = useTranslation(); const router = useRouter(); const [key, setKey] = useState<string | undefined>(undefined); const [dialogOpen, setDialogOpen] = useState<boolean>(false); - const mutator = api.apiKeys.regenerate.useMutation({ - onSuccess: (resp) => { - setKey(resp.key); - router.refresh(); - }, - onError: () => { - toast({ - description: t("common.something_went_wrong"), - variant: "destructive", - }); - setDialogOpen(false); - }, - }); + const mutator = useMutation( + api.apiKeys.regenerate.mutationOptions({ + onSuccess: (resp) => { + setKey(resp.key); + router.refresh(); + }, + onError: () => { + toast({ + description: t("common.something_went_wrong"), + variant: "destructive", + }); + setDialogOpen(false); + }, + }), + ); const handleRegenerate = () => { mutator.mutate({ id }); diff --git a/apps/web/components/settings/SubscriptionSettings.tsx b/apps/web/components/settings/SubscriptionSettings.tsx index 03337c1b..3799d08b 100644 --- a/apps/web/components/settings/SubscriptionSettings.tsx +++ b/apps/web/components/settings/SubscriptionSettings.tsx @@ -3,7 +3,8 @@ import { useEffect } from "react"; import { toast } from "@/components/ui/sonner"; import { useTranslation } from "@/lib/i18n/client"; -import { api } from "@/lib/trpc"; +import { useTRPC } from "@/lib/trpc"; +import { useMutation, useQuery } from "@tanstack/react-query"; import { CreditCard, Loader2 } from "lucide-react"; import { Alert, AlertDescription } from "../ui/alert"; @@ -19,24 +20,27 @@ import { import { Skeleton } from "../ui/skeleton"; export default function SubscriptionSettings() { + const api = useTRPC(); const { t } = useTranslation(); const { data: subscriptionStatus, refetch, isLoading: isQueryLoading, - } = api.subscriptions.getSubscriptionStatus.useQuery(); + } = useQuery(api.subscriptions.getSubscriptionStatus.queryOptions()); - const { data: subscriptionPrice } = - api.subscriptions.getSubscriptionPrice.useQuery(); + const { data: subscriptionPrice } = useQuery( + api.subscriptions.getSubscriptionPrice.queryOptions(), + ); - const { mutate: syncStripeState } = - api.subscriptions.syncWithStripe.useMutation({ + const { mutate: syncStripeState } = useMutation( + api.subscriptions.syncWithStripe.mutationOptions({ onSuccess: () => { refetch(); }, - }); - const createCheckoutSession = - api.subscriptions.createCheckoutSession.useMutation({ + }), + ); + const createCheckoutSession = useMutation( + api.subscriptions.createCheckoutSession.mutationOptions({ onSuccess: (resp) => { if (resp.url) { window.location.href = resp.url; @@ -48,9 +52,10 @@ export default function SubscriptionSettings() { variant: "destructive", }); }, - }); - const createPortalSession = api.subscriptions.createPortalSession.useMutation( - { + }), + ); + const createPortalSession = useMutation( + api.subscriptions.createPortalSession.mutationOptions({ onSuccess: (resp) => { if (resp.url) { window.location.href = resp.url; @@ -62,7 +67,7 @@ export default function SubscriptionSettings() { variant: "destructive", }); }, - }, + }), ); const isLoading = diff --git a/apps/web/components/settings/WebhookSettings.tsx b/apps/web/components/settings/WebhookSettings.tsx index 73671787..89f11b66 100644 --- a/apps/web/components/settings/WebhookSettings.tsx +++ b/apps/web/components/settings/WebhookSettings.tsx @@ -14,8 +14,9 @@ import { FullPageSpinner } from "@/components/ui/full-page-spinner"; import { Input } from "@/components/ui/input"; import { toast } from "@/components/ui/sonner"; import { useTranslation } from "@/lib/i18n/client"; -import { api } from "@/lib/trpc"; +import { useTRPC } from "@/lib/trpc"; import { zodResolver } from "@hookform/resolvers/zod"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { Edit, KeyRound, @@ -56,9 +57,10 @@ import { import { WebhookEventSelector } from "./WebhookEventSelector"; export function WebhooksEditorDialog() { + const api = useTRPC(); const { t } = useTranslation(); const [open, setOpen] = React.useState(false); - const apiUtils = api.useUtils(); + const queryClient = useQueryClient(); const form = useForm<z.infer<typeof zNewWebhookSchema>>({ resolver: zodResolver(zNewWebhookSchema), @@ -75,16 +77,17 @@ export function WebhooksEditorDialog() { } }, [open]); - const { mutateAsync: createWebhook, isPending: isCreating } = - api.webhooks.create.useMutation({ + const { mutateAsync: createWebhook, isPending: isCreating } = useMutation( + api.webhooks.create.mutationOptions({ onSuccess: () => { toast({ description: "Webhook has been created!", }); - apiUtils.webhooks.list.invalidate(); + queryClient.invalidateQueries(api.webhooks.list.pathFilter()); setOpen(false); }, - }); + }), + ); return ( <Dialog open={open} onOpenChange={setOpen}> @@ -179,8 +182,9 @@ export function WebhooksEditorDialog() { } export function EditWebhookDialog({ webhook }: { webhook: ZWebhook }) { + const api = useTRPC(); const { t } = useTranslation(); - const apiUtils = api.useUtils(); + const queryClient = useQueryClient(); const [open, setOpen] = React.useState(false); React.useEffect(() => { if (open) { @@ -191,16 +195,17 @@ export function EditWebhookDialog({ webhook }: { webhook: ZWebhook }) { }); } }, [open]); - const { mutateAsync: updateWebhook, isPending: isUpdating } = - api.webhooks.update.useMutation({ + const { mutateAsync: updateWebhook, isPending: isUpdating } = useMutation( + api.webhooks.update.mutationOptions({ onSuccess: () => { toast({ description: "Webhook has been updated!", }); setOpen(false); - apiUtils.webhooks.list.invalidate(); + queryClient.invalidateQueries(api.webhooks.list.pathFilter()); }, - }); + }), + ); const updateSchema = zUpdateWebhookSchema.required({ events: true, url: true, @@ -302,8 +307,9 @@ export function EditWebhookDialog({ webhook }: { webhook: ZWebhook }) { } export function EditTokenDialog({ webhook }: { webhook: ZWebhook }) { + const api = useTRPC(); const { t } = useTranslation(); - const apiUtils = api.useUtils(); + const queryClient = useQueryClient(); const [open, setOpen] = React.useState(false); React.useEffect(() => { if (open) { @@ -331,16 +337,17 @@ export function EditTokenDialog({ webhook }: { webhook: ZWebhook }) { }, }); - const { mutateAsync: updateWebhook, isPending: isUpdating } = - api.webhooks.update.useMutation({ + const { mutateAsync: updateWebhook, isPending: isUpdating } = useMutation( + api.webhooks.update.mutationOptions({ onSuccess: () => { toast({ description: "Webhook token has been updated!", }); setOpen(false); - apiUtils.webhooks.list.invalidate(); + queryClient.invalidateQueries(api.webhooks.list.pathFilter()); }, - }); + }), + ); return ( <Dialog open={open} onOpenChange={setOpen}> @@ -432,17 +439,19 @@ export function EditTokenDialog({ webhook }: { webhook: ZWebhook }) { } export function WebhookRow({ webhook }: { webhook: ZWebhook }) { + const api = useTRPC(); const { t } = useTranslation(); - const apiUtils = api.useUtils(); - const { mutate: deleteWebhook, isPending: isDeleting } = - api.webhooks.delete.useMutation({ + const queryClient = useQueryClient(); + const { mutate: deleteWebhook, isPending: isDeleting } = useMutation( + api.webhooks.delete.mutationOptions({ onSuccess: () => { toast({ description: "Webhook has been deleted!", }); - apiUtils.webhooks.list.invalidate(); + queryClient.invalidateQueries(api.webhooks.list.pathFilter()); }, - }); + }), + ); return ( <TableRow> @@ -479,8 +488,11 @@ export function WebhookRow({ webhook }: { webhook: ZWebhook }) { } export default function WebhookSettings() { + const api = useTRPC(); const { t } = useTranslation(); - const { data: webhooks, isLoading } = api.webhooks.list.useQuery(); + const { data: webhooks, isLoading } = useQuery( + api.webhooks.list.queryOptions(), + ); return ( <div className="rounded-md border bg-background p-4"> <div className="flex flex-col gap-2"> diff --git a/apps/web/components/signin/ForgotPasswordForm.tsx b/apps/web/components/signin/ForgotPasswordForm.tsx index 29d55f2b..6349c300 100644 --- a/apps/web/components/signin/ForgotPasswordForm.tsx +++ b/apps/web/components/signin/ForgotPasswordForm.tsx @@ -20,8 +20,9 @@ import { FormMessage, } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; -import { api } from "@/lib/trpc"; +import { useTRPC } from "@/lib/trpc"; import { zodResolver } from "@hookform/resolvers/zod"; +import { useMutation } from "@tanstack/react-query"; import { TRPCClientError } from "@trpc/client"; import { AlertCircle, CheckCircle } from "lucide-react"; import { useForm } from "react-hook-form"; @@ -32,6 +33,7 @@ const forgotPasswordSchema = z.object({ }); export default function ForgotPasswordForm() { + const api = useTRPC(); const [isSubmitted, setIsSubmitted] = useState(false); const [errorMessage, setErrorMessage] = useState(""); const router = useRouter(); @@ -40,7 +42,9 @@ export default function ForgotPasswordForm() { resolver: zodResolver(forgotPasswordSchema), }); - const forgotPasswordMutation = api.users.forgotPassword.useMutation(); + const forgotPasswordMutation = useMutation( + api.users.forgotPassword.mutationOptions(), + ); const onSubmit = async (values: z.infer<typeof forgotPasswordSchema>) => { try { diff --git a/apps/web/components/signin/ResetPasswordForm.tsx b/apps/web/components/signin/ResetPasswordForm.tsx index d4d8a285..b6e5f5ae 100644 --- a/apps/web/components/signin/ResetPasswordForm.tsx +++ b/apps/web/components/signin/ResetPasswordForm.tsx @@ -20,8 +20,9 @@ import { FormMessage, } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; -import { api } from "@/lib/trpc"; +import { useTRPC } from "@/lib/trpc"; import { zodResolver } from "@hookform/resolvers/zod"; +import { useMutation } from "@tanstack/react-query"; import { TRPCClientError } from "@trpc/client"; import { AlertCircle, CheckCircle } from "lucide-react"; import { useForm } from "react-hook-form"; @@ -44,6 +45,7 @@ interface ResetPasswordFormProps { } export default function ResetPasswordForm({ token }: ResetPasswordFormProps) { + const api = useTRPC(); const [isSuccess, setIsSuccess] = useState(false); const [errorMessage, setErrorMessage] = useState(""); const router = useRouter(); @@ -52,7 +54,9 @@ export default function ResetPasswordForm({ token }: ResetPasswordFormProps) { resolver: zodResolver(resetPasswordSchema), }); - const resetPasswordMutation = api.users.resetPassword.useMutation(); + const resetPasswordMutation = useMutation( + api.users.resetPassword.mutationOptions(), + ); const onSubmit = async (values: z.infer<typeof resetPasswordSchema>) => { try { diff --git a/apps/web/components/signup/SignUpForm.tsx b/apps/web/components/signup/SignUpForm.tsx index 845c6d1e..77d6ba02 100644 --- a/apps/web/components/signup/SignUpForm.tsx +++ b/apps/web/components/signup/SignUpForm.tsx @@ -25,9 +25,10 @@ import { import { Input } from "@/components/ui/input"; import { signIn } from "@/lib/auth/client"; import { useClientConfig } from "@/lib/clientConfig"; -import { api } from "@/lib/trpc"; +import { useTRPC } from "@/lib/trpc"; import { zodResolver } from "@hookform/resolvers/zod"; import { Turnstile } from "@marsidev/react-turnstile"; +import { useMutation } from "@tanstack/react-query"; import { TRPCClientError } from "@trpc/client"; import { AlertCircle, UserX } from "lucide-react"; import { useForm } from "react-hook-form"; @@ -38,6 +39,7 @@ import { zSignUpSchema } from "@karakeep/shared/types/users"; const VERIFY_EMAIL_ERROR = "Please verify your email address before signing in"; export default function SignUpForm() { + const api = useTRPC(); const form = useForm<z.infer<typeof zSignUpSchema>>({ resolver: zodResolver(zSignUpSchema), defaultValues: { @@ -54,7 +56,7 @@ export default function SignUpForm() { const turnstileSiteKey = clientConfig.turnstile?.siteKey; const turnstileRef = useRef<TurnstileInstance>(null); - const createUserMutation = api.users.create.useMutation(); + const createUserMutation = useMutation(api.users.create.mutationOptions()); if ( clientConfig.auth.disableSignups || diff --git a/apps/web/components/subscription/QuotaProgress.tsx b/apps/web/components/subscription/QuotaProgress.tsx index 525eae8f..b36baec3 100644 --- a/apps/web/components/subscription/QuotaProgress.tsx +++ b/apps/web/components/subscription/QuotaProgress.tsx @@ -1,7 +1,8 @@ "use client"; import { useTranslation } from "@/lib/i18n/client"; -import { api } from "@/lib/trpc"; +import { useTRPC } from "@/lib/trpc"; +import { useQuery } from "@tanstack/react-query"; import { Database, HardDrive } from "lucide-react"; import { @@ -110,9 +111,11 @@ function QuotaProgressItem({ } export function QuotaProgress() { + const api = useTRPC(); const { t } = useTranslation(); - const { data: quotaUsage, isLoading } = - api.subscriptions.getQuotaUsage.useQuery(); + const { data: quotaUsage, isLoading } = useQuery( + api.subscriptions.getQuotaUsage.queryOptions(), + ); if (isLoading) { return ( diff --git a/apps/web/components/utils/ValidAccountCheck.tsx b/apps/web/components/utils/ValidAccountCheck.tsx index 5ca5fd5c..49f42f9c 100644 --- a/apps/web/components/utils/ValidAccountCheck.tsx +++ b/apps/web/components/utils/ValidAccountCheck.tsx @@ -2,22 +2,26 @@ import { useEffect } from "react"; import { useRouter } from "next/navigation"; -import { api } from "@/lib/trpc"; +import { useTRPC } from "@/lib/trpc"; +import { useQuery } from "@tanstack/react-query"; /** * This component is used to address a confusion when the JWT token exists but the user no longer exists in the database. * So this component synchronusly checks if the user is still valid and if not, signs out the user. */ export default function ValidAccountCheck() { + const api = useTRPC(); const router = useRouter(); - const { error } = api.users.whoami.useQuery(undefined, { - retry: (_failureCount, error) => { - if (error.data?.code === "UNAUTHORIZED") { - return false; - } - return true; - }, - }); + const { error } = useQuery( + api.users.whoami.queryOptions(undefined, { + retry: (_failureCount, error) => { + if (error.data?.code === "UNAUTHORIZED") { + return false; + } + return true; + }, + }), + ); useEffect(() => { if (error?.data?.code === "UNAUTHORIZED") { router.push("/logout"); diff --git a/apps/web/components/wrapped/WrappedModal.tsx b/apps/web/components/wrapped/WrappedModal.tsx index 25e376b0..93635d36 100644 --- a/apps/web/components/wrapped/WrappedModal.tsx +++ b/apps/web/components/wrapped/WrappedModal.tsx @@ -7,8 +7,9 @@ import { DialogOverlay, DialogTitle, } from "@/components/ui/dialog"; -import { api } from "@/lib/trpc"; +import { useTRPC } from "@/lib/trpc"; import * as VisuallyHidden from "@radix-ui/react-visually-hidden"; +import { useQuery } from "@tanstack/react-query"; import { Loader2, X } from "lucide-react"; import { ShareButton } from "./ShareButton"; @@ -20,13 +21,18 @@ interface WrappedModalProps { } export function WrappedModal({ open, onClose }: WrappedModalProps) { + const api = useTRPC(); const contentRef = useRef<HTMLDivElement | null>(null); - const { data: stats, isLoading } = api.users.wrapped.useQuery(undefined, { - enabled: open, - }); - const { data: whoami } = api.users.whoami.useQuery(undefined, { - enabled: open, - }); + const { data: stats, isLoading } = useQuery( + api.users.wrapped.queryOptions(undefined, { + enabled: open, + }), + ); + const { data: whoami } = useQuery( + api.users.whoami.queryOptions(undefined, { + enabled: open, + }), + ); return ( <Dialog open={open} onOpenChange={onClose}> diff --git a/apps/web/lib/hooks/bookmark-search.ts b/apps/web/lib/hooks/bookmark-search.ts index f94e4691..0b6b229d 100644 --- a/apps/web/lib/hooks/bookmark-search.ts +++ b/apps/web/lib/hooks/bookmark-search.ts @@ -1,8 +1,8 @@ import { useEffect, useMemo, useRef } from "react"; import { useRouter, useSearchParams } from "next/navigation"; import { useSortOrderStore } from "@/lib/store/useSortOrderStore"; -import { api } from "@/lib/trpc"; -import { keepPreviousData } from "@tanstack/react-query"; +import { useTRPC } from "@/lib/trpc"; +import { keepPreviousData, useInfiniteQuery } from "@tanstack/react-query"; import { parseSearchQuery } from "@karakeep/shared/searchQueryParser"; @@ -55,6 +55,7 @@ export function useDoBookmarkSearch() { } export function useBookmarkSearch() { + const api = useTRPC(); const { searchQuery } = useSearchQuery(); const sortOrder = useSortOrderStore((state) => state.sortOrder); @@ -67,17 +68,19 @@ export function useBookmarkSearch() { fetchNextPage, isFetchingNextPage, refetch, - } = api.bookmarks.searchBookmarks.useInfiniteQuery( - { - text: searchQuery, - sortOrder, - }, - { - placeholderData: keepPreviousData, - gcTime: 0, - initialCursor: null, - getNextPageParam: (lastPage) => lastPage.nextCursor, - }, + } = useInfiniteQuery( + api.bookmarks.searchBookmarks.infiniteQueryOptions( + { + text: searchQuery, + sortOrder, + }, + { + placeholderData: keepPreviousData, + gcTime: 0, + initialCursor: null, + getNextPageParam: (lastPage) => lastPage.nextCursor, + }, + ), ); useEffect(() => { diff --git a/apps/web/lib/hooks/useBookmarkImport.ts b/apps/web/lib/hooks/useBookmarkImport.ts index f2522d9a..71d06522 100644 --- a/apps/web/lib/hooks/useBookmarkImport.ts +++ b/apps/web/lib/hooks/useBookmarkImport.ts @@ -3,7 +3,8 @@ import { useState } from "react"; import { toast } from "@/components/ui/sonner"; import { useTranslation } from "@/lib/i18n/client"; -import { useMutation } from "@tanstack/react-query"; +import { useTRPC } from "@/lib/trpc"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; import { useCreateBookmarkWithPostHook, @@ -13,7 +14,6 @@ import { useAddBookmarkToList, useCreateBookmarkList, } from "@karakeep/shared-react/hooks/lists"; -import { api } from "@karakeep/shared-react/trpc"; import { importBookmarksFromFile, ImportSource, @@ -34,13 +34,14 @@ export interface ImportProgress { export function useBookmarkImport() { const { t } = useTranslation(); + const api = useTRPC(); const [importProgress, setImportProgress] = useState<ImportProgress | null>( null, ); const [quotaError, setQuotaError] = useState<string | null>(null); - const apiUtils = api.useUtils(); + const queryClient = useQueryClient(); const { mutateAsync: createImportSession } = useCreateImportSession(); const { mutateAsync: createBookmark } = useCreateBookmarkWithPostHook(); const { mutateAsync: createList } = useCreateBookmarkList(); @@ -65,8 +66,9 @@ export function useBookmarkImport() { // Check quota before proceeding if (bookmarkCount > 0) { - const quotaUsage = - await apiUtils.client.subscriptions.getQuotaUsage.query(); + const quotaUsage = await queryClient.fetchQuery( + api.subscriptions.getQuotaUsage.queryOptions(), + ); if ( !quotaUsage.bookmarks.unlimited && diff --git a/apps/web/lib/hooks/useImportSessions.ts b/apps/web/lib/hooks/useImportSessions.ts index 1482f33d..133bb29b 100644 --- a/apps/web/lib/hooks/useImportSessions.ts +++ b/apps/web/lib/hooks/useImportSessions.ts @@ -1,62 +1,79 @@ "use client"; import { toast } from "@/components/ui/sonner"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; -import { api } from "@karakeep/shared-react/trpc"; +import { useTRPC } from "@karakeep/shared-react/trpc"; export function useCreateImportSession() { - const apiUtils = api.useUtils(); + const api = useTRPC(); + const queryClient = useQueryClient(); - return api.importSessions.createImportSession.useMutation({ - onSuccess: () => { - apiUtils.importSessions.listImportSessions.invalidate(); - }, - onError: (error) => { - toast({ - description: error.message || "Failed to create import session", - variant: "destructive", - }); - }, - }); + return useMutation( + api.importSessions.createImportSession.mutationOptions({ + onSuccess: () => { + queryClient.invalidateQueries( + api.importSessions.listImportSessions.pathFilter(), + ); + }, + onError: (error) => { + toast({ + description: error.message || "Failed to create import session", + variant: "destructive", + }); + }, + }), + ); } export function useListImportSessions() { - return api.importSessions.listImportSessions.useQuery( - {}, - { - select: (data) => data.sessions, - }, + const api = useTRPC(); + return useQuery( + api.importSessions.listImportSessions.queryOptions( + {}, + { + select: (data) => data.sessions, + }, + ), ); } export function useImportSessionStats(importSessionId: string) { - return api.importSessions.getImportSessionStats.useQuery( - { - importSessionId, - }, - { - refetchInterval: 5000, // Refetch every 5 seconds to show progress - enabled: !!importSessionId, - }, + const api = useTRPC(); + return useQuery( + api.importSessions.getImportSessionStats.queryOptions( + { + importSessionId, + }, + { + refetchInterval: 5000, // Refetch every 5 seconds to show progress + enabled: !!importSessionId, + }, + ), ); } export function useDeleteImportSession() { - const apiUtils = api.useUtils(); + const api = useTRPC(); + const queryClient = useQueryClient(); - return api.importSessions.deleteImportSession.useMutation({ - onSuccess: () => { - apiUtils.importSessions.listImportSessions.invalidate(); - toast({ - description: "Import session deleted successfully", - variant: "default", - }); - }, - onError: (error) => { - toast({ - description: error.message || "Failed to delete import session", - variant: "destructive", - }); - }, - }); + return useMutation( + api.importSessions.deleteImportSession.mutationOptions({ + onSuccess: () => { + queryClient.invalidateQueries( + api.importSessions.listImportSessions.pathFilter(), + ); + toast({ + description: "Import session deleted successfully", + variant: "default", + }); + }, + onError: (error) => { + toast({ + description: error.message || "Failed to delete import session", + variant: "destructive", + }); + }, + }), + ); } diff --git a/apps/web/lib/providers.tsx b/apps/web/lib/providers.tsx index dd4e62e7..a56b77c7 100644 --- a/apps/web/lib/providers.tsx +++ b/apps/web/lib/providers.tsx @@ -7,14 +7,15 @@ import { TooltipProvider } from "@/components/ui/tooltip"; import { Session, SessionProvider } from "@/lib/auth/client"; import { UserLocalSettingsCtx } from "@/lib/userLocalSettings/bookmarksLayout"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; -import { httpBatchLink, loggerLink } from "@trpc/client"; +import { createTRPCClient, httpBatchLink, loggerLink } from "@trpc/client"; import superjson from "superjson"; import type { ClientConfig } from "@karakeep/shared/config"; +import type { AppRouter } from "@karakeep/trpc/routers/_app"; import { ClientConfigCtx } from "./clientConfig"; import CustomI18nextProvider from "./i18n/provider"; -import { api } from "./trpc"; +import { TRPCProvider } from "./trpc"; function makeQueryClient() { return new QueryClient({ @@ -58,7 +59,7 @@ export default function Providers({ const queryClient = getQueryClient(); const [trpcClient] = useState(() => - api.createClient({ + createTRPCClient<AppRouter>({ links: [ loggerLink({ enabled: (op) => @@ -79,8 +80,8 @@ export default function Providers({ <ClientConfigCtx.Provider value={clientConfig}> <UserLocalSettingsCtx.Provider value={userLocalSettings}> <SessionProvider session={session}> - <api.Provider client={trpcClient} queryClient={queryClient}> - <QueryClientProvider client={queryClient}> + <QueryClientProvider client={queryClient}> + <TRPCProvider trpcClient={trpcClient} queryClient={queryClient}> <CustomI18nextProvider lang={userLocalSettings.lang}> <ThemeProvider attribute="class" @@ -93,8 +94,8 @@ export default function Providers({ </TooltipProvider> </ThemeProvider> </CustomI18nextProvider> - </QueryClientProvider> - </api.Provider> + </TRPCProvider> + </QueryClientProvider> </SessionProvider> </UserLocalSettingsCtx.Provider> </ClientConfigCtx.Provider> diff --git a/apps/web/lib/trpc.tsx b/apps/web/lib/trpc.tsx index 1478684f..d0b27ad6 100644 --- a/apps/web/lib/trpc.tsx +++ b/apps/web/lib/trpc.tsx @@ -1,7 +1,5 @@ "use client"; -import { createTRPCReact } from "@trpc/react-query"; - -import type { AppRouter } from "@karakeep/trpc/routers/_app"; - -export const api = createTRPCReact<AppRouter>(); +// Re-export from shared-react to ensure there's only one TRPCProvider context +// This is necessary because the hooks in shared-react use useTRPC from shared-react +export { TRPCProvider, useTRPC } from "@karakeep/shared-react/trpc"; diff --git a/apps/web/lib/userSettings.tsx b/apps/web/lib/userSettings.tsx index 4789e2ba..54518884 100644 --- a/apps/web/lib/userSettings.tsx +++ b/apps/web/lib/userSettings.tsx @@ -1,10 +1,11 @@ "use client"; import { createContext, useContext } from "react"; +import { useQuery } from "@tanstack/react-query"; import { ZUserSettings } from "@karakeep/shared/types/users"; -import { api } from "./trpc"; +import { useTRPC } from "./trpc"; export const UserSettingsContext = createContext<ZUserSettings>({ bookmarkClickAction: "open_original_link", @@ -29,9 +30,12 @@ export function UserSettingsContextProvider({ userSettings: ZUserSettings; children: React.ReactNode; }) { - const { data } = api.users.settings.useQuery(undefined, { - initialData: userSettings, - }); + const api = useTRPC(); + const { data } = useQuery( + api.users.settings.queryOptions(undefined, { + initialData: userSettings, + }), + ); return ( <UserSettingsContext.Provider value={data}> diff --git a/apps/web/package.json b/apps/web/package.json index 154853fc..91c257e1 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -55,9 +55,9 @@ "@svgr/webpack": "^8.1.0", "@tanstack/react-query": "5.90.2", "@tanstack/react-query-devtools": "5.90.2", - "@trpc/client": "^11.4.3", - "@trpc/react-query": "^11.4.3", - "@trpc/server": "^11.4.3", + "@trpc/client": "^11.9.0", + "@trpc/server": "^11.9.0", + "@trpc/tanstack-react-query": "^11.9.0", "cheerio": "^1.0.0", "class-variance-authority": "^0.7.0", "clsx": "^2.1.0", diff --git a/packages/api/package.json b/packages/api/package.json index e49204b9..676f64b0 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -21,7 +21,7 @@ "@karakeep/shared": "workspace:*", "@karakeep/shared-server": "workspace:*", "@karakeep/trpc": "workspace:*", - "@trpc/server": "^11.4.3", + "@trpc/server": "^11.9.0", "drizzle-orm": "^0.44.2", "file-type": "^21.2.0", "hono": "^4.10.6", diff --git a/packages/benchmarks/package.json b/packages/benchmarks/package.json index 52862862..ce768c43 100644 --- a/packages/benchmarks/package.json +++ b/packages/benchmarks/package.json @@ -15,7 +15,7 @@ "dependencies": { "@karakeep/shared": "workspace:^0.1.0", "@karakeep/trpc": "workspace:^0.1.0", - "@trpc/client": "^11.4.3", + "@trpc/client": "^11.9.0", "p-limit": "^7.2.0", "superjson": "^2.2.1", "tinybench": "^6.0.0", diff --git a/packages/e2e_tests/package.json b/packages/e2e_tests/package.json index 45d512bb..d93318aa 100644 --- a/packages/e2e_tests/package.json +++ b/packages/e2e_tests/package.json @@ -19,7 +19,7 @@ "@karakeep/sdk": "workspace:*", "@karakeep/shared": "workspace:^0.1.0", "@karakeep/trpc": "workspace:^0.1.0", - "@trpc/client": "^11.4.3", + "@trpc/client": "^11.9.0", "superjson": "^2.2.1", "zod": "^3.24.2" }, diff --git a/packages/shared-react/hooks/assets.ts b/packages/shared-react/hooks/assets.ts index 5367e97c..8be21304 100644 --- a/packages/shared-react/hooks/assets.ts +++ b/packages/shared-react/hooks/assets.ts @@ -1,49 +1,74 @@ -import { api } from "../trpc"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; + +import { useTRPC } from "../trpc"; + +type TRPCApi = ReturnType<typeof useTRPC>; export function useAttachBookmarkAsset( - ...opts: Parameters<typeof api.assets.attachAsset.useMutation> + opts?: Parameters<TRPCApi["assets"]["attachAsset"]["mutationOptions"]>[0], ) { - const apiUtils = api.useUtils(); - return api.assets.attachAsset.useMutation({ - ...opts[0], - onSuccess: (res, req, meta, context) => { - apiUtils.bookmarks.getBookmarks.invalidate(); - apiUtils.bookmarks.searchBookmarks.invalidate(); - apiUtils.bookmarks.getBookmark.invalidate({ bookmarkId: req.bookmarkId }); - apiUtils.assets.list.invalidate(); - return opts[0]?.onSuccess?.(res, req, meta, context); - }, - }); + const api = useTRPC(); + const queryClient = useQueryClient(); + return useMutation( + api.assets.attachAsset.mutationOptions({ + ...opts, + onSuccess: (res, req, meta, context) => { + queryClient.invalidateQueries(api.bookmarks.getBookmarks.pathFilter()); + queryClient.invalidateQueries( + api.bookmarks.searchBookmarks.pathFilter(), + ); + queryClient.invalidateQueries( + api.bookmarks.getBookmark.queryFilter({ bookmarkId: req.bookmarkId }), + ); + queryClient.invalidateQueries(api.assets.list.pathFilter()); + return opts?.onSuccess?.(res, req, meta, context); + }, + }), + ); } export function useReplaceBookmarkAsset( - ...opts: Parameters<typeof api.assets.replaceAsset.useMutation> + opts?: Parameters<TRPCApi["assets"]["replaceAsset"]["mutationOptions"]>[0], ) { - const apiUtils = api.useUtils(); - return api.assets.replaceAsset.useMutation({ - ...opts[0], - onSuccess: (res, req, meta, context) => { - apiUtils.bookmarks.getBookmarks.invalidate(); - apiUtils.bookmarks.searchBookmarks.invalidate(); - apiUtils.bookmarks.getBookmark.invalidate({ bookmarkId: req.bookmarkId }); - apiUtils.assets.list.invalidate(); - return opts[0]?.onSuccess?.(res, req, meta, context); - }, - }); + const api = useTRPC(); + const queryClient = useQueryClient(); + return useMutation( + api.assets.replaceAsset.mutationOptions({ + ...opts, + onSuccess: (res, req, meta, context) => { + queryClient.invalidateQueries(api.bookmarks.getBookmarks.pathFilter()); + queryClient.invalidateQueries( + api.bookmarks.searchBookmarks.pathFilter(), + ); + queryClient.invalidateQueries( + api.bookmarks.getBookmark.queryFilter({ bookmarkId: req.bookmarkId }), + ); + queryClient.invalidateQueries(api.assets.list.pathFilter()); + return opts?.onSuccess?.(res, req, meta, context); + }, + }), + ); } export function useDetachBookmarkAsset( - ...opts: Parameters<typeof api.assets.detachAsset.useMutation> + opts?: Parameters<TRPCApi["assets"]["detachAsset"]["mutationOptions"]>[0], ) { - const apiUtils = api.useUtils(); - return api.assets.detachAsset.useMutation({ - ...opts[0], - onSuccess: (res, req, meta, context) => { - apiUtils.bookmarks.getBookmarks.invalidate(); - apiUtils.bookmarks.searchBookmarks.invalidate(); - apiUtils.bookmarks.getBookmark.invalidate({ bookmarkId: req.bookmarkId }); - apiUtils.assets.list.invalidate(); - return opts[0]?.onSuccess?.(res, req, meta, context); - }, - }); + const api = useTRPC(); + const queryClient = useQueryClient(); + return useMutation( + api.assets.detachAsset.mutationOptions({ + ...opts, + onSuccess: (res, req, meta, context) => { + queryClient.invalidateQueries(api.bookmarks.getBookmarks.pathFilter()); + queryClient.invalidateQueries( + api.bookmarks.searchBookmarks.pathFilter(), + ); + queryClient.invalidateQueries( + api.bookmarks.getBookmark.queryFilter({ bookmarkId: req.bookmarkId }), + ); + queryClient.invalidateQueries(api.assets.list.pathFilter()); + return opts?.onSuccess?.(res, req, meta, context); + }, + }), + ); } diff --git a/packages/shared-react/hooks/bookmarks.ts b/packages/shared-react/hooks/bookmarks.ts index aea2d185..6ff75d7e 100644 --- a/packages/shared-react/hooks/bookmarks.ts +++ b/packages/shared-react/hooks/bookmarks.ts @@ -1,132 +1,196 @@ +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; + import { getBookmarkRefreshInterval } from "@karakeep/shared/utils/bookmarkUtils"; -import { api } from "../trpc"; +import { useTRPC } from "../trpc"; import { useBookmarkGridContext } from "./bookmark-grid-context"; import { useAddBookmarkToList } from "./lists"; +type TRPCApi = ReturnType<typeof useTRPC>; + export function useAutoRefreshingBookmarkQuery( - input: Parameters<typeof api.bookmarks.getBookmark.useQuery>[0], + input: Parameters<TRPCApi["bookmarks"]["getBookmark"]["queryOptions"]>[0], ) { - return api.bookmarks.getBookmark.useQuery(input, { - refetchInterval: (query) => { - const data = query.state.data; - if (!data) { - return false; - } - return getBookmarkRefreshInterval(data); - }, - }); + const api = useTRPC(); + return useQuery( + api.bookmarks.getBookmark.queryOptions(input, { + refetchInterval: (query) => { + const data = query.state.data; + if (!data) { + return false; + } + return getBookmarkRefreshInterval(data); + }, + }), + ); } export function useCreateBookmark( - ...opts: Parameters<typeof api.bookmarks.createBookmark.useMutation> + opts?: Parameters< + TRPCApi["bookmarks"]["createBookmark"]["mutationOptions"] + >[0], ) { - const apiUtils = api.useUtils(); - return api.bookmarks.createBookmark.useMutation({ - ...opts[0], - onSuccess: (res, req, meta, context) => { - apiUtils.bookmarks.getBookmarks.invalidate(); - apiUtils.bookmarks.searchBookmarks.invalidate(); - apiUtils.lists.stats.invalidate(); - return opts[0]?.onSuccess?.(res, req, meta, context); - }, - }); + const api = useTRPC(); + const queryClient = useQueryClient(); + return useMutation( + api.bookmarks.createBookmark.mutationOptions({ + ...opts, + onSuccess: (res, req, meta, context) => { + queryClient.invalidateQueries(api.bookmarks.getBookmarks.pathFilter()); + queryClient.invalidateQueries( + api.bookmarks.searchBookmarks.pathFilter(), + ); + queryClient.invalidateQueries(api.lists.stats.pathFilter()); + return opts?.onSuccess?.(res, req, meta, context); + }, + }), + ); } export function useCreateBookmarkWithPostHook( - ...opts: Parameters<typeof api.bookmarks.createBookmark.useMutation> + opts?: Parameters< + TRPCApi["bookmarks"]["createBookmark"]["mutationOptions"] + >[0], ) { - const apiUtils = api.useUtils(); + const api = useTRPC(); + const queryClient = useQueryClient(); const postCreationCB = useBookmarkPostCreationHook(); - return api.bookmarks.createBookmark.useMutation({ - ...opts[0], - onSuccess: async (res, req, meta, context) => { - apiUtils.bookmarks.getBookmarks.invalidate(); - apiUtils.bookmarks.searchBookmarks.invalidate(); - await postCreationCB(res.id); - return opts[0]?.onSuccess?.(res, req, meta, context); - }, - }); + return useMutation( + api.bookmarks.createBookmark.mutationOptions({ + ...opts, + onSuccess: async (res, req, meta, context) => { + queryClient.invalidateQueries(api.bookmarks.getBookmarks.pathFilter()); + queryClient.invalidateQueries( + api.bookmarks.searchBookmarks.pathFilter(), + ); + await postCreationCB(res.id); + return opts?.onSuccess?.(res, req, meta, context); + }, + }), + ); } export function useDeleteBookmark( - ...opts: Parameters<typeof api.bookmarks.deleteBookmark.useMutation> + opts?: Parameters< + TRPCApi["bookmarks"]["deleteBookmark"]["mutationOptions"] + >[0], ) { - const apiUtils = api.useUtils(); - return api.bookmarks.deleteBookmark.useMutation({ - ...opts[0], - onSuccess: (res, req, meta, context) => { - apiUtils.bookmarks.getBookmarks.invalidate(); - apiUtils.bookmarks.searchBookmarks.invalidate(); - apiUtils.bookmarks.getBookmark.invalidate({ bookmarkId: req.bookmarkId }); - apiUtils.lists.stats.invalidate(); - return opts[0]?.onSuccess?.(res, req, meta, context); - }, - }); + const api = useTRPC(); + const queryClient = useQueryClient(); + return useMutation( + api.bookmarks.deleteBookmark.mutationOptions({ + ...opts, + onSuccess: (res, req, meta, context) => { + queryClient.invalidateQueries(api.bookmarks.getBookmarks.pathFilter()); + queryClient.invalidateQueries( + api.bookmarks.searchBookmarks.pathFilter(), + ); + queryClient.invalidateQueries( + api.bookmarks.getBookmark.queryFilter({ bookmarkId: req.bookmarkId }), + ); + queryClient.invalidateQueries(api.lists.stats.pathFilter()); + return opts?.onSuccess?.(res, req, meta, context); + }, + }), + ); } export function useUpdateBookmark( - ...opts: Parameters<typeof api.bookmarks.updateBookmark.useMutation> + opts?: Parameters< + TRPCApi["bookmarks"]["updateBookmark"]["mutationOptions"] + >[0], ) { - const apiUtils = api.useUtils(); - return api.bookmarks.updateBookmark.useMutation({ - ...opts[0], - onSuccess: (res, req, meta, context) => { - apiUtils.bookmarks.getBookmarks.invalidate(); - apiUtils.bookmarks.searchBookmarks.invalidate(); - apiUtils.bookmarks.getBookmark.invalidate({ bookmarkId: req.bookmarkId }); - apiUtils.lists.stats.invalidate(); - return opts[0]?.onSuccess?.(res, req, meta, context); - }, - }); + const api = useTRPC(); + const queryClient = useQueryClient(); + return useMutation( + api.bookmarks.updateBookmark.mutationOptions({ + ...opts, + onSuccess: (res, req, meta, context) => { + queryClient.invalidateQueries(api.bookmarks.getBookmarks.pathFilter()); + queryClient.invalidateQueries( + api.bookmarks.searchBookmarks.pathFilter(), + ); + queryClient.invalidateQueries( + api.bookmarks.getBookmark.queryFilter({ bookmarkId: req.bookmarkId }), + ); + queryClient.invalidateQueries(api.lists.stats.pathFilter()); + return opts?.onSuccess?.(res, req, meta, context); + }, + }), + ); } export function useSummarizeBookmark( - ...opts: Parameters<typeof api.bookmarks.summarizeBookmark.useMutation> + opts?: Parameters< + TRPCApi["bookmarks"]["summarizeBookmark"]["mutationOptions"] + >[0], ) { - const apiUtils = api.useUtils(); - return api.bookmarks.summarizeBookmark.useMutation({ - ...opts[0], - onSuccess: (res, req, meta, context) => { - apiUtils.bookmarks.getBookmarks.invalidate(); - apiUtils.bookmarks.searchBookmarks.invalidate(); - apiUtils.bookmarks.getBookmark.invalidate({ bookmarkId: req.bookmarkId }); - return opts[0]?.onSuccess?.(res, req, meta, context); - }, - }); + const api = useTRPC(); + const queryClient = useQueryClient(); + return useMutation( + api.bookmarks.summarizeBookmark.mutationOptions({ + ...opts, + onSuccess: (res, req, meta, context) => { + queryClient.invalidateQueries(api.bookmarks.getBookmarks.pathFilter()); + queryClient.invalidateQueries( + api.bookmarks.searchBookmarks.pathFilter(), + ); + queryClient.invalidateQueries( + api.bookmarks.getBookmark.queryFilter({ bookmarkId: req.bookmarkId }), + ); + return opts?.onSuccess?.(res, req, meta, context); + }, + }), + ); } export function useRecrawlBookmark( - ...opts: Parameters<typeof api.bookmarks.recrawlBookmark.useMutation> + opts?: Parameters< + TRPCApi["bookmarks"]["recrawlBookmark"]["mutationOptions"] + >[0], ) { - const apiUtils = api.useUtils(); - return api.bookmarks.recrawlBookmark.useMutation({ - ...opts[0], - onSuccess: (res, req, meta, context) => { - apiUtils.bookmarks.getBookmark.invalidate({ bookmarkId: req.bookmarkId }); - return opts[0]?.onSuccess?.(res, req, meta, context); - }, - }); + const api = useTRPC(); + const queryClient = useQueryClient(); + return useMutation( + api.bookmarks.recrawlBookmark.mutationOptions({ + ...opts, + onSuccess: (res, req, meta, context) => { + queryClient.invalidateQueries( + api.bookmarks.getBookmark.queryFilter({ bookmarkId: req.bookmarkId }), + ); + return opts?.onSuccess?.(res, req, meta, context); + }, + }), + ); } export function useUpdateBookmarkTags( - ...opts: Parameters<typeof api.bookmarks.updateTags.useMutation> + opts?: Parameters<TRPCApi["bookmarks"]["updateTags"]["mutationOptions"]>[0], ) { - const apiUtils = api.useUtils(); - return api.bookmarks.updateTags.useMutation({ - ...opts[0], - onSuccess: (res, req, meta, context) => { - apiUtils.bookmarks.getBookmark.invalidate({ bookmarkId: req.bookmarkId }); + const api = useTRPC(); + const queryClient = useQueryClient(); + return useMutation( + api.bookmarks.updateTags.mutationOptions({ + ...opts, + onSuccess: (res, req, meta, context) => { + queryClient.invalidateQueries( + api.bookmarks.getBookmark.queryFilter({ bookmarkId: req.bookmarkId }), + ); - [...res.attached, ...res.detached].forEach((id) => { - apiUtils.tags.get.invalidate({ tagId: id }); - apiUtils.bookmarks.getBookmarks.invalidate({ tagId: id }); - }); - apiUtils.tags.list.invalidate(); - apiUtils.lists.stats.invalidate(); - return opts[0]?.onSuccess?.(res, req, meta, context); - }, - }); + [...res.attached, ...res.detached].forEach((id) => { + queryClient.invalidateQueries( + api.tags.get.queryFilter({ tagId: id }), + ); + queryClient.invalidateQueries( + api.bookmarks.getBookmarks.queryFilter({ tagId: id }), + ); + }); + queryClient.invalidateQueries(api.tags.list.pathFilter()); + queryClient.invalidateQueries(api.lists.stats.pathFilter()); + return opts?.onSuccess?.(res, req, meta, context); + }, + }), + ); } /** diff --git a/packages/shared-react/hooks/highlights.ts b/packages/shared-react/hooks/highlights.ts index e642f878..3f6a6e01 100644 --- a/packages/shared-react/hooks/highlights.ts +++ b/packages/shared-react/hooks/highlights.ts @@ -1,49 +1,68 @@ -import { api } from "../trpc"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; + +import { useTRPC } from "../trpc"; + +type TRPCApi = ReturnType<typeof useTRPC>; export function useCreateHighlight( - ...opts: Parameters<typeof api.highlights.create.useMutation> + opts?: Parameters<TRPCApi["highlights"]["create"]["mutationOptions"]>[0], ) { - const apiUtils = api.useUtils(); - return api.highlights.create.useMutation({ - ...opts[0], - onSuccess: (res, req, meta, context) => { - apiUtils.highlights.getForBookmark.invalidate({ - bookmarkId: req.bookmarkId, - }); - apiUtils.highlights.getAll.invalidate(); - return opts[0]?.onSuccess?.(res, req, meta, context); - }, - }); + const api = useTRPC(); + const queryClient = useQueryClient(); + return useMutation( + api.highlights.create.mutationOptions({ + ...opts, + onSuccess: (res, req, meta, context) => { + queryClient.invalidateQueries( + api.highlights.getForBookmark.queryFilter({ + bookmarkId: req.bookmarkId, + }), + ); + queryClient.invalidateQueries(api.highlights.getAll.pathFilter()); + return opts?.onSuccess?.(res, req, meta, context); + }, + }), + ); } export function useUpdateHighlight( - ...opts: Parameters<typeof api.highlights.update.useMutation> + opts?: Parameters<TRPCApi["highlights"]["update"]["mutationOptions"]>[0], ) { - const apiUtils = api.useUtils(); - return api.highlights.update.useMutation({ - ...opts[0], - onSuccess: (res, req, meta, context) => { - apiUtils.highlights.getForBookmark.invalidate({ - bookmarkId: res.bookmarkId, - }); - apiUtils.highlights.getAll.invalidate(); - return opts[0]?.onSuccess?.(res, req, meta, context); - }, - }); + const api = useTRPC(); + const queryClient = useQueryClient(); + return useMutation( + api.highlights.update.mutationOptions({ + ...opts, + onSuccess: (res, req, meta, context) => { + queryClient.invalidateQueries( + api.highlights.getForBookmark.queryFilter({ + bookmarkId: res.bookmarkId, + }), + ); + queryClient.invalidateQueries(api.highlights.getAll.pathFilter()); + return opts?.onSuccess?.(res, req, meta, context); + }, + }), + ); } export function useDeleteHighlight( - ...opts: Parameters<typeof api.highlights.delete.useMutation> + opts?: Parameters<TRPCApi["highlights"]["delete"]["mutationOptions"]>[0], ) { - const apiUtils = api.useUtils(); - return api.highlights.delete.useMutation({ - ...opts[0], - onSuccess: (res, req, meta, context) => { - apiUtils.highlights.getForBookmark.invalidate({ - bookmarkId: res.bookmarkId, - }); - apiUtils.highlights.getAll.invalidate(); - return opts[0]?.onSuccess?.(res, req, meta, context); - }, - }); + const api = useTRPC(); + const queryClient = useQueryClient(); + return useMutation( + api.highlights.delete.mutationOptions({ + ...opts, + onSuccess: (res, req, meta, context) => { + queryClient.invalidateQueries( + api.highlights.getForBookmark.queryFilter({ + bookmarkId: res.bookmarkId, + }), + ); + queryClient.invalidateQueries(api.highlights.getAll.pathFilter()); + return opts?.onSuccess?.(res, req, meta, context); + }, + }), + ); } diff --git a/packages/shared-react/hooks/lists.ts b/packages/shared-react/hooks/lists.ts index d269efe3..231e43a7 100644 --- a/packages/shared-react/hooks/lists.ts +++ b/packages/shared-react/hooks/lists.ts @@ -1,113 +1,155 @@ +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; + import { ZBookmarkList } from "@karakeep/shared/types/lists"; import { listsToTree, ZBookmarkListRoot, } from "@karakeep/shared/utils/listUtils"; -import { api } from "../trpc"; +import { useTRPC } from "../trpc"; + +type TRPCApi = ReturnType<typeof useTRPC>; export function useCreateBookmarkList( - ...opts: Parameters<typeof api.lists.create.useMutation> + opts?: Parameters<TRPCApi["lists"]["create"]["mutationOptions"]>[0], ) { - const apiUtils = api.useUtils(); - return api.lists.create.useMutation({ - ...opts[0], - onSuccess: (res, req, meta, context) => { - apiUtils.lists.list.invalidate(); - return opts[0]?.onSuccess?.(res, req, meta, context); - }, - }); + const api = useTRPC(); + const queryClient = useQueryClient(); + return useMutation( + api.lists.create.mutationOptions({ + ...opts, + onSuccess: (res, req, meta, context) => { + queryClient.invalidateQueries(api.lists.list.pathFilter()); + return opts?.onSuccess?.(res, req, meta, context); + }, + }), + ); } export function useEditBookmarkList( - ...opts: Parameters<typeof api.lists.edit.useMutation> + opts?: Parameters<TRPCApi["lists"]["edit"]["mutationOptions"]>[0], ) { - const apiUtils = api.useUtils(); - return api.lists.edit.useMutation({ - ...opts[0], - onSuccess: (res, req, meta, context) => { - apiUtils.lists.list.invalidate(); - apiUtils.lists.get.invalidate({ listId: req.listId }); - if (res.type === "smart") { - apiUtils.bookmarks.getBookmarks.invalidate({ listId: req.listId }); - } - return opts[0]?.onSuccess?.(res, req, meta, context); - }, - }); + const api = useTRPC(); + const queryClient = useQueryClient(); + return useMutation( + api.lists.edit.mutationOptions({ + ...opts, + onSuccess: (res, req, meta, context) => { + queryClient.invalidateQueries(api.lists.list.pathFilter()); + queryClient.invalidateQueries( + api.lists.get.queryFilter({ listId: req.listId }), + ); + if (res.type === "smart") { + queryClient.invalidateQueries( + api.bookmarks.getBookmarks.queryFilter({ listId: req.listId }), + ); + } + return opts?.onSuccess?.(res, req, meta, context); + }, + }), + ); } export function useMergeLists( - ...opts: Parameters<typeof api.lists.merge.useMutation> + opts?: Parameters<TRPCApi["lists"]["merge"]["mutationOptions"]>[0], ) { - const apiUtils = api.useUtils(); - return api.lists.merge.useMutation({ - ...opts[0], - onSuccess: (res, req, meta, context) => { - apiUtils.lists.list.invalidate(); - apiUtils.bookmarks.getBookmarks.invalidate({ listId: req.targetId }); - apiUtils.lists.stats.invalidate(); - return opts[0]?.onSuccess?.(res, req, meta, context); - }, - }); + const api = useTRPC(); + const queryClient = useQueryClient(); + return useMutation( + api.lists.merge.mutationOptions({ + ...opts, + onSuccess: (res, req, meta, context) => { + queryClient.invalidateQueries(api.lists.list.pathFilter()); + queryClient.invalidateQueries( + api.bookmarks.getBookmarks.queryFilter({ listId: req.targetId }), + ); + queryClient.invalidateQueries(api.lists.stats.pathFilter()); + return opts?.onSuccess?.(res, req, meta, context); + }, + }), + ); } export function useAddBookmarkToList( - ...opts: Parameters<typeof api.lists.addToList.useMutation> + opts?: Parameters<TRPCApi["lists"]["addToList"]["mutationOptions"]>[0], ) { - const apiUtils = api.useUtils(); - return api.lists.addToList.useMutation({ - ...opts[0], - onSuccess: (res, req, meta, context) => { - apiUtils.bookmarks.getBookmarks.invalidate({ listId: req.listId }); - apiUtils.lists.getListsOfBookmark.invalidate({ - bookmarkId: req.bookmarkId, - }); - apiUtils.lists.stats.invalidate(); - return opts[0]?.onSuccess?.(res, req, meta, context); - }, - }); + const api = useTRPC(); + const queryClient = useQueryClient(); + return useMutation( + api.lists.addToList.mutationOptions({ + ...opts, + onSuccess: (res, req, meta, context) => { + queryClient.invalidateQueries( + api.bookmarks.getBookmarks.queryFilter({ listId: req.listId }), + ); + queryClient.invalidateQueries( + api.lists.getListsOfBookmark.queryFilter({ + bookmarkId: req.bookmarkId, + }), + ); + queryClient.invalidateQueries(api.lists.stats.pathFilter()); + return opts?.onSuccess?.(res, req, meta, context); + }, + }), + ); } export function useRemoveBookmarkFromList( - ...opts: Parameters<typeof api.lists.removeFromList.useMutation> + opts?: Parameters<TRPCApi["lists"]["removeFromList"]["mutationOptions"]>[0], ) { - const apiUtils = api.useUtils(); - return api.lists.removeFromList.useMutation({ - ...opts[0], - onSuccess: (res, req, meta, context) => { - apiUtils.bookmarks.getBookmarks.invalidate({ listId: req.listId }); - apiUtils.lists.getListsOfBookmark.invalidate({ - bookmarkId: req.bookmarkId, - }); - apiUtils.lists.stats.invalidate(); - return opts[0]?.onSuccess?.(res, req, meta, context); - }, - }); + const api = useTRPC(); + const queryClient = useQueryClient(); + return useMutation( + api.lists.removeFromList.mutationOptions({ + ...opts, + onSuccess: (res, req, meta, context) => { + queryClient.invalidateQueries( + api.bookmarks.getBookmarks.queryFilter({ listId: req.listId }), + ); + queryClient.invalidateQueries( + api.lists.getListsOfBookmark.queryFilter({ + bookmarkId: req.bookmarkId, + }), + ); + queryClient.invalidateQueries(api.lists.stats.pathFilter()); + return opts?.onSuccess?.(res, req, meta, context); + }, + }), + ); } export function useDeleteBookmarkList( - ...opts: Parameters<typeof api.lists.delete.useMutation> + opts?: Parameters<TRPCApi["lists"]["delete"]["mutationOptions"]>[0], ) { - const apiUtils = api.useUtils(); - return api.lists.delete.useMutation({ - ...opts[0], - onSuccess: (res, req, meta, context) => { - apiUtils.lists.list.invalidate(); - apiUtils.lists.get.invalidate({ listId: req.listId }); - return opts[0]?.onSuccess?.(res, req, meta, context); - }, - }); + const api = useTRPC(); + const queryClient = useQueryClient(); + return useMutation( + api.lists.delete.mutationOptions({ + ...opts, + onSuccess: (res, req, meta, context) => { + queryClient.invalidateQueries(api.lists.list.pathFilter()); + queryClient.invalidateQueries( + api.lists.get.queryFilter({ listId: req.listId }), + ); + return opts?.onSuccess?.(res, req, meta, context); + }, + }), + ); } export function useBookmarkLists( - ...opts: Parameters<typeof api.lists.list.useQuery> + input?: Parameters<TRPCApi["lists"]["list"]["queryOptions"]>[0], + opts?: Parameters<TRPCApi["lists"]["list"]["queryOptions"]>[1], ) { - return api.lists.list.useQuery(opts[0], { - ...opts[1], - select: (data) => { - return { data: data.lists, ...listsToTree(data.lists) }; - }, - }); + const api = useTRPC(); + return useQuery( + api.lists.list.queryOptions(input, { + ...opts, + select: (data) => { + return { data: data.lists, ...listsToTree(data.lists) }; + }, + }), + ); } export function augmentBookmarkListsWithInitialData( diff --git a/packages/shared-react/hooks/reader-settings.tsx b/packages/shared-react/hooks/reader-settings.tsx index 2705a050..7258ebe9 100644 --- a/packages/shared-react/hooks/reader-settings.tsx +++ b/packages/shared-react/hooks/reader-settings.tsx @@ -9,6 +9,7 @@ import { useMemo, useState, } from "react"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { READER_DEFAULTS, @@ -16,7 +17,7 @@ import { ReaderSettingsPartial, } from "@karakeep/shared/types/readers"; -import { api } from "../trpc"; +import { useTRPC } from "../trpc"; export interface UseReaderSettingsOptions { /** @@ -39,6 +40,7 @@ export interface UseReaderSettingsOptions { } export function useReaderSettings(options: UseReaderSettingsOptions) { + const api = useTRPC(); const { getLocalOverrides, saveLocalOverrides, @@ -52,8 +54,8 @@ export function useReaderSettings(options: UseReaderSettingsOptions) { const [pendingServerSave, setPendingServerSave] = useState<ReaderSettings | null>(null); - const { data: serverSettings } = api.users.settings.useQuery(); - const apiUtils = api.useUtils(); + const { data: serverSettings } = useQuery(api.users.settings.queryOptions()); + const queryClient = useQueryClient(); // Load local overrides on mount useEffect(() => { @@ -76,30 +78,33 @@ export function useReaderSettings(options: UseReaderSettingsOptions) { } }, [serverSettings, pendingServerSave]); - const { mutate: updateServerSettings, isPending: isSaving } = - api.users.updateSettings.useMutation({ + const { mutate: updateServerSettings, isPending: isSaving } = useMutation( + api.users.updateSettings.mutationOptions({ onSettled: async () => { - await apiUtils.users.settings.refetch(); + await queryClient.refetchQueries(api.users.settings.pathFilter()); }, - }); + }), + ); // Separate mutation for saving defaults (clears local overrides on success) const { mutate: saveServerSettings, isPending: isSavingDefaults } = - api.users.updateSettings.useMutation({ - onSuccess: () => { - // Clear local and session overrides after successful server save - setLocalOverrides({}); - saveLocalOverrides({}); - onClearSessionOverrides?.(); - }, - onError: () => { - // Clear pending state so we don't show values that failed to persist - setPendingServerSave(null); - }, - onSettled: async () => { - await apiUtils.users.settings.refetch(); - }, - }); + useMutation( + api.users.updateSettings.mutationOptions({ + onSuccess: () => { + // Clear local and session overrides after successful server save + setLocalOverrides({}); + saveLocalOverrides({}); + onClearSessionOverrides?.(); + }, + onError: () => { + // Clear pending state so we don't show values that failed to persist + setPendingServerSave(null); + }, + onSettled: async () => { + await queryClient.refetchQueries(api.users.settings.pathFilter()); + }, + }), + ); // Compute effective settings with precedence: session → local → pendingSave → server → default const settings: ReaderSettings = useMemo( diff --git a/packages/shared-react/hooks/rules.ts b/packages/shared-react/hooks/rules.ts index 8428f883..8bca9d69 100644 --- a/packages/shared-react/hooks/rules.ts +++ b/packages/shared-react/hooks/rules.ts @@ -1,40 +1,53 @@ -import { api } from "../trpc"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; + +import { useTRPC } from "../trpc"; + +type TRPCApi = ReturnType<typeof useTRPC>; export function useCreateRule( - ...opts: Parameters<typeof api.rules.create.useMutation> + opts?: Parameters<TRPCApi["rules"]["create"]["mutationOptions"]>[0], ) { - const apiUtils = api.useUtils(); - return api.rules.create.useMutation({ - ...opts[0], - onSuccess: (res, req, meta, context) => { - apiUtils.rules.list.invalidate(); - return opts[0]?.onSuccess?.(res, req, meta, context); - }, - }); + const api = useTRPC(); + const queryClient = useQueryClient(); + return useMutation( + api.rules.create.mutationOptions({ + ...opts, + onSuccess: (res, req, meta, context) => { + queryClient.invalidateQueries(api.rules.list.pathFilter()); + return opts?.onSuccess?.(res, req, meta, context); + }, + }), + ); } export function useUpdateRule( - ...opts: Parameters<typeof api.rules.update.useMutation> + opts?: Parameters<TRPCApi["rules"]["update"]["mutationOptions"]>[0], ) { - const apiUtils = api.useUtils(); - return api.rules.update.useMutation({ - ...opts[0], - onSuccess: (res, req, meta, context) => { - apiUtils.rules.list.invalidate(); - return opts[0]?.onSuccess?.(res, req, meta, context); - }, - }); + const api = useTRPC(); + const queryClient = useQueryClient(); + return useMutation( + api.rules.update.mutationOptions({ + ...opts, + onSuccess: (res, req, meta, context) => { + queryClient.invalidateQueries(api.rules.list.pathFilter()); + return opts?.onSuccess?.(res, req, meta, context); + }, + }), + ); } export function useDeleteRule( - ...opts: Parameters<typeof api.rules.delete.useMutation> + opts?: Parameters<TRPCApi["rules"]["delete"]["mutationOptions"]>[0], ) { - const apiUtils = api.useUtils(); - return api.rules.delete.useMutation({ - ...opts[0], - onSuccess: (res, req, meta, context) => { - apiUtils.rules.list.invalidate(); - return opts[0]?.onSuccess?.(res, req, meta, context); - }, - }); + const api = useTRPC(); + const queryClient = useQueryClient(); + return useMutation( + api.rules.delete.mutationOptions({ + ...opts, + onSuccess: (res, req, meta, context) => { + queryClient.invalidateQueries(api.rules.list.pathFilter()); + return opts?.onSuccess?.(res, req, meta, context); + }, + }), + ); } diff --git a/packages/shared-react/hooks/tags.ts b/packages/shared-react/hooks/tags.ts index e1e7416f..a616b88a 100644 --- a/packages/shared-react/hooks/tags.ts +++ b/packages/shared-react/hooks/tags.ts @@ -1,120 +1,155 @@ -import { keepPreviousData } from "@tanstack/react-query"; +import { + keepPreviousData, + useInfiniteQuery, + useMutation, + useQuery, + useQueryClient, +} from "@tanstack/react-query"; import { ZTagListResponse } from "@karakeep/shared/types/tags"; -import { api } from "../trpc"; +import { useTRPC } from "../trpc"; + +type TRPCApi = ReturnType<typeof useTRPC>; export function usePaginatedSearchTags( - input: Parameters<typeof api.tags.list.useInfiniteQuery>[0], + input: Parameters<TRPCApi["tags"]["list"]["infiniteQueryOptions"]>[0], ) { - return api.tags.list.useInfiniteQuery(input, { - placeholderData: keepPreviousData, - getNextPageParam: (lastPage) => lastPage.nextCursor, + const api = useTRPC(); + return useInfiniteQuery({ + ...api.tags.list.infiniteQueryOptions(input, { + placeholderData: keepPreviousData, + getNextPageParam: (lastPage) => lastPage.nextCursor, + gcTime: 60_000, + }), select: (data) => ({ tags: data.pages.flatMap((page) => page.tags), }), - gcTime: 60_000, }); } -export function useTagAutocomplete<T>(opts: { +export function useTagAutocomplete<T = ZTagListResponse>(opts: { nameContains: string; - select?: (tags: ZTagListResponse) => T; + select?: (data: ZTagListResponse) => T; enabled?: boolean; }) { - return api.tags.list.useQuery( - { - nameContains: opts.nameContains, - limit: 50, - sortBy: opts.nameContains ? "relevance" : "usage", - }, - { - select: opts.select, - placeholderData: keepPreviousData, - gcTime: opts.nameContains?.length > 0 ? 60_000 : 3_600_000, - enabled: opts.enabled, - }, - ); + const api = useTRPC(); + return useQuery({ + ...api.tags.list.queryOptions( + { + nameContains: opts.nameContains, + limit: 50, + sortBy: opts.nameContains ? "relevance" : "usage", + }, + { + placeholderData: keepPreviousData, + gcTime: opts.nameContains?.length > 0 ? 60_000 : 3_600_000, + enabled: opts.enabled, + }, + ), + select: opts.select, + }); } export function useCreateTag( - ...opts: Parameters<typeof api.tags.create.useMutation> + opts?: Parameters<TRPCApi["tags"]["create"]["mutationOptions"]>[0], ) { - const apiUtils = api.useUtils(); + const api = useTRPC(); + const queryClient = useQueryClient(); - return api.tags.create.useMutation({ - ...opts[0], - onSuccess: (res, req, meta, context) => { - apiUtils.tags.list.invalidate(); - return opts[0]?.onSuccess?.(res, req, meta, context); - }, - }); + return useMutation( + api.tags.create.mutationOptions({ + ...opts, + onSuccess: (res, req, meta, context) => { + queryClient.invalidateQueries(api.tags.list.pathFilter()); + return opts?.onSuccess?.(res, req, meta, context); + }, + }), + ); } export function useUpdateTag( - ...opts: Parameters<typeof api.tags.update.useMutation> + opts?: Parameters<TRPCApi["tags"]["update"]["mutationOptions"]>[0], ) { - const apiUtils = api.useUtils(); + const api = useTRPC(); + const queryClient = useQueryClient(); - return api.tags.update.useMutation({ - ...opts[0], - onSuccess: (res, req, meta, context) => { - apiUtils.tags.list.invalidate(); - apiUtils.tags.get.invalidate({ tagId: res.id }); - apiUtils.bookmarks.getBookmarks.invalidate({ tagId: res.id }); + return useMutation( + api.tags.update.mutationOptions({ + ...opts, + onSuccess: (res, req, meta, context) => { + queryClient.invalidateQueries(api.tags.list.pathFilter()); + queryClient.invalidateQueries( + api.tags.get.queryFilter({ tagId: res.id }), + ); + queryClient.invalidateQueries( + api.bookmarks.getBookmarks.queryFilter({ tagId: res.id }), + ); - // TODO: Maybe we can only look at the cache and invalidate only affected bookmarks - apiUtils.bookmarks.getBookmark.invalidate(); - return opts[0]?.onSuccess?.(res, req, meta, context); - }, - }); + // TODO: Maybe we can only look at the cache and invalidate only affected bookmarks + queryClient.invalidateQueries(api.bookmarks.getBookmark.pathFilter()); + return opts?.onSuccess?.(res, req, meta, context); + }, + }), + ); } export function useMergeTag( - ...opts: Parameters<typeof api.tags.merge.useMutation> + opts?: Parameters<TRPCApi["tags"]["merge"]["mutationOptions"]>[0], ) { - const apiUtils = api.useUtils(); + const api = useTRPC(); + const queryClient = useQueryClient(); - return api.tags.merge.useMutation({ - ...opts[0], - onSuccess: (res, req, meta, context) => { - apiUtils.tags.list.invalidate(); - [res.mergedIntoTagId, ...res.deletedTags].forEach((tagId) => { - apiUtils.tags.get.invalidate({ tagId }); - apiUtils.bookmarks.getBookmarks.invalidate({ tagId }); - }); - // TODO: Maybe we can only look at the cache and invalidate only affected bookmarks - apiUtils.bookmarks.getBookmark.invalidate(); - return opts[0]?.onSuccess?.(res, req, meta, context); - }, - }); + return useMutation( + api.tags.merge.mutationOptions({ + ...opts, + onSuccess: (res, req, meta, context) => { + queryClient.invalidateQueries(api.tags.list.pathFilter()); + [res.mergedIntoTagId, ...res.deletedTags].forEach((tagId) => { + queryClient.invalidateQueries(api.tags.get.queryFilter({ tagId })); + queryClient.invalidateQueries( + api.bookmarks.getBookmarks.queryFilter({ tagId }), + ); + }); + // TODO: Maybe we can only look at the cache and invalidate only affected bookmarks + queryClient.invalidateQueries(api.bookmarks.getBookmark.pathFilter()); + return opts?.onSuccess?.(res, req, meta, context); + }, + }), + ); } export function useDeleteTag( - ...opts: Parameters<typeof api.tags.delete.useMutation> + opts?: Parameters<TRPCApi["tags"]["delete"]["mutationOptions"]>[0], ) { - const apiUtils = api.useUtils(); + const api = useTRPC(); + const queryClient = useQueryClient(); - return api.tags.delete.useMutation({ - ...opts[0], - onSuccess: (res, req, meta, context) => { - apiUtils.tags.list.invalidate(); - apiUtils.bookmarks.getBookmark.invalidate(); - return opts[0]?.onSuccess?.(res, req, meta, context); - }, - }); + return useMutation( + api.tags.delete.mutationOptions({ + ...opts, + onSuccess: (res, req, meta, context) => { + queryClient.invalidateQueries(api.tags.list.pathFilter()); + queryClient.invalidateQueries(api.bookmarks.getBookmark.pathFilter()); + return opts?.onSuccess?.(res, req, meta, context); + }, + }), + ); } export function useDeleteUnusedTags( - ...opts: Parameters<typeof api.tags.deleteUnused.useMutation> + opts?: Parameters<TRPCApi["tags"]["deleteUnused"]["mutationOptions"]>[0], ) { - const apiUtils = api.useUtils(); + const api = useTRPC(); + const queryClient = useQueryClient(); - return api.tags.deleteUnused.useMutation({ - ...opts[0], - onSuccess: (res, req, meta, context) => { - apiUtils.tags.list.invalidate(); - return opts[0]?.onSuccess?.(res, req, meta, context); - }, - }); + return useMutation( + api.tags.deleteUnused.mutationOptions({ + ...opts, + onSuccess: (res, req, meta, context) => { + queryClient.invalidateQueries(api.tags.list.pathFilter()); + return opts?.onSuccess?.(res, req, meta, context); + }, + }), + ); } diff --git a/packages/shared-react/hooks/users.ts b/packages/shared-react/hooks/users.ts index b1909761..221681a4 100644 --- a/packages/shared-react/hooks/users.ts +++ b/packages/shared-react/hooks/users.ts @@ -1,37 +1,49 @@ -import { api } from "../trpc"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; + +import { useTRPC } from "../trpc"; + +type TRPCApi = ReturnType<typeof useTRPC>; export function useUpdateUserSettings( - ...opts: Parameters<typeof api.users.updateSettings.useMutation> + opts?: Parameters<TRPCApi["users"]["updateSettings"]["mutationOptions"]>[0], ) { - const apiUtils = api.useUtils(); - return api.users.updateSettings.useMutation({ - ...opts[0], - onSuccess: (res, req, meta, context) => { - apiUtils.users.settings.invalidate(); - return opts[0]?.onSuccess?.(res, req, meta, context); - }, - }); + const api = useTRPC(); + const queryClient = useQueryClient(); + return useMutation( + api.users.updateSettings.mutationOptions({ + ...opts, + onSuccess: (res, req, meta, context) => { + queryClient.invalidateQueries(api.users.settings.pathFilter()); + return opts?.onSuccess?.(res, req, meta, context); + }, + }), + ); } export function useUpdateUserAvatar( - ...opts: Parameters<typeof api.users.updateAvatar.useMutation> + opts?: Parameters<TRPCApi["users"]["updateAvatar"]["mutationOptions"]>[0], ) { - const apiUtils = api.useUtils(); - return api.users.updateAvatar.useMutation({ - ...opts[0], - onSuccess: (res, req, meta, context) => { - apiUtils.users.whoami.invalidate(); - return opts[0]?.onSuccess?.(res, req, meta, context); - }, - }); + const api = useTRPC(); + const queryClient = useQueryClient(); + return useMutation( + api.users.updateAvatar.mutationOptions({ + ...opts, + onSuccess: (res, req, meta, context) => { + queryClient.invalidateQueries(api.users.whoami.pathFilter()); + return opts?.onSuccess?.(res, req, meta, context); + }, + }), + ); } export function useDeleteAccount( - ...opts: Parameters<typeof api.users.deleteAccount.useMutation> + opts?: Parameters<TRPCApi["users"]["deleteAccount"]["mutationOptions"]>[0], ) { - return api.users.deleteAccount.useMutation(opts[0]); + const api = useTRPC(); + return useMutation(api.users.deleteAccount.mutationOptions(opts)); } export function useWhoAmI() { - return api.users.whoami.useQuery(); + const api = useTRPC(); + return useQuery(api.users.whoami.queryOptions()); } diff --git a/packages/shared-react/package.json b/packages/shared-react/package.json index cf0539be..ce29a24e 100644 --- a/packages/shared-react/package.json +++ b/packages/shared-react/package.json @@ -8,7 +8,8 @@ "@karakeep/shared": "workspace:^0.1.0", "@karakeep/trpc": "workspace:^0.1.0", "@tanstack/react-query": "5.90.2", - "@trpc/client": "^11.4.3", + "@trpc/client": "^11.9.0", + "@trpc/tanstack-react-query": "^11.9.0", "superjson": "^2.2.1" }, "devDependencies": { diff --git a/packages/shared-react/providers/trpc-provider.tsx b/packages/shared-react/providers/trpc-provider.tsx index 696bf195..2c41aa11 100644 --- a/packages/shared-react/providers/trpc-provider.tsx +++ b/packages/shared-react/providers/trpc-provider.tsx @@ -1,9 +1,11 @@ import { useMemo } from "react"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; -import { httpBatchLink } from "@trpc/client"; +import { createTRPCClient, httpBatchLink } from "@trpc/client"; import superjson from "superjson"; -import { api } from "../trpc"; +import type { AppRouter } from "@karakeep/trpc/routers/_app"; + +import { TRPCProvider } from "../trpc"; interface Settings { apiKey?: string; @@ -12,7 +14,7 @@ interface Settings { } function getTRPCClient(settings: Settings) { - return api.createClient({ + return createTRPCClient<AppRouter>({ links: [ httpBatchLink({ url: `${settings.address}/api/trpc`, @@ -31,7 +33,7 @@ function getTRPCClient(settings: Settings) { }); } -export function TRPCProvider({ +export function TRPCSettingsProvider({ settings, children, }: { @@ -42,8 +44,10 @@ export function TRPCProvider({ const trpcClient = useMemo(() => getTRPCClient(settings), [settings]); return ( - <api.Provider client={trpcClient} queryClient={queryClient}> - <QueryClientProvider client={queryClient}>{children}</QueryClientProvider> - </api.Provider> + <QueryClientProvider client={queryClient}> + <TRPCProvider trpcClient={trpcClient} queryClient={queryClient}> + {children} + </TRPCProvider> + </QueryClientProvider> ); } diff --git a/packages/shared-react/trpc.ts b/packages/shared-react/trpc.ts index 1478684f..2a36b257 100644 --- a/packages/shared-react/trpc.ts +++ b/packages/shared-react/trpc.ts @@ -1,7 +1,7 @@ "use client"; -import { createTRPCReact } from "@trpc/react-query"; +import { createTRPCContext } from "@trpc/tanstack-react-query"; import type { AppRouter } from "@karakeep/trpc/routers/_app"; -export const api = createTRPCReact<AppRouter>(); +export const { TRPCProvider, useTRPC } = createTRPCContext<AppRouter>(); diff --git a/packages/trpc/package.json b/packages/trpc/package.json index d9fa12c0..d50a2174 100644 --- a/packages/trpc/package.json +++ b/packages/trpc/package.json @@ -17,7 +17,7 @@ "@karakeep/plugins": "workspace:*", "@karakeep/shared": "workspace:*", "@karakeep/shared-server": "workspace:*", - "@trpc/server": "^11.4.3", + "@trpc/server": "^11.9.0", "bcryptjs": "^2.4.3", "deep-equal": "^2.2.3", "drizzle-orm": "^0.44.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 56e9081e..a1ceb31e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -69,14 +69,14 @@ importers: specifier: 5.90.2 version: 5.90.2(@tanstack/react-query@5.90.2(react@19.2.3))(react@19.2.3) '@trpc/client': - specifier: ^11.4.3 - version: 11.4.3(@trpc/server@11.4.3(typescript@5.9.3))(typescript@5.9.3) - '@trpc/react-query': - specifier: ^11.4.3 - version: 11.4.3(@tanstack/react-query@5.90.2(react@19.2.3))(@trpc/client@11.4.3(@trpc/server@11.4.3(typescript@5.9.3))(typescript@5.9.3))(@trpc/server@11.4.3(typescript@5.9.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3) + specifier: ^11.9.0 + version: 11.9.0(@trpc/server@11.9.0(typescript@5.9.3))(typescript@5.9.3) '@trpc/server': - specifier: ^11.4.3 - version: 11.4.3(typescript@5.9.3) + specifier: ^11.9.0 + version: 11.9.0(typescript@5.9.3) + '@trpc/tanstack-react-query': + specifier: ^11.9.0 + version: 11.9.0(@tanstack/react-query@5.90.2(react@19.2.3))(@trpc/client@11.9.0(@trpc/server@11.9.0(typescript@5.9.3))(typescript@5.9.3))(@trpc/server@11.9.0(typescript@5.9.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3) '@uidotdev/usehooks': specifier: ^2.4.1 version: 2.4.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -172,11 +172,11 @@ importers: specifier: workspace:^0.1.0 version: link:../../tooling/typescript '@trpc/client': - specifier: ^11.4.3 - version: 11.4.3(@trpc/server@11.4.3(typescript@5.9.3))(typescript@5.9.3) + specifier: ^11.9.0 + version: 11.9.0(@trpc/server@11.9.0(typescript@5.9.3))(typescript@5.9.3) '@trpc/server': - specifier: ^11.4.3 - version: 11.4.3(typescript@5.9.3) + specifier: ^11.9.0 + version: 11.9.0(typescript@5.9.3) '@tsconfig/node22': specifier: ^22.0.0 version: 22.0.2 @@ -622,14 +622,14 @@ importers: specifier: 5.90.2 version: 5.90.2(@tanstack/react-query@5.90.2(react@19.2.3))(react@19.2.3) '@trpc/client': - specifier: ^11.4.3 - version: 11.4.3(@trpc/server@11.4.3(typescript@5.9.3))(typescript@5.9.3) - '@trpc/react-query': - specifier: ^11.4.3 - version: 11.4.3(@tanstack/react-query@5.90.2(react@19.2.3))(@trpc/client@11.4.3(@trpc/server@11.4.3(typescript@5.9.3))(typescript@5.9.3))(@trpc/server@11.4.3(typescript@5.9.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3) + specifier: ^11.9.0 + version: 11.9.0(@trpc/server@11.9.0(typescript@5.9.3))(typescript@5.9.3) '@trpc/server': - specifier: ^11.4.3 - version: 11.4.3(typescript@5.9.3) + specifier: ^11.9.0 + version: 11.9.0(typescript@5.9.3) + '@trpc/tanstack-react-query': + specifier: ^11.9.0 + version: 11.9.0(@tanstack/react-query@5.90.2(react@19.2.3))(@trpc/client@11.9.0(@trpc/server@11.9.0(typescript@5.9.3))(typescript@5.9.3))(@trpc/server@11.9.0(typescript@5.9.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3) cheerio: specifier: ^1.0.0 version: 1.0.0 @@ -1046,7 +1046,7 @@ importers: version: 1.0.2(hono@4.10.6)(prom-client@15.1.3) '@hono/trpc-server': specifier: ^0.4.0 - version: 0.4.0(@trpc/server@11.4.3(typescript@5.9.3))(hono@4.10.6) + version: 0.4.0(@trpc/server@11.9.0(typescript@5.9.3))(hono@4.10.6) '@hono/zod-validator': specifier: ^0.5.0 version: 0.5.0(hono@4.10.6)(zod@3.24.2) @@ -1063,8 +1063,8 @@ importers: specifier: workspace:* version: link:../trpc '@trpc/server': - specifier: ^11.4.3 - version: 11.4.3(typescript@5.9.3) + specifier: ^11.9.0 + version: 11.9.0(typescript@5.9.3) drizzle-orm: specifier: ^0.44.2 version: 0.44.2(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(better-sqlite3@11.3.0)(gel@2.1.0)(kysely@0.28.5) @@ -1115,8 +1115,8 @@ importers: specifier: workspace:^0.1.0 version: link:../trpc '@trpc/client': - specifier: ^11.4.3 - version: 11.4.3(@trpc/server@11.4.3(typescript@5.9.3))(typescript@5.9.3) + specifier: ^11.9.0 + version: 11.9.0(@trpc/server@11.9.0(typescript@5.9.3))(typescript@5.9.3) p-limit: specifier: ^7.2.0 version: 7.2.0 @@ -1204,8 +1204,8 @@ importers: specifier: workspace:^0.1.0 version: link:../trpc '@trpc/client': - specifier: ^11.4.3 - version: 11.4.3(@trpc/server@11.4.3(typescript@5.9.3))(typescript@5.9.3) + specifier: ^11.9.0 + version: 11.9.0(@trpc/server@11.9.0(typescript@5.9.3))(typescript@5.9.3) superjson: specifier: ^2.2.1 version: 2.2.1 @@ -1380,8 +1380,11 @@ importers: specifier: 5.90.2 version: 5.90.2(react@19.2.3) '@trpc/client': - specifier: ^11.4.3 - version: 11.4.3(@trpc/server@11.4.3(typescript@5.9.3))(typescript@5.9.3) + specifier: ^11.9.0 + version: 11.9.0(@trpc/server@11.9.0(typescript@5.9.3))(typescript@5.9.3) + '@trpc/tanstack-react-query': + specifier: ^11.9.0 + version: 11.9.0(@tanstack/react-query@5.90.2(react@19.2.3))(@trpc/client@11.9.0(@trpc/server@11.9.0(typescript@5.9.3))(typescript@5.9.3))(@trpc/server@11.9.0(typescript@5.9.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3) react: specifier: ^19.2.1 version: 19.2.3 @@ -1451,8 +1454,8 @@ importers: specifier: workspace:* version: link:../shared-server '@trpc/server': - specifier: ^11.4.3 - version: 11.4.3(typescript@5.9.3) + specifier: ^11.9.0 + version: 11.9.0(typescript@5.9.3) bcryptjs: specifier: ^2.4.3 version: 2.4.3 @@ -6167,25 +6170,25 @@ packages: '@tokenizer/token@0.3.0': resolution: {integrity: sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==} - '@trpc/client@11.4.3': - resolution: {integrity: sha512-i2suttUCfColktXT8bqex5kHW5jpT15nwUh0hGSDiW1keN621kSUQKcLJ095blqQAUgB+lsmgSqSMmB4L9shQQ==} + '@trpc/client@11.9.0': + resolution: {integrity: sha512-3r4RT/GbR263QO+2gCPyrs5fEYaXua3/AzCs+GbWC09X0F+mVkyBpO3GRSDObiNU/N1YB597U7WGW3WA1d1TVw==} peerDependencies: - '@trpc/server': 11.4.3 + '@trpc/server': 11.9.0 typescript: '>=5.7.2' - '@trpc/react-query@11.4.3': - resolution: {integrity: sha512-z+jhAiOBD22NNhHtvF0iFp9hO36YFA7M8AiUu/XtNmMxyLd3Y9/d1SMjMwlTdnGqxEGPo41VEWBrdhDUGtUuHg==} + '@trpc/server@11.9.0': + resolution: {integrity: sha512-T8gC4NOCzx8tCsQEQ5sSjf24bN+9AEqXZRfpThG+YCEmcEwXfS7RP8VVrl5Vodt1S+zGEDyQSof4YVAj1zq/mg==} peerDependencies: - '@tanstack/react-query': ^5.80.3 - '@trpc/client': 11.4.3 - '@trpc/server': 11.4.3 - react: '>=18.2.0' - react-dom: '>=18.2.0' typescript: '>=5.7.2' - '@trpc/server@11.4.3': - resolution: {integrity: sha512-wnWq3wiLlMOlYkaIZz+qbuYA5udPTLS4GVVRyFkr6aT83xpdCHyVtURT+u4hSoIrOXQM9OPCNXSXsAujWZDdaw==} + '@trpc/tanstack-react-query@11.9.0': + resolution: {integrity: sha512-EV9/m9HTD4yrZPI051+lKbKSwvRQpoF+1fIEoYHuJ009yFtCdCovn8XOwsdcugOQu84OCs11qDDKCEmUCNXUIg==} peerDependencies: + '@tanstack/react-query': ^5.80.3 + '@trpc/client': 11.9.0 + '@trpc/server': 11.9.0 + react: '>=18.2.0' + react-dom: '>=18.2.0' typescript: '>=5.7.2' '@trysound/sax@0.2.0': @@ -14391,10 +14394,12 @@ packages: tar@7.4.3: resolution: {integrity: sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==} engines: {node: '>=18'} + deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exhorbitant rates) by contacting i@izs.me tar@7.5.3: resolution: {integrity: sha512-ENg5JUHUm2rDD7IvKNFGzyElLXNjachNLp6RaGf4+JOgxXHkqA+gq81ZAMCUmtMtqBsoU62lcp6S27g1LCYGGQ==} engines: {node: '>=18'} + deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exhorbitant rates) by contacting i@izs.me tdigest@0.1.2: resolution: {integrity: sha512-+G0LLgjjo9BZX2MfdvPfH+MKLCrxlXSYec5DaPYP1fe6Iyhf0/fSmJ0bFiZ1F8BT6cGXl2LpltQptzjXKWEkKA==} @@ -19324,9 +19329,9 @@ snapshots: hono: 4.10.6 prom-client: 15.1.3 - '@hono/trpc-server@0.4.0(@trpc/server@11.4.3(typescript@5.9.3))(hono@4.10.6)': + '@hono/trpc-server@0.4.0(@trpc/server@11.9.0(typescript@5.9.3))(hono@4.10.6)': dependencies: - '@trpc/server': 11.4.3(typescript@5.9.3) + '@trpc/server': 11.9.0(typescript@5.9.3) hono: 4.10.6 '@hono/zod-validator@0.5.0(hono@4.10.6)(zod@3.24.2)': @@ -22224,22 +22229,22 @@ snapshots: '@tokenizer/token@0.3.0': {} - '@trpc/client@11.4.3(@trpc/server@11.4.3(typescript@5.9.3))(typescript@5.9.3)': + '@trpc/client@11.9.0(@trpc/server@11.9.0(typescript@5.9.3))(typescript@5.9.3)': dependencies: - '@trpc/server': 11.4.3(typescript@5.9.3) + '@trpc/server': 11.9.0(typescript@5.9.3) typescript: 5.9.3 - '@trpc/react-query@11.4.3(@tanstack/react-query@5.90.2(react@19.2.3))(@trpc/client@11.4.3(@trpc/server@11.4.3(typescript@5.9.3))(typescript@5.9.3))(@trpc/server@11.4.3(typescript@5.9.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)': + '@trpc/server@11.9.0(typescript@5.9.3)': dependencies: - '@tanstack/react-query': 5.90.2(react@19.2.3) - '@trpc/client': 11.4.3(@trpc/server@11.4.3(typescript@5.9.3))(typescript@5.9.3) - '@trpc/server': 11.4.3(typescript@5.9.3) - react: 19.2.3 - react-dom: 19.2.3(react@19.2.3) typescript: 5.9.3 - '@trpc/server@11.4.3(typescript@5.9.3)': + '@trpc/tanstack-react-query@11.9.0(@tanstack/react-query@5.90.2(react@19.2.3))(@trpc/client@11.9.0(@trpc/server@11.9.0(typescript@5.9.3))(typescript@5.9.3))(@trpc/server@11.9.0(typescript@5.9.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)': dependencies: + '@tanstack/react-query': 5.90.2(react@19.2.3) + '@trpc/client': 11.9.0(@trpc/server@11.9.0(typescript@5.9.3))(typescript@5.9.3) + '@trpc/server': 11.9.0(typescript@5.9.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) typescript: 5.9.3 '@trysound/sax@0.2.0': {} |
