diff --git a/apps/studio/components/interfaces/Storage/StorageExplorer/StorageExplorer.tsx b/apps/studio/components/interfaces/Storage/StorageExplorer/StorageExplorer.tsx
index 1ab2da94f98..310cd7ccf8e 100644
--- a/apps/studio/components/interfaces/Storage/StorageExplorer/StorageExplorer.tsx
+++ b/apps/studio/components/interfaces/Storage/StorageExplorer/StorageExplorer.tsx
@@ -1,6 +1,4 @@
-import { useDebounce } from '@uidotdev/usehooks'
import { useParams } from 'common'
-import { useState } from 'react'
import { useSelectedBucket } from '../FilesBuckets/useSelectedBucket'
import { StorageExplorerContent } from './StorageExplorerContent'
@@ -11,8 +9,8 @@ import { useStorageExplorerStateSnapshot } from '@/state/storage-explorer'
export const StorageExplorer = () => {
const { ref, bucketId } = useParams()
- const { selectedBucket } = useStorageExplorerStateSnapshot()
-
+ const { selectedBucket, debouncedSearchString, itemSearchString, setItemSearchString } =
+ useStorageExplorerStateSnapshot()
useProjectStorageConfigQuery({ projectRef: ref }, { enabled: IS_PLATFORM })
const { data: bucket, isLoading: isBucketQueryLoading } = useSelectedBucket()
@@ -20,8 +18,6 @@ export const StorageExplorer = () => {
const isBucketReady = !isBucketQueryLoading && !!bucket && bucketId === selectedBucket.id
// Deliberately not in the URL, so a shared link points at a folder, not someone's filter.
- const [itemSearchString, setItemSearchString] = useState('')
- const debouncedSearchString = useDebounce(itemSearchString, 500)
return (
diff --git a/apps/studio/state/storage-explorer.tsx b/apps/studio/state/storage-explorer.tsx
index 4d8cb3c12b3..4fc99bb8201 100644
--- a/apps/studio/state/storage-explorer.tsx
+++ b/apps/studio/state/storage-explorer.tsx
@@ -1,6 +1,17 @@
import { BlobReader, BlobWriter, ZipWriter } from '@zip.js/zip.js'
import { IS_PLATFORM } from 'common'
-import { capitalize, chunk, compact, find, findIndex, has, isObject, uniq, uniqBy } from 'lodash'
+import {
+ capitalize,
+ chunk,
+ compact,
+ debounce,
+ find,
+ findIndex,
+ has,
+ isObject,
+ uniq,
+ uniqBy,
+} from 'lodash'
import { createContext, PropsWithChildren, useContext, useEffect, useState } from 'react'
import { useLatest } from 'react-use'
import { toast } from 'sonner'
@@ -91,6 +102,15 @@ export function createStorageExplorerState({
const state = proxy({
projectRef,
connectionString,
+ itemSearchString: '',
+ debouncedSearchString: '',
+ setItemSearchString: (value: string) => {
+ state.itemSearchString = value
+ state.setDebouncedSearchString(value)
+ },
+ setDebouncedSearchString: debounce((value: string) => {
+ state.debouncedSearchString = value
+ }, 500),
resumableUploadUrl,
uploadProgresses: [] as UploadProgress[],
selectedBucket: bucket as Bucket,
@@ -400,7 +420,22 @@ export function createStorageExplorerState({
refetchAllOpenedFolders: async () => {
const paths = state.openedFolders.map((folder) => folder.name)
- await state.fetchFoldersByPath({ paths })
+
+ if (state.itemSearchString && paths.length === 0) {
+ await state.fetchFoldersByPath({ paths, searchString: state.itemSearchString })
+ } else if (state.itemSearchString) {
+ await state.fetchFoldersByPath({ paths })
+ // Reapply the filter to the current column only.
+ await state.fetchFolderContents({
+ bucketId: state.selectedBucket.id,
+ folderId: state.openedFolders[paths.length - 1].id,
+ folderName: paths[paths.length - 1],
+ index: paths.length - 1,
+ searchString: state.itemSearchString,
+ })
+ } else {
+ await state.fetchFoldersByPath({ paths })
+ }
},
refreshAll: async () => {
@@ -777,6 +812,7 @@ export function createStorageExplorerState({
folderId: folder.id,
folderName: folder.name,
index: columnIndex,
+ searchString: state.itemSearchString,
})
},
diff --git a/e2e/studio/features/files/another-file.txt b/e2e/studio/features/files/another-file.txt
new file mode 100644
index 00000000000..eba8160d606
--- /dev/null
+++ b/e2e/studio/features/files/another-file.txt
@@ -0,0 +1,3 @@
+This is a test file for e2e storage testing.
+It contains some sample content to verify file uploads work correctly.
+
diff --git a/e2e/studio/features/storage.spec.ts b/e2e/studio/features/storage.spec.ts
index a6acb389cd3..aaf92753c96 100644
--- a/e2e/studio/features/storage.spec.ts
+++ b/e2e/studio/features/storage.spec.ts
@@ -557,6 +557,51 @@ test.describe('Storage', () => {
await deleteItem(page, fileName)
})
+ test('Deleting a file preserves the current filter', async ({ page, ref }) => {
+ const bucketName = `${bucketNamePrefix}_delete_file_filter`
+ const fileName = 'test-file.txt'
+
+ // Create a bucket via API, navigate to it, and upload a file
+ await using _ = await withSetupCleanup(
+ async () => {
+ await createBucketViaApi(bucketName, false)
+ },
+ async () => {
+ await deleteBucketViaApi(bucketName)
+ }
+ )
+ await navigateToStorageFiles(page, ref)
+ await navigateToBucket(page, ref, bucketName)
+
+ await uploadFile(
+ page,
+ path.join(import.meta.dirname, 'files', 'test-file.txt'),
+ 'test-file.txt'
+ )
+ await uploadFile(
+ page,
+ path.join(import.meta.dirname, 'files', 'test-file-2.txt'),
+ 'test-file-2.txt'
+ )
+ await uploadFile(
+ page,
+ path.join(import.meta.dirname, 'files', 'another-file.txt'),
+ 'another-file.txt'
+ )
+ await page.getByPlaceholder('Search in root directory...').fill('test')
+ await expect(page.getByTitle('another-file.txt')).toHaveCount(0)
+ await expect(page.getByTitle('test-file.txt')).toHaveCount(1)
+ await expect(page.getByTitle('test-file-2.txt')).toHaveCount(1)
+
+ // Delete the file
+ await deleteItem(page, fileName)
+
+ // Check the filter is still applied
+ await expect(page.getByTitle('test-file.txt')).toHaveCount(0)
+ await expect(page.getByTitle('test-file-2.txt')).toHaveCount(1)
+ await expect(page.getByTitle('another-file.txt')).toHaveCount(0)
+ })
+
test('can delete a folder', async ({ page, ref }) => {
const bucketName = `${bucketNamePrefix}_delete_folder`
const folderName = 'test_folder'