mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-21 14:59:39 +00:00
feat: Add pagination support for media assets history (#6373)
## Summary - Implement pagination for media assets history to handle large datasets efficiently - Add infinite scroll support with approach-end event handler - Support offset parameter in history API for both V1 and V2 endpoints ## Changes - Add offset parameter support to `api.getHistory()` method - Update history fetchers (V1/V2) to include offset in API requests - Implement `loadMoreHistory()` in assetsStore with pagination state management - Add `loadMore`, `hasMore`, and `isLoadingMore` to IAssetsProvider interface - Add approach-end handler in AssetsSidebarTab for infinite scroll - Set BATCH_SIZE to 200 for efficient loading ## Implementation Improvements Simplified offset-based pagination by removing unnecessary reconciliation logic: - Remove `reconcileHistory`, `taskItemsMap`, `lastKnownQueueIndex` (offset is sufficient) - Replace `assetItemsByPromptId` Map → `loadedIds` Set (store IDs only) - Replace `findInsertionIndex` binary search → push + sort (faster for batch operations) - Replace `loadingPromise` → `isLoadingMore` boolean (simpler state management) - Fix memory leak by cleaning up Set together with array slice ## Test Plan - [x] TypeScript compilation passes - [x] ESLint and Prettier formatting applied - [x] Test infinite scroll in media assets tab - [x] Verify network requests include correct offset parameter - [x] Confirm no duplicate items when loading more 🤖 Generated with [Claude Code](https://claude.ai/code) --------- Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,11 @@
|
||||
<template>
|
||||
<div class="flex flex-col items-center gap-1">
|
||||
<MediaTitle :file-name="fileName" />
|
||||
<div class="flex items-center gap-2 text-xs text-zinc-400">
|
||||
<!-- TBD: File size will be provided by backend history API -->
|
||||
<div
|
||||
v-if="asset.size"
|
||||
class="flex items-center gap-2 text-xs text-zinc-400"
|
||||
>
|
||||
<span>{{ formatSize(asset.size) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
<template>
|
||||
<div class="flex flex-col items-center gap-1">
|
||||
<MediaTitle :file-name="fileName" />
|
||||
<div class="flex items-center gap-2 text-xs text-zinc-400">
|
||||
<!-- TBD: File size will be provided by backend history API -->
|
||||
<div
|
||||
v-if="asset.size"
|
||||
class="flex items-center gap-2 text-xs text-zinc-400"
|
||||
>
|
||||
<span>{{ formatSize(asset.size) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
<template>
|
||||
<div class="flex flex-col items-center gap-1">
|
||||
<MediaTitle :file-name="fileName" />
|
||||
<div class="flex items-center text-xs text-zinc-400">
|
||||
<!-- TBD: File size will be provided by backend history API -->
|
||||
<div v-if="asset.size" class="flex items-center text-xs text-zinc-400">
|
||||
<span>{{ formatSize(asset.size) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -7,7 +7,11 @@
|
||||
<video
|
||||
ref="videoRef"
|
||||
:controls="shouldShowControls"
|
||||
preload="none"
|
||||
preload="metadata"
|
||||
autoplay
|
||||
muted
|
||||
loop
|
||||
playsinline
|
||||
:poster="asset.preview_url"
|
||||
class="relative size-full object-contain"
|
||||
@click.stop
|
||||
|
||||
@@ -26,4 +26,19 @@ export interface IAssetsProvider {
|
||||
* Refresh the media list (alias for fetchMediaList)
|
||||
*/
|
||||
refresh: () => Promise<AssetItem[]>
|
||||
|
||||
/**
|
||||
* Load more items (for pagination)
|
||||
*/
|
||||
loadMore: () => Promise<void>
|
||||
|
||||
/**
|
||||
* Whether there are more items to load
|
||||
*/
|
||||
hasMore: Ref<boolean>
|
||||
|
||||
/**
|
||||
* Whether currently loading more items
|
||||
*/
|
||||
isLoadingMore: Ref<boolean>
|
||||
}
|
||||
|
||||
@@ -36,11 +36,28 @@ export function useAssetsApi(directory: 'input' | 'output') {
|
||||
|
||||
const refresh = () => fetchMediaList()
|
||||
|
||||
const loadMore = async (): Promise<void> => {
|
||||
if (directory === 'output') {
|
||||
await assetsStore.loadMoreHistory()
|
||||
}
|
||||
}
|
||||
|
||||
const hasMore = computed(() => {
|
||||
return directory === 'output' ? assetsStore.hasMoreHistory : false
|
||||
})
|
||||
|
||||
const isLoadingMore = computed(() => {
|
||||
return directory === 'output' ? assetsStore.isLoadingMore : false
|
||||
})
|
||||
|
||||
return {
|
||||
media,
|
||||
loading,
|
||||
error,
|
||||
fetchMediaList,
|
||||
refresh
|
||||
refresh,
|
||||
loadMore,
|
||||
hasMore,
|
||||
isLoadingMore
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,11 +36,28 @@ export function useInternalFilesApi(directory: 'input' | 'output') {
|
||||
|
||||
const refresh = () => fetchMediaList()
|
||||
|
||||
const loadMore = async (): Promise<void> => {
|
||||
if (directory === 'output') {
|
||||
await assetsStore.loadMoreHistory()
|
||||
}
|
||||
}
|
||||
|
||||
const hasMore = computed(() => {
|
||||
return directory === 'output' ? assetsStore.hasMoreHistory : false
|
||||
})
|
||||
|
||||
const isLoadingMore = computed(() => {
|
||||
return directory === 'output' ? assetsStore.isLoadingMore : false
|
||||
})
|
||||
|
||||
return {
|
||||
media,
|
||||
loading,
|
||||
error,
|
||||
fetchMediaList,
|
||||
refresh
|
||||
refresh,
|
||||
loadMore,
|
||||
hasMore,
|
||||
isLoadingMore
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ const zAsset = z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
asset_hash: z.string().nullish(),
|
||||
size: z.number(),
|
||||
size: z.number().optional(), // TBD: Will be provided by history API in the future
|
||||
mime_type: z.string().nullish(),
|
||||
tags: z.array(z.string()).optional().default([]),
|
||||
preview_id: z.string().nullable().optional(),
|
||||
|
||||
@@ -15,13 +15,28 @@ import type {
|
||||
* Fetches history from V1 API endpoint
|
||||
* @param api - API instance with fetchApi method
|
||||
* @param maxItems - Maximum number of history items to fetch
|
||||
* @param offset - Offset for pagination (must be non-negative integer)
|
||||
* @returns Promise resolving to V1 history response
|
||||
* @throws Error if offset is invalid (negative or non-integer)
|
||||
*/
|
||||
export async function fetchHistoryV1(
|
||||
fetchApi: (url: string) => Promise<Response>,
|
||||
maxItems: number = 200
|
||||
maxItems: number = 200,
|
||||
offset?: number
|
||||
): Promise<HistoryV1Response> {
|
||||
const res = await fetchApi(`/history?max_items=${maxItems}`)
|
||||
// Validate offset parameter
|
||||
if (offset !== undefined && (offset < 0 || !Number.isInteger(offset))) {
|
||||
throw new Error(
|
||||
`Invalid offset parameter: ${offset}. Must be a non-negative integer.`
|
||||
)
|
||||
}
|
||||
|
||||
const params = new URLSearchParams({ max_items: maxItems.toString() })
|
||||
if (offset !== undefined) {
|
||||
params.set('offset', offset.toString())
|
||||
}
|
||||
const url = `/history?${params.toString()}`
|
||||
const res = await fetchApi(url)
|
||||
const json: Record<
|
||||
string,
|
||||
Omit<HistoryTaskItem, 'taskType'>
|
||||
|
||||
@@ -14,13 +14,28 @@ import type { HistoryResponseV2 } from '../types/historyV2Types'
|
||||
* Fetches history from V2 API endpoint and adapts to V1 format
|
||||
* @param fetchApi - API instance with fetchApi method
|
||||
* @param maxItems - Maximum number of history items to fetch
|
||||
* @param offset - Offset for pagination (must be non-negative integer)
|
||||
* @returns Promise resolving to V1 history response (adapted from V2)
|
||||
* @throws Error if offset is invalid (negative or non-integer)
|
||||
*/
|
||||
export async function fetchHistoryV2(
|
||||
fetchApi: (url: string) => Promise<Response>,
|
||||
maxItems: number = 200
|
||||
maxItems: number = 200,
|
||||
offset?: number
|
||||
): Promise<HistoryV1Response> {
|
||||
const res = await fetchApi(`/history_v2?max_items=${maxItems}`)
|
||||
// Validate offset parameter
|
||||
if (offset !== undefined && (offset < 0 || !Number.isInteger(offset))) {
|
||||
throw new Error(
|
||||
`Invalid offset parameter: ${offset}. Must be a non-negative integer.`
|
||||
)
|
||||
}
|
||||
|
||||
const params = new URLSearchParams({ max_items: maxItems.toString() })
|
||||
if (offset !== undefined) {
|
||||
params.set('offset', offset.toString())
|
||||
}
|
||||
const url = `/history_v2?${params.toString()}`
|
||||
const res = await fetchApi(url)
|
||||
const rawData: HistoryResponseV2 = await res.json()
|
||||
const adaptedHistory = mapHistoryV2toHistory(rawData)
|
||||
return { History: adaptedHistory }
|
||||
|
||||
Reference in New Issue
Block a user