mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 17:35:10 +03:00
| | PR | Base | Branch | | --- | --- | --- | --- | | 1 | **this PR** | `master` | pre-existing correctness fixes | | 2 | #50413 | `fix/storage-explorer-listing-and-scroll` | `?path`/`?preview` deep-linking + copy row actions | | 3 | #50478 | `feat/storage-nav-improvement` | end-to-end deep-link test | | 4 | #50480 | `test/storage-deep-link-e2e` | copy path / copy link row actions | To read the whole change in one view: ```bash git diff master...test/storage-deep-link-e2e -- apps/studio e2e ``` ## What is the current behavior? Four independent bugs in the storage explorer, all pre-existing on `master`: - `hasMoreItems` is derived from the *formatted* listing, but `formatFolderItems` drops the `.emptyFolderPlaceholder` — so a full page can format to `LIMIT - 1` and stop pagination a page early. - A failed listing is indistinguishable from an empty folder, so a fetch error reads as "this folder has nothing in it". - `fetchFoldersByPath` commits its result against whichever bucket is selected when the requests resolve. Switching buckets mid-flight files the old bucket's items under the new bucket's name — and because `columns[0].name` then matches, nothing downstream notices and refetches. - The horizontal auto-scroll never runs its guard (`if (fileExplorerRef)` is always truthy), scrolls relatively so repeated runs drift, and depends on the `columns` array identity — so a background refetch yanks the view back to the right. It also scrolls in list view, where there is nothing to scroll. ## What is the new behavior? Each of the above is fixed at its source. Pagination and the exhaustiveness check now compare the raw page length; listings carry an `isComplete` flag; `fetchFoldersByPath` captures the bucket id at entry and discards a stale result; the scroll is absolute, guarded, keyed on `columns.length`, and skipped in list view. Two new test files cover the parts that were silently wrong before: `state/storage-explorer.test.ts` (MSW, the bucket race) and `FileExplorer.test.tsx` (scroll geometry, with the container's layout defined by hand since jsdom reports everything as zero-sized). Both were checked by reverting the fix and confirming they fail. ## Additional context `fetchFoldersByPath` also starts returning `{ missingPaths }` here. Nothing reads it yet — the first consumer is in PR 2 — but it shares a hunk with the `isComplete` work, so separating it would mean two PRs editing the same lines. It is backward-compatible: all three existing call sites ignore the return value. 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit ## Bug Fixes - Improved Storage Explorer column-view scrolling so the newest column remains visible, including when the preview pane opens. - Prevented folder results from a previously selected bucket from appearing after switching buckets during loading. - Improved handling of incomplete or partial folder listings to avoid incorrectly treating failed results as empty folders. - Preserved the correct scroll position when using list view. <!-- end of auto-generated comment: release notes by coderabbit.ai --> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
110 lines
3.2 KiB
TypeScript
110 lines
3.2 KiB
TypeScript
import { HttpResponse } from 'msw'
|
|
import { beforeEach, describe, expect, it } from 'vitest'
|
|
|
|
import { createStorageExplorerState } from './storage-explorer'
|
|
import type { StorageObject } from '@/data/storage/bucket-objects-list-mutation'
|
|
import type { Bucket } from '@/data/storage/buckets-query'
|
|
import { addAPIMock } from '@/tests/lib/msw'
|
|
|
|
function makeBucket(id: string): Bucket {
|
|
return {
|
|
id,
|
|
name: id,
|
|
owner: 'owner',
|
|
public: false,
|
|
type: 'STANDARD',
|
|
created_at: '2024-01-01T00:00:00Z',
|
|
updated_at: '2024-01-01T00:00:00Z',
|
|
} as Bucket
|
|
}
|
|
|
|
const BUCKET_A_LISTING: StorageObject[] = [
|
|
{
|
|
id: 'file-in-bucket-a',
|
|
name: 'only-in-bucket-a.png',
|
|
created_at: '2024-01-01T00:00:00Z',
|
|
updated_at: '2024-01-01T00:00:00Z',
|
|
last_accessed_at: '2024-01-01T00:00:00Z',
|
|
metadata: { size: 1, mimetype: 'image/png' },
|
|
},
|
|
]
|
|
|
|
/** Objects with a null id are prefixes, i.e. folders. */
|
|
const FOLDER_LISTING: StorageObject[] = [
|
|
{
|
|
id: null,
|
|
name: 'shared',
|
|
created_at: null,
|
|
updated_at: null,
|
|
last_accessed_at: null,
|
|
metadata: null,
|
|
},
|
|
]
|
|
|
|
function createState(bucket: Bucket) {
|
|
return createStorageExplorerState({
|
|
projectRef: 'test-ref',
|
|
connectionString: '',
|
|
bucket,
|
|
resumableUploadUrl: '',
|
|
clientEndpoint: '',
|
|
})
|
|
}
|
|
|
|
describe('fetchFoldersByPath', () => {
|
|
beforeEach(() => {
|
|
localStorage.clear()
|
|
})
|
|
|
|
it('discards a restore that resolves after the bucket changed', async () => {
|
|
let releaseListing: (() => void) | undefined
|
|
const listingReleased = new Promise<void>((resolve) => {
|
|
releaseListing = resolve
|
|
})
|
|
|
|
addAPIMock({
|
|
method: 'post',
|
|
path: '/platform/storage/:ref/buckets/:id/objects/list',
|
|
response: async () => {
|
|
await listingReleased
|
|
// Belongs to bucket-a, the bucket the request was issued for
|
|
return HttpResponse.json<StorageObject[]>(BUCKET_A_LISTING)
|
|
},
|
|
})
|
|
|
|
const state = createState(makeBucket('bucket-a'))
|
|
const restore = state.fetchFoldersByPath({ paths: ['shared'], showLoading: true })
|
|
|
|
// The user switches buckets while the listing is still in flight. Same `?path`, so
|
|
// nothing about the requested location changes — only the bucket underneath it.
|
|
state.selectedBucket = makeBucket('bucket-b')
|
|
|
|
releaseListing?.()
|
|
const { missingPaths } = await restore
|
|
|
|
// bucket-a's contents must not be committed, and must not be relabelled as bucket-b
|
|
const allItems = state.columns.flatMap((column) => column.items)
|
|
expect(allItems).toHaveLength(0)
|
|
expect(state.columns[0]?.name).toBe('bucket-a')
|
|
expect(missingPaths).toEqual([])
|
|
})
|
|
|
|
it('commits the restore when the bucket is unchanged', async () => {
|
|
addAPIMock({
|
|
method: 'post',
|
|
path: '/platform/storage/:ref/buckets/:id/objects/list',
|
|
response: FOLDER_LISTING,
|
|
})
|
|
|
|
const state = createState(makeBucket('bucket-a'))
|
|
const { missingPaths } = await state.fetchFoldersByPath({
|
|
paths: ['shared'],
|
|
showLoading: true,
|
|
})
|
|
|
|
expect(missingPaths).toEqual([])
|
|
expect(state.columns[0]?.items.map((item) => item.name)).toEqual(['shared'])
|
|
expect(state.openedFolders.map((folder) => folder.name)).toEqual(['shared'])
|
|
})
|
|
})
|