mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-28 18:22:40 +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:
@@ -1,6 +1,7 @@
|
||||
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||
|
||||
import AssetBrowserModal from '@/platform/assets/components/AssetBrowserModal.vue'
|
||||
import type { AssetDisplayItem } from '@/platform/assets/composables/useAssetBrowser'
|
||||
import {
|
||||
createMockAssets,
|
||||
mockAssets
|
||||
@@ -56,7 +57,7 @@ export const Default: Story = {
|
||||
render: (args) => ({
|
||||
components: { AssetBrowserModal },
|
||||
setup() {
|
||||
const onAssetSelect = (asset: any) => {
|
||||
const onAssetSelect = (asset: AssetDisplayItem) => {
|
||||
console.log('Selected asset:', asset)
|
||||
}
|
||||
const onClose = () => {
|
||||
@@ -96,7 +97,7 @@ export const SingleAssetType: Story = {
|
||||
render: (args) => ({
|
||||
components: { AssetBrowserModal },
|
||||
setup() {
|
||||
const onAssetSelect = (asset: any) => {
|
||||
const onAssetSelect = (asset: AssetDisplayItem) => {
|
||||
console.log('Selected asset:', asset)
|
||||
}
|
||||
const onClose = () => {
|
||||
@@ -145,7 +146,7 @@ export const NoLeftPanel: Story = {
|
||||
render: (args) => ({
|
||||
components: { AssetBrowserModal },
|
||||
setup() {
|
||||
const onAssetSelect = (asset: any) => {
|
||||
const onAssetSelect = (asset: AssetDisplayItem) => {
|
||||
console.log('Selected asset:', asset)
|
||||
}
|
||||
const onClose = () => {
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
:nav-items="availableCategories"
|
||||
>
|
||||
<template #header-icon>
|
||||
<i-lucide:folder class="size-4" />
|
||||
<div class="icon-[lucide--folder] size-4" />
|
||||
</template>
|
||||
<template #header-title>{{ $t('assetBrowser.browseAssets') }}</template>
|
||||
</LeftSidePanel>
|
||||
@@ -37,7 +37,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { computed, provide } from 'vue'
|
||||
|
||||
import SearchBox from '@/components/input/SearchBox.vue'
|
||||
import BaseModalLayout from '@/components/widget/layout/BaseModalLayout.vue'
|
||||
@@ -46,6 +46,7 @@ import AssetGrid from '@/platform/assets/components/AssetGrid.vue'
|
||||
import type { AssetDisplayItem } from '@/platform/assets/composables/useAssetBrowser'
|
||||
import { useAssetBrowser } from '@/platform/assets/composables/useAssetBrowser'
|
||||
import type { AssetItem } from '@/platform/assets/schemas/assetSchema'
|
||||
import { OnCloseKey } from '@/types/widgetTypes'
|
||||
|
||||
const props = defineProps<{
|
||||
nodeType?: string
|
||||
@@ -61,35 +62,28 @@ const emit = defineEmits<{
|
||||
close: []
|
||||
}>()
|
||||
|
||||
// Use AssetBrowser composable for all business logic
|
||||
provide(OnCloseKey, props.onClose ?? (() => {}))
|
||||
|
||||
const {
|
||||
searchQuery,
|
||||
selectedCategory,
|
||||
availableCategories,
|
||||
contentTitle,
|
||||
filteredAssets,
|
||||
selectAsset
|
||||
selectAssetWithCallback
|
||||
} = useAssetBrowser(props.assets)
|
||||
|
||||
// Dialog controls panel visibility via prop
|
||||
const shouldShowLeftPanel = computed(() => {
|
||||
return props.showLeftPanel ?? true
|
||||
})
|
||||
|
||||
// Handle close button - call both the prop callback and emit the event
|
||||
const handleClose = () => {
|
||||
props.onClose?.()
|
||||
emit('close')
|
||||
}
|
||||
|
||||
// Handle asset selection and emit to parent
|
||||
const handleAssetSelectAndEmit = (asset: AssetDisplayItem) => {
|
||||
selectAsset(asset) // This logs the selection for dev mode
|
||||
emit('asset-select', asset) // Emit the full asset object
|
||||
|
||||
// Call prop callback if provided
|
||||
if (props.onSelect) {
|
||||
props.onSelect(asset.name) // Use asset name as the asset path
|
||||
}
|
||||
const handleAssetSelectAndEmit = async (asset: AssetDisplayItem) => {
|
||||
emit('asset-select', asset)
|
||||
await selectAssetWithCallback(asset.id, props.onSelect)
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
'bg-ivory-100 border border-gray-300 dark-theme:bg-charcoal-400 dark-theme:border-charcoal-600',
|
||||
'hover:transform hover:-translate-y-0.5 hover:shadow-lg hover:shadow-black/10 hover:border-gray-400',
|
||||
'dark-theme:hover:shadow-lg dark-theme:hover:shadow-black/30 dark-theme:hover:border-charcoal-700',
|
||||
'focus:outline-none focus:ring-2 focus:ring-blue-500 dark-theme:focus:ring-blue-400'
|
||||
'focus:outline-none focus:transform focus:-translate-y-0.5 focus:shadow-lg focus:shadow-black/10 dark-theme:focus:shadow-black/30'
|
||||
],
|
||||
// Div-specific styles
|
||||
!interactive && [
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<div :class="leftSideClasses" data-component-id="asset-filter-bar-left">
|
||||
<MultiSelect
|
||||
v-model="fileFormats"
|
||||
label="File formats"
|
||||
:label="$t('assetBrowser.fileFormats')"
|
||||
:options="fileFormatOptions"
|
||||
:class="selectClasses"
|
||||
data-component-id="asset-filter-file-formats"
|
||||
@@ -12,7 +12,7 @@
|
||||
|
||||
<MultiSelect
|
||||
v-model="baseModels"
|
||||
label="Base models"
|
||||
:label="$t('assetBrowser.baseModels')"
|
||||
:options="baseModelOptions"
|
||||
:class="selectClasses"
|
||||
data-component-id="asset-filter-base-models"
|
||||
@@ -23,7 +23,7 @@
|
||||
<div :class="rightSideClasses" data-component-id="asset-filter-bar-right">
|
||||
<SingleSelect
|
||||
v-model="sortBy"
|
||||
label="Sort by"
|
||||
:label="$t('assetBrowser.sortBy')"
|
||||
:options="sortOptions"
|
||||
:class="selectClasses"
|
||||
data-component-id="asset-filter-sort"
|
||||
@@ -43,6 +43,7 @@ import { ref } from 'vue'
|
||||
import MultiSelect from '@/components/input/MultiSelect.vue'
|
||||
import SingleSelect from '@/components/input/SingleSelect.vue'
|
||||
import type { SelectOption } from '@/components/input/types'
|
||||
import { t } from '@/i18n'
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
export interface FilterState {
|
||||
@@ -74,10 +75,10 @@ const baseModelOptions = [
|
||||
// TODO: Make sortOptions configurable via props
|
||||
// Different asset types might need different sorting options
|
||||
const sortOptions = [
|
||||
{ name: 'A-Z', value: 'name-asc' },
|
||||
{ name: 'Z-A', value: 'name-desc' },
|
||||
{ name: 'Recent', value: 'recent' },
|
||||
{ name: 'Popular', value: 'popular' }
|
||||
{ name: t('assetBrowser.sortAZ'), value: 'name-asc' },
|
||||
{ name: t('assetBrowser.sortZA'), value: 'name-desc' },
|
||||
{ name: t('assetBrowser.sortRecent'), value: 'recent' },
|
||||
{ name: t('assetBrowser.sortPopular'), value: 'popular' }
|
||||
]
|
||||
|
||||
const emit = defineEmits<{
|
||||
|
||||
Reference in New Issue
Block a user