mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-05-05 21:54:50 +00:00
[feat] integrate asset browser with widget system (#5629)
## Summary Add asset browser dialog integration for combo widgets with full animation support and proper state management. (Thank you Claude from saving me me from merge conflict hell on this one.) ## Changes - Widget integration: combo widgets now use AssetBrowserModal for eligible asset types - Dialog animations: added animateHide() for smooth close transitions - Async operations: proper sequencing of widget updates and dialog animations - Service layer: added getAssetsForNodeType() and getAssetDetails() methods - Type safety: comprehensive TypeScript types and error handling - Test coverage: unit tests for all new functionality - Bonus: fixed the hardcoded labels in AssetFilterBar Widget behavior: - Shows asset browser button for eligible widgets when asset API enabled - Handles asset selection with proper callback sequencing - Maintains widget value updates and litegraph notification ## Review Focus I will call out some stuff inline. ## Screenshots https://github.com/user-attachments/assets/9d3a72cf-d2b0-445f-8022-4c49daa04637 ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-5629-feat-integrate-asset-browser-with-widget-system-2726d73d365081a9a98be9a2307aee0b) by [Unito](https://www.unito.io) --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: GitHub Action <action@github.com>
This commit is contained in:
@@ -19,7 +19,7 @@ const EXPECTED_DEFAULT_TYPES = [
|
||||
'gligen'
|
||||
] as const
|
||||
|
||||
type NodeDefStoreType = typeof import('@/stores/nodeDefStore')
|
||||
type NodeDefStoreType = ReturnType<typeof useNodeDefStore>
|
||||
|
||||
// Create minimal but valid ComfyNodeDefImpl for testing
|
||||
function createMockNodeDef(name: string): ComfyNodeDefImpl {
|
||||
@@ -343,6 +343,107 @@ describe('useModelToNodeStore', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('getCategoryForNodeType', () => {
|
||||
it('should return category for known node type', () => {
|
||||
const modelToNodeStore = useModelToNodeStore()
|
||||
modelToNodeStore.registerDefaults()
|
||||
|
||||
expect(
|
||||
modelToNodeStore.getCategoryForNodeType('CheckpointLoaderSimple')
|
||||
).toBe('checkpoints')
|
||||
expect(modelToNodeStore.getCategoryForNodeType('LoraLoader')).toBe(
|
||||
'loras'
|
||||
)
|
||||
expect(modelToNodeStore.getCategoryForNodeType('VAELoader')).toBe('vae')
|
||||
})
|
||||
|
||||
it('should return undefined for unknown node type', () => {
|
||||
const modelToNodeStore = useModelToNodeStore()
|
||||
modelToNodeStore.registerDefaults()
|
||||
|
||||
expect(
|
||||
modelToNodeStore.getCategoryForNodeType('NonExistentNode')
|
||||
).toBeUndefined()
|
||||
expect(modelToNodeStore.getCategoryForNodeType('')).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should return first category when node type exists in multiple categories', () => {
|
||||
const modelToNodeStore = useModelToNodeStore()
|
||||
|
||||
// Test with a node that exists in the defaults but add our own first
|
||||
// Since defaults register 'StyleModelLoader' in 'style_models',
|
||||
// we verify our custom registrations come after defaults in Object.entries iteration
|
||||
const result = modelToNodeStore.getCategoryForNodeType('StyleModelLoader')
|
||||
expect(result).toBe('style_models') // This proves the method works correctly
|
||||
|
||||
// Now test that custom registrations after defaults also work
|
||||
modelToNodeStore.quickRegister(
|
||||
'unicorn_styles',
|
||||
'StyleModelLoader',
|
||||
'param1'
|
||||
)
|
||||
const result2 =
|
||||
modelToNodeStore.getCategoryForNodeType('StyleModelLoader')
|
||||
// Should still be style_models since it was registered first by defaults
|
||||
expect(result2).toBe('style_models')
|
||||
})
|
||||
|
||||
it('should trigger lazy registration when called before registerDefaults', () => {
|
||||
const modelToNodeStore = useModelToNodeStore()
|
||||
|
||||
const result = modelToNodeStore.getCategoryForNodeType(
|
||||
'CheckpointLoaderSimple'
|
||||
)
|
||||
expect(result).toBe('checkpoints')
|
||||
})
|
||||
|
||||
it('should be performant for repeated lookups', () => {
|
||||
const modelToNodeStore = useModelToNodeStore()
|
||||
modelToNodeStore.registerDefaults()
|
||||
|
||||
// Measure performance without assuming implementation
|
||||
const start = performance.now()
|
||||
for (let i = 0; i < 1000; i++) {
|
||||
modelToNodeStore.getCategoryForNodeType('CheckpointLoaderSimple')
|
||||
}
|
||||
const end = performance.now()
|
||||
|
||||
// Should be fast enough for UI responsiveness
|
||||
expect(end - start).toBeLessThan(10)
|
||||
})
|
||||
|
||||
it('should handle invalid input types gracefully', () => {
|
||||
const modelToNodeStore = useModelToNodeStore()
|
||||
modelToNodeStore.registerDefaults()
|
||||
|
||||
// These should not throw but return undefined
|
||||
expect(
|
||||
modelToNodeStore.getCategoryForNodeType(null as any)
|
||||
).toBeUndefined()
|
||||
expect(
|
||||
modelToNodeStore.getCategoryForNodeType(undefined as any)
|
||||
).toBeUndefined()
|
||||
expect(
|
||||
modelToNodeStore.getCategoryForNodeType(123 as any)
|
||||
).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should be case-sensitive for node type matching', () => {
|
||||
const modelToNodeStore = useModelToNodeStore()
|
||||
modelToNodeStore.registerDefaults()
|
||||
|
||||
expect(
|
||||
modelToNodeStore.getCategoryForNodeType('checkpointloadersimple')
|
||||
).toBeUndefined()
|
||||
expect(
|
||||
modelToNodeStore.getCategoryForNodeType('CHECKPOINTLOADERSIMPLE')
|
||||
).toBeUndefined()
|
||||
expect(
|
||||
modelToNodeStore.getCategoryForNodeType('CheckpointLoaderSimple')
|
||||
).toBe('checkpoints')
|
||||
})
|
||||
})
|
||||
|
||||
describe('edge cases', () => {
|
||||
it('should handle empty string model type', () => {
|
||||
const modelToNodeStore = useModelToNodeStore()
|
||||
|
||||
Reference in New Issue
Block a user