From c145fd9df122aa9a76059d09d312fa1a1f8b1e81 Mon Sep 17 00:00:00 2001 From: Christian Byrne Date: Sat, 13 Sep 2025 23:34:39 -0700 Subject: [PATCH] Show node output images on Vue Nodes (#5456) * add image outputs on Vue nodes * add unit tests and update cursor pointer * use testing pinia * properly mock i18n in component test * get node via current graph * use subgraph ID from node creation * add better error handling for downloadFile util * refactor: simplify image preview component architecture - Replace awkward composable pattern with standard Vue component state - Fix reactivity issues where images didn't update on new outputs - Add proper subgraph-aware node resolution using NodeLocatorId - Enhance accessibility with keyboard navigation and ARIA labels - Add comprehensive error handling and loading states - Include PrimeVue Skeleton for better loading UX - Remove unused composable and test files The image preview now properly updates when new outputs are generated and follows standard Vue reactivity patterns. * resolve merge conflict with main - Keep both subgraphId field and hasErrors field from main - No conflicts in other files (LGraphNode.vue and main.json merged cleanly) * Fix LGraphNode test by adding proper Pinia testing setup Added createTestingPinia and i18n configuration following the pattern from working ImagePreview tests. Resolves test failures due to missing Pinia store dependencies. All 6 tests now pass successfully. --- src/base/common/downloadUtil.ts | 41 +++ src/composables/graph/useGraphNodeManager.ts | 8 + src/locales/en/main.json | 8 + .../vueNodes/components/ImagePreview.vue | 258 ++++++++++++++++ .../vueNodes/components/LGraphNode.vue | 68 ++++- .../vueNodes/components/NodeContent.vue | 22 +- src/stores/imagePreviewStore.ts | 38 ++- .../tests/base/common/downloadUtil.test.ts | 116 ++++++++ .../vueNodes/components/ImagePreview.spec.ts | 277 ++++++++++++++++++ .../vueNodes/components/LGraphNode.spec.ts | 47 ++- 10 files changed, 872 insertions(+), 11 deletions(-) create mode 100644 src/base/common/downloadUtil.ts create mode 100644 src/renderer/extensions/vueNodes/components/ImagePreview.vue create mode 100644 tests-ui/tests/base/common/downloadUtil.test.ts create mode 100644 tests-ui/tests/renderer/extensions/vueNodes/components/ImagePreview.spec.ts diff --git a/src/base/common/downloadUtil.ts b/src/base/common/downloadUtil.ts new file mode 100644 index 000000000..307a3e35b --- /dev/null +++ b/src/base/common/downloadUtil.ts @@ -0,0 +1,41 @@ +/** + * Utility functions for downloading files + */ + +// Constants +const DEFAULT_DOWNLOAD_FILENAME = 'download.png' + +/** + * Download a file from a URL by creating a temporary anchor element + * @param url - The URL of the file to download (must be a valid URL string) + * @param filename - Optional filename override (will use URL filename or default if not provided) + * @throws {Error} If the URL is invalid or empty + */ +export const downloadFile = (url: string, filename?: string): void => { + if (!url || typeof url !== 'string' || url.trim().length === 0) { + throw new Error('Invalid URL provided for download') + } + const link = document.createElement('a') + link.href = url + link.download = + filename || extractFilenameFromUrl(url) || DEFAULT_DOWNLOAD_FILENAME + + // Trigger download + document.body.appendChild(link) + link.click() + document.body.removeChild(link) +} + +/** + * Extract filename from a URL's query parameters + * @param url - The URL to extract filename from + * @returns The extracted filename or null if not found + */ +const extractFilenameFromUrl = (url: string): string | null => { + try { + const urlObj = new URL(url, window.location.origin) + return urlObj.searchParams.get('filename') + } catch { + return null + } +} diff --git a/src/composables/graph/useGraphNodeManager.ts b/src/composables/graph/useGraphNodeManager.ts index e91d1f332..e4031fd51 100644 --- a/src/composables/graph/useGraphNodeManager.ts +++ b/src/composables/graph/useGraphNodeManager.ts @@ -53,6 +53,7 @@ export interface VueNodeData { mode: number selected: boolean executing: boolean + subgraphId?: string | null widgets?: SafeWidgetData[] inputs?: unknown[] outputs?: unknown[] @@ -167,6 +168,11 @@ export const useGraphNodeManager = (graph: LGraph): GraphNodeManager => { // Extract safe data from LiteGraph node for Vue consumption const extractVueNodeData = (node: LGraphNode): VueNodeData => { + // Determine subgraph ID - null for root graph, string for subgraphs + const subgraphId = + node.graph && 'id' in node.graph && node.graph !== node.graph.rootGraph + ? String(node.graph.id) + : null // Extract safe widget data const safeWidgets = node.widgets?.map((widget) => { try { @@ -216,6 +222,8 @@ export const useGraphNodeManager = (graph: LGraph): GraphNodeManager => { mode: node.mode || 0, selected: node.selected || false, executing: false, // Will be updated separately based on execution state + subgraphId, + hasErrors: !!node.has_errors, widgets: safeWidgets, inputs: node.inputs ? [...node.inputs] : undefined, diff --git a/src/locales/en/main.json b/src/locales/en/main.json index 0f98ee103..f736ca149 100644 --- a/src/locales/en/main.json +++ b/src/locales/en/main.json @@ -6,6 +6,14 @@ "noWorkflowsFound": "No workflows found.", "comingSoon": "Coming Soon", "download": "Download", + "downloadImage": "Download image", + "editOrMaskImage": "Edit or mask image", + "removeImage": "Remove image", + "viewImageOfTotal": "View image {index} of {total}", + "imagePreview": "Image preview - Use arrow keys to navigate between images", + "errorLoadingImage": "Error loading image", + "failedToDownloadImage": "Failed to download image", + "calculatingDimensions": "Calculating dimensions", "import": "Import", "loadAllFolders": "Load All Folders", "refresh": "Refresh", diff --git a/src/renderer/extensions/vueNodes/components/ImagePreview.vue b/src/renderer/extensions/vueNodes/components/ImagePreview.vue new file mode 100644 index 000000000..120b24657 --- /dev/null +++ b/src/renderer/extensions/vueNodes/components/ImagePreview.vue @@ -0,0 +1,258 @@ + + + diff --git a/src/renderer/extensions/vueNodes/components/LGraphNode.vue b/src/renderer/extensions/vueNodes/components/LGraphNode.vue index aeb0e9a23..2fa74d573 100644 --- a/src/renderer/extensions/vueNodes/components/LGraphNode.vue +++ b/src/renderer/extensions/vueNodes/components/LGraphNode.vue @@ -119,6 +119,7 @@ :node-data="nodeData" :readonly="readonly" :lod-level="lodLevel" + :image-urls="nodeImageUrls" /> @@ -126,7 +127,15 @@ diff --git a/src/renderer/extensions/vueNodes/components/NodeContent.vue b/src/renderer/extensions/vueNodes/components/NodeContent.vue index f99e10917..3df38bdf6 100644 --- a/src/renderer/extensions/vueNodes/components/NodeContent.vue +++ b/src/renderer/extensions/vueNodes/components/NodeContent.vue @@ -5,28 +5,42 @@
- - +