Files
Drake Costa ca4b3bc624 Refactor Storage Create/Edit/Empty/Delete Modals to use Shadcn components (#37517)
* Refactor `StorageMenu` modals to replace deprecated patterns

* add test for `DeleteBucketModal` and update test setup

Note: Because this component uses `useParams`, it's necessary to have the dynamic route segment passed to `next-router-mock`'s `createDynamicRouteParser`. In order not to have to manually list all of these as the application grows, I added a glob utility that uses the `pages/` directory  to automatically generate an array of dynamic route paths in this case.

* add test for `EmptyBucketModal`

* add test for `EditBucketModal` and add `isNonNullable` utility function

* add test for `CreateBucketModal`

* implement requested changes

* implement visual fixes
2025-08-14 21:23:08 +02:00

123 lines
4.1 KiB
TypeScript

import { useState } from 'react'
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { Columns3, Edit2, MoreVertical, Trash, XCircle } from 'lucide-react'
import Link from 'next/link'
import type { Bucket } from 'data/storage/buckets-query'
import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions'
import EditBucketModal from 'components/interfaces/Storage/EditBucketModal'
import DeleteBucketModal from 'components/interfaces/Storage/DeleteBucketModal'
import EmptyBucketModal from 'components/interfaces/Storage/EmptyBucketModal'
import {
Badge,
Button,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
Tooltip,
TooltipContent,
TooltipTrigger,
cn,
} from 'ui'
export interface BucketRowProps {
bucket: Bucket
projectRef?: string
isSelected: boolean
}
const BucketRow = ({ bucket, projectRef = '', isSelected = false }: BucketRowProps) => {
const { can: canUpdateBuckets } = useAsyncCheckProjectPermissions(
PermissionAction.STORAGE_WRITE,
'*'
)
const [modal, setModal] = useState<string | null>(null)
const onClose = () => setModal(null)
return (
<div
key={bucket.id}
className={cn(
'group flex items-center justify-between rounded-md',
isSelected && 'text-foreground bg-surface-100'
)}
>
{/* Even though we trim whitespaces from bucket names, there may be some existing buckets with trailing whitespaces. */}
<Link
href={`/project/${projectRef}/storage/buckets/${encodeURIComponent(bucket.id)}`}
className={'py-1 px-3 grow'}
>
<div className="flex items-center justify-between space-x-2 truncate w-full">
<p
className={`text-sm group-hover:text-foreground transition truncate ${
isSelected ? 'text-foreground' : 'text-foreground-light'
}`}
title={bucket.name}
>
{bucket.name}
</p>
{bucket.public && <Badge variant="warning">Public</Badge>}
{bucket.type === 'ANALYTICS' && (
<Tooltip>
<TooltipTrigger asChild>
<Columns3 className="text-foreground-lighter" size="20" />
</TooltipTrigger>
<TooltipContent>
<p>Analytics bucket</p>
</TooltipContent>
</Tooltip>
)}
</div>
</Link>
{canUpdateBuckets && isSelected ? (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button type="text" icon={<MoreVertical />} className="mr-1 h-6 w-6" />
</DropdownMenuTrigger>
<DropdownMenuContent side="bottom" align="start">
{bucket.type !== 'ANALYTICS' && (
<>
<DropdownMenuItem
key="toggle-private"
className="space-x-2"
onClick={() => setModal(`edit`)}
>
<Edit2 size={14} />
<p>Edit bucket</p>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
key="empty-bucket"
className="space-x-2"
onClick={() => setModal(`empty`)}
>
<XCircle size={14} />
<p>Empty bucket</p>
</DropdownMenuItem>
</>
)}
<DropdownMenuItem
key="delete-bucket"
className="space-x-2"
onClick={() => setModal(`delete`)}
>
<Trash size={14} />
<p>Delete bucket</p>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
) : (
<div className="w-7 mr-1" />
)}
<EditBucketModal visible={modal === `edit`} bucket={bucket} onClose={onClose} />
<EmptyBucketModal visible={modal === `empty`} bucket={bucket} onClose={onClose} />
<DeleteBucketModal visible={modal === `delete`} bucket={bucket} onClose={onClose} />
</div>
)
}
export default BucketRow