mirror of
https://github.com/supabase/supabase.git
synced 2026-10-10 20:05:06 +03:00
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:
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,
|
||||
|
||||
+1
-1
@@ -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)}
|
||||
|
||||
+1
-1
@@ -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))
|
||||
Reference in new issue
Block a user