Adopts shadcn/ui components (Dialog, Button, Input, Select, Popover, Command, Checkbox, Switch, Toggle, Calendar, etc.) across the app, replacing hand-rolled modals, dropdowns, and form controls. Adds a reusable cmdk-backed MultiSelect for the Type, Tags, and Flag filters so all multi-value filter popovers share one component and layout. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
196 lines
5.8 KiB
TypeScript
196 lines
5.8 KiB
TypeScript
import { useEffect, useState } from 'react'
|
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
|
import { Users, Trash2 } from 'lucide-react'
|
|
import { sharing, type ShareInfo } from '../../services/api'
|
|
import {
|
|
SHARED_HEAPS_KEY,
|
|
SHARED_FOLDERS_KEY,
|
|
} from '../../hooks/useSharingQueries'
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
} from '@/components/ui/dialog'
|
|
import { Button } from '@/components/ui/button'
|
|
import { Input } from '@/components/ui/input'
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from '@/components/ui/select'
|
|
import { Alert, AlertDescription } from '@/components/ui/alert'
|
|
|
|
interface ShareDialogProps {
|
|
isOpen: boolean
|
|
type: 'heap' | 'folder'
|
|
targetId: string
|
|
targetName: string
|
|
onClose: () => void
|
|
}
|
|
|
|
export function ShareDialog({
|
|
isOpen,
|
|
type,
|
|
targetId,
|
|
targetName,
|
|
onClose,
|
|
}: ShareDialogProps) {
|
|
const [username, setUsername] = useState('')
|
|
const [permission, setPermission] = useState<'read' | 'write'>('read')
|
|
const [error, setError] = useState<string | null>(null)
|
|
const queryClient = useQueryClient()
|
|
|
|
const sharesQueryKey = [type, 'shares', targetId]
|
|
|
|
const { data: shares = [], isLoading } = useQuery<ShareInfo[]>({
|
|
queryKey: sharesQueryKey,
|
|
queryFn: () =>
|
|
type === 'heap'
|
|
? sharing.heapShares(targetId)
|
|
: sharing.folderShares(targetId),
|
|
enabled: isOpen,
|
|
})
|
|
|
|
const addMutation = useMutation({
|
|
mutationFn: () =>
|
|
type === 'heap'
|
|
? sharing.shareHeap(targetId, username, permission)
|
|
: sharing.shareFolder(targetId, username, permission),
|
|
onSuccess: () => {
|
|
setUsername('')
|
|
setPermission('read')
|
|
setError(null)
|
|
queryClient.invalidateQueries({ queryKey: sharesQueryKey })
|
|
queryClient.invalidateQueries({
|
|
queryKey: type === 'heap' ? SHARED_HEAPS_KEY : SHARED_FOLDERS_KEY,
|
|
})
|
|
},
|
|
onError: (err: any) => {
|
|
setError(err?.response?.data?.detail || 'Failed to share')
|
|
},
|
|
})
|
|
|
|
const revokeMutation = useMutation({
|
|
mutationFn: (shareId: string) =>
|
|
type === 'heap'
|
|
? sharing.revokeHeapShare(targetId, shareId)
|
|
: sharing.revokeFolderShare(targetId, shareId),
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: sharesQueryKey })
|
|
queryClient.invalidateQueries({
|
|
queryKey: type === 'heap' ? SHARED_HEAPS_KEY : SHARED_FOLDERS_KEY,
|
|
})
|
|
},
|
|
})
|
|
|
|
useEffect(() => {
|
|
if (isOpen) {
|
|
setUsername('')
|
|
setPermission('read')
|
|
setError(null)
|
|
}
|
|
}, [isOpen])
|
|
|
|
return (
|
|
<Dialog open={isOpen} onOpenChange={(o) => !o && onClose()}>
|
|
<DialogContent className="max-w-[420px]">
|
|
<DialogHeader>
|
|
<DialogTitle className="flex items-center gap-2">
|
|
<Users className="h-4 w-4 text-text-muted" />
|
|
Share {type === 'heap' ? 'heap' : 'folder'}
|
|
</DialogTitle>
|
|
</DialogHeader>
|
|
|
|
<div className="text-sm text-text-muted">
|
|
Sharing <span className="font-medium text-text">{targetName}</span>
|
|
</div>
|
|
|
|
{/* Existing shares */}
|
|
{shares.length > 0 && (
|
|
<div className="space-y-1.5">
|
|
{shares.map((share) => (
|
|
<div
|
|
key={share.id}
|
|
className="flex items-center justify-between rounded border border-border bg-surface-2 px-3 py-1.5 text-sm"
|
|
>
|
|
<div className="flex items-center gap-2">
|
|
<span className="font-medium text-text">
|
|
{share.shared_with_username}
|
|
</span>
|
|
<span className="rounded bg-surface px-1.5 py-0.5 text-[10px] font-medium uppercase text-text-muted">
|
|
{share.permission}
|
|
</span>
|
|
</div>
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
onClick={() => revokeMutation.mutate(share.id)}
|
|
className="h-6 w-6 text-text-muted hover:text-reject"
|
|
title="Revoke access"
|
|
>
|
|
<Trash2 className="h-3.5 w-3.5" />
|
|
</Button>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
{isLoading && (
|
|
<div className="text-xs text-text-muted">Loading shares...</div>
|
|
)}
|
|
|
|
<form
|
|
onSubmit={(e) => {
|
|
e.preventDefault()
|
|
if (username.trim()) addMutation.mutate()
|
|
}}
|
|
className="space-y-3"
|
|
>
|
|
<div className="flex gap-2">
|
|
<Input
|
|
type="text"
|
|
value={username}
|
|
onChange={(e) => {
|
|
setUsername(e.target.value)
|
|
setError(null)
|
|
}}
|
|
placeholder="Username"
|
|
className="flex-1"
|
|
autoFocus
|
|
/>
|
|
<Select
|
|
value={permission}
|
|
onValueChange={(v) => setPermission(v as 'read' | 'write')}
|
|
>
|
|
<SelectTrigger className="w-auto">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="read">Read</SelectItem>
|
|
<SelectItem value="write">Read + Write</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
{error && (
|
|
<Alert variant="destructive">
|
|
<AlertDescription>{error}</AlertDescription>
|
|
</Alert>
|
|
)}
|
|
|
|
<div className="flex justify-end">
|
|
<Button
|
|
type="submit"
|
|
disabled={!username.trim() || addMutation.isPending}
|
|
>
|
|
{addMutation.isPending ? 'Sharing...' : 'Share'}
|
|
</Button>
|
|
</div>
|
|
</form>
|
|
</DialogContent>
|
|
</Dialog>
|
|
)
|
|
}
|