import { PermissionAction } from '@supabase/shared-types/out/constants' import { useQueryClient } from '@tanstack/react-query' import { useDebounce } from '@uidotdev/usehooks' import { useParams } from 'common' import { AnimatePresence, motion } from 'framer-motion' import { compact, get, sum, uniqBy } from 'lodash' import { Upload } from 'lucide-react' import { DragEventHandler, useCallback, useMemo, useRef, useState } from 'react' import { toast } from 'sonner' import { Checkbox, cn } from 'ui' import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader' import { STORAGE_ROW_STATUS, STORAGE_ROW_TYPES, STORAGE_VIEWS } from '../Storage.constants' import type { StorageItem } from '../Storage.types' import { formatFolderItems } from '../StorageExplorer/StorageExplorer.utils' import { useStoragePreference } from '../StorageExplorer/useStoragePreference' import { uploadFilesToBucket } from './BucketFilePickerDialog.utils' import { BucketFilePickerRow } from './BucketFilePickerRow' import { useBucketFilePickerStateSnapshot } from './BucketFilePickerState' import { InfiniteListDefault, LoaderForIconMenuItems } from '@/components/ui/InfiniteList' import { useProjectApiUrl } from '@/data/config/project-endpoint-query' import { useBucketObjectsInfiniteQuery } from '@/data/storage/bucket-objects-infinite-query' import { storageKeys } from '@/data/storage/keys' import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions' import { formatBytes } from '@/lib/helpers' import { noop } from '@/lib/void' const SelectAllCheckbox = ({ columnFiles, selectedFilesFromColumn, onChange, }: { columnFiles: StorageItem[] selectedFilesFromColumn: StorageItem[] onChange: () => void }) => ( ) const DragOverOverlay = ({ isOpen, onDragLeave, onDrop, folderIsEmpty, }: { isOpen: boolean onDragLeave: () => void onDrop: () => void folderIsEmpty: boolean }) => { return ( {isOpen && (
{!folderIsEmpty && (

Drop your files to upload to this folder

)}
)}
) } export interface BucketFilePickerColumnProps { index: number fullWidth?: boolean } export const BucketFilePickerColumn = ({ index, fullWidth = false, }: BucketFilePickerColumnProps) => { const { ref: projectRef } = useParams() const queryClient = useQueryClient() const [isDraggedOver, setIsDraggedOver] = useState(false) const columnRef = useRef(null) const { hostEndpoint } = useProjectApiUrl({ projectRef: projectRef! }) const { can: canUpdateStorage } = useAsyncCheckPermissions(PermissionAction.STORAGE_WRITE, '*') const { columns, itemSearchString, bucket, maxFiles, acceptedFileExtensions, pushColumnAtIndex, selectedItems, setSelectedItems, clearSelectedItems, selectedFilePreview, setSelectedFilePreview, popColumnAtIndex, } = useBucketFilePickerStateSnapshot() const isFileAccepted = (fileName: string) => { if (!acceptedFileExtensions || acceptedFileExtensions.length === 0) return true const ext = fileName.split('.').pop()?.toLowerCase() ?? '' return acceptedFileExtensions.map((e) => e.replace(/^\./, '').toLowerCase()).includes(ext) } const path = columns.slice(0, index).join('/') const selectedFolder = columns[index] const setSelectedFolder = (folderName: string | null) => { if (folderName) { pushColumnAtIndex(folderName, index) } } const isLastFolder = index === columns.length const { view, sortBy, sortByOrder } = useStoragePreference(projectRef!) const debouncedSearchString = useDebounce(itemSearchString, 500) const { data, isLoading, isFetching, fetchNextPage, hasNextPage } = useBucketObjectsInfiniteQuery( { projectRef, bucketId: bucket.id, path, options: { sortBy: { column: sortBy, order: sortByOrder, }, // When a user tries to search, only search in the last opened folder (rightmost column) ...(isLastFolder && debouncedSearchString ? { search: debouncedSearchString } : {}), }, } ) const items = useMemo(() => { const objs = data?.pages.flatMap((page) => page) || [] return formatFolderItems(objs) }, [data]) const haveSelectedItems = selectedItems.length > 0 const columnItemsId = items.map((item) => item.id) const columnFiles = items.filter((item) => item.type === STORAGE_ROW_TYPES.FILE) const selectedItemsFromColumn = selectedItems.filter((item) => columnItemsId.includes(item.id)) const selectedFilesFromColumn = selectedItemsFromColumn.filter( (item) => item.type === STORAGE_ROW_TYPES.FILE ) const columnItems = items.map((item) => ({ ...item, columnIndex: index })) const columnItemsSize = sum(columnItems.map((item) => get(item, ['metadata', 'size'], 0))) const isEmpty = items.filter((item) => item.status !== STORAGE_ROW_STATUS.LOADING).length === 0 const getItemKey = useCallback( (index: number) => { const item = columnItems[index] return item?.id || `file-explorer-item-${index}` }, [columnItems] ) const itemProps = useMemo( () => ({ view: view, columnIndex: index, selectedItems, hideCheckbox: maxFiles === 1, }), [view, index, selectedItems, maxFiles] ) const onSelectAllItemsInColumn = () => { const columnFiles = columnItems.filter((item) => item.type === STORAGE_ROW_TYPES.FILE) const columnFilesId = compact(columnFiles.map((item) => item.id)) const selectedItemsFromColumn = selectedItems.filter( (item) => item.id && columnFilesId.includes(item.id) ) if (selectedItemsFromColumn.length === columnFiles.length) { // Deselect all items from column const updatedSelectedItems = selectedItems.filter( (item) => item.id && !columnFilesId.includes(item.id) ) setSelectedItems(updatedSelectedItems) } else { // Select all items from column const updatedSelectedItems = uniqBy(selectedItems.concat(columnFiles), 'id') setSelectedItems(updatedSelectedItems) } } const onSelectColumnEmptySpace = (columnIndex: number) => { popColumnAtIndex(columnIndex) setSelectedFilePreview(undefined) clearSelectedItems() } const onDragOver: DragEventHandler = (event) => { if (event) { event.stopPropagation() event.preventDefault() if (event.type === 'dragover' && !isDraggedOver) { setIsDraggedOver(true) } } } const onDrop: DragEventHandler = async (event) => { onDragOver(event) if (!canUpdateStorage) { toast('You need additional permissions to upload files to this project') return } if (!hostEndpoint) { toast.error('Unable to upload files at this time. Please try again.') return } const files = Array.from(event.dataTransfer?.files ?? []) as File[] await uploadFilesToBucket({ files, projectRef: projectRef!, hostEndpoint: hostEndpoint, bucketName: bucket.name, bucketId: bucket.id, currentPath: columns.slice(0, index).join('/'), queryClient, }) queryClient.invalidateQueries({ queryKey: storageKeys.objects(projectRef!, bucket.id, columns.slice(0, index).join('/')), }) setIsDraggedOver(false) } return ( <>
{ const eventTarget = get(event.target, ['className'], '') if (typeof eventTarget === 'string' && eventTarget.includes('react-contexify')) return onSelectColumnEmptySpace(index) }} > {/* Checkbox selection for select all */} {view === STORAGE_VIEWS.COLUMNS && maxFiles !== 1 && (
event.stopPropagation()} > {columnFiles.length > 0 ? ( <> onSelectAllItemsInColumn()} />

Select all {columnFiles.length} files

) : (

No files available for selection

)}
)} {/* List Interface Header */} {view === STORAGE_VIEWS.LIST && (
{maxFiles !== 1 && ( onSelectAllItemsInColumn()} /> )}

Name

Size

Type

Created at

Last modified at

)} {/* Shimmering loaders while fetching contents */} {isLoading && (
)} {/* Column Interface */} {columnItems.length > 0 && ( (index !== 0 && index === columnItems.length ? 85 : 37)} // eslint-disable-next-line react/no-unstable-nested-components ItemComponent={(props) => { const item = props.item const isPreviewed = !!( selectedFilePreview?.id !== null && selectedFilePreview?.id === item.id ) const isOpened = selectedFolder !== null && item.type === STORAGE_ROW_TYPES.FOLDER && selectedFolder === item.name return ( i.id === item.id)} hideCheckbox={maxFiles === 1} isDisabled={ item.type === STORAGE_ROW_TYPES.FILE && !isFileAccepted(item.name ?? '') } onClick={(event) => { event.stopPropagation() event.preventDefault() if (item.status !== STORAGE_ROW_STATUS.LOADING && !isOpened && !isPreviewed) { if (item.type === STORAGE_ROW_TYPES.FOLDER) { setSelectedFilePreview(undefined) setSelectedFolder(item.name) } else { setSelectedFilePreview(item) // deselect all folders when previewing a file popColumnAtIndex(index) clearSelectedItems() } } }} /> ) }} LoaderComponent={LoaderForIconMenuItems} hasNextPage={hasNextPage} isLoadingNextPage={isFetching} onLoadNextPage={fetchNextPage} /> )} {debouncedSearchString.length > 0 && isEmpty && !isLoading && (

No results found in this folder

Your search for "{debouncedSearchString}" did not return any results

)} {debouncedSearchString.length === 0 && isEmpty && !isLoading && (

Drop your files here

Or upload them via the "Upload files" button above

)} setIsDraggedOver(false)} onDrop={() => setIsDraggedOver(false)} /> {/* List interface footer */} {view === STORAGE_VIEWS.LIST && (

{formatBytes(columnItemsSize)} for {columnItems.length} items

)}
{selectedFolder ? ( ) : null} ) }