From 9ce3bdf55289412abe63c07bb580125082dea88b Mon Sep 17 00:00:00 2001 From: Gildas Garcia <1122076+djhi@users.noreply.github.com> Date: Thu, 5 Mar 2026 12:10:46 +0100 Subject: [PATCH] fix: storage view is not reset when switching buckets (#43370) ## Problem When navigating between buckets after having selected a folder, the storage view shows the previous folder content. ## Solution Set the selected bucket before navigating to it, preventing a race condition with Valtio updates and the effect that loads the bucket content. ## How to test 1. Create 2 buckets 2. In one of them, create a folder and upload some files 3. Go back to the bucket list 4. Open the other bucket Results: - It should not show the previous bucket content anymore - It should not show an empty 2nd column either --------- Co-authored-by: Joshen Lim --- .../StorageExplorer/StorageExplorer.tsx | 10 +++--- apps/studio/state/storage-explorer.tsx | 19 ++++++++-- e2e/studio/features/storage.spec.ts | 35 +++++++++++++++++-- 3 files changed, 54 insertions(+), 10 deletions(-) diff --git a/apps/studio/components/interfaces/Storage/StorageExplorer/StorageExplorer.tsx b/apps/studio/components/interfaces/Storage/StorageExplorer/StorageExplorer.tsx index 75976839083..155586b9e90 100644 --- a/apps/studio/components/interfaces/Storage/StorageExplorer/StorageExplorer.tsx +++ b/apps/studio/components/interfaces/Storage/StorageExplorer/StorageExplorer.tsx @@ -3,7 +3,6 @@ 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' @@ -20,9 +19,9 @@ import { MoveItemsModal } from './MoveItemsModal' import { PreviewPane } from './PreviewPane' export const StorageExplorer = () => { - const { ref, bucketId } = useParams() const storageExplorerRef = useRef(null) const { + bucketId, projectRef, view, columns, @@ -44,7 +43,7 @@ export const StorageExplorer = () => { setSelectedItemsToMove, } = useStorageExplorerStateSnapshot() - useProjectStorageConfigQuery({ projectRef: ref }, { enabled: IS_PLATFORM }) + useProjectStorageConfigQuery({ projectRef }, { enabled: IS_PLATFORM }) const { data: bucket, isLoading: isBucketQueryLoading } = useSelectedBucket() // Detect when transitioning between buckets to avoid showing stale content from the previous bucket. @@ -91,14 +90,15 @@ export const StorageExplorer = () => { } } }) + useEffect(() => { if (bucket && projectRef) fetchContents(bucket) - }, [bucket, projectRef, debouncedSearchString, fetchContents]) + }, [bucketId, bucket, projectRef, debouncedSearchString, fetchContents]) const openBucketRef = useLatest(openBucket) useEffect(() => { if (bucket && !!projectRef) openBucketRef.current(bucket) - }, [bucket, projectRef, openBucketRef]) + }, [bucketId, bucket, projectRef, openBucketRef]) /** Checkbox selection methods */ /** [Joshen] We'll only support checkbox selection for files ONLY */ diff --git a/apps/studio/state/storage-explorer.tsx b/apps/studio/state/storage-explorer.tsx index 807585de246..1060054cff2 100644 --- a/apps/studio/state/storage-explorer.tsx +++ b/apps/studio/state/storage-explorer.tsx @@ -1,7 +1,7 @@ import { BlobReader, BlobWriter, ZipWriter } from '@zip.js/zip.js' import { IS_PLATFORM, LOCAL_STORAGE_KEYS } from 'common' import { capitalize, chunk, compact, find, findIndex, has, isObject, uniq, uniqBy } from 'lodash' -import { createContext, PropsWithChildren, useContext, useEffect, useState } from 'react' +import { createContext, PropsWithChildren, useContext, useEffect, useRef, useState } from 'react' import { useLatest } from 'react-use' import { toast } from 'sonner' import * as tus from 'tus-js-client' @@ -50,6 +50,7 @@ import { tryParseJson } from '@/lib/helpers' import { lookupMime } from '@/lib/mime' import { createProjectSupabaseClient } from '@/lib/project-supabase-client' import { ResponseError } from '@/types' +import { useSelectedBucket } from '@/components/interfaces/Storage/FilesBuckets/useSelectedBucket' type UploadProgress = { percentage: number @@ -82,11 +83,13 @@ function createStorageExplorerState({ connectionString, resumableUploadUrl, clientEndpoint, + bucketId, }: { projectRef: string connectionString: string resumableUploadUrl: string clientEndpoint: string + bucketId?: string }) { const localStorageKey = LOCAL_STORAGE_KEYS.STORAGE_PREFERENCE(projectRef) const { view, sortBy, sortByOrder, sortBucket } = @@ -95,6 +98,7 @@ function createStorageExplorerState({ const state = proxy({ projectRef, + bucketId, connectionString, resumableUploadUrl, uploadProgresses: [] as UploadProgress[], @@ -1864,10 +1868,13 @@ 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)) const stateRef = useLatest(state) + const bucketRef = useRef(bucket?.id) const { storageEndpoint, @@ -1883,13 +1890,20 @@ export const StorageExplorerStateContextProvider = ({ children }: PropsWithChild useEffect(() => { const hasDataReady = !!project?.ref const storeAlreadyLoaded = state.projectRef === project?.ref + const hasBucketChanged = bucket?.id !== bucketRef.current - if (!isPaused && hasDataReady && !storeAlreadyLoaded && isSuccessSettings) { + if ( + !isPaused && + hasDataReady && + isSuccessSettings && + (!storeAlreadyLoaded || hasBucketChanged) + ) { const clientEndpoint = storageEndpoint ?? hostEndpoint ?? '' const resumableUploadUrl = `${clientEndpoint}/storage/v1/upload/resumable` setState( createStorageExplorerState({ projectRef: project?.ref ?? '', + bucketId: bucket?.id, connectionString: project.connectionString ?? '', resumableUploadUrl, clientEndpoint, @@ -1897,6 +1911,7 @@ export const StorageExplorerStateContextProvider = ({ children }: PropsWithChild ) } }, [ + bucket?.id, state.projectRef, project?.ref, project?.connectionString, 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'