diff --git a/apps/studio/components/interfaces/Storage/StorageExplorer/ConfirmPurgeModal.tsx b/apps/studio/components/interfaces/Storage/StorageExplorer/ConfirmPurgeModal.tsx index 9c61a5e4575..b6ff6d95b07 100644 --- a/apps/studio/components/interfaces/Storage/StorageExplorer/ConfirmPurgeModal.tsx +++ b/apps/studio/components/interfaces/Storage/StorageExplorer/ConfirmPurgeModal.tsx @@ -1,7 +1,7 @@ import { toast } from 'sonner' import { ConfirmationModal } from 'ui-patterns/Dialogs/ConfirmationModal' -import { getPathAlongOpenedFolders } from './StorageExplorer.utils' +import { getStorageItemPath } from './StorageExplorer.utils' import { useStorageExplorerNavigation } from './StorageExplorerNavigation' import { useObjectPurgeMutation } from '@/data/storage/versioning/object-purge-mutation' import { useStorageExplorerStateSnapshot } from '@/state/storage-explorer' @@ -33,11 +33,7 @@ export const ConfirmPurgeModal = () => { if (!projectRef || !selectedBucket?.id || itemToPurge === undefined) return // The endpoint addresses an object by full path; a row only knows its leaf name. - const folderPath = getPathAlongOpenedFolders( - { openedFolders: openedFolders.slice(0, itemToPurge.columnIndex), selectedBucket }, - false - ) - const path = [folderPath, itemToPurge.name].filter(Boolean).join('/') + const path = getStorageItemPath({ openedFolders }, itemToPurge) purgeObject({ projectRef, bucketId: selectedBucket.id, path }) } diff --git a/apps/studio/components/interfaces/Storage/StorageExplorer/FilePreview.test.tsx b/apps/studio/components/interfaces/Storage/StorageExplorer/FilePreview.test.tsx index a1d11f83d93..7de9d0aec00 100644 --- a/apps/studio/components/interfaces/Storage/StorageExplorer/FilePreview.test.tsx +++ b/apps/studio/components/interfaces/Storage/StorageExplorer/FilePreview.test.tsx @@ -2,6 +2,7 @@ import { screen, waitFor } from '@testing-library/react' import { describe, expect, it, vi } from 'vitest' import { FilePreview } from '@/components/interfaces/Storage/StorageExplorer/FilePreview' +import { fileUrlKey } from '@/components/interfaces/Storage/StorageExplorer/useFetchFileUrlQuery' import { customRender as render } from '@/tests/lib/custom-render' import { addAPIMock } from '@/tests/lib/msw' @@ -65,6 +66,36 @@ describe('FilePreview', () => { ) }) + it('keys each version separately, so a new one cannot reuse the cached URL', () => { + const current = fileUrlKey({ + projectRef: 'abcdef', + isBucketPublic: false, + bucketId: 'my-bucket', + path: 'photo.png', + }) + + expect( + fileUrlKey({ + projectRef: 'abcdef', + isBucketPublic: false, + bucketId: 'my-bucket', + path: 'photo.png', + versionId: 'v-new', + }) + ).not.toEqual(current) + + // The version-less key is a prefix of every version's, so one invalidation clears all. + expect( + fileUrlKey({ + projectRef: 'abcdef', + isBucketPublic: false, + bucketId: 'my-bucket', + path: 'photo.png', + versionId: 'v-new', + }).slice(0, current.length) + ).toEqual(current) + }) + it('skips the request for a file too large to preview', async () => { const bodies = mockSignEndpoint() diff --git a/apps/studio/components/interfaces/Storage/StorageExplorer/PreviewPane.tsx b/apps/studio/components/interfaces/Storage/StorageExplorer/PreviewPane.tsx index 181df2a5022..c225ca069a4 100644 --- a/apps/studio/components/interfaces/Storage/StorageExplorer/PreviewPane.tsx +++ b/apps/studio/components/interfaces/Storage/StorageExplorer/PreviewPane.tsx @@ -1,6 +1,6 @@ import { PermissionAction } from '@supabase/shared-types/out/constants' import { useQuery } from '@tanstack/react-query' -import { AlertCircle, Archive, ChevronDown, Copy, Download, Trash2, X } from 'lucide-react' +import { AlertCircle, Archive, ChevronDown, Copy, Download, Trash2, Upload, X } from 'lucide-react' import { parseAsBoolean, useQueryState } from 'nuqs' import { useRef, useState } from 'react' import { toast } from 'sonner' @@ -18,6 +18,7 @@ import { URL_EXPIRY_DURATION } from '../Storage.constants' import { StorageItem } from '../Storage.types' import { getBucketVersioningState } from '../StorageVersioning.constants' import { FilePreview } from './FilePreview' +import { pickFile } from './pickFile' import { PreviewSection } from './PreviewSection' import { getPathAlongOpenedFolders } from './StorageExplorer.utils' import { useStorageExplorerNavigation } from './StorageExplorerNavigation' @@ -65,10 +66,13 @@ interface CurrentFilePreviewProps { size: string | null isPublicBucket: boolean isVersionedBucket: boolean + /** Pins the preview to the current version, so a new one busts the cached URL. */ + currentVersionId?: string hasCurrentVersion: boolean canUpdateFiles: boolean onCopyUrl: (path: string, expiry?: number) => void onDownload: () => void + onReplace: () => void onCustomExpiry: () => void onDelete: () => void onPurge: () => void @@ -81,10 +85,12 @@ const CurrentFilePreview = ({ size, isPublicBucket, isVersionedBucket, + currentVersionId, hasCurrentVersion, canUpdateFiles, onCopyUrl, onDownload, + onReplace, onCustomExpiry, onDelete, onPurge, @@ -94,7 +100,12 @@ const CurrentFilePreview = ({ className="flex items-center justify-center overflow-hidden rounded-md border border-overlay" style={{ height: 'clamp(120px, calc((100vh - 144px) * 0.4), 180px)' }} > - +
@@ -129,6 +140,21 @@ const CurrentFilePreview = ({ tooltip={{ content: { side: 'top', text: 'Download current' } }} /> + {canUpdateFiles && ( + } + onClick={onReplace} + tooltip={{ + content: { + side: 'top', + text: isVersionedBucket ? 'Upload new version' : 'Replace file', + }, + }} + /> + )} + {isPublicBucket ? (