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'