diff --git a/apps/studio/components/interfaces/Storage/CreateBucketModal.tsx b/apps/studio/components/interfaces/Storage/CreateBucketModal.tsx index 28386df8cf8..b68d4481452 100644 --- a/apps/studio/components/interfaces/Storage/CreateBucketModal.tsx +++ b/apps/studio/components/interfaces/Storage/CreateBucketModal.tsx @@ -44,6 +44,7 @@ import { import { inverseValidBucketNameRegex, validBucketNameRegex } from './CreateBucketModal.utils' import { getVersioningPlanLimits, + setMockBucketProtection, useIsStorageProtectionEnabled, type VersioningPlanLimits, } from './StorageProtection.constants' @@ -169,6 +170,20 @@ export const CreateBucketModal = ({ open, onOpenChange }: CreateBucketModalProps }) track('storage_bucket_created', { bucketType: 'STANDARD' }) + // [Prototype] Object versioning has no platform API yet — persist it to + // the in-memory mock store so the buckets list reflects it right away. + setMockBucketProtection(values.name, { + versioning: values.enable_versioning ? 'enabled' : 'disabled', + versionExpiryDays: + values.enable_versioning && typeof values.version_expiry_days === 'number' + ? values.version_expiry_days + : null, + maxNoncurrentVersions: + values.enable_versioning && typeof values.max_noncurrent_versions === 'number' + ? values.max_noncurrent_versions + : null, + }) + toast.success(`Successfully created bucket ${values.name}`) form.reset() setSelectedUnit(StorageSizeUnits.MB) diff --git a/apps/studio/components/interfaces/Storage/EditBucketModal.tsx b/apps/studio/components/interfaces/Storage/EditBucketModal.tsx index 7668f53ee32..5fe89daa6bf 100644 --- a/apps/studio/components/interfaces/Storage/EditBucketModal.tsx +++ b/apps/studio/components/interfaces/Storage/EditBucketModal.tsx @@ -36,6 +36,7 @@ import { import { getMockBucketProtection, getVersioningPlanLimits, + setMockBucketProtection, useIsStorageProtectionEnabled, type VersioningPlanLimits, } from '@/components/interfaces/Storage/StorageProtection.constants' @@ -188,6 +189,20 @@ export const EditBucketModal = ({ visible, bucket, onClose }: EditBucketModalPro } } + // [Prototype] Object versioning has no platform API yet — persist it to + // the in-memory mock store so the buckets list reflects it right away. + setMockBucketProtection(bucket.id, { + versioning: values.enable_versioning ? 'enabled' : 'disabled', + versionExpiryDays: + values.enable_versioning && typeof values.version_expiry_days === 'number' + ? values.version_expiry_days + : null, + maxNoncurrentVersions: + values.enable_versioning && typeof values.max_noncurrent_versions === 'number' + ? values.max_noncurrent_versions + : null, + }) + updateBucket({ projectRef: ref, id: bucket.id, diff --git a/apps/studio/components/interfaces/Storage/StorageExplorer/DeletedFilePreviewPane.tsx b/apps/studio/components/interfaces/Storage/StorageExplorer/DeletedFilePreviewPane.tsx index 11f3b91698d..5529bc51b6b 100644 --- a/apps/studio/components/interfaces/Storage/StorageExplorer/DeletedFilePreviewPane.tsx +++ b/apps/studio/components/interfaces/Storage/StorageExplorer/DeletedFilePreviewPane.tsx @@ -120,7 +120,7 @@ export const DeletedFilePreviewPane = () => { Restore } disabled={!canUpdateFiles || file.heldBySnapshot} onClick={() => setShowDeleteConfirm(true)} diff --git a/apps/studio/components/interfaces/Storage/StorageExplorer/DeletedFilesHeaderSelection.tsx b/apps/studio/components/interfaces/Storage/StorageExplorer/DeletedFilesHeaderSelection.tsx index d71d19db21d..8c3444dd7e4 100644 --- a/apps/studio/components/interfaces/Storage/StorageExplorer/DeletedFilesHeaderSelection.tsx +++ b/apps/studio/components/interfaces/Storage/StorageExplorer/DeletedFilesHeaderSelection.tsx @@ -102,7 +102,7 @@ export const DeletedFilesHeaderSelection = () => { } disabled={!canUpdateFiles || isEveryItemHeld} diff --git a/apps/studio/components/interfaces/Storage/StorageProtection.constants.ts b/apps/studio/components/interfaces/Storage/StorageProtection.constants.ts index 56bbff4b9e0..d3e926cdb3b 100644 --- a/apps/studio/components/interfaces/Storage/StorageProtection.constants.ts +++ b/apps/studio/components/interfaces/Storage/StorageProtection.constants.ts @@ -94,5 +94,15 @@ const DEFAULT_PROTECTION: BucketProtection = { export const getMockBucketProtection = (bucketName: string | undefined): BucketProtection => (bucketName && PROTECTED_BUCKETS[bucketName]) || DEFAULT_PROTECTION +/** + * Writes a bucket's versioning settings into the in-memory mock store, so + * changes made via the create/edit bucket modals are reflected immediately + * elsewhere (e.g. the buckets list "Versioning" column) without a real API. + * Resets on page refresh, since it's plain module state. + */ +export const setMockBucketProtection = (bucketName: string, protection: BucketProtection): void => { + PROTECTED_BUCKETS[bucketName] = protection +} + export const isBucketVersioned = (bucketName: string | undefined) => getMockBucketProtection(bucketName).versioning === 'enabled' diff --git a/apps/studio/components/interfaces/Storage/Trash/Trash.tsx b/apps/studio/components/interfaces/Storage/Trash/Trash.tsx index 4902d9aecd5..2e24f459237 100644 --- a/apps/studio/components/interfaces/Storage/Trash/Trash.tsx +++ b/apps/studio/components/interfaces/Storage/Trash/Trash.tsx @@ -92,7 +92,7 @@ export const Trash = ({ bucketId }: TrashProps) => {

{isSuccess && objects.length > 0 && ( } disabled={objects.length === heldInBucketCount} onClick={() => setShowDeleteAll(true)} diff --git a/apps/studio/components/interfaces/Storage/Trash/TrashList.tsx b/apps/studio/components/interfaces/Storage/Trash/TrashList.tsx index 1b81ed5b75b..65f08732177 100644 --- a/apps/studio/components/interfaces/Storage/Trash/TrashList.tsx +++ b/apps/studio/components/interfaces/Storage/Trash/TrashList.tsx @@ -113,7 +113,7 @@ export const TrashList = ({ Restore onDeleteForever(object)} tooltip={{ diff --git a/apps/studio/components/interfaces/Storage/Trash/TrashSelectionBar.tsx b/apps/studio/components/interfaces/Storage/Trash/TrashSelectionBar.tsx index 54c531bd6b3..daf63fd0863 100644 --- a/apps/studio/components/interfaces/Storage/Trash/TrashSelectionBar.tsx +++ b/apps/studio/components/interfaces/Storage/Trash/TrashSelectionBar.tsx @@ -58,7 +58,7 @@ export const TrashSelectionBar = ({ } disabled={!canUpdateFiles || isEveryItemHeld} diff --git a/apps/studio/data/storage/protection/bucket-trash-query.ts b/apps/studio/data/storage/protection/bucket-trash-query.ts index 1014749d57a..133dc5ff68b 100644 --- a/apps/studio/data/storage/protection/bucket-trash-query.ts +++ b/apps/studio/data/storage/protection/bucket-trash-query.ts @@ -2,7 +2,13 @@ import { useMutation, UseMutationOptions, useQuery, useQueryClient } from '@tans import { toast } from 'sonner' import { storageKeys } from '../keys' -import { getMockTrashObjects, mockDelay, type TrashObject } from './protection-mocks' +import { + deleteMockTrashObjectsPermanently, + getMockTrashObjects, + mockDelay, + restoreMockTrashObjects, + type TrashObject, +} from './protection-mocks' export type BucketTrashVariables = { projectRef?: string @@ -29,7 +35,10 @@ export const useBucketTrashRestoreMutation = ({ }: UseMutationOptions = {}) => { const queryClient = useQueryClient() return useMutation({ - mutationFn: () => mockDelay(undefined, 500), + mutationFn: async (variables) => { + restoreMockTrashObjects(variables.objectIds) + await mockDelay(undefined, 500) + }, async onSuccess(data, variables, context) { await queryClient.invalidateQueries({ queryKey: storageKeys.trash(variables.projectRef, variables.bucketId), @@ -57,7 +66,10 @@ export const useBucketTrashDeleteMutation = ({ }: UseMutationOptions = {}) => { const queryClient = useQueryClient() return useMutation({ - mutationFn: () => mockDelay(undefined, 600), + mutationFn: async (variables) => { + deleteMockTrashObjectsPermanently(variables.objectIds) + await mockDelay(undefined, 600) + }, async onSuccess(data, variables, context) { await queryClient.invalidateQueries({ queryKey: storageKeys.trash(variables.projectRef, variables.bucketId), diff --git a/apps/studio/data/storage/protection/protection-mocks.ts b/apps/studio/data/storage/protection/protection-mocks.ts index b6bd8e40996..07e458d6367 100644 --- a/apps/studio/data/storage/protection/protection-mocks.ts +++ b/apps/studio/data/storage/protection/protection-mocks.ts @@ -6,6 +6,11 @@ * designed and demoed end-to-end. The query and mutation hooks in this folder * are shaped exactly like the real ones so swapping in a real fetcher later is * a localized change. + * + * The trash store below lives in a plain module-level variable (not + * persisted), so restoring/permanently deleting a file during a session is + * visible immediately across the app, and resets the moment the page is + * refreshed. */ export type ObjectVersionAction = 'initial upload' | 'overwrite' | 'restore' @@ -80,7 +85,12 @@ export const getMockObjectVersions = (_objectName: string): ObjectVersion[] => [ }, ] -export const getMockTrashObjects = (_bucketId: string): TrashObject[] => [ +/** + * Mutable in-memory "deleted files" store. Shared across buckets, mirroring + * the previous stateless mock's behavior of returning the same list + * regardless of bucketId — only mutated via restore/delete below. + */ +let trashObjects: TrashObject[] = [ { id: 'trash-1', name: 'round-3/final.png', @@ -111,7 +121,77 @@ export const getMockTrashObjects = (_bucketId: string): TrashObject[] => [ expiresAt: null, heldBySnapshot: true, }, + { + id: 'trash-4', + name: 'avatars/user-42.jpg', + originalPath: 'avatars/', + deletedAt: daysAgo(5, '16:45:00'), + deletedBy: 'mark@acme.co', + size: 2.3 * MB, + expiresAt: daysAhead(25), + heldBySnapshot: false, + }, + { + id: 'trash-5', + name: '2026-06-report.pdf', + originalPath: 'exports/', + deletedAt: daysAgo(7, '10:00:00'), + deletedBy: 'api key ····f21', + size: 512 * KB, + expiresAt: daysAhead(23), + heldBySnapshot: false, + }, + { + id: 'trash-6', + name: 'round-2/preview.webp', + originalPath: 'matches/round-2/thumbnails/', + deletedAt: daysAgo(2, '19:30:00'), + deletedBy: 'jane@acme.co', + size: 96 * KB, + expiresAt: null, + heldBySnapshot: true, + }, + { + id: 'trash-7', + name: 'debug-2026-07-01.log', + originalPath: 'logs/', + deletedAt: daysAgo(14, '03:12:00'), + deletedBy: 'system', + size: 3.4 * MB, + expiresAt: daysAhead(16), + heldBySnapshot: false, + }, + { + id: 'trash-8', + name: 'old-assets.zip', + originalPath: 'archive/', + deletedAt: daysAgo(29, '23:55:00'), + deletedBy: 'jane@acme.co', + size: 18 * MB, + expiresAt: daysAhead(1), + heldBySnapshot: false, + }, ] +export const getMockTrashObjects = (_bucketId: string): TrashObject[] => [...trashObjects] + +/** Removes the given ids from the trash store, simulating a restore. */ +export const restoreMockTrashObjects = (objectIds: string[]): TrashObject[] => { + trashObjects = trashObjects.filter((object) => !objectIds.includes(object.id)) + return [...trashObjects] +} + +/** + * Removes the given ids from the trash store, simulating a permanent delete. + * When `objectIds` is omitted, deletes every object that isn't held by a + * snapshot (mirrors the "Delete all permanently" action). + */ +export const deleteMockTrashObjectsPermanently = (objectIds?: string[]): TrashObject[] => { + trashObjects = objectIds + ? trashObjects.filter((object) => !objectIds.includes(object.id)) + : trashObjects.filter((object) => object.heldBySnapshot) + return [...trashObjects] +} + export const mockDelay = (value: T, ms = 350): Promise => new Promise((resolve) => setTimeout(() => resolve(value), ms))