mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-05-03 04:42:39 +02:00
Fix review issues: authz, race conditions, UX safety, and consolidate migrations
This commit is contained in:
parent
5f4f7780d1
commit
aecb58e22b
10 changed files with 227 additions and 217 deletions
|
|
@ -1,7 +1,7 @@
|
|||
"use client";
|
||||
|
||||
import { useAtomValue } from "jotai";
|
||||
import { Copy, Globe, Sparkles } from "lucide-react";
|
||||
import { AlertTriangle, Copy, Globe, Sparkles } from "lucide-react";
|
||||
import { useCallback, useState } from "react";
|
||||
import { copyPromptMutationAtom } from "@/atoms/prompts/prompts-mutation.atoms";
|
||||
import { publicPromptsAtom } from "@/atoms/prompts/prompts-query.atoms";
|
||||
|
|
@ -9,20 +9,24 @@ import { Button } from "@/components/ui/button";
|
|||
import { Spinner } from "@/components/ui/spinner";
|
||||
|
||||
export function CommunityPromptsContent() {
|
||||
const { data: prompts, isLoading } = useAtomValue(publicPromptsAtom);
|
||||
const { mutateAsync: copyPrompt, isPending: isCopying } = useAtomValue(copyPromptMutationAtom);
|
||||
const [copyingId, setCopyingId] = useState<number | null>(null);
|
||||
const { data: prompts, isLoading, isError } = useAtomValue(publicPromptsAtom);
|
||||
const { mutateAsync: copyPrompt } = useAtomValue(copyPromptMutationAtom);
|
||||
const [copyingIds, setCopyingIds] = useState<Set<number>>(new Set());
|
||||
const [expandedId, setExpandedId] = useState<number | null>(null);
|
||||
|
||||
const handleCopy = useCallback(
|
||||
async (id: number) => {
|
||||
setCopyingId(id);
|
||||
setCopyingIds((prev) => new Set(prev).add(id));
|
||||
try {
|
||||
await copyPrompt(id);
|
||||
} catch {
|
||||
// toast handled by mutation atom
|
||||
} finally {
|
||||
setCopyingId(null);
|
||||
setCopyingIds((prev) => {
|
||||
const next = new Set(prev);
|
||||
next.delete(id);
|
||||
return next;
|
||||
});
|
||||
}
|
||||
},
|
||||
[copyPrompt]
|
||||
|
|
@ -38,6 +42,16 @@ export function CommunityPromptsContent() {
|
|||
);
|
||||
}
|
||||
|
||||
if (isError) {
|
||||
return (
|
||||
<div className="rounded-lg border border-dashed border-destructive/40 p-8 text-center">
|
||||
<AlertTriangle className="mx-auto size-8 text-destructive/60" />
|
||||
<p className="mt-2 text-sm text-destructive">Failed to load community prompts</p>
|
||||
<p className="text-xs text-muted-foreground">Please try refreshing the page.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6 min-w-0 overflow-hidden">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
|
|
@ -91,20 +105,20 @@ export function CommunityPromptsContent() {
|
|||
</button>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="shrink-0 gap-1.5"
|
||||
disabled={copyingId === prompt.id && isCopying}
|
||||
onClick={() => handleCopy(prompt.id)}
|
||||
>
|
||||
{copyingId === prompt.id && isCopying ? (
|
||||
<Spinner className="size-3" />
|
||||
) : (
|
||||
<Copy className="size-3" />
|
||||
)}
|
||||
Add to mine
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="shrink-0 gap-1.5"
|
||||
disabled={copyingIds.has(prompt.id)}
|
||||
onClick={() => handleCopy(prompt.id)}
|
||||
>
|
||||
{copyingIds.has(prompt.id) ? (
|
||||
<Spinner className="size-3" />
|
||||
) : (
|
||||
<Copy className="size-3" />
|
||||
)}
|
||||
Add to mine
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
"use client";
|
||||
|
||||
import { useAtomValue } from "jotai";
|
||||
import { Globe, Lock, PenLine, Plus, Sparkles, Trash2 } from "lucide-react";
|
||||
import { AlertTriangle, Globe, Lock, PenLine, Plus, Sparkles, Trash2 } from "lucide-react";
|
||||
import { useCallback, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import {
|
||||
|
|
@ -10,6 +10,16 @@ import {
|
|||
updatePromptMutationAtom,
|
||||
} from "@/atoms/prompts/prompts-mutation.atoms";
|
||||
import { promptsAtom } from "@/atoms/prompts/prompts-query.atoms";
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from "@/components/ui/alert-dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
|
|
@ -27,7 +37,7 @@ interface PromptFormData {
|
|||
const EMPTY_FORM: PromptFormData = { name: "", prompt: "", mode: "transform", is_public: false };
|
||||
|
||||
export function PromptsContent() {
|
||||
const { data: prompts, isLoading } = useAtomValue(promptsAtom);
|
||||
const { data: prompts, isLoading, isError } = useAtomValue(promptsAtom);
|
||||
const { mutateAsync: createPrompt } = useAtomValue(createPromptMutationAtom);
|
||||
const { mutateAsync: updatePrompt } = useAtomValue(updatePromptMutationAtom);
|
||||
const { mutateAsync: deletePrompt } = useAtomValue(deletePromptMutationAtom);
|
||||
|
|
@ -37,6 +47,7 @@ export function PromptsContent() {
|
|||
const [formData, setFormData] = useState<PromptFormData>(EMPTY_FORM);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [expandedId, setExpandedId] = useState<number | null>(null);
|
||||
const [deleteTarget, setDeleteTarget] = useState<number | null>(null);
|
||||
|
||||
const handleSave = useCallback(async () => {
|
||||
if (!formData.name.trim() || !formData.prompt.trim()) {
|
||||
|
|
@ -46,7 +57,7 @@ export function PromptsContent() {
|
|||
|
||||
setIsSaving(true);
|
||||
try {
|
||||
if (editingId) {
|
||||
if (editingId !== null) {
|
||||
await updatePrompt({ id: editingId, ...formData });
|
||||
} else {
|
||||
await createPrompt(formData);
|
||||
|
|
@ -72,16 +83,16 @@ export function PromptsContent() {
|
|||
setShowForm(true);
|
||||
}, []);
|
||||
|
||||
const handleDelete = useCallback(
|
||||
async (id: number) => {
|
||||
try {
|
||||
await deletePrompt(id);
|
||||
} catch {
|
||||
// toast handled by mutation atom
|
||||
}
|
||||
},
|
||||
[deletePrompt]
|
||||
);
|
||||
const handleConfirmDelete = useCallback(async () => {
|
||||
if (deleteTarget === null) return;
|
||||
try {
|
||||
await deletePrompt(deleteTarget);
|
||||
} catch {
|
||||
// toast handled by mutation atom
|
||||
} finally {
|
||||
setDeleteTarget(null);
|
||||
}
|
||||
}, [deleteTarget, deletePrompt]);
|
||||
|
||||
const handleTogglePublic = useCallback(
|
||||
async (prompt: PromptRead) => {
|
||||
|
|
@ -110,6 +121,16 @@ export function PromptsContent() {
|
|||
);
|
||||
}
|
||||
|
||||
if (isError) {
|
||||
return (
|
||||
<div className="rounded-lg border border-dashed border-destructive/40 p-8 text-center">
|
||||
<AlertTriangle className="mx-auto size-8 text-destructive/60" />
|
||||
<p className="mt-2 text-sm text-destructive">Failed to load prompts</p>
|
||||
<p className="text-xs text-muted-foreground">Please try refreshing the page.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6 min-w-0 overflow-hidden">
|
||||
<div className="flex items-center justify-between">
|
||||
|
|
@ -137,7 +158,7 @@ export function PromptsContent() {
|
|||
{showForm && (
|
||||
<div className="rounded-lg border border-border/60 bg-card p-6 space-y-4">
|
||||
<h3 className="text-sm font-semibold tracking-tight">
|
||||
{editingId ? "Edit prompt" : "New prompt"}
|
||||
{editingId !== null ? "Edit prompt" : "New prompt"}
|
||||
</h3>
|
||||
|
||||
<div className="space-y-2">
|
||||
|
|
@ -200,7 +221,7 @@ export function PromptsContent() {
|
|||
Cancel
|
||||
</Button>
|
||||
<Button size="sm" onClick={handleSave} disabled={isSaving}>
|
||||
{isSaving ? <Spinner className="size-3.5" /> : editingId ? "Update" : "Create"}
|
||||
{isSaving ? <Spinner className="size-3.5" /> : editingId !== null ? "Update" : "Create"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -279,7 +300,7 @@ export function PromptsContent() {
|
|||
variant="ghost"
|
||||
size="icon"
|
||||
className="size-7 text-destructive hover:text-destructive"
|
||||
onClick={() => handleDelete(prompt.id)}
|
||||
onClick={() => setDeleteTarget(prompt.id)}
|
||||
>
|
||||
<Trash2 className="size-3.5" />
|
||||
</Button>
|
||||
|
|
@ -288,6 +309,21 @@ export function PromptsContent() {
|
|||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<AlertDialog open={deleteTarget !== null} onOpenChange={(open) => !open && setDeleteTarget(null)}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Delete prompt</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
This action cannot be undone. The prompt will be permanently removed.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={handleConfirmDelete}>Delete</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue