mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-21 15:24:09 +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:
@@ -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'
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user