mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-19 22:09:37 +00:00
feat: fetch publish tag suggestions from hub labels API
Replace hardcoded COMFY_HUB_TAG_OPTIONS with dynamic fetch from GET /hub/labels?type=tag, falling back to the static list on failure.
This commit is contained in:
@@ -1,8 +1,16 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { flushPromises, mount } from '@vue/test-utils'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { COMFY_HUB_TAG_OPTIONS } from '@/platform/workflow/sharing/constants/comfyHubTags'
|
||||
|
||||
const mockFetchTagLabels = vi.hoisted(() => vi.fn())
|
||||
|
||||
vi.mock('@/platform/workflow/sharing/services/comfyHubService', () => ({
|
||||
useComfyHubService: () => ({
|
||||
fetchTagLabels: mockFetchTagLabels
|
||||
})
|
||||
}))
|
||||
|
||||
import ComfyHubDescribeStep from './ComfyHubDescribeStep.vue'
|
||||
|
||||
function mountStep(
|
||||
@@ -66,7 +74,9 @@ function mountStep(
|
||||
|
||||
describe('ComfyHubDescribeStep', () => {
|
||||
it('emits name and description updates', async () => {
|
||||
mockFetchTagLabels.mockRejectedValue(new Error('offline'))
|
||||
const wrapper = mountStep()
|
||||
await flushPromises()
|
||||
|
||||
await wrapper.find('[data-testid="name-input"]').setValue('New workflow')
|
||||
await wrapper
|
||||
@@ -77,35 +87,72 @@ describe('ComfyHubDescribeStep', () => {
|
||||
expect(wrapper.emitted('update:description')).toEqual([['New description']])
|
||||
})
|
||||
|
||||
it('adds a suggested tag when clicked', async () => {
|
||||
it('uses fetched tags from API', async () => {
|
||||
const apiTags = ['Alpha', 'Beta', 'Gamma']
|
||||
mockFetchTagLabels.mockResolvedValue(apiTags)
|
||||
const wrapper = mountStep()
|
||||
const suggestionButtons = wrapper.findAll(
|
||||
'[data-testid="tags-input"][data-disabled="true"] [data-testid="tag-item"]'
|
||||
)
|
||||
await flushPromises()
|
||||
|
||||
expect(suggestionButtons.length).toBeGreaterThan(0)
|
||||
|
||||
const firstSuggestion = suggestionButtons[0].attributes('data-value')
|
||||
await suggestionButtons[0].trigger('click')
|
||||
|
||||
const tagUpdates = wrapper.emitted('update:tags')
|
||||
expect(tagUpdates?.at(-1)).toEqual([[firstSuggestion]])
|
||||
})
|
||||
|
||||
it('hides already-selected tags from suggestions', () => {
|
||||
const selectedTag = COMFY_HUB_TAG_OPTIONS[0]
|
||||
const wrapper = mountStep({ tags: [selectedTag] })
|
||||
const suggestionValues = wrapper
|
||||
.findAll(
|
||||
'[data-testid="tags-input"][data-disabled="true"] [data-testid="tag-item"]'
|
||||
)
|
||||
.map((button) => button.attributes('data-value'))
|
||||
|
||||
expect(suggestionValues).not.toContain(selectedTag)
|
||||
expect(suggestionValues).toEqual(apiTags)
|
||||
})
|
||||
|
||||
it('falls back to hardcoded tags when API fails', async () => {
|
||||
mockFetchTagLabels.mockRejectedValue(new Error('network error'))
|
||||
const wrapper = mountStep()
|
||||
await flushPromises()
|
||||
|
||||
const suggestionValues = wrapper
|
||||
.findAll(
|
||||
'[data-testid="tags-input"][data-disabled="true"] [data-testid="tag-item"]'
|
||||
)
|
||||
.map((button) => button.attributes('data-value'))
|
||||
|
||||
expect(suggestionValues).toHaveLength(10)
|
||||
expect(suggestionValues[0]).toBe(COMFY_HUB_TAG_OPTIONS[0])
|
||||
})
|
||||
|
||||
it('adds a suggested tag when clicked', async () => {
|
||||
const apiTags = ['Alpha', 'Beta']
|
||||
mockFetchTagLabels.mockResolvedValue(apiTags)
|
||||
const wrapper = mountStep()
|
||||
await flushPromises()
|
||||
|
||||
const suggestionButtons = wrapper.findAll(
|
||||
'[data-testid="tags-input"][data-disabled="true"] [data-testid="tag-item"]'
|
||||
)
|
||||
|
||||
await suggestionButtons[0].trigger('click')
|
||||
|
||||
const tagUpdates = wrapper.emitted('update:tags')
|
||||
expect(tagUpdates?.at(-1)).toEqual([['Alpha']])
|
||||
})
|
||||
|
||||
it('hides already-selected tags from suggestions', async () => {
|
||||
const apiTags = ['Alpha', 'Beta', 'Gamma']
|
||||
mockFetchTagLabels.mockResolvedValue(apiTags)
|
||||
const wrapper = mountStep({ tags: ['Alpha'] })
|
||||
await flushPromises()
|
||||
|
||||
const suggestionValues = wrapper
|
||||
.findAll(
|
||||
'[data-testid="tags-input"][data-disabled="true"] [data-testid="tag-item"]'
|
||||
)
|
||||
.map((button) => button.attributes('data-value'))
|
||||
|
||||
expect(suggestionValues).not.toContain('Alpha')
|
||||
expect(suggestionValues).toEqual(['Beta', 'Gamma'])
|
||||
})
|
||||
|
||||
it('toggles between default and full suggestion lists', async () => {
|
||||
mockFetchTagLabels.mockRejectedValue(new Error('offline'))
|
||||
const wrapper = mountStep()
|
||||
await flushPromises()
|
||||
|
||||
const defaultSuggestions = wrapper.findAll(
|
||||
'[data-testid="tags-input"][data-disabled="true"] [data-testid="tag-item"]'
|
||||
|
||||
@@ -89,7 +89,8 @@ import TagsInputItemDelete from '@/components/ui/tags-input/TagsInputItemDelete.
|
||||
import TagsInputItemText from '@/components/ui/tags-input/TagsInputItemText.vue'
|
||||
import Textarea from '@/components/ui/textarea/Textarea.vue'
|
||||
import { COMFY_HUB_TAG_OPTIONS } from '@/platform/workflow/sharing/constants/comfyHubTags'
|
||||
import { computed, ref } from 'vue'
|
||||
import { useComfyHubService } from '@/platform/workflow/sharing/services/comfyHubService'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { vAutoAnimate } from '@formkit/auto-animate/vue'
|
||||
|
||||
const { tags } = defineProps<{
|
||||
@@ -107,9 +108,20 @@ const emit = defineEmits<{
|
||||
const INITIAL_TAG_SUGGESTION_COUNT = 10
|
||||
|
||||
const showAllSuggestions = ref(false)
|
||||
const tagOptions = ref<string[]>(COMFY_HUB_TAG_OPTIONS)
|
||||
|
||||
const { fetchTagLabels } = useComfyHubService()
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
tagOptions.value = await fetchTagLabels()
|
||||
} catch {
|
||||
// Fall back to hardcoded tags
|
||||
}
|
||||
})
|
||||
|
||||
const availableSuggestions = computed(() =>
|
||||
COMFY_HUB_TAG_OPTIONS.filter((tag) => !tags.includes(tag))
|
||||
tagOptions.value.filter((tag) => !tags.includes(tag))
|
||||
)
|
||||
|
||||
const displayedSuggestions = computed(() =>
|
||||
|
||||
@@ -70,3 +70,13 @@ export const zHubWorkflowPublishResponse = z.object({
|
||||
workflow_id: z.string(),
|
||||
thumbnail_type: z.enum(['image', 'video', 'image_comparison']).optional()
|
||||
})
|
||||
|
||||
export const zHubLabelInfo = z.object({
|
||||
name: z.string(),
|
||||
display_name: z.string(),
|
||||
type: z.enum(['tag', 'model', 'custom_node'])
|
||||
})
|
||||
|
||||
export const zHubLabelListResponse = z.object({
|
||||
labels: z.array(zHubLabelInfo)
|
||||
})
|
||||
|
||||
@@ -171,6 +171,23 @@ describe('useComfyHubService', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('fetches tag labels from /hub/labels?type=tag', async () => {
|
||||
mockFetchApi.mockResolvedValue(
|
||||
mockJsonResponse({
|
||||
labels: [
|
||||
{ name: 'video', display_name: 'Video', type: 'tag' },
|
||||
{ name: 'text-to-image', display_name: 'Text to Image', type: 'tag' }
|
||||
]
|
||||
})
|
||||
)
|
||||
|
||||
const service = useComfyHubService()
|
||||
const tags = await service.fetchTagLabels()
|
||||
|
||||
expect(mockFetchApi).toHaveBeenCalledWith('/hub/labels?type=tag')
|
||||
expect(tags).toEqual(['Video', 'Text to Image'])
|
||||
})
|
||||
|
||||
it('fetches current profile from /hub/profiles/me', async () => {
|
||||
mockFetchApi.mockResolvedValue(
|
||||
mockJsonResponse({
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { ComfyHubProfile } from '@/schemas/apiSchema'
|
||||
import {
|
||||
zHubAssetUploadUrlResponse,
|
||||
zHubLabelListResponse,
|
||||
zHubProfileResponse,
|
||||
zHubWorkflowPublishResponse
|
||||
} from '@/platform/workflow/sharing/schemas/shareSchemas'
|
||||
@@ -213,11 +214,30 @@ export function useComfyHubService() {
|
||||
)
|
||||
}
|
||||
|
||||
async function fetchTagLabels(): Promise<string[]> {
|
||||
const response = await api.fetchApi('/hub/labels?type=tag')
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
await parseErrorMessage(response, 'Failed to fetch hub labels')
|
||||
)
|
||||
}
|
||||
|
||||
const data = await parseRequiredJson(
|
||||
response,
|
||||
zHubLabelListResponse,
|
||||
'Invalid label list response from server'
|
||||
)
|
||||
|
||||
return data.labels.map((label) => label.display_name)
|
||||
}
|
||||
|
||||
return {
|
||||
requestAssetUploadUrl,
|
||||
uploadFileToPresignedUrl,
|
||||
getMyProfile,
|
||||
createProfile,
|
||||
publishWorkflow
|
||||
publishWorkflow,
|
||||
fetchTagLabels
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user