fix(web): refresh tracking-dependent queries after mutations

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
jakeandClaude Sonnet 5.5 committed 2026-10-04 15:58:41 -04:00
1 parent 923924a76f
commit 8a7dc2c8c0
5 files changed
+49 -7

No files matched your search

@@ -12,7 +12,7 @@ import {
IconTrash,
IconX,
} from "@tabler/icons-react";
import { useMutation } from "@tanstack/react-query";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { useNavigate, useRouter } from "@tanstack/react-router";
import { AnimatePresence, motion } from "motion/react";
import { useEffect, useMemo, useRef, useState } from "react";
@@ -44,6 +44,7 @@ import { authClient, signOut } from "@/lib/auth/client";
import { getErrorMessage } from "@/lib/error-messages";
import { useAppForm } from "@/lib/form";
import { client, orpc } from "@/lib/orpc/client";
import { invalidateTrackingQueries } from "@/lib/orpc/invalidate";
import type { NormalizedImport } from "@sofa/api/schemas";
import { formatDate } from "@sofa/i18n/format";
@@ -379,6 +380,7 @@ interface ImportResult {
function SofaImportDialog() {
const { t } = useLingui();
const queryClient = useQueryClient();
const fileInputRef = useRef<HTMLInputElement>(null);
const importAbortRef = useRef<AbortController | null>(null);
const [open, setOpen] = useState(false);
@@ -448,6 +450,7 @@ function SofaImportDialog() {
warnings: event.job.warnings,
});
setStep("done");
void invalidateTrackingQueries(queryClient);
const importedCount = event.job.importedCount;
if (importedCount > 0) {
toast.success(t`Imported ${importedCount} items from Sofa export`);
@@ -481,6 +484,7 @@ function SofaImportDialog() {
warnings: finalJob.warnings,
});
setStep("done");
void invalidateTrackingQueries(queryClient);
} else {
toast.info(t`Import is still running in the background. Check back later.`);
handleClose();
@@ -2,7 +2,7 @@ import type { MessageDescriptor } from "@lingui/core";
import { msg } from "@lingui/core/macro";
import { Trans, useLingui } from "@lingui/react/macro";
import { IconCloudUpload, IconLink } from "@tabler/icons-react";
import { useMutation, useQuery } from "@tanstack/react-query";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { useCallback, useEffect, useRef, useState } from "react";
import { toast } from "sonner";
@@ -23,6 +23,7 @@ import { Progress } from "@/components/ui/progress";
import { Spinner } from "@/components/ui/spinner";
import { getErrorMessage } from "@/lib/error-messages";
import { client, orpc } from "@/lib/orpc/client";
import { invalidateTrackingQueries } from "@/lib/orpc/invalidate";
import type { NormalizedImport } from "@sofa/api/schemas";
import { formatList } from "@sofa/i18n/format";
@@ -192,6 +193,7 @@ export function ImportsSection() {
function ImportSourceCard({ config }: { config: SourceConfig }) {
const { i18n, t } = useLingui();
const queryClient = useQueryClient();
const { data: systemStatus } = useQuery(orpc.system.status.queryOptions());
const publicApiUrl = systemStatus?.publicApiUrl ?? "https://public-api.sofa.watch";
const fileInputRef = useRef<HTMLInputElement>(null);
@@ -285,6 +287,7 @@ function ImportSourceCard({ config }: { config: SourceConfig }) {
warnings: event.job.warnings,
});
setStep("done");
void invalidateTrackingQueries(queryClient);
const importedCount = event.job.importedCount;
const sourceLabel = config.label;
if (importedCount > 0) {
@@ -320,6 +323,7 @@ function ImportSourceCard({ config }: { config: SourceConfig }) {
warnings: finalJob.warnings,
});
setStep("done");
void invalidateTrackingQueries(queryClient);
} else {
toast.info(t`Import is still running in the background. Check back later.`);
setStep("preview");
+6 -1
View File
@@ -10,15 +10,17 @@ import {
IconPlus,
IconStarFilled,
} from "@tabler/icons-react";
import { useMutation } from "@tanstack/react-query";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { Link } from "@tanstack/react-router";
import { type MotionStyle, type MotionValue, motion } from "motion/react";
import { useState } from "react";
import { toast } from "sonner";
import { Skeleton } from "@/components/ui/skeleton";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { useTiltEffect } from "@/hooks/use-tilt-effect";
import { orpc } from "@/lib/orpc/client";
import { invalidateTrackingQueries } from "@/lib/orpc/invalidate";
import { thumbHashToUrl } from "@/lib/thumbhash";
export function TitleCardSkeleton() {
@@ -86,11 +88,14 @@ function useStatusConfig() {
function QuickAddButton({ id, userStatus }: { id: string; userStatus?: TitleStatus | null }) {
const { t } = useLingui();
const statusConfig = useStatusConfig();
const queryClient = useQueryClient();
const [optimisticStatus, setOptimisticStatus] = useState<TitleStatus | null>(null);
const addToWatchlistMutation = useMutation(
orpc.tracking.updateStatus.mutationOptions({
onSuccess: () => setOptimisticStatus("in_watchlist"),
onError: () => toast.error(t`Failed to update status`),
onSettled: () => void invalidateTrackingQueries(queryClient),
}),
);
@@ -5,6 +5,7 @@ import { useCallback } from "react";
import { toast } from "sonner";
import { orpc } from "@/lib/orpc/client";
import { invalidateTrackingQueries } from "@/lib/orpc/invalidate";
import type { Season } from "@sofa/api/schemas";
import { useTitleContext } from "./title-context";
@@ -40,10 +41,19 @@ export function useTitleActions() {
[queryClient, userInfoKey],
);
const { mutateAsync: watch } = useMutation(orpc.tracking.watch.mutationOptions());
const { mutateAsync: unwatch } = useMutation(orpc.tracking.unwatch.mutationOptions());
const { mutateAsync: updateStatus } = useMutation(orpc.tracking.updateStatus.mutationOptions());
const { mutateAsync: updateRating } = useMutation(orpc.tracking.rate.mutationOptions());
const invalidateAfterMutation = { onSettled: () => void invalidateTrackingQueries(queryClient) };
const { mutateAsync: watch } = useMutation(
orpc.tracking.watch.mutationOptions(invalidateAfterMutation),
);
const { mutateAsync: unwatch } = useMutation(
orpc.tracking.unwatch.mutationOptions(invalidateAfterMutation),
);
const { mutateAsync: updateStatus } = useMutation(
orpc.tracking.updateStatus.mutationOptions(invalidateAfterMutation),
);
const { mutateAsync: updateRating } = useMutation(
orpc.tracking.rate.mutationOptions(invalidateAfterMutation),
);
const catchUp = useCallback(
async (episodeIds: string[]) => {
+19
View File
@@ -0,0 +1,19 @@
import type { QueryClient } from "@tanstack/react-query";
import { orpc } from "./client";
/**
* Mark every query whose response depends on the user's tracking state
* (statuses, watches, ratings, library membership) as stale. Active queries
* refetch immediately; inactive ones refetch on next mount. Title details
* (`titles.get`) are deliberately excluded — they don't depend on tracking.
*/
export function invalidateTrackingQueries(queryClient: QueryClient): Promise<unknown> {
return Promise.all([
queryClient.invalidateQueries({ queryKey: orpc.tracking.key() }),
queryClient.invalidateQueries({ queryKey: orpc.library.key() }),
queryClient.invalidateQueries({ queryKey: orpc.discover.key() }),
queryClient.invalidateQueries({ queryKey: orpc.people.key() }),
queryClient.invalidateQueries({ queryKey: orpc.titles.similar.key() }),
]);
}