mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-06 08:00:05 +00:00
Add Vue File/Media Upload Widget (#4115)
This commit is contained in:
150
src/components/graph/widgets/MediaLoaderWidget.vue
Normal file
150
src/components/graph/widgets/MediaLoaderWidget.vue
Normal file
@@ -0,0 +1,150 @@
|
||||
<template>
|
||||
<div class="media-loader-widget w-full px-2">
|
||||
<div
|
||||
class="upload-area border-2 border-dashed border-surface-300 dark-theme:border-surface-600 rounded-lg p-6 text-center bg-surface-50 dark-theme:bg-surface-800 hover:bg-surface-100 dark-theme:hover:bg-surface-700 transition-colors cursor-pointer"
|
||||
:class="{
|
||||
'border-primary-500 bg-primary-50 dark-theme:bg-primary-950': isDragOver
|
||||
}"
|
||||
@click="triggerFileUpload"
|
||||
@dragover.prevent="onDragOver"
|
||||
@dragleave.prevent="onDragLeave"
|
||||
@drop.prevent="onDrop"
|
||||
>
|
||||
<div class="flex flex-col items-center gap-2">
|
||||
<i
|
||||
class="pi pi-cloud-upload text-2xl text-surface-500 dark-theme:text-surface-400"
|
||||
></i>
|
||||
<div class="text-sm text-surface-600 dark-theme:text-surface-300">
|
||||
<span>Drop your file here or </span>
|
||||
<span
|
||||
class="text-primary-600 dark-theme:text-primary-400 hover:text-primary-700 dark-theme:hover:text-primary-300 underline cursor-pointer"
|
||||
@click.stop="triggerFileUpload"
|
||||
>
|
||||
browse files
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="accept"
|
||||
class="text-xs text-surface-500 dark-theme:text-surface-400"
|
||||
>
|
||||
Accepted formats: {{ formatAcceptTypes }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Hidden file input -->
|
||||
<input
|
||||
ref="fileInput"
|
||||
type="file"
|
||||
:accept="accept"
|
||||
multiple
|
||||
class="hidden"
|
||||
@change="onFileSelect"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import type { ComponentWidget } from '@/scripts/domWidget'
|
||||
|
||||
// Props and model
|
||||
const modelValue = defineModel<string[]>({ required: true, default: () => [] })
|
||||
const { widget, accept } = defineProps<{
|
||||
widget: ComponentWidget<string[]>
|
||||
accept?: string
|
||||
}>()
|
||||
|
||||
// Reactive state
|
||||
const fileInput = ref<HTMLInputElement>()
|
||||
const isDragOver = ref(false)
|
||||
|
||||
// Computed properties
|
||||
const formatAcceptTypes = computed(() => {
|
||||
if (!accept) return ''
|
||||
return accept
|
||||
.split(',')
|
||||
.map((type) =>
|
||||
type
|
||||
.trim()
|
||||
.replace('image/', '')
|
||||
.replace('video/', '')
|
||||
.replace('audio/', '')
|
||||
)
|
||||
.join(', ')
|
||||
})
|
||||
|
||||
// Methods
|
||||
const triggerFileUpload = () => {
|
||||
fileInput.value?.click()
|
||||
}
|
||||
|
||||
const onFileSelect = (event: Event) => {
|
||||
const target = event.target as HTMLInputElement
|
||||
if (target.files) {
|
||||
handleFiles(Array.from(target.files))
|
||||
}
|
||||
}
|
||||
|
||||
const onDragOver = (event: DragEvent) => {
|
||||
event.preventDefault()
|
||||
isDragOver.value = true
|
||||
}
|
||||
|
||||
const onDragLeave = (event: DragEvent) => {
|
||||
event.preventDefault()
|
||||
isDragOver.value = false
|
||||
}
|
||||
|
||||
const onDrop = (event: DragEvent) => {
|
||||
event.preventDefault()
|
||||
isDragOver.value = false
|
||||
|
||||
if (event.dataTransfer?.files) {
|
||||
handleFiles(Array.from(event.dataTransfer.files))
|
||||
}
|
||||
}
|
||||
|
||||
const handleFiles = (files: File[]) => {
|
||||
// Filter files based on accept prop if provided
|
||||
let validFiles = files
|
||||
if (accept) {
|
||||
const acceptTypes = accept
|
||||
.split(',')
|
||||
.map((type) => type.trim().toLowerCase())
|
||||
validFiles = files.filter((file) => {
|
||||
return acceptTypes.some((acceptType) => {
|
||||
if (acceptType.includes('*')) {
|
||||
// Handle wildcard types like "image/*"
|
||||
const baseType = acceptType.split('/')[0]
|
||||
return file.type.startsWith(baseType + '/')
|
||||
}
|
||||
return file.type.toLowerCase() === acceptType
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
if (validFiles.length > 0) {
|
||||
// Emit files to parent component for handling upload
|
||||
const fileNames = validFiles.map((file) => file.name)
|
||||
modelValue.value = fileNames
|
||||
|
||||
// Trigger the widget's upload handler if available
|
||||
if ((widget.options as any)?.onFilesSelected) {
|
||||
;(widget.options as any).onFilesSelected(validFiles)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.upload-area {
|
||||
min-height: 80px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.upload-area:hover {
|
||||
border-color: var(--p-primary-500);
|
||||
}
|
||||
</style>
|
||||
@@ -2,6 +2,7 @@ import type { LGraphNode } from '@comfyorg/litegraph'
|
||||
|
||||
import { useNodeDragAndDrop } from '@/composables/node/useNodeDragAndDrop'
|
||||
import { useNodeFileInput } from '@/composables/node/useNodeFileInput'
|
||||
import { useNodeMediaUpload } from '@/composables/node/useNodeMediaUpload'
|
||||
import { useNodePaste } from '@/composables/node/useNodePaste'
|
||||
import { api } from '@/scripts/api'
|
||||
import { useToastStore } from '@/stores/toastStore'
|
||||
@@ -36,16 +37,28 @@ interface ImageUploadOptions {
|
||||
* @example 'image/png,image/jpeg,image/webp,video/webm,video/mp4'
|
||||
*/
|
||||
accept?: string
|
||||
/**
|
||||
* Whether to use the new Vue MediaLoader widget instead of traditional drag/drop/paste
|
||||
* @default true
|
||||
*/
|
||||
useMediaLoaderWidget?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds image upload to a node via drag & drop, paste, and file input.
|
||||
* Optionally can use the new Vue MediaLoader widget.
|
||||
*/
|
||||
export const useNodeImageUpload = (
|
||||
node: LGraphNode,
|
||||
options: ImageUploadOptions
|
||||
) => {
|
||||
const { fileFilter, onUploadComplete, allow_batch, accept } = options
|
||||
const {
|
||||
fileFilter,
|
||||
onUploadComplete,
|
||||
allow_batch,
|
||||
accept,
|
||||
useMediaLoaderWidget = true
|
||||
} = options
|
||||
|
||||
const isPastedFile = (file: File): boolean =>
|
||||
file.name === 'image.png' &&
|
||||
@@ -68,7 +81,23 @@ export const useNodeImageUpload = (
|
||||
return validPaths
|
||||
}
|
||||
|
||||
// Handle drag & drop
|
||||
// If using the new MediaLoader widget, set it up and return early
|
||||
if (useMediaLoaderWidget) {
|
||||
const { showMediaLoader } = useNodeMediaUpload()
|
||||
const widget = showMediaLoader(node, {
|
||||
fileFilter,
|
||||
onUploadComplete,
|
||||
allow_batch,
|
||||
accept
|
||||
})
|
||||
return {
|
||||
openFileSelection: () => {},
|
||||
handleUpload,
|
||||
mediaLoaderWidget: widget
|
||||
}
|
||||
}
|
||||
|
||||
// Traditional approach: Handle drag & drop
|
||||
useNodeDragAndDrop(node, {
|
||||
fileFilter,
|
||||
onDrop: handleUploadBatch
|
||||
|
||||
86
src/composables/node/useNodeMediaUpload.ts
Normal file
86
src/composables/node/useNodeMediaUpload.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import type { LGraphNode } from '@comfyorg/litegraph'
|
||||
|
||||
import { useNodeImageUpload } from '@/composables/node/useNodeImageUpload'
|
||||
import { useMediaLoaderWidget } from '@/composables/widgets/useMediaLoaderWidget'
|
||||
import type { InputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
|
||||
const MEDIA_LOADER_WIDGET_NAME = '$$node-media-loader'
|
||||
|
||||
interface MediaUploadOptions {
|
||||
fileFilter?: (file: File) => boolean
|
||||
onUploadComplete: (paths: string[]) => void
|
||||
allow_batch?: boolean
|
||||
accept?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Composable for handling media upload with Vue MediaLoader widget
|
||||
*/
|
||||
export function useNodeMediaUpload() {
|
||||
const mediaLoaderWidget = useMediaLoaderWidget()
|
||||
|
||||
const findMediaLoaderWidget = (node: LGraphNode) =>
|
||||
node.widgets?.find((w) => w.name === MEDIA_LOADER_WIDGET_NAME)
|
||||
|
||||
const addMediaLoaderWidget = (
|
||||
node: LGraphNode,
|
||||
options: MediaUploadOptions
|
||||
) => {
|
||||
// Set up the file upload handling using existing logic
|
||||
const { handleUpload } = useNodeImageUpload(node, options)
|
||||
|
||||
// Create the MediaLoader widget
|
||||
const widget = mediaLoaderWidget(node, {
|
||||
name: MEDIA_LOADER_WIDGET_NAME,
|
||||
type: 'MEDIA_LOADER'
|
||||
} as InputSpec)
|
||||
|
||||
// Connect the widget to the upload handler
|
||||
if (widget.options) {
|
||||
;(widget.options as any).onFilesSelected = async (files: File[]) => {
|
||||
const paths = await Promise.all(files.map(handleUpload))
|
||||
const validPaths = paths.filter((p): p is string => !!p)
|
||||
if (validPaths.length) {
|
||||
options.onUploadComplete(validPaths)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return widget
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows media loader widget for a node
|
||||
* @param node The graph node to show the widget for
|
||||
* @param options Upload configuration options
|
||||
*/
|
||||
function showMediaLoader(node: LGraphNode, options: MediaUploadOptions) {
|
||||
const widget =
|
||||
findMediaLoaderWidget(node) ?? addMediaLoaderWidget(node, options)
|
||||
node.setDirtyCanvas?.(true)
|
||||
return widget
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes media loader widget from a node
|
||||
* @param node The graph node to remove the widget from
|
||||
*/
|
||||
function removeMediaLoader(node: LGraphNode) {
|
||||
if (!node.widgets) return
|
||||
|
||||
const widgetIdx = node.widgets.findIndex(
|
||||
(w) => w.name === MEDIA_LOADER_WIDGET_NAME
|
||||
)
|
||||
|
||||
if (widgetIdx > -1) {
|
||||
node.widgets[widgetIdx].onRemove?.()
|
||||
node.widgets.splice(widgetIdx, 1)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
showMediaLoader,
|
||||
removeMediaLoader,
|
||||
addMediaLoaderWidget
|
||||
}
|
||||
}
|
||||
@@ -52,7 +52,7 @@ export const useDropdownComboWidget = (
|
||||
},
|
||||
|
||||
// Optional: minimum height for the widget (dropdown needs some height)
|
||||
getMinHeight: () => 42 + PADDING,
|
||||
getMinHeight: () => 48 + PADDING,
|
||||
|
||||
// Optional: whether to serialize this widget's value
|
||||
serialize: true
|
||||
|
||||
163
src/composables/widgets/useImageUploadMediaWidget.ts
Normal file
163
src/composables/widgets/useImageUploadMediaWidget.ts
Normal file
@@ -0,0 +1,163 @@
|
||||
import type { LGraphNode } from '@comfyorg/litegraph'
|
||||
import { IComboWidget } from '@comfyorg/litegraph/dist/types/widgets'
|
||||
|
||||
import MediaLoaderWidget from '@/components/graph/widgets/MediaLoaderWidget.vue'
|
||||
import { useNodeImage, useNodeVideo } from '@/composables/node/useNodeImage'
|
||||
import { useNodeImageUpload } from '@/composables/node/useNodeImageUpload'
|
||||
import { useValueTransform } from '@/composables/useValueTransform'
|
||||
import type { ResultItem } from '@/schemas/apiSchema'
|
||||
import { transformInputSpecV1ToV2 } from '@/schemas/nodeDef/migration'
|
||||
import type { InputSpec } from '@/schemas/nodeDefSchema'
|
||||
import { ComponentWidgetImpl, addWidget } from '@/scripts/domWidget'
|
||||
import type { ComfyWidgetConstructor } from '@/scripts/widgets'
|
||||
import { useNodeOutputStore } from '@/stores/imagePreviewStore'
|
||||
import { createAnnotatedPath } from '@/utils/formatUtil'
|
||||
import { addToComboValues } from '@/utils/litegraphUtil'
|
||||
|
||||
const ACCEPTED_IMAGE_TYPES = 'image/png,image/jpeg,image/webp'
|
||||
const ACCEPTED_VIDEO_TYPES = 'video/webm,video/mp4'
|
||||
|
||||
type InternalFile = string | ResultItem
|
||||
type InternalValue = InternalFile | InternalFile[]
|
||||
type ExposedValue = string | string[]
|
||||
|
||||
const isImageFile = (file: File) => file.type.startsWith('image/')
|
||||
const isVideoFile = (file: File) => file.type.startsWith('video/')
|
||||
|
||||
const findFileComboWidget = (node: LGraphNode, inputName: string) =>
|
||||
node.widgets!.find((w) => w.name === inputName) as IComboWidget & {
|
||||
value: ExposedValue
|
||||
}
|
||||
|
||||
export const useImageUploadMediaWidget = () => {
|
||||
const widgetConstructor: ComfyWidgetConstructor = (
|
||||
node: LGraphNode,
|
||||
inputName: string,
|
||||
inputData: InputSpec
|
||||
) => {
|
||||
const inputOptions = inputData[1] ?? {}
|
||||
const { imageInputName, allow_batch, image_folder = 'input' } = inputOptions
|
||||
const nodeOutputStore = useNodeOutputStore()
|
||||
|
||||
const isAnimated = !!inputOptions.animated_image_upload
|
||||
const isVideo = !!inputOptions.video_upload
|
||||
const accept = isVideo ? ACCEPTED_VIDEO_TYPES : ACCEPTED_IMAGE_TYPES
|
||||
const { showPreview } = isVideo ? useNodeVideo(node) : useNodeImage(node)
|
||||
|
||||
const fileFilter = isVideo ? isVideoFile : isImageFile
|
||||
// @ts-expect-error InputSpec is not typed correctly
|
||||
const fileComboWidget = findFileComboWidget(node, imageInputName)
|
||||
const initialFile = `${fileComboWidget.value}`
|
||||
const formatPath = (value: InternalFile) =>
|
||||
// @ts-expect-error InputSpec is not typed correctly
|
||||
createAnnotatedPath(value, { rootFolder: image_folder })
|
||||
|
||||
const transform = (internalValue: InternalValue): ExposedValue => {
|
||||
if (!internalValue) return initialFile
|
||||
if (Array.isArray(internalValue))
|
||||
return allow_batch
|
||||
? internalValue.map(formatPath)
|
||||
: formatPath(internalValue[0])
|
||||
return formatPath(internalValue)
|
||||
}
|
||||
|
||||
Object.defineProperty(
|
||||
fileComboWidget,
|
||||
'value',
|
||||
useValueTransform(transform, initialFile)
|
||||
)
|
||||
|
||||
// Convert the V1 input spec to V2 format for the MediaLoader widget
|
||||
const inputSpecV2 = transformInputSpecV1ToV2(inputData, { name: inputName })
|
||||
|
||||
// Handle widget dimensions based on input options
|
||||
const getMinHeight = () => {
|
||||
let baseHeight = 200
|
||||
|
||||
// Handle multiline attribute for expanded height
|
||||
if (inputOptions.multiline) {
|
||||
baseHeight = Math.max(
|
||||
baseHeight,
|
||||
inputOptions.multiline === true
|
||||
? 120
|
||||
: Number(inputOptions.multiline) || 120
|
||||
)
|
||||
}
|
||||
|
||||
// Handle other height-related attributes
|
||||
if (inputOptions.min_height) {
|
||||
baseHeight = Math.max(baseHeight, Number(inputOptions.min_height))
|
||||
}
|
||||
|
||||
return baseHeight + 8 // Add padding
|
||||
}
|
||||
|
||||
// Create the MediaLoader widget directly
|
||||
const uploadWidget = new ComponentWidgetImpl<string[], { accept?: string }>(
|
||||
{
|
||||
node,
|
||||
name: inputName,
|
||||
component: MediaLoaderWidget,
|
||||
inputSpec: inputSpecV2,
|
||||
props: {
|
||||
accept
|
||||
},
|
||||
options: {
|
||||
getValue: () => [],
|
||||
setValue: () => {},
|
||||
getMinHeight,
|
||||
serialize: false,
|
||||
onFilesSelected: async (files: File[]) => {
|
||||
// Use the existing upload infrastructure
|
||||
const { handleUpload } = useNodeImageUpload(node, {
|
||||
// @ts-expect-error InputSpec is not typed correctly
|
||||
allow_batch,
|
||||
fileFilter,
|
||||
accept,
|
||||
onUploadComplete: (output) => {
|
||||
output.forEach((path) =>
|
||||
addToComboValues(fileComboWidget, path)
|
||||
)
|
||||
// @ts-expect-error litegraph combo value type does not support arrays yet
|
||||
fileComboWidget.value = output
|
||||
fileComboWidget.callback?.(output)
|
||||
}
|
||||
})
|
||||
|
||||
// Handle each file
|
||||
for (const file of files) {
|
||||
if (fileFilter(file)) {
|
||||
await handleUpload(file)
|
||||
}
|
||||
}
|
||||
}
|
||||
} as any
|
||||
}
|
||||
)
|
||||
|
||||
// Register the widget with the node
|
||||
addWidget(node, uploadWidget as any)
|
||||
|
||||
// Add our own callback to the combo widget to render an image when it changes
|
||||
fileComboWidget.callback = function () {
|
||||
nodeOutputStore.setNodeOutputs(node, fileComboWidget.value, {
|
||||
isAnimated
|
||||
})
|
||||
node.graph?.setDirtyCanvas(true)
|
||||
}
|
||||
|
||||
// On load if we have a value then render the image
|
||||
// The value isnt set immediately so we need to wait a moment
|
||||
// No change callbacks seem to be fired on initial setting of the value
|
||||
requestAnimationFrame(() => {
|
||||
nodeOutputStore.setNodeOutputs(node, fileComboWidget.value, {
|
||||
isAnimated
|
||||
})
|
||||
showPreview({ block: false })
|
||||
})
|
||||
|
||||
return { widget: uploadWidget }
|
||||
}
|
||||
|
||||
return widgetConstructor
|
||||
}
|
||||
70
src/composables/widgets/useMediaLoaderWidget.ts
Normal file
70
src/composables/widgets/useMediaLoaderWidget.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import type { LGraphNode } from '@comfyorg/litegraph'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import MediaLoaderWidget from '@/components/graph/widgets/MediaLoaderWidget.vue'
|
||||
import type { InputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
import {
|
||||
ComponentWidgetImpl,
|
||||
type DOMWidgetOptions,
|
||||
addWidget
|
||||
} from '@/scripts/domWidget'
|
||||
import type { ComfyWidgetConstructorV2 } from '@/scripts/widgetTypes'
|
||||
|
||||
const PADDING = 8
|
||||
|
||||
interface MediaLoaderOptions {
|
||||
defaultValue?: string[]
|
||||
minHeight?: number
|
||||
accept?: string
|
||||
onFilesSelected?: (files: File[]) => void
|
||||
}
|
||||
|
||||
interface MediaLoaderWidgetOptions extends DOMWidgetOptions<string[]> {
|
||||
onFilesSelected?: (files: File[]) => void
|
||||
}
|
||||
|
||||
export const useMediaLoaderWidget = (options: MediaLoaderOptions = {}) => {
|
||||
const widgetConstructor: ComfyWidgetConstructorV2 = (
|
||||
node: LGraphNode,
|
||||
inputSpec: InputSpec
|
||||
) => {
|
||||
// Initialize widget value
|
||||
const widgetValue = ref<string[]>(options.defaultValue ?? [])
|
||||
|
||||
// Create the widget instance
|
||||
const widget = new ComponentWidgetImpl<string[], { accept?: string }>({
|
||||
node,
|
||||
name: inputSpec.name,
|
||||
component: MediaLoaderWidget,
|
||||
inputSpec,
|
||||
props: {
|
||||
accept: options.accept
|
||||
},
|
||||
options: {
|
||||
// Required: getter for widget value
|
||||
getValue: () => widgetValue.value,
|
||||
|
||||
// Required: setter for widget value
|
||||
setValue: (value: string[]) => {
|
||||
widgetValue.value = Array.isArray(value) ? value : []
|
||||
},
|
||||
|
||||
// Optional: minimum height for the widget
|
||||
getMinHeight: () => (options.minHeight ?? 500) + PADDING,
|
||||
|
||||
// Optional: whether to serialize this widget's value
|
||||
serialize: true,
|
||||
|
||||
// Custom option for file selection callback
|
||||
onFilesSelected: options.onFilesSelected
|
||||
} as MediaLoaderWidgetOptions
|
||||
})
|
||||
|
||||
// Register the widget with the node
|
||||
addWidget(node, widget as any)
|
||||
|
||||
return widget
|
||||
}
|
||||
|
||||
return widgetConstructor
|
||||
}
|
||||
@@ -9,8 +9,9 @@ import { useBadgedNumberInput } from '@/composables/widgets/useBadgedNumberInput
|
||||
import { useBooleanWidget } from '@/composables/widgets/useBooleanWidget'
|
||||
import { useColorPickerWidget } from '@/composables/widgets/useColorPickerWidget'
|
||||
import { useComboWidget } from '@/composables/widgets/useComboWidget'
|
||||
import { useImageUploadWidget } from '@/composables/widgets/useImageUploadWidget'
|
||||
import { useImageUploadMediaWidget } from '@/composables/widgets/useImageUploadMediaWidget'
|
||||
import { useMarkdownWidget } from '@/composables/widgets/useMarkdownWidget'
|
||||
import { useMediaLoaderWidget } from '@/composables/widgets/useMediaLoaderWidget'
|
||||
import { useStringWidget } from '@/composables/widgets/useStringWidget'
|
||||
import { useStringWidgetVue } from '@/composables/widgets/useStringWidgetVue'
|
||||
import { t } from '@/i18n'
|
||||
@@ -288,7 +289,8 @@ export const ComfyWidgets: Record<string, ComfyWidgetConstructor> = {
|
||||
STRING_DOM: transformWidgetConstructorV2ToV1(useStringWidget()), // Fallback to DOM-based implementation
|
||||
MARKDOWN: transformWidgetConstructorV2ToV1(useMarkdownWidget()),
|
||||
COMBO: transformWidgetConstructorV2ToV1(useComboWidget()),
|
||||
IMAGEUPLOAD: useImageUploadWidget(),
|
||||
BADGED_NUMBER: transformWidgetConstructorV2ToV1(useBadgedNumberInput()),
|
||||
COLOR: transformWidgetConstructorV2ToV1(useColorPickerWidget())
|
||||
COLOR: transformWidgetConstructorV2ToV1(useColorPickerWidget()),
|
||||
IMAGEUPLOAD: useImageUploadMediaWidget(),
|
||||
MEDIA_LOADER: transformWidgetConstructorV2ToV1(useMediaLoaderWidget()),
|
||||
BADGED_NUMBER: transformWidgetConstructorV2ToV1(useBadgedNumberInput())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user