mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-03-22 13:27:34 +00:00
Compare commits
17 Commits
fix/load-a
...
bl-vue-wid
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b06ad201d8 | ||
|
|
4808018b10 | ||
|
|
3ad9c1bd7c | ||
|
|
9056a2e89c | ||
|
|
79b5c75b85 | ||
|
|
36955407e7 | ||
|
|
951b04144a | ||
|
|
e2814ec761 | ||
|
|
8142d6fcbd | ||
|
|
e25a6e5068 | ||
|
|
1bd85099ec | ||
|
|
56f59103a5 | ||
|
|
8129ba2132 | ||
|
|
346cac0889 | ||
|
|
7573bca6a2 | ||
|
|
c25206ad3b | ||
|
|
36e4e79994 |
@@ -88,6 +88,7 @@ const { defaultPanel } = defineProps<{
|
||||
| 'server-config'
|
||||
| 'user'
|
||||
| 'credits'
|
||||
| 'widget-test'
|
||||
}>()
|
||||
|
||||
const {
|
||||
|
||||
405
src/components/dialog/content/setting/WidgetTestPanel.vue
Normal file
405
src/components/dialog/content/setting/WidgetTestPanel.vue
Normal file
@@ -0,0 +1,405 @@
|
||||
<template>
|
||||
<PanelTemplate value="Widget Test" class="widget-test-container">
|
||||
<h2 class="text-2xl font-bold mb-4">{{ $t('g.widgetTesting') }}</h2>
|
||||
|
||||
<div class="mb-4 flex gap-4">
|
||||
<label class="flex items-center gap-2">
|
||||
<input v-model="readonly" type="checkbox" />
|
||||
<span>{{ $t('g.readonlyMode') }}</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<ScrollPanel class="h-[60vh]">
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4 pr-4">
|
||||
<!-- Text Input Widget -->
|
||||
<div class="border rounded-lg p-4">
|
||||
<h3 class="font-semibold mb-2">Text Input (STRING)</h3>
|
||||
<WidgetInputText
|
||||
v-model="widgets.string.value"
|
||||
:widget="widgets.string"
|
||||
:readonly="readonly"
|
||||
@update:model-value="onValueChange('string', $event)"
|
||||
/>
|
||||
<div class="mt-2 text-sm text-gray-600">
|
||||
Value: {{ widgets.string.value }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Textarea Widget -->
|
||||
<div class="border rounded-lg p-4">
|
||||
<h3 class="font-semibold mb-2">Textarea</h3>
|
||||
<WidgetTextarea
|
||||
v-model="widgets.textarea.value"
|
||||
:widget="widgets.textarea"
|
||||
:readonly="readonly"
|
||||
@update:model-value="onValueChange('textarea', $event)"
|
||||
/>
|
||||
<div class="mt-2 text-sm text-gray-600">
|
||||
Value: {{ widgets.textarea.value }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Number Slider Widget -->
|
||||
<div class="border rounded-lg p-4">
|
||||
<h3 class="font-semibold mb-2">Number Slider (INT)</h3>
|
||||
<WidgetSlider
|
||||
v-model="widgets.int.value"
|
||||
:widget="widgets.int"
|
||||
:readonly="readonly"
|
||||
@update:model-value="onValueChange('int', $event)"
|
||||
/>
|
||||
<div class="mt-2 text-sm text-gray-600">
|
||||
Value: {{ widgets.int.value }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Float Slider Widget -->
|
||||
<div class="border rounded-lg p-4">
|
||||
<h3 class="font-semibold mb-2">Float Slider</h3>
|
||||
<WidgetSlider
|
||||
v-model="widgets.float.value"
|
||||
:widget="widgets.float"
|
||||
:readonly="readonly"
|
||||
@update:model-value="onValueChange('float', $event)"
|
||||
/>
|
||||
<div class="mt-2 text-sm text-gray-600">
|
||||
Value: {{ widgets.float.value }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toggle Switch Widget -->
|
||||
<div class="border rounded-lg p-4">
|
||||
<h3 class="font-semibold mb-2">Toggle Switch (BOOLEAN)</h3>
|
||||
<WidgetToggleSwitch
|
||||
v-model="widgets.boolean.value"
|
||||
:widget="widgets.boolean"
|
||||
:readonly="readonly"
|
||||
@update:model-value="onValueChange('boolean', $event)"
|
||||
/>
|
||||
<div class="mt-2 text-sm text-gray-600">
|
||||
Value: {{ widgets.boolean.value }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Select Widget -->
|
||||
<div class="border rounded-lg p-4">
|
||||
<h3 class="font-semibold mb-2">Select (COMBO)</h3>
|
||||
<WidgetSelect
|
||||
v-model="widgets.combo.value"
|
||||
:widget="widgets.combo"
|
||||
:readonly="readonly"
|
||||
@update:model-value="onValueChange('combo', $event)"
|
||||
/>
|
||||
<div class="mt-2 text-sm text-gray-600">
|
||||
Value: {{ widgets.combo.value }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Multi Select Widget -->
|
||||
<div class="border rounded-lg p-4">
|
||||
<h3 class="font-semibold mb-2">Multi Select</h3>
|
||||
<WidgetMultiSelect
|
||||
v-model="widgets.multiselect.value"
|
||||
:widget="widgets.multiselect"
|
||||
:readonly="readonly"
|
||||
@update:model-value="onValueChange('multiselect', $event)"
|
||||
/>
|
||||
<div class="mt-2 text-sm text-gray-600">
|
||||
Value: {{ JSON.stringify(widgets.multiselect.value) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Select Button Widget -->
|
||||
<div class="border rounded-lg p-4">
|
||||
<h3 class="font-semibold mb-2">Select Button</h3>
|
||||
<WidgetSelectButton
|
||||
v-model="widgets.selectbutton.value"
|
||||
:widget="widgets.selectbutton"
|
||||
:readonly="readonly"
|
||||
@update:model-value="onValueChange('selectbutton', $event)"
|
||||
/>
|
||||
<div class="mt-2 text-sm text-gray-600">
|
||||
Value: {{ widgets.selectbutton.value }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Color Picker Widget -->
|
||||
<div class="border rounded-lg p-4">
|
||||
<h3 class="font-semibold mb-2">Color Picker</h3>
|
||||
<WidgetColorPicker
|
||||
v-model="widgets.color.value"
|
||||
:widget="widgets.color"
|
||||
:readonly="readonly"
|
||||
@update:model-value="onValueChange('color', $event)"
|
||||
/>
|
||||
<div class="mt-2 text-sm text-gray-600">
|
||||
Value: {{ widgets.color.value }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Button Widget -->
|
||||
<div class="border rounded-lg p-4">
|
||||
<h3 class="font-semibold mb-2">Button</h3>
|
||||
<WidgetButton
|
||||
:widget="widgets.button"
|
||||
:readonly="readonly"
|
||||
@click="onButtonClick"
|
||||
/>
|
||||
<div class="mt-2 text-sm text-gray-600">
|
||||
Clicks: {{ buttonClicks }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- File Upload Widget -->
|
||||
<div class="border rounded-lg p-4">
|
||||
<h3 class="font-semibold mb-2">File Upload</h3>
|
||||
<WidgetFileUpload
|
||||
v-model="widgets.fileupload.value"
|
||||
:widget="widgets.fileupload"
|
||||
:readonly="readonly"
|
||||
@update:model-value="onValueChange('fileupload', $event)"
|
||||
/>
|
||||
<div class="mt-2 text-sm text-gray-600">
|
||||
Files: {{ widgets.fileupload.value?.length || 0 }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tree Select Widget -->
|
||||
<div class="border rounded-lg p-4">
|
||||
<h3 class="font-semibold mb-2">Tree Select</h3>
|
||||
<WidgetTreeSelect
|
||||
v-model="widgets.treeselect.value"
|
||||
:widget="widgets.treeselect"
|
||||
:readonly="readonly"
|
||||
@update:model-value="onValueChange('treeselect', $event)"
|
||||
/>
|
||||
<div class="mt-2 text-sm text-gray-600">
|
||||
Value: {{ JSON.stringify(widgets.treeselect.value) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Callback Log -->
|
||||
<div class="mt-6 border rounded-lg p-4">
|
||||
<h3 class="font-semibold mb-2">Callback Log</h3>
|
||||
<div class="font-mono text-sm max-h-40 overflow-auto">
|
||||
<div v-for="(log, index) in callbackLog" :key="index" class="py-1">
|
||||
{{ log }}
|
||||
</div>
|
||||
<div v-if="callbackLog.length === 0" class="text-gray-500">
|
||||
{{ $t('g.noCallbacksTriggered') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ScrollPanel>
|
||||
</PanelTemplate>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import ScrollPanel from 'primevue/scrollpanel'
|
||||
import { reactive, ref } from 'vue'
|
||||
|
||||
// Import all widget components from index
|
||||
import {
|
||||
WidgetButton,
|
||||
WidgetColorPicker,
|
||||
WidgetFileUpload,
|
||||
WidgetInputText,
|
||||
WidgetMultiSelect,
|
||||
WidgetSelect,
|
||||
WidgetSelectButton,
|
||||
WidgetSlider,
|
||||
WidgetTextarea,
|
||||
WidgetToggleSwitch,
|
||||
WidgetTreeSelect
|
||||
} from '@/components/graph/vueWidgets'
|
||||
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
|
||||
|
||||
import PanelTemplate from './PanelTemplate.vue'
|
||||
|
||||
// Test state
|
||||
const readonly = ref(false)
|
||||
const buttonClicks = ref(0)
|
||||
const callbackLog = ref<string[]>([])
|
||||
|
||||
// Mock widget data
|
||||
const widgets = reactive({
|
||||
string: {
|
||||
name: 'Text Input',
|
||||
type: 'STRING',
|
||||
value: 'Hello World',
|
||||
options: {
|
||||
placeholder: 'Enter text...'
|
||||
}
|
||||
} as SimplifiedWidget<string>,
|
||||
|
||||
textarea: {
|
||||
name: 'Multi-line Text',
|
||||
type: 'TEXTAREA',
|
||||
value: 'Line 1\nLine 2\nLine 3',
|
||||
options: {
|
||||
rows: 4,
|
||||
placeholder: 'Enter multiple lines...'
|
||||
}
|
||||
} as SimplifiedWidget<string>,
|
||||
|
||||
int: {
|
||||
name: 'Integer Value',
|
||||
type: 'INT',
|
||||
value: 50,
|
||||
options: {
|
||||
min: 0,
|
||||
max: 100,
|
||||
step: 1
|
||||
}
|
||||
} as SimplifiedWidget<number>,
|
||||
|
||||
float: {
|
||||
name: 'Float Value',
|
||||
type: 'FLOAT',
|
||||
value: 0.5,
|
||||
options: {
|
||||
min: 0,
|
||||
max: 1,
|
||||
step: 0.01
|
||||
}
|
||||
} as SimplifiedWidget<number>,
|
||||
|
||||
boolean: {
|
||||
name: 'Enable Feature',
|
||||
type: 'BOOLEAN',
|
||||
value: true,
|
||||
options: {}
|
||||
} as SimplifiedWidget<boolean>,
|
||||
|
||||
combo: {
|
||||
name: 'Select Option',
|
||||
type: 'COMBO',
|
||||
value: 'option2',
|
||||
options: {
|
||||
options: [
|
||||
{ label: 'Option 1', value: 'option1' },
|
||||
{ label: 'Option 2', value: 'option2' },
|
||||
{ label: 'Option 3', value: 'option3' }
|
||||
],
|
||||
placeholder: 'Choose an option...'
|
||||
}
|
||||
} as SimplifiedWidget<string>,
|
||||
|
||||
multiselect: {
|
||||
name: 'Select Multiple',
|
||||
type: 'MULTISELECT',
|
||||
value: ['option1', 'option3'],
|
||||
options: {
|
||||
options: [
|
||||
{ label: 'Option 1', value: 'option1' },
|
||||
{ label: 'Option 2', value: 'option2' },
|
||||
{ label: 'Option 3', value: 'option3' },
|
||||
{ label: 'Option 4', value: 'option4' }
|
||||
],
|
||||
placeholder: 'Choose multiple...'
|
||||
}
|
||||
} as SimplifiedWidget<string[]>,
|
||||
|
||||
selectbutton: {
|
||||
name: 'Choose Size',
|
||||
type: 'SELECTBUTTON',
|
||||
value: 'medium',
|
||||
options: {
|
||||
options: [
|
||||
{ label: 'Small', value: 'small' },
|
||||
{ label: 'Medium', value: 'medium' },
|
||||
{ label: 'Large', value: 'large' }
|
||||
]
|
||||
}
|
||||
} as SimplifiedWidget<string>,
|
||||
|
||||
color: {
|
||||
name: 'Pick Color',
|
||||
type: 'COLOR',
|
||||
value: '#3b82f6',
|
||||
options: {
|
||||
format: 'hex'
|
||||
}
|
||||
} as SimplifiedWidget<string>,
|
||||
|
||||
button: {
|
||||
name: 'Click Me',
|
||||
type: 'BUTTON',
|
||||
value: undefined as any,
|
||||
options: {
|
||||
label: 'Execute Action',
|
||||
icon: 'pi pi-play'
|
||||
}
|
||||
} as SimplifiedWidget<void>,
|
||||
|
||||
fileupload: {
|
||||
name: 'Upload Files',
|
||||
type: 'FILEUPLOAD',
|
||||
value: [],
|
||||
options: {
|
||||
accept: 'image/*',
|
||||
multiple: true
|
||||
}
|
||||
} as SimplifiedWidget<File[] | null>,
|
||||
|
||||
treeselect: {
|
||||
name: 'Select Categories',
|
||||
type: 'TREESELECT',
|
||||
value: null,
|
||||
options: {
|
||||
options: [
|
||||
{
|
||||
key: '0',
|
||||
label: 'Documents',
|
||||
children: [
|
||||
{ key: '0-0', label: 'Work' },
|
||||
{ key: '0-1', label: 'Personal' }
|
||||
]
|
||||
},
|
||||
{
|
||||
key: '1',
|
||||
label: 'Images',
|
||||
children: [
|
||||
{ key: '1-0', label: 'Photos' },
|
||||
{ key: '1-1', label: 'Screenshots' }
|
||||
]
|
||||
}
|
||||
],
|
||||
placeholder: 'Select category...'
|
||||
}
|
||||
} as SimplifiedWidget<any>
|
||||
})
|
||||
|
||||
// Add callbacks to widgets
|
||||
Object.entries(widgets).forEach(([key, widget]) => {
|
||||
if (key !== 'button') {
|
||||
widget.callback = (value: any) => {
|
||||
logCallback(`${key}: ${JSON.stringify(value)}`)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
function onValueChange(widgetType: string, value: any) {
|
||||
console.log(`Widget ${widgetType} changed:`, value)
|
||||
}
|
||||
|
||||
function onButtonClick() {
|
||||
buttonClicks.value++
|
||||
logCallback(`Button clicked ${buttonClicks.value} times`)
|
||||
}
|
||||
|
||||
function logCallback(message: string) {
|
||||
const timestamp = new Date().toLocaleTimeString()
|
||||
callbackLog.value.unshift(`[${timestamp}] ${message}`)
|
||||
if (callbackLog.value.length > 20) {
|
||||
callbackLog.value = callbackLog.value.slice(0, 20)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.widget-test-container {
|
||||
padding: 0;
|
||||
}
|
||||
</style>
|
||||
38
src/components/graph/vueWidgets/WidgetButton.vue
Normal file
38
src/components/graph/vueWidgets/WidgetButton.vue
Normal file
@@ -0,0 +1,38 @@
|
||||
<template>
|
||||
<div class="flex flex-col gap-1">
|
||||
<label v-if="widget.name" class="text-sm opacity-80">{{
|
||||
widget.name
|
||||
}}</label>
|
||||
<Button v-bind="filteredProps" :disabled="readonly" @click="handleClick" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
|
||||
import {
|
||||
BADGE_EXCLUDED_PROPS,
|
||||
filterWidgetProps
|
||||
} from '@/utils/widgetPropFilter'
|
||||
|
||||
// Button widgets don't have a v-model value, they trigger actions
|
||||
const props = defineProps<{
|
||||
widget: SimplifiedWidget<void>
|
||||
readonly?: boolean
|
||||
}>()
|
||||
|
||||
// Button specific excluded props
|
||||
const BUTTON_EXCLUDED_PROPS = [...BADGE_EXCLUDED_PROPS, 'iconClass'] as const
|
||||
|
||||
const filteredProps = computed(() =>
|
||||
filterWidgetProps(props.widget.options, BUTTON_EXCLUDED_PROPS)
|
||||
)
|
||||
|
||||
const handleClick = () => {
|
||||
if (!props.readonly && props.widget.callback) {
|
||||
props.widget.callback()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
65
src/components/graph/vueWidgets/WidgetChart.vue
Normal file
65
src/components/graph/vueWidgets/WidgetChart.vue
Normal file
@@ -0,0 +1,65 @@
|
||||
<template>
|
||||
<div class="flex flex-col gap-1">
|
||||
<label v-if="widget.name" class="text-sm opacity-80">{{
|
||||
widget.name
|
||||
}}</label>
|
||||
<div class="p-4 border border-gray-300 dark-theme:border-gray-600 rounded">
|
||||
<!-- Simple chart placeholder - can be enhanced with Chart.js when available -->
|
||||
<div
|
||||
v-if="!value || !Array.isArray(value.data)"
|
||||
class="text-center text-gray-500 dark-theme:text-gray-400"
|
||||
>
|
||||
No chart data available
|
||||
</div>
|
||||
<div v-else class="space-y-2">
|
||||
<div v-if="value.title" class="text-center font-semibold">
|
||||
{{ value.title }}
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<div
|
||||
v-for="(item, index) in value.data"
|
||||
:key="index"
|
||||
class="flex justify-between items-center"
|
||||
>
|
||||
<span class="text-sm">{{ item.label || `Item ${index + 1}` }}</span>
|
||||
<div class="flex items-center gap-2">
|
||||
<div
|
||||
class="h-3 bg-blue-500 rounded"
|
||||
:style="{
|
||||
width: `${Math.max((item.value / maxValue) * 100, 5)}px`
|
||||
}"
|
||||
></div>
|
||||
<span class="text-sm font-mono">{{ item.value }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
|
||||
|
||||
interface ChartData {
|
||||
title?: string
|
||||
data: Array<{
|
||||
label: string
|
||||
value: number
|
||||
}>
|
||||
}
|
||||
|
||||
const value = defineModel<ChartData>({ required: true })
|
||||
|
||||
defineProps<{
|
||||
widget: SimplifiedWidget<ChartData>
|
||||
readonly?: boolean
|
||||
}>()
|
||||
|
||||
const maxValue = computed(() => {
|
||||
if (!value.value?.data?.length) return 1
|
||||
return Math.max(...value.value.data.map((item) => item.value))
|
||||
})
|
||||
</script>
|
||||
33
src/components/graph/vueWidgets/WidgetColorPicker.vue
Normal file
33
src/components/graph/vueWidgets/WidgetColorPicker.vue
Normal file
@@ -0,0 +1,33 @@
|
||||
<template>
|
||||
<div class="flex flex-col gap-1">
|
||||
<label v-if="widget.name" class="text-sm opacity-80">{{
|
||||
widget.name
|
||||
}}</label>
|
||||
<ColorPicker v-model="value" v-bind="filteredProps" :disabled="readonly" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import ColorPicker from 'primevue/colorpicker'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
|
||||
import {
|
||||
PANEL_EXCLUDED_PROPS,
|
||||
filterWidgetProps
|
||||
} from '@/utils/widgetPropFilter'
|
||||
|
||||
const value = defineModel<string>({ required: true })
|
||||
|
||||
const props = defineProps<{
|
||||
widget: SimplifiedWidget<string>
|
||||
readonly?: boolean
|
||||
}>()
|
||||
|
||||
// ColorPicker specific excluded props include panel/overlay classes
|
||||
const COLOR_PICKER_EXCLUDED_PROPS = [...PANEL_EXCLUDED_PROPS] as const
|
||||
|
||||
const filteredProps = computed(() =>
|
||||
filterWidgetProps(props.widget.options, COLOR_PICKER_EXCLUDED_PROPS)
|
||||
)
|
||||
</script>
|
||||
66
src/components/graph/vueWidgets/WidgetFileUpload.vue
Normal file
66
src/components/graph/vueWidgets/WidgetFileUpload.vue
Normal file
@@ -0,0 +1,66 @@
|
||||
<template>
|
||||
<div class="flex flex-col gap-1">
|
||||
<label v-if="widget.name" class="text-sm opacity-80">{{
|
||||
widget.name
|
||||
}}</label>
|
||||
<FileUpload
|
||||
v-bind="filteredProps"
|
||||
:disabled="readonly"
|
||||
@upload="handleUpload"
|
||||
@select="handleSelect"
|
||||
@remove="handleRemove"
|
||||
@clear="handleClear"
|
||||
@error="handleError"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import FileUpload from 'primevue/fileupload'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
|
||||
import {
|
||||
STANDARD_EXCLUDED_PROPS,
|
||||
filterWidgetProps
|
||||
} from '@/utils/widgetPropFilter'
|
||||
|
||||
// FileUpload doesn't have a traditional v-model, it handles files through events
|
||||
const props = defineProps<{
|
||||
widget: SimplifiedWidget<File[] | null>
|
||||
readonly?: boolean
|
||||
}>()
|
||||
|
||||
const filteredProps = computed(() =>
|
||||
filterWidgetProps(props.widget.options, STANDARD_EXCLUDED_PROPS)
|
||||
)
|
||||
|
||||
const handleUpload = (event: any) => {
|
||||
if (!props.readonly && props.widget.callback) {
|
||||
props.widget.callback(event.files)
|
||||
}
|
||||
}
|
||||
|
||||
const handleSelect = (event: any) => {
|
||||
if (!props.readonly && props.widget.callback) {
|
||||
props.widget.callback(event.files)
|
||||
}
|
||||
}
|
||||
|
||||
const handleRemove = (event: any) => {
|
||||
if (!props.readonly && props.widget.callback) {
|
||||
props.widget.callback(event.files)
|
||||
}
|
||||
}
|
||||
|
||||
const handleClear = () => {
|
||||
if (!props.readonly && props.widget.callback) {
|
||||
props.widget.callback([])
|
||||
}
|
||||
}
|
||||
|
||||
const handleError = (event: any) => {
|
||||
// Could be extended to handle error reporting
|
||||
console.warn('File upload error:', event)
|
||||
}
|
||||
</script>
|
||||
101
src/components/graph/vueWidgets/WidgetGalleria.vue
Normal file
101
src/components/graph/vueWidgets/WidgetGalleria.vue
Normal file
@@ -0,0 +1,101 @@
|
||||
<template>
|
||||
<div class="flex flex-col gap-1">
|
||||
<label v-if="widget.name" class="text-sm opacity-80">{{
|
||||
widget.name
|
||||
}}</label>
|
||||
<Galleria
|
||||
v-model:activeIndex="activeIndex"
|
||||
:value="galleryImages"
|
||||
v-bind="filteredProps"
|
||||
:disabled="readonly"
|
||||
:show-thumbnails="showThumbnails"
|
||||
:show-indicators="showIndicators"
|
||||
:show-nav-buttons="showNavButtons"
|
||||
class="max-w-full"
|
||||
>
|
||||
<template #item="{ item }">
|
||||
<img
|
||||
:src="item.itemImageSrc || item.src || item"
|
||||
:alt="item.alt || 'Gallery image'"
|
||||
class="w-full h-auto max-h-64 object-contain"
|
||||
/>
|
||||
</template>
|
||||
<template #thumbnail="{ item }">
|
||||
<img
|
||||
:src="item.thumbnailImageSrc || item.src || item"
|
||||
:alt="item.alt || 'Gallery thumbnail'"
|
||||
class="w-16 h-16 object-cover"
|
||||
/>
|
||||
</template>
|
||||
</Galleria>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Galleria from 'primevue/galleria'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
|
||||
import {
|
||||
GALLERIA_EXCLUDED_PROPS,
|
||||
filterWidgetProps
|
||||
} from '@/utils/widgetPropFilter'
|
||||
|
||||
interface GalleryImage {
|
||||
itemImageSrc?: string
|
||||
thumbnailImageSrc?: string
|
||||
src?: string
|
||||
alt?: string
|
||||
}
|
||||
|
||||
type GalleryValue = string[] | GalleryImage[]
|
||||
|
||||
const value = defineModel<GalleryValue>({ required: true })
|
||||
|
||||
const props = defineProps<{
|
||||
widget: SimplifiedWidget<GalleryValue>
|
||||
readonly?: boolean
|
||||
}>()
|
||||
|
||||
const activeIndex = ref(0)
|
||||
|
||||
const filteredProps = computed(() =>
|
||||
filterWidgetProps(props.widget.options, GALLERIA_EXCLUDED_PROPS)
|
||||
)
|
||||
|
||||
const galleryImages = computed(() => {
|
||||
if (!value.value || !Array.isArray(value.value)) return []
|
||||
|
||||
return value.value.map((item, index) => {
|
||||
if (typeof item === 'string') {
|
||||
return {
|
||||
itemImageSrc: item,
|
||||
thumbnailImageSrc: item,
|
||||
alt: `Image ${index + 1}`
|
||||
}
|
||||
}
|
||||
return item
|
||||
})
|
||||
})
|
||||
|
||||
const showThumbnails = computed(() => {
|
||||
return (
|
||||
props.widget.options?.showThumbnails !== false &&
|
||||
galleryImages.value.length > 1
|
||||
)
|
||||
})
|
||||
|
||||
const showIndicators = computed(() => {
|
||||
return (
|
||||
props.widget.options?.showIndicators !== false &&
|
||||
galleryImages.value.length > 1
|
||||
)
|
||||
})
|
||||
|
||||
const showNavButtons = computed(() => {
|
||||
return (
|
||||
props.widget.options?.showNavButtons !== false &&
|
||||
galleryImages.value.length > 1
|
||||
)
|
||||
})
|
||||
</script>
|
||||
29
src/components/graph/vueWidgets/WidgetImage.vue
Normal file
29
src/components/graph/vueWidgets/WidgetImage.vue
Normal file
@@ -0,0 +1,29 @@
|
||||
<template>
|
||||
<div class="flex flex-col gap-1">
|
||||
<label v-if="widget.name" class="text-sm opacity-80">{{
|
||||
widget.name
|
||||
}}</label>
|
||||
<Image v-bind="filteredProps" :src="widget.value" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Image from 'primevue/image'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
|
||||
import {
|
||||
IMAGE_EXCLUDED_PROPS,
|
||||
filterWidgetProps
|
||||
} from '@/utils/widgetPropFilter'
|
||||
|
||||
// Image widgets typically don't have v-model, they display a source URL/path
|
||||
const props = defineProps<{
|
||||
widget: SimplifiedWidget<string>
|
||||
readonly?: boolean
|
||||
}>()
|
||||
|
||||
const filteredProps = computed(() =>
|
||||
filterWidgetProps(props.widget.options, IMAGE_EXCLUDED_PROPS)
|
||||
)
|
||||
</script>
|
||||
172
src/components/graph/vueWidgets/WidgetImageCompare.vue
Normal file
172
src/components/graph/vueWidgets/WidgetImageCompare.vue
Normal file
@@ -0,0 +1,172 @@
|
||||
<template>
|
||||
<div class="flex flex-col gap-1">
|
||||
<label v-if="widget.name" class="text-sm opacity-80">{{
|
||||
widget.name
|
||||
}}</label>
|
||||
<div
|
||||
class="image-compare-container relative overflow-hidden rounded border border-gray-300 dark-theme:border-gray-600"
|
||||
>
|
||||
<div
|
||||
v-if="!beforeImage || !afterImage"
|
||||
class="p-4 text-center text-gray-500 dark-theme:text-gray-400"
|
||||
>
|
||||
Before and after images required
|
||||
</div>
|
||||
<div v-else class="relative">
|
||||
<!-- After image (base layer) -->
|
||||
<Image
|
||||
v-bind="filteredProps"
|
||||
:src="afterImage"
|
||||
class="w-full h-auto"
|
||||
:alt="afterAlt"
|
||||
/>
|
||||
<!-- Before image (overlay layer) -->
|
||||
<div
|
||||
class="absolute top-0 left-0 h-full overflow-hidden transition-all duration-300 ease-in-out"
|
||||
:style="{ width: `${sliderPosition}%` }"
|
||||
>
|
||||
<Image
|
||||
v-bind="filteredProps"
|
||||
:src="beforeImage"
|
||||
class="w-full h-auto"
|
||||
:alt="beforeAlt"
|
||||
/>
|
||||
</div>
|
||||
<!-- Slider handle -->
|
||||
<div
|
||||
class="absolute top-0 h-full w-0.5 bg-white shadow-lg cursor-col-resize z-10 transition-all duration-100"
|
||||
:style="{ left: `${sliderPosition}%` }"
|
||||
@mousedown="startDrag"
|
||||
@touchstart="startDrag"
|
||||
>
|
||||
<div
|
||||
class="absolute top-1/2 -translate-y-1/2 -translate-x-1/2 w-8 h-8 bg-white rounded-full shadow-md flex items-center justify-center"
|
||||
>
|
||||
<div class="w-4 h-4 flex items-center justify-center">
|
||||
<div class="w-0.5 h-3 bg-gray-600 mr-0.5"></div>
|
||||
<div class="w-0.5 h-3 bg-gray-600"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Labels -->
|
||||
<div
|
||||
v-if="showLabels"
|
||||
class="absolute top-2 left-2 px-2 py-1 bg-black bg-opacity-50 text-white text-xs rounded"
|
||||
>
|
||||
{{ beforeLabel }}
|
||||
</div>
|
||||
<div
|
||||
v-if="showLabels"
|
||||
class="absolute top-2 right-2 px-2 py-1 bg-black bg-opacity-50 text-white text-xs rounded"
|
||||
>
|
||||
{{ afterLabel }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Image from 'primevue/image'
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||
|
||||
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
|
||||
import {
|
||||
IMAGE_EXCLUDED_PROPS,
|
||||
filterWidgetProps
|
||||
} from '@/utils/widgetPropFilter'
|
||||
|
||||
interface ImageCompareValue {
|
||||
before: string
|
||||
after: string
|
||||
beforeAlt?: string
|
||||
afterAlt?: string
|
||||
beforeLabel?: string
|
||||
afterLabel?: string
|
||||
showLabels?: boolean
|
||||
initialPosition?: number
|
||||
}
|
||||
|
||||
// Image compare widgets typically don't have v-model, they display comparison
|
||||
const props = defineProps<{
|
||||
widget: SimplifiedWidget<ImageCompareValue>
|
||||
readonly?: boolean
|
||||
}>()
|
||||
|
||||
const sliderPosition = ref(50) // Default to 50% (middle)
|
||||
const isDragging = ref(false)
|
||||
|
||||
const filteredProps = computed(() =>
|
||||
filterWidgetProps(props.widget.options, IMAGE_EXCLUDED_PROPS)
|
||||
)
|
||||
|
||||
const beforeImage = computed(() => {
|
||||
const value = props.widget.value
|
||||
return typeof value === 'string' ? value : value?.before
|
||||
})
|
||||
|
||||
const afterImage = computed(() => {
|
||||
const value = props.widget.value
|
||||
return typeof value === 'string' ? value : value?.after
|
||||
})
|
||||
|
||||
const beforeAlt = computed(
|
||||
() => props.widget.value?.beforeAlt || 'Before image'
|
||||
)
|
||||
const afterAlt = computed(() => props.widget.value?.afterAlt || 'After image')
|
||||
const beforeLabel = computed(() => props.widget.value?.beforeLabel || 'Before')
|
||||
const afterLabel = computed(() => props.widget.value?.afterLabel || 'After')
|
||||
const showLabels = computed(() => props.widget.value?.showLabels !== false)
|
||||
|
||||
onMounted(() => {
|
||||
if (props.widget.value?.initialPosition !== undefined) {
|
||||
sliderPosition.value = Math.max(
|
||||
0,
|
||||
Math.min(100, props.widget.value.initialPosition)
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
const startDrag = (event: MouseEvent | TouchEvent) => {
|
||||
if (props.readonly) return
|
||||
|
||||
isDragging.value = true
|
||||
event.preventDefault()
|
||||
|
||||
const handleMouseMove = (e: MouseEvent | TouchEvent) => {
|
||||
if (!isDragging.value) return
|
||||
updateSliderPosition(e)
|
||||
}
|
||||
|
||||
const handleMouseUp = () => {
|
||||
isDragging.value = false
|
||||
document.removeEventListener('mousemove', handleMouseMove)
|
||||
document.removeEventListener('mouseup', handleMouseUp)
|
||||
document.removeEventListener('touchmove', handleMouseMove)
|
||||
document.removeEventListener('touchend', handleMouseUp)
|
||||
}
|
||||
|
||||
document.addEventListener('mousemove', handleMouseMove)
|
||||
document.addEventListener('mouseup', handleMouseUp)
|
||||
document.addEventListener('touchmove', handleMouseMove)
|
||||
document.addEventListener('touchend', handleMouseUp)
|
||||
}
|
||||
|
||||
const updateSliderPosition = (event: MouseEvent | TouchEvent) => {
|
||||
const container = (event.target as HTMLElement).closest(
|
||||
'.image-compare-container'
|
||||
)
|
||||
if (!container) return
|
||||
|
||||
const rect = container.getBoundingClientRect()
|
||||
const clientX = 'touches' in event ? event.touches[0].clientX : event.clientX
|
||||
const x = clientX - rect.left
|
||||
const percentage = Math.max(0, Math.min(100, (x / rect.width) * 100))
|
||||
|
||||
sliderPosition.value = percentage
|
||||
}
|
||||
|
||||
onUnmounted(() => {
|
||||
isDragging.value = false
|
||||
})
|
||||
</script>
|
||||
30
src/components/graph/vueWidgets/WidgetInputText.vue
Normal file
30
src/components/graph/vueWidgets/WidgetInputText.vue
Normal file
@@ -0,0 +1,30 @@
|
||||
<template>
|
||||
<div class="flex flex-col gap-1">
|
||||
<label v-if="widget.name" class="text-sm opacity-80">{{
|
||||
widget.name
|
||||
}}</label>
|
||||
<InputText v-model="value" v-bind="filteredProps" :disabled="readonly" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import InputText from 'primevue/inputtext'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
|
||||
import {
|
||||
INPUT_EXCLUDED_PROPS,
|
||||
filterWidgetProps
|
||||
} from '@/utils/widgetPropFilter'
|
||||
|
||||
const value = defineModel<string>({ required: true })
|
||||
|
||||
const props = defineProps<{
|
||||
widget: SimplifiedWidget<string>
|
||||
readonly?: boolean
|
||||
}>()
|
||||
|
||||
const filteredProps = computed(() =>
|
||||
filterWidgetProps(props.widget.options, INPUT_EXCLUDED_PROPS)
|
||||
)
|
||||
</script>
|
||||
36
src/components/graph/vueWidgets/WidgetMultiSelect.vue
Normal file
36
src/components/graph/vueWidgets/WidgetMultiSelect.vue
Normal file
@@ -0,0 +1,36 @@
|
||||
<template>
|
||||
<div class="flex flex-col gap-1">
|
||||
<label v-if="widget.name" class="text-sm opacity-80">{{
|
||||
widget.name
|
||||
}}</label>
|
||||
<MultiSelect v-model="value" v-bind="filteredProps" :disabled="readonly" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import MultiSelect from 'primevue/multiselect'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
|
||||
import {
|
||||
PANEL_EXCLUDED_PROPS,
|
||||
filterWidgetProps
|
||||
} from '@/utils/widgetPropFilter'
|
||||
|
||||
const value = defineModel<any[]>({ required: true })
|
||||
|
||||
const props = defineProps<{
|
||||
widget: SimplifiedWidget<any[]>
|
||||
readonly?: boolean
|
||||
}>()
|
||||
|
||||
// MultiSelect specific excluded props include overlay styles
|
||||
const MULTISELECT_EXCLUDED_PROPS = [
|
||||
...PANEL_EXCLUDED_PROPS,
|
||||
'overlayStyle'
|
||||
] as const
|
||||
|
||||
const filteredProps = computed(() =>
|
||||
filterWidgetProps(props.widget.options, MULTISELECT_EXCLUDED_PROPS)
|
||||
)
|
||||
</script>
|
||||
30
src/components/graph/vueWidgets/WidgetSelect.vue
Normal file
30
src/components/graph/vueWidgets/WidgetSelect.vue
Normal file
@@ -0,0 +1,30 @@
|
||||
<template>
|
||||
<div class="flex flex-col gap-1">
|
||||
<label v-if="widget.name" class="text-sm opacity-80">{{
|
||||
widget.name
|
||||
}}</label>
|
||||
<Select v-model="value" v-bind="filteredProps" :disabled="readonly" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Select from 'primevue/select'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
|
||||
import {
|
||||
PANEL_EXCLUDED_PROPS,
|
||||
filterWidgetProps
|
||||
} from '@/utils/widgetPropFilter'
|
||||
|
||||
const value = defineModel<any>({ required: true })
|
||||
|
||||
const props = defineProps<{
|
||||
widget: SimplifiedWidget<any>
|
||||
readonly?: boolean
|
||||
}>()
|
||||
|
||||
const filteredProps = computed(() =>
|
||||
filterWidgetProps(props.widget.options, PANEL_EXCLUDED_PROPS)
|
||||
)
|
||||
</script>
|
||||
30
src/components/graph/vueWidgets/WidgetSelectButton.vue
Normal file
30
src/components/graph/vueWidgets/WidgetSelectButton.vue
Normal file
@@ -0,0 +1,30 @@
|
||||
<template>
|
||||
<div class="flex flex-col gap-1">
|
||||
<label v-if="widget.name" class="text-sm opacity-80">{{
|
||||
widget.name
|
||||
}}</label>
|
||||
<SelectButton v-model="value" v-bind="filteredProps" :disabled="readonly" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import SelectButton from 'primevue/selectbutton'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
|
||||
import {
|
||||
STANDARD_EXCLUDED_PROPS,
|
||||
filterWidgetProps
|
||||
} from '@/utils/widgetPropFilter'
|
||||
|
||||
const value = defineModel<any>({ required: true })
|
||||
|
||||
const props = defineProps<{
|
||||
widget: SimplifiedWidget<any>
|
||||
readonly?: boolean
|
||||
}>()
|
||||
|
||||
const filteredProps = computed(() =>
|
||||
filterWidgetProps(props.widget.options, STANDARD_EXCLUDED_PROPS)
|
||||
)
|
||||
</script>
|
||||
30
src/components/graph/vueWidgets/WidgetSlider.vue
Normal file
30
src/components/graph/vueWidgets/WidgetSlider.vue
Normal file
@@ -0,0 +1,30 @@
|
||||
<template>
|
||||
<div class="flex flex-col gap-1">
|
||||
<label v-if="widget.name" class="text-sm opacity-80">{{
|
||||
widget.name
|
||||
}}</label>
|
||||
<Slider v-model="value" v-bind="filteredProps" :disabled="readonly" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Slider from 'primevue/slider'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
|
||||
import {
|
||||
STANDARD_EXCLUDED_PROPS,
|
||||
filterWidgetProps
|
||||
} from '@/utils/widgetPropFilter'
|
||||
|
||||
const value = defineModel<number>({ required: true })
|
||||
|
||||
const props = defineProps<{
|
||||
widget: SimplifiedWidget<number>
|
||||
readonly?: boolean
|
||||
}>()
|
||||
|
||||
const filteredProps = computed(() =>
|
||||
filterWidgetProps(props.widget.options, STANDARD_EXCLUDED_PROPS)
|
||||
)
|
||||
</script>
|
||||
30
src/components/graph/vueWidgets/WidgetTextarea.vue
Normal file
30
src/components/graph/vueWidgets/WidgetTextarea.vue
Normal file
@@ -0,0 +1,30 @@
|
||||
<template>
|
||||
<div class="flex flex-col gap-1">
|
||||
<label v-if="widget.name" class="text-sm opacity-80">{{
|
||||
widget.name
|
||||
}}</label>
|
||||
<Textarea v-model="value" v-bind="filteredProps" :disabled="readonly" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Textarea from 'primevue/textarea'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
|
||||
import {
|
||||
INPUT_EXCLUDED_PROPS,
|
||||
filterWidgetProps
|
||||
} from '@/utils/widgetPropFilter'
|
||||
|
||||
const value = defineModel<string>({ required: true })
|
||||
|
||||
const props = defineProps<{
|
||||
widget: SimplifiedWidget<string>
|
||||
readonly?: boolean
|
||||
}>()
|
||||
|
||||
const filteredProps = computed(() =>
|
||||
filterWidgetProps(props.widget.options, INPUT_EXCLUDED_PROPS)
|
||||
)
|
||||
</script>
|
||||
51
src/components/graph/vueWidgets/WidgetToggleSwitch.vue
Normal file
51
src/components/graph/vueWidgets/WidgetToggleSwitch.vue
Normal file
@@ -0,0 +1,51 @@
|
||||
<template>
|
||||
<div class="flex flex-col gap-1">
|
||||
<label v-if="widget.name" class="text-sm opacity-80">{{
|
||||
widget.name
|
||||
}}</label>
|
||||
<ToggleSwitch
|
||||
v-model="value"
|
||||
v-bind="filteredProps"
|
||||
:disabled="readonly"
|
||||
:pt="{
|
||||
root: {
|
||||
class:
|
||||
'hover:outline hover:outline-1 hover:outline-[#5B5E7D] rounded-full'
|
||||
},
|
||||
slider: ({ props }) => ({
|
||||
style: {
|
||||
backgroundColor: props.modelValue ? '#0b8ce9' : '#0e0e12'
|
||||
}
|
||||
}),
|
||||
handle: ({ props }) => ({
|
||||
style: {
|
||||
backgroundColor: props.modelValue ? '#ffffff' : '#5b5e7d'
|
||||
}
|
||||
})
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import ToggleSwitch from 'primevue/toggleswitch'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
|
||||
import type { WidgetToggleSwitchProps } from '@/types/widgetPropTypes'
|
||||
import {
|
||||
INPUT_EXCLUDED_PROPS,
|
||||
filterWidgetProps
|
||||
} from '@/utils/widgetPropFilter'
|
||||
|
||||
const value = defineModel<boolean>({ required: true })
|
||||
|
||||
const props = defineProps<{
|
||||
widget: SimplifiedWidget<boolean, WidgetToggleSwitchProps>
|
||||
readonly?: boolean
|
||||
}>()
|
||||
|
||||
const filteredProps = computed(() =>
|
||||
filterWidgetProps(props.widget.options, INPUT_EXCLUDED_PROPS)
|
||||
)
|
||||
</script>
|
||||
37
src/components/graph/vueWidgets/WidgetTreeSelect.vue
Normal file
37
src/components/graph/vueWidgets/WidgetTreeSelect.vue
Normal file
@@ -0,0 +1,37 @@
|
||||
<template>
|
||||
<div class="flex flex-col gap-1">
|
||||
<label v-if="widget.name" class="text-sm opacity-80">{{
|
||||
widget.name
|
||||
}}</label>
|
||||
<TreeSelect v-model="value" v-bind="filteredProps" :disabled="readonly" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import TreeSelect from 'primevue/treeselect'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
|
||||
import {
|
||||
PANEL_EXCLUDED_PROPS,
|
||||
filterWidgetProps
|
||||
} from '@/utils/widgetPropFilter'
|
||||
|
||||
const value = defineModel<any>({ required: true })
|
||||
|
||||
const props = defineProps<{
|
||||
widget: SimplifiedWidget<any>
|
||||
readonly?: boolean
|
||||
}>()
|
||||
|
||||
// TreeSelect specific excluded props
|
||||
const TREE_SELECT_EXCLUDED_PROPS = [
|
||||
...PANEL_EXCLUDED_PROPS,
|
||||
'inputClass',
|
||||
'inputStyle'
|
||||
] as const
|
||||
|
||||
const filteredProps = computed(() =>
|
||||
filterWidgetProps(props.widget.options, TREE_SELECT_EXCLUDED_PROPS)
|
||||
)
|
||||
</script>
|
||||
27
src/components/graph/vueWidgets/index.ts
Normal file
27
src/components/graph/vueWidgets/index.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* Central export file for all Vue widget components and utilities
|
||||
*/
|
||||
|
||||
// Widget Components
|
||||
export { default as WidgetButton } from './WidgetButton.vue'
|
||||
export { default as WidgetChart } from './WidgetChart.vue'
|
||||
export { default as WidgetColorPicker } from './WidgetColorPicker.vue'
|
||||
export { default as WidgetFileUpload } from './WidgetFileUpload.vue'
|
||||
export { default as WidgetGalleria } from './WidgetGalleria.vue'
|
||||
export { default as WidgetImage } from './WidgetImage.vue'
|
||||
export { default as WidgetImageCompare } from './WidgetImageCompare.vue'
|
||||
export { default as WidgetInputText } from './WidgetInputText.vue'
|
||||
export { default as WidgetMultiSelect } from './WidgetMultiSelect.vue'
|
||||
export { default as WidgetSelect } from './WidgetSelect.vue'
|
||||
export { default as WidgetSelectButton } from './WidgetSelectButton.vue'
|
||||
export { default as WidgetSlider } from './WidgetSlider.vue'
|
||||
export { default as WidgetTextarea } from './WidgetTextarea.vue'
|
||||
export { default as WidgetToggleSwitch } from './WidgetToggleSwitch.vue'
|
||||
export { default as WidgetTreeSelect } from './WidgetTreeSelect.vue'
|
||||
|
||||
// Registry and Utilities
|
||||
export {
|
||||
WidgetType,
|
||||
widgetTypeToComponent,
|
||||
getWidgetComponent
|
||||
} from './widgetRegistry'
|
||||
80
src/components/graph/vueWidgets/widgetRegistry.ts
Normal file
80
src/components/graph/vueWidgets/widgetRegistry.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
/**
|
||||
* Widget type registry and component mapping for Vue-based widgets
|
||||
*/
|
||||
import type { Component } from 'vue'
|
||||
|
||||
// Component imports
|
||||
import WidgetButton from './WidgetButton.vue'
|
||||
import WidgetChart from './WidgetChart.vue'
|
||||
import WidgetColorPicker from './WidgetColorPicker.vue'
|
||||
import WidgetFileUpload from './WidgetFileUpload.vue'
|
||||
import WidgetGalleria from './WidgetGalleria.vue'
|
||||
import WidgetImage from './WidgetImage.vue'
|
||||
import WidgetImageCompare from './WidgetImageCompare.vue'
|
||||
import WidgetInputText from './WidgetInputText.vue'
|
||||
import WidgetMultiSelect from './WidgetMultiSelect.vue'
|
||||
import WidgetSelect from './WidgetSelect.vue'
|
||||
import WidgetSelectButton from './WidgetSelectButton.vue'
|
||||
import WidgetSlider from './WidgetSlider.vue'
|
||||
import WidgetTextarea from './WidgetTextarea.vue'
|
||||
import WidgetToggleSwitch from './WidgetToggleSwitch.vue'
|
||||
import WidgetTreeSelect from './WidgetTreeSelect.vue'
|
||||
|
||||
/**
|
||||
* Enum of all available widget types
|
||||
*/
|
||||
export enum WidgetType {
|
||||
BUTTON = 'BUTTON',
|
||||
STRING = 'STRING',
|
||||
INT = 'INT',
|
||||
FLOAT = 'FLOAT',
|
||||
NUMBER = 'NUMBER',
|
||||
BOOLEAN = 'BOOLEAN',
|
||||
COMBO = 'COMBO',
|
||||
COLOR = 'COLOR',
|
||||
MULTISELECT = 'MULTISELECT',
|
||||
SELECTBUTTON = 'SELECTBUTTON',
|
||||
SLIDER = 'SLIDER',
|
||||
TEXTAREA = 'TEXTAREA',
|
||||
TOGGLESWITCH = 'TOGGLESWITCH',
|
||||
CHART = 'CHART',
|
||||
IMAGE = 'IMAGE',
|
||||
IMAGECOMPARE = 'IMAGECOMPARE',
|
||||
GALLERIA = 'GALLERIA',
|
||||
FILEUPLOAD = 'FILEUPLOAD',
|
||||
TREESELECT = 'TREESELECT'
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps widget types to their corresponding Vue components
|
||||
* Components will be added as they are implemented
|
||||
*/
|
||||
export const widgetTypeToComponent: Record<string, Component> = {
|
||||
// Components will be uncommented as they are implemented
|
||||
[WidgetType.BUTTON]: WidgetButton,
|
||||
[WidgetType.STRING]: WidgetInputText,
|
||||
[WidgetType.INT]: WidgetSlider,
|
||||
[WidgetType.FLOAT]: WidgetSlider,
|
||||
[WidgetType.NUMBER]: WidgetSlider, // For compatibility
|
||||
[WidgetType.BOOLEAN]: WidgetToggleSwitch,
|
||||
[WidgetType.COMBO]: WidgetSelect,
|
||||
[WidgetType.COLOR]: WidgetColorPicker,
|
||||
[WidgetType.MULTISELECT]: WidgetMultiSelect,
|
||||
[WidgetType.SELECTBUTTON]: WidgetSelectButton,
|
||||
[WidgetType.SLIDER]: WidgetSlider,
|
||||
[WidgetType.TEXTAREA]: WidgetTextarea,
|
||||
[WidgetType.TOGGLESWITCH]: WidgetToggleSwitch,
|
||||
[WidgetType.CHART]: WidgetChart,
|
||||
[WidgetType.IMAGE]: WidgetImage,
|
||||
[WidgetType.IMAGECOMPARE]: WidgetImageCompare,
|
||||
[WidgetType.GALLERIA]: WidgetGalleria,
|
||||
[WidgetType.FILEUPLOAD]: WidgetFileUpload,
|
||||
[WidgetType.TREESELECT]: WidgetTreeSelect
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to get widget component by type
|
||||
*/
|
||||
export function getWidgetComponent(type: string): Component | undefined {
|
||||
return widgetTypeToComponent[type]
|
||||
}
|
||||
@@ -28,6 +28,7 @@ export function useSettingUI(
|
||||
| 'server-config'
|
||||
| 'user'
|
||||
| 'credits'
|
||||
| 'widget-test'
|
||||
) {
|
||||
const { t } = useI18n()
|
||||
const { isLoggedIn } = useCurrentUser()
|
||||
@@ -127,6 +128,17 @@ export function useSettingUI(
|
||||
)
|
||||
}
|
||||
|
||||
const widgetTestPanel: SettingPanelItem = {
|
||||
node: {
|
||||
key: 'widget-test',
|
||||
label: 'Widget Test',
|
||||
children: []
|
||||
},
|
||||
component: defineAsyncComponent(
|
||||
() => import('@/components/dialog/content/setting/WidgetTestPanel.vue')
|
||||
)
|
||||
}
|
||||
|
||||
const panels = computed<SettingPanelItem[]>(() =>
|
||||
[
|
||||
aboutPanel,
|
||||
@@ -134,6 +146,7 @@ export function useSettingUI(
|
||||
userPanel,
|
||||
keybindingPanel,
|
||||
extensionPanel,
|
||||
widgetTestPanel,
|
||||
...(isElectron() ? [serverConfigPanel] : [])
|
||||
].filter((panel) => panel.component)
|
||||
)
|
||||
@@ -182,6 +195,7 @@ export function useSettingUI(
|
||||
children: [
|
||||
keybindingPanel.node,
|
||||
extensionPanel.node,
|
||||
widgetTestPanel.node,
|
||||
aboutPanel.node,
|
||||
...(isElectron() ? [serverConfigPanel.node] : [])
|
||||
].map(translateCategory)
|
||||
|
||||
@@ -72,6 +72,9 @@
|
||||
"searchFailedMessage": "We couldn't find any settings matching your search. Try adjusting your search terms.",
|
||||
"noTasksFound": "No Tasks Found",
|
||||
"noTasksFoundMessage": "There are no tasks in the queue.",
|
||||
"widgetTesting": "Widget Testing",
|
||||
"readonlyMode": "Readonly Mode",
|
||||
"noCallbacksTriggered": "No callbacks triggered yet...",
|
||||
"newFolder": "New Folder",
|
||||
"enableAll": "Enable All",
|
||||
"disableAll": "Disable All",
|
||||
@@ -916,6 +919,7 @@
|
||||
"Light": "Light",
|
||||
"User": "User",
|
||||
"Credits": "Credits",
|
||||
"Widget Test": "Widget Test",
|
||||
"API Nodes": "API Nodes"
|
||||
},
|
||||
"serverConfigItems": {
|
||||
|
||||
27
src/types/simplifiedWidget.ts
Normal file
27
src/types/simplifiedWidget.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* Simplified widget interface for Vue-based node rendering
|
||||
* Removes all DOM manipulation and positioning concerns
|
||||
*/
|
||||
|
||||
export interface SimplifiedWidget<T = any, O = Record<string, any>> {
|
||||
/** Display name of the widget */
|
||||
name: string
|
||||
|
||||
/** Widget type identifier (e.g., 'STRING', 'INT', 'COMBO') */
|
||||
type: string
|
||||
|
||||
/** Current value of the widget */
|
||||
value: T
|
||||
|
||||
/** Widget options including filtered PrimeVue props */
|
||||
options?: O
|
||||
|
||||
/** Callback fired when value changes */
|
||||
callback?: (value: T) => void
|
||||
|
||||
/** Optional serialization method for custom value handling */
|
||||
serializeValue?: () => any
|
||||
|
||||
/** Optional method to compute widget size requirements */
|
||||
computeSize?: () => { minHeight: number; maxHeight?: number }
|
||||
}
|
||||
30
src/types/widgetPropTypes.ts
Normal file
30
src/types/widgetPropTypes.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Type definitions for widget component props
|
||||
*
|
||||
* These interfaces define the subset of PrimeVue component props that are exposed
|
||||
* for the node-based widget system. They exclude props that allow custom styling,
|
||||
* colors, arbitrary CSS, or could create chaotic interfaces.
|
||||
*
|
||||
* Based on the design authority at:
|
||||
* https://www.figma.com/design/CmhEJxo4oZSuYpqG1Yc39w/Nodes-V3?node-id=441-7806&m=dev
|
||||
*/
|
||||
import type { ToggleSwitchProps as PrimeVueToggleSwitchProps } from 'primevue/toggleswitch'
|
||||
|
||||
/**
|
||||
* Widget ToggleSwitch Component
|
||||
* Excludes: style, class, inputClass, inputStyle, dt, pt, ptOptions, unstyled
|
||||
*
|
||||
* These props are excluded from widget.options to prevent external styling overrides.
|
||||
* The widget component itself can still use these props internally for consistent styling.
|
||||
*/
|
||||
export type WidgetToggleSwitchProps = Omit<
|
||||
PrimeVueToggleSwitchProps,
|
||||
| 'style'
|
||||
| 'class'
|
||||
| 'inputClass'
|
||||
| 'inputStyle'
|
||||
| 'dt'
|
||||
| 'pt'
|
||||
| 'ptOptions'
|
||||
| 'unstyled'
|
||||
>
|
||||
76
src/utils/widgetPropFilter.ts
Normal file
76
src/utils/widgetPropFilter.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
/**
|
||||
* Widget prop filtering utilities
|
||||
* Filters out style-related and customization props from PrimeVue components
|
||||
* to maintain consistent widget appearance across the application
|
||||
*/
|
||||
|
||||
// Props to exclude based on the widget interface specifications
|
||||
export const STANDARD_EXCLUDED_PROPS = [
|
||||
'style',
|
||||
'class',
|
||||
'dt',
|
||||
'pt',
|
||||
'ptOptions',
|
||||
'unstyled'
|
||||
] as const
|
||||
|
||||
export const INPUT_EXCLUDED_PROPS = [
|
||||
...STANDARD_EXCLUDED_PROPS,
|
||||
'inputClass',
|
||||
'inputStyle'
|
||||
] as const
|
||||
|
||||
export const PANEL_EXCLUDED_PROPS = [
|
||||
...STANDARD_EXCLUDED_PROPS,
|
||||
'panelClass',
|
||||
'panelStyle',
|
||||
'overlayClass'
|
||||
] as const
|
||||
|
||||
export const IMAGE_EXCLUDED_PROPS = [
|
||||
...STANDARD_EXCLUDED_PROPS,
|
||||
'imageClass',
|
||||
'imageStyle'
|
||||
] as const
|
||||
|
||||
export const GALLERIA_EXCLUDED_PROPS = [
|
||||
...STANDARD_EXCLUDED_PROPS,
|
||||
'thumbnailsPosition',
|
||||
'verticalThumbnailViewPortHeight',
|
||||
'indicatorsPosition',
|
||||
'maskClass',
|
||||
'containerStyle',
|
||||
'containerClass',
|
||||
'galleriaClass'
|
||||
] as const
|
||||
|
||||
export const BADGE_EXCLUDED_PROPS = [
|
||||
...STANDARD_EXCLUDED_PROPS,
|
||||
'badgeClass'
|
||||
] as const
|
||||
|
||||
export const LABEL_EXCLUDED_PROPS = [
|
||||
...PANEL_EXCLUDED_PROPS,
|
||||
'labelStyle'
|
||||
] as const
|
||||
|
||||
/**
|
||||
* Filters widget props by excluding specified properties
|
||||
* @param props - The props object to filter
|
||||
* @param excludeList - List of property names to exclude
|
||||
* @returns Filtered props object
|
||||
*/
|
||||
export function filterWidgetProps<T extends Record<string, any>>(
|
||||
props: T | undefined,
|
||||
excludeList: readonly string[]
|
||||
): Partial<T> {
|
||||
if (!props) return {}
|
||||
|
||||
const filtered: Record<string, any> = {}
|
||||
for (const [key, value] of Object.entries(props)) {
|
||||
if (!excludeList.includes(key)) {
|
||||
filtered[key] = value
|
||||
}
|
||||
}
|
||||
return filtered as Partial<T>
|
||||
}
|
||||
Reference in New Issue
Block a user