Fix: storage explorer ignore current filter after mutations (#51174)

## Problem

When users trigger actions such as deleting an item, the storage
explorer reloads the opened folders but ignore the currently applied
filter.

## Solution

Move the filter state in Valtio so that its other functions are aware of
it.

## Review instructions

1. Create a Supabase project and upload objects in Storage with date
prefixes (e.g., 202608XX)
2. Navigate to Storage, select a bucket with multi-dated/prefixed
objects
3. Enter a filter in the search box (e.g., 20260820) to show only
matching objects
4. Select one or more filtered objects and delete them

Observe the file list after deletion - it should show filtered contents
according to the search box value


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Bug Fixes**
* Storage search now stays in sync as you open folders and refresh their
contents.
* When restoring open folders, search results are filtered in the
deepest open folder rather than hiding ancestor folders.
* Deleting a file from filtered results keeps the search applied and
displays the remaining matches correctly.
* Search results remain consistent across folder navigation, refreshes,
and file deletion.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Gildas Garcia authored and GitHub committed 2026-10-05 09:52:32 +02:00
1 parent ba82106697
commit 5de3666930
4 files changed
+88 -8

No files matched your search

@@ -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 (
<div className="bg-studio flex h-full w-full flex-col">
+38 -2
View File
@@ -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,
})
},
@@ -0,0 +1,3 @@
This is a test file for e2e storage testing.
It contains some sample content to verify file uploads work correctly.
+45
View File
@@ -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'