mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-02 14:27:40 +00:00
refactor: improve Vue widget type safety and runtime prop handling
- Add proper type guards for all widget input specs (Color, TreeSelect, MultiSelect, FileUpload, Galleria) - Enhance schemas with missing properties (format, placeholder, accept, extensions, tooltip) - Fix widgets to honor runtime props like disabled while accessing spec metadata - Eliminate all 'as any' usage in widget components with proper TypeScript types - Clean separation: widget.spec.options for metadata, widget.options for runtime state - Refactor devtools into modular structure with vue_widgets showcase nodes
This commit is contained in:
41
browser_tests/utils/devtoolsSync.ts
Normal file
41
browser_tests/utils/devtoolsSync.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import fs from 'fs-extra'
|
||||
import path from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
|
||||
export function syncDevtools(targetComfyDir: string) {
|
||||
if (!targetComfyDir) {
|
||||
console.warn('syncDevtools skipped: TEST_COMFYUI_DIR not set')
|
||||
return
|
||||
}
|
||||
|
||||
const moduleDir =
|
||||
typeof __dirname !== 'undefined'
|
||||
? __dirname
|
||||
: path.dirname(fileURLToPath(import.meta.url))
|
||||
|
||||
const devtoolsSrc = path.resolve(moduleDir, '..', '..', 'tools', 'devtools')
|
||||
|
||||
if (!fs.pathExistsSync(devtoolsSrc)) {
|
||||
console.warn(
|
||||
`syncDevtools skipped: source directory not found at ${devtoolsSrc}`
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
const devtoolsDest = path.resolve(
|
||||
targetComfyDir,
|
||||
'custom_nodes',
|
||||
'ComfyUI_devtools'
|
||||
)
|
||||
|
||||
console.warn(`syncDevtools: copying ${devtoolsSrc} -> ${devtoolsDest}`)
|
||||
|
||||
try {
|
||||
fs.removeSync(devtoolsDest)
|
||||
fs.ensureDirSync(devtoolsDest)
|
||||
fs.copySync(devtoolsSrc, devtoolsDest, { overwrite: true })
|
||||
console.warn('syncDevtools: copy complete')
|
||||
} catch (error) {
|
||||
console.error(`Failed to sync DevTools to ${devtoolsDest}:`, error)
|
||||
}
|
||||
}
|
||||
@@ -27,6 +27,7 @@
|
||||
import ColorPicker from 'primevue/colorpicker'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
|
||||
import { isColorInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
|
||||
import { isColorFormat, toHexFromFormat } from '@/utils/colorUtil'
|
||||
import type { ColorFormat, HSB } from '@/utils/colorUtil'
|
||||
@@ -51,18 +52,18 @@ const emit = defineEmits<{
|
||||
}>()
|
||||
|
||||
const format = computed<ColorFormat>(() => {
|
||||
const optionFormat = props.widget.options?.format
|
||||
const spec = props.widget.spec
|
||||
if (!spec || !isColorInputSpec(spec)) {
|
||||
return 'hex'
|
||||
}
|
||||
|
||||
const optionFormat = spec.options?.format
|
||||
return isColorFormat(optionFormat) ? optionFormat : 'hex'
|
||||
})
|
||||
|
||||
type PickerValue = string | HSB
|
||||
const localValue = ref<PickerValue>(
|
||||
toHexFromFormat(
|
||||
props.modelValue || '#000000',
|
||||
isColorFormat(props.widget.options?.format)
|
||||
? props.widget.options.format
|
||||
: 'hex'
|
||||
)
|
||||
toHexFromFormat(props.modelValue || '#000000', format.value)
|
||||
)
|
||||
|
||||
watch(
|
||||
|
||||
@@ -176,7 +176,11 @@
|
||||
ref="fileInputRef"
|
||||
type="file"
|
||||
class="hidden"
|
||||
:accept="widget.options?.accept"
|
||||
:accept="
|
||||
widget.spec && isFileUploadInputSpec(widget.spec)
|
||||
? widget.spec.options?.accept
|
||||
: undefined
|
||||
"
|
||||
:aria-label="`${$t('g.upload')} ${widget.name || $t('g.file')}`"
|
||||
:multiple="false"
|
||||
@change="handleFileChange"
|
||||
@@ -190,6 +194,7 @@ import { computed, onUnmounted, ref, watch } from 'vue'
|
||||
|
||||
import { useWidgetValue } from '@/composables/graph/useWidgetValue'
|
||||
import { useTransformCompatOverlayProps } from '@/composables/useTransformCompatOverlayProps'
|
||||
import { isFileUploadInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
|
||||
|
||||
const { widget, modelValue } = defineProps<{
|
||||
|
||||
@@ -53,6 +53,7 @@ import Galleria from 'primevue/galleria'
|
||||
import { computed, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { isGalleriaInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
|
||||
import {
|
||||
GALLERIA_EXCLUDED_PROPS,
|
||||
@@ -78,9 +79,9 @@ const activeIndex = ref(0)
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const filteredProps = computed(() =>
|
||||
filterWidgetProps(props.widget.options, GALLERIA_EXCLUDED_PROPS)
|
||||
)
|
||||
const filteredProps = computed(() => {
|
||||
return filterWidgetProps(props.widget.options, GALLERIA_EXCLUDED_PROPS)
|
||||
})
|
||||
|
||||
const galleryImages = computed(() => {
|
||||
if (!value.value || !Array.isArray(value.value)) return []
|
||||
@@ -100,16 +101,22 @@ const galleryImages = computed(() => {
|
||||
})
|
||||
|
||||
const showThumbnails = computed(() => {
|
||||
const spec = props.widget.spec
|
||||
if (!spec || !isGalleriaInputSpec(spec)) {
|
||||
return galleryImages.value.length > 1
|
||||
}
|
||||
return (
|
||||
props.widget.options?.showThumbnails !== false &&
|
||||
galleryImages.value.length > 1
|
||||
spec.options?.showThumbnails !== false && galleryImages.value.length > 1
|
||||
)
|
||||
})
|
||||
|
||||
const showNavButtons = computed(() => {
|
||||
const spec = props.widget.spec
|
||||
if (!spec || !isGalleriaInputSpec(spec)) {
|
||||
return galleryImages.value.length > 1
|
||||
}
|
||||
return (
|
||||
props.widget.options?.showItemNavigators !== false &&
|
||||
galleryImages.value.length > 1
|
||||
spec.options?.showItemNavigators !== false && galleryImages.value.length > 1
|
||||
)
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -23,6 +23,7 @@ import { computed } from 'vue'
|
||||
|
||||
import { useWidgetValue } from '@/composables/graph/useWidgetValue'
|
||||
import { useTransformCompatOverlayProps } from '@/composables/useTransformCompatOverlayProps'
|
||||
import { isMultiSelectInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
import type { SimplifiedWidget, WidgetValue } from '@/types/simplifiedWidget'
|
||||
import {
|
||||
PANEL_EXCLUDED_PROPS,
|
||||
@@ -57,19 +58,20 @@ const MULTISELECT_EXCLUDED_PROPS = [
|
||||
'overlayStyle'
|
||||
] as const
|
||||
|
||||
// Extract spec options directly
|
||||
const combinedProps = computed(() => ({
|
||||
...filterWidgetProps(props.widget.options, MULTISELECT_EXCLUDED_PROPS),
|
||||
...transformCompatProps.value
|
||||
}))
|
||||
|
||||
// Extract multiselect options from widget options
|
||||
// Extract multiselect options from widget spec options
|
||||
const multiSelectOptions = computed((): T[] => {
|
||||
const options = props.widget.options
|
||||
|
||||
if (Array.isArray(options?.values)) {
|
||||
return options.values
|
||||
const spec = props.widget.spec
|
||||
if (!spec || !isMultiSelectInputSpec(spec)) {
|
||||
return []
|
||||
}
|
||||
|
||||
return []
|
||||
const values = spec.options?.values
|
||||
return Array.isArray(values) ? (values as T[]) : []
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<WidgetLayoutField :widget="widget">
|
||||
<FormSelectButton
|
||||
v-model="localValue"
|
||||
:options="widget.options?.values || []"
|
||||
:options="selectOptions"
|
||||
class="w-full"
|
||||
@update:model-value="onChange"
|
||||
/>
|
||||
@@ -10,7 +10,10 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { useStringWidgetValue } from '@/composables/graph/useWidgetValue'
|
||||
import { isSelectButtonInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
|
||||
|
||||
import FormSelectButton from './form/FormSelectButton.vue'
|
||||
@@ -31,4 +34,13 @@ const { localValue, onChange } = useStringWidgetValue(
|
||||
props.modelValue,
|
||||
emit
|
||||
)
|
||||
|
||||
// Extract spec options directly
|
||||
const selectOptions = computed(() => {
|
||||
const spec = props.widget.spec
|
||||
if (!spec || !isSelectButtonInputSpec(spec)) {
|
||||
return []
|
||||
}
|
||||
return spec.options?.values || []
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -20,11 +20,8 @@ import { computed } from 'vue'
|
||||
|
||||
import { useWidgetValue } from '@/composables/graph/useWidgetValue'
|
||||
import { useTransformCompatOverlayProps } from '@/composables/useTransformCompatOverlayProps'
|
||||
import { isTreeSelectInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
|
||||
import {
|
||||
PANEL_EXCLUDED_PROPS,
|
||||
filterWidgetProps
|
||||
} from '@/utils/widgetPropFilter'
|
||||
|
||||
import WidgetLayoutField from './layout/WidgetLayoutField.vue'
|
||||
|
||||
@@ -57,15 +54,29 @@ const { localValue, onChange } = useWidgetValue({
|
||||
// Transform compatibility props for overlay positioning
|
||||
const transformCompatProps = useTransformCompatOverlayProps()
|
||||
|
||||
// TreeSelect specific excluded props
|
||||
const TREE_SELECT_EXCLUDED_PROPS = [
|
||||
...PANEL_EXCLUDED_PROPS,
|
||||
'inputClass',
|
||||
'inputStyle'
|
||||
] as const
|
||||
const combinedProps = computed(() => {
|
||||
const spec = props.widget.spec
|
||||
if (!spec || !isTreeSelectInputSpec(spec)) {
|
||||
return {
|
||||
...props.widget.options,
|
||||
...transformCompatProps.value
|
||||
}
|
||||
}
|
||||
|
||||
const combinedProps = computed(() => ({
|
||||
...filterWidgetProps(props.widget.options, TREE_SELECT_EXCLUDED_PROPS),
|
||||
...transformCompatProps.value
|
||||
}))
|
||||
const specOptions = spec.options || {}
|
||||
return {
|
||||
// Include runtime props like disabled
|
||||
...props.widget.options,
|
||||
// PrimeVue TreeSelect expects 'options' to be an array of tree nodes
|
||||
options: (specOptions.values as TreeNode[]) || [],
|
||||
// Convert 'multiple' to PrimeVue's 'selectionMode'
|
||||
selectionMode: (specOptions.multiple ? 'multiple' : 'single') as
|
||||
| 'single'
|
||||
| 'multiple'
|
||||
| 'checkbox',
|
||||
// Pass through other props like placeholder
|
||||
placeholder: specOptions.placeholder,
|
||||
...transformCompatProps.value
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -45,7 +45,8 @@ const zColorInputSpec = zBaseInputOptions.extend({
|
||||
isOptional: z.boolean().optional(),
|
||||
options: z
|
||||
.object({
|
||||
default: z.string().optional()
|
||||
default: z.string().optional(),
|
||||
format: z.enum(['hex', 'rgb', 'hsl', 'hsb']).optional()
|
||||
})
|
||||
.optional()
|
||||
})
|
||||
@@ -54,7 +55,13 @@ const zFileUploadInputSpec = zBaseInputOptions.extend({
|
||||
type: z.literal('FILEUPLOAD'),
|
||||
name: z.string(),
|
||||
isOptional: z.boolean().optional(),
|
||||
options: z.record(z.unknown()).optional()
|
||||
options: z
|
||||
.object({
|
||||
accept: z.string().optional(),
|
||||
extensions: z.array(z.string()).optional(),
|
||||
tooltip: z.string().optional()
|
||||
})
|
||||
.optional()
|
||||
})
|
||||
|
||||
const zImageInputSpec = zBaseInputOptions.extend({
|
||||
@@ -89,7 +96,8 @@ const zTreeSelectInputSpec = zBaseInputOptions.extend({
|
||||
options: z
|
||||
.object({
|
||||
multiple: z.boolean().optional(),
|
||||
values: z.array(z.unknown()).optional()
|
||||
values: z.array(z.unknown()).optional(),
|
||||
placeholder: z.string().optional()
|
||||
})
|
||||
.optional()
|
||||
})
|
||||
@@ -123,7 +131,9 @@ const zGalleriaInputSpec = zBaseInputOptions.extend({
|
||||
isOptional: z.boolean().optional(),
|
||||
options: z
|
||||
.object({
|
||||
images: z.array(z.string()).optional()
|
||||
images: z.array(z.string()).optional(),
|
||||
showThumbnails: z.boolean().optional(),
|
||||
showItemNavigators: z.boolean().optional()
|
||||
})
|
||||
.optional()
|
||||
})
|
||||
@@ -262,3 +272,39 @@ export const isChartInputSpec = (
|
||||
): inputSpec is ChartInputSpec => {
|
||||
return inputSpec.type === 'CHART'
|
||||
}
|
||||
|
||||
export const isTreeSelectInputSpec = (
|
||||
inputSpec: InputSpec
|
||||
): inputSpec is TreeSelectInputSpec => {
|
||||
return inputSpec.type === 'TREESELECT'
|
||||
}
|
||||
|
||||
export const isSelectButtonInputSpec = (
|
||||
inputSpec: InputSpec
|
||||
): inputSpec is SelectButtonInputSpec => {
|
||||
return inputSpec.type === 'SELECTBUTTON'
|
||||
}
|
||||
|
||||
export const isMultiSelectInputSpec = (
|
||||
inputSpec: InputSpec
|
||||
): inputSpec is MultiSelectInputSpec => {
|
||||
return inputSpec.type === 'MULTISELECT'
|
||||
}
|
||||
|
||||
export const isGalleriaInputSpec = (
|
||||
inputSpec: InputSpec
|
||||
): inputSpec is GalleriaInputSpec => {
|
||||
return inputSpec.type === 'GALLERIA'
|
||||
}
|
||||
|
||||
export const isColorInputSpec = (
|
||||
inputSpec: InputSpec
|
||||
): inputSpec is ColorInputSpec => {
|
||||
return inputSpec.type === 'COLOR'
|
||||
}
|
||||
|
||||
export const isFileUploadInputSpec = (
|
||||
inputSpec: InputSpec
|
||||
): inputSpec is FileUploadInputSpec => {
|
||||
return inputSpec.type === 'FILEUPLOAD'
|
||||
}
|
||||
|
||||
@@ -6,11 +6,13 @@ This directory contains development tools and test utilities for ComfyUI, previo
|
||||
|
||||
- `__init__.py` - Server endpoints for development tools (`/api/devtools/*`)
|
||||
- `dev_nodes.py` - Development and testing nodes for ComfyUI
|
||||
- `nodes/vue_widgets.py` - Widget showcase nodes used to exercise new Vue-based widgets
|
||||
- `fake_model.safetensors` - Test fixture for model loading tests
|
||||
|
||||
## Purpose
|
||||
|
||||
These tools provide:
|
||||
|
||||
- Test endpoints for browser automation
|
||||
- Development nodes for testing various UI features
|
||||
- Mock data for consistent testing environments
|
||||
@@ -25,4 +27,4 @@ cp -r tools/devtools/* /path/to/your/ComfyUI/custom_nodes/ComfyUI_devtools/
|
||||
|
||||
## Migration
|
||||
|
||||
This directory was created as part of issue #4683 to merge the ComfyUI_devtools repository into the main frontend repository, eliminating the need for separate versioning and simplifying the development workflow.
|
||||
This directory was created as part of issue #4683 to merge the ComfyUI_devtools repository into the main frontend repository, eliminating the need for separate versioning and simplifying the development workflow.
|
||||
|
||||
@@ -1,13 +1,20 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from .nodes import (
|
||||
VueAudioPreviewComboNode,
|
||||
VueAudioRecordWidgetNode,
|
||||
VueChartWidgetNode,
|
||||
DeprecatedNode,
|
||||
DummyPatch,
|
||||
ErrorRaiseNode,
|
||||
ErrorRaiseNodeWithMessage,
|
||||
ExperimentalNode,
|
||||
VueFileUploadWidgetNode,
|
||||
LoadAnimatedImageTest,
|
||||
LongComboDropdown,
|
||||
VueMarkdownWidgetNode,
|
||||
VueGalleriaWidgetNode,
|
||||
VueImageCompareWidgetNode,
|
||||
MultiSelectNode,
|
||||
NodeWithBooleanInput,
|
||||
NodeWithDefaultInput,
|
||||
@@ -23,24 +30,36 @@ from .nodes import (
|
||||
NodeWithValidation,
|
||||
NodeWithV2ComboInput,
|
||||
ObjectPatchNode,
|
||||
VueSelectButtonWidgetNode,
|
||||
VueTextareaWidgetNode,
|
||||
VueTreeSelectMultiWidgetNode,
|
||||
VueTreeSelectWidgetNode,
|
||||
RemoteWidgetNode,
|
||||
RemoteWidgetNodeWithControlAfterRefresh,
|
||||
RemoteWidgetNodeWithParams,
|
||||
RemoteWidgetNodeWithRefresh,
|
||||
RemoteWidgetNodeWithRefreshButton,
|
||||
SimpleSlider,
|
||||
VueColorWidgetNode,
|
||||
NODE_CLASS_MAPPINGS,
|
||||
NODE_DISPLAY_NAME_MAPPINGS,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"VueAudioPreviewComboNode",
|
||||
"VueAudioRecordWidgetNode",
|
||||
"VueChartWidgetNode",
|
||||
"DeprecatedNode",
|
||||
"DummyPatch",
|
||||
"ErrorRaiseNode",
|
||||
"ErrorRaiseNodeWithMessage",
|
||||
"ExperimentalNode",
|
||||
"VueFileUploadWidgetNode",
|
||||
"LoadAnimatedImageTest",
|
||||
"LongComboDropdown",
|
||||
"VueMarkdownWidgetNode",
|
||||
"VueGalleriaWidgetNode",
|
||||
"VueImageCompareWidgetNode",
|
||||
"MultiSelectNode",
|
||||
"NodeWithBooleanInput",
|
||||
"NodeWithDefaultInput",
|
||||
@@ -56,12 +75,17 @@ __all__ = [
|
||||
"NodeWithValidation",
|
||||
"NodeWithV2ComboInput",
|
||||
"ObjectPatchNode",
|
||||
"VueSelectButtonWidgetNode",
|
||||
"VueTextareaWidgetNode",
|
||||
"VueTreeSelectMultiWidgetNode",
|
||||
"VueTreeSelectWidgetNode",
|
||||
"RemoteWidgetNode",
|
||||
"RemoteWidgetNodeWithControlAfterRefresh",
|
||||
"RemoteWidgetNodeWithParams",
|
||||
"RemoteWidgetNodeWithRefresh",
|
||||
"RemoteWidgetNodeWithRefreshButton",
|
||||
"SimpleSlider",
|
||||
"VueColorWidgetNode",
|
||||
"NODE_CLASS_MAPPINGS",
|
||||
"NODE_DISPLAY_NAME_MAPPINGS",
|
||||
]
|
||||
|
||||
@@ -44,12 +44,29 @@ from .remote import (
|
||||
NODE_CLASS_MAPPINGS as remote_class_mappings,
|
||||
NODE_DISPLAY_NAME_MAPPINGS as remote_display_name_mappings,
|
||||
)
|
||||
from .vue_widgets import (
|
||||
VueAudioPreviewComboNode,
|
||||
VueAudioRecordWidgetNode,
|
||||
VueChartWidgetNode,
|
||||
VueColorWidgetNode,
|
||||
VueFileUploadWidgetNode,
|
||||
VueGalleriaWidgetNode,
|
||||
VueImageCompareWidgetNode,
|
||||
VueMarkdownWidgetNode,
|
||||
VueSelectButtonWidgetNode,
|
||||
VueTextareaWidgetNode,
|
||||
VueTreeSelectMultiWidgetNode,
|
||||
VueTreeSelectWidgetNode,
|
||||
NODE_CLASS_MAPPINGS as vue_widgets_class_mappings,
|
||||
NODE_DISPLAY_NAME_MAPPINGS as vue_widgets_display_name_mappings,
|
||||
)
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
**errors_class_mappings,
|
||||
**inputs_class_mappings,
|
||||
**remote_class_mappings,
|
||||
**models_class_mappings,
|
||||
**vue_widgets_class_mappings,
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
@@ -57,6 +74,7 @@ NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
**inputs_display_name_mappings,
|
||||
**remote_display_name_mappings,
|
||||
**models_display_name_mappings,
|
||||
**vue_widgets_display_name_mappings,
|
||||
}
|
||||
|
||||
__all__ = [
|
||||
@@ -88,6 +106,18 @@ __all__ = [
|
||||
"RemoteWidgetNodeWithRefresh",
|
||||
"RemoteWidgetNodeWithRefreshButton",
|
||||
"SimpleSlider",
|
||||
"VueAudioPreviewComboNode",
|
||||
"VueAudioRecordWidgetNode",
|
||||
"VueChartWidgetNode",
|
||||
"VueColorWidgetNode",
|
||||
"VueFileUploadWidgetNode",
|
||||
"VueGalleriaWidgetNode",
|
||||
"VueImageCompareWidgetNode",
|
||||
"VueMarkdownWidgetNode",
|
||||
"VueSelectButtonWidgetNode",
|
||||
"VueTextareaWidgetNode",
|
||||
"VueTreeSelectMultiWidgetNode",
|
||||
"VueTreeSelectWidgetNode",
|
||||
"NODE_CLASS_MAPPINGS",
|
||||
"NODE_DISPLAY_NAME_MAPPINGS",
|
||||
]
|
||||
|
||||
477
tools/devtools/nodes/vue_widgets.py
Normal file
477
tools/devtools/nodes/vue_widgets.py
Normal file
@@ -0,0 +1,477 @@
|
||||
from __future__ import annotations
|
||||
|
||||
SAMPLE_IMAGE_DATA_URI = (
|
||||
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAIAAAD8GO2jAAAAKklEQVR4nO3NsQEAAATAMLzr/5kn2NIDmpzu+Kxe7wAAAAAAAAAAAOCwBcUDAhU8Tp3xAAAAAElFTkSuQmCC"
|
||||
)
|
||||
SAMPLE_IMAGE_DATA_URI_ALT = (
|
||||
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAIAAAD8GO2jAAAALUlEQVR4nGM0nvWBgZaAiaamj1owasGoBaMWjFowasGoBaMWjFowasGoBVQEAKDTAf3D6Eg+AAAAAElFTkSuQmCC"
|
||||
)
|
||||
SAMPLE_IMAGE_DATA_URI_THIRD = (
|
||||
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAIAAAD8GO2jAAAALUlEQVR4nGMMPJ/GQEvARFPTRy0YtWDUglELRi0YtWDUglELRi0YtWDUAioCAAbrAcZ7cQHKAAAAAElFTkSuQmCC"
|
||||
)
|
||||
|
||||
|
||||
class VueFileUploadWidgetNode:
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"uploaded_file": (
|
||||
"FILEUPLOAD",
|
||||
{
|
||||
"default": [],
|
||||
"options": {
|
||||
"extensions": ["png", "jpg", "jpeg", "webp"],
|
||||
"accept": "image/png,image/jpeg,image/webp",
|
||||
"tooltip": "Upload an image file",
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("STRING",)
|
||||
FUNCTION = "return_file"
|
||||
CATEGORY = "DevTools/Vue Widgets"
|
||||
DESCRIPTION = "Showcases the FILEUPLOAD widget"
|
||||
|
||||
def return_file(self, uploaded_file: str | None):
|
||||
return (uploaded_file or "",)
|
||||
|
||||
|
||||
class VueImageCompareWidgetNode:
|
||||
BEFORE_IMAGE = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAAB4CAIAAAA48Cq8AAAC8ElEQVR4nO3Wv0uqURzH8Y/ykNXgFBQ1NERBNIS7jTUF4VBQ7RE4SH9Hg0jQ3KA1WVO74RYWREskREE0NRUNYvrc4XZBL3V/wP3kNd+v7eGcL5zhzTlPJEynBfxr0U4fAF8TYcGCsGBBWLAgLFgQFiwICxaEBQvCggVhwYKwYEFYsCAsWBAWLAgLFoQFC8KCBWHBgrBgQViwICxYEBYsCAsWhAULwoIFYcGCsGBBWLAgLFgQFiwICxaEBQvCggVhwYKwYEFYsCAsWBAWLAgLFoQFC8KCBWHBgrBgQViwICxYEBYsuhHW6KgWFzU01IWj8b8EEf9ePq/7e8ViGhzU6amurz/Zs7WlalVPNbMdHsJdEHdbzu07aaY0Pb252Elk6pUIj4XPSbqsH55elKzqeFhra4qmVQQ6PhYDw9aWFAioVxOpZLW1pRIKAxVLOrlRbu7urrS46POz3+fQr+xfWPNzKhcViajszPt7engQBsbklStKgxVKGh5WTVa8nnVaspiLCtCh8e6m0N7NZpclMUyo3LUqk0U0Ppra+vwOaPNZCn1ddn8dEtYvVSpOYqSRMT89g4MdKRpJKpRMoqXTdfqfBWm08RF4z/y9Ktfm+fXpLLjEgcFPTd1WsHyJRKxBqVTpIqH9f1f3xBd5JJkEplI+LtMv4ucVaFZ7o2j/AuAxn4OwIEFYkCAsSBAWJAgLEoQFCcKCBGFBgrAgQViQICxIEBYkCAsShAUJwoIEYUGCsCBBWJAgLEgQFiQICxKEBQnCggRhQYKwIEFYkCAsSBAWJAgLEoQFCcKCBGFBgrAgQViQICxIEBYkCAsShAUJwoIEYUGCsCBBWJAgLEgQFiw+AcwZf7lBBNZAAA"
|
||||
AFTER_IMAGE = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAAB4CAIAAAA48Cq8AAADDklEQVR4nO3Z32uNcRzA8e/3Oc85Oz/WLFqNzjZuxKgtWpmwluxiUYoLudlKubH/QHJBuZE7LuYfoCQXolxJasmF1EjJhRrhQpQ1O85zjouzEm69d7Ler6vvU8+Pz8W7b9964u7ZL0H615J2D6C1ybCEMCwhDEsIwxLCsIQwLCEMSwjDEsKwhDAsIQxLCMMSwrCEMCwhDEsIwxLCsIQwLCEMSwjDEsKwhDAsIQxLCMMSwrCEMCwhDEsIwxLCsIQwLCEMSwjDEsKwhDAsIQxLCMMSwrCEMCwhDEsIwxLCsIQwLCEMSwjDEsKwhDAsIQxLCMMSwrCEMCwhDEsIwxLCsIRY1bA2dydTQx2dhbiaH1VbsGEd3VaYO7VufWmlpMuHKstZM2uE6eEO9LtqOzasA/3pjfnlfX351uWGcrwxX1uqN6eHDGuNA8MqprGYxjuvavsG0hDC8cFCJR9nD1dmRorlfLw6WenqiBfGy9cmK9ePVHb05FpPPZzqOj9WOrGzwA2mVZByrx6tpnML9bdfG5s6k3wSbr2szYwUT99dDCEcHyycubd47kDp5ovl+U9Zb2dyZaJ88va3EEIhFx+8+TG3UOcG0yoAwxobSLduyB3cku8pJ7s2pk/e/dnKaDXt61rZMkv5mMTQaIas2fz7Tv13qLCSGPrX5Vqb0Gg13d+f/zuXXIwz9xdrWUhiGE5NG80QQsgaobXQf406Yw33pq8/Z631sw/ZnupvBScxJDE8/1gf35wPIeztSz3OrzHUjjU2kD59v7JFfa83Py81tnT/ivjZh+zKROXS46Wz+0vHtheyZrj4aAmaRG0Rd89+afcMWoP8pSOEYQlhWEIYlhCGJYRhCWFYQhiWEIYlhGEJYVhCGJYQhiWEYQlhWEIYlhCGJYRhCWFYQhiWEIYlhGEJYVhCGJYQhiWEYQlhWEIYlhCGJYRhCWFYQhiWEIYlhGEJYVhCGJYQhiWEYQlhWEIYlhCGJYRhCWFYQhiWEIYlhGEJYVhCGJYQhiWEYQlhWEIYlhCGJYRhCWFYQvwE4Ex5XANtu7QAAAAASUVORK5CYII="
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"comparison": (
|
||||
"IMAGECOMPARE",
|
||||
{
|
||||
"default": {
|
||||
"before": BEFORE_IMAGE,
|
||||
"after": AFTER_IMAGE,
|
||||
},
|
||||
"options": {
|
||||
"beforeAlt": "Before",
|
||||
"afterAlt": "After",
|
||||
"initialPosition": 40,
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ()
|
||||
FUNCTION = "noop"
|
||||
CATEGORY = "DevTools/Vue Widgets"
|
||||
DESCRIPTION = "Showcases the IMAGECOMPARE widget"
|
||||
|
||||
def noop(self, comparison):
|
||||
return tuple()
|
||||
|
||||
|
||||
class VueTreeSelectWidgetNode:
|
||||
TREE_DATA = [
|
||||
{
|
||||
"key": "root",
|
||||
"label": "Root",
|
||||
"children": [
|
||||
{
|
||||
"key": "section-a",
|
||||
"label": "Section A",
|
||||
"children": [
|
||||
{"key": "item-a1", "label": "Item A1"},
|
||||
{"key": "item-a2", "label": "Item A2"},
|
||||
],
|
||||
},
|
||||
{
|
||||
"key": "section-b",
|
||||
"label": "Section B",
|
||||
"children": [
|
||||
{"key": "item-b1", "label": "Item B1"},
|
||||
{"key": "item-b2", "label": "Item B2"},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"selection": (
|
||||
"TREESELECT",
|
||||
{
|
||||
"default": "item-a1",
|
||||
"options": {
|
||||
"values": cls.TREE_DATA,
|
||||
"multiple": False,
|
||||
"placeholder": "Select an item",
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("STRING",)
|
||||
FUNCTION = "return_selection"
|
||||
CATEGORY = "DevTools/Vue Widgets"
|
||||
DESCRIPTION = "Showcases the TREESELECT widget"
|
||||
|
||||
def return_selection(self, selection: str):
|
||||
return (selection,)
|
||||
|
||||
|
||||
class VueTreeSelectMultiWidgetNode(VueTreeSelectWidgetNode):
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"selection": (
|
||||
"TREESELECT",
|
||||
{
|
||||
"default": ["item-a1", "item-b1"],
|
||||
"options": {
|
||||
"values": cls.TREE_DATA,
|
||||
"multiple": True,
|
||||
"placeholder": "Select items",
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("STRING",)
|
||||
OUTPUT_IS_LIST = (True,)
|
||||
FUNCTION = "return_selection"
|
||||
CATEGORY = "DevTools/Vue Widgets"
|
||||
DESCRIPTION = "Showcases the TREESELECT widget in multi-select mode"
|
||||
|
||||
def return_selection(self, selection: list[str]):
|
||||
return (selection,)
|
||||
|
||||
|
||||
class VueSelectButtonWidgetNode:
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
options = [
|
||||
{"label": "Low", "value": "low"},
|
||||
{"label": "Medium", "value": "medium"},
|
||||
{"label": "High", "value": "high"},
|
||||
]
|
||||
|
||||
return {
|
||||
"required": {
|
||||
"mode": (
|
||||
"SELECTBUTTON",
|
||||
{
|
||||
"default": "Medium",
|
||||
"options": {
|
||||
"values": ["Low", "Medium", "High"],
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("STRING",)
|
||||
FUNCTION = "return_mode"
|
||||
CATEGORY = "DevTools/Vue Widgets"
|
||||
DESCRIPTION = "Showcases the SELECTBUTTON widget"
|
||||
|
||||
def return_mode(self, mode: str):
|
||||
return (mode,)
|
||||
|
||||
|
||||
class VueTextareaWidgetNode:
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"notes": (
|
||||
"TEXTAREA",
|
||||
{
|
||||
"default": "This is a DevTools textarea widget.\nFeel free to edit me!",
|
||||
"options": {
|
||||
"rows": 4,
|
||||
"cols": 40,
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("STRING",)
|
||||
FUNCTION = "return_notes"
|
||||
CATEGORY = "DevTools/Vue Widgets"
|
||||
DESCRIPTION = "Showcases the TEXTAREA widget"
|
||||
|
||||
def return_notes(self, notes: str):
|
||||
return (notes,)
|
||||
|
||||
|
||||
class VueChartWidgetNode:
|
||||
CHART_DATA = {
|
||||
"labels": ["Iteration 1", "Iteration 2", "Iteration 3"],
|
||||
"datasets": [
|
||||
{
|
||||
"label": "Loss",
|
||||
"data": [0.95, 0.62, 0.31],
|
||||
"borderColor": "#339AF0",
|
||||
"backgroundColor": "rgba(51, 154, 240, 0.2)",
|
||||
"fill": True,
|
||||
"tension": 0.35,
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"chart": (
|
||||
"CHART",
|
||||
{
|
||||
"options": {
|
||||
"type": "line",
|
||||
"data": cls.CHART_DATA,
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("DICT",)
|
||||
FUNCTION = "return_chart"
|
||||
CATEGORY = "DevTools/Vue Widgets"
|
||||
DESCRIPTION = "Showcases the CHART widget"
|
||||
|
||||
def return_chart(self, chart):
|
||||
return (chart,)
|
||||
|
||||
|
||||
class VueGalleriaWidgetNode:
|
||||
GALLERIA_IMAGES = [
|
||||
{
|
||||
"itemImageSrc": SAMPLE_IMAGE_DATA_URI,
|
||||
"thumbnailImageSrc": SAMPLE_IMAGE_DATA_URI,
|
||||
"alt": "Warm gradient",
|
||||
},
|
||||
{
|
||||
"itemImageSrc": SAMPLE_IMAGE_DATA_URI_ALT,
|
||||
"thumbnailImageSrc": SAMPLE_IMAGE_DATA_URI_ALT,
|
||||
"alt": "Cool gradient",
|
||||
},
|
||||
{
|
||||
"itemImageSrc": SAMPLE_IMAGE_DATA_URI_THIRD,
|
||||
"thumbnailImageSrc": SAMPLE_IMAGE_DATA_URI_THIRD,
|
||||
"alt": "Fresh gradient",
|
||||
},
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"gallery": (
|
||||
"GALLERIA",
|
||||
{
|
||||
"default": cls.GALLERIA_IMAGES,
|
||||
"options": {
|
||||
"images": cls.GALLERIA_IMAGES,
|
||||
"showThumbnails": True,
|
||||
"showItemNavigators": True,
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ()
|
||||
FUNCTION = "noop"
|
||||
CATEGORY = "DevTools/Vue Widgets"
|
||||
DESCRIPTION = "Showcases the GALLERIA widget"
|
||||
|
||||
def noop(self, gallery):
|
||||
return tuple()
|
||||
|
||||
|
||||
class VueMarkdownWidgetNode:
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"markdown": (
|
||||
"MARKDOWN",
|
||||
{
|
||||
"default": "# DevTools Markdown\nThis widget renders **Markdown** content.",
|
||||
"options": {
|
||||
"content": "# DevTools Markdown\nThis widget renders **Markdown** content.",
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ()
|
||||
FUNCTION = "noop"
|
||||
CATEGORY = "DevTools/Vue Widgets"
|
||||
DESCRIPTION = "Showcases the MARKDOWN widget"
|
||||
|
||||
def noop(self, markdown):
|
||||
return tuple()
|
||||
|
||||
|
||||
class VueAudioRecordWidgetNode:
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"recording": (
|
||||
"AUDIORECORD",
|
||||
{
|
||||
"default": "",
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("STRING",)
|
||||
FUNCTION = "return_recording"
|
||||
CATEGORY = "DevTools/Vue Widgets"
|
||||
DESCRIPTION = "Showcases the AUDIORECORD widget"
|
||||
|
||||
def return_recording(self, recording: str):
|
||||
return (recording,)
|
||||
|
||||
|
||||
class VueMultiSelectWidgetNode:
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"selection": (
|
||||
"MULTISELECT",
|
||||
{
|
||||
"default": ["option1", "option3"],
|
||||
"options": {
|
||||
"values": ["option1", "option2", "option3", "option4", "option5"],
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("STRING",)
|
||||
OUTPUT_IS_LIST = (True,)
|
||||
FUNCTION = "return_selection"
|
||||
CATEGORY = "DevTools/Vue Widgets"
|
||||
DESCRIPTION = "Showcases the MULTISELECT widget"
|
||||
|
||||
def return_selection(self, selection: list[str]):
|
||||
return (selection,)
|
||||
|
||||
|
||||
class VueColorWidgetNode:
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"color": (
|
||||
"COLOR",
|
||||
{
|
||||
"default": "#ff6b6b",
|
||||
"options": {
|
||||
"tooltip": "Pick a color",
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("STRING",)
|
||||
FUNCTION = "return_color"
|
||||
CATEGORY = "DevTools/Vue Widgets"
|
||||
DESCRIPTION = "Showcases the COLOR widget"
|
||||
|
||||
def return_color(self, color: str):
|
||||
return (color,)
|
||||
|
||||
|
||||
class VueAudioPreviewComboNode:
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"audio": (
|
||||
"COMBO",
|
||||
{
|
||||
"options": ["ambient.wav", "dialog.wav"],
|
||||
"default": "ambient.wav",
|
||||
"tooltip": "Pick an audio clip",
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("STRING",)
|
||||
FUNCTION = "return_audio"
|
||||
CATEGORY = "DevTools/Vue Widgets"
|
||||
DESCRIPTION = "Showcases the COMBO widget rendered as Audio UI"
|
||||
|
||||
def return_audio(self, audio: str):
|
||||
return (audio,)
|
||||
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"DevToolsVueFileUploadWidgetNode": VueFileUploadWidgetNode,
|
||||
"DevToolsVueImageCompareWidgetNode": VueImageCompareWidgetNode,
|
||||
"DevToolsVueTreeSelectWidgetNode": VueTreeSelectWidgetNode,
|
||||
"DevToolsVueTreeSelectMultiWidgetNode": VueTreeSelectMultiWidgetNode,
|
||||
"DevToolsVueSelectButtonWidgetNode": VueSelectButtonWidgetNode,
|
||||
"DevToolsVueMultiSelectWidgetNode": VueMultiSelectWidgetNode,
|
||||
"DevToolsVueTextareaWidgetNode": VueTextareaWidgetNode,
|
||||
"DevToolsVueChartWidgetNode": VueChartWidgetNode,
|
||||
"DevToolsVueGalleriaWidgetNode": VueGalleriaWidgetNode,
|
||||
"DevToolsVueMarkdownWidgetNode": VueMarkdownWidgetNode,
|
||||
"DevToolsVueAudioRecordWidgetNode": VueAudioRecordWidgetNode,
|
||||
"DevToolsVueColorWidgetNode": VueColorWidgetNode,
|
||||
"DevToolsVueAudioPreviewComboNode": VueAudioPreviewComboNode,
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"DevToolsVueFileUploadWidgetNode": "Vue File Upload Widget",
|
||||
"DevToolsVueImageCompareWidgetNode": "Vue Image Compare Widget",
|
||||
"DevToolsVueTreeSelectWidgetNode": "Vue Tree Select Widget",
|
||||
"DevToolsVueTreeSelectMultiWidgetNode": "Vue Tree Select (Multi) Widget",
|
||||
"DevToolsVueSelectButtonWidgetNode": "Vue Select Button Widget",
|
||||
"DevToolsVueMultiSelectWidgetNode": "Vue Multi Select Widget",
|
||||
"DevToolsVueTextareaWidgetNode": "Vue Textarea Widget",
|
||||
"DevToolsVueChartWidgetNode": "Vue Chart Widget",
|
||||
"DevToolsVueGalleriaWidgetNode": "Vue Galleria Widget",
|
||||
"DevToolsVueMarkdownWidgetNode": "Vue Markdown Widget",
|
||||
"DevToolsVueAudioRecordWidgetNode": "Vue Audio Record Widget",
|
||||
"DevToolsVueColorWidgetNode": "Vue Color Widget",
|
||||
"DevToolsVueAudioPreviewComboNode": "Vue Audio Combo Widget",
|
||||
}
|
||||
|
||||
__all__ = [
|
||||
"VueFileUploadWidgetNode",
|
||||
"VueImageCompareWidgetNode",
|
||||
"VueTreeSelectWidgetNode",
|
||||
"VueTreeSelectMultiWidgetNode",
|
||||
"VueSelectButtonWidgetNode",
|
||||
"VueMultiSelectWidgetNode",
|
||||
"VueTextareaWidgetNode",
|
||||
"VueChartWidgetNode",
|
||||
"VueGalleriaWidgetNode",
|
||||
"VueMarkdownWidgetNode",
|
||||
"VueAudioRecordWidgetNode",
|
||||
"VueColorWidgetNode",
|
||||
"VueAudioPreviewComboNode",
|
||||
"NODE_CLASS_MAPPINGS",
|
||||
"NODE_DISPLAY_NAME_MAPPINGS",
|
||||
]
|
||||
Reference in New Issue
Block a user