diff --git a/apps/studio/components/interfaces/Storage/StorageExplorer/StorageExplorer.tsx b/apps/studio/components/interfaces/Storage/StorageExplorer/StorageExplorer.tsx index 75976839083..9de3e77cfba 100644 --- a/apps/studio/components/interfaces/Storage/StorageExplorer/StorageExplorer.tsx +++ b/apps/studio/components/interfaces/Storage/StorageExplorer/StorageExplorer.tsx @@ -1,14 +1,13 @@ -import { compact, get, isEmpty, uniqBy } from 'lodash' -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' import { IS_PLATFORM } from 'lib/constants' +import { compact, get, isEmpty, uniqBy } from 'lodash' +import { useEffect, useRef, useState } from 'react' import { useStorageExplorerStateSnapshot } from 'state/storage-explorer' + import { useSelectedBucket } from '../FilesBuckets/useSelectedBucket' import { STORAGE_ROW_TYPES, STORAGE_VIEWS } from '../Storage.constants' import { ConfirmDeleteModal } from './ConfirmDeleteModal' @@ -18,6 +17,7 @@ import { FileExplorerHeader } from './FileExplorerHeader' import { FileExplorerHeaderSelection } from './FileExplorerHeaderSelection' import { MoveItemsModal } from './MoveItemsModal' import { PreviewPane } from './PreviewPane' +import { useStaticEffectEvent } from '@/hooks/useStaticEffectEvent' export const StorageExplorer = () => { const { ref, bucketId } = useParams() @@ -30,7 +30,6 @@ export const StorageExplorer = () => { openedFolders, selectedItemsToMove, selectedBucket, - openBucket, fetchFolderContents, fetchMoreFolderContents, fetchFoldersByPath, @@ -91,14 +90,10 @@ export const StorageExplorer = () => { } } }) + useEffect(() => { if (bucket && projectRef) fetchContents(bucket) - }, [bucket, projectRef, debouncedSearchString, fetchContents]) - - const openBucketRef = useLatest(openBucket) - useEffect(() => { - if (bucket && !!projectRef) openBucketRef.current(bucket) - }, [bucket, projectRef, openBucketRef]) + }, [bucket, projectRef, debouncedSearchString, selectedBucket.id, fetchContents]) /** Checkbox selection methods */ /** [Joshen] We'll only support checkbox selection for files ONLY */ diff --git a/apps/studio/components/layouts/ProjectLayout/ProjectContext.tsx b/apps/studio/components/layouts/ProjectLayout/ProjectContext.tsx index a796c54102e..461c8814c69 100644 --- a/apps/studio/components/layouts/ProjectLayout/ProjectContext.tsx +++ b/apps/studio/components/layouts/ProjectLayout/ProjectContext.tsx @@ -17,13 +17,11 @@ export const ProjectContextProvider = ({ return ( - - - - {children} - - - + + + {children} + + ) diff --git a/apps/studio/components/layouts/StorageLayout/StorageBucketsLayout.tsx b/apps/studio/components/layouts/StorageLayout/StorageBucketsLayout.tsx index 1859d7ff578..4a74e00a152 100644 --- a/apps/studio/components/layouts/StorageLayout/StorageBucketsLayout.tsx +++ b/apps/studio/components/layouts/StorageLayout/StorageBucketsLayout.tsx @@ -1,11 +1,10 @@ -import Link from 'next/link' -import { PropsWithChildren } from 'react' - import { IS_PLATFORM, useParams } from 'common' import { BUCKET_TYPES } from 'components/interfaces/Storage/Storage.constants' import { useStorageV2Page } from 'components/interfaces/Storage/Storage.utils' import { DocsButton } from 'components/ui/DocsButton' +import Link from 'next/link' import { usePathname } from 'next/navigation' +import { PropsWithChildren } from 'react' import { NavMenu, NavMenuItem } from 'ui' import { PageHeader, diff --git a/apps/studio/pages/project/[ref]/storage/files/buckets/[bucketId].tsx b/apps/studio/pages/project/[ref]/storage/files/buckets/[bucketId].tsx index 89d8152eeaf..fdacc798f6e 100644 --- a/apps/studio/pages/project/[ref]/storage/files/buckets/[bucketId].tsx +++ b/apps/studio/pages/project/[ref]/storage/files/buckets/[bucketId].tsx @@ -1,10 +1,3 @@ -import { ChevronDown, FolderOpen, Settings, Shield, Trash2 } from 'lucide-react' -import Link from 'next/link' -import { useRouter } from 'next/router' -import { parseAsBoolean, useQueryState } from 'nuqs' -import { useEffect } from 'react' -import { toast } from 'sonner' - import { useParams } from 'common' import { DeleteBucketModal } from 'components/interfaces/Storage/DeleteBucketModal' import { EditBucketModal } from 'components/interfaces/Storage/EditBucketModal' @@ -16,6 +9,12 @@ import { useBucketPolicyCount } from 'components/interfaces/Storage/useBucketPol import DefaultLayout from 'components/layouts/DefaultLayout' import { PageLayout } from 'components/layouts/PageLayout/PageLayout' import StorageLayout from 'components/layouts/StorageLayout/StorageLayout' +import { ChevronDown, FolderOpen, Settings, Shield, Trash2 } from 'lucide-react' +import Link from 'next/link' +import { useRouter } from 'next/router' +import { parseAsBoolean, useQueryState } from 'nuqs' +import { useEffect } from 'react' +import { toast } from 'sonner' import type { NextPageWithLayout } from 'types' import { Badge, @@ -27,6 +26,8 @@ import { DropdownMenuTrigger, } from 'ui' +import { StorageExplorerStateContextProvider } from '@/state/storage-explorer' + const BucketPage: NextPageWithLayout = () => { const router = useRouter() const { bucketId, ref } = useParams() @@ -57,11 +58,11 @@ const BucketPage: NextPageWithLayout = () => { }, [isSuccess]) if (isError) { - return + return } return ( - <> + { /> )} - + ) } diff --git a/apps/studio/state/storage-explorer.tsx b/apps/studio/state/storage-explorer.tsx index 807585de246..b72295411dd 100644 --- a/apps/studio/state/storage-explorer.tsx +++ b/apps/studio/state/storage-explorer.tsx @@ -8,6 +8,7 @@ import * as tus from 'tus-js-client' import { Button, SONNER_DEFAULT_DURATION, SonnerProgress } from 'ui' import { proxy, useSnapshot } from 'valtio' +import { useSelectedBucket } from '@/components/interfaces/Storage/FilesBuckets/useSelectedBucket' import { STORAGE_BUCKET_SORT, STORAGE_ROW_STATUS, @@ -80,11 +81,13 @@ if (typeof window !== 'undefined') { function createStorageExplorerState({ projectRef, connectionString, + bucket, resumableUploadUrl, clientEndpoint, }: { projectRef: string connectionString: string + bucket?: Bucket resumableUploadUrl: string clientEndpoint: string }) { @@ -98,6 +101,7 @@ function createStorageExplorerState({ connectionString, resumableUploadUrl, uploadProgresses: [] as UploadProgress[], + selectedBucket: bucket as Bucket, abortApiCalls: () => { if (abortController) { @@ -158,14 +162,6 @@ function createStorageExplorerState({ state.selectedItemsToMove = items }, - selectedBucket: {} as Bucket, - setSelectedBucket: (bucket: Bucket) => { - state.selectedBucket = bucket - state.setSelectedFilePreview(undefined) - state.clearOpenedFolders() - state.clearSelectedItems() - }, - setSelectedItemToRename: (file: { name: string; columnIndex: number }) => { state.updateRowStatus({ name: file.name, @@ -232,10 +228,6 @@ function createStorageExplorerState({ }) }, - openBucket: async (bucket: Bucket) => { - state.setSelectedBucket(bucket) - }, - // ======== Folders CRUD ======== addNewFolderPlaceholder: (columnIndex: number) => { @@ -1856,6 +1848,7 @@ const DEFAULT_STATE_CONFIG = { connectionString: '', resumableUploadUrl: '', clientEndpoint: '', + bucket: {} as Bucket, } const StorageExplorerStateContext = createContext( @@ -1864,6 +1857,7 @@ 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)) @@ -1881,16 +1875,18 @@ export const StorageExplorerStateContextProvider = ({ children }: PropsWithChild // So the useEffect here is to make sure that the project ref is loaded into the state properly // Although I'd be keen to re-investigate this to see if we can remove this useEffect(() => { - const hasDataReady = !!project?.ref + const hasDataReady = !!project && !!bucket const storeAlreadyLoaded = state.projectRef === project?.ref if (!isPaused && hasDataReady && !storeAlreadyLoaded && isSuccessSettings) { const clientEndpoint = storageEndpoint ?? hostEndpoint ?? '' const resumableUploadUrl = `${clientEndpoint}/storage/v1/upload/resumable` + setState( createStorageExplorerState({ - projectRef: project?.ref ?? '', + projectRef: project.ref, connectionString: project.connectionString ?? '', + bucket, resumableUploadUrl, clientEndpoint, }) @@ -1898,13 +1894,13 @@ export const StorageExplorerStateContextProvider = ({ children }: PropsWithChild } }, [ state.projectRef, - project?.ref, - project?.connectionString, + project, stateRef, isPaused, hostEndpoint, storageEndpoint, isSuccessSettings, + bucket, ]) return ( 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'