mirror of
https://github.com/supabase/supabase.git
synced 2026-10-08 10:55:06 +03:00
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:
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">
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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'
|
||||
|
||||
Reference in new issue
Block a user