BucketFilePickerRow.tsx184 lines · main
| 1 | import { FilesBucket as FilesBucketIcon } from 'icons' |
| 2 | import { AlertCircle, File, Film, FolderOpen, Image, LoaderCircle, Music } from 'lucide-react' |
| 3 | import type { CSSProperties, MouseEvent } from 'react' |
| 4 | import { Checkbox, cn, Tooltip, TooltipContent, TooltipTrigger } from 'ui' |
| 5 | |
| 6 | import { STORAGE_ROW_STATUS, STORAGE_ROW_TYPES, STORAGE_VIEWS } from '../Storage.constants' |
| 7 | import { type StorageItem } from '../Storage.types' |
| 8 | import { formatBytes } from '@/lib/helpers' |
| 9 | |
| 10 | const RowIcon = ({ |
| 11 | view, |
| 12 | status, |
| 13 | fileType, |
| 14 | isOpened = false, |
| 15 | mimeType, |
| 16 | }: { |
| 17 | view: STORAGE_VIEWS |
| 18 | status: STORAGE_ROW_STATUS |
| 19 | fileType: string |
| 20 | isOpened?: boolean |
| 21 | mimeType: string | undefined |
| 22 | }) => { |
| 23 | if (view === STORAGE_VIEWS.LIST && status === STORAGE_ROW_STATUS.LOADING) { |
| 24 | return ( |
| 25 | <LoaderCircle size={14} strokeWidth={2} className="animate-spin text-foreground-lighter" /> |
| 26 | ) |
| 27 | } |
| 28 | |
| 29 | if (fileType === STORAGE_ROW_TYPES.FOLDER) { |
| 30 | return isOpened ? ( |
| 31 | <FolderOpen size={16} strokeWidth={2} className="text-foreground-lighter" /> |
| 32 | ) : ( |
| 33 | <FilesBucketIcon size={16} strokeWidth={2} className="text-foreground-lighter" /> |
| 34 | ) |
| 35 | } |
| 36 | |
| 37 | if (mimeType?.includes('image')) { |
| 38 | return <Image size={16} className="text-foreground-lighter" /> |
| 39 | } |
| 40 | |
| 41 | if (mimeType?.includes('audio')) { |
| 42 | return <Music size={16} strokeWidth={2} className="text-foreground-lighter" /> |
| 43 | } |
| 44 | |
| 45 | if (mimeType?.includes('video')) { |
| 46 | return <Film size={16} strokeWidth={2} className="text-foreground-lighter" /> |
| 47 | } |
| 48 | |
| 49 | return <File size={16} strokeWidth={2} className="text-foreground-lighter" /> |
| 50 | } |
| 51 | |
| 52 | interface BucketFilePickerRowProps { |
| 53 | item: StorageItem |
| 54 | view: STORAGE_VIEWS |
| 55 | isSelected: boolean |
| 56 | isPreviewed: boolean |
| 57 | isOpened: boolean |
| 58 | isDisabled?: boolean |
| 59 | hideCheckbox: boolean |
| 60 | onCheck: (isShiftKeyHeld: boolean) => void |
| 61 | onClick?: (event: MouseEvent<HTMLDivElement>) => void |
| 62 | style?: CSSProperties |
| 63 | } |
| 64 | |
| 65 | export const BucketFilePickerRow = ({ |
| 66 | item, |
| 67 | view = STORAGE_VIEWS.COLUMNS, |
| 68 | onCheck, |
| 69 | onClick, |
| 70 | isSelected, |
| 71 | isPreviewed, |
| 72 | isOpened, |
| 73 | isDisabled = false, |
| 74 | hideCheckbox, |
| 75 | style, |
| 76 | }: BucketFilePickerRowProps) => { |
| 77 | const size = item.metadata ? formatBytes(item.metadata.size) : '-' |
| 78 | const mimeType = item.metadata ? item.metadata.mimetype : '-' |
| 79 | const createdAt = item.created_at ? new Date(item.created_at).toLocaleString() : '-' |
| 80 | const updatedAt = item.updated_at ? new Date(item.updated_at).toLocaleString() : '-' |
| 81 | |
| 82 | const nameWidth = |
| 83 | view === STORAGE_VIEWS.LIST && item.isCorrupted |
| 84 | ? `calc(100% - 60px)` |
| 85 | : view === STORAGE_VIEWS.LIST && !item.isCorrupted |
| 86 | ? `calc(100% - 50px)` |
| 87 | : '100%' |
| 88 | |
| 89 | return ( |
| 90 | <div style={style} className="h-full border-b border-default"> |
| 91 | <div |
| 92 | className={cn( |
| 93 | 'storage-row group flex h-full items-center px-2.5', |
| 94 | 'hover:bg-panel-footer-light in-data-[theme*=dark]:hover:bg-panel-footer-dark', |
| 95 | `${isOpened ? 'bg-selection' : ''}`, |
| 96 | `${isSelected ? 'bg-selection' : ''}`, |
| 97 | `${isPreviewed ? 'bg-selection hover:bg-selection' : ''}`, |
| 98 | `${item.status !== STORAGE_ROW_STATUS.LOADING ? 'cursor-pointer' : ''}`, |
| 99 | isDisabled && 'cursor-not-allowed opacity-40 hover:bg-transparent' |
| 100 | )} |
| 101 | onClick={isDisabled ? undefined : onClick} |
| 102 | > |
| 103 | <div |
| 104 | className={cn( |
| 105 | 'flex items-center', |
| 106 | view === STORAGE_VIEWS.LIST ? 'w-[40%] min-w-[250px]' : 'w-[90%]' |
| 107 | )} |
| 108 | > |
| 109 | <div className="relative w-[30px]" onClick={(event) => event.stopPropagation()}> |
| 110 | <div |
| 111 | className={cn('top-0.5', { |
| 112 | absolute: !hideCheckbox, |
| 113 | 'group-hover:hidden': !hideCheckbox && item.type === STORAGE_ROW_TYPES.FILE, |
| 114 | hidden: isSelected, |
| 115 | })} |
| 116 | > |
| 117 | <RowIcon |
| 118 | view={view} |
| 119 | status={item.status} |
| 120 | fileType={item.type} |
| 121 | isOpened={isOpened} |
| 122 | mimeType={item.metadata?.mimetype} |
| 123 | /> |
| 124 | </div> |
| 125 | {!hideCheckbox && ( |
| 126 | <Checkbox |
| 127 | className={cn( |
| 128 | 'w-full', |
| 129 | { invisible: item.type !== STORAGE_ROW_TYPES.FILE }, |
| 130 | isSelected ? 'opacity-100' : 'opacity-0 group-hover:opacity-100' |
| 131 | )} |
| 132 | checked={isSelected} |
| 133 | onChange={(event) => { |
| 134 | event.stopPropagation() |
| 135 | onCheck((event.nativeEvent as KeyboardEvent).shiftKey) |
| 136 | }} |
| 137 | /> |
| 138 | )} |
| 139 | </div> |
| 140 | <p title={item.name} className="truncate text-sm" style={{ width: nameWidth }}> |
| 141 | {item.name} |
| 142 | </p> |
| 143 | {item.isCorrupted && ( |
| 144 | <Tooltip> |
| 145 | <TooltipTrigger> |
| 146 | <AlertCircle size={18} strokeWidth={2} className="text-foreground-light" /> |
| 147 | </TooltipTrigger> |
| 148 | <TooltipContent side="bottom"> |
| 149 | File is corrupted, please delete and reupload again. |
| 150 | </TooltipContent> |
| 151 | </Tooltip> |
| 152 | )} |
| 153 | </div> |
| 154 | |
| 155 | {view === STORAGE_VIEWS.LIST && ( |
| 156 | <> |
| 157 | <p className="w-[11%] min-w-[100px] truncate text-sm">{size}</p> |
| 158 | <p className="w-[14%] min-w-[100px] truncate text-sm">{mimeType}</p> |
| 159 | <p className="w-[15%] min-w-[160px] truncate text-sm">{createdAt}</p> |
| 160 | <p className="w-[15%] min-w-[160px] truncate text-sm">{updatedAt}</p> |
| 161 | </> |
| 162 | )} |
| 163 | |
| 164 | <div |
| 165 | className={`flex items-center justify-end ${ |
| 166 | view === STORAGE_VIEWS.LIST ? 'grow' : 'w-[10%]' |
| 167 | }`} |
| 168 | onClick={(event) => |
| 169 | // Stops click event from this div, to resolve an issue with menu item's click event triggering unexpected row select |
| 170 | event.stopPropagation() |
| 171 | } |
| 172 | > |
| 173 | {item.status === STORAGE_ROW_STATUS.LOADING && ( |
| 174 | <LoaderCircle |
| 175 | className={`animate-spin text-foreground-lighter ${view === STORAGE_VIEWS.LIST ? 'invisible' : ''}`} |
| 176 | size={14} |
| 177 | strokeWidth={2} |
| 178 | /> |
| 179 | )} |
| 180 | </div> |
| 181 | </div> |
| 182 | </div> |
| 183 | ) |
| 184 | } |