Chore/fix storage explorer when switching buckets (#43541)

## Context

Taking a slightly different approach to [this
PR](https://github.com/supabase/supabase/pull/43370)

Original problem was that if you opened some folders while in a bucket
and then switched to a different bucket, the folder UI will persists
(folders from Bucket A will render when landing on Bucket B)

## Changes involved
- Shift `StorageExplorerStateContextProvider` into `[bucketId].tsx]`
instead of `ProjectContext`
- The valtio store here only applies for the storage explorer so having
it so high in the project's context was unnecessary
- This also just implies that the valtio store will automatically reset
whenever the bucket changes
- Simplify storage explorer valtio store by initializing the store with
the bucket
- We'll initialize the selected bucket with the store now (Same as
previous PR)
- Removes unnecessary `setSelectedBucket` method which required a
separate `useEffect` in `StorageExplorer.tsx`

## To test
- [ ] Verify that the original is resolved
- [ ] General smoke test of the storage explorer - i've also re-added
the e2e test that Gildas wrote up in his PR
This commit is contained in:
Joshen Lim authored and GitHub committed 2026-03-09 14:32:34 +07:00
1 parent 4a173eea60
commit 9b0dc8d9b9
6 files changed
+68 -50

No files matched your search

@@ -1,14 +1,13 @@
import { compact, get, isEmpty, uniqBy } from 'lodash'
import { useEffect, useRef, useState } from 'react'
import { useStaticEffectEvent } from '@/hooks/useStaticEffectEvent'
import { useDebounce } from '@uidotdev/usehooks'
import { useParams } from 'common'
import { useProjectStorageConfigQuery } from 'data/config/project-storage-config-query'
import type { Bucket } from 'data/storage/buckets-query'
import { useLatest } from 'hooks/misc/useLatest'
import { IS_PLATFORM } from 'lib/constants'
import { compact, get, isEmpty, uniqBy } from 'lodash'
import { useEffect, useRef, useState } from 'react'
import { useStorageExplorerStateSnapshot } from 'state/storage-explorer'
import { useSelectedBucket } from '../FilesBuckets/useSelectedBucket'
import { STORAGE_ROW_TYPES, STORAGE_VIEWS } from '../Storage.constants'
import { ConfirmDeleteModal } from './ConfirmDeleteModal'
@@ -18,6 +17,7 @@ import { FileExplorerHeader } from './FileExplorerHeader'
import { FileExplorerHeaderSelection } from './FileExplorerHeaderSelection'
import { MoveItemsModal } from './MoveItemsModal'
import { PreviewPane } from './PreviewPane'
import { useStaticEffectEvent } from '@/hooks/useStaticEffectEvent'
export const StorageExplorer = () => {
const { ref, bucketId } = useParams()
@@ -30,7 +30,6 @@ export const StorageExplorer = () => {
openedFolders,
selectedItemsToMove,
selectedBucket,
openBucket,
fetchFolderContents,
fetchMoreFolderContents,
fetchFoldersByPath,
@@ -91,14 +90,10 @@ export const StorageExplorer = () => {
}
}
})
useEffect(() => {
if (bucket && projectRef) fetchContents(bucket)
}, [bucket, projectRef, debouncedSearchString, fetchContents])
const openBucketRef = useLatest(openBucket)
useEffect(() => {
if (bucket && !!projectRef) openBucketRef.current(bucket)
}, [bucket, projectRef, openBucketRef])
}, [bucket, projectRef, debouncedSearchString, selectedBucket.id, fetchContents])
/** Checkbox selection methods */
/** [Joshen] We'll only support checkbox selection for files ONLY */
@@ -17,13 +17,11 @@ export const ProjectContextProvider = ({
return (
<TableEditorStateContextProvider key={`table-editor-state-${projectRef}`}>
<TabsStateContextProvider key={`tabs-state-${projectRef}`}>
<StorageExplorerStateContextProvider key={`storage-explorer-state-${projectRef}`}>
<DatabaseSelectorStateContextProvider key={`database-selector-state-${projectRef}`}>
<RoleImpersonationStateContextProvider key={`role-impersonation-state-${projectRef}`}>
{children}
</RoleImpersonationStateContextProvider>
</DatabaseSelectorStateContextProvider>
</StorageExplorerStateContextProvider>
<DatabaseSelectorStateContextProvider key={`database-selector-state-${projectRef}`}>
<RoleImpersonationStateContextProvider key={`role-impersonation-state-${projectRef}`}>
{children}
</RoleImpersonationStateContextProvider>
</DatabaseSelectorStateContextProvider>
</TabsStateContextProvider>
</TableEditorStateContextProvider>
)
@@ -1,11 +1,10 @@
import Link from 'next/link'
import { PropsWithChildren } from 'react'
import { IS_PLATFORM, useParams } from 'common'
import { BUCKET_TYPES } from 'components/interfaces/Storage/Storage.constants'
import { useStorageV2Page } from 'components/interfaces/Storage/Storage.utils'
import { DocsButton } from 'components/ui/DocsButton'
import Link from 'next/link'
import { usePathname } from 'next/navigation'
import { PropsWithChildren } from 'react'
import { NavMenu, NavMenuItem } from 'ui'
import {
PageHeader,
@@ -1,10 +1,3 @@
import { ChevronDown, FolderOpen, Settings, Shield, Trash2 } from 'lucide-react'
import Link from 'next/link'
import { useRouter } from 'next/router'
import { parseAsBoolean, useQueryState } from 'nuqs'
import { useEffect } from 'react'
import { toast } from 'sonner'
import { useParams } from 'common'
import { DeleteBucketModal } from 'components/interfaces/Storage/DeleteBucketModal'
import { EditBucketModal } from 'components/interfaces/Storage/EditBucketModal'
@@ -16,6 +9,12 @@ import { useBucketPolicyCount } from 'components/interfaces/Storage/useBucketPol
import DefaultLayout from 'components/layouts/DefaultLayout'
import { PageLayout } from 'components/layouts/PageLayout/PageLayout'
import StorageLayout from 'components/layouts/StorageLayout/StorageLayout'
import { ChevronDown, FolderOpen, Settings, Shield, Trash2 } from 'lucide-react'
import Link from 'next/link'
import { useRouter } from 'next/router'
import { parseAsBoolean, useQueryState } from 'nuqs'
import { useEffect } from 'react'
import { toast } from 'sonner'
import type { NextPageWithLayout } from 'types'
import {
Badge,
@@ -27,6 +26,8 @@ import {
DropdownMenuTrigger,
} from 'ui'
import { StorageExplorerStateContextProvider } from '@/state/storage-explorer'
const BucketPage: NextPageWithLayout = () => {
const router = useRouter()
const { bucketId, ref } = useParams()
@@ -57,11 +58,11 @@ const BucketPage: NextPageWithLayout = () => {
}, [isSuccess])
if (isError) {
return <StorageBucketsError error={error as any} />
return <StorageBucketsError error={error} />
}
return (
<>
<StorageExplorerStateContextProvider key={`storage-explorer-state-${ref}`}>
<PageLayout
size="full"
isCompact
@@ -163,7 +164,7 @@ const BucketPage: NextPageWithLayout = () => {
/>
</>
)}
</>
</StorageExplorerStateContextProvider>
)
}
+12 -16
View File
@@ -8,6 +8,7 @@ import * as tus from 'tus-js-client'
import { Button, SONNER_DEFAULT_DURATION, SonnerProgress } from 'ui'
import { proxy, useSnapshot } from 'valtio'
import { useSelectedBucket } from '@/components/interfaces/Storage/FilesBuckets/useSelectedBucket'
import {
STORAGE_BUCKET_SORT,
STORAGE_ROW_STATUS,
@@ -80,11 +81,13 @@ if (typeof window !== 'undefined') {
function createStorageExplorerState({
projectRef,
connectionString,
bucket,
resumableUploadUrl,
clientEndpoint,
}: {
projectRef: string
connectionString: string
bucket?: Bucket
resumableUploadUrl: string
clientEndpoint: string
}) {
@@ -98,6 +101,7 @@ function createStorageExplorerState({
connectionString,
resumableUploadUrl,
uploadProgresses: [] as UploadProgress[],
selectedBucket: bucket as Bucket,
abortApiCalls: () => {
if (abortController) {
@@ -158,14 +162,6 @@ function createStorageExplorerState({
state.selectedItemsToMove = items
},
selectedBucket: {} as Bucket,
setSelectedBucket: (bucket: Bucket) => {
state.selectedBucket = bucket
state.setSelectedFilePreview(undefined)
state.clearOpenedFolders()
state.clearSelectedItems()
},
setSelectedItemToRename: (file: { name: string; columnIndex: number }) => {
state.updateRowStatus({
name: file.name,
@@ -232,10 +228,6 @@ function createStorageExplorerState({
})
},
openBucket: async (bucket: Bucket) => {
state.setSelectedBucket(bucket)
},
// ======== Folders CRUD ========
addNewFolderPlaceholder: (columnIndex: number) => {
@@ -1856,6 +1848,7 @@ const DEFAULT_STATE_CONFIG = {
connectionString: '',
resumableUploadUrl: '',
clientEndpoint: '',
bucket: {} as Bucket,
}
const StorageExplorerStateContext = createContext<StorageExplorerState>(
@@ -1864,6 +1857,7 @@ const StorageExplorerStateContext = createContext<StorageExplorerState>(
export const StorageExplorerStateContextProvider = ({ children }: PropsWithChildren) => {
const { data: project } = useSelectedProjectQuery()
const { data: bucket } = useSelectedBucket()
const isPaused = project?.status === PROJECT_STATUS.INACTIVE
const [state, setState] = useState(() => createStorageExplorerState(DEFAULT_STATE_CONFIG))
@@ -1881,16 +1875,18 @@ export const StorageExplorerStateContextProvider = ({ children }: PropsWithChild
// So the useEffect here is to make sure that the project ref is loaded into the state properly
// Although I'd be keen to re-investigate this to see if we can remove this
useEffect(() => {
const hasDataReady = !!project?.ref
const hasDataReady = !!project && !!bucket
const storeAlreadyLoaded = state.projectRef === project?.ref
if (!isPaused && hasDataReady && !storeAlreadyLoaded && isSuccessSettings) {
const clientEndpoint = storageEndpoint ?? hostEndpoint ?? ''
const resumableUploadUrl = `${clientEndpoint}/storage/v1/upload/resumable`
setState(
createStorageExplorerState({
projectRef: project?.ref ?? '',
projectRef: project.ref,
connectionString: project.connectionString ?? '',
bucket,
resumableUploadUrl,
clientEndpoint,
})
@@ -1898,13 +1894,13 @@ export const StorageExplorerStateContextProvider = ({ children }: PropsWithChild
}
}, [
state.projectRef,
project?.ref,
project?.connectionString,
project,
stateRef,
isPaused,
hostEndpoint,
storageEndpoint,
isSuccessSettings,
bucket,
])
return (
+32 -3
View File
@@ -1,8 +1,7 @@
import { expect } from '@playwright/test'
import path from 'path'
import { expect } from '@playwright/test'
import { env } from '../env.config.js'
import { test } from '../utils/test.js'
import { waitForApiResponse } from '../utils/wait-for-response.js'
import {
createBucket,
createFolder,
@@ -18,6 +17,8 @@ import {
createBucket as createBucketViaApi,
deleteBucket as deleteBucketViaApi,
} from '../utils/storage/index.js'
import { test } from '../utils/test.js'
import { waitForApiResponse } from '../utils/wait-for-response.js'
const bucketNamePrefix = 'pw_bucket'
@@ -291,6 +292,34 @@ test.describe('Storage', () => {
).toBeVisible()
})
test('resets storage view when switching buckets', async ({ page, ref }) => {
const bucketName = `${bucketNamePrefix}_navigation`
const bucketName2 = `${bucketNamePrefix}2_navigation`
const folderName = 'folder_navigation'
const fileName = 'test-file.txt'
// Create 2 bucket via API, navigate to the first
await deleteBucketViaApi(bucketName)
await deleteBucketViaApi(bucketName2)
await createBucketViaApi(bucketName, false)
await createBucketViaApi(bucketName2, false)
await navigateToStorageFiles(page, ref)
await navigateToBucket(page, ref, bucketName)
// create a folder and add a file
await createFolder(page, folderName)
// Open the folder
await page.getByTitle(folderName).click()
const filePath = path.join(import.meta.dirname, 'files', fileName)
await uploadFile(page, filePath, fileName)
// Navigate to bucket list
await page.getByRole('link', { name: 'Files' }).nth(1).click()
// Navigate to the 2nd bucket
await navigateToBucket(page, ref, bucketName2)
await expect(page.getByTitle(fileName)).not.toBeVisible()
})
test('can delete a file', async ({ page, ref }) => {
const bucketName = `${bucketNamePrefix}_delete_file`
const fileName = 'test-file.txt'