feat(admin): nach jeder Aktion automatisch neu laden
AdminAutoRefresh im Admin-Layout lädt nach jeder erfolgreichen Mutation alle sichtbaren Daten neu (invalidateQueries, nur aktive Queries holen sofort neu). Damit braucht keine Admin-Seite mehr F5, auch die, die bisher nach manchen Aktionen gar nicht neu geladen haben. Die ~35 einzelnen invalidateQueries-Aufrufe in den Seiten entfallen. Gilt nur im Admin-Panel. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
20d6888449
commit
5f75998512
22 files changed
+101
-103
No files matched your search
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useRef, useCallback } from "react";
|
||||
import { useQuery, useQueries, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { useQuery, useQueries, useMutation } from "@tanstack/react-query";
|
||||
import { useTRPC } from "@/trpc/client";
|
||||
import { AdminPageHeader } from "@/components/admin/AdminSection";
|
||||
import { StatusBadge } from "@/components/admin/StatusBadge";
|
||||
@@ -44,7 +44,6 @@ const MAX_SIZE = 10 * 1024 * 1024;
|
||||
|
||||
export default function AdminAdsPage() {
|
||||
const trpc = useTRPC();
|
||||
const qc = useQueryClient();
|
||||
const [form, setForm] = useState<AdForm>(emptyForm);
|
||||
const [editingId, setEditingId] = useState<string | null>(null);
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
@@ -68,18 +67,16 @@ export default function AdminAdsPage() {
|
||||
breedsBySpecies[speciesId] = breedQueries[i]?.data ?? [];
|
||||
});
|
||||
|
||||
const invalidate = () => void qc.invalidateQueries({ queryKey: trpc.admin.listAds.queryKey() });
|
||||
|
||||
const getPresignedUrl = useMutation(trpc.admin.getAdImagePresignedUrl.mutationOptions());
|
||||
|
||||
const createAd = useMutation(
|
||||
trpc.admin.createAd.mutationOptions({ onSuccess: () => { invalidate(); closeDialog(); } })
|
||||
trpc.admin.createAd.mutationOptions({ onSuccess: () => { closeDialog(); } })
|
||||
);
|
||||
const updateAd = useMutation(
|
||||
trpc.admin.updateAd.mutationOptions({ onSuccess: () => { invalidate(); closeDialog(); } })
|
||||
trpc.admin.updateAd.mutationOptions({ onSuccess: () => { closeDialog(); } })
|
||||
);
|
||||
const deleteAd = useMutation(
|
||||
trpc.admin.deleteAd.mutationOptions({ onSuccess: () => { invalidate(); setDeleteId(null); } })
|
||||
trpc.admin.deleteAd.mutationOptions({ onSuccess: () => { setDeleteId(null); } })
|
||||
);
|
||||
|
||||
function openCreate() {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import { useTRPC } from "@/trpc/client";
|
||||
import { Trash2, Loader2, Plus, Ban } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -31,7 +31,6 @@ const FILTER_LABEL: Record<TypeFilter, string> = { all: "Alle Arten", ...TYPE_LA
|
||||
|
||||
export default function AdminBlacklistPage() {
|
||||
const trpc = useTRPC();
|
||||
const qc = useQueryClient();
|
||||
const [typeFilter, setTypeFilter] = useState<TypeFilter>("all");
|
||||
const [newType, setNewType] = useState<BlacklistEntryType>(BlacklistEntryType.WORD);
|
||||
const [newValue, setNewValue] = useState("");
|
||||
@@ -49,7 +48,6 @@ export default function AdminBlacklistPage() {
|
||||
const deleteMutation = useMutation(
|
||||
trpc.admin.deleteBlacklistEntry.mutationOptions({
|
||||
onSuccess: () => {
|
||||
void qc.invalidateQueries({ queryKey: trpc.admin.listBlacklist.queryKey() });
|
||||
setDeleteTarget(null);
|
||||
},
|
||||
})
|
||||
@@ -81,7 +79,6 @@ export default function AdminBlacklistPage() {
|
||||
}
|
||||
|
||||
setIsBulkAdding(false);
|
||||
void qc.invalidateQueries({ queryKey: trpc.admin.listBlacklist.queryKey() });
|
||||
setNewValue("");
|
||||
|
||||
if (addedCount > 0) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { useQuery, useMutation } from "@tanstack/react-query";
|
||||
import { useTRPC } from "@/trpc/client";
|
||||
import { Send, Loader2, Trash2 } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -23,7 +23,6 @@ function formatDate(d: string | Date) {
|
||||
|
||||
export default function AdminBroadcastsPage() {
|
||||
const trpc = useTRPC();
|
||||
const qc = useQueryClient();
|
||||
const [title, setTitle] = useState("");
|
||||
const [body, setBody] = useState("");
|
||||
const [deleteTarget, setDeleteTarget] = useState<{ id: string; title: string } | null>(null);
|
||||
@@ -34,7 +33,6 @@ export default function AdminBroadcastsPage() {
|
||||
const createMutation = useMutation(
|
||||
trpc.admin.createBroadcast.mutationOptions({
|
||||
onSuccess: () => {
|
||||
void qc.invalidateQueries({ queryKey: trpc.admin.listBroadcasts.queryKey() });
|
||||
toast.success("Mitteilung gesendet – Halter sehen sie beim nächsten Öffnen der App");
|
||||
setTitle("");
|
||||
setBody("");
|
||||
@@ -46,7 +44,6 @@ export default function AdminBroadcastsPage() {
|
||||
const deleteMutation = useMutation(
|
||||
trpc.admin.deleteBroadcast.mutationOptions({
|
||||
onSuccess: () => {
|
||||
void qc.invalidateQueries({ queryKey: trpc.admin.listBroadcasts.queryKey() });
|
||||
toast.success("Mitteilung gelöscht");
|
||||
setDeleteTarget(null);
|
||||
setDeleteReason("");
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import { Loader2, Sprout } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { useTRPC } from "@/trpc/client";
|
||||
@@ -55,7 +55,6 @@ export default function AdminCuratorPage() {
|
||||
|
||||
function CuratorForm({ initial }: { initial: Form }) {
|
||||
const trpc = useTRPC();
|
||||
const qc = useQueryClient();
|
||||
const [form, setForm] = useState<Form>({
|
||||
welcomeEnabled: initial.welcomeEnabled,
|
||||
autoFollowEnabled: initial.autoFollowEnabled,
|
||||
@@ -68,7 +67,6 @@ function CuratorForm({ initial }: { initial: Form }) {
|
||||
trpc.curatorAdmin.update.mutationOptions({
|
||||
onSuccess: () => {
|
||||
toast.success("Begrüßung gespeichert");
|
||||
void qc.invalidateQueries(trpc.curatorAdmin.get.queryFilter());
|
||||
},
|
||||
onError: (err) => toast.error(err.message),
|
||||
})
|
||||
|
||||
@@ -6,6 +6,7 @@ import { createTRPCCaller } from "@/trpc/server";
|
||||
import { timingSafeStringEqual } from "@/lib/timing-safe-compare";
|
||||
import { AdminNavList } from "@/components/admin/AdminNavList";
|
||||
import { AdminTopbar } from "@/components/admin/AdminTopbar";
|
||||
import { AdminAutoRefresh } from "@/components/admin/AdminAutoRefresh";
|
||||
import { ADMIN_ROOT_ID, ADMIN_THEME_COOKIE, type AdminTheme } from "@/components/admin/admin-theme";
|
||||
import { ADMIN_ROLE_LABEL, buildAdminNav } from "@/components/admin/admin-nav";
|
||||
import { ClerkThemeProvider } from "@/components/providers/ClerkThemeProvider";
|
||||
@@ -56,6 +57,7 @@ export default async function AdminLayout({ children, params }: AdminLayoutProps
|
||||
// SpeedUp-Plan P0 deliberately kept off non-admin routes — acceptable here
|
||||
// since the admin panel is an internal ops tool, not perf-critical.
|
||||
<ClerkThemeProvider>
|
||||
<AdminAutoRefresh />
|
||||
{/* Eigene Admin-Palette (globals.css .admin-theme), hell als Standard,
|
||||
dunkel per Umschalter in der Kopfzeile — unabhängig vom Website-Theme. */}
|
||||
<div
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { useQuery, useMutation } from "@tanstack/react-query";
|
||||
import { useTRPC } from "@/trpc/client";
|
||||
import { Send, Loader2 } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -33,7 +33,6 @@ function formatDate(d: string | Date) {
|
||||
|
||||
export default function AdminLegalPage() {
|
||||
const trpc = useTRPC();
|
||||
const qc = useQueryClient();
|
||||
const [docType, setDocType] = useState<"NUTZUNGSBEDINGUNGEN" | "DATENSCHUTZ">("NUTZUNGSBEDINGUNGEN");
|
||||
const [changeSummary, setChangeSummary] = useState("");
|
||||
|
||||
@@ -42,7 +41,6 @@ export default function AdminLegalPage() {
|
||||
const publishMutation = useMutation(
|
||||
trpc.legal.publish.mutationOptions({
|
||||
onSuccess: () => {
|
||||
void qc.invalidateQueries({ queryKey: trpc.legal.listVersions.queryKey() });
|
||||
toast.success("Veröffentlicht – Halter sehen den Hinweis beim nächsten Öffnen der App");
|
||||
setChangeSummary("");
|
||||
},
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import { ExternalLink, Loader2, Mail, MapPin, Phone, Search, Users } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { useTRPC } from "@/trpc/client";
|
||||
@@ -27,7 +27,6 @@ const DATE_FORMAT = new Intl.DateTimeFormat("de-DE", { dateStyle: "medium", time
|
||||
*/
|
||||
export default function AdminLostReportsPage() {
|
||||
const trpc = useTRPC();
|
||||
const qc = useQueryClient();
|
||||
const [status, setStatus] = useState<Status>("ACTIVE");
|
||||
const [target, setTarget] = useState<{ id: string; name: string; mode: Mode } | null>(null);
|
||||
const [reason, setReason] = useState("");
|
||||
@@ -39,7 +38,6 @@ export default function AdminLostReportsPage() {
|
||||
toast.success(target?.mode === "DELETE" ? "Meldung gelöscht" : "Meldung geschlossen");
|
||||
setTarget(null);
|
||||
setReason("");
|
||||
void qc.invalidateQueries({ queryKey: trpc.lostReportAdmin.list.queryKey() });
|
||||
},
|
||||
onError: (err) => toast.error(err.message),
|
||||
})
|
||||
|
||||
@@ -53,8 +53,6 @@ export default function AdminMessagesPage() {
|
||||
trpc.admin.sendTeamReply.mutationOptions({
|
||||
onSuccess: () => {
|
||||
setDraft("");
|
||||
void qc.invalidateQueries({ queryKey: trpc.admin.getTeamConversationThread.queryKey({ conversationId: activeId ?? "" }) });
|
||||
void qc.invalidateQueries({ queryKey: trpc.admin.listTeamConversations.queryKey() });
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { useQuery, useMutation } from "@tanstack/react-query";
|
||||
import { AdminRoleType } from "@prisma/client";
|
||||
import { Check, Loader2, Plus, Search, Trash2, UserRound } from "lucide-react";
|
||||
import { useTRPC } from "@/trpc/client";
|
||||
@@ -30,7 +30,6 @@ const SEARCH_DEBOUNCE_MS = 250;
|
||||
*/
|
||||
export default function AdminModeratorsPage() {
|
||||
const trpc = useTRPC();
|
||||
const qc = useQueryClient();
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
const [query, setQuery] = useState("");
|
||||
const [picked, setPicked] = useState<{ id: string; label: string } | null>(null);
|
||||
@@ -45,7 +44,6 @@ export default function AdminModeratorsPage() {
|
||||
);
|
||||
const actionCountByModeratorId = new Map(throughput.map((t) => [t.moderatorId, t.actionCount]));
|
||||
|
||||
const invalidate = () => void qc.invalidateQueries({ queryKey: trpc.admin.listModerators.queryKey() });
|
||||
const closeGrant = () => {
|
||||
setDialogOpen(false);
|
||||
setQuery("");
|
||||
@@ -56,7 +54,6 @@ export default function AdminModeratorsPage() {
|
||||
const grantRole = useMutation(
|
||||
trpc.admin.grantRole.mutationOptions({
|
||||
onSuccess: () => {
|
||||
invalidate();
|
||||
toast.success("Rolle vergeben.");
|
||||
closeGrant();
|
||||
},
|
||||
@@ -66,7 +63,6 @@ export default function AdminModeratorsPage() {
|
||||
const revokeRole = useMutation(
|
||||
trpc.admin.revokeRole.mutationOptions({
|
||||
onSuccess: () => {
|
||||
invalidate();
|
||||
toast.success("Rolle entzogen.");
|
||||
setRevokeTarget(null);
|
||||
},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useRef, useCallback } from "react";
|
||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { useQuery, useMutation } from "@tanstack/react-query";
|
||||
import { useTRPC } from "@/trpc/client";
|
||||
import { AdminPageHeader } from "@/components/admin/AdminSection";
|
||||
import { StatusBadge } from "@/components/admin/StatusBadge";
|
||||
@@ -73,7 +73,6 @@ function readAudioDuration(file: File): Promise<number> {
|
||||
|
||||
export default function AdminMusicPage() {
|
||||
const trpc = useTRPC();
|
||||
const qc = useQueryClient();
|
||||
const [form, setForm] = useState<TrackForm>(emptyForm);
|
||||
const [editingId, setEditingId] = useState<string | null>(null);
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
@@ -86,18 +85,17 @@ export default function AdminMusicPage() {
|
||||
const { data: rawTracks, isLoading } = useQuery(trpc.admin.listMusicTracks.queryOptions(undefined));
|
||||
const tracks = rawTracks ?? [];
|
||||
|
||||
const invalidate = () => void qc.invalidateQueries({ queryKey: trpc.admin.listMusicTracks.queryKey() });
|
||||
|
||||
const getUploadUrl = useMutation(trpc.admin.getMusicTrackUploadUrl.mutationOptions());
|
||||
|
||||
const createTrack = useMutation(
|
||||
trpc.admin.createMusicTrack.mutationOptions({ onSuccess: () => { invalidate(); closeDialog(); } })
|
||||
trpc.admin.createMusicTrack.mutationOptions({ onSuccess: () => { closeDialog(); } })
|
||||
);
|
||||
const updateTrack = useMutation(
|
||||
trpc.admin.updateMusicTrack.mutationOptions({ onSuccess: () => { invalidate(); closeDialog(); } })
|
||||
trpc.admin.updateMusicTrack.mutationOptions({ onSuccess: () => { closeDialog(); } })
|
||||
);
|
||||
const deleteTrack = useMutation(
|
||||
trpc.admin.deleteMusicTrack.mutationOptions({ onSuccess: () => { invalidate(); setDeleteId(null); } })
|
||||
trpc.admin.deleteMusicTrack.mutationOptions({ onSuccess: () => { setDeleteId(null); } })
|
||||
);
|
||||
|
||||
function openCreate() {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useSyncExternalStore } from "react";
|
||||
import { useInfiniteQuery, useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { useInfiniteQuery, useMutation, useQuery } from "@tanstack/react-query";
|
||||
import { useTRPC } from "@/trpc/client";
|
||||
import { Trash2, MessageSquare, Loader2, ChevronDown, Play, ImageOff, PawPrint, EyeOff, Eye, Sparkles, Pin, PinOff, Hash, MoreHorizontal, Flag, Search, Repeat2 } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -57,7 +57,6 @@ function useGridSize(): [GridSize, (size: GridSize) => void] {
|
||||
|
||||
export default function AdminPostsPage() {
|
||||
const trpc = useTRPC();
|
||||
const qc = useQueryClient();
|
||||
const [search, setSearch] = useState("");
|
||||
const [gridSize, handleGridSizeChange] = useGridSize();
|
||||
const [commentsPostId, setCommentsPostId] = useState<string | null>(null);
|
||||
@@ -88,7 +87,6 @@ export default function AdminPostsPage() {
|
||||
const deletePost = useMutation(
|
||||
trpc.admin.deletePost.mutationOptions({
|
||||
onSuccess: () => {
|
||||
void qc.invalidateQueries({ queryKey: trpc.admin.listPosts.queryKey() });
|
||||
setDeleteTarget(null);
|
||||
setReason("");
|
||||
},
|
||||
@@ -98,9 +96,6 @@ export default function AdminPostsPage() {
|
||||
const deleteComment = useMutation(
|
||||
trpc.admin.deleteComment.mutationOptions({
|
||||
onSuccess: () => {
|
||||
void qc.invalidateQueries({
|
||||
queryKey: trpc.admin.listComments.queryKey({ postId: commentsPostId ?? "" }),
|
||||
});
|
||||
setDeleteTarget(null);
|
||||
setReason("");
|
||||
},
|
||||
@@ -110,43 +105,23 @@ export default function AdminPostsPage() {
|
||||
const hidePost = useMutation(
|
||||
trpc.admin.hidePost.mutationOptions({
|
||||
onSuccess: () => {
|
||||
void qc.invalidateQueries({ queryKey: trpc.admin.listPosts.queryKey() });
|
||||
setHideTargetId(null);
|
||||
setHideReason("");
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
const unhidePost = useMutation(
|
||||
trpc.admin.unhidePost.mutationOptions({
|
||||
onSuccess: () => {
|
||||
void qc.invalidateQueries({ queryKey: trpc.admin.listPosts.queryKey() });
|
||||
},
|
||||
})
|
||||
);
|
||||
const unhidePost = useMutation(trpc.admin.unhidePost.mutationOptions());
|
||||
|
||||
const pinPost = useMutation(
|
||||
trpc.admin.pinPost.mutationOptions({
|
||||
onSuccess: () => {
|
||||
void qc.invalidateQueries({ queryKey: trpc.admin.listPosts.queryKey() });
|
||||
},
|
||||
})
|
||||
);
|
||||
const pinPost = useMutation(trpc.admin.pinPost.mutationOptions());
|
||||
|
||||
const unpinPost = useMutation(
|
||||
trpc.admin.unpinPost.mutationOptions({
|
||||
onSuccess: () => {
|
||||
void qc.invalidateQueries({ queryKey: trpc.admin.listPosts.queryKey() });
|
||||
},
|
||||
})
|
||||
);
|
||||
const unpinPost = useMutation(trpc.admin.unpinPost.mutationOptions());
|
||||
|
||||
const { data: trending } = useQuery(trpc.explore.getTrending.queryOptions());
|
||||
|
||||
const setAiDisclosure = useMutation(
|
||||
trpc.admin.setAiDisclosure.mutationOptions({
|
||||
onSuccess: () => {
|
||||
void qc.invalidateQueries({ queryKey: trpc.admin.listPosts.queryKey() });
|
||||
setAiTargetId(null);
|
||||
setAiReason("");
|
||||
},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { useQuery, useMutation } from "@tanstack/react-query";
|
||||
import { useTRPC } from "@/trpc/client";
|
||||
import { getMediaUrl } from "@/lib/media-url";
|
||||
import { BlurImage } from "@/components/ui/blur-image";
|
||||
@@ -35,7 +35,6 @@ type ReportAction =
|
||||
|
||||
export default function AdminReportsPage() {
|
||||
const trpc = useTRPC();
|
||||
const qc = useQueryClient();
|
||||
const [action, setAction] = useState<ReportAction | null>(null);
|
||||
const [reason, setReason] = useState("");
|
||||
// found.md: Melder bekommt eine Team-Nachricht (Begründung bzw. Dank).
|
||||
@@ -58,18 +57,15 @@ export default function AdminReportsPage() {
|
||||
)
|
||||
);
|
||||
|
||||
const invalidate = () =>
|
||||
void qc.invalidateQueries({ queryKey: trpc.admin.listReports.queryKey() });
|
||||
|
||||
const dismiss = useMutation(
|
||||
trpc.admin.dismissReport.mutationOptions({
|
||||
onSuccess: () => { invalidate(); closeDialog(); },
|
||||
onSuccess: () => { closeDialog(); },
|
||||
})
|
||||
);
|
||||
|
||||
const deletePost = useMutation(
|
||||
trpc.admin.deleteReportedPost.mutationOptions({
|
||||
onSuccess: () => { invalidate(); closeDialog(); },
|
||||
onSuccess: () => { closeDialog(); },
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { useState } from "react";
|
||||
import { useRouter, useParams, useSearchParams } from "next/navigation";
|
||||
import { toast } from "sonner";
|
||||
import { useInfiniteQuery, useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { useInfiniteQuery, useMutation, useQuery } from "@tanstack/react-query";
|
||||
import type { inferRouterOutputs } from "@trpc/server";
|
||||
import { AdminRoleType } from "@prisma/client";
|
||||
import {
|
||||
@@ -78,7 +78,6 @@ function UserStatus({ user }: { user: User }) {
|
||||
*/
|
||||
export default function AdminUsersPage() {
|
||||
const trpc = useTRPC();
|
||||
const qc = useQueryClient();
|
||||
const router = useRouter();
|
||||
const params = useParams<{ secret: string }>();
|
||||
// ?q= kommt von der globalen Suche in der Kopfzeile; ein neuer Treffer ersetzt die Suche.
|
||||
@@ -120,7 +119,6 @@ export default function AdminUsersPage() {
|
||||
const verify = useMutation(
|
||||
trpc.admin.setPetVerified.mutationOptions({
|
||||
onSuccess: (pet) => {
|
||||
void qc.invalidateQueries({ queryKey: trpc.admin.listUsers.queryKey() });
|
||||
toast.success(pet.isVerified ? `${formatPetName(pet)} ist jetzt verifiziert.` : `Verifizierung von ${formatPetName(pet)} entfernt.`);
|
||||
},
|
||||
onError: (err) => toast.error(`Verifizierung fehlgeschlagen: ${err.message}`),
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { useInfiniteQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { useInfiniteQuery, useMutation } from "@tanstack/react-query";
|
||||
import { useTRPC } from "@/trpc/client";
|
||||
import {
|
||||
Loader2,
|
||||
@@ -78,7 +78,6 @@ const EMPTY_CHECKLIST: ChecklistState = {
|
||||
|
||||
export default function AdminVerificationPage() {
|
||||
const trpc = useTRPC();
|
||||
const qc = useQueryClient();
|
||||
const [status, setStatus] = useState<RequestStatus>("PENDING");
|
||||
const [reviewTarget, setReviewTarget] = useState<string | null>(null);
|
||||
const [checklist, setChecklist] = useState<ChecklistState>(EMPTY_CHECKLIST);
|
||||
@@ -96,7 +95,6 @@ export default function AdminVerificationPage() {
|
||||
const reviewMutation = useMutation(
|
||||
trpc.admin.reviewVerificationRequest.mutationOptions({
|
||||
onSuccess: () => {
|
||||
void qc.invalidateQueries({ queryKey: trpc.admin.listVerificationRequests.queryKey() });
|
||||
setReviewTarget(null);
|
||||
setChecklist(EMPTY_CHECKLIST);
|
||||
setNote("");
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect } from "react";
|
||||
import { useQueryClient, type QueryClient } from "@tanstack/react-query";
|
||||
|
||||
/**
|
||||
* Nach jeder erfolgreichen Aktion alle Daten neu laden. invalidateQueries()
|
||||
* holt nur die gerade sichtbaren (aktiven) Queries sofort neu, der Rest gilt
|
||||
* als veraltet und lädt beim nächsten Anzeigen. Gibt die Abmeldung zurück.
|
||||
*/
|
||||
export function subscribeAutoRefresh(queryClient: QueryClient): () => void {
|
||||
return queryClient.getMutationCache().subscribe((event) => {
|
||||
if (event.type === "updated" && event.action.type === "success") {
|
||||
void queryClient.invalidateQueries();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Admin-Panel: keine Seite muss nach einer Aktion selbst neu laden. Hängt nur,
|
||||
* solange das Admin-Layout gemountet ist — der QueryClient ist app-weit, die
|
||||
* normale App bleibt davon unberührt.
|
||||
*/
|
||||
export function AdminAutoRefresh() {
|
||||
const queryClient = useQueryClient();
|
||||
useEffect(() => subscribeAutoRefresh(queryClient), [queryClient]);
|
||||
return null;
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import { QueryClient } from "@tanstack/react-query";
|
||||
import { subscribeAutoRefresh } from "./AdminAutoRefresh";
|
||||
|
||||
function runMutation(queryClient: QueryClient, mutationFn: () => Promise<unknown>) {
|
||||
return queryClient.getMutationCache().build(queryClient, { mutationFn }).execute(undefined);
|
||||
}
|
||||
|
||||
describe("subscribeAutoRefresh", () => {
|
||||
it("lädt nach einer erfolgreichen Aktion alle Daten neu", async () => {
|
||||
const queryClient = new QueryClient();
|
||||
const invalidate = vi.spyOn(queryClient, "invalidateQueries").mockResolvedValue();
|
||||
subscribeAutoRefresh(queryClient);
|
||||
|
||||
await runMutation(queryClient, () => Promise.resolve("ok"));
|
||||
|
||||
expect(invalidate).toHaveBeenCalledOnce();
|
||||
expect(invalidate).toHaveBeenCalledWith();
|
||||
});
|
||||
|
||||
it("lädt nach einer fehlgeschlagenen Aktion nichts neu", async () => {
|
||||
const queryClient = new QueryClient();
|
||||
const invalidate = vi.spyOn(queryClient, "invalidateQueries").mockResolvedValue();
|
||||
subscribeAutoRefresh(queryClient);
|
||||
|
||||
await runMutation(queryClient, () => Promise.reject(new Error("FORBIDDEN"))).catch(() => {});
|
||||
|
||||
expect(invalidate).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("hört nach dem Abmelden (Admin-Layout verlassen) auf", async () => {
|
||||
const queryClient = new QueryClient();
|
||||
const invalidate = vi.spyOn(queryClient, "invalidateQueries").mockResolvedValue();
|
||||
const unsubscribe = subscribeAutoRefresh(queryClient);
|
||||
unsubscribe();
|
||||
|
||||
await runMutation(queryClient, () => Promise.resolve("ok"));
|
||||
|
||||
expect(invalidate).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import { Sparkles, TriangleAlert } from "lucide-react";
|
||||
import { useTRPC } from "@/trpc/client";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
@@ -35,17 +35,14 @@ function SwitchRow({ icon: Icon, title, description, checked, isDanger, disabled
|
||||
*/
|
||||
export function SystemCard({ canToggleMaintenance }: { canToggleMaintenance: boolean }) {
|
||||
const trpc = useTRPC();
|
||||
const qc = useQueryClient();
|
||||
const { data: maintenance } = useQuery(trpc.admin.getMaintenanceMode.queryOptions(undefined, { enabled: canToggleMaintenance }));
|
||||
const { data: storyEditor } = useQuery(trpc.admin.getStoryEditorEnabled.queryOptions());
|
||||
const setMaintenance = useMutation(
|
||||
trpc.admin.setMaintenanceMode.mutationOptions({
|
||||
onSuccess: () => void qc.invalidateQueries({ queryKey: trpc.admin.getMaintenanceMode.queryKey() }),
|
||||
})
|
||||
);
|
||||
const setStoryEditor = useMutation(
|
||||
trpc.admin.setStoryEditorEnabled.mutationOptions({
|
||||
onSuccess: () => void qc.invalidateQueries({ queryKey: trpc.admin.getStoryEditorEnabled.queryKey() }),
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import { AlertTriangle, Loader2 } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { useTRPC } from "@/trpc/client";
|
||||
@@ -31,14 +31,12 @@ function statusOf(o: { referralRevokedAt: MaybeDate; referralQualifiedAt: MaybeD
|
||||
|
||||
export function OverviewTab() {
|
||||
const trpc = useTRPC();
|
||||
const qc = useQueryClient();
|
||||
const { data, isLoading } = useQuery(trpc.referralAdmin.overview.queryOptions());
|
||||
const { data: featurePool = [] } = useQuery(trpc.referralAdmin.perkHolders.queryOptions({ perk: "FEATURE_POOL" }));
|
||||
const { data: spotlight = [] } = useQuery(trpc.referralAdmin.perkHolders.queryOptions({ perk: "CURATOR_SPOTLIGHT" }));
|
||||
|
||||
const refresh = () => void qc.invalidateQueries(trpc.referralAdmin.overview.queryFilter());
|
||||
const approve = useMutation(trpc.referralAdmin.approveFlagged.mutationOptions({ onSuccess: () => { toast.success("Freigegeben"); refresh(); }, onError: (e) => toast.error(e.message) }));
|
||||
const revoke = useMutation(trpc.referralAdmin.revokeReferral.mutationOptions({ onSuccess: () => { toast.success("Aberkannt"); refresh(); }, onError: (e) => toast.error(e.message) }));
|
||||
const approve = useMutation(trpc.referralAdmin.approveFlagged.mutationOptions({ onSuccess: () => { toast.success("Freigegeben") }, onError: (e) => toast.error(e.message) }));
|
||||
const revoke = useMutation(trpc.referralAdmin.revokeReferral.mutationOptions({ onSuccess: () => { toast.success("Aberkannt") }, onError: (e) => toast.error(e.message) }));
|
||||
|
||||
if (isLoading || !data) return <Loader2 className="mx-auto my-12 h-5 w-5 animate-spin text-muted-foreground" />;
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import { Loader2, Pencil, Plus } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { useTRPC } from "@/trpc/client";
|
||||
@@ -15,15 +15,12 @@ type EditorState = { mode: "create" | "edit"; id: string | null; initial: RankFo
|
||||
|
||||
export function RanksTab() {
|
||||
const trpc = useTRPC();
|
||||
const qc = useQueryClient();
|
||||
const [editor, setEditor] = useState<EditorState>(null);
|
||||
const { data: ranks = [], isLoading } = useQuery(trpc.referralAdmin.listRanks.queryOptions());
|
||||
|
||||
function onSaved() {
|
||||
toast.success("Gespeichert. Ränge aller Nutzer wurden neu berechnet.");
|
||||
setEditor(null);
|
||||
void qc.invalidateQueries(trpc.referralAdmin.listRanks.queryFilter());
|
||||
void qc.invalidateQueries(trpc.referrals.ranks.queryFilter());
|
||||
}
|
||||
const onError = (err: { message: string }) => toast.error(err.message);
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { useTRPC } from "@/trpc/client";
|
||||
@@ -25,7 +25,6 @@ export function SettingsTab() {
|
||||
|
||||
function SettingsForm({ initial }: { initial: SettingsValues }) {
|
||||
const trpc = useTRPC();
|
||||
const qc = useQueryClient();
|
||||
const [form, setForm] = useState<SettingsValues>({
|
||||
programActive: initial.programActive,
|
||||
redeemWindowDays: initial.redeemWindowDays,
|
||||
@@ -38,7 +37,6 @@ function SettingsForm({ initial }: { initial: SettingsValues }) {
|
||||
trpc.referralAdmin.updateSettings.mutationOptions({
|
||||
onSuccess: () => {
|
||||
toast.success("Einstellungen gespeichert");
|
||||
void qc.invalidateQueries(trpc.referralAdmin.getSettings.queryFilter());
|
||||
},
|
||||
onError: (err) => toast.error(err.message),
|
||||
})
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { useTRPC } from "@/trpc/client";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -32,7 +32,6 @@ interface UserActionDialogsProps {
|
||||
*/
|
||||
export function UserActionDialogs({ target, onClose }: UserActionDialogsProps) {
|
||||
const trpc = useTRPC();
|
||||
const qc = useQueryClient();
|
||||
const [reason, setReason] = useState("");
|
||||
const [banUntil, setBanUntil] = useState("");
|
||||
const [confirmation, setConfirmation] = useState("");
|
||||
@@ -45,10 +44,8 @@ export function UserActionDialogs({ target, onClose }: UserActionDialogsProps) {
|
||||
setConfirmation("");
|
||||
onClose();
|
||||
};
|
||||
const refresh = () => void qc.invalidateQueries({ queryKey: trpc.admin.listUsers.queryKey() });
|
||||
const failed = (label: string) => (err: { message: string }) => toast.error(`${label} fehlgeschlagen: ${err.message}`);
|
||||
const done = (message: string) => () => {
|
||||
refresh();
|
||||
toast.success(message);
|
||||
close();
|
||||
};
|
||||
@@ -61,8 +58,6 @@ export function UserActionDialogs({ target, onClose }: UserActionDialogsProps) {
|
||||
const warn = useMutation(
|
||||
trpc.admin.issueWarning.mutationOptions({
|
||||
onSuccess: () => {
|
||||
refresh();
|
||||
void qc.invalidateQueries({ queryKey: trpc.admin.listWarnings.queryKey({ ownerId }) });
|
||||
setReason("");
|
||||
toast.success("Verwarnung gespeichert.");
|
||||
},
|
||||
|
||||
@@ -434,7 +434,6 @@ export const adminRouter = router({
|
||||
.input(z.object({ postId: z.string().min(1), reason: z.string().max(500).optional() }))
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const callerRole = await assertAdmin(ctx.prisma, ctx.userId, AdminRoleType.MODERATOR);
|
||||
|
||||
return deletePostAsModerator(ctx, callerRole, input.postId, input.reason, false);
|
||||
}),
|
||||
|
||||
@@ -1319,7 +1318,6 @@ export const adminRouter = router({
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const callerRole = await assertAdmin(ctx.prisma, ctx.userId, AdminRoleType.MODERATOR);
|
||||
|
||||
return deletePostAsModerator(ctx, callerRole, input.postId, input.reason, input.notifyReporters);
|
||||
}),
|
||||
|
||||
|
||||
Reference in new issue
Block a user