BucketFilePickerRow.tsx184 lines · main
1import { FilesBucket as FilesBucketIcon } from 'icons'
2import { AlertCircle, File, Film, FolderOpen, Image, LoaderCircle, Music } from 'lucide-react'
3import type { CSSProperties, MouseEvent } from 'react'
4import { Checkbox, cn, Tooltip, TooltipContent, TooltipTrigger } from 'ui'
5
6import { STORAGE_ROW_STATUS, STORAGE_ROW_TYPES, STORAGE_VIEWS } from '../Storage.constants'
7import { type StorageItem } from '../Storage.types'
8import { formatBytes } from '@/lib/helpers'
9
10const 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
52interface 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
65export 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}