mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-19 22:09:37 +00:00
feat(templates): group hub index entries by section metadata
Add section/sectionGroup fields to hub workflow index schema to enable category nav grouping on cloud. The mapper now groups entries by section (e.g. "Image", "Video") under sectionGroup headers (e.g. "GENERATION TYPE") instead of flattening everything into a single "All" category. Falls back to flat "All" when BE does not yet provide section metadata. TODO(hub-api): field names pending BE spec confirmation.
This commit is contained in:
@@ -108,21 +108,76 @@ describe('mapHubWorkflowIndexEntryToTemplate', () => {
|
||||
})
|
||||
|
||||
describe('mapHubWorkflowIndexToCategories', () => {
|
||||
it('wraps flat hub entries in a single default category', () => {
|
||||
const result = mapHubWorkflowIndexToCategories(
|
||||
[
|
||||
makeEntry({ name: 'template-a', title: 'Template A' }),
|
||||
makeEntry({ name: 'template-b', title: 'Template B' })
|
||||
],
|
||||
'All Templates'
|
||||
)
|
||||
it('groups entries by section and sectionGroup into WorkflowTemplates', () => {
|
||||
const result = mapHubWorkflowIndexToCategories([
|
||||
makeEntry({
|
||||
name: 'img-template',
|
||||
title: 'Image Template',
|
||||
section: 'Image',
|
||||
sectionGroup: 'GENERATION TYPE'
|
||||
}),
|
||||
makeEntry({
|
||||
name: 'vid-template',
|
||||
title: 'Video Template',
|
||||
section: 'Video',
|
||||
sectionGroup: 'GENERATION TYPE'
|
||||
}),
|
||||
makeEntry({
|
||||
name: 'img-template-2',
|
||||
title: 'Image Template 2',
|
||||
section: 'Image',
|
||||
sectionGroup: 'GENERATION TYPE'
|
||||
})
|
||||
])
|
||||
|
||||
expect(result).toHaveLength(2)
|
||||
|
||||
const imageCategory = result.find((c) => c.title === 'Image')!
|
||||
expect(imageCategory.moduleName).toBe('default')
|
||||
expect(imageCategory.category).toBe('GENERATION TYPE')
|
||||
expect(imageCategory.templates.map((t) => t.name)).toEqual([
|
||||
'img-template',
|
||||
'img-template-2'
|
||||
])
|
||||
|
||||
const videoCategory = result.find((c) => c.title === 'Video')!
|
||||
expect(videoCategory.moduleName).toBe('default')
|
||||
expect(videoCategory.category).toBe('GENERATION TYPE')
|
||||
expect(videoCategory.templates.map((t) => t.name)).toEqual(['vid-template'])
|
||||
})
|
||||
|
||||
it('falls back to a single "All" category when entries lack section metadata', () => {
|
||||
const result = mapHubWorkflowIndexToCategories([
|
||||
makeEntry({ name: 'template-a', title: 'Template A' }),
|
||||
makeEntry({ name: 'template-b', title: 'Template B' })
|
||||
])
|
||||
|
||||
expect(result).toHaveLength(1)
|
||||
expect(result[0].moduleName).toBe('default')
|
||||
expect(result[0].title).toBe('All Templates')
|
||||
expect(result[0].templates.map((template) => template.name)).toEqual([
|
||||
expect(result[0].title).toBe('All')
|
||||
expect(result[0].templates.map((t) => t.name)).toEqual([
|
||||
'template-a',
|
||||
'template-b'
|
||||
])
|
||||
})
|
||||
|
||||
it('propagates isEssential from entries to categories', () => {
|
||||
const result = mapHubWorkflowIndexToCategories([
|
||||
makeEntry({
|
||||
name: 'essential-1',
|
||||
section: 'Getting Started',
|
||||
sectionGroup: 'GENERATION TYPE',
|
||||
isEssential: true
|
||||
}),
|
||||
makeEntry({
|
||||
name: 'non-essential',
|
||||
section: 'Getting Started',
|
||||
sectionGroup: 'GENERATION TYPE',
|
||||
isEssential: false
|
||||
})
|
||||
])
|
||||
|
||||
const category = result.find((c) => c.title === 'Getting Started')!
|
||||
expect(category.isEssential).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -123,15 +123,46 @@ export function mapHubWorkflowIndexEntryToTemplate(
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(hub-api): Pending BE spec confirmation — field names (section/sectionGroup) may change.
|
||||
export function mapHubWorkflowIndexToCategories(
|
||||
entries: HubWorkflowIndexEntry[],
|
||||
title: string = 'All'
|
||||
entries: HubWorkflowIndexEntry[]
|
||||
): WorkflowTemplates[] {
|
||||
return [
|
||||
{
|
||||
moduleName: 'default',
|
||||
title,
|
||||
templates: entries.map(mapHubWorkflowIndexEntryToTemplate)
|
||||
const hasSections = entries.some((e) => e.section)
|
||||
|
||||
if (!hasSections) {
|
||||
return [
|
||||
{
|
||||
moduleName: 'default',
|
||||
title: 'All',
|
||||
templates: entries.map(mapHubWorkflowIndexEntryToTemplate)
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
const sectionMap = new Map<
|
||||
string,
|
||||
{ entries: HubWorkflowIndexEntry[]; sectionGroup?: string }
|
||||
>()
|
||||
|
||||
for (const entry of entries) {
|
||||
const key = entry.section ?? 'All'
|
||||
if (!sectionMap.has(key)) {
|
||||
sectionMap.set(key, { entries: [], sectionGroup: entry.sectionGroup })
|
||||
}
|
||||
]
|
||||
sectionMap.get(key)!.entries.push(entry)
|
||||
}
|
||||
|
||||
return Array.from(
|
||||
sectionMap,
|
||||
([section, { entries: sectionEntries, sectionGroup }]) => {
|
||||
const templates = sectionEntries.map(mapHubWorkflowIndexEntryToTemplate)
|
||||
return {
|
||||
moduleName: 'default',
|
||||
title: section,
|
||||
category: sectionGroup,
|
||||
isEssential: sectionEntries.some((e) => e.isEssential),
|
||||
templates
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -51,7 +51,29 @@ describe('workflowTemplatesStore', () => {
|
||||
usage: 10,
|
||||
searchRank: 5,
|
||||
isEssential: true,
|
||||
section: 'Use Cases',
|
||||
sectionGroup: 'GENERATION TYPE',
|
||||
thumbnailUrl: 'https://cdn.example.com/thumb.webp'
|
||||
},
|
||||
{
|
||||
name: 'image-gen',
|
||||
title: 'Image Generation',
|
||||
status: 'approved',
|
||||
description: 'Image generation workflow',
|
||||
shareId: 'share-456',
|
||||
section: 'Image',
|
||||
sectionGroup: 'GENERATION TYPE',
|
||||
thumbnailUrl: 'https://cdn.example.com/img.webp'
|
||||
},
|
||||
{
|
||||
name: 'video-gen',
|
||||
title: 'Video Generation',
|
||||
status: 'approved',
|
||||
description: 'Video generation workflow',
|
||||
shareId: 'share-789',
|
||||
section: 'Video',
|
||||
sectionGroup: 'GENERATION TYPE',
|
||||
thumbnailUrl: 'https://cdn.example.com/vid.webp'
|
||||
}
|
||||
])
|
||||
|
||||
@@ -77,24 +99,49 @@ describe('workflowTemplatesStore', () => {
|
||||
expect(store.knownTemplateNames.has('starter-template')).toBe(true)
|
||||
})
|
||||
|
||||
it('creates a generic getting started nav item for essential cloud templates', async () => {
|
||||
it('builds nav groups from section metadata on cloud templates', async () => {
|
||||
const store = useWorkflowTemplatesStore()
|
||||
|
||||
await store.loadWorkflowTemplates()
|
||||
|
||||
const navItem = store.navGroupedTemplates.find(
|
||||
(item) => 'id' in item && item.id === 'basics-getting-started'
|
||||
// Essential templates appear under Basics
|
||||
const basicsNav = store.navGroupedTemplates.find(
|
||||
(item) => 'id' in item && item.id === 'basics-use-cases'
|
||||
)
|
||||
|
||||
expect(navItem).toEqual({
|
||||
id: 'basics-getting-started',
|
||||
label: 'Getting Started',
|
||||
icon: expect.any(String)
|
||||
})
|
||||
expect(basicsNav).toBeDefined()
|
||||
expect(
|
||||
store
|
||||
.filterTemplatesByCategory('basics-getting-started')
|
||||
.map((template) => template.name)
|
||||
store.filterTemplatesByCategory('basics-use-cases').map((t) => t.name)
|
||||
).toEqual(['starter-template'])
|
||||
|
||||
// Non-essential templates grouped under GENERATION TYPE
|
||||
const genTypeGroup = store.navGroupedTemplates.find(
|
||||
(item) => 'title' in item && item.title === 'Generation Type'
|
||||
)
|
||||
expect(genTypeGroup).toBeDefined()
|
||||
expect('items' in genTypeGroup!).toBe(true)
|
||||
|
||||
const groupItems = (genTypeGroup as { items: Array<{ id: string }> }).items
|
||||
expect(groupItems.map((i) => i.id)).toEqual(
|
||||
expect.arrayContaining(['generation-type-image', 'generation-type-video'])
|
||||
)
|
||||
})
|
||||
|
||||
it('filters templates by section-based category id', async () => {
|
||||
const store = useWorkflowTemplatesStore()
|
||||
|
||||
await store.loadWorkflowTemplates()
|
||||
|
||||
// Access navGroupedTemplates to populate categoryFilters
|
||||
expect(store.navGroupedTemplates.length).toBeGreaterThan(0)
|
||||
|
||||
const imageTemplates = store.filterTemplatesByCategory(
|
||||
'generation-type-image'
|
||||
)
|
||||
expect(imageTemplates.map((t) => t.name)).toEqual(['image-gen'])
|
||||
|
||||
const videoTemplates = store.filterTemplatesByCategory(
|
||||
'generation-type-video'
|
||||
)
|
||||
expect(videoTemplates.map((t) => t.name)).toEqual(['video-gen'])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -503,10 +503,7 @@ export const useWorkflowTemplatesStore = defineStore(
|
||||
fetchLogoIndex()
|
||||
])
|
||||
|
||||
coreTemplates.value = mapHubWorkflowIndexToCategories(
|
||||
hubIndexResult,
|
||||
st('templateWorkflows.category.All', 'All')
|
||||
)
|
||||
coreTemplates.value = mapHubWorkflowIndexToCategories(hubIndexResult)
|
||||
englishTemplates.value = []
|
||||
logoIndex.value = logoIndexResult
|
||||
|
||||
|
||||
@@ -8,7 +8,11 @@ export const zHubWorkflowIndexEntry = zHubWorkflowTemplateEntry.extend({
|
||||
searchRank: z.number().optional(),
|
||||
isEssential: z.boolean().optional(),
|
||||
useCase: z.string().optional(),
|
||||
license: z.string().optional()
|
||||
license: z.string().optional(),
|
||||
// TODO(hub-api): Pending BE spec confirmation — field names may change.
|
||||
// These enable category nav grouping (e.g. section="Image", sectionGroup="GENERATION TYPE").
|
||||
section: z.string().optional(),
|
||||
sectionGroup: z.string().optional()
|
||||
})
|
||||
|
||||
export const zHubWorkflowIndexResponse = z.array(zHubWorkflowIndexEntry)
|
||||
|
||||
Reference in New Issue
Block a user