Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion src/renderer/extensions/vueNodes/components/NodeWidgets.vue
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
:widget="widget.simplified"
:model-value="widget.value"
:node-id="nodeData?.id != null ? String(nodeData.id) : ''"
:node-type="nodeType"
class="flex-1"
@update:model-value="widget.updateHandler"
/>
Expand Down Expand Up @@ -162,7 +163,9 @@ const processedWidgets = computed((): ProcessedWidget[] => {
// Update the widget value directly
widget.value = value as WidgetValue

if (widget.callback) {
// Skip callback for asset widgets - their callback opens the modal,
// but Vue asset mode handles selection through the dropdown
if (widget.callback && widget.type !== 'asset') {
widget.callback(value)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,30 +5,37 @@
:asset-kind="assetKind"
:allow-upload="allowUpload"
:upload-folder="uploadFolder"
:is-asset-mode="isAssetMode"
:default-layout-mode="defaultLayoutMode"
@update:model-value="handleUpdateModelValue"
/>
<WidgetSelectDefault
v-else
v-bind="props"
:widget="widget"
:model-value="modelValue"
@update:model-value="handleUpdateModelValue"
/>
</template>

<script setup lang="ts">
import { computed } from 'vue'

import { assetService } from '@/platform/assets/services/assetService'
import { isCloud } from '@/platform/distribution/types'
import { useSettingStore } from '@/platform/settings/settingStore'
import WidgetSelectDefault from '@/renderer/extensions/vueNodes/widgets/components/WidgetSelectDefault.vue'
import WidgetSelectDropdown from '@/renderer/extensions/vueNodes/widgets/components/WidgetSelectDropdown.vue'
import type { LayoutMode } from '@/renderer/extensions/vueNodes/widgets/components/form/dropdown/types'
import type { ResultItemType } from '@/schemas/apiSchema'
import { isComboInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
import type { ComboInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
import type { AssetKind } from '@/types/widgetTypes'

import WidgetSelectDefault from './WidgetSelectDefault.vue'
import WidgetSelectDropdown from './WidgetSelectDropdown.vue'

const props = defineProps<{
widget: SimplifiedWidget<string | number | undefined>
modelValue: string | number | undefined
nodeType?: string
}>()

const emit = defineEmits<{
Expand Down Expand Up @@ -90,10 +97,30 @@ const specDescriptor = computed<{
}
})

const isAssetMode = computed(() => {
if (isCloud) {
const settingStore = useSettingStore()
const isUsingAssetAPI = settingStore.get('Comfy.Assets.UseAssetAPI')
const isEligible = assetService.isAssetBrowserEligible(
props.nodeType,
props.widget.name
)

return isUsingAssetAPI && isEligible
}

return false
})

const assetKind = computed(() => specDescriptor.value.kind)
const isDropdownUIWidget = computed(() => assetKind.value !== 'unknown')
const isDropdownUIWidget = computed(
() => isAssetMode.value || assetKind.value !== 'unknown'
)
const allowUpload = computed(() => specDescriptor.value.allowUpload)
const uploadFolder = computed<ResultItemType>(() => {
return specDescriptor.value.folder ?? 'input'
})
const defaultLayoutMode = computed<LayoutMode>(() => {
return isAssetMode.value ? 'list' : 'grid'
})
</script>
Original file line number Diff line number Diff line change
@@ -1,10 +1,21 @@
<script setup lang="ts">
import { computed, provide, ref, watch } from 'vue'
import { capitalize } from 'es-toolkit'
import { computed, provide, ref, toRef, watch } from 'vue'

import { useWidgetValue } from '@/composables/graph/useWidgetValue'
import { useTransformCompatOverlayProps } from '@/composables/useTransformCompatOverlayProps'
import { t } from '@/i18n'
import { useToastStore } from '@/platform/updates/common/toastStore'
import FormDropdown from '@/renderer/extensions/vueNodes/widgets/components/form/dropdown/FormDropdown.vue'
import { AssetKindKey } from '@/renderer/extensions/vueNodes/widgets/components/form/dropdown/types'
import type {
DropdownItem,
FilterOption,
LayoutMode,
SelectedKey
} from '@/renderer/extensions/vueNodes/widgets/components/form/dropdown/types'
import WidgetLayoutField from '@/renderer/extensions/vueNodes/widgets/components/layout/WidgetLayoutField.vue'
import { useAssetWidgetData } from '@/renderer/extensions/vueNodes/widgets/composables/useAssetWidgetData'
import type { ResultItemType } from '@/schemas/apiSchema'
import { api } from '@/scripts/api'
import { useAssetsStore } from '@/stores/assetsStore'
Expand All @@ -16,21 +27,15 @@ import {
filterWidgetProps
} from '@/utils/widgetPropFilter'

import FormDropdown from './form/dropdown/FormDropdown.vue'
import { AssetKindKey } from './form/dropdown/types'
import type {
DropdownItem,
FilterOption,
SelectedKey
} from './form/dropdown/types'
import WidgetLayoutField from './layout/WidgetLayoutField.vue'

const props = defineProps<{
widget: SimplifiedWidget<string | number | undefined>
modelValue: string | number | undefined
nodeType?: string
assetKind?: AssetKind
allowUpload?: boolean
uploadFolder?: ResultItemType
isAssetMode?: boolean
defaultLayoutMode?: LayoutMode
}>()

provide(
Expand Down Expand Up @@ -59,12 +64,26 @@ const combinedProps = computed(() => ({
...transformCompatProps.value
}))

const getAssetData = () => {
if (props.isAssetMode && props.nodeType) {
return useAssetWidgetData(toRef(() => props.nodeType))
}
return null
}
const assetData = getAssetData()

const filterSelected = ref('all')
const filterOptions = ref<FilterOption[]>([
{ id: 'all', name: 'All' },
{ id: 'inputs', name: 'Inputs' },
{ id: 'outputs', name: 'Outputs' }
])
const filterOptions = computed<FilterOption[]>(() => {
if (props.isAssetMode) {
const categoryName = assetData?.category.value ?? 'All'
return [{ id: 'all', name: capitalize(categoryName) }]
}
return [
{ id: 'all', name: 'All' },
{ id: 'inputs', name: 'Inputs' },
{ id: 'outputs', name: 'Outputs' }
]
})

const selectedSet = ref<Set<SelectedKey>>(new Set())

Expand Down Expand Up @@ -132,9 +151,16 @@ const outputItems = computed<DropdownItem[]>(() => {
})

const allItems = computed<DropdownItem[]>(() => {
if (props.isAssetMode && assetData) {
return assetData.dropdownItems.value
}
return [...inputItems.value, ...outputItems.value]
})
const dropdownItems = computed<DropdownItem[]>(() => {
if (props.isAssetMode) {
return allItems.value
}

switch (filterSelected.value) {
case 'inputs':
return inputItems.value
Expand Down Expand Up @@ -169,7 +195,10 @@ const mediaPlaceholder = computed(() => {
return t('widgets.uploadSelect.placeholder')
})

const uploadable = computed(() => props.allowUpload === true)
const uploadable = computed(() => {
if (props.isAssetMode) return false
return props.allowUpload === true
})

const acceptTypes = computed(() => {
// Be permissive with accept types because backend uses libraries
Expand All @@ -186,6 +215,8 @@ const acceptTypes = computed(() => {
}
})

const layoutMode = ref<LayoutMode>(props.defaultLayoutMode ?? 'grid')

watch(
localValue,
(currentValue) => {
Expand Down Expand Up @@ -313,6 +344,7 @@ function getMediaUrl(
<FormDropdown
v-model:selected="selectedSet"
v-model:filter-selected="filterSelected"
v-model:layout-mode="layoutMode"
:items="dropdownItems"
:placeholder="mediaPlaceholder"
:multiple="false"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,16 +124,17 @@ function handleVideoLoad(event: Event) {
:class="
cn('flex gap-1', {
'flex-col': layout === 'grid',
'flex-col px-4 py-1 w-full justify-center': layout === 'list',
'flex-col px-4 py-1 w-full justify-center min-w-0': layout === 'list',
'flex-row p-2 items-center justify-between w-full':
layout === 'list-small'
})
"
>
<span
v-tooltip="layout === 'grid' ? (label ?? name) : undefined"
:class="
cn(
'block text-[15px] line-clamp-2 wrap-break-word',
'block text-[15px] line-clamp-2 break-words overflow-hidden',
'transition-colors duration-150',
// selection
!!selected && 'text-blue-500'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { computed, toValue, watch } from 'vue'
import type { MaybeRefOrGetter } from 'vue'

import { isCloud } from '@/platform/distribution/types'
import type { AssetItem } from '@/platform/assets/schemas/assetSchema'
import type { DropdownItem } from '@/renderer/extensions/vueNodes/widgets/components/form/dropdown/types'
import { useAssetsStore } from '@/stores/assetsStore'
import { useModelToNodeStore } from '@/stores/modelToNodeStore'

/**
* Composable for fetching and transforming asset data for Vue node widgets.
* Provides reactive asset data based on node type with automatic category detection.
* Uses store-based caching to avoid duplicate fetches across multiple instances.
*
* Cloud-only composable - returns empty data when not in cloud environment.
*
* @param nodeType - ComfyUI node type (ref, getter, or plain value). Can be undefined.
* Accepts: ref('CheckpointLoaderSimple'), () => 'CheckpointLoaderSimple', or 'CheckpointLoaderSimple'
* @returns Reactive data including category, assets, dropdown items, loading state, and errors
*/
export function useAssetWidgetData(
nodeType: MaybeRefOrGetter<string | undefined>
) {
if (isCloud) {
const assetsStore = useAssetsStore()
const modelToNodeStore = useModelToNodeStore()

const category = computed(() => {
const resolvedType = toValue(nodeType)
return resolvedType
? modelToNodeStore.getCategoryForNodeType(resolvedType)
: undefined
})

const assets = computed<AssetItem[]>(() => {
const resolvedType = toValue(nodeType)
return resolvedType
? (assetsStore.modelAssetsByNodeType.get(resolvedType) ?? [])
: []
})

const isLoading = computed(() => {
const resolvedType = toValue(nodeType)
return resolvedType
? (assetsStore.modelLoadingByNodeType.get(resolvedType) ?? false)
: false
})

const error = computed<Error | null>(() => {
const resolvedType = toValue(nodeType)
return resolvedType
? (assetsStore.modelErrorByNodeType.get(resolvedType) ?? null)
: null
})

const dropdownItems = computed<DropdownItem[]>(() => {
return assets.value.map((asset) => ({
id: asset.id,
name:
(asset.user_metadata?.filename as string | undefined) ?? asset.name,
label: asset.name,
mediaSrc: asset.preview_url ?? '',
metadata: ''
}))
})

watch(
() => toValue(nodeType),
async (currentNodeType) => {
if (!currentNodeType) {
return
}

const hasData = assetsStore.modelAssetsByNodeType.has(currentNodeType)

if (!hasData) {
await assetsStore.updateModelsForNodeType(currentNodeType)
}
},
{ immediate: true }
)

return {
category,
assets,
dropdownItems,
isLoading,
error
}
}

return {
category: computed(() => undefined),
assets: computed(() => []),
dropdownItems: computed(() => []),
isLoading: computed(() => false),
error: computed(() => null)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,10 @@ const coreWidgetDefinitions: Array<[string, WidgetDefinition]> = [
essential: true
}
],
['combo', { component: WidgetSelect, aliases: ['COMBO'], essential: true }],
[
'combo',
{ component: WidgetSelect, aliases: ['COMBO', 'asset'], essential: true }
],
[
'color',
{ component: WidgetColorPicker, aliases: ['COLOR'], essential: false }
Expand Down
Loading