mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-01 18:19:09 +00:00
Compare commits
1 Commits
test/asset
...
dev/remote
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fff5e4bd30 |
@@ -0,0 +1,183 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, provide, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import type { FilterOption } from '@/platform/assets/types/filterTypes'
|
||||
import { isComboInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
|
||||
|
||||
import FormDropdown from './form/dropdown/FormDropdown.vue'
|
||||
import type { FormDropdownItem, LayoutMode } from './form/dropdown/types'
|
||||
import { AssetKindKey } from './form/dropdown/types'
|
||||
import {
|
||||
buildSearchText,
|
||||
extractFilterValues,
|
||||
getByPath,
|
||||
mapToDropdownItem
|
||||
} from '../utils/resolveItemSchema'
|
||||
import { fetchRemoteRoute } from '../utils/resolveRemoteRoute'
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue?: string
|
||||
widget: SimplifiedWidget<string | undefined>
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: string]
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const comboSpec = computed(() => {
|
||||
if (props.widget.spec && isComboInputSpec(props.widget.spec)) {
|
||||
return props.widget.spec
|
||||
}
|
||||
return undefined
|
||||
})
|
||||
const remoteConfig = computed(() => comboSpec.value?.remote!)
|
||||
const itemSchema = computed(() => remoteConfig.value?.item_schema!)
|
||||
|
||||
const rawItems = ref<unknown[]>([])
|
||||
const loading = ref(false)
|
||||
|
||||
async function fetchItems() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await fetchRemoteRoute(remoteConfig.value.route, {
|
||||
params: remoteConfig.value.query_params,
|
||||
timeout: remoteConfig.value.timeout ?? 30000
|
||||
})
|
||||
const data = remoteConfig.value.response_key
|
||||
? res.data[remoteConfig.value.response_key]
|
||||
: res.data
|
||||
rawItems.value = Array.isArray(data) ? data : []
|
||||
} catch (err) {
|
||||
console.error('RichComboWidget: fetch error', err)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
void fetchItems()
|
||||
})
|
||||
|
||||
const assetKind = computed(() => {
|
||||
const pt = itemSchema.value.preview_type ?? 'image'
|
||||
return pt as 'image' | 'video' | 'audio'
|
||||
})
|
||||
|
||||
provide(AssetKindKey, assetKind)
|
||||
|
||||
const items = computed<FormDropdownItem[]>(() =>
|
||||
rawItems.value.map((raw) => mapToDropdownItem(raw, itemSchema.value))
|
||||
)
|
||||
|
||||
const searchIndex = computed(() => {
|
||||
const schema = itemSchema.value
|
||||
const fields = schema.search_fields ?? [schema.label_field]
|
||||
const index = new Map<string, string>()
|
||||
for (const raw of rawItems.value) {
|
||||
const id = String(getByPath(raw, schema.value_field) ?? '')
|
||||
index.set(id, buildSearchText(raw, fields))
|
||||
}
|
||||
return index
|
||||
})
|
||||
|
||||
const filterOptions = computed<FilterOption[]>(() => {
|
||||
const schema = itemSchema.value
|
||||
if (!schema.filter_field) return []
|
||||
const values = extractFilterValues(rawItems.value, schema.filter_field)
|
||||
return [
|
||||
{ name: 'All', value: 'all' },
|
||||
...values.map((v) => ({ name: v, value: v }))
|
||||
]
|
||||
})
|
||||
|
||||
const filterSelected = ref('all')
|
||||
const layoutMode = ref<LayoutMode>('list')
|
||||
const selectedSet = ref<Set<string>>(new Set())
|
||||
|
||||
const filteredItems = computed<FormDropdownItem[]>(() => {
|
||||
const schema = itemSchema.value
|
||||
if (filterSelected.value === 'all' || !schema.filter_field) {
|
||||
return items.value
|
||||
}
|
||||
const filterField = schema.filter_field
|
||||
return rawItems.value
|
||||
.filter(
|
||||
(raw) =>
|
||||
String(getByPath(raw, filterField) ?? '') === filterSelected.value
|
||||
)
|
||||
.map((raw) => mapToDropdownItem(raw, schema))
|
||||
})
|
||||
|
||||
async function searcher(
|
||||
query: string,
|
||||
searchItems: FormDropdownItem[],
|
||||
_onCleanup: (cleanupFn: () => void) => void
|
||||
): Promise<FormDropdownItem[]> {
|
||||
if (!query.trim()) return searchItems
|
||||
const q = query.toLowerCase()
|
||||
return searchItems.filter((item) => {
|
||||
const text = searchIndex.value.get(item.id) ?? item.name.toLowerCase()
|
||||
return text.includes(q)
|
||||
})
|
||||
}
|
||||
|
||||
watch(
|
||||
[() => props.modelValue, items],
|
||||
([val]) => {
|
||||
selectedSet.value.clear()
|
||||
if (val) {
|
||||
const item = items.value.find((i) => i.id === val)
|
||||
if (item) selectedSet.value.add(item.id)
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
function handleRefresh() {
|
||||
void fetchItems()
|
||||
}
|
||||
|
||||
function handleSelection(selected: Set<string>) {
|
||||
const id = selected.values().next().value
|
||||
if (id) {
|
||||
emit('update:modelValue', id)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex w-full items-center gap-1">
|
||||
<FormDropdown
|
||||
v-model:selected="selectedSet"
|
||||
v-model:filter-selected="filterSelected"
|
||||
v-model:layout-mode="layoutMode"
|
||||
:items="filteredItems"
|
||||
:placeholder="loading ? 'Loading...' : t('widgets.uploadSelect.placeholder')"
|
||||
:multiple="false"
|
||||
:filter-options="[]"
|
||||
:show-sort="false"
|
||||
:show-layout-switcher="false"
|
||||
:searcher="searcher"
|
||||
class="flex-1"
|
||||
@update:selected="handleSelection"
|
||||
/>
|
||||
<button
|
||||
v-if="remoteConfig?.refresh_button !== false"
|
||||
class="flex size-7 shrink-0 items-center justify-center rounded text-secondary hover:bg-component-node-widget-background-hovered"
|
||||
title="Refresh"
|
||||
@pointerdown.stop
|
||||
@click.stop="handleRefresh"
|
||||
>
|
||||
<i
|
||||
:class="[
|
||||
'icon-[lucide--refresh-cw] size-3.5',
|
||||
loading && 'animate-spin'
|
||||
]"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,6 +1,11 @@
|
||||
<template>
|
||||
<RichComboWidget
|
||||
v-if="hasItemSchema"
|
||||
v-model="modelValue"
|
||||
:widget
|
||||
/>
|
||||
<WidgetSelectDropdown
|
||||
v-if="isDropdownUIWidget"
|
||||
v-else-if="isDropdownUIWidget"
|
||||
v-model="modelValue"
|
||||
:widget
|
||||
:node-type="widget.nodeType ?? nodeType"
|
||||
@@ -24,6 +29,7 @@
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { assetService } from '@/platform/assets/services/assetService'
|
||||
import RichComboWidget from '@/renderer/extensions/vueNodes/widgets/components/RichComboWidget.vue'
|
||||
import WidgetSelectDefault from '@/renderer/extensions/vueNodes/widgets/components/WidgetSelectDefault.vue'
|
||||
import WidgetSelectDropdown from '@/renderer/extensions/vueNodes/widgets/components/WidgetSelectDropdown.vue'
|
||||
import WidgetWithControl from '@/renderer/extensions/vueNodes/widgets/components/WidgetWithControl.vue'
|
||||
@@ -53,6 +59,10 @@ const comboSpec = computed<ComboInputSpec | undefined>(() => {
|
||||
return undefined
|
||||
})
|
||||
|
||||
const hasItemSchema = computed(
|
||||
() => !!comboSpec.value?.remote?.item_schema
|
||||
)
|
||||
|
||||
const specDescriptor = computed<{
|
||||
kind: AssetKind
|
||||
allowUpload: boolean
|
||||
|
||||
@@ -34,6 +34,8 @@ interface Props {
|
||||
accept?: string
|
||||
filterOptions?: FilterOption[]
|
||||
sortOptions?: SortOption[]
|
||||
showSort?: boolean
|
||||
showLayoutSwitcher?: boolean
|
||||
showOwnershipFilter?: boolean
|
||||
ownershipOptions?: OwnershipFilterOption[]
|
||||
showBaseModelFilter?: boolean
|
||||
@@ -61,6 +63,8 @@ const {
|
||||
accept,
|
||||
filterOptions = [],
|
||||
sortOptions = getDefaultSortOptions(),
|
||||
showSort = true,
|
||||
showLayoutSwitcher = true,
|
||||
showOwnershipFilter,
|
||||
ownershipOptions,
|
||||
showBaseModelFilter,
|
||||
@@ -232,6 +236,8 @@ function handleSelection(item: FormDropdownItem, index: number) {
|
||||
v-model:base-model-selected="baseModelSelected"
|
||||
:filter-options
|
||||
:sort-options
|
||||
:show-sort
|
||||
:show-layout-switcher="showLayoutSwitcher"
|
||||
:show-ownership-filter
|
||||
:ownership-options
|
||||
:show-base-model-filter
|
||||
|
||||
@@ -20,6 +20,8 @@ interface Props {
|
||||
isSelected: (item: FormDropdownItem, index: number) => boolean
|
||||
filterOptions: FilterOption[]
|
||||
sortOptions: SortOption[]
|
||||
showSort?: boolean
|
||||
showLayoutSwitcher?: boolean
|
||||
showOwnershipFilter?: boolean
|
||||
ownershipOptions?: OwnershipFilterOption[]
|
||||
showBaseModelFilter?: boolean
|
||||
@@ -31,6 +33,8 @@ const {
|
||||
isSelected,
|
||||
filterOptions,
|
||||
sortOptions,
|
||||
showSort = true,
|
||||
showLayoutSwitcher = true,
|
||||
showOwnershipFilter,
|
||||
ownershipOptions,
|
||||
showBaseModelFilter,
|
||||
@@ -112,6 +116,8 @@ const virtualItems = computed<VirtualDropdownItem[]>(() =>
|
||||
v-model:ownership-selected="ownershipSelected"
|
||||
v-model:base-model-selected="baseModelSelected"
|
||||
:sort-options
|
||||
:show-sort
|
||||
:show-layout-switcher="showLayoutSwitcher"
|
||||
:show-ownership-filter
|
||||
:ownership-options
|
||||
:show-base-model-filter
|
||||
@@ -145,6 +151,7 @@ const virtualItems = computed<VirtualDropdownItem[]>(() =>
|
||||
:preview-url="item.preview_url ?? ''"
|
||||
:name="item.name"
|
||||
:label="item.label"
|
||||
:description="item.description"
|
||||
:layout="layoutMode"
|
||||
@click="emit('item-click', item, index)"
|
||||
/>
|
||||
|
||||
@@ -18,8 +18,13 @@ import type { LayoutMode, SortOption } from './types'
|
||||
const { t } = useI18n()
|
||||
const overlayProps = useTransformCompatOverlayProps()
|
||||
|
||||
defineProps<{
|
||||
const {
|
||||
showSort = true,
|
||||
showLayoutSwitcher = true
|
||||
} = defineProps<{
|
||||
sortOptions: SortOption[]
|
||||
showSort?: boolean
|
||||
showLayoutSwitcher?: boolean
|
||||
showOwnershipFilter?: boolean
|
||||
ownershipOptions?: OwnershipFilterOption[]
|
||||
showBaseModelFilter?: boolean
|
||||
@@ -114,6 +119,7 @@ function toggleBaseModelSelection(item: FilterOption) {
|
||||
/>
|
||||
|
||||
<Button
|
||||
v-if="showSort"
|
||||
ref="sortTriggerRef"
|
||||
variant="textonly"
|
||||
size="icon"
|
||||
@@ -132,6 +138,7 @@ function toggleBaseModelSelection(item: FilterOption) {
|
||||
<i class="icon-[lucide--arrow-up-down] size-4" />
|
||||
</Button>
|
||||
<Popover
|
||||
v-if="showSort"
|
||||
ref="sortPopoverRef"
|
||||
:dismissable="true"
|
||||
:close-on-escape="true"
|
||||
@@ -309,6 +316,7 @@ function toggleBaseModelSelection(item: FilterOption) {
|
||||
</Popover>
|
||||
|
||||
<div
|
||||
v-if="showLayoutSwitcher"
|
||||
:class="
|
||||
cn(
|
||||
actionButtonStyle,
|
||||
|
||||
@@ -12,6 +12,7 @@ interface Props {
|
||||
previewUrl: string
|
||||
name: string
|
||||
label?: string
|
||||
description?: string
|
||||
layout?: LayoutMode
|
||||
}
|
||||
|
||||
@@ -27,11 +28,31 @@ const actualDimensions = ref<string | null>(null)
|
||||
const assetKind = inject(AssetKindKey)
|
||||
|
||||
const isVideo = computed(() => assetKind?.value === 'video')
|
||||
const isAudio = computed(() => assetKind?.value === 'audio')
|
||||
|
||||
const audioRef = ref<HTMLAudioElement | null>(null)
|
||||
const isPlayingAudio = ref(false)
|
||||
|
||||
function handleClick() {
|
||||
emit('click', props.index)
|
||||
}
|
||||
|
||||
function toggleAudioPreview(event: Event) {
|
||||
event.stopPropagation()
|
||||
if (!audioRef.value) return
|
||||
if (isPlayingAudio.value) {
|
||||
audioRef.value.pause()
|
||||
isPlayingAudio.value = false
|
||||
} else {
|
||||
void audioRef.value.play()
|
||||
isPlayingAudio.value = true
|
||||
}
|
||||
}
|
||||
|
||||
function handleAudioEnded() {
|
||||
isPlayingAudio.value = false
|
||||
}
|
||||
|
||||
function handleImageLoad(event: Event) {
|
||||
emit('mediaLoad', event)
|
||||
if (!event.target || !(event.target instanceof HTMLImageElement)) return
|
||||
@@ -107,6 +128,25 @@ function handleVideoLoad(event: Event) {
|
||||
muted
|
||||
@loadeddata="handleVideoLoad"
|
||||
/>
|
||||
<div
|
||||
v-else-if="previewUrl && isAudio"
|
||||
class="flex size-full cursor-pointer items-center justify-center bg-gradient-to-tr from-violet-500 via-purple-500 to-fuchsia-400"
|
||||
@click.stop="toggleAudioPreview"
|
||||
>
|
||||
<audio
|
||||
ref="audioRef"
|
||||
:src="previewUrl"
|
||||
preload="none"
|
||||
@ended="handleAudioEnded"
|
||||
/>
|
||||
<i
|
||||
:class="
|
||||
isPlayingAudio
|
||||
? 'icon-[lucide--pause] size-5 text-white'
|
||||
: 'icon-[lucide--play] size-5 text-white'
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<img
|
||||
v-else-if="previewUrl"
|
||||
:src="previewUrl"
|
||||
@@ -144,6 +184,13 @@ function handleVideoLoad(event: Event) {
|
||||
>
|
||||
{{ label ?? name }}
|
||||
</span>
|
||||
<!-- Description -->
|
||||
<span
|
||||
v-if="description && layout !== 'grid'"
|
||||
class="text-secondary line-clamp-1 block overflow-hidden text-xs"
|
||||
>
|
||||
{{ description }}
|
||||
</span>
|
||||
<!-- Meta Data -->
|
||||
<span v-if="actualDimensions" class="text-secondary block text-xs">
|
||||
{{ actualDimensions }}
|
||||
|
||||
@@ -12,7 +12,9 @@ export interface FormDropdownItem {
|
||||
name: string
|
||||
/** Original/alternate label (e.g., original filename) */
|
||||
label?: string
|
||||
/** Preview image/video URL */
|
||||
/** Short description shown below the name in list view */
|
||||
description?: string
|
||||
/** Preview image/video/audio URL */
|
||||
preview_url?: string
|
||||
/** Whether the item is immutable (public model) - used for ownership filtering */
|
||||
is_immutable?: boolean
|
||||
|
||||
@@ -214,7 +214,9 @@ const addComboWidget = (
|
||||
}
|
||||
)
|
||||
|
||||
if (inputSpec.remote) {
|
||||
if (inputSpec.remote && !inputSpec.remote.item_schema) {
|
||||
// Skip useRemoteWidget when item_schema is present —
|
||||
// RichComboWidget handles its own data fetching and rendering.
|
||||
if (!isComboWidget(widget)) {
|
||||
throw new Error(`Expected combo widget but received ${widget.type}`)
|
||||
}
|
||||
|
||||
@@ -2,10 +2,12 @@ import axios from 'axios'
|
||||
|
||||
import { useChainCallback } from '@/composables/functional/useChainCallback'
|
||||
import type { IWidget, LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import { isCloud } from '@/platform/distribution/types'
|
||||
import type { RemoteWidgetConfig } from '@/schemas/nodeDefSchema'
|
||||
import { api } from '@/scripts/api'
|
||||
import { useAuthStore } from '@/stores/authStore'
|
||||
import {
|
||||
getRemoteAuthHeaders,
|
||||
resolveRoute
|
||||
} from '../utils/resolveRemoteRoute'
|
||||
|
||||
const MAX_RETRIES = 5
|
||||
const TIMEOUT = 4096
|
||||
@@ -21,17 +23,6 @@ interface CacheEntry<T> {
|
||||
failed?: boolean
|
||||
}
|
||||
|
||||
async function getAuthHeaders() {
|
||||
if (isCloud) {
|
||||
const authStore = useAuthStore()
|
||||
const authHeader = await authStore.getAuthHeader()
|
||||
return {
|
||||
...(authHeader && { headers: authHeader })
|
||||
}
|
||||
}
|
||||
return {}
|
||||
}
|
||||
|
||||
const dataCache = new Map<string, CacheEntry<unknown>>()
|
||||
|
||||
const createCacheKey = (config: RemoteWidgetConfig): string => {
|
||||
@@ -73,9 +64,10 @@ const fetchData = async (
|
||||
) => {
|
||||
const { route, response_key, query_params, timeout = TIMEOUT } = config
|
||||
|
||||
const authHeaders = await getAuthHeaders()
|
||||
const url = resolveRoute(route)
|
||||
const authHeaders = await getRemoteAuthHeaders(route)
|
||||
|
||||
const res = await axios.get(route, {
|
||||
const res = await axios.get(url, {
|
||||
params: query_params,
|
||||
signal: controller.signal,
|
||||
timeout,
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
import type { RemoteItemSchema } from '@/schemas/nodeDefSchema'
|
||||
import type { FormDropdownItem } from '@/renderer/extensions/vueNodes/widgets/components/form/dropdown/types'
|
||||
|
||||
/** Traverse an object by dot-path, treating numeric segments as array indices */
|
||||
export function getByPath(obj: unknown, path: string): unknown {
|
||||
return path.split('.').reduce((acc: unknown, key: string) => {
|
||||
if (acc == null) return undefined
|
||||
const idx = Number(key)
|
||||
if (Number.isInteger(idx) && idx >= 0 && Array.isArray(acc)) return acc[idx]
|
||||
return (acc as Record<string, unknown>)[key]
|
||||
}, obj)
|
||||
}
|
||||
|
||||
/** Resolve a label — either dot-path or template with {field.path} placeholders */
|
||||
export function resolveLabel(template: string, item: unknown): string {
|
||||
if (!template.includes('{')) {
|
||||
return String(getByPath(item, template) ?? '')
|
||||
}
|
||||
return template.replace(/\{([^}]+)\}/g, (_, path: string) =>
|
||||
String(getByPath(item, path) ?? '')
|
||||
)
|
||||
}
|
||||
|
||||
/** Map a raw API object to a FormDropdownItem using the item_schema */
|
||||
export function mapToDropdownItem(
|
||||
raw: unknown,
|
||||
schema: RemoteItemSchema
|
||||
): FormDropdownItem {
|
||||
return {
|
||||
id: String(getByPath(raw, schema.value_field) ?? ''),
|
||||
name: resolveLabel(schema.label_field, raw),
|
||||
description: schema.description_field
|
||||
? resolveLabel(schema.description_field, raw)
|
||||
: undefined,
|
||||
preview_url: schema.preview_url_field
|
||||
? String(getByPath(raw, schema.preview_url_field) ?? '')
|
||||
: undefined
|
||||
}
|
||||
}
|
||||
|
||||
/** Extract items array from full API response using response_key */
|
||||
export function extractItems(
|
||||
response: unknown,
|
||||
responseKey?: string
|
||||
): unknown[] {
|
||||
const data = responseKey ? getByPath(response, responseKey) : response
|
||||
return Array.isArray(data) ? data : []
|
||||
}
|
||||
|
||||
/** Build search text for an item from the specified search fields */
|
||||
export function buildSearchText(raw: unknown, searchFields: string[]): string {
|
||||
return searchFields
|
||||
.map((field) => String(getByPath(raw, field) ?? ''))
|
||||
.filter(Boolean)
|
||||
.join(' ')
|
||||
.toLowerCase()
|
||||
}
|
||||
|
||||
/** Extract unique filter values from items */
|
||||
export function extractFilterValues(
|
||||
items: unknown[],
|
||||
filterField: string
|
||||
): string[] {
|
||||
const values = new Set<string>()
|
||||
for (const item of items) {
|
||||
const value = getByPath(item, filterField)
|
||||
if (value != null) values.add(String(value))
|
||||
}
|
||||
return Array.from(values).sort()
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import axios from 'axios'
|
||||
|
||||
import { getComfyApiBaseUrl } from '@/config/comfyApi'
|
||||
import { useAuthStore } from '@/stores/authStore'
|
||||
|
||||
/**
|
||||
* Check if a route is a comfy-api proxy route.
|
||||
* These routes need the comfy-api base URL prepended and always require auth.
|
||||
*/
|
||||
function isProxyRoute(route: string): boolean {
|
||||
return route.startsWith('/proxy/')
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a RemoteOptions route to a full URL.
|
||||
* - "/proxy/..." routes → prepend getComfyApiBaseUrl()
|
||||
* - Everything else → use as-is
|
||||
*/
|
||||
export function resolveRoute(route: string): string {
|
||||
if (isProxyRoute(route)) {
|
||||
return getComfyApiBaseUrl() + route
|
||||
}
|
||||
return route
|
||||
}
|
||||
|
||||
/**
|
||||
* Get auth headers for a remote request.
|
||||
* - "/proxy/..." routes → ALWAYS inject auth (comfy-api requires it)
|
||||
* - Other routes → only inject auth in cloud mode
|
||||
*/
|
||||
export async function getRemoteAuthHeaders(
|
||||
route: string
|
||||
): Promise<Record<string, any>> {
|
||||
if (isProxyRoute(route)) {
|
||||
const authStore = useAuthStore()
|
||||
const authHeader = await authStore.getAuthHeader()
|
||||
if (authHeader) {
|
||||
return { headers: authHeader }
|
||||
}
|
||||
}
|
||||
return {}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience: make an authenticated GET request to a remote route.
|
||||
*/
|
||||
export async function fetchRemoteRoute(
|
||||
route: string,
|
||||
options: {
|
||||
params?: Record<string, string>
|
||||
timeout?: number
|
||||
signal?: AbortSignal
|
||||
} = {}
|
||||
) {
|
||||
const url = resolveRoute(route)
|
||||
const authHeaders = await getRemoteAuthHeaders(route)
|
||||
return axios.get(url, { ...options, ...authHeaders })
|
||||
}
|
||||
@@ -5,6 +5,16 @@ import { resultItemType } from '@/schemas/apiSchema'
|
||||
import { CONTROL_OPTIONS } from '@/types/simplifiedWidget'
|
||||
|
||||
const zComboOption = z.union([z.string(), z.number()])
|
||||
const zRemoteItemSchema = z.object({
|
||||
value_field: z.string(),
|
||||
label_field: z.string(),
|
||||
preview_url_field: z.string().optional(),
|
||||
preview_type: z.enum(['image', 'video', 'audio']).default('image'),
|
||||
description_field: z.string().optional(),
|
||||
search_fields: z.array(z.string()).optional(),
|
||||
filter_field: z.string().optional()
|
||||
})
|
||||
|
||||
const zRemoteWidgetConfig = z.object({
|
||||
route: z.string().url().or(z.string().startsWith('/')),
|
||||
refresh: z.number().gte(128).safe().or(z.number().lte(0).safe()).optional(),
|
||||
@@ -13,7 +23,8 @@ const zRemoteWidgetConfig = z.object({
|
||||
refresh_button: z.boolean().optional(),
|
||||
control_after_refresh: z.enum(['first', 'last']).optional(),
|
||||
timeout: z.number().gte(0).optional(),
|
||||
max_retries: z.number().gte(0).optional()
|
||||
max_retries: z.number().gte(0).optional(),
|
||||
item_schema: zRemoteItemSchema.optional()
|
||||
})
|
||||
const zMultiSelectOption = z.object({
|
||||
placeholder: z.string().optional(),
|
||||
@@ -354,6 +365,7 @@ export const zMatchTypeOptions = z.object({
|
||||
export type ComfyInputsSpec = z.infer<typeof zComfyInputsSpec>
|
||||
export type ComfyOutputTypesSpec = z.infer<typeof zComfyOutputTypesSpec>
|
||||
export type ComfyNodeDef = z.infer<typeof zComfyNodeDef>
|
||||
export type RemoteItemSchema = z.infer<typeof zRemoteItemSchema>
|
||||
export type RemoteWidgetConfig = z.infer<typeof zRemoteWidgetConfig>
|
||||
|
||||
export type ComboInputOptions = z.infer<typeof zComboInputOptions>
|
||||
|
||||
Reference in New Issue
Block a user