diff --git a/browser_tests/fixtures/utils/vueNodeFixtures.ts b/browser_tests/fixtures/utils/vueNodeFixtures.ts new file mode 100644 index 0000000000..5c4541b926 --- /dev/null +++ b/browser_tests/fixtures/utils/vueNodeFixtures.ts @@ -0,0 +1,131 @@ +import type { Locator, Page } from '@playwright/test' + +import type { NodeReference } from './litegraphUtils' + +/** + * VueNodeFixture provides Vue-specific testing utilities for interacting with + * Vue node components. It bridges the gap between litegraph node references + * and Vue UI components. + */ +export class VueNodeFixture { + constructor( + private readonly nodeRef: NodeReference, + private readonly page: Page + ) {} + + /** + * Get the node's header element using data-testid + */ + async getHeader(): Promise { + const nodeId = this.nodeRef.id + return this.page.locator(`[data-testid="node-header-${nodeId}"]`) + } + + /** + * Get the node's title element + */ + async getTitleElement(): Promise { + const header = await this.getHeader() + return header.locator('[data-testid="node-title"]') + } + + /** + * Get the current title text + */ + async getTitle(): Promise { + const titleElement = await this.getTitleElement() + return (await titleElement.textContent()) || '' + } + + /** + * Set a new title by double-clicking and entering text + */ + async setTitle(newTitle: string): Promise { + const titleElement = await this.getTitleElement() + await titleElement.dblclick() + + const input = (await this.getHeader()).locator( + '[data-testid="node-title-input"]' + ) + await input.fill(newTitle) + await input.press('Enter') + } + + /** + * Cancel title editing + */ + async cancelTitleEdit(): Promise { + const titleElement = await this.getTitleElement() + await titleElement.dblclick() + + const input = (await this.getHeader()).locator( + '[data-testid="node-title-input"]' + ) + await input.press('Escape') + } + + /** + * Check if the title is currently being edited + */ + async isEditingTitle(): Promise { + const header = await this.getHeader() + const input = header.locator('[data-testid="node-title-input"]') + return await input.isVisible() + } + + /** + * Get the collapse/expand button + */ + async getCollapseButton(): Promise { + const header = await this.getHeader() + return header.locator('[data-testid="node-collapse-button"]') + } + + /** + * Toggle the node's collapsed state + */ + async toggleCollapse(): Promise { + const button = await this.getCollapseButton() + await button.click() + } + + /** + * Get the collapse icon element + */ + async getCollapseIcon(): Promise { + const button = await this.getCollapseButton() + return button.locator('i') + } + + /** + * Get the collapse icon's CSS classes + */ + async getCollapseIconClass(): Promise { + const icon = await this.getCollapseIcon() + return (await icon.getAttribute('class')) || '' + } + + /** + * Check if the collapse button is visible + */ + async isCollapseButtonVisible(): Promise { + const button = await this.getCollapseButton() + return await button.isVisible() + } + + /** + * Get the node's body/content element + */ + async getBody(): Promise { + const nodeId = this.nodeRef.id + return this.page.locator(`[data-testid="node-body-${nodeId}"]`) + } + + /** + * Check if the node body is visible (not collapsed) + */ + async isBodyVisible(): Promise { + const body = await this.getBody() + return await body.isVisible() + } +} diff --git a/browser_tests/tests/vueNodes/NodeHeader.spec.ts b/browser_tests/tests/vueNodes/NodeHeader.spec.ts new file mode 100644 index 0000000000..fd98c04342 --- /dev/null +++ b/browser_tests/tests/vueNodes/NodeHeader.spec.ts @@ -0,0 +1,138 @@ +import { + comfyExpect as expect, + comfyPageFixture as test +} from '../../fixtures/ComfyPage' +import { VueNodeFixture } from '../../fixtures/utils/vueNodeFixtures' + +test.describe('NodeHeader', () => { + test.beforeEach(async ({ comfyPage }) => { + await comfyPage.setSetting('Comfy.UseNewMenu', 'Enabled') + await comfyPage.setSetting('Comfy.Graph.CanvasMenu', false) + await comfyPage.setSetting('Comfy.EnableTooltips', true) + await comfyPage.setup() + // Load single SaveImage node workflow (positioned below menu bar) + await comfyPage.loadWorkflow('single_save_image_node') + }) + + test('displays node title', async ({ comfyPage }) => { + // Get the single SaveImage node from the workflow + const nodes = await comfyPage.getNodeRefsByType('SaveImage') + expect(nodes.length).toBeGreaterThanOrEqual(1) + + const node = nodes[0] + const vueNode = new VueNodeFixture(node, comfyPage.page) + + const title = await vueNode.getTitle() + expect(title).toBe('Save Image') + + // Verify title is visible in the header + const header = await vueNode.getHeader() + await expect(header).toContainText('Save Image') + }) + + test('allows title renaming', async ({ comfyPage }) => { + // Get the single SaveImage node from the workflow + const nodes = await comfyPage.getNodeRefsByType('SaveImage') + const node = nodes[0] + const vueNode = new VueNodeFixture(node, comfyPage.page) + + // Test renaming with Enter + await vueNode.setTitle('My Custom Sampler') + const newTitle = await vueNode.getTitle() + expect(newTitle).toBe('My Custom Sampler') + + // Verify the title is displayed + const header = await vueNode.getHeader() + await expect(header).toContainText('My Custom Sampler') + + // Test cancel with Escape + const titleElement = await vueNode.getTitleElement() + await titleElement.dblclick() + await comfyPage.nextFrame() + + // Type a different value but cancel + const input = (await vueNode.getHeader()).locator( + '[data-testid="node-title-input"]' + ) + await input.fill('This Should Be Cancelled') + await input.press('Escape') + await comfyPage.nextFrame() + + // Title should remain as the previously saved value + const titleAfterCancel = await vueNode.getTitle() + expect(titleAfterCancel).toBe('My Custom Sampler') + }) + + test('handles node collapsing', async ({ comfyPage }) => { + // Get the single SaveImage node from the workflow + const nodes = await comfyPage.getNodeRefsByType('SaveImage') + const node = nodes[0] + const vueNode = new VueNodeFixture(node, comfyPage.page) + + // Initially should not be collapsed + expect(await node.isCollapsed()).toBe(false) + const body = await vueNode.getBody() + await expect(body).toBeVisible() + + // Collapse the node + await vueNode.toggleCollapse() + expect(await node.isCollapsed()).toBe(true) + + // Verify node content is hidden + const collapsedSize = await node.getSize() + await expect(body).not.toBeVisible() + + // Expand again + await vueNode.toggleCollapse() + expect(await node.isCollapsed()).toBe(false) + await expect(body).toBeVisible() + + // Size should be restored + const expandedSize = await node.getSize() + expect(expandedSize.height).toBeGreaterThanOrEqual(collapsedSize.height) + }) + + test('shows collapse/expand icon state', async ({ comfyPage }) => { + // Get the single SaveImage node from the workflow + const nodes = await comfyPage.getNodeRefsByType('SaveImage') + const node = nodes[0] + const vueNode = new VueNodeFixture(node, comfyPage.page) + + // Check initial expanded state icon + let iconClass = await vueNode.getCollapseIconClass() + expect(iconClass).toContain('pi-chevron-down') + + // Collapse and check icon + await vueNode.toggleCollapse() + iconClass = await vueNode.getCollapseIconClass() + expect(iconClass).toContain('pi-chevron-right') + + // Expand and check icon + await vueNode.toggleCollapse() + iconClass = await vueNode.getCollapseIconClass() + expect(iconClass).toContain('pi-chevron-down') + }) + + test('preserves title when collapsing/expanding', async ({ comfyPage }) => { + // Get the single SaveImage node from the workflow + const nodes = await comfyPage.getNodeRefsByType('SaveImage') + const node = nodes[0] + const vueNode = new VueNodeFixture(node, comfyPage.page) + + // Set custom title + await vueNode.setTitle('Test Sampler') + expect(await vueNode.getTitle()).toBe('Test Sampler') + + // Collapse + await vueNode.toggleCollapse() + expect(await vueNode.getTitle()).toBe('Test Sampler') + + // Expand + await vueNode.toggleCollapse() + expect(await vueNode.getTitle()).toBe('Test Sampler') + + // Verify title is still displayed + const header = await vueNode.getHeader() + await expect(header).toContainText('Test Sampler') + }) +}) diff --git a/eslint.config.js b/eslint.config.js index 9f212cfc1e..0a13203bcb 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -13,7 +13,8 @@ export default [ ignores: [ 'src/scripts/*', 'src/extensions/core/*', - 'src/types/vue-shim.d.ts' + 'src/types/vue-shim.d.ts', + 'src/lib/litegraph/**/*' ] }, { diff --git a/lint-staged.config.js b/lint-staged.config.js index 2d1a6f0511..699f13b274 100644 --- a/lint-staged.config.js +++ b/lint-staged.config.js @@ -3,7 +3,7 @@ export default { './**/*.{ts,tsx,vue,mts}': (stagedFiles) => [ ...formatAndEslint(stagedFiles), - 'vue-tsc --noEmit' + 'npm run typecheck' ] } diff --git a/package-lock.json b/package-lock.json index eee43ab87f..15f6235001 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,6 @@ "@alloc/quick-lru": "^5.2.0", "@atlaskit/pragmatic-drag-and-drop": "^1.3.1", "@comfyorg/comfyui-electron-types": "^0.4.43", - "@comfyorg/litegraph": "^0.16.3", "@primevue/forms": "^4.2.5", "@primevue/themes": "^4.2.5", "@sentry/vue": "^8.48.0", @@ -29,6 +28,7 @@ "@xterm/xterm": "^5.5.0", "algoliasearch": "^5.21.0", "axios": "^1.8.2", + "chart.js": "^4.5.0", "dompurify": "^3.2.5", "dotenv": "^16.4.5", "firebase": "^11.6.0", @@ -947,12 +947,6 @@ "integrity": "sha512-o6WFbYn9yAkGbkOwvhPF7pbKDvN0occZ21Tfyhya8CIsIqKpTHLft0aOqo4yhSh+kTxN16FYjsfrTH5Olk4WuA==", "license": "GPL-3.0-only" }, - "node_modules/@comfyorg/litegraph": { - "version": "0.16.3", - "resolved": "https://registry.npmjs.org/@comfyorg/litegraph/-/litegraph-0.16.3.tgz", - "integrity": "sha512-dst29g8+aZW8sWTYxj3LK1W4lX07elBPWFB1L4HLTkYgkzQoyBkHR1O2lSvAn+7bKagi0Q5PjIcZnWG+JAi0lg==", - "license": "MIT" - }, "node_modules/@cspotcode/source-map-support": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", @@ -2543,6 +2537,12 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@kurkle/color": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz", + "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==", + "license": "MIT" + }, "node_modules/@langchain/core": { "version": "0.2.36", "resolved": "https://registry.npmjs.org/@langchain/core/-/core-0.2.36.tgz", @@ -6148,6 +6148,18 @@ "node": "*" } }, + "node_modules/chart.js": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.0.tgz", + "integrity": "sha512-aYeC/jDgSEx8SHWZvANYMioYMZ2KX02W6f6uVfyteuCGcadDLcYVHdfdygsTQkQ4TKn5lghoojAsPj5pu0SnvQ==", + "license": "MIT", + "dependencies": { + "@kurkle/color": "^0.3.0" + }, + "engines": { + "pnpm": ">=8" + } + }, "node_modules/check-error": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", diff --git a/package.json b/package.json index 74773da4c7..ab8ba2a7c4 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "build": "npm run typecheck && vite build", "build:types": "vite build --config vite.types.config.mts && node scripts/prepare-types.js", "zipdist": "node scripts/zipdist.js", - "typecheck": "vue-tsc --noEmit", + "typecheck": "vue-tsc --build", "format": "prettier --write './**/*.{js,ts,tsx,vue,mts}'", "format:check": "prettier --check './**/*.{js,ts,tsx,vue,mts}'", "test:browser": "npx playwright test", @@ -76,7 +76,6 @@ "@alloc/quick-lru": "^5.2.0", "@atlaskit/pragmatic-drag-and-drop": "^1.3.1", "@comfyorg/comfyui-electron-types": "^0.4.43", - "@comfyorg/litegraph": "^0.16.3", "@primevue/forms": "^4.2.5", "@primevue/themes": "^4.2.5", "@sentry/vue": "^8.48.0", @@ -93,6 +92,7 @@ "@xterm/xterm": "^5.5.0", "algoliasearch": "^5.21.0", "axios": "^1.8.2", + "chart.js": "^4.5.0", "dompurify": "^3.2.5", "dotenv": "^16.4.5", "firebase": "^11.6.0", diff --git a/src/assets/css/style.css b/src/assets/css/style.css index 9adab19c7c..9fe38ec955 100644 --- a/src/assets/css/style.css +++ b/src/assets/css/style.css @@ -135,6 +135,188 @@ body { border: thin solid; } +/* Shared markdown content styling for consistent rendering across components */ +.comfy-markdown-content { + /* Typography */ + font-size: 0.875rem; /* text-sm */ + line-height: 1.6; + word-wrap: break-word; +} + +/* Headings */ +.comfy-markdown-content h1 { + font-size: 22px; /* text-[22px] */ + font-weight: 700; /* font-bold */ + margin-top: 2rem; /* mt-8 */ + margin-bottom: 1rem; /* mb-4 */ +} + +.comfy-markdown-content h1:first-child { + margin-top: 0; /* first:mt-0 */ +} + +.comfy-markdown-content h2 { + font-size: 18px; /* text-[18px] */ + font-weight: 700; /* font-bold */ + margin-top: 2rem; /* mt-8 */ + margin-bottom: 1rem; /* mb-4 */ +} + +.comfy-markdown-content h2:first-child { + margin-top: 0; /* first:mt-0 */ +} + +.comfy-markdown-content h3 { + font-size: 16px; /* text-[16px] */ + font-weight: 700; /* font-bold */ + margin-top: 2rem; /* mt-8 */ + margin-bottom: 1rem; /* mb-4 */ +} + +.comfy-markdown-content h3:first-child { + margin-top: 0; /* first:mt-0 */ +} + +.comfy-markdown-content h4, +.comfy-markdown-content h5, +.comfy-markdown-content h6 { + margin-top: 2rem; /* mt-8 */ + margin-bottom: 1rem; /* mb-4 */ +} + +.comfy-markdown-content h4:first-child, +.comfy-markdown-content h5:first-child, +.comfy-markdown-content h6:first-child { + margin-top: 0; /* first:mt-0 */ +} + +/* Paragraphs */ +.comfy-markdown-content p { + margin: 0 0 0.5em; +} + +.comfy-markdown-content p:last-child { + margin-bottom: 0; +} + +/* First child reset */ +.comfy-markdown-content *:first-child { + margin-top: 0; /* mt-0 */ +} + +/* Lists */ +.comfy-markdown-content ul, +.comfy-markdown-content ol { + padding-left: 2rem; /* pl-8 */ + margin: 0.5rem 0; /* my-2 */ +} + +/* Nested lists */ +.comfy-markdown-content ul ul, +.comfy-markdown-content ol ol, +.comfy-markdown-content ul ol, +.comfy-markdown-content ol ul { + padding-left: 1.5rem; /* pl-6 */ + margin: 0.5rem 0; /* my-2 */ +} + +.comfy-markdown-content li { + margin: 0.5rem 0; /* my-2 */ +} + +/* Code */ +.comfy-markdown-content code { + color: var(--code-text-color); + background-color: var(--code-bg-color); + border-radius: 0.25rem; /* rounded */ + padding: 0.125rem 0.375rem; /* px-1.5 py-0.5 */ + font-family: monospace; +} + +.comfy-markdown-content pre { + background-color: var(--code-block-bg-color); + border-radius: 0.25rem; /* rounded */ + padding: 1rem; /* p-4 */ + margin: 1rem 0; /* my-4 */ + overflow-x: auto; /* overflow-x-auto */ +} + +.comfy-markdown-content pre code { + background-color: transparent; /* bg-transparent */ + padding: 0; /* p-0 */ + color: var(--p-text-color); +} + +/* Tables */ +.comfy-markdown-content table { + width: 100%; /* w-full */ + border-collapse: collapse; /* border-collapse */ +} + +.comfy-markdown-content th, +.comfy-markdown-content td { + padding: 0.5rem; /* px-2 py-2 */ +} + +.comfy-markdown-content th { + color: var(--fg-color); +} + +.comfy-markdown-content td { + color: var(--drag-text); +} + +.comfy-markdown-content tr { + border-bottom: 1px solid var(--content-bg); +} + +.comfy-markdown-content tr:last-child { + border-bottom: none; +} + +.comfy-markdown-content thead { + border-bottom: 1px solid var(--p-text-color); +} + +/* Links */ +.comfy-markdown-content a { + color: var(--drag-text); + text-decoration: underline; +} + +/* Media */ +.comfy-markdown-content img, +.comfy-markdown-content video { + max-width: 100%; /* max-w-full */ + height: auto; /* h-auto */ + display: block; /* block */ + margin-bottom: 1rem; /* mb-4 */ +} + +/* Blockquotes */ +.comfy-markdown-content blockquote { + border-left: 3px solid var(--p-primary-color, var(--primary-bg)); + padding-left: 0.75em; + margin: 0.5em 0; + opacity: 0.8; +} + +/* Horizontal rule */ +.comfy-markdown-content hr { + border: none; + border-top: 1px solid var(--p-border-color, var(--border-color)); + margin: 1em 0; +} + +/* Strong and emphasis */ +.comfy-markdown-content strong { + font-weight: bold; +} + +.comfy-markdown-content em { + font-style: italic; +} + .comfy-modal { display: none; /* Hidden by default */ position: fixed; /* Stay in place */ diff --git a/src/components/common/EditableText.spec.ts b/src/components/common/EditableText.spec.ts index 2e7b036b55..2d31123b98 100644 --- a/src/components/common/EditableText.spec.ts +++ b/src/components/common/EditableText.spec.ts @@ -68,4 +68,73 @@ describe('EditableText', () => { // @ts-expect-error fixme ts strict error expect(wrapper.emitted('edit')[0]).toEqual(['Test Text']) }) + + it('cancels editing on escape key', async () => { + const wrapper = mountComponent({ + modelValue: 'Original Text', + isEditing: true + }) + + // Change the input value + await wrapper.findComponent(InputText).setValue('Modified Text') + + // Press escape + await wrapper.findComponent(InputText).trigger('keyup.escape') + + // Should emit cancel event + expect(wrapper.emitted('cancel')).toBeTruthy() + + // Should NOT emit edit event + expect(wrapper.emitted('edit')).toBeFalsy() + + // Input value should be reset to original + expect(wrapper.findComponent(InputText).props()['modelValue']).toBe( + 'Original Text' + ) + }) + + it('does not save changes when escape is pressed and blur occurs', async () => { + const wrapper = mountComponent({ + modelValue: 'Original Text', + isEditing: true + }) + + // Change the input value + await wrapper.findComponent(InputText).setValue('Modified Text') + + // Press escape (which triggers blur internally) + await wrapper.findComponent(InputText).trigger('keyup.escape') + + // Manually trigger blur to simulate the blur that happens after escape + await wrapper.findComponent(InputText).trigger('blur') + + // Should emit cancel but not edit + expect(wrapper.emitted('cancel')).toBeTruthy() + expect(wrapper.emitted('edit')).toBeFalsy() + }) + + it('saves changes on enter but not on escape', async () => { + // Test Enter key saves changes + const enterWrapper = mountComponent({ + modelValue: 'Original Text', + isEditing: true + }) + await enterWrapper.findComponent(InputText).setValue('Saved Text') + await enterWrapper.findComponent(InputText).trigger('keyup.enter') + // Trigger blur that happens after enter + await enterWrapper.findComponent(InputText).trigger('blur') + expect(enterWrapper.emitted('edit')).toBeTruthy() + // @ts-expect-error fixme ts strict error + expect(enterWrapper.emitted('edit')[0]).toEqual(['Saved Text']) + + // Test Escape key cancels changes with a fresh wrapper + const escapeWrapper = mountComponent({ + modelValue: 'Original Text', + isEditing: true + }) + await escapeWrapper.findComponent(InputText).setValue('Cancelled Text') + await escapeWrapper.findComponent(InputText).trigger('keyup.escape') + expect(escapeWrapper.emitted('cancel')).toBeTruthy() + expect(escapeWrapper.emitted('edit')).toBeFalsy() + }) }) diff --git a/src/components/common/EditableText.vue b/src/components/common/EditableText.vue index 16510d3fd4..c6fa18a8d8 100644 --- a/src/components/common/EditableText.vue +++ b/src/components/common/EditableText.vue @@ -14,10 +14,12 @@ fluid :pt="{ root: { - onBlur: finishEditing + onBlur: finishEditing, + ...inputAttrs } }" @keyup.enter="blurInputElement" + @keyup.escape="cancelEditing" @click.stop /> @@ -27,21 +29,41 @@ import InputText from 'primevue/inputtext' import { nextTick, ref, watch } from 'vue' -const { modelValue, isEditing = false } = defineProps<{ +const { + modelValue, + isEditing = false, + inputAttrs = {} +} = defineProps<{ modelValue: string isEditing?: boolean + inputAttrs?: Record }>() -const emit = defineEmits(['update:modelValue', 'edit']) +const emit = defineEmits(['update:modelValue', 'edit', 'cancel']) const inputValue = ref(modelValue) const inputRef = ref | undefined>() +const isCanceling = ref(false) const blurInputElement = () => { // @ts-expect-error - $el is an internal property of the InputText component inputRef.value?.$el.blur() } const finishEditing = () => { - emit('edit', inputValue.value) + // Don't save if we're canceling + if (!isCanceling.value) { + emit('edit', inputValue.value) + } + isCanceling.value = false +} +const cancelEditing = () => { + // Set canceling flag to prevent blur from saving + isCanceling.value = true + // Reset to original value + inputValue.value = modelValue + // Emit cancel event + emit('cancel') + // Blur the input to exit edit mode + blurInputElement() } watch( () => isEditing, diff --git a/src/components/dialog/content/MissingCoreNodesMessage.vue b/src/components/dialog/content/MissingCoreNodesMessage.vue index 40347061e4..3c407b666e 100644 --- a/src/components/dialog/content/MissingCoreNodesMessage.vue +++ b/src/components/dialog/content/MissingCoreNodesMessage.vue @@ -42,7 +42,6 @@ diff --git a/src/components/graph/vueNodes/NodeSlots.vue b/src/components/graph/vueNodes/NodeSlots.vue index 76ec3e9a6b..6d1d6ec885 100644 --- a/src/components/graph/vueNodes/NodeSlots.vue +++ b/src/components/graph/vueNodes/NodeSlots.vue @@ -2,44 +2,49 @@
⚠️ Node Slots Error
-
- -
-
Inputs:
-
+
+ - {{ getInputName(input, index) }} ({{ getInputType(input) }}) -
+ :slot-data="input" + :index="getActualInputIndex(input, index)" + :readonly="readonly" + @slot-click=" + handleInputSlotClick(getActualInputIndex(input, index), $event) + " + />
-
-
Outputs:
-
+ - {{ getOutputName(output, index) }} ({{ getOutputType(output) }}) -
+ :slot-data="output" + :index="index" + :readonly="readonly" + @slot-click="handleOutputSlotClick(index, $event)" + />
diff --git a/src/components/graph/vueNodes/OutputSlot.vue b/src/components/graph/vueNodes/OutputSlot.vue index 16be94ebe0..2edd3f096f 100644 --- a/src/components/graph/vueNodes/OutputSlot.vue +++ b/src/components/graph/vueNodes/OutputSlot.vue @@ -2,44 +2,57 @@
⚠️
- + {{ slotData.name || `Output ${index}` }} -
+
+
+
diff --git a/src/components/graph/vueWidgets/WidgetColorPicker.vue b/src/components/graph/vueWidgets/WidgetColorPicker.vue index 67d08f0d16..16a3dd3746 100644 --- a/src/components/graph/vueWidgets/WidgetColorPicker.vue +++ b/src/components/graph/vueWidgets/WidgetColorPicker.vue @@ -1,9 +1,16 @@ + @@ -11,19 +18,31 @@ import ColorPicker from 'primevue/colorpicker' import { computed } from 'vue' +import { useWidgetValue } from '@/composables/graph/useWidgetValue' import type { SimplifiedWidget } from '@/types/simplifiedWidget' import { PANEL_EXCLUDED_PROPS, filterWidgetProps } from '@/utils/widgetPropFilter' -const value = defineModel({ required: true }) - const props = defineProps<{ widget: SimplifiedWidget + modelValue: string readonly?: boolean }>() +const emit = defineEmits<{ + 'update:modelValue': [value: string] +}>() + +// Use the composable for consistent widget value handling +const { localValue, onChange } = useWidgetValue({ + widget: props.widget, + modelValue: props.modelValue, + defaultValue: '#000000', + emit +}) + // ColorPicker specific excluded props include panel/overlay classes const COLOR_PICKER_EXCLUDED_PROPS = [...PANEL_EXCLUDED_PROPS] as const diff --git a/src/components/graph/vueWidgets/WidgetFileUpload.vue b/src/components/graph/vueWidgets/WidgetFileUpload.vue index b8908c734f..29cff10c07 100644 --- a/src/components/graph/vueWidgets/WidgetFileUpload.vue +++ b/src/components/graph/vueWidgets/WidgetFileUpload.vue @@ -1,66 +1,323 @@ diff --git a/src/components/graph/vueWidgets/WidgetGalleria.vue b/src/components/graph/vueWidgets/WidgetGalleria.vue index faa31ce913..3603b7ab67 100644 --- a/src/components/graph/vueWidgets/WidgetGalleria.vue +++ b/src/components/graph/vueWidgets/WidgetGalleria.vue @@ -1,17 +1,27 @@