Files
mule-image/frontend/src/components/sharing/ShareDialog.tsx
dtoro 7efac4354e ui: migrate to shadcn/ui primitives across dialogs, filters, and forms
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>
2026-04-15 09:07:20 +02:00

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>
)
}