Files
supabase/apps/studio/components/interfaces/Storage/BucketFilePickerDialog/BucketFilePickerPreviewPane.tsx
Ivan Vasilov 35905e70d5 feat: Add a logo picker for OAuth app creation sheet (#44995)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Logo field now accepts/editable logo URL, plus a new storage-based
Logo Picker to select or remove images from project storage.
* Full storage picker: browse buckets, columns/list views, search,
drag‑and‑drop uploads, file previews (image/audio/video), and
single-file selection with responsive mobile/desktop layouts.

* **Refactor**
* Logo submission streamlined to send the provided URL directly (legacy
file-read/upload flow removed).
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-05-06 16:44:18 +02:00

192 lines
6.4 KiB
TypeScript

import { useParams } from 'common'
import { AlertCircle, LoaderCircle, X } from 'lucide-react'
import SVG from 'react-inlinesvg'
import { Button } from 'ui'
import { StorageItemWithColumn } from '../Storage.types'
import { useFetchFileUrlQuery } from '../StorageExplorer/useFetchFileUrlQuery'
import { useBucketFilePickerStateSnapshot } from './BucketFilePickerState'
import { BASE_PATH } from '@/lib/constants'
import { formatBytes } from '@/lib/helpers'
const PREVIEW_SIZE_LIMIT = 10 * 1024 * 1024 // 10MB
const PreviewFile = ({ item }: { item: StorageItemWithColumn }) => {
const { ref: projectRef } = useParams()
const { bucket, columns } = useBucketFilePickerStateSnapshot()
const path = columns.slice(0, item.columnIndex).concat(item.name).join('/')
const { data: previewUrl, isPending: isLoading } = useFetchFileUrlQuery({
path,
projectRef: projectRef!,
bucket,
})
// if the size is not available, we set it to be greater than the max size
const size = +(item.metadata?.size ?? PREVIEW_SIZE_LIMIT + 1)
const mimeType = item.metadata?.mimetype
const isSkipped = !!mimeType && !!size && size > PREVIEW_SIZE_LIMIT
if (isLoading) {
return (
<div className="flex h-full w-full items-center justify-center text-foreground-lighter">
<LoaderCircle size={14} strokeWidth={2} className="animate-spin text-foreground-lighter" />
</div>
)
}
if (isSkipped) {
return (
<div className="flex h-full w-full flex-col items-center justify-center">
<SVG
src={`${BASE_PATH}/img/file-filled.svg`}
preProcessor={(code) =>
code.replace(/svg/, 'svg class="mx-auto w-32 h-32 text-color-inherit opacity-75"')
}
/>
<p className="mt-2 w-2/5 text-center text-sm">
File size is too large to preview in the explorer
</p>
</div>
)
}
if (!mimeType || !previewUrl) {
return (
<SVG
src={`${BASE_PATH}/img/file-filled.svg`}
preProcessor={(code) =>
code.replace(/svg/, 'svg class="mx-auto w-32 h-32 text-color-inherit opacity-75"')
}
/>
)
}
if (mimeType.includes('image')) {
return (
<div
className="flex h-full w-full items-center justify-center bg-contain bg-center bg-no-repeat"
style={{ backgroundImage: `url('${previewUrl}')` }}
/>
)
}
if (mimeType.includes('audio')) {
return (
<div className="flex h-full w-full items-center justify-center px-10">
<audio key={previewUrl} controls style={{ width: 'inherit' }}>
<source src={previewUrl} type="audio/mpeg" />
<p className="text-sm text-foreground-light">
Your browser does not support the audio element.
</p>
</audio>
</div>
)
}
if (mimeType.includes('video')) {
return (
<div className="flex h-full w-full items-center justify-center">
<video key={previewUrl} controls style={{ maxHeight: '100%' }}>
<source src={previewUrl} type="video/mp4" />
<p className="text-sm text-foreground-light">
Your browser does not support the video tag.
</p>
</video>
</div>
)
}
return (
<SVG
src={`${BASE_PATH}/img/file-filled.svg`}
preProcessor={(code) =>
code.replace(/svg/, 'svg class="mx-auto w-32 h-32 text-color-inherit opacity-75"')
}
/>
)
}
export const PreviewPane = ({ onSelect }: { onSelect: (url: string) => void }) => {
const { ref: projectRef } = useParams()
const {
selectedFilePreview: file,
setSelectedFilePreview,
bucket,
columns,
} = useBucketFilePickerStateSnapshot()
const path = file ? columns.slice(0, file.columnIndex).concat(file.name).join('/') : ''
const { data: previewUrl, isLoading } = useFetchFileUrlQuery(
{ path, projectRef: projectRef!, bucket },
{ enabled: !!file }
)
if (!file) return null
const size = file.metadata ? formatBytes(file.metadata.size) : null
const mimeType = file.metadata ? file.metadata.mimetype : undefined
const createdAt = file.created_at ? new Date(file.created_at).toLocaleString() : 'Unknown'
const updatedAt = file.updated_at ? new Date(file.updated_at).toLocaleString() : 'Unknown'
return (
<div className="h-full border-l border-overlay bg-surface-100 overflow-y-auto w-[450px] pb-4">
{/* Preview Header */}
<div className="flex w-full justify-end items-center gap-2 sticky top-0 bg-surface-100 p-4 border-b">
<Button
size="tiny"
onClick={() => onSelect(previewUrl!)}
disabled={!previewUrl}
loading={isLoading}
>
Select
</Button>
<div className="text-foreground-lighter transition-colors hover:text-foreground">
<X
className="cursor-pointer"
size={14}
strokeWidth={2}
onClick={() => setSelectedFilePreview(undefined)}
/>
</div>
</div>
{/* Preview Thumbnail*/}
<div className="my-4 border border-overlay mx-4">
<div className="flex h-56 w-full items-center 2xl:h-72">
<PreviewFile item={file} />
</div>
</div>
<div className="w-full space-y-6 px-4">
{/* Preview Information */}
<div className="space-y-1">
<h5 className="wrap-break-word text-base text-foreground">{file.name}</h5>
{file.isCorrupted && (
<div className="flex items-center space-x-2">
<AlertCircle size={14} strokeWidth={2} className="text-foreground-light" />
<p className="text-sm text-foreground-light">
File is corrupted, please delete and reupload this file again
</p>
</div>
)}
{mimeType && (
<p className="text-sm text-foreground-light">
{mimeType}
{size && <span> - {size}</span>}
</p>
)}
</div>
{/* Preview Metadata */}
<div className="space-y-2">
<div>
<label className="mb-1 text-xs text-foreground-lighter">Added on</label>
<p className="text-sm text-foreground-light">{createdAt}</p>
</div>
<div>
<label className="mb-1 text-xs text-foreground-lighter">Last modified</label>
<p className="text-sm text-foreground-light">{updatedAt}</p>
</div>
</div>
</div>
</div>
)
}