mirror of
https://github.com/supabase/supabase.git
synced 2026-10-06 01:45:10 +03:00
Merge pull request #11634 from supabase/chore/storage-explorer-support-custom-expiry-for-signed-url
Chore/storage explorer support custom expiry for signed url
This commit is contained in:
16 files changed
+552
-403
No files matched your search
@@ -40,10 +40,7 @@ const Header: FC<HeaderProps> = ({ sorts, filters, onAddColumn, onAddRow, header
|
||||
onAddRow={onAddRow}
|
||||
/>
|
||||
)}
|
||||
<div className="sb-grid-header__inner">
|
||||
<RefreshButton filters={filters} sorts={sorts} />
|
||||
{headerActions}
|
||||
</div>
|
||||
<div className="sb-grid-header__inner">{headerActions}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -77,6 +74,7 @@ const DefaultHeader: FC<DefaultHeaderProps> = ({ sorts, filters, onAddColumn, on
|
||||
return (
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<RefreshButton filters={filters} sorts={sorts} />
|
||||
<FilterDropdown />
|
||||
<SortPopover />
|
||||
</div>
|
||||
|
||||
@@ -158,18 +158,6 @@ const EnterpriseUpdate: FC<Props> = ({
|
||||
// Last todo to support enterprise billing on dashboard + E2E test
|
||||
const onConfirmPayment = async () => {
|
||||
setIsSubmitting(true)
|
||||
let token = captchaToken
|
||||
|
||||
try {
|
||||
if (!token) {
|
||||
const captchaResponse = await captchaRef.current?.execute({ async: true })
|
||||
token = captchaResponse?.response ?? null
|
||||
}
|
||||
} catch (error) {
|
||||
setIsSubmitting(false)
|
||||
return
|
||||
}
|
||||
|
||||
const payload = {
|
||||
...formSubscriptionUpdatePayload(
|
||||
currentSubscription,
|
||||
|
||||
@@ -177,6 +177,7 @@ const ProUpgrade: FC<Props> = ({
|
||||
}
|
||||
|
||||
const onConfirmPayment = async () => {
|
||||
setIsSubmitting(true)
|
||||
const payload = formSubscriptionUpdatePayload(
|
||||
currentSubscription,
|
||||
selectedTier,
|
||||
|
||||
@@ -169,6 +169,7 @@ const TeamUpgrade: FC<Props> = ({
|
||||
message: 'Team Plan is not enabled yet.',
|
||||
})
|
||||
}
|
||||
setIsSubmitting(true)
|
||||
const payload = formSubscriptionUpdatePayload(
|
||||
currentSubscription,
|
||||
selectedTier,
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
import { FC } from 'react'
|
||||
import { Badge, Dropdown, IconLoader, IconMoreVertical, IconTrash } from 'ui'
|
||||
|
||||
import ProductMenuItem from 'components/ui/ProductMenu/ProductMenuItem'
|
||||
import { STORAGE_ROW_STATUS } from 'components/to-be-cleaned/Storage/Storage.constants'
|
||||
|
||||
interface Props {
|
||||
bucket: any
|
||||
projectRef?: string
|
||||
isSelected: boolean
|
||||
onSelectDeleteBucket: (bucket: any) => void
|
||||
onSelectToggleBucketPublic: (bucket: any) => void
|
||||
}
|
||||
|
||||
const BucketRow: FC<Props> = ({
|
||||
bucket = {},
|
||||
projectRef = '',
|
||||
isSelected = false,
|
||||
onSelectDeleteBucket = () => {},
|
||||
onSelectToggleBucketPublic = () => {},
|
||||
}) => {
|
||||
return (
|
||||
<ProductMenuItem
|
||||
key={bucket.id}
|
||||
name={
|
||||
<div className="flex items-center justify-between space-x-2 truncate w-full">
|
||||
<p className="truncate" title={bucket.name}>
|
||||
{bucket.name}
|
||||
</p>
|
||||
{bucket.public && <Badge color="yellow">Public</Badge>}
|
||||
</div>
|
||||
}
|
||||
url={`/project/${projectRef}/storage/buckets/${bucket.id}`}
|
||||
isActive={isSelected}
|
||||
action={
|
||||
bucket.status === STORAGE_ROW_STATUS.LOADING ? (
|
||||
<IconLoader className="animate-spin" size={16} strokeWidth={2} />
|
||||
) : bucket.status === STORAGE_ROW_STATUS.READY ? (
|
||||
<Dropdown
|
||||
side="bottom"
|
||||
align="start"
|
||||
overlay={[
|
||||
<Dropdown.Item
|
||||
key="toggle-private"
|
||||
onClick={() => onSelectToggleBucketPublic(bucket)}
|
||||
>
|
||||
{bucket.public ? 'Make private' : 'Make public'}
|
||||
</Dropdown.Item>,
|
||||
<Dropdown.Separator key="bucket-separator" />,
|
||||
<Dropdown.Item
|
||||
icon={<IconTrash size="tiny" />}
|
||||
key="delete-bucket"
|
||||
onClick={() => onSelectDeleteBucket(bucket)}
|
||||
>
|
||||
Delete bucket
|
||||
</Dropdown.Item>,
|
||||
]}
|
||||
>
|
||||
<IconMoreVertical
|
||||
className="opacity-0 group-hover:opacity-100"
|
||||
size="tiny"
|
||||
strokeWidth={2}
|
||||
/>
|
||||
</Dropdown>
|
||||
) : (
|
||||
<div />
|
||||
)
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default BucketRow
|
||||
@@ -2,28 +2,18 @@ import { FC } from 'react'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { useStore } from 'hooks'
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
Dropdown,
|
||||
Menu,
|
||||
IconLoader,
|
||||
IconMoreVertical,
|
||||
Alert,
|
||||
IconEdit,
|
||||
IconTrash,
|
||||
} from 'ui'
|
||||
import { useParams } from 'hooks'
|
||||
import { Button, Menu, IconLoader, Alert, IconEdit } from 'ui'
|
||||
|
||||
import ProductMenuItem from 'components/ui/ProductMenu/ProductMenuItem'
|
||||
import { STORAGE_ROW_STATUS } from 'components/to-be-cleaned/Storage/Storage.constants'
|
||||
import BucketRow from './BucketRow'
|
||||
import { useStorageStore } from 'localStores/storageExplorer/StorageExplorerStore'
|
||||
|
||||
interface Props {}
|
||||
|
||||
const StorageMenu: FC<Props> = () => {
|
||||
const router = useRouter()
|
||||
const { ref, bucketId } = router.query
|
||||
const { ref, bucketId } = useParams()
|
||||
|
||||
const page = router.pathname.split('/')[4] as
|
||||
| undefined
|
||||
| 'policies'
|
||||
@@ -31,9 +21,6 @@ const StorageMenu: FC<Props> = () => {
|
||||
| 'usage'
|
||||
| 'logs'
|
||||
|
||||
const { ui } = useStore()
|
||||
const projectRef = ui.selectedProject?.ref
|
||||
|
||||
const storageExplorerStore = useStorageStore()
|
||||
const {
|
||||
loaded,
|
||||
@@ -98,7 +85,7 @@ const StorageMenu: FC<Props> = () => {
|
||||
<div className="h-px w-full bg-scale-500"></div>
|
||||
<div className="">
|
||||
<Menu.Group title="Configuration" />
|
||||
<Link href={`/project/${projectRef}/storage/policies`}>
|
||||
<Link href={`/project/${ref}/storage/policies`}>
|
||||
<Menu.Item rounded active={page === 'policies'}>
|
||||
<p className="truncate">Policies</p>
|
||||
</Menu.Item>
|
||||
@@ -110,61 +97,3 @@ const StorageMenu: FC<Props> = () => {
|
||||
}
|
||||
|
||||
export default observer(StorageMenu)
|
||||
|
||||
const BucketRow = ({
|
||||
bucket = {},
|
||||
projectRef = '',
|
||||
isSelected = false,
|
||||
onSelectDeleteBucket = () => {},
|
||||
onSelectToggleBucketPublic = () => {},
|
||||
}: any) => {
|
||||
return (
|
||||
<ProductMenuItem
|
||||
key={bucket.id}
|
||||
name={
|
||||
<div className="flex items-center justify-between space-x-2 truncate w-full">
|
||||
<p className="truncate" title={bucket.name}>
|
||||
{bucket.name}
|
||||
</p>
|
||||
{bucket.public && <Badge color="yellow">Public</Badge>}
|
||||
</div>
|
||||
}
|
||||
url={`/project/${projectRef}/storage/buckets/${bucket.id}`}
|
||||
isActive={isSelected}
|
||||
action={
|
||||
bucket.status === STORAGE_ROW_STATUS.LOADING ? (
|
||||
<IconLoader className="animate-spin" size={16} strokeWidth={2} />
|
||||
) : bucket.status === STORAGE_ROW_STATUS.READY ? (
|
||||
<Dropdown
|
||||
side="bottom"
|
||||
align="start"
|
||||
overlay={[
|
||||
<Dropdown.Item
|
||||
key="toggle-private"
|
||||
onClick={() => onSelectToggleBucketPublic(bucket)}
|
||||
>
|
||||
{bucket.public ? 'Make private' : 'Make public'}
|
||||
</Dropdown.Item>,
|
||||
<Dropdown.Separator key="bucket-separator" />,
|
||||
<Dropdown.Item
|
||||
icon={<IconTrash size="tiny" />}
|
||||
key="delete-bucket"
|
||||
onClick={() => onSelectDeleteBucket(bucket)}
|
||||
>
|
||||
Delete bucket
|
||||
</Dropdown.Item>,
|
||||
]}
|
||||
>
|
||||
<IconMoreVertical
|
||||
className="opacity-0 group-hover:opacity-100"
|
||||
size="tiny"
|
||||
strokeWidth={2}
|
||||
/>
|
||||
</Dropdown>
|
||||
) : (
|
||||
<div />
|
||||
)
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -1,3 +1,9 @@
|
||||
export const URL_EXPIRY_DURATION = {
|
||||
WEEK: 60 * 60 * 24 * 7,
|
||||
MONTH: 60 * 60 * 24 * 30,
|
||||
YEAR: 60 * 60 * 24 * 365,
|
||||
}
|
||||
|
||||
export const STORAGE_VIEWS = {
|
||||
COLUMNS: 'COLUMNS',
|
||||
LIST: 'LIST',
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
import { FC } from 'react'
|
||||
import { Form, Modal, Input, Button } from 'ui'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { useStorageStore } from 'localStores/storageExplorer/StorageExplorerStore'
|
||||
|
||||
interface Props {}
|
||||
|
||||
const CustomExpiryModal: FC<Props> = () => {
|
||||
const storageExplorerStore = useStorageStore()
|
||||
const { selectedFileCustomExpiry, setSelectedFileCustomExpiry, copyFileURLToClipboard } =
|
||||
storageExplorerStore
|
||||
|
||||
const visible = selectedFileCustomExpiry !== undefined
|
||||
const onClose = () => setSelectedFileCustomExpiry(undefined)
|
||||
|
||||
return (
|
||||
<Modal
|
||||
hideFooter
|
||||
size="small"
|
||||
header="Custom expiry for signed URL"
|
||||
visible={visible}
|
||||
alignFooter="right"
|
||||
confirmText="Get URL"
|
||||
onCancel={() => onClose()}
|
||||
>
|
||||
<Form
|
||||
validateOnBlur
|
||||
initialValues={{ expiresIn: '' }}
|
||||
onSubmit={async (values: any, { setSubmitting }: any) => {
|
||||
setSubmitting(true)
|
||||
await copyFileURLToClipboard(selectedFileCustomExpiry, values.expiresIn)
|
||||
setSubmitting(false)
|
||||
onClose()
|
||||
}}
|
||||
validate={(values: any) => {
|
||||
const errors: any = {}
|
||||
if (values.expiresIn !== '' && values.expiresIn <= 0)
|
||||
errors.expiresIn = 'Expiry duration cannot be less than 0'
|
||||
return errors
|
||||
}}
|
||||
>
|
||||
{({ values, isSubmitting }: { values: any; isSubmitting: boolean }) => (
|
||||
<>
|
||||
<div className="py-6">
|
||||
<Modal.Content>
|
||||
<Input
|
||||
disabled={isSubmitting}
|
||||
type="number"
|
||||
id="expiresIn"
|
||||
label="Enter the duration for which the URL will be valid:"
|
||||
actions={<p className="text-sm text-scale-1000 mr-2">seconds</p>}
|
||||
/>
|
||||
</Modal.Content>
|
||||
</div>
|
||||
<Modal.Separator />
|
||||
<Modal.Content>
|
||||
<div className="flex items-center justify-end space-x-2 pt-1 pb-3">
|
||||
<Button type="default" onClick={() => onClose()}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
disabled={values.expiresIn === '' || isSubmitting}
|
||||
loading={isSubmitting}
|
||||
htmlType="submit"
|
||||
type="primary"
|
||||
>
|
||||
Get signed URL
|
||||
</Button>
|
||||
</div>
|
||||
</Modal.Content>
|
||||
</>
|
||||
)}
|
||||
</Form>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default observer(CustomExpiryModal)
|
||||
@@ -1,10 +1,12 @@
|
||||
import React, { useEffect, useRef } from 'react'
|
||||
import { STORAGE_VIEWS, CONTEXT_MENU_KEYS } from '../Storage.constants'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
|
||||
import { STORAGE_VIEWS, CONTEXT_MENU_KEYS } from '../Storage.constants'
|
||||
import ItemContextMenu from './ItemContextMenu'
|
||||
import FolderContextMenu from './FolderContextMenu'
|
||||
import ColumnContextMenu from './ColumnContextMenu'
|
||||
import FileExplorerColumn from './FileExplorerColumn'
|
||||
import { useStorageStore } from 'localStores/storageExplorer/StorageExplorerStore'
|
||||
|
||||
const FileExplorer = ({
|
||||
view = STORAGE_VIEWS.COLUMNS,
|
||||
@@ -12,18 +14,7 @@ const FileExplorer = ({
|
||||
openedFolders = [],
|
||||
selectedItems = [],
|
||||
selectedFilePreview = {},
|
||||
onCheckItem = () => {},
|
||||
onSelectItemDelete = () => {},
|
||||
onSelectItemRename = () => {},
|
||||
onSelectItemMove = () => {},
|
||||
onSelectFile = () => {},
|
||||
onRenameFile = () => {},
|
||||
onFilesUpload = () => {},
|
||||
onCopyFileURL = () => {},
|
||||
onDownloadFile = () => {},
|
||||
onSelectFolder = () => {},
|
||||
onRenameFolder = () => {},
|
||||
onCreateFolder = () => {},
|
||||
onSelectAllItemsInColumn = () => {},
|
||||
onSelectColumnEmptySpace = () => {},
|
||||
onSelectCreateFolder = () => {},
|
||||
@@ -33,6 +24,9 @@ const FileExplorer = ({
|
||||
onColumnLoadMore = () => {},
|
||||
}) => {
|
||||
const fileExplorerRef = useRef(null)
|
||||
const storageExplorerStore = useStorageStore()
|
||||
|
||||
const { setSelectedItemToRename } = storageExplorerStore
|
||||
|
||||
useEffect(() => {
|
||||
if (fileExplorerRef) {
|
||||
@@ -56,18 +50,11 @@ const FileExplorer = ({
|
||||
onSelectSort={onChangeSortBy}
|
||||
onSelectSortByOrder={onChangeSortByOrder}
|
||||
/>
|
||||
<ItemContextMenu
|
||||
id={CONTEXT_MENU_KEYS.STORAGE_ITEM}
|
||||
onCopyFileURL={onCopyFileURL}
|
||||
onSelectItemRename={onSelectItemRename}
|
||||
onSelectItemMove={onSelectItemMove}
|
||||
onDownloadFile={onDownloadFile}
|
||||
onSelectItemDelete={onSelectItemDelete}
|
||||
/>
|
||||
<ItemContextMenu id={CONTEXT_MENU_KEYS.STORAGE_ITEM} />
|
||||
<FolderContextMenu
|
||||
id={CONTEXT_MENU_KEYS.STORAGE_FOLDER}
|
||||
onRenameFolder={onSelectItemRename}
|
||||
onDeleteFolder={onSelectItemDelete}
|
||||
onRenameFolder={(folder) => setSelectedItemToRename(folder)}
|
||||
onDeleteFolder={(folder) => setSelectedItemsToDelete([folder])}
|
||||
/>
|
||||
{view === STORAGE_VIEWS.COLUMNS ? (
|
||||
<div className="flex">
|
||||
@@ -80,18 +67,7 @@ const FileExplorer = ({
|
||||
openedFolders={openedFolders}
|
||||
selectedItems={selectedItems}
|
||||
selectedFilePreview={selectedFilePreview}
|
||||
onCheckItem={onCheckItem}
|
||||
onSelectFile={onSelectFile}
|
||||
onRenameFile={onRenameFile}
|
||||
onCopyFileURL={onCopyFileURL}
|
||||
onFilesUpload={onFilesUpload}
|
||||
onDownloadFile={onDownloadFile}
|
||||
onSelectFolder={onSelectFolder}
|
||||
onRenameFolder={onRenameFolder}
|
||||
onCreateFolder={onCreateFolder}
|
||||
onSelectItemDelete={onSelectItemDelete}
|
||||
onSelectItemRename={onSelectItemRename}
|
||||
onSelectItemMove={onSelectItemMove}
|
||||
onSelectAllItemsInColumn={onSelectAllItemsInColumn}
|
||||
onSelectColumnEmptySpace={onSelectColumnEmptySpace}
|
||||
onColumnLoadMore={onColumnLoadMore}
|
||||
@@ -108,18 +84,7 @@ const FileExplorer = ({
|
||||
column={columns[columns.length - 1]}
|
||||
selectedItems={selectedItems}
|
||||
selectedFilePreview={selectedFilePreview}
|
||||
onCheckItem={onCheckItem}
|
||||
onSelectFile={onSelectFile}
|
||||
onRenameFile={onRenameFile}
|
||||
onCopyFileURL={onCopyFileURL}
|
||||
onFilesUpload={onFilesUpload}
|
||||
onDownloadFile={onDownloadFile}
|
||||
onSelectFolder={onSelectFolder}
|
||||
onRenameFolder={onRenameFolder}
|
||||
onCreateFolder={onCreateFolder}
|
||||
onSelectItemDelete={onSelectItemDelete}
|
||||
onSelectItemRename={onSelectItemRename}
|
||||
onSelectItemMove={onSelectItemMove}
|
||||
onSelectAllItemsInColumn={onSelectAllItemsInColumn}
|
||||
onSelectColumnEmptySpace={onSelectColumnEmptySpace}
|
||||
onColumnLoadMore={onColumnLoadMore}
|
||||
@@ -133,4 +98,4 @@ const FileExplorer = ({
|
||||
)
|
||||
}
|
||||
|
||||
export default FileExplorer
|
||||
export default observer(FileExplorer)
|
||||
@@ -57,18 +57,7 @@ const FileExplorerColumn = ({
|
||||
openedFolders = [],
|
||||
selectedItems = [],
|
||||
selectedFilePreview = {},
|
||||
onCheckItem = () => {},
|
||||
onSelectItemDelete = () => {},
|
||||
onSelectItemRename = () => {},
|
||||
onSelectItemMove = () => {},
|
||||
onSelectFile = () => {},
|
||||
onRenameFile = () => {},
|
||||
onCopyFileURL = () => {},
|
||||
onFilesUpload = () => {},
|
||||
onDownloadFile = () => {},
|
||||
onSelectFolder = () => {},
|
||||
onRenameFolder = () => {},
|
||||
onCreateFolder = () => {},
|
||||
onSelectAllItemsInColumn = () => {},
|
||||
onSelectColumnEmptySpace = () => {},
|
||||
onColumnLoadMore = () => {},
|
||||
@@ -212,17 +201,15 @@ const FileExplorerColumn = ({
|
||||
selectedItems,
|
||||
openedFolders,
|
||||
selectedFilePreview,
|
||||
onCheckItem,
|
||||
onSelectFile,
|
||||
onRenameFile,
|
||||
onCopyFileURL,
|
||||
onDownloadFile,
|
||||
onSelectFolder,
|
||||
onRenameFolder,
|
||||
onCreateFolder,
|
||||
onSelectItemDelete,
|
||||
onSelectItemRename,
|
||||
onSelectItemMove,
|
||||
// onCheckItem,
|
||||
// onRenameFile,
|
||||
// onCopyFileURL,
|
||||
// onDownloadFile,
|
||||
// onRenameFolder,
|
||||
// onCreateFolder,
|
||||
// onSelectItemDelete,
|
||||
// onSelectItemRename,
|
||||
// onSelectItemMove,
|
||||
}}
|
||||
ItemComponent={FileExplorerRow}
|
||||
getItemSize={(index) => (index !== 0 && index === columnItems.length ? 85 : 37)}
|
||||
|
||||
+135
-46
@@ -1,4 +1,4 @@
|
||||
import { useState, useRef, useEffect } from 'react'
|
||||
import { FC, useState, useRef, useEffect } from 'react'
|
||||
import { find, has, isEmpty, isEqual } from 'lodash'
|
||||
import {
|
||||
Checkbox,
|
||||
@@ -11,10 +11,11 @@ import {
|
||||
IconFile,
|
||||
IconAlertCircle,
|
||||
IconDownload,
|
||||
IconTrash,
|
||||
IconCopy,
|
||||
IconEdit,
|
||||
IconMove,
|
||||
IconClipboard,
|
||||
IconTrash2,
|
||||
IconChevronRight,
|
||||
} from 'ui'
|
||||
import SVG from 'react-inlinesvg'
|
||||
import * as Tooltip from '@radix-ui/react-tooltip'
|
||||
@@ -24,11 +25,12 @@ import {
|
||||
STORAGE_ROW_TYPES,
|
||||
STORAGE_ROW_STATUS,
|
||||
CONTEXT_MENU_KEYS,
|
||||
} from '../Storage.constants.ts'
|
||||
URL_EXPIRY_DURATION,
|
||||
} from '../Storage.constants'
|
||||
import { formatBytes } from 'lib/helpers'
|
||||
import { useStorageStore } from 'localStores/storageExplorer/StorageExplorerStore'
|
||||
|
||||
const RowIcon = ({ view, status, fileType, mimeType }) => {
|
||||
const RowIcon = ({ view, status, fileType, mimeType }: any) => {
|
||||
if (view === STORAGE_VIEWS.LIST && status === STORAGE_ROW_STATUS.LOADING) {
|
||||
return <IconLoader size={16} strokeWidth={2} className="animate-spin" />
|
||||
}
|
||||
@@ -43,7 +45,6 @@ const RowIcon = ({ view, status, fileType, mimeType }) => {
|
||||
return (
|
||||
<SVG
|
||||
src={iconSrc}
|
||||
alt={fileType}
|
||||
preProcessor={(code) =>
|
||||
code.replace(/svg/, 'svg class="w-4 h-4 text-color-inherit opacity-75"')
|
||||
}
|
||||
@@ -66,28 +67,44 @@ const RowIcon = ({ view, status, fileType, mimeType }) => {
|
||||
return <IconFile size={16} strokeWidth={2} />
|
||||
}
|
||||
|
||||
const FileExplorerRow = ({
|
||||
interface Props {
|
||||
item: any
|
||||
view: string
|
||||
columnIndex: number
|
||||
selectedItems: any[]
|
||||
openedFolders: any[]
|
||||
selectedFilePreview: any
|
||||
}
|
||||
|
||||
const FileExplorerRow: FC<Props> = ({
|
||||
item = {},
|
||||
view = STORAGE_VIEWS.COLUMNS,
|
||||
columnIndex = 0,
|
||||
selectedItems = [],
|
||||
openedFolders = [],
|
||||
selectedFilePreview = {},
|
||||
|
||||
onCheckItem = () => {},
|
||||
onSelectFile = () => {},
|
||||
onRenameFile = () => {},
|
||||
onCopyFileURL = () => {},
|
||||
onDownloadFile = () => {},
|
||||
onSelectFolder = () => {},
|
||||
onRenameFolder = () => {},
|
||||
onCreateFolder = () => {},
|
||||
onSelectItemDelete = () => {},
|
||||
onSelectItemRename = () => {},
|
||||
onSelectItemMove = () => {},
|
||||
}) => {
|
||||
const storageExplorerStore = useStorageStore()
|
||||
const { downloadFolder } = storageExplorerStore
|
||||
const {
|
||||
popColumnAtIndex,
|
||||
pushOpenedFolderAtIndex,
|
||||
popOpenedFoldersAtIndex,
|
||||
setFilePreview,
|
||||
closeFilePreview,
|
||||
clearSelectedItems,
|
||||
addNewFolder,
|
||||
renameFolder,
|
||||
renameFile,
|
||||
setSelectedItems,
|
||||
setSelectedItemsToDelete,
|
||||
setSelectedItemToRename,
|
||||
setSelectedItemsToMove,
|
||||
setSelectedFileCustomExpiry,
|
||||
fetchFolderContents,
|
||||
downloadFile,
|
||||
downloadFolder,
|
||||
copyFileURLToClipboard,
|
||||
} = storageExplorerStore
|
||||
|
||||
const itemWithColumnIndex = { ...item, columnIndex }
|
||||
const isSelected = find(selectedItems, item) !== undefined
|
||||
@@ -95,25 +112,47 @@ const FileExplorerRow = ({
|
||||
openedFolders.length > columnIndex ? isEqual(openedFolders[columnIndex], item) : false
|
||||
const isPreviewed = !isEmpty(selectedFilePreview) && isEqual(selectedFilePreview.id, item.id)
|
||||
|
||||
const onSelectFile = async (columnIndex: number, file: any) => {
|
||||
popColumnAtIndex(columnIndex)
|
||||
popOpenedFoldersAtIndex(columnIndex - 1)
|
||||
setFilePreview(file)
|
||||
clearSelectedItems()
|
||||
}
|
||||
|
||||
const onSelectFolder = async (columnIndex: number, folder: any) => {
|
||||
closeFilePreview()
|
||||
clearSelectedItems(columnIndex + 1)
|
||||
popOpenedFoldersAtIndex(columnIndex - 1)
|
||||
pushOpenedFolderAtIndex(folder, columnIndex)
|
||||
await fetchFolderContents(folder.id, folder.name, columnIndex)
|
||||
}
|
||||
|
||||
const onCheckItem = (item: any) => {
|
||||
if (find(selectedItems, item) === undefined) {
|
||||
setSelectedItems(selectedItems.concat([item]))
|
||||
} else {
|
||||
setSelectedItems(selectedItems.filter((selectedItem: any) => item.id !== selectedItem.id))
|
||||
}
|
||||
closeFilePreview()
|
||||
}
|
||||
|
||||
if (item.status === STORAGE_ROW_STATUS.EDITING) {
|
||||
const inputRef = useRef(null)
|
||||
const inputRef = useRef<any>(null)
|
||||
const [itemName, setItemName] = useState(item.name)
|
||||
|
||||
useEffect(() => {
|
||||
if (inputRef.current) {
|
||||
inputRef.current.select()
|
||||
}
|
||||
if (inputRef.current) inputRef.current.select()
|
||||
}, [])
|
||||
|
||||
const onSetItemName = (event) => {
|
||||
const onSetItemName = async (event: any) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
if (item.type === STORAGE_ROW_TYPES.FILE) {
|
||||
onRenameFile(item, itemName, columnIndex)
|
||||
await renameFile(item, itemName, columnIndex)
|
||||
} else if (has(item, 'id')) {
|
||||
onRenameFolder(itemWithColumnIndex, itemName, columnIndex)
|
||||
renameFolder(itemWithColumnIndex, itemName, columnIndex)
|
||||
} else {
|
||||
onCreateFolder(itemName, columnIndex)
|
||||
addNewFolder(itemName, columnIndex)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -146,57 +185,78 @@ const FileExplorerRow = ({
|
||||
}
|
||||
|
||||
const rowOptions =
|
||||
item.type === STORAGE_ROW_TYPES.BUCKET
|
||||
? [{ name: 'Delete', onClick: () => onSelectItemDelete(itemWithColumnIndex) }]
|
||||
: item.type === STORAGE_ROW_TYPES.FOLDER
|
||||
item.type === STORAGE_ROW_TYPES.FOLDER
|
||||
? [
|
||||
{
|
||||
name: 'Rename',
|
||||
icon: <IconEdit size="tiny" />,
|
||||
onClick: () => onSelectItemRename(itemWithColumnIndex),
|
||||
onClick: () => setSelectedItemToRename(itemWithColumnIndex),
|
||||
},
|
||||
{
|
||||
name: 'Download',
|
||||
icon: <IconDownload size="tiny" />,
|
||||
onClick: () => downloadFolder(itemWithColumnIndex),
|
||||
},
|
||||
{ name: 'Separator' },
|
||||
{ name: 'Separator', icon: undefined, onClick: undefined },
|
||||
{
|
||||
name: 'Delete',
|
||||
icon: <IconTrash size="tiny" />,
|
||||
onClick: () => onSelectItemDelete(itemWithColumnIndex),
|
||||
icon: <IconTrash2 size="tiny" />,
|
||||
onClick: () => setSelectedItemsToDelete([itemWithColumnIndex]),
|
||||
},
|
||||
]
|
||||
: [
|
||||
...(!item.isCorrupted
|
||||
? [
|
||||
{
|
||||
name: 'Copy URL',
|
||||
icon: <IconCopy size="tiny" />,
|
||||
onClick: () => onCopyFileURL(itemWithColumnIndex),
|
||||
name: 'Get URL',
|
||||
icon: <IconClipboard size="tiny" />,
|
||||
children: [
|
||||
{
|
||||
name: 'Expire in 1 week',
|
||||
onClick: async () =>
|
||||
await copyFileURLToClipboard(itemWithColumnIndex, URL_EXPIRY_DURATION.WEEK),
|
||||
},
|
||||
{
|
||||
name: 'Expire in 1 month',
|
||||
onClick: async () =>
|
||||
await copyFileURLToClipboard(
|
||||
itemWithColumnIndex,
|
||||
URL_EXPIRY_DURATION.MONTH
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'Expire in 1 year',
|
||||
onClick: async () =>
|
||||
await copyFileURLToClipboard(itemWithColumnIndex, URL_EXPIRY_DURATION.YEAR),
|
||||
},
|
||||
{
|
||||
name: 'Custom expiry',
|
||||
onClick: async () => setSelectedFileCustomExpiry(itemWithColumnIndex),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Rename',
|
||||
icon: <IconEdit size="tiny" />,
|
||||
onClick: () => onSelectItemRename(itemWithColumnIndex),
|
||||
onClick: () => setSelectedItemToRename(itemWithColumnIndex),
|
||||
},
|
||||
{
|
||||
name: 'Move',
|
||||
icon: <IconMove size="tiny" />,
|
||||
onClick: () => onSelectItemMove(itemWithColumnIndex),
|
||||
onClick: () => setSelectedItemsToMove([itemWithColumnIndex]),
|
||||
},
|
||||
{
|
||||
name: 'Download',
|
||||
icon: <IconDownload size="tiny" />,
|
||||
onClick: () => onDownloadFile(itemWithColumnIndex),
|
||||
onClick: async () => await downloadFile(itemWithColumnIndex),
|
||||
},
|
||||
{ name: 'Separator' },
|
||||
{ name: 'Separator', icon: undefined, onClick: undefined },
|
||||
]
|
||||
: []),
|
||||
{
|
||||
name: 'Delete',
|
||||
icon: <IconTrash size="tiny" />,
|
||||
onClick: () => onSelectItemDelete(itemWithColumnIndex),
|
||||
icon: <IconTrash2 size="tiny" />,
|
||||
onClick: () => setSelectedItemsToDelete([itemWithColumnIndex]),
|
||||
},
|
||||
]
|
||||
|
||||
@@ -206,7 +266,7 @@ const FileExplorerRow = ({
|
||||
const updatedAt = item.updated_at ? new Date(item.updated_at).toLocaleString() : '-'
|
||||
|
||||
const { show } = useContextMenu()
|
||||
const displayMenu = (event, rowType) => {
|
||||
const displayMenu = (event: any, rowType: any) => {
|
||||
show(event, {
|
||||
id:
|
||||
rowType === STORAGE_ROW_TYPES.FILE
|
||||
@@ -342,7 +402,36 @@ const FileExplorerRow = ({
|
||||
align="end"
|
||||
overlay={[
|
||||
rowOptions.map((option) => {
|
||||
if (option.name === 'Separator') {
|
||||
if ((option?.children ?? []).length > 0) {
|
||||
return (
|
||||
<Dropdown
|
||||
isNested
|
||||
key={option.name}
|
||||
side="right"
|
||||
align="start"
|
||||
overlay={(option?.children ?? [])?.map((child) => {
|
||||
return (
|
||||
<Dropdown.Item key={child.name} onClick={child.onClick}>
|
||||
{child.name}
|
||||
</Dropdown.Item>
|
||||
)
|
||||
})}
|
||||
>
|
||||
<div
|
||||
className={[
|
||||
'flex items-center justify-between px-4 py-1.5 text-xs text-scale-1100',
|
||||
'w-full focus:bg-scale-300 dark:focus:bg-scale-500 focus:text-scale-1200',
|
||||
].join(' ')}
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
{option.icon}
|
||||
<p>{option.name}</p>
|
||||
</div>
|
||||
<IconChevronRight size="tiny" />
|
||||
</div>
|
||||
</Dropdown>
|
||||
)
|
||||
} else if (option.name === 'Separator') {
|
||||
return <Dropdown.Separator key="row-separator" />
|
||||
} else {
|
||||
return (
|
||||
@@ -1,43 +1,97 @@
|
||||
import { Menu, Item } from 'react-contexify'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { Menu, Item, Separator, Submenu } from 'react-contexify'
|
||||
import 'react-contexify/dist/ReactContexify.css'
|
||||
import { IconClipboard, IconEdit, IconMove, IconDownload, IconTrash2, IconChevronRight } from 'ui'
|
||||
|
||||
import { URL_EXPIRY_DURATION } from '../Storage.constants'
|
||||
import { useStorageStore } from 'localStores/storageExplorer/StorageExplorerStore'
|
||||
|
||||
const ItemContextMenu = ({ id = '' }) => {
|
||||
const storageExplorerStore = useStorageStore()
|
||||
const {
|
||||
downloadFile,
|
||||
selectedBucket,
|
||||
setSelectedItemsToDelete,
|
||||
setSelectedItemToRename,
|
||||
setSelectedItemsToMove,
|
||||
setSelectedFileCustomExpiry,
|
||||
copyFileURLToClipboard,
|
||||
} = storageExplorerStore
|
||||
const isPublic = selectedBucket.public
|
||||
|
||||
const onHandleClick = async (event, item, expiresIn) => {
|
||||
if (item.isCorrupted) return
|
||||
switch (event) {
|
||||
case 'copy':
|
||||
if (expiresIn < 0) return setSelectedFileCustomExpiry(item)
|
||||
else return await copyFileURLToClipboard(item, expiresIn)
|
||||
case 'rename':
|
||||
return setSelectedItemToRename(item)
|
||||
case 'move':
|
||||
return setSelectedItemsToMove([item])
|
||||
case 'download':
|
||||
return await downloadFile(item)
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
const ItemContextMenu = ({
|
||||
id = '',
|
||||
onCopyFileURL = () => {},
|
||||
onSelectItemRename = () => {},
|
||||
onSelectItemMove = () => {},
|
||||
onDownloadFile = () => {},
|
||||
onSelectItemDelete = () => {},
|
||||
}) => {
|
||||
return (
|
||||
<Menu id={id} animation="fade" className="!bg-scale-300 border border-scale-500">
|
||||
<Item
|
||||
onClick={({ props }) => (!props.item.isCorrupted ? onCopyFileURL(props.item) : () => {})}
|
||||
>
|
||||
<span className="text-xs">Copy URL</span>
|
||||
{isPublic ? (
|
||||
<Item onClick={({ props }) => onHandleClick('copy', props.item)}>
|
||||
<IconClipboard size="tiny" />
|
||||
<span className="ml-2 text-xs">Get URL</span>
|
||||
</Item>
|
||||
) : (
|
||||
<Submenu
|
||||
label={
|
||||
<div className="flex items-center space-x-2">
|
||||
<IconClipboard size="tiny" />
|
||||
<span className="text-xs">Get URL</span>
|
||||
</div>
|
||||
}
|
||||
arrow={<IconChevronRight size="tiny" />}
|
||||
>
|
||||
<Item
|
||||
onClick={({ props }) => onHandleClick('copy', props.item, URL_EXPIRY_DURATION.WEEK)}
|
||||
>
|
||||
<span className="ml-2 text-xs">Expire in 1 week</span>
|
||||
</Item>
|
||||
<Item
|
||||
onClick={({ props }) => onHandleClick('copy', props.item, URL_EXPIRY_DURATION.MONTH)}
|
||||
>
|
||||
<span className="ml-2 text-xs">Expire in 1 month</span>
|
||||
</Item>
|
||||
<Item
|
||||
onClick={({ props }) => onHandleClick('copy', props.item, URL_EXPIRY_DURATION.YEAR)}
|
||||
>
|
||||
<span className="ml-2 text-xs">Expire in 1 year</span>
|
||||
</Item>
|
||||
<Item onClick={({ props }) => onHandleClick('copy', props.item, -1)}>
|
||||
<span className="ml-2 text-xs">Custom expiry</span>
|
||||
</Item>
|
||||
</Submenu>
|
||||
)}
|
||||
<Item onClick={({ props }) => onHandleClick('rename', props.item)}>
|
||||
<IconEdit size="tiny" />
|
||||
<span className="ml-2 text-xs">Rename</span>
|
||||
</Item>
|
||||
<Item
|
||||
onClick={({ props }) =>
|
||||
!props.item.isCorrupted ? onSelectItemRename(props.item) : () => {}
|
||||
}
|
||||
>
|
||||
<span className="text-xs">Rename</span>
|
||||
<Item onClick={({ props }) => onHandleClick('move', props.item)}>
|
||||
<IconMove size="tiny" />
|
||||
<span className="ml-2 text-xs">Move</span>
|
||||
</Item>
|
||||
<Item
|
||||
onClick={({ props }) => (!props.item.isCorrupted ? onSelectItemMove(props.item) : () => {})}
|
||||
>
|
||||
<span className="text-xs">Move</span>
|
||||
<Item onClick={({ props }) => onHandleClick('download', props.item)}>
|
||||
<IconDownload size="tiny" />
|
||||
<span className="ml-2 text-xs">Download</span>
|
||||
</Item>
|
||||
<Item
|
||||
onClick={({ props }) => (!props.item.isCorrupted ? onDownloadFile(props.item) : () => {})}
|
||||
>
|
||||
<span className="text-xs">Download</span>
|
||||
</Item>
|
||||
<Item onClick={({ props }) => onSelectItemDelete(props.item)}>
|
||||
<span className="text-xs">Delete</span>
|
||||
<Separator />
|
||||
<Item onClick={({ props }) => setSelectedItemsToDelete([props.item])}>
|
||||
<IconTrash2 size="tiny" />
|
||||
<span className="ml-2 text-xs">Delete</span>
|
||||
</Item>
|
||||
</Menu>
|
||||
)
|
||||
}
|
||||
|
||||
export default ItemContextMenu
|
||||
export default observer(ItemContextMenu)
|
||||
@@ -1,17 +1,20 @@
|
||||
import Link from 'next/link'
|
||||
import { isEmpty } from 'lodash'
|
||||
import {
|
||||
Button,
|
||||
Dropdown,
|
||||
IconX,
|
||||
IconLoader,
|
||||
IconClipboard,
|
||||
IconDownload,
|
||||
IconTrash2,
|
||||
IconAlertCircle,
|
||||
IconChevronDown,
|
||||
} from 'ui'
|
||||
import SVG from 'react-inlinesvg'
|
||||
import { formatBytes } from 'lib/helpers'
|
||||
import { Transition } from '@headlessui/react'
|
||||
import Link from 'next/link'
|
||||
import { URL_EXPIRY_DURATION } from '../Storage.constants'
|
||||
import { useStorageStore } from 'localStores/storageExplorer/StorageExplorerStore'
|
||||
|
||||
const PreviewFile = ({ mimeType, previewUrl }: { mimeType: string; previewUrl: string }) => {
|
||||
@@ -90,10 +93,12 @@ const PreviewFile = ({ mimeType, previewUrl }: { mimeType: string; previewUrl: s
|
||||
const PreviewPane = () => {
|
||||
const storageExplorerStore = useStorageStore()
|
||||
const {
|
||||
selectedBucket,
|
||||
selectedFilePreview: file,
|
||||
copyFileURLToClipboard,
|
||||
closeFilePreview,
|
||||
setSelectedItemsToDelete,
|
||||
setSelectedFileCustomExpiry,
|
||||
} = storageExplorerStore
|
||||
|
||||
const width = 450
|
||||
@@ -104,104 +109,148 @@ const PreviewPane = () => {
|
||||
const updatedAt = file.updated_at ? new Date(file.updated_at).toLocaleString() : 'Unknown'
|
||||
|
||||
return (
|
||||
<Transition
|
||||
show={isOpen}
|
||||
enter="transition ease-out duration-150"
|
||||
enterFrom="transform opacity-0"
|
||||
enterTo="transform opacity-100"
|
||||
leave="transition ease-in duration-100"
|
||||
leaveFrom="transform opacity-100"
|
||||
leaveTo="transform opacity-0"
|
||||
>
|
||||
<div
|
||||
className="
|
||||
<>
|
||||
<Transition
|
||||
show={isOpen}
|
||||
enter="transition ease-out duration-150"
|
||||
enterFrom="transform opacity-0"
|
||||
enterTo="transform opacity-100"
|
||||
leave="transition ease-in duration-100"
|
||||
leaveFrom="transform opacity-100"
|
||||
leaveTo="transform opacity-0"
|
||||
>
|
||||
<div
|
||||
className="
|
||||
h-full border-l
|
||||
border-panel-border-light bg-panel-header-light p-4 dark:border-panel-border-dark dark:bg-panel-header-dark"
|
||||
style={{ width }}
|
||||
>
|
||||
{/* Preview Header */}
|
||||
<div className="flex w-full justify-end text-scale-900 transition-colors hover:text-scale-1200">
|
||||
<IconX
|
||||
className="cursor-pointer"
|
||||
size={14}
|
||||
strokeWidth={2}
|
||||
onClick={() => closeFilePreview()}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Preview Thumbnail*/}
|
||||
<div className="my-4 border border-panel-border-light dark:border-panel-border-dark">
|
||||
<div className="flex h-56 w-full items-center 2xl:h-72">
|
||||
<PreviewFile mimeType={mimeType} previewUrl={file.previewUrl} />
|
||||
style={{ width }}
|
||||
>
|
||||
{/* Preview Header */}
|
||||
<div className="flex w-full justify-end text-scale-900 transition-colors hover:text-scale-1200">
|
||||
<IconX
|
||||
className="cursor-pointer"
|
||||
size={14}
|
||||
strokeWidth={2}
|
||||
onClick={() => closeFilePreview()}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="w-full space-y-6">
|
||||
{/* Preview Information */}
|
||||
<div className="space-y-1">
|
||||
<h5 className="break-words text-base text-scale-1200">{file.name}</h5>
|
||||
{file.isCorrupted && (
|
||||
<div className="flex items-center space-x-2">
|
||||
<IconAlertCircle size={14} strokeWidth={2} className="text-scale-1100" />
|
||||
{/* Preview Thumbnail*/}
|
||||
<div className="my-4 border border-panel-border-light dark:border-panel-border-dark">
|
||||
<div className="flex h-56 w-full items-center 2xl:h-72">
|
||||
<PreviewFile mimeType={mimeType} previewUrl={file.previewUrl} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="w-full space-y-6">
|
||||
{/* Preview Information */}
|
||||
<div className="space-y-1">
|
||||
<h5 className="break-words text-base text-scale-1200">{file.name}</h5>
|
||||
{file.isCorrupted && (
|
||||
<div className="flex items-center space-x-2">
|
||||
<IconAlertCircle size={14} strokeWidth={2} className="text-scale-1100" />
|
||||
<p className="text-sm text-scale-1100">
|
||||
File is corrupted, please delete and reupload this file again
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{mimeType && (
|
||||
<p className="text-sm text-scale-1100">
|
||||
File is corrupted, please delete and reupload this file again
|
||||
{mimeType}
|
||||
{size && <span> - {size}</span>}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Preview Metadata */}
|
||||
<div className="space-y-2">
|
||||
<div>
|
||||
<label className="mb-1 text-xs text-scale-900">Added on</label>
|
||||
<p className="text-sm text-scale-1100">{createdAt}</p>
|
||||
</div>
|
||||
<div>
|
||||
<label className="mb-1 text-xs text-scale-900">Last modified</label>
|
||||
<p className="text-sm text-scale-1100">{updatedAt}</p>
|
||||
</div>
|
||||
)}
|
||||
{mimeType && (
|
||||
<p className="text-sm text-scale-1100">
|
||||
{mimeType}
|
||||
{size && <span> - {size}</span>}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Preview Metadata */}
|
||||
<div className="space-y-2">
|
||||
<div>
|
||||
<label className="mb-1 text-xs text-scale-900">Added on</label>
|
||||
<p className="text-sm text-scale-1100">{createdAt}</p>
|
||||
</div>
|
||||
<div>
|
||||
<label className="mb-1 text-xs text-scale-900">Last modified</label>
|
||||
<p className="text-sm text-scale-1100">{updatedAt}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex space-x-2 border-b border-panel-border-light pb-4 dark:border-panel-border-dark">
|
||||
<Link href={`${file.previewUrl}&download`}>
|
||||
<a>
|
||||
{/* Actions */}
|
||||
<div className="flex space-x-2 border-b border-panel-border-light pb-4 dark:border-panel-border-dark">
|
||||
<Link href={`${file.previewUrl}&download`}>
|
||||
<a>
|
||||
<Button
|
||||
type="default"
|
||||
icon={<IconDownload size={16} strokeWidth={2} />}
|
||||
disabled={file.isCorrupted}
|
||||
>
|
||||
Download
|
||||
</Button>
|
||||
</a>
|
||||
</Link>
|
||||
{selectedBucket.public ? (
|
||||
<Button
|
||||
type="default"
|
||||
icon={<IconDownload size={16} strokeWidth={2} />}
|
||||
type="outline"
|
||||
icon={<IconClipboard size={16} strokeWidth={2} />}
|
||||
onClick={async () => await copyFileURLToClipboard(file)}
|
||||
disabled={file.isCorrupted}
|
||||
>
|
||||
Download
|
||||
Get URL
|
||||
</Button>
|
||||
</a>
|
||||
</Link>
|
||||
) : (
|
||||
<Dropdown
|
||||
side="bottom"
|
||||
align="center"
|
||||
overlay={[
|
||||
<Dropdown.Item
|
||||
onClick={async () =>
|
||||
await copyFileURLToClipboard(file, URL_EXPIRY_DURATION.WEEK)
|
||||
}
|
||||
>
|
||||
Expire in 1 week
|
||||
</Dropdown.Item>,
|
||||
<Dropdown.Item
|
||||
onClick={async () =>
|
||||
await copyFileURLToClipboard(file, URL_EXPIRY_DURATION.MONTH)
|
||||
}
|
||||
>
|
||||
Expire in 1 month
|
||||
</Dropdown.Item>,
|
||||
<Dropdown.Item
|
||||
onClick={async () =>
|
||||
await copyFileURLToClipboard(file, URL_EXPIRY_DURATION.YEAR)
|
||||
}
|
||||
>
|
||||
Expire in 1 year
|
||||
</Dropdown.Item>,
|
||||
<Dropdown.Item onClick={() => setSelectedFileCustomExpiry(file)}>
|
||||
Custom expiry
|
||||
</Dropdown.Item>,
|
||||
]}
|
||||
>
|
||||
<Button
|
||||
type="outline"
|
||||
icon={<IconClipboard size={16} strokeWidth={2} />}
|
||||
iconRight={<IconChevronDown />}
|
||||
disabled={file.isCorrupted}
|
||||
>
|
||||
Get URL
|
||||
</Button>
|
||||
</Dropdown>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
type="outline"
|
||||
icon={<IconClipboard size={16} strokeWidth={2} />}
|
||||
onClick={async () => await copyFileURLToClipboard(file)}
|
||||
disabled={file.isCorrupted}
|
||||
shadow={false}
|
||||
size="tiny"
|
||||
icon={<IconTrash2 size={16} strokeWidth={2} />}
|
||||
onClick={() => setSelectedItemsToDelete([file])}
|
||||
>
|
||||
Copy URL
|
||||
Delete file
|
||||
</Button>
|
||||
</div>
|
||||
<Button
|
||||
type="outline"
|
||||
shadow={false}
|
||||
size="tiny"
|
||||
icon={<IconTrash2 size={16} strokeWidth={2} />}
|
||||
onClick={() => setSelectedItemsToDelete([file])}
|
||||
>
|
||||
Delete file
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</Transition>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useEffect, useState, useRef } from 'react'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { compact, find, isEmpty, uniqBy, get } from 'lodash'
|
||||
import { compact, isEmpty, uniqBy, get } from 'lodash'
|
||||
|
||||
import { useStorageStore } from 'localStores/storageExplorer/StorageExplorerStore'
|
||||
import { STORAGE_ROW_TYPES } from '../Storage.constants'
|
||||
@@ -11,47 +11,38 @@ import FileExplorerHeaderSelection from './FileExplorerHeaderSelection'
|
||||
import ConfirmDeleteModal from './ConfirmDeleteModal'
|
||||
import MoveItemsModal from './MoveItemsModal'
|
||||
import PreviewPane from './PreviewPane'
|
||||
import CustomExpiryModal from './CustomExpiryModal'
|
||||
|
||||
const StorageExplorer = observer(({ bucket }) => {
|
||||
const storageExplorerStore = useStorageStore()
|
||||
const {
|
||||
columns,
|
||||
selectedFilePreview,
|
||||
setFilePreview,
|
||||
closeFilePreview,
|
||||
selectedItems,
|
||||
setSelectedItems,
|
||||
clearSelectedItems,
|
||||
selectedItemsToDelete,
|
||||
setSelectedItemsToDelete,
|
||||
clearSelectedItemsToDelete,
|
||||
openedFolders,
|
||||
pushOpenedFolderAtIndex,
|
||||
popColumnAtIndex,
|
||||
popOpenedFoldersAtIndex,
|
||||
setSelectedItemToRename,
|
||||
selectedItemsToMove,
|
||||
setSelectedItemsToMove,
|
||||
clearSelectedItemsToMove,
|
||||
view,
|
||||
setView,
|
||||
setSortBy,
|
||||
setSortByOrder,
|
||||
currentBucketName,
|
||||
copyFileURLToClipboard,
|
||||
openBucket,
|
||||
|
||||
loadExplorerPreferences,
|
||||
addNewFolderPlaceholder,
|
||||
addNewFolder,
|
||||
fetchFolderContents,
|
||||
fetchMoreFolderContents,
|
||||
renameFolder,
|
||||
deleteFolder,
|
||||
uploadFiles,
|
||||
deleteFiles,
|
||||
downloadFile,
|
||||
renameFile,
|
||||
moveFiles,
|
||||
} = storageExplorerStore
|
||||
|
||||
@@ -100,35 +91,9 @@ const StorageExplorer = observer(({ bucket }) => {
|
||||
openBucket(bucket)
|
||||
}, [bucket])
|
||||
|
||||
/** Navigation methods */
|
||||
|
||||
const onSelectFile = async (columnIndex, file) => {
|
||||
popColumnAtIndex(columnIndex)
|
||||
popOpenedFoldersAtIndex(columnIndex - 1)
|
||||
setFilePreview(file)
|
||||
clearSelectedItems()
|
||||
}
|
||||
|
||||
const onSelectFolder = async (columnIndex, folder) => {
|
||||
closeFilePreview()
|
||||
clearSelectedItems(columnIndex + 1)
|
||||
popOpenedFoldersAtIndex(columnIndex - 1)
|
||||
pushOpenedFolderAtIndex(folder, columnIndex)
|
||||
await fetchFolderContents(folder.id, folder.name, columnIndex)
|
||||
}
|
||||
|
||||
/** Checkbox selection methods */
|
||||
/** [Joshen] We'll only support checkbox selection for files ONLY */
|
||||
|
||||
const onCheckItem = (item) => {
|
||||
if (find(selectedItems, item) === undefined) {
|
||||
setSelectedItems(selectedItems.concat([item]))
|
||||
} else {
|
||||
setSelectedItems(selectedItems.filter((selectedItem) => item.id !== selectedItem.id))
|
||||
}
|
||||
closeFilePreview()
|
||||
}
|
||||
|
||||
const onSelectAllItemsInColumn = (columnIndex) => {
|
||||
const columnFiles = columns[columnIndex].items
|
||||
.filter((item) => item.type === STORAGE_ROW_TYPES.FILE)
|
||||
@@ -155,38 +120,6 @@ const StorageExplorer = observer(({ bucket }) => {
|
||||
addNewFolderPlaceholder(columnIndex)
|
||||
}
|
||||
|
||||
const onCreateFolder = (folderName, columnIndex) => {
|
||||
addNewFolder(folderName, columnIndex)
|
||||
}
|
||||
|
||||
const onRenameFolder = (folder, newName, columnIndex) => {
|
||||
renameFolder(folder, newName, columnIndex)
|
||||
}
|
||||
|
||||
const onSelectItemDelete = (file) => {
|
||||
setSelectedItemsToDelete([file])
|
||||
}
|
||||
|
||||
const onSelectItemRename = (file) => {
|
||||
setSelectedItemToRename(file)
|
||||
}
|
||||
|
||||
const onSelectItemMove = (file) => {
|
||||
setSelectedItemsToMove([file])
|
||||
}
|
||||
|
||||
const onCopyFileURL = async (file) => {
|
||||
await copyFileURLToClipboard(file)
|
||||
}
|
||||
|
||||
const onDownloadFile = async (file) => {
|
||||
await downloadFile(file)
|
||||
}
|
||||
|
||||
const onRenameFile = async (file, newName, columnIndex) => {
|
||||
await renameFile(file, newName, columnIndex)
|
||||
}
|
||||
|
||||
const onFilesUpload = async (event, columnIndex = -1) => {
|
||||
event.persist()
|
||||
const items = event.target.files || event.dataTransfer.items
|
||||
@@ -255,18 +188,7 @@ const StorageExplorer = observer(({ bucket }) => {
|
||||
openedFolders={openedFolders}
|
||||
selectedItems={selectedItems}
|
||||
selectedFilePreview={selectedFilePreview}
|
||||
onCheckItem={onCheckItem}
|
||||
onSelectFile={onSelectFile}
|
||||
onRenameFile={onRenameFile}
|
||||
onFilesUpload={onFilesUpload}
|
||||
onCopyFileURL={onCopyFileURL}
|
||||
onDownloadFile={onDownloadFile}
|
||||
onSelectFolder={onSelectFolder}
|
||||
onRenameFolder={onRenameFolder}
|
||||
onCreateFolder={onCreateFolder}
|
||||
onSelectItemDelete={onSelectItemDelete}
|
||||
onSelectItemRename={onSelectItemRename}
|
||||
onSelectItemMove={onSelectItemMove}
|
||||
onSelectAllItemsInColumn={onSelectAllItemsInColumn}
|
||||
onSelectColumnEmptySpace={onSelectColumnEmptySpace}
|
||||
onSelectCreateFolder={onSelectCreateFolder}
|
||||
@@ -292,6 +214,7 @@ const StorageExplorer = observer(({ bucket }) => {
|
||||
onSelectCancel={clearSelectedItemsToMove}
|
||||
onSelectMove={onMoveSelectedFiles}
|
||||
/>
|
||||
<CustomExpiryModal />
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
@@ -37,7 +37,7 @@ export const useStorageExplorerStore = () => {
|
||||
const CORRUPTED_THRESHOLD_MS = 15 * 60 * 1000 // 15 minutes
|
||||
const LIMIT = 200
|
||||
const OFFSET = 0
|
||||
const DEFAULT_EXPIRY = 10 * 365 * 24 * 60 * 60 // in seconds, default to 1 year
|
||||
const DEFAULT_EXPIRY = 10 * 365 * 24 * 60 * 60 // in seconds, default to 10 years
|
||||
const PREVIEW_SIZE_LIMIT = 10000000 // 10MB
|
||||
const BATCH_SIZE = 2
|
||||
const EMPTY_FOLDER_PLACEHOLDER_FILE_NAME = '.emptyFolderPlaceholder'
|
||||
@@ -58,6 +58,7 @@ class StorageExplorerStore {
|
||||
selectedItemsToDelete = []
|
||||
selectedItemsToMove = []
|
||||
selectedFilePreview = {}
|
||||
selectedFileCustomExpiry = undefined
|
||||
|
||||
DEFAULT_OPTIONS = {
|
||||
limit: LIMIT,
|
||||
@@ -300,6 +301,10 @@ class StorageExplorerStore {
|
||||
this.selectedItemsToMove = []
|
||||
}
|
||||
|
||||
setSelectedFileCustomExpiry = (item) => {
|
||||
this.selectedFileCustomExpiry = item
|
||||
}
|
||||
|
||||
addNewFolderPlaceholder = (columnIndex) => {
|
||||
const isPrepend = true
|
||||
const folderName = 'Untitled folder'
|
||||
@@ -397,9 +402,9 @@ class StorageExplorerStore {
|
||||
this.selectedFilePreview = {}
|
||||
}
|
||||
|
||||
copyFileURLToClipboard = async (file) => {
|
||||
copyFileURLToClipboard = async (file, expiresIn = 0) => {
|
||||
const filePreview = find(this.filePreviewCache, { id: file.id })
|
||||
if (filePreview) {
|
||||
if (filePreview && expiresIn === 0) {
|
||||
// Already generated signed URL
|
||||
copyToClipboard(filePreview.url, () => {
|
||||
this.ui.setNotification({
|
||||
@@ -410,7 +415,7 @@ class StorageExplorerStore {
|
||||
})
|
||||
} else {
|
||||
// Need to generate signed URL, and might as well save it to cache as well
|
||||
const signedUrl = await this.fetchFilePreview(file.name)
|
||||
const signedUrl = await this.fetchFilePreview(file.name, expiresIn)
|
||||
|
||||
try {
|
||||
let formattedUrl = new URL(signedUrl)
|
||||
@@ -840,7 +845,7 @@ class StorageExplorerStore {
|
||||
this.clearSelectedItemsToMove()
|
||||
}
|
||||
|
||||
fetchFilePreview = async (fileName) => {
|
||||
fetchFilePreview = async (fileName, expiresIn = 0) => {
|
||||
const includeBucket = false
|
||||
const pathToFile = this.getPathAlongOpenedFolders(includeBucket)
|
||||
const formattedPathToFile = pathToFile.length > 0 ? `${pathToFile}/${fileName}` : fileName
|
||||
@@ -857,7 +862,7 @@ class StorageExplorerStore {
|
||||
|
||||
const { data, error } = await this.supabaseClient.storage
|
||||
.from(this.selectedBucket.name)
|
||||
.createSignedUrl(formattedPathToFile, DEFAULT_EXPIRY)
|
||||
.createSignedUrl(formattedPathToFile, expiresIn || DEFAULT_EXPIRY)
|
||||
|
||||
if (!error) {
|
||||
return data.signedUrl
|
||||
|
||||
@@ -4,6 +4,9 @@
|
||||
.react-contexify__item__content {
|
||||
@apply dark:text-white text-sm;
|
||||
}
|
||||
.react-contexify__submenu {
|
||||
@apply dark:bg-gray-300 border;
|
||||
}
|
||||
}
|
||||
.react-contexify__item:not(.react-contexify__item--disabled):hover
|
||||
> .react-contexify__item__content,
|
||||
|
||||
Reference in new issue
Block a user