mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 17:35:10 +03:00
fix(studio): re-sort a column's items after appending the next page
Each list-v2 page is sorted on its own, so fetchMoreFolderContents' col.items.concat(items) didn't keep the column sorted once a second page came in — a naive concat leaves the new page's items after the old ones regardless of the active sort column. Extracts the merge comparator from formatFolderItemsV2 into a shared sortStorageItems helper and re-sorts the combined list after every load-more.
This commit is contained in:
1 parent
0098c08b36
commit
dedbea2ed3
4 files changed
+102
-14
No files matched your search
+43
@@ -21,6 +21,7 @@ import {
|
||||
parseStoragePath,
|
||||
sanitizeNameForDuplicateInColumn,
|
||||
serializeStoragePath,
|
||||
sortStorageItems,
|
||||
validateFolderName,
|
||||
} from '@/components/interfaces/Storage/StorageExplorer/StorageExplorer.utils'
|
||||
|
||||
@@ -472,6 +473,48 @@ describe('getListV2EntryName', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('sortStorageItems', () => {
|
||||
function makeSortable(name: string, date: string | null = null) {
|
||||
return { name, created_at: date, updated_at: date, last_accessed_at: date }
|
||||
}
|
||||
|
||||
it('sorts by name ascending by default direction', () => {
|
||||
const items = [makeSortable('zebra'), makeSortable('apple')]
|
||||
expect(
|
||||
sortStorageItems(items, {
|
||||
column: STORAGE_SORT_BY.NAME,
|
||||
order: STORAGE_SORT_BY_ORDER.ASC,
|
||||
}).map((i) => i.name)
|
||||
).toEqual(['apple', 'zebra'])
|
||||
})
|
||||
|
||||
it('sorts descending when requested', () => {
|
||||
const items = [makeSortable('apple'), makeSortable('zebra')]
|
||||
expect(
|
||||
sortStorageItems(items, {
|
||||
column: STORAGE_SORT_BY.NAME,
|
||||
order: STORAGE_SORT_BY_ORDER.DESC,
|
||||
}).map((i) => i.name)
|
||||
).toEqual(['zebra', 'apple'])
|
||||
})
|
||||
|
||||
it('sorts by created_at, treating a null value as empty', () => {
|
||||
const items = [makeSortable('has-date', '2024-01-01T00:00:00Z'), makeSortable('no-date', null)]
|
||||
expect(
|
||||
sortStorageItems(items, {
|
||||
column: STORAGE_SORT_BY.CREATED_AT,
|
||||
order: STORAGE_SORT_BY_ORDER.ASC,
|
||||
}).map((i) => i.name)
|
||||
).toEqual(['no-date', 'has-date'])
|
||||
})
|
||||
|
||||
it('does not mutate the input array', () => {
|
||||
const items = [makeSortable('zebra'), makeSortable('apple')]
|
||||
sortStorageItems(items, { column: STORAGE_SORT_BY.NAME, order: STORAGE_SORT_BY_ORDER.ASC })
|
||||
expect(items.map((i) => i.name)).toEqual(['zebra', 'apple'])
|
||||
})
|
||||
})
|
||||
|
||||
describe('formatFolderItemsV2', () => {
|
||||
const NAME_ASC = { column: STORAGE_SORT_BY.NAME, order: STORAGE_SORT_BY_ORDER.ASC }
|
||||
|
||||
|
||||
+25
-6
@@ -266,6 +266,30 @@ export function getListV2EntryName(name: string): string {
|
||||
return trimmed.split('/').pop() ?? trimmed
|
||||
}
|
||||
|
||||
type SortableByColumn = {
|
||||
name: string
|
||||
created_at: string | null
|
||||
updated_at: string | null
|
||||
last_accessed_at: string | null
|
||||
}
|
||||
|
||||
/**
|
||||
* Sorts by the given column/order. Used both to order a single merged folders+files page and
|
||||
* to re-sort a column's items after appending another page onto it — concatenating two
|
||||
* independently-sorted pages does not itself produce a sorted list.
|
||||
*/
|
||||
export function sortStorageItems<T extends SortableByColumn>(
|
||||
items: T[],
|
||||
sortBy: { column: STORAGE_SORT_BY; order: STORAGE_SORT_BY_ORDER }
|
||||
): T[] {
|
||||
const direction = sortBy.order === STORAGE_SORT_BY_ORDER.DESC ? -1 : 1
|
||||
return [...items].sort((a, b) => {
|
||||
const aValue = (sortBy.column === STORAGE_SORT_BY.NAME ? a.name : a[sortBy.column]) ?? ''
|
||||
const bValue = (sortBy.column === STORAGE_SORT_BY.NAME ? b.name : b[sortBy.column]) ?? ''
|
||||
return direction * String(aValue).localeCompare(String(bValue))
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* v2 returns folders and files as two separate arrays; merges them into the single sorted list
|
||||
* `formatFolderItems` expects, tagging folders with `id: null` per its v1-derived convention.
|
||||
@@ -290,12 +314,7 @@ export function formatFolderItemsV2(
|
||||
...object,
|
||||
name: getListV2EntryName(object.name),
|
||||
}))
|
||||
const direction = sortBy.order === STORAGE_SORT_BY_ORDER.DESC ? -1 : 1
|
||||
const merged = [...folders, ...objects].sort((a, b) => {
|
||||
const aValue = (sortBy.column === STORAGE_SORT_BY.NAME ? a.name : a[sortBy.column]) ?? ''
|
||||
const bValue = (sortBy.column === STORAGE_SORT_BY.NAME ? b.name : b[sortBy.column]) ?? ''
|
||||
return direction * String(aValue).localeCompare(String(bValue))
|
||||
})
|
||||
const merged = sortStorageItems([...folders, ...objects], sortBy)
|
||||
return formatFolderItems(merged, prefix)
|
||||
}
|
||||
|
||||
|
||||
@@ -2,8 +2,11 @@ import { HttpResponse } from 'msw'
|
||||
import { beforeEach, describe, expect, it } from 'vitest'
|
||||
|
||||
import { createStorageExplorerState } from './storage-explorer'
|
||||
import { STORAGE_ROW_STATUS } from '@/components/interfaces/Storage/Storage.constants'
|
||||
import type { StorageColumn } from '@/components/interfaces/Storage/Storage.types'
|
||||
import {
|
||||
STORAGE_ROW_STATUS,
|
||||
STORAGE_ROW_TYPES,
|
||||
} from '@/components/interfaces/Storage/Storage.constants'
|
||||
import type { StorageColumn, StorageItem } from '@/components/interfaces/Storage/Storage.types'
|
||||
import type { StorageObjectsPage } from '@/data/storage/bucket-objects-infinite-query'
|
||||
import type { Bucket } from '@/data/storage/buckets-query'
|
||||
import { addAPIMock } from '@/tests/lib/msw'
|
||||
@@ -189,7 +192,21 @@ describe('fetchMoreFolderContents', () => {
|
||||
}
|
||||
}
|
||||
|
||||
it('sends the column cursor and appends the next page', async () => {
|
||||
function makeItem(name: string): StorageItem {
|
||||
return {
|
||||
id: name,
|
||||
name,
|
||||
type: STORAGE_ROW_TYPES.FILE,
|
||||
status: STORAGE_ROW_STATUS.READY,
|
||||
metadata: null,
|
||||
isCorrupted: false,
|
||||
created_at: '2024-01-01T00:00:00Z',
|
||||
updated_at: '2024-01-01T00:00:00Z',
|
||||
last_accessed_at: '2024-01-01T00:00:00Z',
|
||||
}
|
||||
}
|
||||
|
||||
it('sends the column cursor, re-sorts, and appends the next page', async () => {
|
||||
let requestBody: any
|
||||
addAPIMock({
|
||||
method: 'post',
|
||||
@@ -201,7 +218,7 @@ describe('fetchMoreFolderContents', () => {
|
||||
objects: [
|
||||
{
|
||||
id: 'f2',
|
||||
name: 'page2.png',
|
||||
name: 'aaa-page2.png',
|
||||
created_at: '2024-01-01T00:00:00Z',
|
||||
updated_at: '2024-01-01T00:00:00Z',
|
||||
last_accessed_at: '2024-01-01T00:00:00Z',
|
||||
@@ -214,13 +231,18 @@ describe('fetchMoreFolderContents', () => {
|
||||
})
|
||||
|
||||
const state = createState(makeBucket('bucket-a'))
|
||||
const column = makeReadyColumn()
|
||||
// Existing item sorts after the new page's item by name — a naive concat would leave it
|
||||
// first anyway, so this only passes if the combined list is actually re-sorted.
|
||||
const column = makeReadyColumn({ items: [makeItem('zzz-page1.png')] })
|
||||
state.columns = [column]
|
||||
|
||||
await state.fetchMoreFolderContents({ index: 0, column })
|
||||
|
||||
expect(requestBody.cursor).toBe('cursor-1')
|
||||
expect(state.columns[0].items.map((item) => item.name)).toEqual(['page2.png'])
|
||||
expect(state.columns[0].items.map((item) => item.name)).toEqual([
|
||||
'aaa-page2.png',
|
||||
'zzz-page1.png',
|
||||
])
|
||||
expect(state.columns[0].hasMoreItems).toBe(false)
|
||||
expect(state.columns[0].cursor).toBeNull()
|
||||
})
|
||||
|
||||
@@ -29,6 +29,7 @@ import {
|
||||
getListV2EntryName,
|
||||
getPathAlongFoldersToIndex,
|
||||
sanitizeNameForDuplicateInColumn,
|
||||
sortStorageItems,
|
||||
validateFolderName,
|
||||
} from '@/components/interfaces/Storage/StorageExplorer/StorageExplorer.utils'
|
||||
import { fetchFileUrl } from '@/components/interfaces/Storage/StorageExplorer/useFetchFileUrlQuery'
|
||||
@@ -127,6 +128,7 @@ export function createStorageExplorerState({
|
||||
items: formatFolderItemsV2(page, sortOptions, path),
|
||||
hasMoreItems: nextCursor !== null,
|
||||
cursor: nextCursor,
|
||||
sortBy: sortOptions,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -389,7 +391,7 @@ export function createStorageExplorerState({
|
||||
state.setColumnIsLoadingMore(index)
|
||||
|
||||
try {
|
||||
const { items, hasMoreItems, cursor } = await listFolderPage(
|
||||
const { items, hasMoreItems, cursor, sortBy } = await listFolderPage(
|
||||
{
|
||||
bucketId: state.selectedBucket.id,
|
||||
path: column.path,
|
||||
@@ -407,7 +409,9 @@ export function createStorageExplorerState({
|
||||
if (isStale) return { ...col, isLoadingMoreItems: false }
|
||||
return {
|
||||
...col,
|
||||
items: col.items.concat(items),
|
||||
// Each page is sorted on its own; concatenating two sorted pages doesn't produce
|
||||
// a sorted list, so the combined items need re-sorting.
|
||||
items: sortStorageItems(col.items.concat(items), sortBy),
|
||||
isLoadingMoreItems: false,
|
||||
hasMoreItems,
|
||||
cursor,
|
||||
|
||||
Reference in new issue
Block a user