diff --git a/apps/studio/components/interfaces/Storage/StorageExplorer/StorageExplorer.tsx b/apps/studio/components/interfaces/Storage/StorageExplorer/StorageExplorer.tsx
index 75976839083..9de3e77cfba 100644
--- a/apps/studio/components/interfaces/Storage/StorageExplorer/StorageExplorer.tsx
+++ b/apps/studio/components/interfaces/Storage/StorageExplorer/StorageExplorer.tsx
@@ -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 */
diff --git a/apps/studio/components/layouts/ProjectLayout/ProjectContext.tsx b/apps/studio/components/layouts/ProjectLayout/ProjectContext.tsx
index a796c54102e..461c8814c69 100644
--- a/apps/studio/components/layouts/ProjectLayout/ProjectContext.tsx
+++ b/apps/studio/components/layouts/ProjectLayout/ProjectContext.tsx
@@ -17,13 +17,11 @@ export const ProjectContextProvider = ({
return (
-
-
-
- {children}
-
-
-
+
+
+ {children}
+
+
)
diff --git a/apps/studio/components/layouts/StorageLayout/StorageBucketsLayout.tsx b/apps/studio/components/layouts/StorageLayout/StorageBucketsLayout.tsx
index 1859d7ff578..4a74e00a152 100644
--- a/apps/studio/components/layouts/StorageLayout/StorageBucketsLayout.tsx
+++ b/apps/studio/components/layouts/StorageLayout/StorageBucketsLayout.tsx
@@ -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,
diff --git a/apps/studio/pages/project/[ref]/storage/files/buckets/[bucketId].tsx b/apps/studio/pages/project/[ref]/storage/files/buckets/[bucketId].tsx
index 89d8152eeaf..fdacc798f6e 100644
--- a/apps/studio/pages/project/[ref]/storage/files/buckets/[bucketId].tsx
+++ b/apps/studio/pages/project/[ref]/storage/files/buckets/[bucketId].tsx
@@ -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
+ return
}
return (
- <>
+
{
/>
>
)}
- >
+
)
}
diff --git a/apps/studio/state/storage-explorer.tsx b/apps/studio/state/storage-explorer.tsx
index 807585de246..b72295411dd 100644
--- a/apps/studio/state/storage-explorer.tsx
+++ b/apps/studio/state/storage-explorer.tsx
@@ -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(
@@ -1864,6 +1857,7 @@ const StorageExplorerStateContext = createContext(
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 (
diff --git a/e2e/studio/features/storage.spec.ts b/e2e/studio/features/storage.spec.ts
index b2d371b9de9..c03136bc19d 100644
--- a/e2e/studio/features/storage.spec.ts
+++ b/e2e/studio/features/storage.spec.ts
@@ -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'