feat(storage): drive version history off the real endpoints

The preview panel now addresses versions by the object's full path, which
is what the list, move and delete endpoints take, rather than by the leaf
name the explorer renders. `VersionHistory` keeps `objectName` for copy
and takes `path` separately.

Expiry countdowns read the bucket's stored lifecycle policy instead of an
empty placeholder, so a row's fate reflects the policy that governs it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N3euXsz25sTybsGTcxCFfn
This commit is contained in:
Claude authored and GitHub committed 2026-10-08 18:37:02 +02:00
1 parent 0cd08c6b1a
commit 0b8ef6dad2
2 files changed
+35 -18

No files matched your search

@@ -24,6 +24,7 @@ import {
} from 'ui'
import { ConfirmationModal } from 'ui-patterns/Dialogs/ConfirmationModal'
import { fromLifecycleRules } from '../BucketVersioningFields.lifecycle'
import { URL_EXPIRY_DURATION } from '../Storage.constants'
import { StorageItem } from '../Storage.types'
import { getBucketVersioningState } from '../StorageVersioning.constants'
@@ -36,6 +37,7 @@ import { VersionCompareWidget } from './VersionCompareWidget'
import { VersionHistory } from './VersionHistory'
import { useIsStorageVersioningEnabled } from '@/components/interfaces/App/FeaturePreview/FeaturePreviewContext'
import { ButtonTooltip } from '@/components/ui/ButtonTooltip'
import { bucketLifecycleQueryOptions } from '@/data/storage/bucket-lifecycle-query'
import { useObjectPurgeMutation } from '@/data/storage/versioning/object-purge-mutation'
import { useObjectVersionRestoreMutation } from '@/data/storage/versioning/object-version-restore-mutation'
import {
@@ -52,12 +54,6 @@ const PREVIEW_SIZE_LIMIT = 10 * 1024 * 1024 // 10MB
const PANEL_WIDTH = 450
/**
* TODO(storage-versioning): read the bucket's stored policy once the Storage API
* returns it. No condition is set until then, so no row shows an expiry.
*/
const EMPTY_LIFECYCLE_POLICY: LifecyclePolicy = { expiryDays: null, maxVersions: null }
const PreviewFile = ({ item }: { item: StorageItem }) => {
const { projectRef, selectedBucket, openedFolders } = useStorageExplorerStateSnapshot()
const folderPath = getPathAlongOpenedFolders({ openedFolders, selectedBucket }, false)
@@ -338,6 +334,7 @@ export const PreviewPane = () => {
projectRef,
selectedBucket,
selectedFilePreview: file,
openedFolders,
setSelectedItemsToDelete,
setSelectedFileCustomExpiry,
downloadFile,
@@ -359,14 +356,29 @@ export const PreviewPane = () => {
const versioningState = getBucketVersioningState(selectedBucket)
// The version endpoints address an object by its full path in the bucket, not
// by the leaf name the explorer renders.
const folderPath = getPathAlongOpenedFolders({ openedFolders, selectedBucket }, false)
const filePath = file ? [folderPath, file.name].filter(Boolean).join('/') : undefined
const { data: lifecycle } = useQuery({
...bucketLifecycleQueryOptions({ projectRef, bucketId: selectedBucket?.id }),
enabled: isStorageVersioningEnabled && !!projectRef && !!selectedBucket?.id,
})
const storedPolicy = fromLifecycleRules(lifecycle)
const lifecyclePolicy: LifecyclePolicy = {
expiryDays: storedPolicy.versionExpiryDays,
maxVersions: storedPolicy.maxNoncurrentVersions,
}
const { data: versions } = useQuery({
...objectVersionsQueryOptions({
projectRef,
bucketId: selectedBucket?.id,
objectName: file?.name,
lifecyclePolicy: EMPTY_LIFECYCLE_POLICY,
path: filePath,
lifecyclePolicy,
}),
enabled: isStorageVersioningEnabled && !!projectRef && !!selectedBucket?.id && !!file?.name,
enabled: isStorageVersioningEnabled && !!projectRef && !!selectedBucket?.id && !!filePath,
})
const { mutate: restoreVersion, isPending: isRestoring } = useObjectVersionRestoreMutation({
@@ -395,18 +407,18 @@ export const PreviewPane = () => {
const isComparing = previewedVersion !== undefined && !previewedVersion.isCurrent
const handleRestore = () => {
if (!projectRef || !selectedBucket?.id || !previewedVersion) return
if (!projectRef || !selectedBucket?.id || !previewedVersion || !filePath) return
restoreVersion({
projectRef,
bucketId: selectedBucket.id,
objectName: file.name,
path: filePath,
versionId: previewedVersion.versionId,
})
}
const handlePurge = () => {
if (!projectRef || !selectedBucket?.id) return
purgeObject({ projectRef, bucketId: selectedBucket.id, objectName: file.name })
if (!projectRef || !selectedBucket?.id || !filePath) return
purgeObject({ projectRef, bucketId: selectedBucket.id, path: filePath })
}
// The compare widget replaces the top of the panel, so scroll up to show it.
@@ -469,9 +481,10 @@ export const PreviewPane = () => {
projectRef={projectRef}
bucketId={selectedBucket?.id}
objectName={file.name}
path={filePath ?? file.name}
versioningState={versioningState}
lifecyclePolicy={EMPTY_LIFECYCLE_POLICY}
expirationMode="and"
lifecyclePolicy={lifecyclePolicy}
expirationMode={storedPolicy.expirationMode}
mimeType={mimeType}
previewedVersionId={previewedVersion?.versionId}
onPreview={handlePreviewVersion}
@@ -127,7 +127,10 @@ const VersionActionsMenu = ({
interface VersionHistoryProps {
projectRef?: string
bucketId?: string
/** Leaf name, for copy. */
objectName: string
/** Full path within the bucket, which is what the version endpoints address. */
path: string
versioningState: BucketVersioningState
lifecyclePolicy: LifecyclePolicy
expirationMode: ExpirationMode
@@ -142,6 +145,7 @@ export const VersionHistory = ({
projectRef,
bucketId,
objectName,
path,
versioningState,
lifecyclePolicy,
expirationMode,
@@ -157,7 +161,7 @@ export const VersionHistory = ({
isError,
error,
isSuccess,
} = useQuery(objectVersionsQueryOptions({ projectRef, bucketId, objectName, lifecyclePolicy }))
} = useQuery(objectVersionsQueryOptions({ projectRef, bucketId, path, lifecyclePolicy }))
const [versionToDelete, setVersionToDelete] = useState<ObjectVersion>()
@@ -197,7 +201,7 @@ export const VersionHistory = ({
const handleRestore = (version: ObjectVersion) => {
if (!projectRef || !bucketId) return
restoreVersion({ projectRef, bucketId, objectName, versionId: version.versionId })
restoreVersion({ projectRef, bucketId, path, versionId: version.versionId })
}
if (isPending) return <GenericSkeletonLoader />
@@ -325,7 +329,7 @@ export const VersionHistory = ({
deleteVersion({
projectRef,
bucketId,
objectName,
path,
versionId: versionToDelete.versionId,
})
}}