mirror of
https://github.com/supabase/supabase.git
synced 2026-05-06 08:56:46 -04:00
fix: storage copy url include the opened folder for files that are not in it (#43875)
Fixes #42357 Supersedes #42364 ## Problem When copying the URL of a file that is not in the currently opened folder, the folder path is still included in that file URL. ## Solution We actually always have the file path, use it directly instead of rebuilding it from the storage explorer state ## How to test 1. Create a bucket and upload a file in it 2. Create a folder and upload a file in it While the folder is selected, right click the root file uploaded in 1 and copy its URL. It should not contain the folder name. Same with the dropdown menu that appears when hovering the file.
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
import dayjs from 'dayjs'
|
||||
import { Button, Form, Input, Listbox, Modal } from 'ui'
|
||||
|
||||
import { DATETIME_FORMAT } from 'lib/constants'
|
||||
import { useStorageExplorerStateSnapshot } from 'state/storage-explorer'
|
||||
import { Button, Form, Input, Listbox, Modal } from 'ui'
|
||||
|
||||
import { useCopyUrl } from './useCopyUrl'
|
||||
|
||||
const unitMap = {
|
||||
@@ -36,7 +36,7 @@ export const CustomExpiryModal = () => {
|
||||
onSubmit={async (values: any, { setSubmitting }: any) => {
|
||||
setSubmitting(true)
|
||||
await onCopyUrl(
|
||||
selectedFileCustomExpiry!.name,
|
||||
selectedFileCustomExpiry!.path!,
|
||||
values.expiresIn * unitMap[values.units as 'days' | 'weeks' | 'months' | 'years']
|
||||
)
|
||||
setSubmitting(false)
|
||||
|
||||
@@ -202,7 +202,9 @@ export const FileExplorerRow = ({
|
||||
{
|
||||
name: 'Get URL',
|
||||
icon: <Copy size={12} className="text-foreground-light" />,
|
||||
onClick: () => onCopyUrl(itemWithColumnIndex.name),
|
||||
onClick: () => {
|
||||
onCopyUrl(itemWithColumnIndex.path!)
|
||||
},
|
||||
},
|
||||
]
|
||||
: [
|
||||
@@ -213,17 +215,17 @@ export const FileExplorerRow = ({
|
||||
{
|
||||
name: 'Expire in 1 week',
|
||||
onClick: () =>
|
||||
onCopyUrl(itemWithColumnIndex.name, URL_EXPIRY_DURATION.WEEK),
|
||||
onCopyUrl(itemWithColumnIndex.path!, URL_EXPIRY_DURATION.WEEK),
|
||||
},
|
||||
{
|
||||
name: 'Expire in 1 month',
|
||||
onClick: () =>
|
||||
onCopyUrl(itemWithColumnIndex.name, URL_EXPIRY_DURATION.MONTH),
|
||||
onCopyUrl(itemWithColumnIndex.path!, URL_EXPIRY_DURATION.MONTH),
|
||||
},
|
||||
{
|
||||
name: 'Expire in 1 year',
|
||||
onClick: () =>
|
||||
onCopyUrl(itemWithColumnIndex.name, URL_EXPIRY_DURATION.YEAR),
|
||||
onCopyUrl(itemWithColumnIndex.path!, URL_EXPIRY_DURATION.YEAR),
|
||||
},
|
||||
{
|
||||
name: 'Custom expiry',
|
||||
@@ -404,6 +406,7 @@ export const FileExplorerRow = ({
|
||||
<DropdownMenuTrigger>
|
||||
<div className="storage-row-menu opacity-0">
|
||||
<MoreVertical size={16} strokeWidth={2} />
|
||||
<span className="sr-only">{item.name} actions</span>
|
||||
</div>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent side="bottom" align="end">
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
||||
import { ChevronRight, Copy, Download, Edit, Move, Trash2 } from 'lucide-react'
|
||||
import { Item, Menu, Separator, Submenu } from 'react-contexify'
|
||||
|
||||
import 'react-contexify/dist/ReactContexify.css'
|
||||
|
||||
import { useParams } from 'common'
|
||||
import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions'
|
||||
import { useStorageExplorerStateSnapshot } from 'state/storage-explorer'
|
||||
|
||||
import { URL_EXPIRY_DURATION } from '../Storage.constants'
|
||||
import { StorageItemWithColumn } from '../Storage.types'
|
||||
import { useCopyUrl } from './useCopyUrl'
|
||||
@@ -34,7 +36,7 @@ export const ItemContextMenu = ({ id = '' }: ItemContextMenuProps) => {
|
||||
switch (event) {
|
||||
case 'copy':
|
||||
if (expiresIn !== undefined && expiresIn < 0) return setSelectedFileCustomExpiry(item)
|
||||
else return onCopyUrl(item.name, expiresIn)
|
||||
if (item.path) return onCopyUrl(item.path, expiresIn)
|
||||
case 'rename':
|
||||
return setSelectedItemToRename(item)
|
||||
case 'move':
|
||||
|
||||
@@ -212,7 +212,7 @@ export const PreviewPane = () => {
|
||||
<Button
|
||||
type="outline"
|
||||
icon={<Copy />}
|
||||
onClick={() => onCopyUrl(file.name)}
|
||||
onClick={() => onCopyUrl(file.path!)}
|
||||
disabled={file.isCorrupted}
|
||||
>
|
||||
Get URL
|
||||
@@ -232,19 +232,19 @@ export const PreviewPane = () => {
|
||||
<DropdownMenuContent side="bottom" align="center">
|
||||
<DropdownMenuItem
|
||||
key="expires-one-week"
|
||||
onClick={() => onCopyUrl(file.name, URL_EXPIRY_DURATION.WEEK)}
|
||||
onClick={() => onCopyUrl(file.path!, URL_EXPIRY_DURATION.WEEK)}
|
||||
>
|
||||
Expire in 1 week
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
key="expires-one-month"
|
||||
onClick={() => onCopyUrl(file.name, URL_EXPIRY_DURATION.MONTH)}
|
||||
onClick={() => onCopyUrl(file.path!, URL_EXPIRY_DURATION.MONTH)}
|
||||
>
|
||||
Expire in 1 month
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
key="expires-one-year"
|
||||
onClick={() => onCopyUrl(file.name, URL_EXPIRY_DURATION.YEAR)}
|
||||
onClick={() => onCopyUrl(file.path!, URL_EXPIRY_DURATION.YEAR)}
|
||||
>
|
||||
Expire in 1 year
|
||||
</DropdownMenuItem>
|
||||
|
||||
@@ -4,42 +4,32 @@ import { useStorageExplorerStateSnapshot } from 'state/storage-explorer'
|
||||
import { copyToClipboard } from 'ui'
|
||||
|
||||
import { URL_EXPIRY_DURATION } from '../Storage.constants'
|
||||
import { getPathAlongOpenedFolders } from './StorageExplorer.utils'
|
||||
import { fetchFileUrl } from './useFetchFileUrlQuery'
|
||||
import { useProjectApiUrl } from '@/data/config/project-endpoint-query'
|
||||
|
||||
export const useCopyUrl = () => {
|
||||
const { projectRef, selectedBucket, openedFolders } = useStorageExplorerStateSnapshot()
|
||||
const { projectRef, selectedBucket } = useStorageExplorerStateSnapshot()
|
||||
|
||||
const { hostEndpoint, customEndpoint } = useProjectApiUrl({ projectRef })
|
||||
const isCustomDomainActive = !!customEndpoint
|
||||
|
||||
const getFileUrl = useCallback(
|
||||
(fileName: string, expiresIn?: URL_EXPIRY_DURATION) => {
|
||||
const pathToFile = getPathAlongOpenedFolders({ openedFolders, selectedBucket }, false)
|
||||
const formattedPathToFile = [pathToFile, fileName].join('/')
|
||||
|
||||
return fetchFileUrl(
|
||||
formattedPathToFile,
|
||||
projectRef,
|
||||
selectedBucket.id,
|
||||
selectedBucket.public,
|
||||
expiresIn
|
||||
)
|
||||
(filePath: string, expiresIn?: URL_EXPIRY_DURATION) => {
|
||||
return fetchFileUrl(filePath, projectRef, selectedBucket.id, selectedBucket.public, expiresIn)
|
||||
},
|
||||
[projectRef, selectedBucket, openedFolders]
|
||||
[projectRef, selectedBucket]
|
||||
)
|
||||
|
||||
const onCopyUrl = useCallback(
|
||||
(name: string, expiresIn?: URL_EXPIRY_DURATION) => {
|
||||
const formattedUrl = getFileUrl(name, expiresIn).then((url) => {
|
||||
(filePath: string, expiresIn?: URL_EXPIRY_DURATION) => {
|
||||
const formattedUrl = getFileUrl(filePath, expiresIn).then((url) => {
|
||||
return isCustomDomainActive && hostEndpoint
|
||||
? url.replace(hostEndpoint, customEndpoint)
|
||||
: url
|
||||
})
|
||||
|
||||
return copyToClipboard(formattedUrl, () => {
|
||||
toast.success(`Copied URL for ${name} to clipboard.`)
|
||||
toast.success(`Copied URL for ${filePath} to clipboard.`)
|
||||
})
|
||||
},
|
||||
[customEndpoint, getFileUrl, hostEndpoint, isCustomDomainActive]
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
This is a test file for e2e storage testing.
|
||||
It contains some sample content to verify file uploads work correctly.
|
||||
|
||||
@@ -217,12 +217,94 @@ test.describe('Storage', () => {
|
||||
await createBucketViaApi(bucketName, false)
|
||||
await navigateToStorageFiles(page, ref)
|
||||
await navigateToBucket(page, ref, bucketName)
|
||||
|
||||
await createFolder(page, folderName)
|
||||
|
||||
// Rename the folder
|
||||
await renameItem(page, folderName, newFolderName)
|
||||
})
|
||||
|
||||
test('can copy a file url regardless of the opened folders', async ({ page, ref }) => {
|
||||
const bucketName = `${bucketNamePrefix}_urls`
|
||||
const folderName = 'test_folder'
|
||||
const rootFileName = 'test-file.txt'
|
||||
const rootFilePath = path.join(import.meta.dirname, 'files', rootFileName)
|
||||
const folderFileName = 'test-file-2.txt'
|
||||
const folderFilePath = path.join(import.meta.dirname, 'files', folderFileName)
|
||||
|
||||
// Create a bucket via API and navigate to it
|
||||
await deleteBucketViaApi(bucketName)
|
||||
await createBucketViaApi(bucketName, true)
|
||||
await navigateToStorageFiles(page, ref)
|
||||
await navigateToBucket(page, ref, bucketName)
|
||||
await uploadFile(page, rootFilePath, rootFileName)
|
||||
// Create a folder
|
||||
await createFolder(page, folderName)
|
||||
// Wait for the folder file input
|
||||
await expect(page.getByText('Drop your files here')).toBeVisible()
|
||||
await uploadFile(page, folderFilePath, folderFileName)
|
||||
|
||||
// Right-click on the folder file to open context menu
|
||||
const folderFile = page.getByTitle(folderFileName)
|
||||
await folderFile.click({ button: 'right' })
|
||||
await page.getByRole('menuitem', { name: 'Get URL' }).click()
|
||||
await expect(async () => {
|
||||
const copiedUrl = await page.evaluate(() => navigator.clipboard.readText())
|
||||
expect(copiedUrl).toContain(
|
||||
`storage/v1/object/public/${bucketName}/${folderName}/${folderFileName}`
|
||||
)
|
||||
}).toPass({ timeout: 2000 })
|
||||
await expect(page.getByRole('menuitem', { name: 'Get URL' })).not.toBeVisible()
|
||||
|
||||
// Right-click on the root file to open context menu while the folder is still open
|
||||
const rootFile = page.getByTitle(rootFileName)
|
||||
await rootFile.click({ button: 'right' })
|
||||
await page.getByRole('menuitem', { name: 'Get URL' }).click()
|
||||
await expect(async () => {
|
||||
const copiedUrl = await page.evaluate(() => navigator.clipboard.readText())
|
||||
expect(copiedUrl).toContain(`storage/v1/object/public/${bucketName}/${rootFileName}`)
|
||||
}).toPass({ timeout: 2000 })
|
||||
await expect(page.getByRole('menuitem', { name: 'Get URL' })).not.toBeVisible()
|
||||
|
||||
// Click the actions button on the folder file to open dropdown menu
|
||||
await page.getByRole('button', { name: `${folderFileName} actions` }).click()
|
||||
await page.getByRole('menuitem', { name: 'Get URL' }).click()
|
||||
await expect(async () => {
|
||||
const copiedUrl = await page.evaluate(() => navigator.clipboard.readText())
|
||||
expect(copiedUrl).toContain(
|
||||
`storage/v1/object/public/${bucketName}/${folderName}/${folderFileName}`
|
||||
)
|
||||
}).toPass({ timeout: 2000 })
|
||||
await expect(page.getByRole('menuitem', { name: 'Get URL' })).not.toBeVisible()
|
||||
|
||||
// Click the actions button on the root file to open dropdown menu while the folder is still open
|
||||
await page.getByRole('button', { name: `${rootFileName} actions` }).click()
|
||||
await page.getByRole('menuitem', { name: 'Get URL' }).click()
|
||||
await expect(async () => {
|
||||
const copiedUrl = await page.evaluate(() => navigator.clipboard.readText())
|
||||
expect(copiedUrl).toContain(`storage/v1/object/public/${bucketName}/${rootFileName}`)
|
||||
}).toPass({ timeout: 2000 })
|
||||
await expect(page.getByRole('menuitem', { name: 'Get URL' })).not.toBeVisible()
|
||||
|
||||
// Click the folder file to open its preview pane
|
||||
await folderFile.click()
|
||||
await page.getByRole('button', { name: 'Get URL' }).click()
|
||||
await expect(async () => {
|
||||
const copiedUrl = await page.evaluate(() => navigator.clipboard.readText())
|
||||
expect(copiedUrl).toContain(
|
||||
`storage/v1/object/public/${bucketName}/${folderName}/${folderFileName}`
|
||||
)
|
||||
}).toPass({ timeout: 2000 })
|
||||
|
||||
// Click the root file to open its preview pane while folder is still open
|
||||
await rootFile.click()
|
||||
await page.getByRole('button', { name: 'Get URL' }).click()
|
||||
await expect(async () => {
|
||||
const copiedUrl = await page.evaluate(() => navigator.clipboard.readText())
|
||||
expect(copiedUrl).toContain(`storage/v1/object/public/${bucketName}/${rootFileName}`)
|
||||
}).toPass({ timeout: 2000 })
|
||||
})
|
||||
|
||||
test('resets folder name when renaming with empty string', async ({ page, ref }) => {
|
||||
const bucketName = `${bucketNamePrefix}_reset_enter`
|
||||
const folderName = 'folder_to_rename'
|
||||
|
||||
Reference in New Issue
Block a user