From 65b68de19752861625a70c77433c7777b79e4a7a Mon Sep 17 00:00:00 2001 From: cxymds Date: Wed, 8 Jul 2026 10:57:22 +0800 Subject: [PATCH] feat: improve object browser lazy loading (#152) --- components/object/list.tsx | 236 ++++++++++++++--------- i18n/locales/ar-MA.json | 9 +- i18n/locales/de-DE.json | 9 +- i18n/locales/en-US.json | 9 +- i18n/locales/es-ES.json | 9 +- i18n/locales/fr-FR.json | 9 +- i18n/locales/id-ID.json | 9 +- i18n/locales/it-IT.json | 9 +- i18n/locales/ja-JP.json | 9 +- i18n/locales/ko-KR.json | 9 +- i18n/locales/pt-BR.json | 9 +- i18n/locales/ru-RU.json | 9 +- i18n/locales/tr-TR.json | 9 +- i18n/locales/vi-VN.json | 9 +- i18n/locales/zh-CN.json | 9 +- lib/object-list-pagination.js | 6 +- lib/object-list-pagination.ts | 12 +- tests/lib/object-list-pagination.test.ts | 29 ++- tests/lib/object-list-source.test.js | 26 +++ 19 files changed, 295 insertions(+), 140 deletions(-) diff --git a/components/object/list.tsx b/components/object/list.tsx index 2521d23..06904f6 100644 --- a/components/object/list.tsx +++ b/components/object/list.tsx @@ -10,10 +10,10 @@ import { RiRefreshLine, RiFolderLine, RiFileLine, - RiArrowLeftSLine, - RiArrowRightSLine, RiEyeLine, RiEdit2Line, + RiArrowUpSLine, + RiArrowDownSLine, } from "@remixicon/react" import { AlertDialog, @@ -36,10 +36,10 @@ import { DialogTitle, } from "@/components/ui/dialog" import { Input } from "@/components/ui/input" -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" import { SearchInput } from "@/components/search-input" import { PageHeader } from "@/components/page-header" import { DataTable } from "@/components/data-table/data-table" +import { Spinner } from "@/components/ui/spinner" import { useDataTable } from "@/hooks/use-data-table" import { useObject } from "@/hooks/use-object" import { useBucket } from "@/hooks/use-bucket" @@ -57,11 +57,7 @@ import { shouldApplyObjectListResponse, shouldResetObjectListPagination, } from "@/lib/object-list-state" -import { - OBJECT_LIST_DEFAULT_PAGE_SIZE, - OBJECT_LIST_PAGE_SIZE_OPTIONS, - normalizeObjectListPageSize, -} from "@/lib/object-list-pagination" +import { OBJECT_LIST_DEFAULT_PAGE_SIZE, resolveObjectListPageSize } from "@/lib/object-list-pagination" import { resolveBucketVersioningState, shouldForceDeleteObjects, @@ -125,15 +121,10 @@ export function ObjectList({ const [searchTerm, setSearchTerm] = React.useState("") const [showDeleted, setShowDeleted] = useLocalStorage("object-list-show-deleted", false) - const [storedPageSize, setStoredPageSize] = useLocalStorage( - "object-list-page-size", - normalizeObjectListPageSize(pageSize), - ) const [loading, setLoading] = React.useState(false) const [data, setData] = React.useState([]) - const [continuationToken, setContinuationToken] = React.useState() - const [tokenHistory, setTokenHistory] = React.useState([]) const [nextToken, setNextToken] = React.useState() + const [showScrollShortcuts, setShowScrollShortcuts] = React.useState(false) const [bucketVersioningState, setBucketVersioningState] = React.useState("unknown") const [deleteDialogOpen, setDeleteDialogOpen] = React.useState(false) const [deleteDialogKeys, setDeleteDialogKeys] = React.useState([]) @@ -144,7 +135,7 @@ export function ObjectList({ const [renameSubmitting, setRenameSubmitting] = React.useState(false) const prefix = decodeURIComponent(path) - const resolvedPageSize = normalizeObjectListPageSize(storedPageSize) + const resolvedPageSize = resolveObjectListPageSize(pageSize) const listScope = React.useMemo( () => createObjectListScope({ @@ -162,17 +153,52 @@ export function ObjectList({ const requestIdRef = React.useRef(0) const activeScopeRef = React.useRef(listScope) const previousScopeRef = React.useRef(listScope) + const loadingRef = React.useRef(false) + const loadMoreRef = React.useRef(null) React.useEffect(() => { activeScopeRef.current = listScope }, [listScope]) + React.useEffect(() => { + loadingRef.current = loading + }, [loading]) + + const updateScrollShortcutVisibility = React.useCallback(() => { + setShowScrollShortcuts(document.documentElement.scrollHeight > window.innerHeight) + }, []) + + React.useEffect(() => { + updateScrollShortcutVisibility() + }, [data.length, loading, updateScrollShortcutVisibility]) + + React.useEffect(() => { + updateScrollShortcutVisibility() + window.addEventListener("resize", updateScrollShortcutVisibility) + window.addEventListener("scroll", updateScrollShortcutVisibility, { passive: true }) + + return () => { + window.removeEventListener("resize", updateScrollShortcutVisibility) + window.removeEventListener("scroll", updateScrollShortcutVisibility) + } + }, [updateScrollShortcutVisibility]) + + const scrollToTop = React.useCallback(() => { + window.scrollTo({ top: 0, left: 0, behavior: "auto" }) + }, []) + + const scrollToBottom = React.useCallback(() => { + window.scrollTo({ top: document.documentElement.scrollHeight, left: 0, behavior: "auto" }) + }, []) + const fetchObjects = React.useCallback( - async (options?: { token?: string; resetPagination?: boolean }) => { - const token = options?.resetPagination ? undefined : (options?.token ?? continuationToken) + async (options?: { token?: string; resetPagination?: boolean; append?: boolean }) => { + const token = options?.resetPagination ? undefined : options?.token + const shouldAppend = Boolean(options?.append && token) const requestId = requestIdRef.current + 1 requestIdRef.current = requestId const requestScope = activeScopeRef.current + loadingRef.current = true setLoading(true) try { const response = await listObject(bucket, prefix || undefined, resolvedPageSize, token, { @@ -212,7 +238,12 @@ export function ObjectList({ LastModified: normalizeDateToIso(item.LastModified), })) - setData([...prefixItems, ...objectItems]) + const rows = [...prefixItems, ...objectItems] + if (shouldAppend) { + setData((currentRows) => [...currentRows, ...rows]) + } else { + setData(rows) + } } catch (error) { console.error("Failed to fetch objects:", error) message.error((error as Error)?.message ?? t("Failed to load objects")) @@ -225,7 +256,9 @@ export function ObjectList({ }) ) { setNextToken(undefined) - setData([]) + if (!shouldAppend) { + setData([]) + } } } finally { window.setTimeout(() => { @@ -237,14 +270,21 @@ export function ObjectList({ activeScope: activeScopeRef.current, }) ) { + loadingRef.current = false setLoading(false) } }, 200) } }, - [bucket, prefix, resolvedPageSize, continuationToken, showDeleted, listObject, message, t], + [bucket, prefix, resolvedPageSize, showDeleted, listObject, message, t], ) + const resetAndFetchObjects = React.useCallback(() => { + setNextToken(undefined) + setData([]) + void fetchObjects({ resetPagination: true }) + }, [fetchObjects]) + const prevRefreshTriggerRef = React.useRef(refreshTrigger) React.useEffect(() => { @@ -254,13 +294,11 @@ export function ObjectList({ prevRefreshTriggerRef.current = refreshTrigger if (isRefresh || shouldResetPagination) { - setContinuationToken(undefined) - setTokenHistory([]) - void fetchObjects({ resetPagination: true }) + resetAndFetchObjects() } else { - void fetchObjects() + void fetchObjects({ resetPagination: true }) } - }, [listScope, bucket, prefix, resolvedPageSize, continuationToken, showDeleted, refreshTrigger, fetchObjects]) + }, [listScope, refreshTrigger, fetchObjects, resetAndFetchObjects]) const prevDeleteTaskIdsRef = React.useRef>(new Set()) @@ -274,12 +312,12 @@ export function ObjectList({ const anyActive = currentDeleteTasks.some((t) => ["pending", "running"].includes(t.status)) if (!anyActive) { // No more active delete tasks, refresh the list - void fetchObjects({ resetPagination: true }) + resetAndFetchObjects() } } prevDeleteTaskIdsRef.current = new Set(completedDeleteTasks.map((t) => t.id)) - }, [tasks, fetchObjects]) + }, [tasks, resetAndFetchObjects]) React.useEffect(() => { let cancelled = false @@ -306,11 +344,6 @@ export function ObjectList({ } }, [bucket, getBucketVersioning]) - React.useEffect(() => { - setContinuationToken(undefined) - setTokenHistory([]) - }, [showDeleted]) - const displayKey = React.useCallback( (key: string) => { if (!prefix) return key @@ -553,7 +586,7 @@ export function ObjectList({ setRenameDialogOpen(false) setRenameSourceKey("") setRenameName("") - void fetchObjects({ resetPagination: true }) + resetAndFetchObjects() } catch (err) { message.error((err as Error)?.message ?? t("Rename Failed")) } finally { @@ -640,32 +673,36 @@ export function ObjectList({ openDeleteDialog([...checkedKeys]) } - const goToNextPage = () => { - if (!nextToken) return - setTokenHistory((h) => [...h, continuationToken as string]) - setContinuationToken(nextToken) - } - - const goToPreviousPage = () => { - if (tokenHistory.length === 0) return - const prevToken = tokenHistory[tokenHistory.length - 1] - setContinuationToken(prevToken) - setTokenHistory((h) => h.slice(0, -1)) - } - - const handlePageSizeChange = (value: string) => { - const parsed = Number.parseInt(value, 10) - const nextPageSize = normalizeObjectListPageSize(Number.isNaN(parsed) ? undefined : parsed) - setStoredPageSize(nextPageSize) - setContinuationToken(undefined) - setTokenHistory([]) - } - React.useEffect(() => { const col = table.getColumn("object") if (col) col.setFilterValue(searchTerm || undefined) }, [searchTerm, table]) + const loadNextBatch = React.useCallback(() => { + if (!nextToken || loadingRef.current) return + void fetchObjects({ token: nextToken, append: true }) + }, [fetchObjects, nextToken]) + + React.useEffect(() => { + const node = loadMoreRef.current + if (!node || !nextToken || typeof IntersectionObserver === "undefined") return + + const observer = new IntersectionObserver( + (entries) => { + if (entries.some((entry) => entry.isIntersecting)) { + loadNextBatch() + } + }, + { rootMargin: "320px 0px" }, + ) + + observer.observe(node) + + return () => { + observer.disconnect() + } + }, [loadNextBatch, nextToken]) + return (
) : null} - @@ -710,7 +744,7 @@ export function ObjectList({ @@ -730,48 +764,60 @@ export function ObjectList({ -
-
- - {t("Loaded {count} of up to {pageSize} keys on this page", { - count: data.length, - pageSize: resolvedPageSize, - })} - - {t("Filtering and sorting apply to the current page")} -
- {t("Rows per page")} - -
-
-
- - -
+
+ + {t("Loaded {count} objects", { + count: data.length, + })} + + {t("Filtering and sorting apply to loaded objects")}
+ {nextToken ? ( +
+ {loading && data.length > 0 ? ( + + + {t("Loading more objects")} + + ) : ( + {t("Scroll to load more objects")} + )} +
+ ) : null} + + {showScrollShortcuts ? ( +
+ + +
+ ) : null} + diff --git a/i18n/locales/ar-MA.json b/i18n/locales/ar-MA.json index 5fa7665..2c00835 100644 --- a/i18n/locales/ar-MA.json +++ b/i18n/locales/ar-MA.json @@ -1360,5 +1360,12 @@ "Rename": "إعادة التسمية", "Rename Failed": "فشلت إعادة التسمية", "Rename Object": "إعادة تسمية الكائن", - "Renaming object": "جارٍ إعادة تسمية الكائن" + "Renaming object": "جارٍ إعادة تسمية الكائن", + "Filter loaded objects": "Filter loaded objects", + "Loaded {count} objects": "Loaded {count} objects", + "Filtering and sorting apply to loaded objects": "Filtering and sorting apply to loaded objects", + "Loading more objects": "Loading more objects", + "Scroll to load more objects": "Scroll to load more objects", + "Back to top": "Back to top", + "Go to bottom": "Go to bottom" } diff --git a/i18n/locales/de-DE.json b/i18n/locales/de-DE.json index 3e4ba7d..39dbed0 100644 --- a/i18n/locales/de-DE.json +++ b/i18n/locales/de-DE.json @@ -1360,5 +1360,12 @@ "Rename": "Umbenennen", "Rename Failed": "Umbenennen fehlgeschlagen", "Rename Object": "Objekt umbenennen", - "Renaming object": "Objekt wird umbenannt" + "Renaming object": "Objekt wird umbenannt", + "Filter loaded objects": "Filter loaded objects", + "Loaded {count} objects": "Loaded {count} objects", + "Filtering and sorting apply to loaded objects": "Filtering and sorting apply to loaded objects", + "Loading more objects": "Loading more objects", + "Scroll to load more objects": "Scroll to load more objects", + "Back to top": "Back to top", + "Go to bottom": "Go to bottom" } diff --git a/i18n/locales/en-US.json b/i18n/locales/en-US.json index 8b39630..0aee5fb 100644 --- a/i18n/locales/en-US.json +++ b/i18n/locales/en-US.json @@ -1360,5 +1360,12 @@ "Rename": "Rename", "Rename Failed": "Rename Failed", "Rename Object": "Rename Object", - "Renaming object": "Renaming object" + "Renaming object": "Renaming object", + "Filter loaded objects": "Filter loaded objects", + "Loaded {count} objects": "Loaded {count} objects", + "Filtering and sorting apply to loaded objects": "Filtering and sorting apply to loaded objects", + "Loading more objects": "Loading more objects", + "Scroll to load more objects": "Scroll to load more objects", + "Back to top": "Back to top", + "Go to bottom": "Go to bottom" } diff --git a/i18n/locales/es-ES.json b/i18n/locales/es-ES.json index d3b2f36..8e7468f 100644 --- a/i18n/locales/es-ES.json +++ b/i18n/locales/es-ES.json @@ -1360,5 +1360,12 @@ "Rename": "Renombrar", "Rename Failed": "Error al renombrar", "Rename Object": "Renombrar objeto", - "Renaming object": "Renombrando objeto" + "Renaming object": "Renombrando objeto", + "Filter loaded objects": "Filter loaded objects", + "Loaded {count} objects": "Loaded {count} objects", + "Filtering and sorting apply to loaded objects": "Filtering and sorting apply to loaded objects", + "Loading more objects": "Loading more objects", + "Scroll to load more objects": "Scroll to load more objects", + "Back to top": "Back to top", + "Go to bottom": "Go to bottom" } diff --git a/i18n/locales/fr-FR.json b/i18n/locales/fr-FR.json index 076c944..e02eaaa 100644 --- a/i18n/locales/fr-FR.json +++ b/i18n/locales/fr-FR.json @@ -1360,5 +1360,12 @@ "Rename": "Renommer", "Rename Failed": "Échec du renommage", "Rename Object": "Renommer l’objet", - "Renaming object": "Renommage de l’objet" + "Renaming object": "Renommage de l’objet", + "Filter loaded objects": "Filter loaded objects", + "Loaded {count} objects": "Loaded {count} objects", + "Filtering and sorting apply to loaded objects": "Filtering and sorting apply to loaded objects", + "Loading more objects": "Loading more objects", + "Scroll to load more objects": "Scroll to load more objects", + "Back to top": "Back to top", + "Go to bottom": "Go to bottom" } diff --git a/i18n/locales/id-ID.json b/i18n/locales/id-ID.json index a1df89e..dcd2345 100644 --- a/i18n/locales/id-ID.json +++ b/i18n/locales/id-ID.json @@ -1360,5 +1360,12 @@ "Rename": "Ganti nama", "Rename Failed": "Gagal mengganti nama", "Rename Object": "Ganti nama objek", - "Renaming object": "Mengganti nama objek" + "Renaming object": "Mengganti nama objek", + "Filter loaded objects": "Filter loaded objects", + "Loaded {count} objects": "Loaded {count} objects", + "Filtering and sorting apply to loaded objects": "Filtering and sorting apply to loaded objects", + "Loading more objects": "Loading more objects", + "Scroll to load more objects": "Scroll to load more objects", + "Back to top": "Back to top", + "Go to bottom": "Go to bottom" } diff --git a/i18n/locales/it-IT.json b/i18n/locales/it-IT.json index 14259a1..91a6d2e 100644 --- a/i18n/locales/it-IT.json +++ b/i18n/locales/it-IT.json @@ -1360,5 +1360,12 @@ "Rename": "Rinomina", "Rename Failed": "Rinomina non riuscita", "Rename Object": "Rinomina oggetto", - "Renaming object": "Rinomina oggetto in corso" + "Renaming object": "Rinomina oggetto in corso", + "Filter loaded objects": "Filter loaded objects", + "Loaded {count} objects": "Loaded {count} objects", + "Filtering and sorting apply to loaded objects": "Filtering and sorting apply to loaded objects", + "Loading more objects": "Loading more objects", + "Scroll to load more objects": "Scroll to load more objects", + "Back to top": "Back to top", + "Go to bottom": "Go to bottom" } diff --git a/i18n/locales/ja-JP.json b/i18n/locales/ja-JP.json index f6feb99..bb5e7af 100644 --- a/i18n/locales/ja-JP.json +++ b/i18n/locales/ja-JP.json @@ -1360,5 +1360,12 @@ "Rename": "名前を変更", "Rename Failed": "名前の変更に失敗しました", "Rename Object": "オブジェクト名を変更", - "Renaming object": "オブジェクト名を変更中" + "Renaming object": "オブジェクト名を変更中", + "Filter loaded objects": "Filter loaded objects", + "Loaded {count} objects": "Loaded {count} objects", + "Filtering and sorting apply to loaded objects": "Filtering and sorting apply to loaded objects", + "Loading more objects": "Loading more objects", + "Scroll to load more objects": "Scroll to load more objects", + "Back to top": "Back to top", + "Go to bottom": "Go to bottom" } diff --git a/i18n/locales/ko-KR.json b/i18n/locales/ko-KR.json index f6eb270..76c4a19 100644 --- a/i18n/locales/ko-KR.json +++ b/i18n/locales/ko-KR.json @@ -1360,5 +1360,12 @@ "Rename": "이름 변경", "Rename Failed": "이름 변경 실패", "Rename Object": "객체 이름 변경", - "Renaming object": "객체 이름 변경 중" + "Renaming object": "객체 이름 변경 중", + "Filter loaded objects": "Filter loaded objects", + "Loaded {count} objects": "Loaded {count} objects", + "Filtering and sorting apply to loaded objects": "Filtering and sorting apply to loaded objects", + "Loading more objects": "Loading more objects", + "Scroll to load more objects": "Scroll to load more objects", + "Back to top": "Back to top", + "Go to bottom": "Go to bottom" } diff --git a/i18n/locales/pt-BR.json b/i18n/locales/pt-BR.json index a7dd421..f13a1ff 100644 --- a/i18n/locales/pt-BR.json +++ b/i18n/locales/pt-BR.json @@ -1360,5 +1360,12 @@ "Rename": "Renomear", "Rename Failed": "Falha ao renomear", "Rename Object": "Renomear objeto", - "Renaming object": "Renomeando objeto" + "Renaming object": "Renomeando objeto", + "Filter loaded objects": "Filter loaded objects", + "Loaded {count} objects": "Loaded {count} objects", + "Filtering and sorting apply to loaded objects": "Filtering and sorting apply to loaded objects", + "Loading more objects": "Loading more objects", + "Scroll to load more objects": "Scroll to load more objects", + "Back to top": "Back to top", + "Go to bottom": "Go to bottom" } diff --git a/i18n/locales/ru-RU.json b/i18n/locales/ru-RU.json index db9c001..30c370c 100644 --- a/i18n/locales/ru-RU.json +++ b/i18n/locales/ru-RU.json @@ -1360,5 +1360,12 @@ "Rename": "Переименовать", "Rename Failed": "Не удалось переименовать", "Rename Object": "Переименовать объект", - "Renaming object": "Переименование объекта" + "Renaming object": "Переименование объекта", + "Filter loaded objects": "Filter loaded objects", + "Loaded {count} objects": "Loaded {count} objects", + "Filtering and sorting apply to loaded objects": "Filtering and sorting apply to loaded objects", + "Loading more objects": "Loading more objects", + "Scroll to load more objects": "Scroll to load more objects", + "Back to top": "Back to top", + "Go to bottom": "Go to bottom" } diff --git a/i18n/locales/tr-TR.json b/i18n/locales/tr-TR.json index d6993f0..7d8ef16 100644 --- a/i18n/locales/tr-TR.json +++ b/i18n/locales/tr-TR.json @@ -1360,5 +1360,12 @@ "Rename": "Yeniden adlandır", "Rename Failed": "Yeniden adlandırma başarısız", "Rename Object": "Nesneyi yeniden adlandır", - "Renaming object": "Nesne yeniden adlandırılıyor" + "Renaming object": "Nesne yeniden adlandırılıyor", + "Filter loaded objects": "Filter loaded objects", + "Loaded {count} objects": "Loaded {count} objects", + "Filtering and sorting apply to loaded objects": "Filtering and sorting apply to loaded objects", + "Loading more objects": "Loading more objects", + "Scroll to load more objects": "Scroll to load more objects", + "Back to top": "Back to top", + "Go to bottom": "Go to bottom" } diff --git a/i18n/locales/vi-VN.json b/i18n/locales/vi-VN.json index f0cd729..af5cb30 100644 --- a/i18n/locales/vi-VN.json +++ b/i18n/locales/vi-VN.json @@ -1360,5 +1360,12 @@ "Rename": "Đổi tên", "Rename Failed": "Đổi tên thất bại", "Rename Object": "Đổi tên đối tượng", - "Renaming object": "Đang đổi tên đối tượng" + "Renaming object": "Đang đổi tên đối tượng", + "Filter loaded objects": "Filter loaded objects", + "Loaded {count} objects": "Loaded {count} objects", + "Filtering and sorting apply to loaded objects": "Filtering and sorting apply to loaded objects", + "Loading more objects": "Loading more objects", + "Scroll to load more objects": "Scroll to load more objects", + "Back to top": "Back to top", + "Go to bottom": "Go to bottom" } diff --git a/i18n/locales/zh-CN.json b/i18n/locales/zh-CN.json index f1323e5..24f53c9 100644 --- a/i18n/locales/zh-CN.json +++ b/i18n/locales/zh-CN.json @@ -1360,5 +1360,12 @@ "Rename": "重命名", "Rename Failed": "重命名失败", "Rename Object": "重命名对象", - "Renaming object": "正在重命名对象" + "Renaming object": "正在重命名对象", + "Filter loaded objects": "筛选已加载对象", + "Loaded {count} objects": "已加载 {count} 个对象", + "Filtering and sorting apply to loaded objects": "筛选和排序作用于已加载对象", + "Loading more objects": "正在加载更多对象", + "Scroll to load more objects": "滚动以加载更多对象", + "Back to top": "回到顶部", + "Go to bottom": "直达底部" } diff --git a/lib/object-list-pagination.js b/lib/object-list-pagination.js index a3b525a..4ffb619 100644 --- a/lib/object-list-pagination.js +++ b/lib/object-list-pagination.js @@ -1,5 +1 @@ -export { - OBJECT_LIST_DEFAULT_PAGE_SIZE, - OBJECT_LIST_PAGE_SIZE_OPTIONS, - normalizeObjectListPageSize, -} from "./object-list-pagination.ts" +export { OBJECT_LIST_DEFAULT_PAGE_SIZE, resolveObjectListPageSize } from "./object-list-pagination.ts" diff --git a/lib/object-list-pagination.ts b/lib/object-list-pagination.ts index 8777a4d..a554a23 100644 --- a/lib/object-list-pagination.ts +++ b/lib/object-list-pagination.ts @@ -1,12 +1,8 @@ -export const OBJECT_LIST_DEFAULT_PAGE_SIZE = 100 +export const OBJECT_LIST_DEFAULT_PAGE_SIZE = 1000 -export const OBJECT_LIST_PAGE_SIZE_OPTIONS = [25, 50, 100, 500, 1000] as const - -export type ObjectListPageSize = (typeof OBJECT_LIST_PAGE_SIZE_OPTIONS)[number] - -export function normalizeObjectListPageSize(value: unknown): ObjectListPageSize { - if (typeof value === "number" && OBJECT_LIST_PAGE_SIZE_OPTIONS.includes(value as ObjectListPageSize)) { - return value as ObjectListPageSize +export function resolveObjectListPageSize(value: unknown): number { + if (typeof value === "number" && Number.isInteger(value) && value > 0) { + return value } return OBJECT_LIST_DEFAULT_PAGE_SIZE diff --git a/tests/lib/object-list-pagination.test.ts b/tests/lib/object-list-pagination.test.ts index 6475194..dfdb9c6 100644 --- a/tests/lib/object-list-pagination.test.ts +++ b/tests/lib/object-list-pagination.test.ts @@ -1,26 +1,19 @@ import test from "node:test" import assert from "node:assert/strict" -import { - OBJECT_LIST_DEFAULT_PAGE_SIZE, - OBJECT_LIST_PAGE_SIZE_OPTIONS, - normalizeObjectListPageSize, -} from "../../lib/object-list-pagination.js" +import { OBJECT_LIST_DEFAULT_PAGE_SIZE, resolveObjectListPageSize } from "../../lib/object-list-pagination.js" -test("object list pagination defaults to 100 keys per page", () => { - assert.equal(OBJECT_LIST_DEFAULT_PAGE_SIZE, 100) +test("object list pagination defaults to 1000 keys per request", () => { + assert.equal(OBJECT_LIST_DEFAULT_PAGE_SIZE, 1000) }) -test("object list pagination supports S3-safe page size options", () => { - assert.deepEqual([...OBJECT_LIST_PAGE_SIZE_OPTIONS], [25, 50, 100, 500, 1000]) +test("resolveObjectListPageSize keeps valid positive integer values", () => { + assert.equal(resolveObjectListPageSize(500), 500) + assert.equal(resolveObjectListPageSize(1000), 1000) }) -test("normalizeObjectListPageSize keeps supported values", () => { - assert.equal(normalizeObjectListPageSize(25), 25) - assert.equal(normalizeObjectListPageSize(1000), 1000) -}) - -test("normalizeObjectListPageSize falls back for unsupported values", () => { - assert.equal(normalizeObjectListPageSize(10), OBJECT_LIST_DEFAULT_PAGE_SIZE) - assert.equal(normalizeObjectListPageSize("100"), OBJECT_LIST_DEFAULT_PAGE_SIZE) - assert.equal(normalizeObjectListPageSize(undefined), OBJECT_LIST_DEFAULT_PAGE_SIZE) +test("resolveObjectListPageSize falls back for invalid values", () => { + assert.equal(resolveObjectListPageSize(0), OBJECT_LIST_DEFAULT_PAGE_SIZE) + assert.equal(resolveObjectListPageSize(1.5), OBJECT_LIST_DEFAULT_PAGE_SIZE) + assert.equal(resolveObjectListPageSize("1000"), OBJECT_LIST_DEFAULT_PAGE_SIZE) + assert.equal(resolveObjectListPageSize(undefined), OBJECT_LIST_DEFAULT_PAGE_SIZE) }) diff --git a/tests/lib/object-list-source.test.js b/tests/lib/object-list-source.test.js index e6aba2b..78e3b88 100644 --- a/tests/lib/object-list-source.test.js +++ b/tests/lib/object-list-source.test.js @@ -17,3 +17,29 @@ test("object list falls back to an empty table instead of crashing the page on f assert.equal(source.includes('message.error((error as Error)?.message ?? t("Failed to load objects"))'), true) assert.equal(source.includes("setData([])"), true) }) + +test("object list lazy loads additional object batches instead of showing a paginator", () => { + const source = fs.readFileSync("components/object/list.tsx", "utf8") + + assert.equal(source.includes("IntersectionObserver"), true) + assert.equal(source.includes("setData((currentRows) => [...currentRows, ...rows])"), true) + assert.equal(source.includes('t("Rows per page")'), false) + assert.equal(source.includes('t("Previous Page")'), false) + assert.equal(source.includes('t("Next Page")'), false) +}) + +test("object list shows fixed scroll shortcut buttons only when content overflows", () => { + const source = fs.readFileSync("components/object/list.tsx", "utf8") + + assert.equal(source.includes("RiArrowUpSLine"), true) + assert.equal(source.includes("RiArrowDownSLine"), true) + assert.equal( + source.includes("setShowScrollShortcuts(document.documentElement.scrollHeight > window.innerHeight)"), + true, + ) + assert.equal(source.includes('window.scrollTo({ top: 0, left: 0, behavior: "auto" })'), true) + assert.equal( + source.includes('window.scrollTo({ top: document.documentElement.scrollHeight, left: 0, behavior: "auto" })'), + true, + ) +})