mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-20 14:30:41 +00:00
feat(templates): use server-side pagination and search for cloud
Replace loading all hub workflows at once with cursor-based pagination. The store now loads one page at a time and fetches more on scroll via the intersection observer. Search on cloud delegates to the API search param instead of client-side Fuse.js, resetting loaded data with server-filtered results.
This commit is contained in:
@@ -339,7 +339,7 @@
|
||||
|
||||
<!-- Loading More Skeletons -->
|
||||
<CardContainer
|
||||
v-for="n in isLoadingMore ? 6 : 0"
|
||||
v-for="n in effectiveIsLoading ? 6 : 0"
|
||||
:key="`skeleton-${n}`"
|
||||
size="compact"
|
||||
variant="ghost"
|
||||
@@ -371,11 +371,11 @@
|
||||
|
||||
<!-- Load More Trigger -->
|
||||
<div
|
||||
v-if="!isLoading && hasMoreTemplates"
|
||||
v-if="!isLoading && effectiveHasMore"
|
||||
ref="loadTrigger"
|
||||
class="mt-4 flex h-4 w-full items-center justify-center"
|
||||
>
|
||||
<div v-if="isLoadingMore" class="text-sm text-muted">
|
||||
<div v-if="effectiveIsLoading" class="text-sm text-muted">
|
||||
{{ $t('templateWorkflows.loadingMore', 'Loading more...') }}
|
||||
</div>
|
||||
</div>
|
||||
@@ -769,13 +769,35 @@ const {
|
||||
|
||||
// Display templates (all when searching, paginated when not)
|
||||
const displayTemplates = computed(() => {
|
||||
if (isCloud) {
|
||||
// On cloud, all loaded templates are displayed (server handles pagination)
|
||||
return filteredTemplates.value
|
||||
}
|
||||
return shouldUsePagination.value
|
||||
? paginatedTemplates.value
|
||||
: filteredTemplates.value
|
||||
})
|
||||
|
||||
// Effective "has more" and "is loading" that unify client/server pagination
|
||||
const effectiveHasMore = computed(() =>
|
||||
isCloud ? workflowTemplatesStore.hubHasMore : hasMoreTemplates.value
|
||||
)
|
||||
const effectiveIsLoading = computed(() =>
|
||||
isCloud ? workflowTemplatesStore.hubIsLoadingPage : isLoadingMore.value
|
||||
)
|
||||
|
||||
// Set up intersection observer for lazy loading
|
||||
useIntersectionObserver(loadTrigger, () => {
|
||||
if (isCloud) {
|
||||
if (
|
||||
workflowTemplatesStore.hubHasMore &&
|
||||
!workflowTemplatesStore.hubIsLoadingPage
|
||||
) {
|
||||
void workflowTemplatesStore.loadHubNextPage()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (
|
||||
shouldUsePagination.value &&
|
||||
hasMoreTemplates.value &&
|
||||
|
||||
@@ -4,9 +4,11 @@ import type { IFuseOptions } from 'fuse.js'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import type { Ref } from 'vue'
|
||||
|
||||
import { isCloud } from '@/platform/distribution/types'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { useTelemetry } from '@/platform/telemetry'
|
||||
import type { TemplateInfo } from '@/platform/workflow/templates/types/template'
|
||||
import { useWorkflowTemplatesStore } from '@/platform/workflow/templates/repositories/workflowTemplatesStore'
|
||||
import { useTemplateRankingStore } from '@/stores/templateRankingStore'
|
||||
import { debounce } from 'es-toolkit/compat'
|
||||
import { api } from '@/scripts/api'
|
||||
@@ -121,11 +123,24 @@ export function useTemplateFiltering(
|
||||
|
||||
const debouncedSearchQuery = refDebounced(searchQuery, 150)
|
||||
|
||||
// On cloud, delegate search to the hub API instead of Fuse.js
|
||||
if (isCloud) {
|
||||
const workflowTemplatesStore = useWorkflowTemplatesStore()
|
||||
watch(debouncedSearchQuery, (query) => {
|
||||
void workflowTemplatesStore.searchHubWorkflows(query.trim())
|
||||
})
|
||||
}
|
||||
|
||||
const filteredBySearch = computed(() => {
|
||||
if (!debouncedSearchQuery.value.trim()) {
|
||||
return templatesArray.value
|
||||
}
|
||||
|
||||
// On cloud, search is handled server-side — templates are already filtered
|
||||
if (isCloud) {
|
||||
return templatesArray.value
|
||||
}
|
||||
|
||||
const results = fuse.value.search(debouncedSearchQuery.value)
|
||||
return results.map((result) => result.item)
|
||||
})
|
||||
|
||||
@@ -8,7 +8,12 @@ import type { NavGroupData, NavItemData } from '@/types/navTypes'
|
||||
import { generateCategoryId, getCategoryIcon } from '@/utils/categoryUtil'
|
||||
import { normalizeI18nKey } from '@/utils/formatUtil'
|
||||
|
||||
import { adaptHubWorkflowsToCategories } from '../adapters/hubTemplateAdapter'
|
||||
import type { HubWorkflowSummary } from '@comfyorg/ingest-types'
|
||||
|
||||
import {
|
||||
adaptHubWorkflowsToCategories,
|
||||
adaptHubWorkflowToTemplate
|
||||
} from '../adapters/hubTemplateAdapter'
|
||||
import { zLogoIndex } from '../schemas/templateSchema'
|
||||
import type { LogoIndex } from '../schemas/templateSchema'
|
||||
import type {
|
||||
@@ -39,6 +44,12 @@ export const useWorkflowTemplatesStore = defineStore(
|
||||
const isLoaded = ref(false)
|
||||
const knownTemplateNames = ref(new Set<string>())
|
||||
|
||||
// Hub pagination state (cloud only)
|
||||
const hubNextCursor = ref<string | undefined>()
|
||||
const hubHasMore = ref(false)
|
||||
const hubIsLoadingPage = ref(false)
|
||||
const hubSearchQuery = ref('')
|
||||
|
||||
const getTemplateByName = (name: string): EnhancedTemplate | undefined => {
|
||||
return enhancedTemplates.value.find((template) => template.name === name)
|
||||
}
|
||||
@@ -482,18 +493,111 @@ export const useWorkflowTemplatesStore = defineStore(
|
||||
return items
|
||||
})
|
||||
|
||||
async function fetchCoreTemplates() {
|
||||
if (isCloud) {
|
||||
const summaries = await api.listAllHubWorkflows()
|
||||
/**
|
||||
* Appends hub workflow summaries to the existing coreTemplates.
|
||||
*/
|
||||
function appendHubWorkflows(summaries: HubWorkflowSummary[]) {
|
||||
const newTemplates = summaries.map(adaptHubWorkflowToTemplate)
|
||||
const existing = coreTemplates.value[0]
|
||||
if (existing?.moduleName === 'hub') {
|
||||
// Append to existing hub category
|
||||
coreTemplates.value = [
|
||||
{
|
||||
...existing,
|
||||
templates: [...existing.templates, ...newTemplates]
|
||||
}
|
||||
]
|
||||
} else {
|
||||
coreTemplates.value = adaptHubWorkflowsToCategories(summaries)
|
||||
// Hub templates use absolute thumbnail URLs — no logo index needed
|
||||
// Hub has no i18n variant — skip english templates fetch
|
||||
}
|
||||
|
||||
const coreNames = coreTemplates.value.flatMap((category) =>
|
||||
category.templates.map((template) => template.name)
|
||||
)
|
||||
const customNames = Object.values(customTemplates.value).flat()
|
||||
knownTemplateNames.value = new Set([...coreNames, ...customNames])
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the next page of hub workflows (cloud only).
|
||||
*/
|
||||
async function loadHubNextPage() {
|
||||
if (!isCloud || !hubHasMore.value || hubIsLoadingPage.value) return
|
||||
|
||||
hubIsLoadingPage.value = true
|
||||
try {
|
||||
const page = await api.fetchHubWorkflowPage({
|
||||
limit: 20,
|
||||
cursor: hubNextCursor.value,
|
||||
search: hubSearchQuery.value || undefined
|
||||
})
|
||||
appendHubWorkflows(page.workflows as HubWorkflowSummary[])
|
||||
hubNextCursor.value = page.next_cursor || undefined
|
||||
hubHasMore.value = !!page.next_cursor
|
||||
} catch (error) {
|
||||
console.error('Error loading next hub page:', error)
|
||||
} finally {
|
||||
hubIsLoadingPage.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Searches hub workflows via API (cloud only). Resets loaded data.
|
||||
*/
|
||||
async function searchHubWorkflows(query: string) {
|
||||
if (!isCloud) return
|
||||
|
||||
hubSearchQuery.value = query
|
||||
hubNextCursor.value = undefined
|
||||
hubIsLoadingPage.value = true
|
||||
|
||||
try {
|
||||
const page = await api.fetchHubWorkflowPage({
|
||||
limit: 20,
|
||||
search: query || undefined
|
||||
})
|
||||
// Replace all templates with search results
|
||||
coreTemplates.value = adaptHubWorkflowsToCategories(
|
||||
page.workflows as HubWorkflowSummary[]
|
||||
)
|
||||
hubNextCursor.value = page.next_cursor || undefined
|
||||
hubHasMore.value = !!page.next_cursor
|
||||
|
||||
const coreNames = coreTemplates.value.flatMap((category) =>
|
||||
category.templates.map((template) => template.name)
|
||||
)
|
||||
const customNames = Object.values(customTemplates.value).flat()
|
||||
knownTemplateNames.value = new Set([...coreNames, ...customNames])
|
||||
} catch (error) {
|
||||
console.error('Error searching hub workflows:', error)
|
||||
} finally {
|
||||
hubIsLoadingPage.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchCoreTemplates() {
|
||||
if (isCloud) {
|
||||
// Load first page only — subsequent pages loaded via loadHubNextPage()
|
||||
hubSearchQuery.value = ''
|
||||
hubNextCursor.value = undefined
|
||||
hubIsLoadingPage.value = true
|
||||
|
||||
try {
|
||||
const page = await api.fetchHubWorkflowPage({ limit: 20 })
|
||||
coreTemplates.value = adaptHubWorkflowsToCategories(
|
||||
page.workflows as HubWorkflowSummary[]
|
||||
)
|
||||
hubNextCursor.value = page.next_cursor || undefined
|
||||
hubHasMore.value = !!page.next_cursor
|
||||
|
||||
const coreNames = coreTemplates.value.flatMap((category) =>
|
||||
category.templates.map((template) => template.name)
|
||||
)
|
||||
const customNames = Object.values(customTemplates.value).flat()
|
||||
knownTemplateNames.value = new Set([...coreNames, ...customNames])
|
||||
} finally {
|
||||
hubIsLoadingPage.value = false
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -609,7 +713,12 @@ export const useWorkflowTemplatesStore = defineStore(
|
||||
getTemplateByName,
|
||||
getTemplateByShareId,
|
||||
getEnglishMetadata,
|
||||
getLogoUrl
|
||||
getLogoUrl,
|
||||
// Hub pagination (cloud only)
|
||||
hubHasMore,
|
||||
hubIsLoadingPage,
|
||||
loadHubNextPage,
|
||||
searchHubWorkflows
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
@@ -62,8 +62,7 @@ import type {
|
||||
import type { ComfyNodeDef } from '@/schemas/nodeDefSchema'
|
||||
import type {
|
||||
HubWorkflowDetail,
|
||||
HubWorkflowListResponse,
|
||||
HubWorkflowSummary
|
||||
HubWorkflowListResponse
|
||||
} from '@comfyorg/ingest-types'
|
||||
import {
|
||||
zHubWorkflowDetail,
|
||||
@@ -837,16 +836,17 @@ export class ComfyApi extends EventTarget {
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches a single page of hub workflows. Only pagination params (limit, cursor)
|
||||
* are sent — no filtering is applied so the caller always gets the full list.
|
||||
* Fetches a single page of hub workflows with optional search.
|
||||
*/
|
||||
private async fetchHubWorkflowPage(
|
||||
limit: number,
|
||||
async fetchHubWorkflowPage(params?: {
|
||||
limit?: number
|
||||
cursor?: string
|
||||
): Promise<HubWorkflowListResponse> {
|
||||
search?: string
|
||||
}): Promise<HubWorkflowListResponse> {
|
||||
const query = new URLSearchParams()
|
||||
query.set('limit', String(limit))
|
||||
if (cursor) query.set('cursor', cursor)
|
||||
query.set('limit', String(params?.limit ?? 20))
|
||||
if (params?.cursor) query.set('cursor', params.cursor)
|
||||
if (params?.search) query.set('search', params.search)
|
||||
// TODO: Remove after production has approved data — fetch all statuses for testing
|
||||
query.set('status', 'pending,approved,rejected,deprecated')
|
||||
const res = await this.fetchApi(`/hub/workflows?${query.toString()}`)
|
||||
@@ -861,20 +861,6 @@ export class ComfyApi extends EventTarget {
|
||||
return parsed.data
|
||||
}
|
||||
|
||||
/**
|
||||
* Lists all hub workflows by paginating through all pages.
|
||||
*/
|
||||
async listAllHubWorkflows(): Promise<HubWorkflowSummary[]> {
|
||||
const all: HubWorkflowSummary[] = []
|
||||
let cursor: string | undefined
|
||||
do {
|
||||
const page = await this.fetchHubWorkflowPage(100, cursor)
|
||||
all.push(...(page.workflows as HubWorkflowSummary[]))
|
||||
cursor = page.next_cursor || undefined
|
||||
} while (cursor)
|
||||
return all
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets full details of a hub workflow including workflow JSON.
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user