feat: delete doc

This commit is contained in:
monoid 2024-11-23 03:02:35 +09:00
parent 9f24f812c9
commit a92ddadaf0
5 changed files with 325 additions and 13 deletions

View File

@ -11,6 +11,7 @@
"shadcn": "shadcn-ui" "shadcn": "shadcn-ui"
}, },
"dependencies": { "dependencies": {
"@radix-ui/react-dropdown-menu": "^2.1.2",
"@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-label": "^2.1.0", "@radix-ui/react-label": "^2.1.0",
"@radix-ui/react-popover": "^1.1.2", "@radix-ui/react-popover": "^1.1.2",

View File

@ -0,0 +1,203 @@
import * as React from "react"
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
import {
CheckIcon,
ChevronRightIcon,
DotFilledIcon,
} from "@radix-ui/react-icons"
import { cn } from "@/lib/utils"
const DropdownMenu = DropdownMenuPrimitive.Root
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
const DropdownMenuGroup = DropdownMenuPrimitive.Group
const DropdownMenuPortal = DropdownMenuPrimitive.Portal
const DropdownMenuSub = DropdownMenuPrimitive.Sub
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
const DropdownMenuSubTrigger = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean
}
>(({ className, inset, children, ...props }, ref) => (
<DropdownMenuPrimitive.SubTrigger
ref={ref}
className={cn(
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent",
inset && "pl-8",
className
)}
{...props}
>
{children}
<ChevronRightIcon className="ml-auto h-4 w-4" />
</DropdownMenuPrimitive.SubTrigger>
))
DropdownMenuSubTrigger.displayName =
DropdownMenuPrimitive.SubTrigger.displayName
const DropdownMenuSubContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.SubContent
ref={ref}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
))
DropdownMenuSubContent.displayName =
DropdownMenuPrimitive.SubContent.displayName
const DropdownMenuContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md",
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
))
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
const DropdownMenuItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
inset && "pl-8",
className
)}
{...props}
/>
))
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
const DropdownMenuCheckboxItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
>(({ className, children, checked, ...props }, ref) => (
<DropdownMenuPrimitive.CheckboxItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
checked={checked}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<CheckIcon className="h-4 w-4" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
))
DropdownMenuCheckboxItem.displayName =
DropdownMenuPrimitive.CheckboxItem.displayName
const DropdownMenuRadioItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
>(({ className, children, ...props }, ref) => (
<DropdownMenuPrimitive.RadioItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<DotFilledIcon className="h-4 w-4 fill-current" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
))
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
const DropdownMenuLabel = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Label
ref={ref}
className={cn(
"px-2 py-1.5 text-sm font-semibold",
inset && "pl-8",
className
)}
{...props}
/>
))
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
const DropdownMenuSeparator = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
))
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
const DropdownMenuShortcut = ({
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
{...props}
/>
)
}
DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
export {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuGroup,
DropdownMenuPortal,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuRadioGroup,
}

View File

@ -26,6 +26,16 @@ export function useRescanDoc() {
}; };
} }
export function useDeleteDoc() {
const { mutate } = useSWRConfig();
return async (id: string) => {
await fetch(`/api/doc/${id}`, {
method: "DELETE",
});
mutate(`/api/doc/${id}`);
};
}
export function useGalleryDocSimilar(id: string) { export function useGalleryDocSimilar(id: string) {
return useSWR<Document[]>(`/api/doc/${id}/similars`, fetcher); return useSWR<Document[]>(`/api/doc/${id}/similars`, fetcher);
} }

View File

@ -1,5 +1,5 @@
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { useGalleryDoc, useGalleryDocSimilar, useRehashDoc, useRescanDoc } from "../hook/useGalleryDoc.ts"; import { useDeleteDoc, useGalleryDoc, useGalleryDocSimilar, useRehashDoc, useRescanDoc } from "../hook/useGalleryDoc.ts";
import TagBadge from "@/components/gallery/TagBadge"; import TagBadge from "@/components/gallery/TagBadge";
import StyledLink from "@/components/gallery/StyledLink"; import StyledLink from "@/components/gallery/StyledLink";
import { Link, useLocation } from "wouter"; import { Link, useLocation } from "wouter";
@ -9,6 +9,8 @@ import { Button } from "@/components/ui/button.tsx";
import { GalleryCard } from "@/components/gallery/GalleryCard.tsx"; import { GalleryCard } from "@/components/gallery/GalleryCard.tsx";
import { useEffect, useRef } from "react"; import { useEffect, useRef } from "react";
import { useLogin } from "@/state/user.ts"; import { useLogin } from "@/state/user.ts";
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu.tsx";
import { DotsVerticalIcon } from "@radix-ui/react-icons";
export interface ContentInfoPageProps { export interface ContentInfoPageProps {
params: { params: {
@ -37,6 +39,7 @@ export function ContentInfoPage({ params }: ContentInfoPageProps) {
const { data, error, isLoading } = useGalleryDoc(params.id); const { data, error, isLoading } = useGalleryDoc(params.id);
const rehashDoc = useRehashDoc(); const rehashDoc = useRehashDoc();
const rescanDoc = useRescanDoc(); const rescanDoc = useRescanDoc();
const deleteDoc = useDeleteDoc();
const user = useLogin(); const user = useLogin();
const username = user?.username; const username = user?.username;
const isAdmin = username === "admin"; const isAdmin = username === "admin";
@ -75,18 +78,43 @@ export function ContentInfoPage({ params }: ContentInfoPageProps) {
</Link> </Link>
<Card className="flex-1 relative"> <Card className="flex-1 relative">
{isAdmin && {isAdmin &&
<div className="absolute top-0 right-0 p-2 grid"> <div className="absolute top-0 right-0 p-2">
<Button variant="ghost" onClick={async () => { <DropdownMenu>
// Rehash <DropdownMenuTrigger asChild>
<Button variant="ghost"> <DotsVerticalIcon /> Actions</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" >
<DropdownMenuItem
onClick={async () => {
await rehashDoc(params.id); await rehashDoc(params.id);
}}>Rehash</Button> }}
<Button variant="ghost" onClick={async () => { >
Rehash
</DropdownMenuItem>
<DropdownMenuItem
onClick={async () => {
if (!window.confirm("Are you sure?")) { if (!window.confirm("Are you sure?")) {
return; return;
} }
// Rescan // Rescan
await rescanDoc(params.id); await rescanDoc(params.id);
}}>Rescan</Button> }}
>
Rescan
</DropdownMenuItem>
<DropdownMenuItem
onClick={async () => {
if (!window.confirm("Are you sure?")) {
return;
}
// Delete
await deleteDoc(params.id);
}}
>
Delete
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div> </div>
} }
<CardHeader> <CardHeader>

View File

@ -14,6 +14,9 @@ importers:
packages/client: packages/client:
dependencies: dependencies:
'@radix-ui/react-dropdown-menu':
specifier: ^2.1.2
version: 2.1.2(@types/react-dom@18.3.0)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-icons': '@radix-ui/react-icons':
specifier: ^1.3.0 specifier: ^1.3.0
version: 1.3.0(react@18.3.1) version: 1.3.0(react@18.3.1)
@ -876,6 +879,19 @@ packages:
'@types/react-dom': '@types/react-dom':
optional: true optional: true
'@radix-ui/react-dropdown-menu@2.1.2':
resolution: {integrity: sha512-GVZMR+eqK8/Kes0a36Qrv+i20bAPXSn8rCBTHx30w+3ECnR5o3xixAlqcVaYvLeyKUsm0aqyhWfmUcqufM8nYA==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
'@radix-ui/react-focus-guards@1.1.1': '@radix-ui/react-focus-guards@1.1.1':
resolution: {integrity: sha512-pSIwfrT1a6sIoDASCSpFwOasEwKTZWDw/iBdtnqKO7v6FeOzYJ7U53cPzYFVR3geGGXgVHaH+CdngrrAzqUGxg==} resolution: {integrity: sha512-pSIwfrT1a6sIoDASCSpFwOasEwKTZWDw/iBdtnqKO7v6FeOzYJ7U53cPzYFVR3geGGXgVHaH+CdngrrAzqUGxg==}
peerDependencies: peerDependencies:
@ -925,6 +941,19 @@ packages:
'@types/react-dom': '@types/react-dom':
optional: true optional: true
'@radix-ui/react-menu@2.1.2':
resolution: {integrity: sha512-lZ0R4qR2Al6fZ4yCCZzu/ReTFrylHFxIqy7OezIpWF4bL0o9biKo0pFIvkaew3TyZ9Fy5gYVrR5zCGZBVbO1zg==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
'@radix-ui/react-popover@1.1.2': '@radix-ui/react-popover@1.1.2':
resolution: {integrity: sha512-u2HRUyWW+lOiA2g0Le0tMmT55FGOEWHwPFt1EPfbLly7uXQExFo5duNKqG2DzmFXIdqOeNd+TpE8baHWJCyP9w==} resolution: {integrity: sha512-u2HRUyWW+lOiA2g0Le0tMmT55FGOEWHwPFt1EPfbLly7uXQExFo5duNKqG2DzmFXIdqOeNd+TpE8baHWJCyP9w==}
peerDependencies: peerDependencies:
@ -4018,6 +4047,21 @@ snapshots:
'@types/react': 18.3.11 '@types/react': 18.3.11
'@types/react-dom': 18.3.0 '@types/react-dom': 18.3.0
'@radix-ui/react-dropdown-menu@2.1.2(@types/react-dom@18.3.0)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@radix-ui/primitive': 1.1.0
'@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.11)(react@18.3.1)
'@radix-ui/react-context': 1.1.1(@types/react@18.3.11)(react@18.3.1)
'@radix-ui/react-id': 1.1.0(@types/react@18.3.11)(react@18.3.1)
'@radix-ui/react-menu': 2.1.2(@types/react-dom@18.3.0)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.11)(react@18.3.1)
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
optionalDependencies:
'@types/react': 18.3.11
'@types/react-dom': 18.3.0
'@radix-ui/react-focus-guards@1.1.1(@types/react@18.3.11)(react@18.3.1)': '@radix-ui/react-focus-guards@1.1.1(@types/react@18.3.11)(react@18.3.1)':
dependencies: dependencies:
react: 18.3.1 react: 18.3.1
@ -4055,6 +4099,32 @@ snapshots:
'@types/react': 18.3.11 '@types/react': 18.3.11
'@types/react-dom': 18.3.0 '@types/react-dom': 18.3.0
'@radix-ui/react-menu@2.1.2(@types/react-dom@18.3.0)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@radix-ui/primitive': 1.1.0
'@radix-ui/react-collection': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.11)(react@18.3.1)
'@radix-ui/react-context': 1.1.1(@types/react@18.3.11)(react@18.3.1)
'@radix-ui/react-direction': 1.1.0(@types/react@18.3.11)(react@18.3.1)
'@radix-ui/react-dismissable-layer': 1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-focus-guards': 1.1.1(@types/react@18.3.11)(react@18.3.1)
'@radix-ui/react-focus-scope': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-id': 1.1.0(@types/react@18.3.11)(react@18.3.1)
'@radix-ui/react-popper': 1.2.0(@types/react-dom@18.3.0)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-portal': 1.1.2(@types/react-dom@18.3.0)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-presence': 1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-roving-focus': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-slot': 1.1.0(@types/react@18.3.11)(react@18.3.1)
'@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.11)(react@18.3.1)
aria-hidden: 1.2.4
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
react-remove-scroll: 2.6.0(@types/react@18.3.11)(react@18.3.1)
optionalDependencies:
'@types/react': 18.3.11
'@types/react-dom': 18.3.0
'@radix-ui/react-popover@1.1.2(@types/react-dom@18.3.0)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': '@radix-ui/react-popover@1.1.2(@types/react-dom@18.3.0)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies: dependencies:
'@radix-ui/primitive': 1.1.0 '@radix-ui/primitive': 1.1.0