mock client-side mutations for bucket versioning + deleted files

- Bucket versioning: create/edit bucket modals now write to an in-memory
  mock store (setMockBucketProtection) on save, so the buckets list
  "Versioning" column reflects the change immediately. Resets on page
  refresh since it's plain module state (no real API yet).
- Deleted files: restore/permanent-delete mutations now actually mutate
  the trash mock store instead of no-op delaying, so the deleted files
  list updates after restoring or hard-deleting items.
- Added 5 more dummy trash entries (8 total) with varied held/expiry
  states.
- Changed all "Delete permanently"/"Delete all permanently" trigger
  buttons to the `danger` Button variant.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TiUEvmC84bRsteqsWXHY2p
This commit is contained in:
Claude committed 2026-08-06 08:12:18 +00:00
1 parent 1514526241
commit fecea6bf06
10 files changed
+141 -9

No files matched your search

@@ -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)
@@ -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,
@@ -120,7 +120,7 @@ export const DeletedFilePreviewPane = () => {
Restore
</ButtonTooltip>
<ButtonTooltip
variant="default"
variant="danger"
icon={<Trash2 size={14} />}
disabled={!canUpdateFiles || file.heldBySnapshot}
onClick={() => setShowDeleteConfirm(true)}
@@ -102,7 +102,7 @@ export const DeletedFilesHeaderSelection = () => {
</ButtonTooltip>
<ButtonTooltip
variant="default"
variant="danger"
size="tiny"
icon={<Trash2 size={12} />}
disabled={!canUpdateFiles || isEveryItemHeld}
@@ -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'
@@ -92,7 +92,7 @@ export const Trash = ({ bucketId }: TrashProps) => {
</p>
{isSuccess && objects.length > 0 && (
<ButtonTooltip
variant="default"
variant="danger"
icon={<Trash2 />}
disabled={objects.length === heldInBucketCount}
onClick={() => setShowDeleteAll(true)}
@@ -113,7 +113,7 @@ export const TrashList = ({
Restore
</Button>
<ButtonTooltip
variant="outline"
variant="danger"
disabled={object.heldBySnapshot}
onClick={() => onDeleteForever(object)}
tooltip={{
@@ -58,7 +58,7 @@ export const TrashSelectionBar = ({
</ButtonTooltip>
<ButtonTooltip
variant="default"
variant="danger"
size="tiny"
icon={<Trash2 size={12} />}
disabled={!canUpdateFiles || isEveryItemHeld}
@@ -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<void, Error, TrashRestoreVariables> = {}) => {
const queryClient = useQueryClient()
return useMutation<void, Error, TrashRestoreVariables>({
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<void, Error, TrashDeleteVariables> = {}) => {
const queryClient = useQueryClient()
return useMutation<void, Error, TrashDeleteVariables>({
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),
@@ -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 = <T>(value: T, ms = 350): Promise<T> =>
new Promise((resolve) => setTimeout(() => resolve(value), ms))