mirror of
https://github.com/supabase/supabase.git
synced 2026-10-06 01:45:10 +03:00
Chore/fix storage explorer when switching buckets (#43541)
## Context Taking a slightly different approach to [this PR](https://github.com/supabase/supabase/pull/43370) Original problem was that if you opened some folders while in a bucket and then switched to a different bucket, the folder UI will persists (folders from Bucket A will render when landing on Bucket B) ## Changes involved - Shift `StorageExplorerStateContextProvider` into `[bucketId].tsx]` instead of `ProjectContext` - The valtio store here only applies for the storage explorer so having it so high in the project's context was unnecessary - This also just implies that the valtio store will automatically reset whenever the bucket changes - Simplify storage explorer valtio store by initializing the store with the bucket - We'll initialize the selected bucket with the store now (Same as previous PR) - Removes unnecessary `setSelectedBucket` method which required a separate `useEffect` in `StorageExplorer.tsx` ## To test - [ ] Verify that the original is resolved - [ ] General smoke test of the storage explorer - i've also re-added the e2e test that Gildas wrote up in his PR
This commit is contained in:
1 parent
4a173eea60
commit
9b0dc8d9b9
6 files changed
+68
-50
No files matched your search
@@ -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 */
|
||||
|
||||
@@ -17,13 +17,11 @@ export const ProjectContextProvider = ({
|
||||
return (
|
||||
<TableEditorStateContextProvider key={`table-editor-state-${projectRef}`}>
|
||||
<TabsStateContextProvider key={`tabs-state-${projectRef}`}>
|
||||
<StorageExplorerStateContextProvider key={`storage-explorer-state-${projectRef}`}>
|
||||
<DatabaseSelectorStateContextProvider key={`database-selector-state-${projectRef}`}>
|
||||
<RoleImpersonationStateContextProvider key={`role-impersonation-state-${projectRef}`}>
|
||||
{children}
|
||||
</RoleImpersonationStateContextProvider>
|
||||
</DatabaseSelectorStateContextProvider>
|
||||
</StorageExplorerStateContextProvider>
|
||||
<DatabaseSelectorStateContextProvider key={`database-selector-state-${projectRef}`}>
|
||||
<RoleImpersonationStateContextProvider key={`role-impersonation-state-${projectRef}`}>
|
||||
{children}
|
||||
</RoleImpersonationStateContextProvider>
|
||||
</DatabaseSelectorStateContextProvider>
|
||||
</TabsStateContextProvider>
|
||||
</TableEditorStateContextProvider>
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 <StorageBucketsError error={error as any} />
|
||||
return <StorageBucketsError error={error} />
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<StorageExplorerStateContextProvider key={`storage-explorer-state-${ref}`}>
|
||||
<PageLayout
|
||||
size="full"
|
||||
isCompact
|
||||
@@ -163,7 +164,7 @@ const BucketPage: NextPageWithLayout = () => {
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
</StorageExplorerStateContextProvider>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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<StorageExplorerState>(
|
||||
@@ -1864,6 +1857,7 @@ const StorageExplorerStateContext = createContext<StorageExplorerState>(
|
||||
|
||||
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 (
|
||||
|
||||
@@ -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'
|
||||
|
||||
Reference in new issue
Block a user