+
-
+
+
= {
+ title: 'Platform/Assets/AssetsListItem',
+ component: AssetsListItem,
+ parameters: {
+ layout: 'centered'
+ },
+ decorators: [
+ () => ({
+ template: '
'
+ })
+ ]
+}
+
+export default meta
+type Story = StoryObj
+type AssetsListItemProps = InstanceType['$props']
+
+function renderActiveJob(args: AssetsListItemProps) {
+ return {
+ components: { Button, AssetsListItem },
+ setup() {
+ return { args }
+ },
+ template: `
+
+
+
+ Total:
+ 30%
+
+
+
+
+ CLIP Text Encode:
+ 70%
+
+
+
+
+
+
+ `
+ }
+}
+
+function renderGeneratedAsset(args: AssetsListItemProps) {
+ return {
+ components: { AssetsListItem },
+ setup() {
+ return { args }
+ },
+ template: `
+
+
+
+ 1m 56s
+ 512x512
+
+
+
+ `
+ }
+}
+
+export const ActiveJob: Story = {
+ args: {
+ previewUrl: '/assets/images/comfy-logo-single.svg',
+ previewAlt: 'Job preview',
+ progressTotalPercent: 30,
+ progressCurrentPercent: 70
+ },
+ render: renderActiveJob
+}
+
+export const FailedJob: Story = {
+ args: {
+ iconName: 'icon-[lucide--circle-alert]',
+ iconClass: 'text-destructive-background',
+ iconWrapperClass: 'bg-modal-card-placeholder-background',
+ primaryText: 'Failed',
+ secondaryText: '8:59:30pm'
+ }
+}
+
+export const GeneratedAsset: Story = {
+ args: {
+ previewUrl: '/assets/images/comfy-logo-single.svg',
+ previewAlt: 'image03.png',
+ primaryText: 'image03.png'
+ },
+ render: renderGeneratedAsset
+}
diff --git a/src/platform/assets/components/AssetsListItem.vue b/src/platform/assets/components/AssetsListItem.vue
new file mode 100644
index 000000000..3f04d372f
--- /dev/null
+++ b/src/platform/assets/components/AssetsListItem.vue
@@ -0,0 +1,116 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ primaryText }}
+
+
+ {{ secondaryText }}
+
+
+
+
+
+
+
+
+
+
diff --git a/src/platform/assets/utils/mediaIconUtil.ts b/src/platform/assets/utils/mediaIconUtil.ts
new file mode 100644
index 000000000..d698fa610
--- /dev/null
+++ b/src/platform/assets/utils/mediaIconUtil.ts
@@ -0,0 +1,14 @@
+import type { MediaKind } from '@/platform/assets/schemas/mediaAssetSchema'
+
+export function iconForMediaType(mediaType: MediaKind): string {
+ switch (mediaType) {
+ case 'video':
+ return 'icon-[lucide--video]'
+ case 'audio':
+ return 'icon-[lucide--music]'
+ case '3D':
+ return 'icon-[lucide--box]'
+ default:
+ return 'icon-[lucide--image]'
+ }
+}
From 7bc633406546a64bd3b9e559d1d27b95fb6ba378 Mon Sep 17 00:00:00 2001
From: brucew4yn3rp <135722417+brucew4yn3rp@users.noreply.github.com>
Date: Sat, 10 Jan 2026 15:45:08 -0500
Subject: [PATCH 03/63] Added MaskEditor Rotate and Mirror Functions (#7841)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
# Canvas Rotation and Mirroring
## Overview
Adds rotation (90° left/right) and mirroring (horizontal/vertical)
capabilities to the mask editor canvas. All three layers (image, mask,
RGB) transform together. Redo and Undo respect transformations as new
states. Keyboard shortcuts also added for all four functions in
Keybinding settings.
Additionally, fixed the issue of ctrl+z and ctrl+y keyboard commands not
restricting to the mask editor canvas while opened.
https://github.com/user-attachments/assets/fb8d5347-b357-4a3a-840a-721cdf8a6125
## What Changed
### New Files
- **`src/composables/maskeditor/useCanvasTransform.ts`**
- Core transformation logic for rotation and mirroring
- GPU texture recreation after transformations
### Modified Files
#### **`src/composables/useCoreCommands.ts`**
- Added check to see if Mask Editor is opened for undo and redo commands
#### **`src/stores/maskEditorStore.ts`**
- Added GPU texture recreation signals
#### **`src/composables/maskeditor/useBrushDrawing.ts`**
- Added watcher for `gpuTexturesNeedRecreation` signal
- Handles GPU texture recreation when canvas dimensions change
- Recreates textures with new dimensions after rotation
- Updates preview canvas and readback buffers accordingly
- Ensures proper ArrayBuffer backing for WebGPU compatibility
#### **`src/components/maskeditor/TopBarHeader.vue`**
- Added 4 new transform buttons with icons:
- Rotate Left (counter-clockwise)
- Rotate Right (clockwise)
- Mirror Horizontal
- Mirror Vertical
- Added visual separators between button groups
#### **`src/extensions/core/maskEditor.ts`**
- Added keyboard shortcut settings for rotate and mirror
#### **Translation Files** (e.g., `src/locales/en.json`)
- Added i18n keys:
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7841-Added-MaskEditor-Rotate-and-Mirror-Functions-2de6d73d365081bc9b84ea4919a3c6a1)
by [Unito](https://www.unito.io)
---------
Co-authored-by: Alexander Brown
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
---
.../maskeditor/MaskEditorContent.vue | 1 +
.../maskeditor/dialog/TopBarHeader.vue | 108 ++-
src/composables/maskeditor/useBrushDrawing.ts | 123 ++++
.../maskeditor/useCanvasHistory.test.ts | 264 +++++--
.../maskeditor/useCanvasHistory.ts | 149 ++--
.../maskeditor/useCanvasTransform.test.ts | 683 ++++++++++++++++++
.../maskeditor/useCanvasTransform.ts | 359 +++++++++
src/composables/useCoreCommands.ts | 19 +-
src/extensions/core/maskeditor.ts | 37 +
src/locales/en/main.json | 4 +
src/stores/maskEditorStore.ts | 29 +
11 files changed, 1648 insertions(+), 128 deletions(-)
create mode 100644 src/composables/maskeditor/useCanvasTransform.test.ts
create mode 100644 src/composables/maskeditor/useCanvasTransform.ts
diff --git a/src/components/maskeditor/MaskEditorContent.vue b/src/components/maskeditor/MaskEditorContent.vue
index a524edfcb..0563dc571 100644
--- a/src/components/maskeditor/MaskEditorContent.vue
+++ b/src/components/maskeditor/MaskEditorContent.vue
@@ -4,6 +4,7 @@
class="maskEditor-dialog-root flex h-full w-full flex-col"
@contextmenu.prevent
@dragstart="handleDragStart"
+ @keydown.stop
>
`
+
+ usePaste()
+
+ const dataTransfer = new DataTransfer()
+ dataTransfer.setData('text/html', html)
+
+ const event = new ClipboardEvent('paste', { clipboardData: dataTransfer })
+ document.dispatchEvent(event)
+
+ await vi.waitFor(() => {
+ expect(mockCanvas._deserializeItems).toHaveBeenCalledWith(
+ data,
+ expect.any(Object)
+ )
+ })
+ })
+})
diff --git a/src/composables/usePaste.ts b/src/composables/usePaste.ts
index 9a40fab9a..6bfefab81 100644
--- a/src/composables/usePaste.ts
+++ b/src/composables/usePaste.ts
@@ -1,7 +1,7 @@
import { useEventListener } from '@vueuse/core'
+import type { LGraphCanvas, LGraphNode } from '@/lib/litegraph/src/litegraph'
import { LiteGraph } from '@/lib/litegraph/src/litegraph'
-import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
import type { ComfyWorkflowJSON } from '@/platform/workflow/validation/schemas/workflowSchema'
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
import { app } from '@/scripts/app'
@@ -26,6 +26,48 @@ function pasteClipboardItems(data: DataTransfer): boolean {
return false
}
+function pasteItemsOnNode(
+ items: DataTransferItemList,
+ node: LGraphNode | null,
+ contentType: string
+): void {
+ if (!node) return
+
+ const filteredItems = Array.from(items).filter((item) =>
+ item.type.startsWith(contentType)
+ )
+
+ const blob = filteredItems[0]?.getAsFile()
+ if (!blob) return
+
+ node.pasteFile?.(blob)
+ node.pasteFiles?.(
+ Array.from(filteredItems)
+ .map((i) => i.getAsFile())
+ .filter((f) => f !== null)
+ )
+}
+
+export function pasteImageNode(
+ canvas: LGraphCanvas,
+ items: DataTransferItemList,
+ imageNode: LGraphNode | null = null
+): void {
+ const { graph, graph_mouse: [posX, posY] } = canvas
+
+ if (!imageNode) {
+ // No image node selected: add a new one
+ const newNode = LiteGraph.createNode('LoadImage')
+ if (newNode) {
+ newNode.pos = [posX, posY]
+ imageNode = graph?.add(newNode) ?? null
+ }
+ graph?.change()
+ }
+
+ pasteItemsOnNode(items, imageNode, 'image')
+}
+
/**
* Adds a handler on paste that extracts and loads images or workflows from pasted JSON data
*/
@@ -33,28 +75,6 @@ export const usePaste = () => {
const workspaceStore = useWorkspaceStore()
const canvasStore = useCanvasStore()
- const pasteItemsOnNode = (
- items: DataTransferItemList,
- node: LGraphNode | null,
- contentType: string
- ) => {
- if (!node) return
-
- const filteredItems = Array.from(items).filter((item) =>
- item.type.startsWith(contentType)
- )
-
- const blob = filteredItems[0]?.getAsFile()
- if (!blob) return
-
- node.pasteFile?.(blob)
- node.pasteFiles?.(
- Array.from(filteredItems)
- .map((i) => i.getAsFile())
- .filter((f) => f !== null)
- )
- }
-
useEventListener(document, 'paste', async (e) => {
if (shouldIgnoreCopyPaste(e.target)) {
// Default system copy
@@ -80,8 +100,10 @@ export const usePaste = () => {
const isVideoNodeSelected = isNodeSelected && isVideoNode(currentNode)
const isAudioNodeSelected = isNodeSelected && isAudioNode(currentNode)
- let imageNode: LGraphNode | null = isImageNodeSelected ? currentNode : null
let audioNode: LGraphNode | null = isAudioNodeSelected ? currentNode : null
+ const imageNode: LGraphNode | null = isImageNodeSelected
+ ? currentNode
+ : null
const videoNode: LGraphNode | null = isVideoNodeSelected
? currentNode
: null
@@ -89,16 +111,7 @@ export const usePaste = () => {
// Look for image paste data
for (const item of items) {
if (item.type.startsWith('image/')) {
- if (!imageNode) {
- // No image node selected: add a new one
- const newNode = LiteGraph.createNode('LoadImage')
- if (newNode) {
- newNode.pos = [canvas.graph_mouse[0], canvas.graph_mouse[1]]
- imageNode = graph?.add(newNode) ?? null
- }
- graph?.change()
- }
- pasteItemsOnNode(items, imageNode, 'image')
+ pasteImageNode(canvas as LGraphCanvas, items, imageNode)
return
} else if (item.type.startsWith('video/')) {
if (!videoNode) {
diff --git a/src/scripts/app.ts b/src/scripts/app.ts
index cd75be7f7..0701f694d 100644
--- a/src/scripts/app.ts
+++ b/src/scripts/app.ts
@@ -96,6 +96,7 @@ import { type ComfyWidgetConstructor } from './widgets'
import { ensureCorrectLayoutScale } from '@/renderer/extensions/vueNodes/layout/ensureCorrectLayoutScale'
import { extractFileFromDragEvent } from '@/utils/eventUtils'
import { getWorkflowDataFromFile } from '@/scripts/metadata/parser'
+import { pasteImageNode } from '@/composables/usePaste'
export const ANIM_PREVIEW_WIDGET = '$$comfy_animation_preview'
@@ -1441,6 +1442,13 @@ export class ComfyApp {
const fileName = file.name.replace(/\.\w+$/, '') // Strip file extension
const workflowData = await getWorkflowDataFromFile(file)
if (!workflowData) {
+ if (file.type.startsWith('image')) {
+ const transfer = new DataTransfer()
+ transfer.items.add(file)
+ pasteImageNode(this.canvas, transfer.items)
+ return
+ }
+
this.showErrorOnFileLoad(file)
return
}
From 11bd9022c82c7ee97f746ba0e6054e62bf26e6e8 Mon Sep 17 00:00:00 2001
From: Comfy Org PR Bot
Date: Sun, 11 Jan 2026 09:26:51 +0900
Subject: [PATCH 06/63] 1.37.9 (#7951)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Patch version increment to 1.37.9
**Base branch:** `main`
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7951-1-37-9-2e56d73d36508115bca9f9f8934ef189)
by [Unito](https://www.unito.io)
---------
Co-authored-by: christian-byrne <72887196+christian-byrne@users.noreply.github.com>
Co-authored-by: GitHub Action
Co-authored-by: github-actions
---
package.json | 2 +-
src/composables/usePaste.ts | 5 ++++-
src/locales/ar/commands.json | 15 ++++++++++++---
src/locales/ar/main.json | 9 +++++++++
src/locales/en/commands.json | 12 ++++++++++++
src/locales/en/main.json | 4 ++++
src/locales/es/commands.json | 15 ++++++++++++---
src/locales/es/main.json | 9 +++++++++
src/locales/fr/commands.json | 15 ++++++++++++---
src/locales/fr/main.json | 9 +++++++++
src/locales/ja/commands.json | 15 ++++++++++++---
src/locales/ja/main.json | 9 +++++++++
src/locales/ko/commands.json | 15 ++++++++++++---
src/locales/ko/main.json | 9 +++++++++
src/locales/pt-BR/commands.json | 15 ++++++++++++---
src/locales/pt-BR/main.json | 9 +++++++++
src/locales/ru/commands.json | 15 ++++++++++++---
src/locales/ru/main.json | 9 +++++++++
src/locales/tr/commands.json | 15 ++++++++++++---
src/locales/tr/main.json | 9 +++++++++
src/locales/zh-TW/commands.json | 15 ++++++++++++---
src/locales/zh-TW/main.json | 9 +++++++++
src/locales/zh/commands.json | 15 ++++++++++++---
src/locales/zh/main.json | 9 +++++++++
24 files changed, 231 insertions(+), 32 deletions(-)
diff --git a/package.json b/package.json
index 5f9458d5a..8c1b19896 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "@comfyorg/comfyui-frontend",
"private": true,
- "version": "1.37.8",
+ "version": "1.37.9",
"type": "module",
"repository": "https://github.com/Comfy-Org/ComfyUI_frontend",
"homepage": "https://comfy.org",
diff --git a/src/composables/usePaste.ts b/src/composables/usePaste.ts
index 6bfefab81..1809eb838 100644
--- a/src/composables/usePaste.ts
+++ b/src/composables/usePaste.ts
@@ -53,7 +53,10 @@ export function pasteImageNode(
items: DataTransferItemList,
imageNode: LGraphNode | null = null
): void {
- const { graph, graph_mouse: [posX, posY] } = canvas
+ const {
+ graph,
+ graph_mouse: [posX, posY]
+ } = canvas
if (!imageNode) {
// No image node selected: add a new one
diff --git a/src/locales/ar/commands.json b/src/locales/ar/commands.json
index dac6940e4..82f7cbdea 100644
--- a/src/locales/ar/commands.json
+++ b/src/locales/ar/commands.json
@@ -191,9 +191,6 @@
"Comfy_Manager_ShowUpdateAvailablePacks": {
"label": "التحقق من تحديثات العقد المخصصة"
},
- "Comfy_Manager_ToggleManagerProgressDialog": {
- "label": "تبديل شريط تقدم مدير العقد المخصصة"
- },
"Comfy_MaskEditor_BrushSize_Decrease": {
"label": "تقليل حجم الفرشاة في محرر القناع"
},
@@ -203,9 +200,21 @@
"Comfy_MaskEditor_ColorPicker": {
"label": "فتح منتقي الألوان في محرر القناع"
},
+ "Comfy_MaskEditor_Mirror_Horizontal": {
+ "label": "انعكاس أفقي في محرر القناع"
+ },
+ "Comfy_MaskEditor_Mirror_Vertical": {
+ "label": "انعكاس عمودي في محرر القناع"
+ },
"Comfy_MaskEditor_OpenMaskEditor": {
"label": "فتح محرر القناع للعقدة المحددة"
},
+ "Comfy_MaskEditor_Rotate_Left": {
+ "label": "تدوير لليسار في محرر القناع"
+ },
+ "Comfy_MaskEditor_Rotate_Right": {
+ "label": "تدوير لليمين في محرر القناع"
+ },
"Comfy_Memory_UnloadModels": {
"label": "تفريغ النماذج"
},
diff --git a/src/locales/ar/main.json b/src/locales/ar/main.json
index f9e372b42..9a4489a21 100644
--- a/src/locales/ar/main.json
+++ b/src/locales/ar/main.json
@@ -1329,12 +1329,16 @@
"maskOpacity": "شفافية القناع",
"maskTolerance": "تسامح القناع",
"method": "طريقة",
+ "mirrorHorizontal": "انعكاس أفقي",
+ "mirrorVertical": "انعكاس عمودي",
"negative": "سلبي",
"opacity": "الشفافية",
"paintBucketSettings": "إعدادات دلو الطلاء",
"paintLayer": "طبقة الطلاء",
"redo": "إعادة",
"resetToDefault": "إعادة إلى الافتراضي",
+ "rotateLeft": "تدوير لليسار",
+ "rotateRight": "تدوير لليمين",
"selectionOpacity": "شفافية التحديد",
"smoothingPrecision": "دقة التنعيم",
"stepSize": "حجم الخطوة",
@@ -1480,6 +1484,8 @@
"Manager": "المدير",
"Manager Menu (Legacy)": "قائمة المدير (قديم)",
"Minimap": "خريطة مصغرة",
+ "Mirror Horizontal in MaskEditor": "انعكاس أفقي في محرر القناع",
+ "Mirror Vertical in MaskEditor": "انعكاس عمودي في محرر القناع",
"Model Library": "مكتبة النماذج",
"Move Selected Nodes Down": "تحريك العقد المحددة للأسفل",
"Move Selected Nodes Left": "تحريك العقد المحددة لليسار",
@@ -1516,6 +1522,8 @@
"Reset View": "إعادة تعيين العرض",
"Resize Selected Nodes": "تغيير حجم العقد المحددة",
"Restart": "إعادة التشغيل",
+ "Rotate Left in MaskEditor": "تدوير لليسار في محرر القناع",
+ "Rotate Right in MaskEditor": "تدوير لليمين في محرر القناع",
"Save": "حفظ",
"Save As": "حفظ باسم",
"Show Keybindings Dialog": "عرض مربع حوار اختصارات لوحة المفاتيح",
@@ -2049,6 +2057,7 @@
"backToAssets": "العودة إلى جميع الأصول",
"browseTemplates": "تصفح القوالب المثال",
"downloads": "التنزيلات",
+ "generatedAssetsHeader": "الأصول المُولدة",
"helpCenter": "مركز المساعدة",
"labels": {
"assets": "الأصول",
diff --git a/src/locales/en/commands.json b/src/locales/en/commands.json
index 1af5d505e..1032083c0 100644
--- a/src/locales/en/commands.json
+++ b/src/locales/en/commands.json
@@ -200,9 +200,21 @@
"Comfy_MaskEditor_ColorPicker": {
"label": "Open Color Picker in MaskEditor"
},
+ "Comfy_MaskEditor_Mirror_Horizontal": {
+ "label": "Mirror Horizontal in MaskEditor"
+ },
+ "Comfy_MaskEditor_Mirror_Vertical": {
+ "label": "Mirror Vertical in MaskEditor"
+ },
"Comfy_MaskEditor_OpenMaskEditor": {
"label": "Open Mask Editor for Selected Node"
},
+ "Comfy_MaskEditor_Rotate_Left": {
+ "label": "Rotate Left in MaskEditor"
+ },
+ "Comfy_MaskEditor_Rotate_Right": {
+ "label": "Rotate Right in MaskEditor"
+ },
"Comfy_Memory_UnloadModels": {
"label": "Unload Models"
},
diff --git a/src/locales/en/main.json b/src/locales/en/main.json
index 44a6d1d3e..04c6b8082 100644
--- a/src/locales/en/main.json
+++ b/src/locales/en/main.json
@@ -1157,7 +1157,11 @@
"Decrease Brush Size in MaskEditor": "Decrease Brush Size in MaskEditor",
"Increase Brush Size in MaskEditor": "Increase Brush Size in MaskEditor",
"Open Color Picker in MaskEditor": "Open Color Picker in MaskEditor",
+ "Mirror Horizontal in MaskEditor": "Mirror Horizontal in MaskEditor",
+ "Mirror Vertical in MaskEditor": "Mirror Vertical in MaskEditor",
"Open Mask Editor for Selected Node": "Open Mask Editor for Selected Node",
+ "Rotate Left in MaskEditor": "Rotate Left in MaskEditor",
+ "Rotate Right in MaskEditor": "Rotate Right in MaskEditor",
"Unload Models": "Unload Models",
"Unload Models and Execution Cache": "Unload Models and Execution Cache",
"New": "New",
diff --git a/src/locales/es/commands.json b/src/locales/es/commands.json
index 9a290ed9a..9a592660b 100644
--- a/src/locales/es/commands.json
+++ b/src/locales/es/commands.json
@@ -191,9 +191,6 @@
"Comfy_Manager_ShowUpdateAvailablePacks": {
"label": "Buscar actualizaciones"
},
- "Comfy_Manager_ToggleManagerProgressDialog": {
- "label": "Alternar diálogo de progreso del administrador"
- },
"Comfy_MaskEditor_BrushSize_Decrease": {
"label": "Disminuir tamaño del pincel en MaskEditor"
},
@@ -203,9 +200,21 @@
"Comfy_MaskEditor_ColorPicker": {
"label": "Abrir selector de color en MaskEditor"
},
+ "Comfy_MaskEditor_Mirror_Horizontal": {
+ "label": "Espejar horizontalmente en MaskEditor"
+ },
+ "Comfy_MaskEditor_Mirror_Vertical": {
+ "label": "Espejar verticalmente en MaskEditor"
+ },
"Comfy_MaskEditor_OpenMaskEditor": {
"label": "Abrir editor de máscara para el nodo seleccionado"
},
+ "Comfy_MaskEditor_Rotate_Left": {
+ "label": "Girar a la izquierda en MaskEditor"
+ },
+ "Comfy_MaskEditor_Rotate_Right": {
+ "label": "Girar a la derecha en MaskEditor"
+ },
"Comfy_Memory_UnloadModels": {
"label": "Descargar modelos"
},
diff --git a/src/locales/es/main.json b/src/locales/es/main.json
index 456e154b9..130d52432 100644
--- a/src/locales/es/main.json
+++ b/src/locales/es/main.json
@@ -1329,12 +1329,16 @@
"maskOpacity": "Opacidad de máscara",
"maskTolerance": "Tolerancia de máscara",
"method": "Método",
+ "mirrorHorizontal": "Espejar horizontalmente",
+ "mirrorVertical": "Espejar verticalmente",
"negative": "Negativo",
"opacity": "Opacidad",
"paintBucketSettings": "Configuración del bote de pintura",
"paintLayer": "Capa de pintura",
"redo": "Rehacer",
"resetToDefault": "Restablecer a valores predeterminados",
+ "rotateLeft": "Girar a la izquierda",
+ "rotateRight": "Girar a la derecha",
"selectionOpacity": "Opacidad de selección",
"smoothingPrecision": "Precisión de suavizado",
"stepSize": "Tamaño del paso",
@@ -1480,6 +1484,8 @@
"Manager": "Administrador",
"Manager Menu (Legacy)": "Menú de gestión (heredado)",
"Minimap": "Minimapa",
+ "Mirror Horizontal in MaskEditor": "Espejar horizontalmente en el editor de máscaras",
+ "Mirror Vertical in MaskEditor": "Espejar verticalmente en el editor de máscaras",
"Model Library": "Biblioteca de Modelos",
"Move Selected Nodes Down": "Mover nodos seleccionados hacia abajo",
"Move Selected Nodes Left": "Mover nodos seleccionados hacia la izquierda",
@@ -1516,6 +1522,8 @@
"Reset View": "Restablecer vista",
"Resize Selected Nodes": "Redimensionar Nodos Seleccionados",
"Restart": "Reiniciar",
+ "Rotate Left in MaskEditor": "Girar a la izquierda en el editor de máscaras",
+ "Rotate Right in MaskEditor": "Girar a la derecha en el editor de máscaras",
"Save": "Guardar",
"Save As": "Guardar como",
"Show Keybindings Dialog": "Mostrar diálogo de combinaciones de teclas",
@@ -2049,6 +2057,7 @@
"backToAssets": "Volver a todos los recursos",
"browseTemplates": "Explorar plantillas de ejemplo",
"downloads": "Descargas",
+ "generatedAssetsHeader": "Recursos generados",
"helpCenter": "Centro de ayuda",
"labels": {
"assets": "Recursos",
diff --git a/src/locales/fr/commands.json b/src/locales/fr/commands.json
index d4cb1dc54..3673bfd09 100644
--- a/src/locales/fr/commands.json
+++ b/src/locales/fr/commands.json
@@ -191,9 +191,6 @@
"Comfy_Manager_ShowUpdateAvailablePacks": {
"label": "Vérifier les mises à jour"
},
- "Comfy_Manager_ToggleManagerProgressDialog": {
- "label": "Basculer la boîte de dialogue de progression"
- },
"Comfy_MaskEditor_BrushSize_Decrease": {
"label": "Réduire la taille du pinceau dans MaskEditor"
},
@@ -203,9 +200,21 @@
"Comfy_MaskEditor_ColorPicker": {
"label": "Ouvrir le sélecteur de couleur dans MaskEditor"
},
+ "Comfy_MaskEditor_Mirror_Horizontal": {
+ "label": "Miroir horizontal dans MaskEditor"
+ },
+ "Comfy_MaskEditor_Mirror_Vertical": {
+ "label": "Miroir vertical dans MaskEditor"
+ },
"Comfy_MaskEditor_OpenMaskEditor": {
"label": "Ouvrir l'éditeur de masque pour le nœud sélectionné"
},
+ "Comfy_MaskEditor_Rotate_Left": {
+ "label": "Rotation à gauche dans MaskEditor"
+ },
+ "Comfy_MaskEditor_Rotate_Right": {
+ "label": "Rotation à droite dans MaskEditor"
+ },
"Comfy_Memory_UnloadModels": {
"label": "Décharger les modèles"
},
diff --git a/src/locales/fr/main.json b/src/locales/fr/main.json
index cbe54e5b6..93445a675 100644
--- a/src/locales/fr/main.json
+++ b/src/locales/fr/main.json
@@ -1329,12 +1329,16 @@
"maskOpacity": "Opacité du masque",
"maskTolerance": "Tolérance du masque",
"method": "Méthode",
+ "mirrorHorizontal": "Miroir horizontal",
+ "mirrorVertical": "Miroir vertical",
"negative": "Négatif",
"opacity": "Opacité",
"paintBucketSettings": "Paramètres du pot de peinture",
"paintLayer": "Calque de peinture",
"redo": "Rétablir",
"resetToDefault": "Réinitialiser par défaut",
+ "rotateLeft": "Tourner à gauche",
+ "rotateRight": "Tourner à droite",
"selectionOpacity": "Opacité de la sélection",
"smoothingPrecision": "Précision du lissage",
"stepSize": "Taille de pas",
@@ -1480,6 +1484,8 @@
"Manager": "Gestionnaire",
"Manager Menu (Legacy)": "Menu du gestionnaire (héritage)",
"Minimap": "Mini-carte",
+ "Mirror Horizontal in MaskEditor": "Miroir horizontal dans l'éditeur de masque",
+ "Mirror Vertical in MaskEditor": "Miroir vertical dans l'éditeur de masque",
"Model Library": "Bibliothèque de modèles",
"Move Selected Nodes Down": "Déplacer les nœuds sélectionnés vers le bas",
"Move Selected Nodes Left": "Déplacer les nœuds sélectionnés vers la gauche",
@@ -1516,6 +1522,8 @@
"Reset View": "Réinitialiser la vue",
"Resize Selected Nodes": "Redimensionner les nœuds sélectionnés",
"Restart": "Redémarrer",
+ "Rotate Left in MaskEditor": "Tourner à gauche dans l'éditeur de masque",
+ "Rotate Right in MaskEditor": "Tourner à droite dans l'éditeur de masque",
"Save": "Enregistrer",
"Save As": "Enregistrer sous",
"Show Keybindings Dialog": "Afficher la boîte de dialogue des raccourcis clavier",
@@ -2049,6 +2057,7 @@
"backToAssets": "Retour à toutes les ressources",
"browseTemplates": "Parcourir les modèles d'exemple",
"downloads": "Téléchargements",
+ "generatedAssetsHeader": "Ressources générées",
"helpCenter": "Centre d'aide",
"labels": {
"assets": "Ressources",
diff --git a/src/locales/ja/commands.json b/src/locales/ja/commands.json
index 2a1b4d88b..07814cc7a 100644
--- a/src/locales/ja/commands.json
+++ b/src/locales/ja/commands.json
@@ -191,9 +191,6 @@
"Comfy_Manager_ShowUpdateAvailablePacks": {
"label": "更新を確認"
},
- "Comfy_Manager_ToggleManagerProgressDialog": {
- "label": "プログレスダイアログの切り替え"
- },
"Comfy_MaskEditor_BrushSize_Decrease": {
"label": "マスクエディタでブラシサイズを縮小"
},
@@ -203,9 +200,21 @@
"Comfy_MaskEditor_ColorPicker": {
"label": "MaskEditorでカラーピッカーを開く"
},
+ "Comfy_MaskEditor_Mirror_Horizontal": {
+ "label": "マスクエディタで左右反転"
+ },
+ "Comfy_MaskEditor_Mirror_Vertical": {
+ "label": "マスクエディタで上下反転"
+ },
"Comfy_MaskEditor_OpenMaskEditor": {
"label": "選択したノードのマスクエディタを開く"
},
+ "Comfy_MaskEditor_Rotate_Left": {
+ "label": "マスクエディタで左回転"
+ },
+ "Comfy_MaskEditor_Rotate_Right": {
+ "label": "マスクエディタで右回転"
+ },
"Comfy_Memory_UnloadModels": {
"label": "モデルのアンロード"
},
diff --git a/src/locales/ja/main.json b/src/locales/ja/main.json
index 866faca84..96f5256c6 100644
--- a/src/locales/ja/main.json
+++ b/src/locales/ja/main.json
@@ -1329,12 +1329,16 @@
"maskOpacity": "マスク不透明度",
"maskTolerance": "マスク許容値",
"method": "方法",
+ "mirrorHorizontal": "左右反転",
+ "mirrorVertical": "上下反転",
"negative": "ネガティブ",
"opacity": "不透明度",
"paintBucketSettings": "ペイントバケツ設定",
"paintLayer": "ペイントレイヤー",
"redo": "やり直し",
"resetToDefault": "デフォルトにリセット",
+ "rotateLeft": "左に回転",
+ "rotateRight": "右に回転",
"selectionOpacity": "選択範囲の不透明度",
"smoothingPrecision": "スムージング精度",
"stepSize": "ステップサイズ",
@@ -1480,6 +1484,8 @@
"Manager": "マネージャー",
"Manager Menu (Legacy)": "マネージャーメニュー(レガシー)",
"Minimap": "ミニマップ",
+ "Mirror Horizontal in MaskEditor": "マスクエディタで左右反転",
+ "Mirror Vertical in MaskEditor": "マスクエディタで上下反転",
"Model Library": "モデルライブラリ",
"Move Selected Nodes Down": "選択したノードを下へ移動",
"Move Selected Nodes Left": "選択したノードを左へ移動",
@@ -1516,6 +1522,8 @@
"Reset View": "ビューをリセット",
"Resize Selected Nodes": "選択したノードのサイズ変更",
"Restart": "再起動",
+ "Rotate Left in MaskEditor": "マスクエディタで左に回転",
+ "Rotate Right in MaskEditor": "マスクエディタで右に回転",
"Save": "保存",
"Save As": "名前を付けて保存",
"Show Keybindings Dialog": "キーバインドダイアログを表示",
@@ -2049,6 +2057,7 @@
"backToAssets": "すべてのアセットに戻る",
"browseTemplates": "サンプルテンプレートを表示",
"downloads": "ダウンロード",
+ "generatedAssetsHeader": "生成されたアセット",
"helpCenter": "ヘルプセンター",
"labels": {
"assets": "アセット",
diff --git a/src/locales/ko/commands.json b/src/locales/ko/commands.json
index 707da5a7c..0bd325d14 100644
--- a/src/locales/ko/commands.json
+++ b/src/locales/ko/commands.json
@@ -191,9 +191,6 @@
"Comfy_Manager_ShowUpdateAvailablePacks": {
"label": "업데이트 확인"
},
- "Comfy_Manager_ToggleManagerProgressDialog": {
- "label": "진행 상황 대화 상자 전환"
- },
"Comfy_MaskEditor_BrushSize_Decrease": {
"label": "마스크 편집기에서 브러시 크기 줄이기"
},
@@ -203,9 +200,21 @@
"Comfy_MaskEditor_ColorPicker": {
"label": "MaskEditor에서 색상 선택기 열기"
},
+ "Comfy_MaskEditor_Mirror_Horizontal": {
+ "label": "마스크 에디터에서 수평 반전"
+ },
+ "Comfy_MaskEditor_Mirror_Vertical": {
+ "label": "마스크 에디터에서 수직 반전"
+ },
"Comfy_MaskEditor_OpenMaskEditor": {
"label": "선택한 노드 마스크 편집기 열기"
},
+ "Comfy_MaskEditor_Rotate_Left": {
+ "label": "마스크 에디터에서 왼쪽으로 회전"
+ },
+ "Comfy_MaskEditor_Rotate_Right": {
+ "label": "마스크 에디터에서 오른쪽으로 회전"
+ },
"Comfy_Memory_UnloadModels": {
"label": "모델 언로드"
},
diff --git a/src/locales/ko/main.json b/src/locales/ko/main.json
index 6010adecb..38776a662 100644
--- a/src/locales/ko/main.json
+++ b/src/locales/ko/main.json
@@ -1329,12 +1329,16 @@
"maskOpacity": "마스크 불투명도",
"maskTolerance": "마스크 허용치",
"method": "방법",
+ "mirrorHorizontal": "수평 반전",
+ "mirrorVertical": "수직 반전",
"negative": "네거티브",
"opacity": "불투명도",
"paintBucketSettings": "페인트 버킷 설정",
"paintLayer": "페인트 레이어",
"redo": "다시 실행",
"resetToDefault": "기본값으로 재설정",
+ "rotateLeft": "왼쪽으로 회전",
+ "rotateRight": "오른쪽으로 회전",
"selectionOpacity": "선택 영역 불투명도",
"smoothingPrecision": "부드럽기 정밀도",
"stepSize": "단계 크기",
@@ -1480,6 +1484,8 @@
"Manager": "매니저",
"Manager Menu (Legacy)": "매니저 메뉴(구버전)",
"Minimap": "미니맵",
+ "Mirror Horizontal in MaskEditor": "마스크 편집기에서 수평 반전",
+ "Mirror Vertical in MaskEditor": "마스크 편집기에서 수직 반전",
"Model Library": "모델 라이브러리",
"Move Selected Nodes Down": "선택한 노드 아래로 이동",
"Move Selected Nodes Left": "선택한 노드 왼쪽으로 이동",
@@ -1516,6 +1522,8 @@
"Reset View": "보기 초기화",
"Resize Selected Nodes": "선택된 노드 크기 조정",
"Restart": "재시작",
+ "Rotate Left in MaskEditor": "마스크 편집기에서 왼쪽으로 회전",
+ "Rotate Right in MaskEditor": "마스크 편집기에서 오른쪽으로 회전",
"Save": "저장",
"Save As": "다른 이름으로 저장",
"Show Keybindings Dialog": "단축키 대화상자 표시",
@@ -2049,6 +2057,7 @@
"backToAssets": "모든 에셋으로 돌아가기",
"browseTemplates": "예제 템플릿 탐색",
"downloads": "다운로드",
+ "generatedAssetsHeader": "생성된 에셋",
"helpCenter": "도움말 센터",
"labels": {
"assets": "에셋",
diff --git a/src/locales/pt-BR/commands.json b/src/locales/pt-BR/commands.json
index 6d2c3de4d..87004ec52 100644
--- a/src/locales/pt-BR/commands.json
+++ b/src/locales/pt-BR/commands.json
@@ -191,9 +191,6 @@
"Comfy_Manager_ShowUpdateAvailablePacks": {
"label": "Verificar Atualizações de Nós Personalizados"
},
- "Comfy_Manager_ToggleManagerProgressDialog": {
- "label": "Alternar Barra de Progresso do Gerenciador de Nós Personalizados"
- },
"Comfy_MaskEditor_BrushSize_Decrease": {
"label": "Diminuir tamanho do pincel no Editor de Máscara"
},
@@ -203,9 +200,21 @@
"Comfy_MaskEditor_ColorPicker": {
"label": "Abrir Seletor de Cores no Editor de Máscara"
},
+ "Comfy_MaskEditor_Mirror_Horizontal": {
+ "label": "Espelhar horizontalmente no MaskEditor"
+ },
+ "Comfy_MaskEditor_Mirror_Vertical": {
+ "label": "Espelhar verticalmente no MaskEditor"
+ },
"Comfy_MaskEditor_OpenMaskEditor": {
"label": "Abrir Editor de Máscara para o nó selecionado"
},
+ "Comfy_MaskEditor_Rotate_Left": {
+ "label": "Girar para a esquerda no MaskEditor"
+ },
+ "Comfy_MaskEditor_Rotate_Right": {
+ "label": "Girar para a direita no MaskEditor"
+ },
"Comfy_Memory_UnloadModels": {
"label": "Descarregar Modelos"
},
diff --git a/src/locales/pt-BR/main.json b/src/locales/pt-BR/main.json
index 84ec5c610..61cbc1e36 100644
--- a/src/locales/pt-BR/main.json
+++ b/src/locales/pt-BR/main.json
@@ -1329,12 +1329,16 @@
"maskOpacity": "Opacidade da máscara",
"maskTolerance": "Tolerância da máscara",
"method": "Método",
+ "mirrorHorizontal": "Espelhar horizontalmente",
+ "mirrorVertical": "Espelhar verticalmente",
"negative": "Negativo",
"opacity": "Opacidade",
"paintBucketSettings": "Configurações do balde de tinta",
"paintLayer": "Camada de pintura",
"redo": "Refazer",
"resetToDefault": "Restaurar padrão",
+ "rotateLeft": "Girar para a esquerda",
+ "rotateRight": "Girar para a direita",
"selectionOpacity": "Opacidade da seleção",
"smoothingPrecision": "Precisão de suavização",
"stepSize": "Tamanho do passo",
@@ -1480,6 +1484,8 @@
"Manager": "Gerenciador",
"Manager Menu (Legacy)": "Menu do gerenciador (Legado)",
"Minimap": "Minimapa",
+ "Mirror Horizontal in MaskEditor": "Espelhar horizontalmente no MaskEditor",
+ "Mirror Vertical in MaskEditor": "Espelhar verticalmente no MaskEditor",
"Model Library": "Biblioteca de modelos",
"Move Selected Nodes Down": "Mover nós selecionados para baixo",
"Move Selected Nodes Left": "Mover nós selecionados para a esquerda",
@@ -1516,6 +1522,8 @@
"Reset View": "Redefinir visualização",
"Resize Selected Nodes": "Redimensionar nós selecionados",
"Restart": "Reiniciar",
+ "Rotate Left in MaskEditor": "Girar para a esquerda no MaskEditor",
+ "Rotate Right in MaskEditor": "Girar para a direita no MaskEditor",
"Save": "Salvar",
"Save As": "Salvar como",
"Show Keybindings Dialog": "Mostrar diálogo de atalhos",
@@ -2049,6 +2057,7 @@
"backToAssets": "Voltar para todos os ativos",
"browseTemplates": "Explorar modelos de exemplo",
"downloads": "Downloads",
+ "generatedAssetsHeader": "Ativos gerados",
"helpCenter": "Central de Ajuda",
"labels": {
"assets": "Ativos",
diff --git a/src/locales/ru/commands.json b/src/locales/ru/commands.json
index 275152366..bd77a89f9 100644
--- a/src/locales/ru/commands.json
+++ b/src/locales/ru/commands.json
@@ -191,9 +191,6 @@
"Comfy_Manager_ShowUpdateAvailablePacks": {
"label": "Проверить наличие обновлений"
},
- "Comfy_Manager_ToggleManagerProgressDialog": {
- "label": "Переключить диалоговое окно прогресса"
- },
"Comfy_MaskEditor_BrushSize_Decrease": {
"label": "Уменьшить размер кисти в MaskEditor"
},
@@ -203,9 +200,21 @@
"Comfy_MaskEditor_ColorPicker": {
"label": "Открыть палитру цветов в MaskEditor"
},
+ "Comfy_MaskEditor_Mirror_Horizontal": {
+ "label": "Отразить по горизонтали в MaskEditor"
+ },
+ "Comfy_MaskEditor_Mirror_Vertical": {
+ "label": "Отразить по вертикали в MaskEditor"
+ },
"Comfy_MaskEditor_OpenMaskEditor": {
"label": "Открыть редактор масок для выбранной ноды"
},
+ "Comfy_MaskEditor_Rotate_Left": {
+ "label": "Повернуть влево в MaskEditor"
+ },
+ "Comfy_MaskEditor_Rotate_Right": {
+ "label": "Повернуть вправо в MaskEditor"
+ },
"Comfy_Memory_UnloadModels": {
"label": "Выгрузить модели"
},
diff --git a/src/locales/ru/main.json b/src/locales/ru/main.json
index 1e85ee9fc..11b59128d 100644
--- a/src/locales/ru/main.json
+++ b/src/locales/ru/main.json
@@ -1329,12 +1329,16 @@
"maskOpacity": "Непрозрачность маски",
"maskTolerance": "Допуск маски",
"method": "Метод",
+ "mirrorHorizontal": "Отразить по горизонтали",
+ "mirrorVertical": "Отразить по вертикали",
"negative": "Негатив",
"opacity": "Непрозрачность",
"paintBucketSettings": "Настройки заливки",
"paintLayer": "Слой рисования",
"redo": "Повторить",
"resetToDefault": "Сбросить по умолчанию",
+ "rotateLeft": "Повернуть влево",
+ "rotateRight": "Повернуть вправо",
"selectionOpacity": "Непрозрачность выделения",
"smoothingPrecision": "Точность сглаживания",
"stepSize": "Размер шага",
@@ -1480,6 +1484,8 @@
"Manager": "Менеджер",
"Manager Menu (Legacy)": "Меню управления (устаревшее)",
"Minimap": "Мини-карта",
+ "Mirror Horizontal in MaskEditor": "Отразить по горизонтали в MaskEditor",
+ "Mirror Vertical in MaskEditor": "Отразить по вертикали в MaskEditor",
"Model Library": "Библиотека моделей",
"Move Selected Nodes Down": "Переместить выбранные узлы вниз",
"Move Selected Nodes Left": "Переместить выбранные узлы влево",
@@ -1516,6 +1522,8 @@
"Reset View": "Сбросить вид",
"Resize Selected Nodes": "Изменить размер выбранных узлов",
"Restart": "Перезапустить",
+ "Rotate Left in MaskEditor": "Повернуть влево в MaskEditor",
+ "Rotate Right in MaskEditor": "Повернуть вправо в MaskEditor",
"Save": "Сохранить",
"Save As": "Сохранить как",
"Show Keybindings Dialog": "Показать диалог клавиш быстрого доступа",
@@ -2049,6 +2057,7 @@
"backToAssets": "Назад ко всем ассетам",
"browseTemplates": "Просмотреть примеры шаблонов",
"downloads": "Загрузки",
+ "generatedAssetsHeader": "Сгенерированные ресурсы",
"helpCenter": "Центр поддержки",
"labels": {
"assets": "Ассеты",
diff --git a/src/locales/tr/commands.json b/src/locales/tr/commands.json
index 8910ddd64..8ca03bcfe 100644
--- a/src/locales/tr/commands.json
+++ b/src/locales/tr/commands.json
@@ -191,9 +191,6 @@
"Comfy_Manager_ShowUpdateAvailablePacks": {
"label": "Özel Düğüm Güncellemelerini Kontrol Et"
},
- "Comfy_Manager_ToggleManagerProgressDialog": {
- "label": "Özel Düğüm Yöneticisi İlerleme Çubuğunu Aç/Kapat"
- },
"Comfy_MaskEditor_BrushSize_Decrease": {
"label": "Maske Düzenleyicide Fırça Boyutunu Azalt"
},
@@ -203,9 +200,21 @@
"Comfy_MaskEditor_ColorPicker": {
"label": "MaskEditor'da Renk Seçiciyi Aç"
},
+ "Comfy_MaskEditor_Mirror_Horizontal": {
+ "label": "MaskEditor'da Yatay Aynala"
+ },
+ "Comfy_MaskEditor_Mirror_Vertical": {
+ "label": "MaskEditor'da Dikey Aynala"
+ },
"Comfy_MaskEditor_OpenMaskEditor": {
"label": "Seçili Düğüm için Maske Düzenleyiciyi Aç"
},
+ "Comfy_MaskEditor_Rotate_Left": {
+ "label": "MaskEditor'da Sola Döndür"
+ },
+ "Comfy_MaskEditor_Rotate_Right": {
+ "label": "MaskEditor'da Sağa Döndür"
+ },
"Comfy_Memory_UnloadModels": {
"label": "Modelleri Boşalt"
},
diff --git a/src/locales/tr/main.json b/src/locales/tr/main.json
index 380ee1a39..8b9ff437d 100644
--- a/src/locales/tr/main.json
+++ b/src/locales/tr/main.json
@@ -1329,12 +1329,16 @@
"maskOpacity": "Maske Opaklığı",
"maskTolerance": "Maske Toleransı",
"method": "Yöntem",
+ "mirrorHorizontal": "Yatay ayna",
+ "mirrorVertical": "Dikey ayna",
"negative": "Negatif",
"opacity": "Opaklık",
"paintBucketSettings": "Boya Kovası Ayarları",
"paintLayer": "Boya Katmanı",
"redo": "Yinele",
"resetToDefault": "Varsayılana Sıfırla",
+ "rotateLeft": "Sola döndür",
+ "rotateRight": "Sağa döndür",
"selectionOpacity": "Seçim Opaklığı",
"smoothingPrecision": "Yumuşatma Hassasiyeti",
"stepSize": "Adım Boyutu",
@@ -1480,6 +1484,8 @@
"Manager": "Yönetici",
"Manager Menu (Legacy)": "Yönetici Menüsü (Eski)",
"Minimap": "Mini Harita",
+ "Mirror Horizontal in MaskEditor": "MaskEditor'da yatay ayna",
+ "Mirror Vertical in MaskEditor": "MaskEditor'da dikey ayna",
"Model Library": "Model Kütüphanesi",
"Move Selected Nodes Down": "Seçili Düğümleri Aşağı Taşı",
"Move Selected Nodes Left": "Seçili Düğümleri Sola Taşı",
@@ -1516,6 +1522,8 @@
"Reset View": "Görünümü Sıfırla",
"Resize Selected Nodes": "Seçili Düğümleri Yeniden Boyutlandır",
"Restart": "Yeniden Başlat",
+ "Rotate Left in MaskEditor": "MaskEditor'da sola döndür",
+ "Rotate Right in MaskEditor": "MaskEditor'da sağa döndür",
"Save": "Kaydet",
"Save As": "Farklı Kaydet",
"Show Keybindings Dialog": "Tuş Atamaları İletişim Kutusunu Göster",
@@ -2049,6 +2057,7 @@
"backToAssets": "Tüm varlıklara dön",
"browseTemplates": "Örnek şablonlara göz atın",
"downloads": "İndirmeler",
+ "generatedAssetsHeader": "Oluşturulan varlıklar",
"helpCenter": "Yardım Merkezi",
"labels": {
"assets": "Varlıklar",
diff --git a/src/locales/zh-TW/commands.json b/src/locales/zh-TW/commands.json
index f22554bc5..9b98bd230 100644
--- a/src/locales/zh-TW/commands.json
+++ b/src/locales/zh-TW/commands.json
@@ -191,9 +191,6 @@
"Comfy_Manager_ShowUpdateAvailablePacks": {
"label": "檢查自訂節點更新"
},
- "Comfy_Manager_ToggleManagerProgressDialog": {
- "label": "切換自訂節點管理器進度條"
- },
"Comfy_MaskEditor_BrushSize_Decrease": {
"label": "減少 MaskEditor 畫筆大小"
},
@@ -203,9 +200,21 @@
"Comfy_MaskEditor_ColorPicker": {
"label": "在 MaskEditor 中開啟顏色選擇器"
},
+ "Comfy_MaskEditor_Mirror_Horizontal": {
+ "label": "在 MaskEditor 中水平鏡像"
+ },
+ "Comfy_MaskEditor_Mirror_Vertical": {
+ "label": "在 MaskEditor 中垂直鏡像"
+ },
"Comfy_MaskEditor_OpenMaskEditor": {
"label": "為選取的節點開啟 Mask 編輯器"
},
+ "Comfy_MaskEditor_Rotate_Left": {
+ "label": "在 MaskEditor 中向左旋轉"
+ },
+ "Comfy_MaskEditor_Rotate_Right": {
+ "label": "在 MaskEditor 中向右旋轉"
+ },
"Comfy_Memory_UnloadModels": {
"label": "卸載模型"
},
diff --git a/src/locales/zh-TW/main.json b/src/locales/zh-TW/main.json
index fce5855d7..0e7e14e7b 100644
--- a/src/locales/zh-TW/main.json
+++ b/src/locales/zh-TW/main.json
@@ -1329,12 +1329,16 @@
"maskOpacity": "遮罩不透明度",
"maskTolerance": "遮罩容差",
"method": "方法",
+ "mirrorHorizontal": "水平鏡像",
+ "mirrorVertical": "垂直鏡像",
"negative": "負片",
"opacity": "不透明度",
"paintBucketSettings": "油漆桶設定",
"paintLayer": "繪圖圖層",
"redo": "重做",
"resetToDefault": "重設為預設值",
+ "rotateLeft": "向左旋轉",
+ "rotateRight": "向右旋轉",
"selectionOpacity": "選取不透明度",
"smoothingPrecision": "平滑精度",
"stepSize": "步進大小",
@@ -1480,6 +1484,8 @@
"Manager": "管理員",
"Manager Menu (Legacy)": "管理員選單(舊版)",
"Minimap": "迷你地圖",
+ "Mirror Horizontal in MaskEditor": "在遮罩編輯器中水平鏡像",
+ "Mirror Vertical in MaskEditor": "在遮罩編輯器中垂直鏡像",
"Model Library": "模型庫",
"Move Selected Nodes Down": "選取節點下移",
"Move Selected Nodes Left": "選取節點左移",
@@ -1516,6 +1522,8 @@
"Reset View": "重設視圖",
"Resize Selected Nodes": "調整選取節點大小",
"Restart": "重新啟動",
+ "Rotate Left in MaskEditor": "在遮罩編輯器中向左旋轉",
+ "Rotate Right in MaskEditor": "在遮罩編輯器中向右旋轉",
"Save": "儲存",
"Save As": "另存新檔",
"Show Keybindings Dialog": "顯示快捷鍵對話框",
@@ -2049,6 +2057,7 @@
"backToAssets": "返回所有資源",
"browseTemplates": "瀏覽範例模板",
"downloads": "下載",
+ "generatedAssetsHeader": "已產生資產",
"helpCenter": "說明中心",
"labels": {
"assets": "資源",
diff --git a/src/locales/zh/commands.json b/src/locales/zh/commands.json
index 11ee23a95..7e0132964 100644
--- a/src/locales/zh/commands.json
+++ b/src/locales/zh/commands.json
@@ -191,9 +191,6 @@
"Comfy_Manager_ShowUpdateAvailablePacks": {
"label": "检查更新"
},
- "Comfy_Manager_ToggleManagerProgressDialog": {
- "label": "切换进度对话框"
- },
"Comfy_MaskEditor_BrushSize_Decrease": {
"label": "减小 MaskEditor 中的笔刷大小"
},
@@ -203,9 +200,21 @@
"Comfy_MaskEditor_ColorPicker": {
"label": "在MaskEditor中打开取色器"
},
+ "Comfy_MaskEditor_Mirror_Horizontal": {
+ "label": "在MaskEditor中水平镜像"
+ },
+ "Comfy_MaskEditor_Mirror_Vertical": {
+ "label": "在MaskEditor中垂直镜像"
+ },
"Comfy_MaskEditor_OpenMaskEditor": {
"label": "打开选中节点的遮罩编辑器"
},
+ "Comfy_MaskEditor_Rotate_Left": {
+ "label": "在MaskEditor中向左旋转"
+ },
+ "Comfy_MaskEditor_Rotate_Right": {
+ "label": "在MaskEditor中向右旋转"
+ },
"Comfy_Memory_UnloadModels": {
"label": "卸载模型"
},
diff --git a/src/locales/zh/main.json b/src/locales/zh/main.json
index 679b01098..3206a2977 100644
--- a/src/locales/zh/main.json
+++ b/src/locales/zh/main.json
@@ -1329,12 +1329,16 @@
"maskOpacity": "遮罩不透明度",
"maskTolerance": "遮罩阈值",
"method": "方法",
+ "mirrorHorizontal": "水平翻转",
+ "mirrorVertical": "垂直翻转",
"negative": "负面",
"opacity": "不透明度",
"paintBucketSettings": "填充设置",
"paintLayer": "绘画层",
"redo": "重做",
"resetToDefault": "重置为默认",
+ "rotateLeft": "向左旋转",
+ "rotateRight": "向右旋转",
"selectionOpacity": "选取不透明度",
"smoothingPrecision": "预测平滑",
"stepSize": "间距",
@@ -1480,6 +1484,8 @@
"Manager": "管理器",
"Manager Menu (Legacy)": "管理菜单(旧版)",
"Minimap": "小地图",
+ "Mirror Horizontal in MaskEditor": "在蒙版编辑器中水平翻转",
+ "Mirror Vertical in MaskEditor": "在蒙版编辑器中垂直翻转",
"Model Library": "模型库",
"Move Selected Nodes Down": "下移所选节点",
"Move Selected Nodes Left": "左移所选节点",
@@ -1516,6 +1522,8 @@
"Reset View": "重置视图",
"Resize Selected Nodes": "调整选定节点的大小",
"Restart": "重启",
+ "Rotate Left in MaskEditor": "在蒙版编辑器中向左旋转",
+ "Rotate Right in MaskEditor": "在蒙版编辑器中向右旋转",
"Save": "保存",
"Save As": "另存为",
"Show Keybindings Dialog": "显示快捷键对话框",
@@ -2049,6 +2057,7 @@
"backToAssets": "返回所有资产",
"browseTemplates": "浏览示例模板",
"downloads": "下载",
+ "generatedAssetsHeader": "生成的资源",
"helpCenter": "帮助中心",
"labels": {
"assets": "资产",
From dbb0bd961fe65bd42d06da7764385abe1dea4e22 Mon Sep 17 00:00:00 2001
From: Alexander Brown
Date: Sat, 10 Jan 2026 21:17:31 -0800
Subject: [PATCH 07/63] Chore: TypeScript cleanup - remove 254 @ts-expect-error
suppressions (#7884)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## Summary
Removes **254** `@ts-expect-error` suppressions through proper type
fixes rather than type assertions.
## Key Changes
### Type System Improvements
- Add `globalDefs` and `groupNodes` types to `ComfyAppWindowExtension`
- Extract interfaces for group node handling (`GroupNodeHandler`,
`InnerNodeOutput`, etc.)
- Add `getHandler()` helper to consolidate GROUP symbol access pattern
### Files Fixed
- **pnginfo.ts**: 39 suppressions removed via proper typing of
workflow/prompt data
- **app.ts**: 39 suppressions removed via interface extraction and type
narrowing
- **Tier 1 files**: 17 suppressions removed (maskeditor, imageDrawer,
groupNode, etc.)
- **groupNode.ts**: Major refactoring with proper interface organization
## Approach
Following established constraints:
- No `any` types
- No `as unknown as T` casts (except legacy API boundaries)
- Priority: Fix actual types > Type narrowing > Targeted suppressions as
last resort
- Prefix unused callback parameters with underscore
- Extract repeated inline types into named interfaces
## Validation
- ✅ `pnpm typecheck` passes
- ✅ `pnpm lint` passes
- ✅ `pnpm knip` passes
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7884-Chore-TypeScript-cleanup-remove-254-ts-expect-error-suppressions-2e26d73d3650812e9b48da203ce1d296)
by [Unito](https://www.unito.io)
---------
Co-authored-by: Amp
---
src/extensions/core/groupNode.ts | 1298 ++++++++++-------
src/extensions/core/groupNodeManage.ts | 11 +-
src/extensions/core/nodeTemplates.ts | 12 +-
src/lib/litegraph/src/LGraph.ts | 15 +-
.../validation/schemas/workflowSchema.ts | 1 -
src/scripts/app.ts | 188 +--
src/scripts/pnginfo.ts | 264 ++--
7 files changed, 1005 insertions(+), 784 deletions(-)
diff --git a/src/extensions/core/groupNode.ts b/src/extensions/core/groupNode.ts
index 0dac84e94..a7af7361a 100644
--- a/src/extensions/core/groupNode.ts
+++ b/src/extensions/core/groupNode.ts
@@ -1,21 +1,23 @@
import { PREFIX, SEPARATOR } from '@/constants/groupNodeConstants'
import { t } from '@/i18n'
-import { type NodeId } from '@/lib/litegraph/src/LGraphNode'
+import type { GroupNodeWorkflowData } from '@/lib/litegraph/src/LGraph'
+import type { SerialisedLLinkArray } from '@/lib/litegraph/src/LLink'
+import type { NodeId } from '@/lib/litegraph/src/LGraphNode'
import type { IContextMenuValue } from '@/lib/litegraph/src/interfaces'
import {
type ExecutableLGraphNode,
type ExecutionId,
LGraphNode,
+ type LGraphNodeConstructor,
LiteGraph,
SubgraphNode
} from '@/lib/litegraph/src/litegraph'
import { useToastStore } from '@/platform/updates/common/toastStore'
import {
- type ComfyLink,
type ComfyNode,
type ComfyWorkflowJSON
} from '@/platform/workflow/validation/schemas/workflowSchema'
-import type { ComfyNodeDef } from '@/schemas/nodeDefSchema'
+import type { ComfyNodeDef, InputSpec } from '@/schemas/nodeDefSchema'
import { useDialogService } from '@/services/dialogService'
import { useExecutionStore } from '@/stores/executionStore'
import { useNodeDefStore } from '@/stores/nodeDefStore'
@@ -30,10 +32,55 @@ import { app } from '../../scripts/app'
import { ManageGroupDialog } from './groupNodeManage'
import { mergeIfValid } from './widgetInputs'
-type GroupNodeWorkflowData = {
- external: ComfyLink[]
- links: ComfyLink[]
- nodes: ComfyNode[]
+type GroupNodeLink = SerialisedLLinkArray
+type LinksFromMap = Record>
+type LinksToMap = Record>
+type ExternalFromMap = Record>
+
+interface GroupNodeInput {
+ name?: string
+ type?: string
+ label?: string
+ widget?: { name: string }
+}
+
+interface GroupNodeOutput {
+ name?: string
+ type?: string
+ label?: string
+ widget?: { name: string }
+ links?: number[]
+}
+
+interface GroupNodeData extends Omit<
+ GroupNodeWorkflowData['nodes'][number],
+ 'inputs' | 'outputs'
+> {
+ title?: string
+ widgets_values?: unknown[]
+ inputs?: GroupNodeInput[]
+ outputs?: GroupNodeOutput[]
+}
+
+interface GroupNodeDef {
+ input: {
+ required: Record
+ optional?: Record
+ }
+ output: unknown[]
+ output_name: string[]
+ output_is_list: boolean[]
+}
+
+interface NodeConfigEntry {
+ input?: Record
+ output?: Record
+}
+
+interface SerializedGroupConfig {
+ nodes: unknown[]
+ links: GroupNodeLink[]
+ external?: (number | string)[][]
}
const Workflow = {
@@ -42,11 +89,9 @@ const Workflow = {
Registered: 1,
InWorkflow: 2
},
- // @ts-expect-error fixme ts strict error
- isInUseGroupNode(name) {
+ isInUseGroupNode(name: string) {
const id = `${PREFIX}${SEPARATOR}${name}`
// Check if lready registered/in use in this workflow
- // @ts-expect-error fixme ts strict error
if (app.rootGraph.extra?.groupNodes?.[name]) {
if (app.rootGraph.nodes.find((n) => n.type === id)) {
return Workflow.InUse.InWorkflow
@@ -61,15 +106,13 @@ const Workflow = {
if (!extra) app.rootGraph.extra = extra = {}
let groupNodes = extra.groupNodes
if (!groupNodes) extra.groupNodes = groupNodes = {}
- // @ts-expect-error fixme ts strict error
groupNodes[name] = data
}
}
class GroupNodeBuilder {
nodes: LGraphNode[]
- // @ts-expect-error fixme ts strict error
- nodeData: GroupNodeWorkflowData
+ nodeData!: GroupNodeWorkflowData
constructor(nodes: LGraphNode[]) {
this.nodes = nodes
@@ -121,25 +164,25 @@ class GroupNodeBuilder {
const nodesInOrder = app.rootGraph.computeExecutionOrder(false)
this.nodes = this.nodes
.map((node) => ({ index: nodesInOrder.indexOf(node), node }))
- // @ts-expect-error id might be string
- .sort((a, b) => a.index - b.index || a.node.id - b.node.id)
+ .sort(
+ (a, b) =>
+ a.index - b.index ||
+ String(a.node.id).localeCompare(String(b.node.id))
+ )
.map(({ node }) => node)
}
- getNodeData() {
- // @ts-expect-error fixme ts strict error
- const storeLinkTypes = (config) => {
+ getNodeData(): GroupNodeWorkflowData {
+ const storeLinkTypes = (config: SerializedGroupConfig) => {
// Store link types for dynamically typed nodes e.g. reroutes
for (const link of config.links) {
- const origin = app.rootGraph.getNodeById(link[4])
- // @ts-expect-error fixme ts strict error
- const type = origin.outputs[link[1]].type
- link.push(type)
+ const origin = app.rootGraph.getNodeById(link[4] as NodeId)
+ const type = origin?.outputs?.[Number(link[1])]?.type
+ if (type !== undefined) link.push(type)
}
}
- // @ts-expect-error fixme ts strict error
- const storeExternalLinks = (config) => {
+ const storeExternalLinks = (config: SerializedGroupConfig) => {
// Store any external links to the group in the config so when rebuilding we add extra slots
config.external = []
for (let i = 0; i < this.nodes.length; i++) {
@@ -161,53 +204,50 @@ class GroupNodeBuilder {
}
}
if (hasExternal) {
- config.external.push([i, slot, type])
+ config.external.push([i, slot, String(type)])
}
}
}
}
// Use the built in copyToClipboard function to generate the node data we need
- try {
- // @ts-expect-error fixme ts strict error
- const serialised = serialise(this.nodes, app.canvas?.graph)
- const config = JSON.parse(serialised)
+ const graph = app.canvas?.graph
+ if (!graph) return { nodes: [], links: [], external: [] }
+ const serialised = serialise(this.nodes, graph)
+ const config = JSON.parse(serialised) as SerializedGroupConfig
+ config.external = []
- storeLinkTypes(config)
- storeExternalLinks(config)
+ storeLinkTypes(config)
+ storeExternalLinks(config)
- return config
- } finally {
- }
+ return config as GroupNodeWorkflowData
}
}
export class GroupNodeConfig {
name: string
- nodeData: any
+ nodeData: GroupNodeWorkflowData
inputCount: number
- oldToNewOutputMap: {}
- newToOldOutputMap: {}
- oldToNewInputMap: {}
- oldToNewWidgetMap: {}
- newToOldWidgetMap: {}
- primitiveDefs: {}
- widgetToPrimitive: {}
- primitiveToWidget: {}
- nodeInputs: {}
- outputVisibility: any[]
+ oldToNewOutputMap: Record>
+ newToOldOutputMap: Record
+ oldToNewInputMap: Record>
+ oldToNewWidgetMap: Record>
+ newToOldWidgetMap: Record
+ primitiveDefs: Record
+ widgetToPrimitive: Record>
+ primitiveToWidget: Record<
+ number,
+ { nodeId: number | string | null; inputName: string }[]
+ >
+ nodeInputs: Record>
+ outputVisibility: boolean[]
nodeDef: (ComfyNodeDef & { [GROUP]: GroupNodeConfig }) | undefined
- // @ts-expect-error fixme ts strict error
- inputs: any[]
- // @ts-expect-error fixme ts strict error
- linksFrom: {}
- // @ts-expect-error fixme ts strict error
- linksTo: {}
- // @ts-expect-error fixme ts strict error
- externalFrom: {}
+ inputs!: unknown[]
+ linksFrom!: LinksFromMap
+ linksTo!: LinksToMap
+ externalFrom!: ExternalFromMap
- // @ts-expect-error fixme ts strict error
- constructor(name, nodeData) {
+ constructor(name: string, nodeData: GroupNodeWorkflowData) {
this.name = name
this.nodeData = nodeData
this.getLinks()
@@ -236,7 +276,6 @@ export class GroupNodeConfig {
category: 'group nodes' + (SEPARATOR + source),
input: { required: {} },
description: `Group node combining ${this.nodeData.nodes
- // @ts-expect-error fixme ts strict error
.map((n) => n.type)
.join(', ')}`,
python_module: 'custom_nodes.' + this.name,
@@ -248,17 +287,15 @@ export class GroupNodeConfig {
const seenInputs = {}
const seenOutputs = {}
for (let i = 0; i < this.nodeData.nodes.length; i++) {
- const node = this.nodeData.nodes[i]
+ const node = this.nodeData.nodes[i] as GroupNodeData
node.index = i
this.processNode(node, seenInputs, seenOutputs)
}
for (const p of this.#convertedToProcess) {
- // @ts-expect-error fixme ts strict error
p()
}
- // @ts-expect-error fixme ts strict error
- this.#convertedToProcess = null
+ this.#convertedToProcess = []
if (!this.nodeDef) return
await app.registerNodeDef(`${PREFIX}${SEPARATOR}` + this.name, this.nodeDef)
useNodeDefStore().addNodeDef(this.nodeDef)
@@ -270,50 +307,57 @@ export class GroupNodeConfig {
this.externalFrom = {}
// Extract links for easy lookup
- for (const l of this.nodeData.links) {
- const [sourceNodeId, sourceNodeSlot, targetNodeId, targetNodeSlot] = l
+ for (const link of this.nodeData.links) {
+ const [sourceNodeId, sourceNodeSlot, targetNodeId, targetNodeSlot] = link
// Skip links outside the copy config
- if (sourceNodeId == null) continue
+ if (
+ sourceNodeId == null ||
+ sourceNodeSlot == null ||
+ targetNodeId == null ||
+ targetNodeSlot == null
+ )
+ continue
- // @ts-expect-error fixme ts strict error
- if (!this.linksFrom[sourceNodeId]) {
- // @ts-expect-error fixme ts strict error
- this.linksFrom[sourceNodeId] = {}
- }
- // @ts-expect-error fixme ts strict error
- if (!this.linksFrom[sourceNodeId][sourceNodeSlot]) {
- // @ts-expect-error fixme ts strict error
- this.linksFrom[sourceNodeId][sourceNodeSlot] = []
- }
- // @ts-expect-error fixme ts strict error
- this.linksFrom[sourceNodeId][sourceNodeSlot].push(l)
+ const srcId = Number(sourceNodeId)
+ const srcSlot = Number(sourceNodeSlot)
+ const tgtId = Number(targetNodeId)
+ const tgtSlot = Number(targetNodeSlot)
- // @ts-expect-error fixme ts strict error
- if (!this.linksTo[targetNodeId]) {
- // @ts-expect-error fixme ts strict error
- this.linksTo[targetNodeId] = {}
+ if (!this.linksFrom[srcId]) {
+ this.linksFrom[srcId] = {}
}
- // @ts-expect-error fixme ts strict error
- this.linksTo[targetNodeId][targetNodeSlot] = l
+ if (!this.linksFrom[srcId][srcSlot]) {
+ this.linksFrom[srcId][srcSlot] = []
+ }
+ this.linksFrom[srcId][srcSlot].push(link)
+
+ if (!this.linksTo[tgtId]) {
+ this.linksTo[tgtId] = {}
+ }
+ this.linksTo[tgtId][tgtSlot] = link
}
if (this.nodeData.external) {
for (const ext of this.nodeData.external) {
- // @ts-expect-error fixme ts strict error
- if (!this.externalFrom[ext[0]]) {
- // @ts-expect-error fixme ts strict error
- this.externalFrom[ext[0]] = { [ext[1]]: ext[2] }
+ const nodeIdx = Number(ext[0])
+ const slotIdx = Number(ext[1])
+ const typeVal = ext[2]
+ if (typeVal == null) continue
+ if (!this.externalFrom[nodeIdx]) {
+ this.externalFrom[nodeIdx] = { [slotIdx]: typeVal }
} else {
- // @ts-expect-error fixme ts strict error
- this.externalFrom[ext[0]][ext[1]] = ext[2]
+ this.externalFrom[nodeIdx][slotIdx] = typeVal
}
}
}
}
- // @ts-expect-error fixme ts strict error
- processNode(node, seenInputs, seenOutputs) {
+ processNode(
+ node: GroupNodeData,
+ seenInputs: Record,
+ seenOutputs: Record
+ ) {
const def = this.getNodeDef(node)
if (!def) return
@@ -323,32 +367,44 @@ export class GroupNodeConfig {
if (def.output?.length) this.processNodeOutputs(node, seenOutputs, def)
}
- // @ts-expect-error fixme ts strict error
- getNodeDef(node) {
- // @ts-expect-error fixme ts strict error
- const def = globalDefs[node.type]
- if (def) return def
+ getNodeDef(
+ node: GroupNodeData
+ ): GroupNodeDef | ComfyNodeDef | null | undefined {
+ if (node.type) {
+ const def = globalDefs[node.type]
+ if (def) return def
+ }
- // @ts-expect-error fixme ts strict error
- const linksFrom = this.linksFrom[node.index]
+ const nodeIndex = node.index
+ if (nodeIndex == null) return undefined
+
+ const linksFrom = this.linksFrom[nodeIndex]
if (node.type === 'PrimitiveNode') {
// Skip as its not linked
if (!linksFrom) return
- let type = linksFrom['0'][0][5]
+ let type: string | number | null = linksFrom[0]?.[0]?.[5] ?? null
if (type === 'COMBO') {
// Use the array items
- const source = node.outputs[0].widget.name
- const fromTypeName = this.nodeData.nodes[linksFrom['0'][0][2]].type
- // @ts-expect-error fixme ts strict error
- const fromType = globalDefs[fromTypeName]
- const input =
- fromType.input.required[source] ?? fromType.input.optional[source]
- type = input[0]
+ const source = node.outputs?.[0]?.widget?.name
+ const nodeIdx = linksFrom[0]?.[0]?.[2]
+ if (source && nodeIdx != null) {
+ const fromTypeName = this.nodeData.nodes[Number(nodeIdx)]?.type
+ if (fromTypeName) {
+ const fromType = globalDefs[fromTypeName]
+ const input =
+ fromType?.input?.required?.[source] ??
+ fromType?.input?.optional?.[source]
+ const inputType = input?.[0]
+ type =
+ typeof inputType === 'string' || typeof inputType === 'number'
+ ? inputType
+ : null
+ }
+ }
}
- // @ts-expect-error fixme ts strict error
- const def = (this.primitiveDefs[node.index] = {
+ const def = (this.primitiveDefs[nodeIndex] = {
input: {
required: {
value: [type, {}]
@@ -360,66 +416,85 @@ export class GroupNodeConfig {
})
return def
} else if (node.type === 'Reroute') {
- // @ts-expect-error fixme ts strict error
- const linksTo = this.linksTo[node.index]
- // @ts-expect-error fixme ts strict error
- if (linksTo && linksFrom && !this.externalFrom[node.index]?.[0]) {
+ const linksTo = this.linksTo[nodeIndex]
+ if (linksTo && linksFrom && !this.externalFrom[nodeIndex]?.[0]) {
// Being used internally
return null
}
- let config = {}
+ let config: Record = {}
let rerouteType = '*'
if (linksFrom) {
- for (const [, , id, slot] of linksFrom['0']) {
- const node = this.nodeData.nodes[id]
- const input = node.inputs[slot]
- if (rerouteType === '*') {
+ const links = linksFrom[0] ?? []
+ for (const link of links) {
+ const id = link[2]
+ const slot = link[3]
+ if (id == null || slot == null) continue
+ const targetNode = this.nodeData.nodes[Number(id)]
+ const input = targetNode?.inputs?.[Number(slot)] as
+ | GroupNodeInput
+ | undefined
+ if (input?.type && rerouteType === '*') {
rerouteType = input.type
}
- if (input.widget) {
- // @ts-expect-error fixme ts strict error
- const targetDef = globalDefs[node.type]
+ if (input?.widget && targetNode?.type) {
+ const targetDef = globalDefs[targetNode.type]
const targetWidget =
- targetDef.input.required[input.widget.name] ??
- targetDef.input.optional[input.widget.name]
+ targetDef?.input?.required?.[input.widget.name] ??
+ targetDef?.input?.optional?.[input.widget.name]
- const widget = [targetWidget[0], config]
- const res = mergeIfValid(
- {
- // @ts-expect-error fixme ts strict error
- widget
- },
- targetWidget,
- false,
- null,
- widget
- )
- config = res?.customConfig ?? config
+ if (targetWidget) {
+ const widgetSpec = [targetWidget[0], config] as Parameters<
+ typeof mergeIfValid
+ >[4]
+ const res = mergeIfValid(
+ { widget: widgetSpec } as unknown as Parameters<
+ typeof mergeIfValid
+ >[0],
+ targetWidget,
+ false,
+ undefined,
+ widgetSpec
+ )
+ config = (res?.customConfig as Record) ?? config
+ }
}
}
} else if (linksTo) {
- const [id, slot] = linksTo['0']
- rerouteType = this.nodeData.nodes[id].outputs[slot].type
+ const link = linksTo[0]
+ if (link) {
+ const id = link[0]
+ const slot = link[1]
+ if (id != null && slot != null) {
+ const outputType =
+ this.nodeData.nodes[Number(id)]?.outputs?.[Number(slot)]
+ if (
+ outputType &&
+ typeof outputType === 'object' &&
+ 'type' in outputType
+ ) {
+ rerouteType = String((outputType as GroupNodeOutput).type ?? '*')
+ }
+ }
+ }
} else {
// Reroute used as a pipe
for (const l of this.nodeData.links) {
if (l[2] === node.index) {
- rerouteType = l[5]
+ const linkType = l[5]
+ if (linkType != null) rerouteType = String(linkType)
break
}
}
if (rerouteType === '*') {
// Check for an external link
- // @ts-expect-error fixme ts strict error
- const t = this.externalFrom[node.index]?.[0]
+ const t = this.externalFrom[nodeIndex]?.[0]
if (t) {
- rerouteType = t
+ rerouteType = String(t)
}
}
}
- // @ts-expect-error
config.forceInput = true
return {
input: {
@@ -441,12 +516,19 @@ export class GroupNodeConfig {
)
}
- // @ts-expect-error fixme ts strict error
- getInputConfig(node, inputName, seenInputs, config, extra?) {
- const customConfig = this.nodeData.config?.[node.index]?.input?.[inputName]
+ getInputConfig(
+ node: GroupNodeData,
+ inputName: string,
+ seenInputs: Record,
+ config: unknown[],
+ extra?: Record
+ ) {
+ const nodeConfig = this.nodeData.config?.[node.index ?? -1] as
+ | NodeConfigEntry
+ | undefined
+ const customConfig = nodeConfig?.input?.[inputName]
let name =
customConfig?.name ??
- // @ts-expect-error fixme ts strict error
node.inputs?.find((inp) => inp.name === inputName)?.label ??
inputName
let key = name
@@ -467,36 +549,55 @@ export class GroupNodeConfig {
}
if (config[0] === 'IMAGEUPLOAD') {
if (!extra) extra = {}
- extra.widget =
- // @ts-expect-error fixme ts strict error
- this.oldToNewWidgetMap[node.index]?.[config[1]?.widget ?? 'image'] ??
- 'image'
+ const nodeIndex = node.index ?? -1
+ const configOptions =
+ typeof config[1] === 'object' && config[1] !== null ? config[1] : {}
+ const widgetKey =
+ 'widget' in configOptions && typeof configOptions.widget === 'string'
+ ? configOptions.widget
+ : 'image'
+ extra.widget = this.oldToNewWidgetMap[nodeIndex]?.[widgetKey] ?? 'image'
}
if (extra) {
- config = [config[0], { ...config[1], ...extra }]
+ const configObj =
+ typeof config[1] === 'object' && config[1] ? config[1] : {}
+ config = [config[0], { ...configObj, ...extra }]
}
return { name, config, customConfig }
}
- // @ts-expect-error fixme ts strict error
- processWidgetInputs(inputs, node, inputNames, seenInputs) {
- const slots = []
- const converted = new Map()
- // @ts-expect-error fixme ts strict error
- const widgetMap = (this.oldToNewWidgetMap[node.index] = {})
+ processWidgetInputs(
+ inputs: Record,
+ node: GroupNodeData,
+ inputNames: string[],
+ seenInputs: Record
+ ) {
+ const slots: string[] = []
+ const converted = new Map()
+ const nodeIndex = node.index ?? -1
+ const widgetMap: Record = (this.oldToNewWidgetMap[
+ nodeIndex
+ ] = {})
for (const inputName of inputNames) {
- if (useWidgetStore().inputIsWidget(inputs[inputName])) {
- const convertedIndex = node.inputs?.findIndex(
- // @ts-expect-error fixme ts strict error
- (inp) => inp.name === inputName && inp.widget?.name === inputName
- )
+ const inputSpec = inputs[inputName]
+ const isValidSpec =
+ Array.isArray(inputSpec) &&
+ inputSpec.length >= 1 &&
+ typeof inputSpec[0] === 'string'
+ if (
+ isValidSpec &&
+ useWidgetStore().inputIsWidget(inputSpec as InputSpec)
+ ) {
+ const convertedIndex =
+ node.inputs?.findIndex(
+ (inp) => inp.name === inputName && inp.widget?.name === inputName
+ ) ?? -1
if (convertedIndex > -1) {
// This widget has been converted to a widget
// We need to store this in the correct position so link ids line up
converted.set(convertedIndex, inputName)
- // @ts-expect-error fixme ts strict error
widgetMap[inputName] = null
} else {
// Normal widget
@@ -504,13 +605,13 @@ export class GroupNodeConfig {
node,
inputName,
seenInputs,
- inputs[inputName]
+ inputs[inputName] as unknown[]
)
- // @ts-expect-error fixme ts strict error
- this.nodeDef.input.required[name] = config
- // @ts-expect-error fixme ts strict error
+ if (this.nodeDef?.input?.required) {
+ // @ts-expect-error legacy dynamic input assignment
+ this.nodeDef.input.required[name] = config
+ }
widgetMap[inputName] = name
- // @ts-expect-error fixme ts strict error
this.newToOldWidgetMap[name] = { node, inputName }
}
} else {
@@ -521,61 +622,78 @@ export class GroupNodeConfig {
return { converted, slots }
}
- // @ts-expect-error fixme ts strict error
- checkPrimitiveConnection(link, inputName, inputs) {
- const sourceNode = this.nodeData.nodes[link[0]]
- if (sourceNode.type === 'PrimitiveNode') {
+ checkPrimitiveConnection(
+ link: GroupNodeLink,
+ inputName: string,
+ inputs: Record
+ ) {
+ const linkSourceIdx = link[0]
+ if (linkSourceIdx == null) return
+ const sourceNode = this.nodeData.nodes[Number(linkSourceIdx)]
+ if (sourceNode?.type === 'PrimitiveNode') {
// Merge link configurations
- const [sourceNodeId, _, targetNodeId, __] = link
- // @ts-expect-error fixme ts strict error
+ const sourceNodeId = Number(link[0])
+ const targetNodeId = Number(link[2])
const primitiveDef = this.primitiveDefs[sourceNodeId]
+ if (!primitiveDef) return
const targetWidget = inputs[inputName]
- const primitiveConfig = primitiveDef.input.required.value
+ const primitiveConfig = primitiveDef.input.required.value as [
+ unknown,
+ Record
+ ]
const output = { widget: primitiveConfig }
const config = mergeIfValid(
- // @ts-expect-error invalid slot type
+ // @ts-expect-error slot type mismatch - legacy API
output,
targetWidget,
false,
- null,
+ undefined,
primitiveConfig
)
+ const inputConfig = inputs[inputName]?.[1]
primitiveConfig[1] =
- (config?.customConfig ?? inputs[inputName][1])
- ? { ...inputs[inputName][1] }
+ (config?.customConfig ?? inputConfig)
+ ? { ...(typeof inputConfig === 'object' ? inputConfig : {}) }
: {}
- // @ts-expect-error fixme ts strict error
- let name = this.oldToNewWidgetMap[sourceNodeId]['value']
- name = name.substr(0, name.length - 6)
- primitiveConfig[1].control_after_generate = true
- primitiveConfig[1].control_prefix = name
+ const widgetName = this.oldToNewWidgetMap[sourceNodeId]?.['value']
+ if (widgetName) {
+ const name = widgetName.substring(0, widgetName.length - 6)
+ primitiveConfig[1].control_after_generate = true
+ primitiveConfig[1].control_prefix = name
+ }
- // @ts-expect-error fixme ts strict error
let toPrimitive = this.widgetToPrimitive[targetNodeId]
if (!toPrimitive) {
- // @ts-expect-error fixme ts strict error
toPrimitive = this.widgetToPrimitive[targetNodeId] = {}
}
- if (toPrimitive[inputName]) {
- toPrimitive[inputName].push(sourceNodeId)
+ const existing = toPrimitive[inputName]
+ if (Array.isArray(existing)) {
+ existing.push(sourceNodeId)
+ } else if (typeof existing === 'number') {
+ toPrimitive[inputName] = [existing, sourceNodeId]
+ } else {
+ toPrimitive[inputName] = sourceNodeId
}
- toPrimitive[inputName] = sourceNodeId
- // @ts-expect-error fixme ts strict error
let toWidget = this.primitiveToWidget[sourceNodeId]
if (!toWidget) {
- // @ts-expect-error fixme ts strict error
toWidget = this.primitiveToWidget[sourceNodeId] = []
}
toWidget.push({ nodeId: targetNodeId, inputName })
}
}
- // @ts-expect-error fixme ts strict error
- processInputSlots(inputs, node, slots, linksTo, inputMap, seenInputs) {
- // @ts-expect-error fixme ts strict error
- this.nodeInputs[node.index] = {}
+ processInputSlots(
+ inputs: Record,
+ node: GroupNodeData,
+ slots: string[],
+ linksTo: Record,
+ inputMap: Record,
+ seenInputs: Record
+ ) {
+ const nodeIdx = node.index ?? -1
+ this.nodeInputs[nodeIdx] = {}
for (let i = 0; i < slots.length; i++) {
const inputName = slots[i]
if (linksTo[i]) {
@@ -591,31 +709,25 @@ export class GroupNodeConfig {
inputs[inputName]
)
- // @ts-expect-error fixme ts strict error
- this.nodeInputs[node.index][inputName] = name
+ this.nodeInputs[nodeIdx][inputName] = name
if (customConfig?.visible === false) continue
- // @ts-expect-error fixme ts strict error
- this.nodeDef.input.required[name] = config
+ if (this.nodeDef?.input?.required) {
+ // @ts-expect-error legacy dynamic input assignment
+ this.nodeDef.input.required[name] = config
+ }
inputMap[i] = this.inputCount++
}
}
processConvertedWidgets(
- // @ts-expect-error fixme ts strict error
- inputs,
- // @ts-expect-error fixme ts strict error
- node,
- // @ts-expect-error fixme ts strict error
- slots,
- // @ts-expect-error fixme ts strict error
- converted,
- // @ts-expect-error fixme ts strict error
- linksTo,
- // @ts-expect-error fixme ts strict error
- inputMap,
- // @ts-expect-error fixme ts strict error
- seenInputs
+ inputs: Record,
+ node: GroupNodeData,
+ slots: string[],
+ converted: Map,
+ linksTo: Record,
+ inputMap: Record,
+ seenInputs: Record
) {
// Add converted widgets sorted into their index order (ordered as they were converted) so link ids match up
const convertedSlots = [...converted.keys()]
@@ -623,11 +735,12 @@ export class GroupNodeConfig {
.map((k) => converted.get(k))
for (let i = 0; i < convertedSlots.length; i++) {
const inputName = convertedSlots[i]
+ if (!inputName) continue
if (linksTo[slots.length + i]) {
this.checkPrimitiveConnection(
linksTo[slots.length + i],
inputName,
- inputs
+ inputs as Record
)
// This input is linked so we can skip it
continue
@@ -637,34 +750,35 @@ export class GroupNodeConfig {
node,
inputName,
seenInputs,
- inputs[inputName],
+ inputs[inputName] as unknown[],
{
defaultInput: true
}
)
- // @ts-expect-error fixme ts strict error
- this.nodeDef.input.required[name] = config
- // @ts-expect-error fixme ts strict error
+ if (this.nodeDef?.input?.required) {
+ // @ts-expect-error legacy dynamic input assignment
+ this.nodeDef.input.required[name] = config
+ }
this.newToOldWidgetMap[name] = { node, inputName }
- // @ts-expect-error fixme ts strict error
- if (!this.oldToNewWidgetMap[node.index]) {
- // @ts-expect-error fixme ts strict error
- this.oldToNewWidgetMap[node.index] = {}
+ const nodeIndex = node.index ?? -1
+ if (!this.oldToNewWidgetMap[nodeIndex]) {
+ this.oldToNewWidgetMap[nodeIndex] = {}
}
- // @ts-expect-error fixme ts strict error
- this.oldToNewWidgetMap[node.index][inputName] = name
+ this.oldToNewWidgetMap[nodeIndex][inputName] = name
inputMap[slots.length + i] = this.inputCount++
}
}
- #convertedToProcess = []
- // @ts-expect-error fixme ts strict error
- processNodeInputs(node, seenInputs, inputs) {
- // @ts-expect-error fixme ts strict error
- const inputMapping = []
+ #convertedToProcess: (() => void)[] = []
+ processNodeInputs(
+ node: GroupNodeData,
+ seenInputs: Record,
+ inputs: Record
+ ) {
+ const inputMapping: unknown[] = []
const inputNames = Object.keys(inputs)
if (!inputNames.length) return
@@ -675,14 +789,20 @@ export class GroupNodeConfig {
inputNames,
seenInputs
)
- // @ts-expect-error fixme ts strict error
- const linksTo = this.linksTo[node.index] ?? {}
- // @ts-expect-error fixme ts strict error
- const inputMap = (this.oldToNewInputMap[node.index] = {})
- this.processInputSlots(inputs, node, slots, linksTo, inputMap, seenInputs)
+ const nodeIndex = node.index ?? -1
+ const linksTo = this.linksTo[nodeIndex] ?? {}
+ const inputMap: Record = (this.oldToNewInputMap[nodeIndex] =
+ {})
+ this.processInputSlots(
+ inputs as unknown as Record,
+ node,
+ slots,
+ linksTo,
+ inputMap,
+ seenInputs
+ )
// Converted inputs have to be processed after all other nodes as they'll be at the end of the list
- // @ts-expect-error fixme ts strict error
this.#convertedToProcess.push(() =>
this.processConvertedWidgets(
inputs,
@@ -695,79 +815,91 @@ export class GroupNodeConfig {
)
)
- // @ts-expect-error fixme ts strict error
return inputMapping
}
- // @ts-expect-error fixme ts strict error
- processNodeOutputs(node, seenOutputs, def) {
- // @ts-expect-error fixme ts strict error
- const oldToNew = (this.oldToNewOutputMap[node.index] = {})
+ processNodeOutputs(
+ node: GroupNodeData,
+ seenOutputs: Record,
+ def: GroupNodeDef | ComfyNodeDef
+ ) {
+ const nodeIndex = node.index ?? -1
+ const oldToNew: Record = (this.oldToNewOutputMap[
+ nodeIndex
+ ] = {})
+ const defOutput = def.output ?? []
// Add outputs
- for (let outputId = 0; outputId < def.output.length; outputId++) {
- // @ts-expect-error fixme ts strict error
- const linksFrom = this.linksFrom[node.index]
+ for (let outputId = 0; outputId < defOutput.length; outputId++) {
+ const linksFrom = this.linksFrom[nodeIndex]
// If this output is linked internally we flag it to hide
const hasLink =
- // @ts-expect-error fixme ts strict error
- linksFrom?.[outputId] && !this.externalFrom[node.index]?.[outputId]
- const customConfig =
- this.nodeData.config?.[node.index]?.output?.[outputId]
+ linksFrom?.[outputId] && !this.externalFrom[nodeIndex]?.[outputId]
+ const outputConfig = this.nodeData.config?.[node.index ?? -1] as
+ | NodeConfigEntry
+ | undefined
+ const customConfig = outputConfig?.output?.[outputId]
const visible = customConfig?.visible ?? !hasLink
this.outputVisibility.push(visible)
if (!visible) {
continue
}
- // @ts-expect-error fixme ts strict error
- oldToNew[outputId] = this.nodeDef.output.length
- // @ts-expect-error fixme ts strict error
- this.newToOldOutputMap[this.nodeDef.output.length] = {
- node,
- slot: outputId
+ if (this.nodeDef?.output) {
+ oldToNew[outputId] = this.nodeDef.output.length
+ this.newToOldOutputMap[this.nodeDef.output.length] = {
+ node,
+ slot: outputId
+ }
+ // @ts-expect-error legacy dynamic output type assignment
+ this.nodeDef.output.push(defOutput[outputId])
+ this.nodeDef.output_is_list?.push(
+ def.output_is_list?.[outputId] ?? false
+ )
}
- // @ts-expect-error fixme ts strict error
- this.nodeDef.output.push(def.output[outputId])
- // @ts-expect-error fixme ts strict error
- this.nodeDef.output_is_list.push(def.output_is_list[outputId])
- let label = customConfig?.name
+ let label: string | undefined = customConfig?.name
if (!label) {
- label = def.output_name?.[outputId] ?? def.output[outputId]
- // @ts-expect-error fixme ts strict error
- const output = node.outputs.find((o) => o.name === label)
+ const outputVal = defOutput[outputId]
+ label =
+ def.output_name?.[outputId] ??
+ (typeof outputVal === 'string' ? outputVal : undefined)
+ const output = node.outputs?.find((o) => o.name === label)
if (output?.label) {
label = output.label
}
}
- let name = label
+ let name: string = String(label ?? `output_${outputId}`)
if (name in seenOutputs) {
const prefix = `${node.title ?? node.type} `
- name = `${prefix}${label}`
+ name = `${prefix}${label ?? outputId}`
if (name in seenOutputs) {
- name = `${prefix}${node.index} ${label}`
+ name = `${prefix}${node.index} ${label ?? outputId}`
}
}
seenOutputs[name] = 1
- // @ts-expect-error fixme ts strict error
- this.nodeDef.output_name.push(name)
+ this.nodeDef?.output_name?.push(name)
}
}
- // @ts-expect-error fixme ts strict error
- static async registerFromWorkflow(groupNodes, missingNodeTypes) {
+ static async registerFromWorkflow(
+ groupNodes: Record,
+ missingNodeTypes: (
+ | string
+ | { type: string; hint?: string; action?: unknown }
+ )[]
+ ) {
for (const g in groupNodes) {
const groupData = groupNodes[g]
let hasMissing = false
for (const n of groupData.nodes) {
// Find missing node types
- if (!(n.type in LiteGraph.registered_node_types)) {
+ if (!n.type || !(n.type in LiteGraph.registered_node_types)) {
missingNodeTypes.push({
- type: n.type,
+ type: n.type ?? 'unknown',
hint: ` (In group node '${PREFIX}${SEPARATOR}${g}')`
})
@@ -775,12 +907,12 @@ export class GroupNodeConfig {
type: `${PREFIX}${SEPARATOR}` + g,
action: {
text: 'Remove from workflow',
- // @ts-expect-error fixme ts strict error
- callback: (e) => {
+ callback: (e: MouseEvent) => {
delete groupNodes[g]
- e.target.textContent = 'Removed'
- e.target.style.pointerEvents = 'none'
- e.target.style.opacity = 0.7
+ const target = e.target as HTMLElement
+ target.textContent = 'Removed'
+ target.style.pointerEvents = 'none'
+ target.style.opacity = '0.7'
}
}
})
@@ -799,12 +931,12 @@ export class GroupNodeConfig {
export class GroupNodeHandler {
node: LGraphNode
- groupData: any
- innerNodes: any
+ groupData: GroupNodeConfig
+ innerNodes: LGraphNode[] | null = null
constructor(node: LGraphNode) {
this.node = node
- this.groupData = node.constructor?.nodeData?.[GROUP]
+ this.groupData = node.constructor?.nodeData?.[GROUP] as GroupNodeConfig
this.node.setInnerNodes = (innerNodes) => {
this.innerNodes = innerNodes
@@ -819,60 +951,63 @@ export class GroupNodeHandler {
for (const w of innerNode.widgets ?? []) {
if (w.type === 'converted-widget') {
+ // @ts-expect-error legacy widget property for converted widgets
w.serializeValue = w.origSerializeValue
}
}
innerNode.index = innerNodeIndex
- // @ts-expect-error fixme ts strict error
- innerNode.getInputNode = (slot) => {
+ innerNode.getInputNode = (slot: number) => {
// Check if this input is internal or external
- const externalSlot =
- this.groupData.oldToNewInputMap[innerNode.index]?.[slot]
+ const nodeIdx = innerNode.index ?? 0
+ const externalSlot = this.groupData.oldToNewInputMap[nodeIdx]?.[slot]
if (externalSlot != null) {
return this.node.getInputNode(externalSlot)
}
// Internal link
- const innerLink = this.groupData.linksTo[innerNode.index]?.[slot]
+ const innerLink = this.groupData.linksTo[nodeIdx]?.[slot]
if (!innerLink) return null
- const inputNode = innerNodes[innerLink[0]]
+ const linkSrcIdx = innerLink[0]
+ if (linkSrcIdx == null) return null
+ const inputNode = innerNodes[Number(linkSrcIdx)]
// Primitives will already apply their values
if (inputNode.type === 'PrimitiveNode') return null
return inputNode
}
- // @ts-expect-error fixme ts strict error
- innerNode.getInputLink = (slot) => {
- const externalSlot =
- this.groupData.oldToNewInputMap[innerNode.index]?.[slot]
+ // @ts-expect-error returns partial link object, not full LLink
+ innerNode.getInputLink = (slot: number) => {
+ const nodeIdx = innerNode.index ?? 0
+ const externalSlot = this.groupData.oldToNewInputMap[nodeIdx]?.[slot]
if (externalSlot != null) {
// The inner node is connected via the group node inputs
const linkId = this.node.inputs[externalSlot].link
- // @ts-expect-error fixme ts strict error
- let link = app.rootGraph.links[linkId]
+ if (linkId == null) return null
+ const existingLink = app.rootGraph.links[linkId]
+ if (!existingLink) return null
// Use the outer link, but update the target to the inner node
- link = {
- ...link,
+ return {
+ ...existingLink,
target_id: innerNode.id,
target_slot: +slot
}
- return link
}
- let link = this.groupData.linksTo[innerNode.index]?.[slot]
- if (!link) return null
+ const innerLink = this.groupData.linksTo[nodeIdx]?.[slot]
+ if (!innerLink) return null
+ const linkSrcIdx = innerLink[0]
+ if (linkSrcIdx == null) return null
// Use the inner link, but update the origin node to be inner node id
- link = {
- origin_id: innerNodes[link[0]].id,
- origin_slot: link[1],
+ return {
+ origin_id: innerNodes[Number(linkSrcIdx)].id,
+ origin_slot: innerLink[1],
target_id: innerNode.id,
target_slot: +slot
}
- return link
}
}
}
@@ -882,7 +1017,9 @@ export class GroupNodeHandler {
// @ts-expect-error Can this be removed? Or replaced with: LLink.create(link.asSerialisable())
link = { ...link }
const output = this.groupData.newToOldOutputMap[link.origin_slot]
- let innerNode = this.innerNodes[output.node.index]
+ if (!output || !this.innerNodes) return null
+ const nodeIdx = output.node.index ?? 0
+ let innerNode: LGraphNode | null = this.innerNodes[nodeIdx]
let l
while (innerNode?.type === 'Reroute') {
l = innerNode.getInputLink(0)
@@ -893,7 +1030,11 @@ export class GroupNodeHandler {
return null
}
- if (l && GroupNodeHandler.isGroupNode(innerNode)) {
+ if (
+ l &&
+ GroupNodeHandler.isGroupNode(innerNode) &&
+ innerNode.updateLink
+ ) {
return innerNode.updateLink(l)
}
@@ -917,20 +1058,19 @@ export class GroupNodeHandler {
visited.add(this.node)
if (!this.innerNodes) {
- // @ts-expect-error fixme ts strict error
- this.node.setInnerNodes(
- // @ts-expect-error fixme ts strict error
- this.groupData.nodeData.nodes.map((n, i) => {
+ const createdNodes = this.groupData.nodeData.nodes
+ .map((n, i) => {
+ if (!n.type) return null
const innerNode = LiteGraph.createNode(n.type)
- // @ts-expect-error fixme ts strict error
+ if (!innerNode) return null
+ // @ts-expect-error legacy node data format used for configure
innerNode.configure(n)
- // @ts-expect-error fixme ts strict error
innerNode.id = `${this.node.id}:${i}`
- // @ts-expect-error fixme ts strict error
innerNode.graph = this.node.graph
return innerNode
})
- )
+ .filter((n): n is LGraphNode => n !== null)
+ this.node.setInnerNodes?.(createdNodes)
}
this.updateInnerWidgets()
@@ -942,11 +1082,12 @@ export class GroupNodeHandler {
subgraphNodePath.at(-1)
) ?? undefined) as SubgraphNode | undefined
- for (const node of this.innerNodes) {
+ for (const node of this.innerNodes ?? []) {
node.graph ??= this.node.graph
// Create minimal DTOs rather than cloning the node
const currentId = String(node.id)
+ // @ts-expect-error temporary id reassignment for DTO creation
node.id = currentId.split(':').at(-1)
const aVeryRealNode = new ExecutableGroupNodeChildDTO(
node,
@@ -962,95 +1103,87 @@ export class GroupNodeHandler {
return nodes
}
- // @ts-expect-error fixme ts strict error
+ // @ts-expect-error recreate returns null if creation fails
this.node.recreate = async () => {
const id = this.node.id
const sz = this.node.size
- // @ts-expect-error fixme ts strict error
- const nodes = this.node.convertToNodes()
+ const nodes = (
+ this.node as LGraphNode & { convertToNodes?: () => LGraphNode[] }
+ ).convertToNodes?.()
+ if (!nodes) return null
const groupNode = LiteGraph.createNode(this.node.type)
- // @ts-expect-error fixme ts strict error
+ if (!groupNode) return null
groupNode.id = id
// Reuse the existing nodes for this instance
- // @ts-expect-error fixme ts strict error
- groupNode.setInnerNodes(nodes)
- // @ts-expect-error fixme ts strict error
- groupNode[GROUP].populateWidgets()
- // @ts-expect-error fixme ts strict error
+ groupNode.setInnerNodes?.(nodes)
+ const handler = GroupNodeHandler.getHandler(groupNode)
+ handler?.populateWidgets()
app.rootGraph.add(groupNode)
- // @ts-expect-error fixme ts strict error
- groupNode.setSize([
- // @ts-expect-error fixme ts strict error
+ groupNode.setSize?.([
Math.max(groupNode.size[0], sz[0]),
- // @ts-expect-error fixme ts strict error
Math.max(groupNode.size[1], sz[1])
])
// Remove all converted nodes and relink them
const builder = new GroupNodeBuilder(nodes)
const nodeData = builder.getNodeData()
- // @ts-expect-error fixme ts strict error
- groupNode[GROUP].groupData.nodeData.links = nodeData.links
- // @ts-expect-error fixme ts strict error
- groupNode[GROUP].replaceNodes(nodes)
+ if (handler) {
+ handler.groupData.nodeData.links = nodeData.links
+ handler.replaceNodes(nodes)
+ }
return groupNode
}
-
- // @ts-expect-error fixme ts strict error
- this.node.convertToNodes = () => {
+ ;(
+ this.node as LGraphNode & { convertToNodes: () => LGraphNode[] }
+ ).convertToNodes = () => {
const addInnerNodes = () => {
// Clone the node data so we dont mutate it for other nodes
const c = { ...this.groupData.nodeData }
c.nodes = [...c.nodes]
- // @ts-expect-error fixme ts strict error
- const innerNodes = this.node.getInnerNodes()
- let ids = []
+ // @ts-expect-error getInnerNodes called without args in legacy conversion code
+ const innerNodes = this.node.getInnerNodes?.()
+ const ids: (string | number)[] = []
for (let i = 0; i < c.nodes.length; i++) {
- let id = innerNodes?.[i]?.id
+ let id: string | number | undefined = innerNodes?.[i]?.id
// Use existing IDs if they are set on the inner nodes
- // @ts-expect-error id can be string or number
- if (id == null || isNaN(id)) {
- // @ts-expect-error fixme ts strict error
+ if (id == null || (typeof id === 'number' && isNaN(id))) {
id = undefined
} else {
ids.push(id)
}
+ // @ts-expect-error adding id to node copy for serialization
c.nodes[i] = { ...c.nodes[i], id }
}
deserialiseAndCreate(JSON.stringify(c), app.canvas)
const [x, y] = this.node.pos
- let top
- let left
+ let top: number | undefined
+ let left: number | undefined
// Configure nodes with current widget data
const selectedIds = ids.length
? ids
: Object.keys(app.canvas.selected_nodes)
- const newNodes = []
+ const newNodes: LGraphNode[] = []
for (let i = 0; i < selectedIds.length; i++) {
const id = selectedIds[i]
const newNode = app.rootGraph.getNodeById(id)
- const innerNode = innerNodes[i]
+ const innerNode = innerNodes?.[i]
+ if (!newNode) continue
newNodes.push(newNode)
- // @ts-expect-error fixme ts strict error
if (left == null || newNode.pos[0] < left) {
- // @ts-expect-error fixme ts strict error
left = newNode.pos[0]
}
- // @ts-expect-error fixme ts strict error
if (top == null || newNode.pos[1] < top) {
- // @ts-expect-error fixme ts strict error
top = newNode.pos[1]
}
- // @ts-expect-error fixme ts strict error
- if (!newNode.widgets) continue
+ if (!newNode.widgets || !innerNode) continue
- // @ts-expect-error fixme ts strict error
- const map = this.groupData.oldToNewWidgetMap[innerNode.index]
+ // @ts-expect-error index property access on ExecutableLGraphNode
+ const map = this.groupData.oldToNewWidgetMap[innerNode.index ?? 0]
if (map) {
const widgets = Object.keys(map)
@@ -1058,37 +1191,32 @@ export class GroupNodeHandler {
const newName = map[oldName]
if (!newName) continue
- // @ts-expect-error fixme ts strict error
- const widgetIndex = this.node.widgets.findIndex(
- (w) => w.name === newName
- )
+ const widgetIndex =
+ this.node.widgets?.findIndex((w) => w.name === newName) ?? -1
if (widgetIndex === -1) continue
// Populate the main and any linked widgets
if (innerNode.type === 'PrimitiveNode') {
- // @ts-expect-error fixme ts strict error
for (let i = 0; i < newNode.widgets.length; i++) {
- // @ts-expect-error fixme ts strict error
- newNode.widgets[i].value =
- // @ts-expect-error fixme ts strict error
- this.node.widgets[widgetIndex + i].value
+ const srcWidget = this.node.widgets?.[widgetIndex + i]
+ if (srcWidget) {
+ newNode.widgets[i].value = srcWidget.value
+ }
}
} else {
- // @ts-expect-error fixme ts strict error
- const outerWidget = this.node.widgets[widgetIndex]
- // @ts-expect-error fixme ts strict error
+ const outerWidget = this.node.widgets?.[widgetIndex]
const newWidget = newNode.widgets.find(
(w) => w.name === oldName
)
- if (!newWidget) continue
+ if (!newWidget || !outerWidget) continue
newWidget.value = outerWidget.value
- // @ts-expect-error fixme ts strict error
- for (let w = 0; w < outerWidget.linkedWidgets?.length; w++) {
- // @ts-expect-error fixme ts strict error
- newWidget.linkedWidgets[w].value =
- // @ts-expect-error fixme ts strict error
- outerWidget.linkedWidgets[w].value
+ const linkedWidgets = outerWidget.linkedWidgets ?? []
+ for (let w = 0; w < linkedWidgets.length; w++) {
+ const newLinked = newWidget.linkedWidgets?.[w]
+ if (newLinked && linkedWidgets[w]) {
+ newLinked.value = linkedWidgets[w].value
+ }
}
}
}
@@ -1097,23 +1225,21 @@ export class GroupNodeHandler {
// Shift each node
for (const newNode of newNodes) {
- // @ts-expect-error fixme ts strict error
- newNode.pos[0] -= left - x
- // @ts-expect-error fixme ts strict error
- newNode.pos[1] -= top - y
+ newNode.pos[0] -= (left ?? 0) - x
+ newNode.pos[1] -= (top ?? 0) - y
}
return { newNodes, selectedIds }
}
- // @ts-expect-error fixme ts strict error
- const reconnectInputs = (selectedIds) => {
+ const reconnectInputs = (selectedIds: (string | number)[]) => {
for (const innerNodeIndex in this.groupData.oldToNewInputMap) {
- const id = selectedIds[innerNodeIndex]
+ const id = selectedIds[Number(innerNodeIndex)]
const newNode = app.rootGraph.getNodeById(id)
- const map = this.groupData.oldToNewInputMap[innerNodeIndex]
+ if (!newNode) continue
+ const map = this.groupData.oldToNewInputMap[Number(innerNodeIndex)]
for (const innerInputId in map) {
- const groupSlotId = map[innerInputId]
+ const groupSlotId = map[Number(innerInputId)]
if (groupSlotId == null) continue
const slot = node.inputs[groupSlotId]
if (slot.link == null) continue
@@ -1121,14 +1247,12 @@ export class GroupNodeHandler {
if (!link) continue
// connect this node output to the input of another node
const originNode = app.rootGraph.getNodeById(link.origin_id)
- // @ts-expect-error fixme ts strict error
- originNode.connect(link.origin_slot, newNode, +innerInputId)
+ originNode?.connect(link.origin_slot, newNode, +innerInputId)
}
}
}
- // @ts-expect-error fixme ts strict error
- const reconnectOutputs = (selectedIds) => {
+ const reconnectOutputs = (selectedIds: (string | number)[]) => {
for (
let groupOutputId = 0;
groupOutputId < node.outputs?.length;
@@ -1139,13 +1263,16 @@ export class GroupNodeHandler {
const links = [...output.links]
for (const l of links) {
const slot = this.groupData.newToOldOutputMap[groupOutputId]
+ if (!slot) continue
const link = app.rootGraph.links[l]
+ if (!link) continue
const targetNode = app.rootGraph.getNodeById(link.target_id)
const newNode = app.rootGraph.getNodeById(
- selectedIds[slot.node.index]
+ selectedIds[slot.node.index ?? 0]
)
- // @ts-expect-error fixme ts strict error
- newNode.connect(slot.slot, targetNode, link.target_slot)
+ if (targetNode) {
+ newNode?.connect(slot.slot, targetNode, link.target_slot)
+ }
}
}
}
@@ -1165,10 +1292,9 @@ export class GroupNodeHandler {
}
const getExtraMenuOptions = this.node.getExtraMenuOptions
- // @ts-expect-error Should pass patched return value getExtraMenuOptions
- this.node.getExtraMenuOptions = function (_, options) {
- // @ts-expect-error fixme ts strict error
- getExtraMenuOptions?.apply(this, arguments)
+ const handlerNode = this.node
+ this.node.getExtraMenuOptions = function (_canvas, options) {
+ getExtraMenuOptions?.call(this, _canvas, options)
let optionIndex = options.findIndex((o) => o?.content === 'Outputs')
if (optionIndex === -1) optionIndex = options.length
@@ -1179,10 +1305,14 @@ export class GroupNodeHandler {
null,
{
content: 'Convert to nodes',
- // @ts-expect-error
- callback: () => {
- // @ts-expect-error fixme ts strict error
- return this.convertToNodes()
+ // @ts-expect-error async callback not expected by legacy menu API
+ callback: async () => {
+ const convertFn = (
+ handlerNode as LGraphNode & {
+ convertToNodes?: () => LGraphNode[]
+ }
+ ).convertToNodes
+ return convertFn?.()
}
},
{
@@ -1190,13 +1320,15 @@ export class GroupNodeHandler {
callback: () => manageGroupNodes(this.type)
}
)
+ // Return empty array to satisfy type signature without triggering
+ // LGraphCanvas concatenation (which only happens when length > 0)
+ return []
}
// Draw custom collapse icon to identity this as a group
const onDrawTitleBox = this.node.onDrawTitleBox
- this.node.onDrawTitleBox = function (ctx, height) {
- // @ts-expect-error fixme ts strict error
- onDrawTitleBox?.apply(this, arguments)
+ this.node.onDrawTitleBox = function (ctx, height, size, scale) {
+ onDrawTitleBox?.call(this, ctx, height, size, scale)
const fill = ctx.fillStyle
ctx.beginPath()
@@ -1218,17 +1350,19 @@ export class GroupNodeHandler {
// Draw progress label
const onDrawForeground = node.onDrawForeground
const groupData = this.groupData.nodeData
- node.onDrawForeground = function (ctx) {
- // @ts-expect-error fixme ts strict error
- onDrawForeground?.apply?.(this, arguments)
+ node.onDrawForeground = function (ctx, canvas, canvasElement) {
+ onDrawForeground?.call(this, ctx, canvas, canvasElement)
const progressState = useExecutionStore().nodeProgressStates[this.id]
if (
progressState &&
progressState.state === 'running' &&
this.runningInternalNodeId !== null
) {
- // @ts-expect-error fixme ts strict error
- const n = groupData.nodes[this.runningInternalNodeId]
+ const nodeIdx =
+ typeof this.runningInternalNodeId === 'number'
+ ? this.runningInternalNodeId
+ : parseInt(String(this.runningInternalNodeId), 10)
+ const n = groupData.nodes[nodeIdx] as { title?: string; type?: string }
if (!n) return
const message = `Running ${n.title || n.type} (${this.runningInternalNodeId}/${groupData.nodes.length})`
ctx.save()
@@ -1254,26 +1388,28 @@ export class GroupNodeHandler {
// Flag this node as needing to be reset
const onExecutionStart = this.node.onExecutionStart
this.node.onExecutionStart = function () {
- // @ts-expect-error fixme ts strict error
- this.resetExecution = true
- // @ts-expect-error fixme ts strict error
- return onExecutionStart?.apply(this, arguments)
+ ;(this as LGraphNode & { resetExecution?: boolean }).resetExecution = true
+ return onExecutionStart?.call(this)
}
- const self = this
const onNodeCreated = this.node.onNodeCreated
+ const handlerGroupData = this.groupData
this.node.onNodeCreated = function () {
if (!this.widgets) {
return
}
- const config = self.groupData.nodeData.config
+ const config = handlerGroupData.nodeData.config as
+ | Record
+ | undefined
if (config) {
for (const n in config) {
const inputs = config[n]?.input
+ if (!inputs) continue
for (const w in inputs) {
- if (inputs[w].visible !== false) continue
- const widgetName = self.groupData.oldToNewWidgetMap[n][w]
- const widget = this.widgets.find((w) => w.name === widgetName)
+ if (inputs[w]?.visible !== false) continue
+ const widgetName =
+ handlerGroupData.oldToNewWidgetMap[Number(n)]?.[w]
+ const widget = this.widgets.find((wg) => wg.name === widgetName)
if (widget) {
widget.type = 'hidden'
widget.computeSize = () => [0, -4]
@@ -1282,91 +1418,88 @@ export class GroupNodeHandler {
}
}
- // @ts-expect-error fixme ts strict error
- return onNodeCreated?.apply(this, arguments)
+ return onNodeCreated?.call(this)
}
- // @ts-expect-error fixme ts strict error
- function handleEvent(type, getId, getEvent) {
- // @ts-expect-error fixme ts strict error
- const handler = ({ detail }) => {
+ type EventDetail = { display_node?: string; node?: string } | string
+ const handleEvent = (
+ type: string,
+ getId: (detail: EventDetail) => string | undefined,
+ getEvent: (
+ detail: EventDetail,
+ id: string,
+ node: LGraphNode
+ ) => EventDetail
+ ) => {
+ const handler = ({ detail }: CustomEvent) => {
const id = getId(detail)
if (!id) return
- const node = app.rootGraph.getNodeById(id)
- if (node) return
+ const existingNode = app.rootGraph.getNodeById(id)
+ if (existingNode) return
- // @ts-expect-error fixme ts strict error
- const innerNodeIndex = this.innerNodes?.findIndex((n) => n.id == id)
+ const innerNodeIndex =
+ this.innerNodes?.findIndex((n) => n.id == id) ?? -1
if (innerNodeIndex > -1) {
- // @ts-expect-error fixme ts strict error
- this.node.runningInternalNodeId = innerNodeIndex
+ ;(
+ this.node as LGraphNode & { runningInternalNodeId?: number }
+ ).runningInternalNodeId = innerNodeIndex
api.dispatchCustomEvent(
- type,
- // @ts-expect-error fixme ts strict error
- getEvent(detail, `${this.node.id}`, this.node)
+ type as 'executing',
+ getEvent(detail, `${this.node.id}`, this.node) as string
)
}
}
- api.addEventListener(type, handler)
+ api.addEventListener(
+ type as 'executing' | 'executed',
+ handler as EventListener
+ )
return handler
}
- const executing = handleEvent.call(
- this,
+ const executing = handleEvent(
'executing',
- // @ts-expect-error fixme ts strict error
- (d) => d,
- // @ts-expect-error fixme ts strict error
- (_, id) => id
+ (d) => (typeof d === 'string' ? d : undefined),
+ (_d, id) => id
)
- const executed = handleEvent.call(
- this,
+ const executed = handleEvent(
'executed',
- // @ts-expect-error fixme ts strict error
- (d) => d?.display_node || d?.node,
- // @ts-expect-error fixme ts strict error
+ (d) => (typeof d === 'object' ? d?.display_node || d?.node : undefined),
(d, id, node) => ({
- ...d,
+ ...(typeof d === 'object' ? d : {}),
node: id,
display_node: id,
- merge: !node.resetExecution
+ merge: !(node as LGraphNode & { resetExecution?: boolean })
+ .resetExecution
})
)
const onRemoved = node.onRemoved
this.node.onRemoved = function () {
- // @ts-expect-error fixme ts strict error
- onRemoved?.apply(this, arguments)
- // api.removeEventListener('progress_state', progress_state)
- api.removeEventListener('executing', executing)
- api.removeEventListener('executed', executed)
+ onRemoved?.call(this)
+ api.removeEventListener('executing', executing as EventListener)
+ api.removeEventListener('executed', executed as EventListener)
}
this.node.refreshComboInNode = (defs) => {
// Update combo widget options
for (const widgetName in this.groupData.newToOldWidgetMap) {
- // @ts-expect-error fixme ts strict error
- const widget = this.node.widgets.find((w) => w.name === widgetName)
+ const widget = this.node.widgets?.find((w) => w.name === widgetName)
if (widget?.type === 'combo') {
const old = this.groupData.newToOldWidgetMap[widgetName]
+ if (!old.node.type) continue
const def = defs[old.node.type]
const input =
def?.input?.required?.[old.inputName] ??
def?.input?.optional?.[old.inputName]
if (!input) continue
- widget.options.values = input[0]
+ widget.options.values = input[0] as unknown[]
- if (
- old.inputName !== 'image' &&
- // @ts-expect-error Widget values
- !widget.options.values.includes(widget.value)
- ) {
- // @ts-expect-error fixme ts strict error
- widget.value = widget.options.values[0]
- // @ts-expect-error fixme ts strict error
- widget.callback(widget.value)
+ const values = widget.options.values as unknown[]
+ if (old.inputName !== 'image' && !values.includes(widget.value)) {
+ widget.value = values[0] as typeof widget.value
+ widget.callback?.(widget.value)
}
}
}
@@ -1374,22 +1507,30 @@ export class GroupNodeHandler {
}
updateInnerWidgets() {
+ if (!this.innerNodes) return
for (const newWidgetName in this.groupData.newToOldWidgetMap) {
- // @ts-expect-error fixme ts strict error
- const newWidget = this.node.widgets.find((w) => w.name === newWidgetName)
+ const newWidget = this.node.widgets?.find((w) => w.name === newWidgetName)
if (!newWidget) continue
const newValue = newWidget.value
const old = this.groupData.newToOldWidgetMap[newWidgetName]
- let innerNode = this.innerNodes[old.node.index]
+ const nodeIdx = old.node.index ?? 0
+ const innerNode = this.innerNodes[nodeIdx]
+ if (!innerNode) continue
if (innerNode.type === 'PrimitiveNode') {
+ // @ts-expect-error primitiveValue is a custom property on PrimitiveNode
innerNode.primitiveValue = newValue
- const primitiveLinked = this.groupData.primitiveToWidget[old.node.index]
+ const primitiveLinked = this.groupData.primitiveToWidget[nodeIdx]
for (const linked of primitiveLinked ?? []) {
- const node = this.innerNodes[linked.nodeId]
- // @ts-expect-error fixme ts strict error
- const widget = node.widgets.find((w) => w.name === linked.inputName)
+ const linkedNodeId =
+ typeof linked.nodeId === 'number'
+ ? linked.nodeId
+ : Number(linked.nodeId)
+ const linkedNode = this.innerNodes[linkedNodeId]
+ const widget = linkedNode?.widgets?.find(
+ (w) => w.name === linked.inputName
+ )
if (widget) {
widget.value = newValue
@@ -1397,15 +1538,17 @@ export class GroupNodeHandler {
}
continue
} else if (innerNode.type === 'Reroute') {
- const rerouteLinks = this.groupData.linksFrom[old.node.index]
+ const rerouteLinks = this.groupData.linksFrom[nodeIdx]
if (rerouteLinks) {
- for (const [_, , targetNodeId, targetSlot] of rerouteLinks['0']) {
- const node = this.innerNodes[targetNodeId]
- const input = node.inputs[targetSlot]
- if (input.widget) {
- const widget = node.widgets?.find(
- // @ts-expect-error fixme ts strict error
- (w) => w.name === input.widget.name
+ for (const [, , targetNodeId, targetSlot] of rerouteLinks[0] ?? []) {
+ if (targetNodeId == null || targetSlot == null) continue
+ const targetNode = this.innerNodes[Number(targetNodeId)]
+ if (!targetNode) continue
+ const input = targetNode.inputs?.[Number(targetSlot)]
+ if (input?.widget) {
+ const widgetName = input.widget.name
+ const widget = targetNode.widgets?.find(
+ (w) => w.name === widgetName
)
if (widget) {
widget.value = newValue
@@ -1415,7 +1558,6 @@ export class GroupNodeHandler {
}
}
- // @ts-expect-error fixme ts strict error
const widget = innerNode.widgets?.find((w) => w.name === old.inputName)
if (widget) {
widget.value = newValue
@@ -1423,57 +1565,73 @@ export class GroupNodeHandler {
}
}
- // @ts-expect-error fixme ts strict error
- populatePrimitive(_node, nodeId, oldName) {
+ populatePrimitive(
+ _node: GroupNodeData,
+ nodeId: number,
+ oldName: string
+ ): boolean {
// Converted widget, populate primitive if linked
const primitiveId = this.groupData.widgetToPrimitive[nodeId]?.[oldName]
- if (primitiveId == null) return
+ if (primitiveId == null) return false
const targetWidgetName =
- this.groupData.oldToNewWidgetMap[primitiveId]['value']
- // @ts-expect-error fixme ts strict error
- const targetWidgetIndex = this.node.widgets.findIndex(
- (w) => w.name === targetWidgetName
- )
- if (targetWidgetIndex > -1) {
- const primitiveNode = this.innerNodes[primitiveId]
+ this.groupData.oldToNewWidgetMap[
+ Array.isArray(primitiveId) ? primitiveId[0] : primitiveId
+ ]?.['value']
+ if (!targetWidgetName) return false
+ const targetWidgetIndex =
+ this.node.widgets?.findIndex((w) => w.name === targetWidgetName) ?? -1
+ if (targetWidgetIndex > -1 && this.innerNodes) {
+ const primIdx = Array.isArray(primitiveId) ? primitiveId[0] : primitiveId
+ const primitiveNode = this.innerNodes[primIdx]
+ if (!primitiveNode?.widgets) return true
let len = primitiveNode.widgets.length
if (
len - 1 !==
- // @ts-expect-error fixme ts strict error
- this.node.widgets[targetWidgetIndex].linkedWidgets?.length
+ (this.node.widgets?.[targetWidgetIndex]?.linkedWidgets?.length ?? 0)
) {
// Fallback handling for if some reason the primitive has a different number of widgets
// we dont want to overwrite random widgets, better to leave blank
len = 1
}
for (let i = 0; i < len; i++) {
- // @ts-expect-error fixme ts strict error
- this.node.widgets[targetWidgetIndex + i].value =
- primitiveNode.widgets[i].value
+ const targetWidget = this.node.widgets?.[targetWidgetIndex + i]
+ const srcWidget = primitiveNode.widgets[i]
+ if (targetWidget && srcWidget) {
+ targetWidget.value = srcWidget.value
+ }
}
}
return true
}
- // @ts-expect-error fixme ts strict error
- populateReroute(node, nodeId, map) {
+ populateReroute(
+ node: GroupNodeData,
+ nodeId: number,
+ map: Record
+ ) {
if (node.type !== 'Reroute') return
const link = this.groupData.linksFrom[nodeId]?.[0]?.[0]
if (!link) return
- const [, , targetNodeId, targetNodeSlot] = link
- const targetNode = this.groupData.nodeData.nodes[targetNodeId]
- const inputs = targetNode.inputs
- const targetWidget = inputs?.[targetNodeSlot]?.widget
+ const targetNodeId = link[2]
+ const targetNodeSlot = link[3]
+ if (targetNodeId == null || targetNodeSlot == null) return
+ const targetNode = this.groupData.nodeData.nodes[Number(targetNodeId)] as
+ | GroupNodeData
+ | undefined
+ const inputs = targetNode?.inputs
+ const targetWidget = (inputs as GroupNodeInput[] | undefined)?.[
+ Number(targetNodeSlot)
+ ]?.widget
if (!targetWidget) return
- const offset = inputs.length - (targetNode.widgets_values?.length ?? 0)
- const v = targetNode.widgets_values?.[targetNodeSlot - offset]
+ const offset =
+ (inputs?.length ?? 0) - (targetNode?.widgets_values?.length ?? 0)
+ const v = targetNode?.widgets_values?.[Number(targetNodeSlot) - offset]
if (v == null) return
const widgetName = Object.values(map)[0]
- // @ts-expect-error fixme ts strict error
- const widget = this.node.widgets.find((w) => w.name === widgetName)
+ const widget = this.node.widgets?.find((w) => w.name === widgetName)
if (widget) {
widget.value = v
}
@@ -1487,7 +1645,7 @@ export class GroupNodeHandler {
nodeId < this.groupData.nodeData.nodes.length;
nodeId++
) {
- const node = this.groupData.nodeData.nodes[nodeId]
+ const node = this.groupData.nodeData.nodes[nodeId] as GroupNodeData
const map = this.groupData.oldToNewWidgetMap[nodeId] ?? {}
const widgets = Object.keys(map)
@@ -1511,8 +1669,7 @@ export class GroupNodeHandler {
widgetIndex === -1
) {
// Find the inner widget and shift by the number of linked widgets as they will have been removed too
- const innerWidget = this.innerNodes[nodeId].widgets?.find(
- // @ts-expect-error fixme ts strict error
+ const innerWidget = this.innerNodes?.[nodeId]?.widgets?.find(
(w) => w.name === oldName
)
linkedShift += innerWidget?.linkedWidgets?.length ?? 0
@@ -1522,20 +1679,22 @@ export class GroupNodeHandler {
}
// Populate the main and any linked widget
- mainWidget.value = node.widgets_values[i + linkedShift]
- // @ts-expect-error fixme ts strict error
- for (let w = 0; w < mainWidget.linkedWidgets?.length; w++) {
- this.node.widgets[widgetIndex + w + 1].value =
- node.widgets_values[i + ++linkedShift]
+ mainWidget.value = node.widgets_values?.[
+ i + linkedShift
+ ] as typeof mainWidget.value
+ const linkedWidgets = mainWidget.linkedWidgets ?? []
+ for (let w = 0; w < linkedWidgets.length; w++) {
+ this.node.widgets[widgetIndex + w + 1].value = node.widgets_values?.[
+ i + ++linkedShift
+ ] as typeof mainWidget.value
}
}
}
}
- // @ts-expect-error fixme ts strict error
- replaceNodes(nodes) {
- let top
- let left
+ replaceNodes(nodes: LGraphNode[]) {
+ let top: number | undefined
+ let left: number | undefined
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i]
@@ -1554,11 +1713,10 @@ export class GroupNodeHandler {
}
this.linkInputs()
- this.node.pos = [left, top]
+ this.node.pos = [left ?? 0, top ?? 0]
}
- // @ts-expect-error fixme ts strict error
- linkOutputs(originalNode, nodeId) {
+ linkOutputs(originalNode: LGraphNode, nodeId: number) {
if (!originalNode.outputs) return
for (const output of originalNode.outputs) {
@@ -1572,8 +1730,7 @@ export class GroupNodeHandler {
const targetNode = app.rootGraph.getNodeById(link.target_id)
const newSlot =
this.groupData.oldToNewOutputMap[nodeId]?.[link.origin_slot]
- if (newSlot != null) {
- // @ts-expect-error fixme ts strict error
+ if (newSlot != null && targetNode) {
this.node.connect(newSlot, targetNode, link.target_slot)
}
}
@@ -1583,20 +1740,58 @@ export class GroupNodeHandler {
linkInputs() {
for (const link of this.groupData.nodeData.links ?? []) {
const [, originSlot, targetId, targetSlot, actualOriginId] = link
+ if (actualOriginId == null || typeof actualOriginId === 'object') continue
const originNode = app.rootGraph.getNodeById(actualOriginId)
if (!originNode) continue // this node is in the group
- originNode.connect(
- originSlot,
- // @ts-expect-error Valid - uses deprecated interface. Required check: if (graph.getNodeById(this.node.id) !== this.node) report()
- this.node.id,
- this.groupData.oldToNewInputMap[targetId][targetSlot]
- )
+ if (targetId == null || targetSlot == null) continue
+ const mappedSlot =
+ this.groupData.oldToNewInputMap[Number(targetId)]?.[Number(targetSlot)]
+ if (mappedSlot == null) continue
+ if (typeof originSlot === 'number' || typeof originSlot === 'string') {
+ originNode.connect(
+ originSlot,
+ // @ts-expect-error Valid - uses deprecated interface (node ID instead of node reference)
+ this.node.id,
+ mappedSlot
+ )
+ }
}
}
- // @ts-expect-error fixme ts strict error
- static getGroupData(node) {
- return (node.nodeData ?? node.constructor?.nodeData)?.[GROUP]
+ static getGroupData(
+ node: LGraphNodeConstructor
+ ): GroupNodeConfig | undefined
+ static getGroupData(node: LGraphNode): GroupNodeConfig | undefined
+ static getGroupData(
+ node: LGraphNode | LGraphNodeConstructor
+ ): GroupNodeConfig | undefined {
+ // Check if this is a constructor (function) or an instance
+ if (typeof node === 'function') {
+ // Constructor case - access nodeData directly
+ const nodeData = (node as LGraphNodeConstructor & { nodeData?: unknown })
+ .nodeData as Record | undefined
+ return nodeData?.[GROUP]
+ }
+ // Instance case - check instance property first, then constructor
+ const instanceData = (node as LGraphNode & { nodeData?: unknown })
+ .nodeData as Record | undefined
+ if (instanceData?.[GROUP]) return instanceData[GROUP]
+ const ctorData = (
+ node.constructor as LGraphNodeConstructor & { nodeData?: unknown }
+ )?.nodeData as Record | undefined
+ return ctorData?.[GROUP]
+ }
+
+ static getHandler(node: LGraphNode): GroupNodeHandler | undefined {
+ // @ts-expect-error GROUP symbol indexing on LGraphNode
+ let handler = node[GROUP] as GroupNodeHandler | undefined
+ // Handler may not be set yet if nodeCreated async hook hasn't run
+ // Create it synchronously if needed
+ if (!handler && GroupNodeHandler.isGroupNode(node)) {
+ handler = new GroupNodeHandler(node)
+ ;(node as LGraphNode & { [GROUP]: GroupNodeHandler })[GROUP] = handler
+ }
+ return handler
}
static isGroupNode(node: LGraphNode) {
@@ -1616,17 +1811,15 @@ export class GroupNodeHandler {
await config.registerType()
const groupNode = LiteGraph.createNode(`${PREFIX}${SEPARATOR}${name}`)
+ if (!groupNode) return
// Reuse the existing nodes for this instance
- // @ts-expect-error fixme ts strict error
- groupNode.setInnerNodes(builder.nodes)
- // @ts-expect-error fixme ts strict error
- groupNode[GROUP].populateWidgets()
- // @ts-expect-error fixme ts strict error
+ groupNode.setInnerNodes?.(builder.nodes)
+ const handler = GroupNodeHandler.getHandler(groupNode)
+ handler?.populateWidgets()
app.rootGraph.add(groupNode)
// Remove all converted nodes and relink them
- // @ts-expect-error fixme ts strict error
- groupNode[GROUP].replaceNodes(builder.nodes)
+ handler?.replaceNodes(builder.nodes)
return groupNode
}
}
@@ -1685,8 +1878,24 @@ function manageGroupNodes(type?: string) {
}
const id = 'Comfy.GroupNode'
-// @ts-expect-error fixme ts strict error
-let globalDefs
+
+/**
+ * Global node definitions cache, populated and mutated by extension callbacks.
+ *
+ * **Initialization**: Set by `addCustomNodeDefs` during extension initialization.
+ * This callback runs early in the app lifecycle, before any code that reads
+ * `globalDefs` is executed.
+ *
+ * **Mutation**: `refreshComboInNodes` merges updated definitions into this object
+ * when combo options are refreshed (e.g., after model files change).
+ *
+ * **Usage Notes**:
+ * - Functions reading `globalDefs` (e.g., `getNodeDef`, `checkPrimitiveConnection`)
+ * must only be called after `addCustomNodeDefs` has run.
+ * - Not thread-safe; assumes single-threaded JS execution model.
+ * - The object reference is stable after initialization; only contents are mutated.
+ */
+let globalDefs: Record
const ext: ComfyExtension = {
name: id,
commands: [
@@ -1739,8 +1948,8 @@ const ext: ComfyExtension = {
items.push({
content: `Convert to Group Node (Deprecated)`,
disabled: !convertEnabled,
- // @ts-expect-error fixme ts strict error - async callback
- callback: () => convertSelectedNodesToGroupNode()
+ // @ts-expect-error async callback - legacy menu API doesn't expect Promise
+ callback: async () => convertSelectedNodesToGroupNode()
})
const groups = canvas.graph?.extra?.groupNodes
@@ -1766,8 +1975,8 @@ const ext: ComfyExtension = {
{
content: `Convert to Group Node (Deprecated)`,
disabled: !convertEnabled,
- // @ts-expect-error fixme ts strict error - async callback
- callback: () => convertSelectedNodesToGroupNode()
+ // @ts-expect-error async callback - legacy menu API doesn't expect Promise
+ callback: async () => convertSelectedNodesToGroupNode()
}
]
},
@@ -1775,7 +1984,9 @@ const ext: ComfyExtension = {
graphData: ComfyWorkflowJSON,
missingNodeTypes: string[]
) {
- const nodes = graphData?.extra?.groupNodes
+ const nodes = graphData?.extra?.groupNodes as
+ | Record
+ | undefined
if (nodes) {
replaceLegacySeparators(graphData.nodes)
await GroupNodeConfig.registerFromWorkflow(nodes, missingNodeTypes)
@@ -1787,25 +1998,24 @@ const ext: ComfyExtension = {
},
nodeCreated(node) {
if (GroupNodeHandler.isGroupNode(node)) {
- // @ts-expect-error fixme ts strict error
- node[GROUP] = new GroupNodeHandler(node)
+ ;(node as LGraphNode & { [GROUP]: GroupNodeHandler })[GROUP] =
+ new GroupNodeHandler(node)
// Ensure group nodes pasted from other workflows are stored
- // @ts-expect-error fixme ts strict error
- if (node.title && node[GROUP]?.groupData?.nodeData) {
- // @ts-expect-error fixme ts strict error
- Workflow.storeGroupNode(node.title, node[GROUP].groupData.nodeData)
+ const handler = GroupNodeHandler.getHandler(node)
+ if (node.title && handler?.groupData?.nodeData) {
+ Workflow.storeGroupNode(node.title, handler.groupData.nodeData)
}
}
},
- // @ts-expect-error fixme ts strict error
- async refreshComboInNodes(defs) {
+ async refreshComboInNodes(defs: Record) {
// Re-register group nodes so new ones are created with the correct options
- // @ts-expect-error fixme ts strict error
Object.assign(globalDefs, defs)
- const nodes = app.rootGraph.extra?.groupNodes
+ const nodes = app.rootGraph.extra?.groupNodes as
+ | Record
+ | undefined
if (nodes) {
- await GroupNodeConfig.registerFromWorkflow(nodes, {})
+ await GroupNodeConfig.registerFromWorkflow(nodes, [])
}
}
}
diff --git a/src/extensions/core/groupNodeManage.ts b/src/extensions/core/groupNodeManage.ts
index 7ea480614..0e91af317 100644
--- a/src/extensions/core/groupNodeManage.ts
+++ b/src/extensions/core/groupNodeManage.ts
@@ -483,23 +483,30 @@ export class ManageGroupDialog extends ComfyDialog {
// Rewrite links
for (const l of type.links) {
+ // @ts-expect-error l[0]/l[2] used as node index
if (l[0] != null) l[0] = type.nodes[l[0]].index
+ // @ts-expect-error l[0]/l[2] used as node index
if (l[2] != null) l[2] = type.nodes[l[2]].index
}
// Rewrite externals
if (type.external) {
for (const ext of type.external) {
- ext[0] = type.nodes[ext[0]]
+ if (ext[0] != null) {
+ // @ts-expect-error ext[0] used as node index
+ ext[0] = type.nodes[ext[0]].index
+ }
}
}
// Rewrite modifications
for (const id of keys) {
+ // @ts-expect-error id used as node index
if (config[id]) {
// @ts-expect-error fixme ts strict error
orderedConfig[type.nodes[id].index] = config[id]
}
+ // @ts-expect-error id used as config key
delete config[id]
}
@@ -529,7 +536,7 @@ export class ManageGroupDialog extends ComfyDialog {
if (nodes) recreateNodes.push(...nodes)
}
- await GroupNodeConfig.registerFromWorkflow(types, {})
+ await GroupNodeConfig.registerFromWorkflow(types, [])
for (const node of recreateNodes) {
node.recreate()
diff --git a/src/extensions/core/nodeTemplates.ts b/src/extensions/core/nodeTemplates.ts
index 457f54c48..040b54526 100644
--- a/src/extensions/core/nodeTemplates.ts
+++ b/src/extensions/core/nodeTemplates.ts
@@ -370,9 +370,10 @@ const ext: ComfyExtension = {
const node = app.canvas.graph?.getNodeById(nodeIds[i])
const nodeData = node?.constructor.nodeData
- let groupData = GroupNodeHandler.getGroupData(node)
- if (groupData) {
- groupData = groupData.nodeData
+ if (!node) continue
+ const groupConfig = GroupNodeHandler.getGroupData(node)
+ if (groupConfig) {
+ const groupData = groupConfig.nodeData
// @ts-expect-error
if (!data.groupNodes) {
// @ts-expect-error
@@ -402,7 +403,10 @@ const ext: ComfyExtension = {
callback: () => {
clipboardAction(async () => {
const data = JSON.parse(t.data)
- await GroupNodeConfig.registerFromWorkflow(data.groupNodes, {})
+ await GroupNodeConfig.registerFromWorkflow(
+ data.groupNodes ?? {},
+ []
+ )
// Check for old clipboard format
if (!data.reroutes) {
diff --git a/src/lib/litegraph/src/LGraph.ts b/src/lib/litegraph/src/LGraph.ts
index 478a2a784..b3ad35c99 100644
--- a/src/lib/litegraph/src/LGraph.ts
+++ b/src/lib/litegraph/src/LGraph.ts
@@ -15,7 +15,7 @@ import { LGraphGroup } from './LGraphGroup'
import { LGraphNode } from './LGraphNode'
import type { NodeId } from './LGraphNode'
import { LLink } from './LLink'
-import type { LinkId } from './LLink'
+import type { LinkId, SerialisedLLinkArray } from './LLink'
import { MapProxyHandler } from './MapProxyHandler'
import { Reroute } from './Reroute'
import type { RerouteId } from './Reroute'
@@ -102,11 +102,24 @@ export interface LGraphConfig {
links_ontop?: any
}
+export interface GroupNodeWorkflowData {
+ external: (number | string)[][]
+ links: SerialisedLLinkArray[]
+ nodes: {
+ index?: number
+ type?: string
+ inputs?: unknown[]
+ outputs?: unknown[]
+ }[]
+ config?: Record
+}
+
export interface LGraphExtra extends Dictionary {
reroutes?: SerialisableReroute[]
linkExtensions?: { id: number; parentId: number | undefined }[]
ds?: DragAndScaleState
workflowRendererVersion?: RendererType
+ groupNodes?: Record
}
export interface BaseLGraph {
diff --git a/src/platform/workflow/validation/schemas/workflowSchema.ts b/src/platform/workflow/validation/schemas/workflowSchema.ts
index 558ef819d..9517cc226 100644
--- a/src/platform/workflow/validation/schemas/workflowSchema.ts
+++ b/src/platform/workflow/validation/schemas/workflowSchema.ts
@@ -452,7 +452,6 @@ const zSubgraphDefinition = zComfyWorkflow1
.passthrough()
export type ModelFile = z.infer
-export type ComfyLink = z.infer
export type ComfyLinkObject = z.infer
export type ComfyNode = z.infer
export type Reroute = z.infer
diff --git a/src/scripts/app.ts b/src/scripts/app.ts
index 0701f694d..edcc64517 100644
--- a/src/scripts/app.ts
+++ b/src/scripts/app.ts
@@ -34,6 +34,7 @@ import {
import type {
ExecutionErrorWsMessage,
NodeError,
+ NodeExecutionOutput,
ResultItem
} from '@/schemas/apiSchema'
import {
@@ -153,10 +154,8 @@ export class ComfyApp {
vueAppReady: boolean
api: ComfyApi
ui: ComfyUI
- // @ts-expect-error fixme ts strict error
- extensionManager: ExtensionManager
- // @ts-expect-error fixme ts strict error
- _nodeOutputs: Record
+ extensionManager!: ExtensionManager
+ private _nodeOutputs!: Record
nodePreviewImages: Record
private rootGraphInternal: LGraph | undefined
@@ -174,8 +173,7 @@ export class ComfyApp {
return this.rootGraphInternal!
}
- // @ts-expect-error fixme ts strict error
- canvas: LGraphCanvas
+ canvas!: LGraphCanvas
dragOverNode: LGraphNode | null = null
readonly canvasElRef = shallowRef()
get canvasEl() {
@@ -187,8 +185,7 @@ export class ComfyApp {
get configuringGraph() {
return this.configuringGraphLevel > 0
}
- // @ts-expect-error fixme ts strict error
- ctx: CanvasRenderingContext2D
+ ctx!: CanvasRenderingContext2D
bodyTop: HTMLElement
bodyLeft: HTMLElement
bodyRight: HTMLElement
@@ -491,18 +488,17 @@ export class ComfyApp {
}
}
}
- if (ComfyApp.clipspace.widgets) {
+ if (ComfyApp.clipspace.widgets && node.widgets) {
ComfyApp.clipspace.widgets.forEach(({ type, name, value }) => {
- // @ts-expect-error fixme ts strict error
- const prop = Object.values(node.widgets).find(
+ const prop = node.widgets?.find(
(obj) => obj.type === type && obj.name === name
)
if (prop && prop.type != 'button') {
+ const valueObj = value as Record | undefined
if (
prop.type != 'image' &&
typeof prop.value == 'string' &&
- // @ts-expect-error Custom widget value
- value.filename
+ valueObj?.filename
) {
const resultItem = value as ResultItem
prop.value =
@@ -752,16 +748,11 @@ export class ComfyApp {
* Set up the app on the page
*/
async setup(canvasEl: HTMLCanvasElement) {
- // @ts-expect-error fixme ts strict error
- this.bodyTop = document.getElementById('comfyui-body-top')
- // @ts-expect-error fixme ts strict error
- this.bodyLeft = document.getElementById('comfyui-body-left')
- // @ts-expect-error fixme ts strict error
- this.bodyRight = document.getElementById('comfyui-body-right')
- // @ts-expect-error fixme ts strict error
- this.bodyBottom = document.getElementById('comfyui-body-bottom')
- // @ts-expect-error fixme ts strict error
- this.canvasContainer = document.getElementById('graph-canvas-container')
+ this.bodyTop = document.getElementById('comfyui-body-top')!
+ this.bodyLeft = document.getElementById('comfyui-body-left')!
+ this.bodyRight = document.getElementById('comfyui-body-right')!
+ this.bodyBottom = document.getElementById('comfyui-body-bottom')!
+ this.canvasContainer = document.getElementById('graph-canvas-container')!
this.canvasElRef.value = canvasEl
@@ -798,8 +789,7 @@ export class ComfyApp {
// Make canvas states reactive so we can observe changes on them.
this.canvas.state = reactive(this.canvas.state)
- // @ts-expect-error fixme ts strict error
- this.ctx = canvasEl.getContext('2d')
+ this.ctx = canvasEl.getContext('2d')!
LiteGraph.alt_drag_do_clone_nodes = true
LiteGraph.macGesturesRequireMac = false
@@ -887,8 +877,7 @@ export class ComfyApp {
const { width, height } = canvas.getBoundingClientRect()
canvas.width = Math.round(width * scale)
canvas.height = Math.round(height * scale)
- // @ts-expect-error fixme ts strict error
- canvas.getContext('2d').scale(scale, scale)
+ canvas.getContext('2d')?.scale(scale, scale)
this.canvas?.draw(true, true)
}
@@ -981,16 +970,15 @@ export class ComfyApp {
}
}
- // @ts-expect-error fixme ts strict error
- loadTemplateData(templateData) {
+ loadTemplateData(templateData: {
+ templates?: { name?: string; data?: string }[]
+ }): void {
if (!templateData?.templates) {
return
}
const old = localStorage.getItem('litegrapheditor_clipboard')
- var maxY, nodeBottom, node
-
for (const template of templateData.templates) {
if (!template?.data) {
continue
@@ -1006,26 +994,24 @@ export class ComfyApp {
}
// Move mouse position down to paste the next template below
-
- maxY = false
+ let maxY: number | undefined
for (const i in app.canvas.selected_nodes) {
- node = app.canvas.selected_nodes[i]
-
- nodeBottom = node.pos[1] + node.size[1]
-
- // @ts-expect-error fixme ts strict error
- if (maxY === false || nodeBottom > maxY) {
+ const node = app.canvas.selected_nodes[i]
+ const nodeBottom = node.pos[1] + node.size[1]
+ if (maxY === undefined || nodeBottom > maxY) {
maxY = nodeBottom
}
}
- // @ts-expect-error fixme ts strict error
- app.canvas.graph_mouse[1] = maxY + 50
+ if (maxY !== undefined) {
+ app.canvas.graph_mouse[1] = maxY + 50
+ }
}
- // @ts-expect-error fixme ts strict error
- localStorage.setItem('litegrapheditor_clipboard', old)
+ if (old !== null) {
+ localStorage.setItem('litegrapheditor_clipboard', old)
+ }
}
private showMissingNodesError(missingNodeTypes: MissingNodeType[]) {
@@ -1034,8 +1020,10 @@ export class ComfyApp {
}
}
- // @ts-expect-error fixme ts strict error
- private showMissingModelsError(missingModels, paths) {
+ private showMissingModelsError(
+ missingModels: ModelFile[],
+ paths: Record
+ ): void {
if (useSettingStore().get('Comfy.Workflow.ShowMissingModelsWarning')) {
useDialogService().showMissingModelsWarning({
missingModels,
@@ -1191,8 +1179,9 @@ export class ComfyApp {
await modelStore.loadModelFolders()
for (const m of uniqueModels) {
const modelFolder = await modelStore.getLoadedModelFolder(m.directory)
- // @ts-expect-error
- if (!modelFolder) m.directory_invalid = true
+ if (!modelFolder)
+ (m as ModelFile & { directory_invalid?: boolean }).directory_invalid =
+ true
const modelsAvailable = modelFolder?.models
const modelExists =
@@ -1288,14 +1277,15 @@ export class ComfyApp {
}
if (reset_invalid_values) {
if (widget.type == 'combo') {
+ const values = widget.options.values as
+ | (string | number | boolean)[]
+ | undefined
if (
- // @ts-expect-error fixme ts strict error
- !widget.options.values.includes(widget.value as string) &&
- // @ts-expect-error fixme ts strict error
- widget.options.values.length > 0
+ values &&
+ values.length > 0 &&
+ !values.includes(widget.value as string | number | boolean)
) {
- // @ts-expect-error fixme ts strict error
- widget.value = widget.options.values[0]
+ widget.value = values[0]
}
}
}
@@ -1455,8 +1445,14 @@ export class ComfyApp {
const { workflow, prompt, parameters, templates } = workflowData
- if (templates) {
- this.loadTemplateData({ templates })
+ if (
+ templates &&
+ typeof templates === 'object' &&
+ Array.isArray(templates)
+ ) {
+ this.loadTemplateData({
+ templates: templates as { name?: string; data?: string }[]
+ })
}
// Check workflow first - it should take priority over parameters
@@ -1505,11 +1501,9 @@ export class ComfyApp {
}
// Use parameters strictly as the final fallback
- if (parameters) {
- // Note: Not putting this in `importA1111` as it is mostly not used
- // by external callers, and `importA1111` has no access to `app`.
+ if (parameters && typeof parameters === 'string') {
useWorkflowService().beforeLoadNewGraph()
- importA1111(this.graph, parameters)
+ importA1111(this.rootGraph, parameters)
useWorkflowService().afterLoadNewGraph(
fileName,
this.rootGraph.serialize() as unknown as ComfyWorkflowJSON
@@ -1560,35 +1554,40 @@ export class ComfyApp {
app.rootGraph.add(node)
}
- //TODO: Investigate repeat of for loop. Can compress?
- for (const id of ids) {
+ const processNodeInputs = (id: string) => {
const data = apiData[id]
const node = app.rootGraph.getNodeById(id)
+ if (!node) return
+
for (const input in data.inputs ?? {}) {
const value = data.inputs[input]
if (value instanceof Array) {
const [fromId, fromSlot] = value
const fromNode = app.rootGraph.getNodeById(fromId)
- // @ts-expect-error fixme ts strict error
- let toSlot = node.inputs?.findIndex((inp) => inp.name === input)
- if (toSlot == null || toSlot === -1) {
+ if (!fromNode) continue
+
+ let toSlot = node.inputs?.findIndex((inp) => inp.name === input) ?? -1
+ if (toSlot === -1) {
try {
- // Target has no matching input, most likely a converted widget
- // @ts-expect-error fixme ts strict error
const widget = node.widgets?.find((w) => w.name === input)
- // @ts-expect-error
- if (widget && node.convertWidgetToInput?.(widget)) {
- // @ts-expect-error fixme ts strict error
- toSlot = node.inputs?.length - 1
+ const convertFn = (
+ node as LGraphNode & {
+ convertWidgetToInput?: (w: IBaseWidget) => boolean
+ }
+ ).convertWidgetToInput
+ if (widget && convertFn?.(widget)) {
+ // Re-find the target slot by name after conversion
+ toSlot =
+ node.inputs?.findIndex((inp) => inp.name === input) ?? -1
}
- } catch (error) {}
+ } catch (_error) {
+ // Ignore conversion errors
+ }
}
- if (toSlot != null || toSlot !== -1) {
- // @ts-expect-error fixme ts strict error
+ if (toSlot !== -1) {
fromNode.connect(fromSlot, node, toSlot)
}
} else {
- // @ts-expect-error fixme ts strict error
const widget = node.widgets?.find((w) => w.name === input)
if (widget) {
widget.value = value
@@ -1597,45 +1596,10 @@ export class ComfyApp {
}
}
}
+
+ for (const id of ids) processNodeInputs(id)
app.rootGraph.arrange()
-
- for (const id of ids) {
- const data = apiData[id]
- const node = app.rootGraph.getNodeById(id)
- for (const input in data.inputs ?? {}) {
- const value = data.inputs[input]
- if (value instanceof Array) {
- const [fromId, fromSlot] = value
- const fromNode = app.rootGraph.getNodeById(fromId)
- // @ts-expect-error fixme ts strict error
- let toSlot = node.inputs?.findIndex((inp) => inp.name === input)
- if (toSlot == null || toSlot === -1) {
- try {
- // Target has no matching input, most likely a converted widget
- // @ts-expect-error fixme ts strict error
- const widget = node.widgets?.find((w) => w.name === input)
- // @ts-expect-error
- if (widget && node.convertWidgetToInput?.(widget)) {
- // @ts-expect-error fixme ts strict error
- toSlot = node.inputs?.length - 1
- }
- } catch (error) {}
- }
- if (toSlot != null || toSlot !== -1) {
- // @ts-expect-error fixme ts strict error
- fromNode.connect(fromSlot, node, toSlot)
- }
- } else {
- // @ts-expect-error fixme ts strict error
- const widget = node.widgets?.find((w) => w.name === input)
- if (widget) {
- widget.value = value
- widget.callback?.(value)
- }
- }
- }
- }
-
+ for (const id of ids) processNodeInputs(id)
app.rootGraph.arrange()
useWorkflowService().afterLoadNewGraph(
diff --git a/src/scripts/pnginfo.ts b/src/scripts/pnginfo.ts
index 97c6a9320..58fe2da0f 100644
--- a/src/scripts/pnginfo.ts
+++ b/src/scripts/pnginfo.ts
@@ -1,4 +1,5 @@
-import { LiteGraph } from '@/lib/litegraph/src/litegraph'
+import { LGraph, LGraphNode, LiteGraph } from '@/lib/litegraph/src/litegraph'
+import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets'
import { api } from './api'
import { getFromAvifFile } from './metadata/avif'
@@ -18,14 +19,16 @@ export function getAvifMetadata(file: File): Promise> {
return getFromAvifFile(file)
}
-// @ts-expect-error fixme ts strict error
-function parseExifData(exifData) {
+function parseExifData(exifData: Uint8Array) {
// Check for the correct TIFF header (0x4949 for little-endian or 0x4D4D for big-endian)
const isLittleEndian = String.fromCharCode(...exifData.slice(0, 2)) === 'II'
// Function to read 16-bit and 32-bit integers from binary data
- // @ts-expect-error fixme ts strict error
- function readInt(offset, isLittleEndian, length) {
+ function readInt(
+ offset: number,
+ isLittleEndian: boolean,
+ length: 2 | 4
+ ): number {
let arr = exifData.slice(offset, offset + length)
if (length === 2) {
return new DataView(arr.buffer, arr.byteOffset, arr.byteLength).getUint16(
@@ -38,17 +41,16 @@ function parseExifData(exifData) {
isLittleEndian
)
}
+ return 0
}
// Read the offset to the first IFD (Image File Directory)
const ifdOffset = readInt(4, isLittleEndian, 4)
- // @ts-expect-error fixme ts strict error
- function parseIFD(offset) {
+ function parseIFD(offset: number): Record {
const numEntries = readInt(offset, isLittleEndian, 2)
- const result = {}
+ const result: Record = {}
- // @ts-expect-error fixme ts strict error
for (let i = 0; i < numEntries; i++) {
const entryOffset = offset + 2 + i * 12
const tag = readInt(entryOffset, isLittleEndian, 2)
@@ -61,12 +63,10 @@ function parseExifData(exifData) {
if (type === 2) {
// ASCII string
value = new TextDecoder('utf-8').decode(
- // @ts-expect-error fixme ts strict error
exifData.subarray(valueOffset, valueOffset + numValues - 1)
)
}
- // @ts-expect-error fixme ts strict error
result[tag] = value
}
@@ -78,13 +78,11 @@ function parseExifData(exifData) {
return ifdData
}
-// @ts-expect-error fixme ts strict error
-export function getWebpMetadata(file) {
+export function getWebpMetadata(file: File) {
return new Promise>((r) => {
const reader = new FileReader()
reader.onload = (event) => {
- // @ts-expect-error fixme ts strict error
- const webp = new Uint8Array(event.target.result as ArrayBuffer)
+ const webp = new Uint8Array(event.target?.result as ArrayBuffer)
const dataView = new DataView(webp.buffer)
// Check that the WEBP signature is present
@@ -99,7 +97,7 @@ export function getWebpMetadata(file) {
// Start searching for chunks after the WEBP signature
let offset = 12
- let txt_chunks = {}
+ const txt_chunks: Record = {}
// Loop through the chunks in the WEBP file
while (offset < webp.length) {
const chunk_length = dataView.getUint32(offset + 4, true)
@@ -116,12 +114,10 @@ export function getWebpMetadata(file) {
let data = parseExifData(
webp.slice(offset + 8, offset + 8 + chunk_length)
)
- for (var key in data) {
- // @ts-expect-error fixme ts strict error
- const value = data[key] as string
+ for (const key in data) {
+ const value = data[Number(key)]
if (typeof value === 'string') {
const index = value.indexOf(':')
- // @ts-expect-error fixme ts strict error
txt_chunks[value.slice(0, index)] = value.slice(index + 1)
}
}
@@ -161,26 +157,42 @@ export function getLatentMetadata(file: File): Promise> {
})
}
-// @ts-expect-error fixme ts strict error
-export async function importA1111(graph, parameters) {
+interface NodeConnection {
+ node: LGraphNode
+ index: number
+}
+
+interface LoraEntry {
+ name: string
+ weight: number
+}
+
+export async function importA1111(
+ graph: LGraph,
+ parameters: string
+): Promise {
const p = parameters.lastIndexOf('\nSteps:')
if (p > -1) {
const embeddings = await api.getEmbeddings()
- const opts = parameters
+ const matchResult = parameters
.substr(p)
.split('\n')[1]
.match(
new RegExp('\\s*([^:]+:\\s*([^"\\{].*?|".*?"|\\{.*?\\}))\\s*(,|$)', 'g')
)
- // @ts-expect-error fixme ts strict error
- .reduce((p, n) => {
+ if (!matchResult) return
+
+ const opts: Record = matchResult.reduce(
+ (acc: Record, n: string) => {
const s = n.split(':')
if (s[1].endsWith(',')) {
s[1] = s[1].substr(0, s[1].length - 1)
}
- p[s[0].trim().toLowerCase()] = s[1].trim()
- return p
- }, {})
+ acc[s[0].trim().toLowerCase()] = s[1].trim()
+ return acc
+ },
+ {}
+ )
const p2 = parameters.lastIndexOf('\nNegative prompt:', p)
if (p2 > -1) {
let positive = parameters.substr(0, p2).trim()
@@ -194,25 +206,45 @@ export async function importA1111(graph, parameters) {
const imageNode = LiteGraph.createNode('EmptyLatentImage')
const vaeNode = LiteGraph.createNode('VAEDecode')
const saveNode = LiteGraph.createNode('SaveImage')
- // @ts-expect-error fixme ts strict error
- let hrSamplerNode = null
- let hrSteps = null
- // @ts-expect-error fixme ts strict error
- const ceil64 = (v) => Math.ceil(v / 64) * 64
-
- // @ts-expect-error fixme ts strict error
- const getWidget = (node, name) => {
- // @ts-expect-error fixme ts strict error
- return node.widgets.find((w) => w.name === name)
+ if (
+ !ckptNode ||
+ !clipSkipNode ||
+ !positiveNode ||
+ !negativeNode ||
+ !samplerNode ||
+ !imageNode ||
+ !vaeNode ||
+ !saveNode
+ ) {
+ console.error('Failed to create required nodes for A1111 import')
+ return
}
- // @ts-expect-error fixme ts strict error
- const setWidgetValue = (node, name, value, isOptionPrefix?) => {
+ let hrSamplerNode: LGraphNode | null = null
+ let hrSteps: string | null = null
+
+ const ceil64 = (v: number) => Math.ceil(v / 64) * 64
+
+ function getWidget(
+ node: LGraphNode | null,
+ name: string
+ ): IBaseWidget | undefined {
+ return node?.widgets?.find((w) => w.name === name)
+ }
+
+ function setWidgetValue(
+ node: LGraphNode | null,
+ name: string,
+ value: string | number,
+ isOptionPrefix?: boolean
+ ): void {
const w = getWidget(node, name)
+ if (!w) return
+
if (isOptionPrefix) {
- // @ts-expect-error fixme ts strict error
- const o = w.options.values.find((w) => w.startsWith(value))
+ const values = w.options.values as string[] | undefined
+ const o = values?.find((v) => v.startsWith(String(value)))
if (o) {
w.value = o
} else {
@@ -224,25 +256,28 @@ export async function importA1111(graph, parameters) {
}
}
- // @ts-expect-error fixme ts strict error
- const createLoraNodes = (clipNode, text, prevClip, prevModel) => {
- // @ts-expect-error fixme ts strict error
- const loras = []
- // @ts-expect-error fixme ts strict error
- text = text.replace(/]+)>/g, function (m, c) {
+ function createLoraNodes(
+ clipNode: LGraphNode,
+ text: string,
+ prevClip: NodeConnection,
+ prevModel: NodeConnection,
+ targetSamplerNode: LGraphNode
+ ): { text: string; prevModel: NodeConnection; prevClip: NodeConnection } {
+ const loras: LoraEntry[] = []
+ text = text.replace(/]+)>/g, (_m, c: string) => {
const s = c.split(':')
const weight = parseFloat(s[1])
if (isNaN(weight)) {
- console.warn('Invalid LORA', m)
+ console.warn('Invalid LORA', _m)
} else {
loras.push({ name: s[0], weight })
}
return ''
})
- // @ts-expect-error fixme ts strict error
for (const l of loras) {
const loraNode = LiteGraph.createNode('LoraLoader')
+ if (!loraNode) continue
graph.add(loraNode)
setWidgetValue(loraNode, 'lora_name', l.name, true)
setWidgetValue(loraNode, 'strength_model', l.weight)
@@ -254,8 +289,7 @@ export async function importA1111(graph, parameters) {
}
prevClip.node.connect(1, clipNode, 0)
- prevModel.node.connect(0, samplerNode, 0)
- // @ts-expect-error fixme ts strict error
+ prevModel.node.connect(0, targetSamplerNode, 0)
if (hrSamplerNode) {
prevModel.node.connect(0, hrSamplerNode, 0)
}
@@ -263,8 +297,7 @@ export async function importA1111(graph, parameters) {
return { text, prevModel, prevClip }
}
- // @ts-expect-error fixme ts strict error
- const replaceEmbeddings = (text) => {
+ function replaceEmbeddings(text: string): string {
if (!embeddings.length) return text
return text.replaceAll(
new RegExp(
@@ -279,8 +312,7 @@ export async function importA1111(graph, parameters) {
)
}
- // @ts-expect-error fixme ts strict error
- const popOpt = (name) => {
+ function popOpt(name: string): string | undefined {
const v = opts[name]
delete opts[name]
return v
@@ -296,43 +328,29 @@ export async function importA1111(graph, parameters) {
graph.add(vaeNode)
graph.add(saveNode)
- // @ts-expect-error fixme ts strict error
ckptNode.connect(1, clipSkipNode, 0)
- // @ts-expect-error fixme ts strict error
clipSkipNode.connect(0, positiveNode, 0)
- // @ts-expect-error fixme ts strict error
clipSkipNode.connect(0, negativeNode, 0)
- // @ts-expect-error fixme ts strict error
ckptNode.connect(0, samplerNode, 0)
- // @ts-expect-error fixme ts strict error
positiveNode.connect(0, samplerNode, 1)
- // @ts-expect-error fixme ts strict error
negativeNode.connect(0, samplerNode, 2)
- // @ts-expect-error fixme ts strict error
imageNode.connect(0, samplerNode, 3)
- // @ts-expect-error fixme ts strict error
vaeNode.connect(0, saveNode, 0)
- // @ts-expect-error fixme ts strict error
samplerNode.connect(0, vaeNode, 0)
- // @ts-expect-error fixme ts strict error
ckptNode.connect(2, vaeNode, 1)
- const handlers = {
- // @ts-expect-error fixme ts strict error
- model(v) {
+ const handlers: Record void> = {
+ model(v: string) {
setWidgetValue(ckptNode, 'ckpt_name', v, true)
},
vae() {},
- // @ts-expect-error fixme ts strict error
- 'cfg scale'(v) {
+ 'cfg scale'(v: string) {
setWidgetValue(samplerNode, 'cfg', +v)
},
- // @ts-expect-error fixme ts strict error
- 'clip skip'(v) {
- setWidgetValue(clipSkipNode, 'stop_at_clip_layer', -v)
+ 'clip skip'(v: string) {
+ setWidgetValue(clipSkipNode, 'stop_at_clip_layer', -Number(v))
},
- // @ts-expect-error fixme ts strict error
- sampler(v) {
+ sampler(v: string) {
let name = v.toLowerCase().replace('++', 'pp').replaceAll(' ', '_')
if (name.includes('karras')) {
name = name.replace('karras', '').replace(/_+$/, '')
@@ -341,45 +359,44 @@ export async function importA1111(graph, parameters) {
setWidgetValue(samplerNode, 'scheduler', 'normal')
}
const w = getWidget(samplerNode, 'sampler_name')
- const o = w.options.values.find(
- // @ts-expect-error fixme ts strict error
- (w) => w === name || w === 'sample_' + name
- )
+ const values = w?.options.values as string[] | undefined
+ const o = values?.find((v) => v === name || v === 'sample_' + name)
if (o) {
setWidgetValue(samplerNode, 'sampler_name', o)
}
},
- // @ts-expect-error fixme ts strict error
- size(v) {
+ size(v: string) {
const wxh = v.split('x')
const w = ceil64(+wxh[0])
const h = ceil64(+wxh[1])
const hrUp = popOpt('hires upscale')
const hrSz = popOpt('hires resize')
- hrSteps = popOpt('hires steps')
+ hrSteps = popOpt('hires steps') ?? null
let hrMethod = popOpt('hires upscaler')
setWidgetValue(imageNode, 'width', w)
setWidgetValue(imageNode, 'height', h)
if (hrUp || hrSz) {
- let uw, uh
+ let uw: number, uh: number
if (hrUp) {
- uw = w * hrUp
- uh = h * hrUp
- } else {
+ uw = w * Number(hrUp)
+ uh = h * Number(hrUp)
+ } else if (hrSz) {
const s = hrSz.split('x')
uw = +s[0]
uh = +s[1]
+ } else {
+ return
}
- let upscaleNode
- let latentNode
+ let upscaleNode: LGraphNode | null
+ let latentNode: LGraphNode | null
- if (hrMethod.startsWith('Latent')) {
+ if (hrMethod?.startsWith('Latent')) {
latentNode = upscaleNode = LiteGraph.createNode('LatentUpscale')
+ if (!upscaleNode) return
graph.add(upscaleNode)
- // @ts-expect-error fixme ts strict error
samplerNode.connect(0, upscaleNode, 0)
switch (hrMethod) {
@@ -390,37 +407,40 @@ export async function importA1111(graph, parameters) {
setWidgetValue(upscaleNode, 'upscale_method', hrMethod, true)
} else {
const decode = LiteGraph.createNode('VAEDecodeTiled')
+ if (!decode) return
graph.add(decode)
- // @ts-expect-error fixme ts strict error
samplerNode.connect(0, decode, 0)
- // @ts-expect-error fixme ts strict error
ckptNode.connect(2, decode, 1)
const upscaleLoaderNode =
LiteGraph.createNode('UpscaleModelLoader')
+ if (!upscaleLoaderNode) return
graph.add(upscaleLoaderNode)
- setWidgetValue(upscaleLoaderNode, 'model_name', hrMethod, true)
+ setWidgetValue(
+ upscaleLoaderNode,
+ 'model_name',
+ hrMethod ?? '',
+ true
+ )
const modelUpscaleNode = LiteGraph.createNode(
'ImageUpscaleWithModel'
)
+ if (!modelUpscaleNode) return
graph.add(modelUpscaleNode)
- // @ts-expect-error fixme ts strict error
decode.connect(0, modelUpscaleNode, 1)
- // @ts-expect-error fixme ts strict error
upscaleLoaderNode.connect(0, modelUpscaleNode, 0)
upscaleNode = LiteGraph.createNode('ImageScale')
+ if (!upscaleNode) return
graph.add(upscaleNode)
- // @ts-expect-error fixme ts strict error
modelUpscaleNode.connect(0, upscaleNode, 0)
- const vaeEncodeNode = (latentNode =
- LiteGraph.createNode('VAEEncodeTiled'))
+ const vaeEncodeNode = LiteGraph.createNode('VAEEncodeTiled')
+ if (!vaeEncodeNode) return
+ latentNode = vaeEncodeNode
graph.add(vaeEncodeNode)
- // @ts-expect-error fixme ts strict error
upscaleNode.connect(0, vaeEncodeNode, 0)
- // @ts-expect-error fixme ts strict error
ckptNode.connect(2, vaeEncodeNode, 1)
}
@@ -428,33 +448,28 @@ export async function importA1111(graph, parameters) {
setWidgetValue(upscaleNode, 'height', ceil64(uh))
hrSamplerNode = LiteGraph.createNode('KSampler')
+ if (!hrSamplerNode || !latentNode) return
graph.add(hrSamplerNode)
- // @ts-expect-error fixme ts strict error
ckptNode.connect(0, hrSamplerNode, 0)
- // @ts-expect-error fixme ts strict error
positiveNode.connect(0, hrSamplerNode, 1)
- // @ts-expect-error fixme ts strict error
negativeNode.connect(0, hrSamplerNode, 2)
- // @ts-expect-error fixme ts strict error
latentNode.connect(0, hrSamplerNode, 3)
- // @ts-expect-error fixme ts strict error
hrSamplerNode.connect(0, vaeNode, 0)
}
},
- // @ts-expect-error fixme ts strict error
- steps(v) {
+ steps(v: string) {
setWidgetValue(samplerNode, 'steps', +v)
},
- // @ts-expect-error fixme ts strict error
- seed(v) {
+ seed(v: string) {
setWidgetValue(samplerNode, 'seed', +v)
}
}
for (const opt in opts) {
- if (opt in handlers) {
- // @ts-expect-error fixme ts strict error
- handlers[opt](popOpt(opt))
+ const handler = handlers[opt]
+ if (handler) {
+ const value = popOpt(opt)
+ if (value !== undefined) handler(value)
}
}
@@ -462,27 +477,29 @@ export async function importA1111(graph, parameters) {
setWidgetValue(
hrSamplerNode,
'steps',
- hrSteps ? +hrSteps : getWidget(samplerNode, 'steps').value
+ hrSteps
+ ? +hrSteps
+ : (getWidget(samplerNode, 'steps')?.value as number)
)
setWidgetValue(
hrSamplerNode,
'cfg',
- getWidget(samplerNode, 'cfg').value
+ getWidget(samplerNode, 'cfg')?.value as number
)
setWidgetValue(
hrSamplerNode,
'scheduler',
- getWidget(samplerNode, 'scheduler').value
+ getWidget(samplerNode, 'scheduler')?.value as string
)
setWidgetValue(
hrSamplerNode,
'sampler_name',
- getWidget(samplerNode, 'sampler_name').value
+ getWidget(samplerNode, 'sampler_name')?.value as string
)
setWidgetValue(
hrSamplerNode,
'denoise',
- +(popOpt('denoising strength') || '1')
+ +(popOpt('denoising strength') ?? '1')
)
}
@@ -490,10 +507,17 @@ export async function importA1111(graph, parameters) {
positiveNode,
positive,
{ node: clipSkipNode, index: 0 },
- { node: ckptNode, index: 0 }
+ { node: ckptNode, index: 0 },
+ samplerNode
)
positive = n.text
- n = createLoraNodes(negativeNode, negative, n.prevClip, n.prevModel)
+ n = createLoraNodes(
+ negativeNode,
+ negative,
+ n.prevClip,
+ n.prevModel,
+ samplerNode
+ )
negative = n.text
setWidgetValue(positiveNode, 'text', replaceEmbeddings(positive))
From 818c5c32e58d9eb953a6f77a8b487da0d93bb67b Mon Sep 17 00:00:00 2001
From: Benjamin Lu
Date: Sat, 10 Jan 2026 21:34:37 -0800
Subject: [PATCH 08/63] [QPOv2] Add stories for list view and general job card
(#7743)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Add stories for the media assets sidebar tab for easier prototyping.
Includes mocks for storybook.
Because some functions in the mocks are only used in the storybook
main.ts resolve, knip flags them as unused because it doesn't check that
path. So knipIgnoreUnusedButUsedByStorybook was added.
Part of the QPO v2 iteration, figma design can be found
[here](https://www.figma.com/design/LVilZgHGk5RwWOkVN6yCEK/Queue-Progress-Modal?node-id=3330-37286&m=dev).
This will be implemented in a series of stacked PRs that can be reviewed
and merged individually.
main <-- #7737, #7743, #7745
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7743-QPOv2-Add-stories-for-list-view-and-general-job-card-2d26d73d365081bca59afa925fb232d7)
by [Unito](https://www.unito.io)
---------
Co-authored-by: GitHub Action
---
.storybook/main.ts | 29 +++-
knip.config.ts | 3 +-
.../tabs/AssetsSidebarListView.stories.ts | 154 ++++++++++++++++++
src/composables/queue/useJobActions.ts | 2 +-
.../components/AssetsListItem.stories.ts | 137 ++++++++++------
src/storybook/mocks/useJobActions.ts | 51 ++++++
src/storybook/mocks/useJobList.ts | 58 +++++++
7 files changed, 379 insertions(+), 55 deletions(-)
create mode 100644 src/components/sidebar/tabs/AssetsSidebarListView.stories.ts
create mode 100644 src/storybook/mocks/useJobActions.ts
create mode 100644 src/storybook/mocks/useJobList.ts
diff --git a/.storybook/main.ts b/.storybook/main.ts
index af23c6db3..5b7c126e9 100644
--- a/.storybook/main.ts
+++ b/.storybook/main.ts
@@ -69,9 +69,32 @@ const config: StorybookConfig = {
allowedHosts: true
},
resolve: {
- alias: {
- '@': process.cwd() + '/src'
- }
+ alias: [
+ {
+ find: '@/composables/queue/useJobList',
+ replacement: process.cwd() + '/src/storybook/mocks/useJobList.ts'
+ },
+ {
+ find: '@/composables/queue/useJobActions',
+ replacement: process.cwd() + '/src/storybook/mocks/useJobActions.ts'
+ },
+ {
+ find: '@/utils/formatUtil',
+ replacement:
+ process.cwd() +
+ '/packages/shared-frontend-utils/src/formatUtil.ts'
+ },
+ {
+ find: '@/utils/networkUtil',
+ replacement:
+ process.cwd() +
+ '/packages/shared-frontend-utils/src/networkUtil.ts'
+ },
+ {
+ find: '@',
+ replacement: process.cwd() + '/src'
+ }
+ ]
},
esbuild: {
// Prevent minification of identifiers to preserve _sfc_main
diff --git a/knip.config.ts b/knip.config.ts
index f2907a266..d6a4a7517 100644
--- a/knip.config.ts
+++ b/knip.config.ts
@@ -8,7 +8,8 @@ const config: KnipConfig = {
'src/assets/css/style.css',
'src/main.ts',
'src/scripts/ui/menu/index.ts',
- 'src/types/index.ts'
+ 'src/types/index.ts',
+ 'src/storybook/mocks/**/*.ts'
],
project: ['**/*.{js,ts,vue}', '*.{js,ts,mts}']
},
diff --git a/src/components/sidebar/tabs/AssetsSidebarListView.stories.ts b/src/components/sidebar/tabs/AssetsSidebarListView.stories.ts
new file mode 100644
index 000000000..2c50cab17
--- /dev/null
+++ b/src/components/sidebar/tabs/AssetsSidebarListView.stories.ts
@@ -0,0 +1,154 @@
+import type { Meta, StoryObj } from '@storybook/vue3-vite'
+
+import type { JobAction } from '@/composables/queue/useJobActions'
+import type { JobListItem } from '@/composables/queue/useJobList'
+import type { AssetItem } from '@/platform/assets/schemas/assetSchema'
+import { setMockJobActions } from '@/storybook/mocks/useJobActions'
+import { setMockJobItems } from '@/storybook/mocks/useJobList'
+import { iconForJobState } from '@/utils/queueDisplay'
+
+import AssetsSidebarListView from './AssetsSidebarListView.vue'
+
+type StoryArgs = {
+ assets: AssetItem[]
+ jobs: JobListItem[]
+ selectedAssetIds?: string[]
+ actionsByJobId?: Record
+}
+
+function baseDecorator() {
+ return {
+ template: `
+
+
+
+ `
+ }
+}
+
+const meta: Meta = {
+ title: 'Components/Sidebar/AssetsSidebarListView',
+ component: AssetsSidebarListView,
+ parameters: {
+ layout: 'centered'
+ },
+ decorators: [baseDecorator]
+}
+
+export default meta
+type Story = StoryObj
+
+const baseTimestamp = '2024-01-15T10:00:00Z'
+
+const sampleJobs: JobListItem[] = [
+ {
+ id: 'job-pending-1',
+ title: 'In queue',
+ meta: '8:59:30pm',
+ state: 'pending',
+ iconName: iconForJobState('pending'),
+ showClear: true
+ },
+ {
+ id: 'job-init-1',
+ title: 'Initializing...',
+ meta: '8:59:35pm',
+ state: 'initialization',
+ iconName: iconForJobState('initialization'),
+ showClear: true
+ },
+ {
+ id: 'job-running-1',
+ title: 'Total: 30%',
+ meta: 'KSampler: 70%',
+ state: 'running',
+ iconName: iconForJobState('running'),
+ showClear: true,
+ progressTotalPercent: 30,
+ progressCurrentPercent: 70
+ }
+]
+
+const sampleAssets: AssetItem[] = [
+ {
+ id: 'asset-image-1',
+ name: 'image-032.png',
+ created_at: baseTimestamp,
+ preview_url: '/assets/images/comfy-logo-single.svg',
+ size: 1887437,
+ tags: [],
+ user_metadata: {
+ promptId: 'job-running-1',
+ nodeId: 12,
+ executionTimeInSeconds: 1.84
+ }
+ },
+ {
+ id: 'asset-video-1',
+ name: 'clip-01.mp4',
+ created_at: baseTimestamp,
+ preview_url: '/assets/images/default-template.png',
+ size: 8394820,
+ tags: [],
+ user_metadata: {
+ duration: 132000
+ }
+ },
+ {
+ id: 'asset-audio-1',
+ name: 'soundtrack-01.mp3',
+ created_at: baseTimestamp,
+ size: 5242880,
+ tags: [],
+ user_metadata: {
+ duration: 200000
+ }
+ },
+ {
+ id: 'asset-3d-1',
+ name: 'scene-01.glb',
+ created_at: baseTimestamp,
+ size: 134217728,
+ tags: []
+ }
+]
+
+const cancelAction: JobAction = {
+ icon: 'icon-[lucide--x]',
+ label: 'Cancel',
+ variant: 'destructive'
+}
+
+export const RunningAndGenerated: Story = {
+ args: {
+ assets: sampleAssets,
+ jobs: sampleJobs,
+ actionsByJobId: {
+ 'job-pending-1': [cancelAction],
+ 'job-init-1': [cancelAction],
+ 'job-running-1': [cancelAction]
+ }
+ },
+ render: renderAssetsSidebarListView
+}
+
+function renderAssetsSidebarListView(args: StoryArgs) {
+ return {
+ components: { AssetsSidebarListView },
+ setup() {
+ setMockJobItems(args.jobs)
+ setMockJobActions(args.actionsByJobId ?? {})
+ const selectedIds = new Set(args.selectedAssetIds ?? [])
+ function isSelected(assetId: string) {
+ return selectedIds.has(assetId)
+ }
+
+ return { args, isSelected }
+ },
+ template: `
+
+ `
+ }
+}
diff --git a/src/composables/queue/useJobActions.ts b/src/composables/queue/useJobActions.ts
index 280a45224..d350503d9 100644
--- a/src/composables/queue/useJobActions.ts
+++ b/src/composables/queue/useJobActions.ts
@@ -7,7 +7,7 @@ import type { JobListItem } from '@/composables/queue/useJobList'
import { useJobMenu } from '@/composables/queue/useJobMenu'
import type { JobState } from '@/types/queue'
-type JobAction = {
+export type JobAction = {
icon: string
label: string
variant: 'destructive' | 'secondary' | 'textonly'
diff --git a/src/platform/assets/components/AssetsListItem.stories.ts b/src/platform/assets/components/AssetsListItem.stories.ts
index 4d54f6477..ebf66fcd7 100644
--- a/src/platform/assets/components/AssetsListItem.stories.ts
+++ b/src/platform/assets/components/AssetsListItem.stories.ts
@@ -2,6 +2,7 @@ import type { Meta, StoryObj } from '@storybook/vue3-vite'
import Button from '@/components/ui/button/Button.vue'
import AssetsListItem from '@/platform/assets/components/AssetsListItem.vue'
+import { iconForJobState } from '@/utils/queueDisplay'
const meta: Meta = {
title: 'Platform/Assets/AssetsListItem',
@@ -18,11 +19,95 @@ const meta: Meta = {
export default meta
type Story = StoryObj
+
+const IMAGE_PREVIEW = '/assets/images/comfy-logo-single.svg'
+const VIDEO_PREVIEW = '/assets/images/default-template.png'
+
+export const PendingJob: Story = {
+ args: {
+ iconName: iconForJobState('pending'),
+ iconClass: 'animate-spin',
+ primaryText: 'In queue',
+ secondaryText: '8:59:30pm'
+ }
+}
+
+export const InitializationJob: Story = {
+ args: {
+ iconName: iconForJobState('initialization'),
+ primaryText: 'Initializing...',
+ secondaryText: '8:59:35pm'
+ }
+}
+
+export const RunningJob: Story = {
+ args: {
+ iconName: iconForJobState('running'),
+ primaryText: 'Total: 30%',
+ secondaryText: 'CLIP Text Encode: 70%',
+ progressTotalPercent: 30,
+ progressCurrentPercent: 70
+ }
+}
+
+export const RunningJobWithActions: Story = {
+ args: {
+ iconName: iconForJobState('running'),
+ progressTotalPercent: 30,
+ progressCurrentPercent: 70
+ },
+ render: renderRunningJobWithActions
+}
+
+export const FailedJob: Story = {
+ args: {
+ iconName: iconForJobState('failed'),
+ iconClass: 'text-destructive-background',
+ iconWrapperClass: 'bg-modal-card-placeholder-background',
+ primaryText: 'Failed',
+ secondaryText: '8:59:30pm'
+ }
+}
+
+export const GeneratedImage: Story = {
+ args: {
+ previewUrl: IMAGE_PREVIEW,
+ previewAlt: 'image-032.png',
+ primaryText: 'image-032.png',
+ secondaryText: '1.84s'
+ }
+}
+
+export const GeneratedVideo: Story = {
+ args: {
+ previewUrl: VIDEO_PREVIEW,
+ previewAlt: 'clip-01.mp4',
+ primaryText: 'clip-01.mp4',
+ secondaryText: '2m 12s'
+ }
+}
+
+export const GeneratedAudio: Story = {
+ args: {
+ iconName: 'icon-[lucide--music]',
+ primaryText: 'soundtrack-01.mp3',
+ secondaryText: '3m 20s'
+ }
+}
+
+export const Generated3D: Story = {
+ args: {
+ iconName: 'icon-[lucide--box]',
+ primaryText: 'scene-01.glb',
+ secondaryText: '128 MB'
+ }
+}
+
type AssetsListItemProps = InstanceType['$props']
-function renderActiveJob(args: AssetsListItemProps) {
+function renderRunningJobWithActions(args: AssetsListItemProps) {
return {
- components: { Button, AssetsListItem },
+ components: { AssetsListItem, Button },
setup() {
return { args }
},
@@ -49,51 +134,3 @@ function renderActiveJob(args: AssetsListItemProps) {
`
}
}
-
-function renderGeneratedAsset(args: AssetsListItemProps) {
- return {
- components: { AssetsListItem },
- setup() {
- return { args }
- },
- template: `
-
-
-
- 1m 56s
- 512x512
-
-
-
- `
- }
-}
-
-export const ActiveJob: Story = {
- args: {
- previewUrl: '/assets/images/comfy-logo-single.svg',
- previewAlt: 'Job preview',
- progressTotalPercent: 30,
- progressCurrentPercent: 70
- },
- render: renderActiveJob
-}
-
-export const FailedJob: Story = {
- args: {
- iconName: 'icon-[lucide--circle-alert]',
- iconClass: 'text-destructive-background',
- iconWrapperClass: 'bg-modal-card-placeholder-background',
- primaryText: 'Failed',
- secondaryText: '8:59:30pm'
- }
-}
-
-export const GeneratedAsset: Story = {
- args: {
- previewUrl: '/assets/images/comfy-logo-single.svg',
- previewAlt: 'image03.png',
- primaryText: 'image03.png'
- },
- render: renderGeneratedAsset
-}
diff --git a/src/storybook/mocks/useJobActions.ts b/src/storybook/mocks/useJobActions.ts
new file mode 100644
index 000000000..b2b823341
--- /dev/null
+++ b/src/storybook/mocks/useJobActions.ts
@@ -0,0 +1,51 @@
+import { computed, ref, toValue } from 'vue'
+import type { MaybeRefOrGetter } from 'vue'
+
+import type { JobAction } from '../../composables/queue/useJobActions'
+import type { JobListItem } from '../../composables/queue/useJobList'
+import type { JobState } from '../../types/queue'
+
+const actionsByJobId = ref>({})
+const cancellableStates: JobState[] = ['pending', 'initialization', 'running']
+const cancelAction: JobAction = {
+ icon: 'icon-[lucide--x]',
+ label: 'Cancel',
+ variant: 'destructive'
+}
+
+export function setMockJobActions(actions: Record) {
+ actionsByJobId.value = actions
+}
+
+export function useJobActions(
+ job?: MaybeRefOrGetter
+) {
+ const jobRef = computed(() => (job ? (toValue(job) ?? null) : null))
+
+ const canCancelJob = computed(() => {
+ const currentJob = jobRef.value
+ if (!currentJob) {
+ return false
+ }
+
+ const configuredActions = actionsByJobId.value[currentJob.id]
+ if (configuredActions) {
+ return configuredActions.length > 0
+ }
+
+ return (
+ currentJob.showClear !== false &&
+ cancellableStates.includes(currentJob.state)
+ )
+ })
+
+ async function runCancelJob() {
+ return undefined
+ }
+
+ return {
+ cancelAction,
+ canCancelJob,
+ runCancelJob
+ }
+}
diff --git a/src/storybook/mocks/useJobList.ts b/src/storybook/mocks/useJobList.ts
new file mode 100644
index 000000000..702c1cebc
--- /dev/null
+++ b/src/storybook/mocks/useJobList.ts
@@ -0,0 +1,58 @@
+import { computed, ref } from 'vue'
+
+import type { TaskItemImpl } from '../../stores/queueStore'
+import type {
+ JobGroup,
+ JobListItem,
+ JobSortMode,
+ JobTab
+} from '../../composables/queue/useJobList'
+
+const jobItems = ref([])
+
+function buildGroupedJobItems(): JobGroup[] {
+ return [
+ {
+ key: 'storybook',
+ label: 'Storybook',
+ items: jobItems.value
+ }
+ ]
+}
+
+const groupedJobItems = computed(buildGroupedJobItems)
+
+const selectedJobTab = ref('All')
+const selectedWorkflowFilter = ref<'all' | 'current'>('all')
+const selectedSortMode = ref('mostRecent')
+const currentNodeName = ref('KSampler')
+function buildEmptyTasks(): TaskItemImpl[] {
+ return []
+}
+
+const allTasksSorted = computed(buildEmptyTasks)
+const filteredTasks = computed(buildEmptyTasks)
+
+function buildHasFailedJobs() {
+ return jobItems.value.some((item) => item.state === 'failed')
+}
+
+const hasFailedJobs = computed(buildHasFailedJobs)
+
+export function setMockJobItems(items: JobListItem[]) {
+ jobItems.value = items
+}
+
+export function useJobList() {
+ return {
+ selectedJobTab,
+ selectedWorkflowFilter,
+ selectedSortMode,
+ hasFailedJobs,
+ allTasksSorted,
+ filteredTasks,
+ jobItems,
+ groupedJobItems,
+ currentNodeName
+ }
+}
From 3ce588ad4248e13b7087c7dfd4ba6a1b90ddb0c2 Mon Sep 17 00:00:00 2001
From: Kelly Yang <124ykl@gmail.com>
Date: Sat, 10 Jan 2026 21:45:53 -0800
Subject: [PATCH 09/63] Update viewjobhistorycommand (#7911)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## Summary
Add the key binding to the schema and mark the setting as hidden.
https://github.com/Comfy-Org/ComfyUI_frontend/pull/7805#pullrequestreview-3627969654
## Changes
**What**:
- Added a new `shortcuts` field to the user settings database model.
- Marked the `shortcuts` field as `hidden` in the `API/Schema` to ensure
it remains internal for now, as suggested by the reviewer @benceruleanlu
.
- Migrated shortcut storage logic from frontend-only (store) to
persistent backend storage.
- **Breaking**: None
- **Dependencies**: None
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7911-Update-viewjobhistorycommand-2e26d73d3650813297c3f9f7deb53b14)
by [Unito](https://www.unito.io)
---
src/platform/settings/constants/coreSettings.ts | 7 +++++++
src/schemas/apiSchema.ts | 1 +
src/stores/queueStore.ts | 8 +++++++-
3 files changed, 15 insertions(+), 1 deletion(-)
diff --git a/src/platform/settings/constants/coreSettings.ts b/src/platform/settings/constants/coreSettings.ts
index 929614811..e01141429 100644
--- a/src/platform/settings/constants/coreSettings.ts
+++ b/src/platform/settings/constants/coreSettings.ts
@@ -817,6 +817,13 @@ export const CORE_SETTINGS: SettingParams[] = [
defaultValue: 64,
versionAdded: '1.4.12'
},
+ {
+ id: 'Comfy.Queue.History.Expanded',
+ name: 'Queue history expanded',
+ type: 'hidden',
+ defaultValue: false,
+ versionAdded: '1.37.0'
+ },
{
id: 'Comfy.Execution.PreviewMethod',
category: ['Comfy', 'Execution', 'PreviewMethod'],
diff --git a/src/schemas/apiSchema.ts b/src/schemas/apiSchema.ts
index 6e01d6f4e..0c029b3ae 100644
--- a/src/schemas/apiSchema.ts
+++ b/src/schemas/apiSchema.ts
@@ -467,6 +467,7 @@ const zSettings = z.object({
'Comfy.Notification.ShowVersionUpdates': z.boolean(),
'Comfy.QueueButton.BatchCountLimit': z.number(),
'Comfy.Queue.MaxHistoryItems': z.number(),
+ 'Comfy.Queue.History.Expanded': z.boolean(),
'Comfy.Keybinding.UnsetBindings': z.array(zKeybinding),
'Comfy.Keybinding.NewBindings': z.array(zKeybinding),
'Comfy.Extension.Disabled': z.array(z.string()),
diff --git a/src/stores/queueStore.ts b/src/stores/queueStore.ts
index c7e17c1a0..7571ad280 100644
--- a/src/stores/queueStore.ts
+++ b/src/stores/queueStore.ts
@@ -4,6 +4,7 @@ import { computed, ref, shallowRef, toRaw, toValue } from 'vue'
import { isCloud } from '@/platform/distribution/types'
import { reconcileHistory } from '@/platform/remote/comfyui/history/reconciliation'
+import { useSettingStore } from '@/platform/settings/settingStore'
import { getWorkflowFromHistory } from '@/platform/workflow/cloud'
import type {
ComfyWorkflowJSON,
@@ -620,7 +621,12 @@ export const useQueueSettingsStore = defineStore('queueSettingsStore', {
})
export const useQueueUIStore = defineStore('queueUIStore', () => {
- const isOverlayExpanded = ref(false)
+ const settingStore = useSettingStore()
+
+ const isOverlayExpanded = computed({
+ get: () => settingStore.get('Comfy.Queue.History.Expanded'),
+ set: (value) => settingStore.set('Comfy.Queue.History.Expanded', value)
+ })
function toggleOverlay() {
isOverlayExpanded.value = !isOverlayExpanded.value
From e3906a06560ad41a1a6d8d3928f5f8030b6860e5 Mon Sep 17 00:00:00 2001
From: Johnpaul Chiwetelu <49923152+Myestery@users.noreply.github.com>
Date: Sun, 11 Jan 2026 06:53:15 +0100
Subject: [PATCH 10/63] chore: bump CI container to 0.0.10 (#7881)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Updates CI container from `0.0.8` to `0.0.10`
**Triggered by:** [Tag
0.0.10](https://github.com/Comfy-Org/comfyui-ci-container/tree/0.0.10)
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7881-chore-bump-CI-container-to-0-0-10-2e16d73d3650814aa715cb4e12eaec9d)
by [Unito](https://www.unito.io)
---
.github/workflows/ci-tests-e2e.yaml | 4 ++--
.github/workflows/pr-update-playwright-expectations.yaml | 2 +-
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/.github/workflows/ci-tests-e2e.yaml b/.github/workflows/ci-tests-e2e.yaml
index 12ccfae93..91ae9a481 100644
--- a/.github/workflows/ci-tests-e2e.yaml
+++ b/.github/workflows/ci-tests-e2e.yaml
@@ -37,7 +37,7 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 60
container:
- image: ghcr.io/comfy-org/comfyui-ci-container:0.0.8
+ image: ghcr.io/comfy-org/comfyui-ci-container:0.0.10
credentials:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
@@ -85,7 +85,7 @@ jobs:
needs: setup
runs-on: ubuntu-latest
container:
- image: ghcr.io/comfy-org/comfyui-ci-container:0.0.8
+ image: ghcr.io/comfy-org/comfyui-ci-container:0.0.10
credentials:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.github/workflows/pr-update-playwright-expectations.yaml b/.github/workflows/pr-update-playwright-expectations.yaml
index e1c51c546..0ca696721 100644
--- a/.github/workflows/pr-update-playwright-expectations.yaml
+++ b/.github/workflows/pr-update-playwright-expectations.yaml
@@ -77,7 +77,7 @@ jobs:
needs: setup
runs-on: ubuntu-latest
container:
- image: ghcr.io/comfy-org/comfyui-ci-container:0.0.8
+ image: ghcr.io/comfy-org/comfyui-ci-container:0.0.10
credentials:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
From 6883241e5065c1fee2afd6302593e9fc0277a5ad Mon Sep 17 00:00:00 2001
From: danialshirali16 <69107616+danialshirali16@users.noreply.github.com>
Date: Sun, 11 Jan 2026 09:32:16 +0330
Subject: [PATCH 11/63] Add Persian (Farsi) language support (#7876)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## Description
This PR adds Persian (Farsi) language support to ComfyUI.
## Changes
- Added `fa` to output locales in `.i18nrc.cjs` with Persian-specific
translation guidelines
- Added Persian loaders for all translation files (main, nodeDefs,
commands, settings) in `src/i18n.ts`
- Added Persian (فارسی) option to language settings dropdown in
`src/platform/settings/constants/coreSettings.ts`
- Created empty Persian locale files in `src/locales/fa/` directory
(will be populated by the CI translation system)
## Translation Guidelines
The Persian translation will follow these guidelines:
- Use formal Persian (فارسی رسمی) for professional tone throughout the
UI
- Keep commonly used technical terms in English when they are standard
in Persian software (e.g., node, workflow)
- Use Arabic-Indic numerals (۰-۹) for numbers where appropriate
- Maintain consistency with terminology used in Persian software and
design applications
## Testing
The configuration has been tested to ensure:
- TypeScript compilation succeeds
- All four translation files are properly referenced
- Language option appears correctly in settings
## Notes
Following the contribution guidelines in `src/locales/CONTRIBUTING.md`,
the empty translation files will be automatically populated by the CI
system using OpenAI. Persian-speaking contributors can review and refine
these translations after the automated generation.
---
Special names to keep untranslated: flux, photomaker, clip, vae, cfg,
stable audio, stable cascade, stable zero, controlnet, lora, HiDream,
Civitai, Hugging Face
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7876-Add-Persian-Farsi-language-support-2e16d73d365081f69df0e50048ce87ba)
by [Unito](https://www.unito.io)
Co-authored-by: danialshirali16
---
.i18nrc.cjs | 8 +++++++-
src/i18n.ts | 4 ++++
src/locales/fa/commands.json | 1 +
src/locales/fa/main.json | 1 +
src/locales/fa/nodeDefs.json | 1 +
src/locales/fa/settings.json | 1 +
src/platform/settings/constants/coreSettings.ts | 3 ++-
7 files changed, 17 insertions(+), 2 deletions(-)
create mode 100644 src/locales/fa/commands.json
create mode 100644 src/locales/fa/main.json
create mode 100644 src/locales/fa/nodeDefs.json
create mode 100644 src/locales/fa/settings.json
diff --git a/.i18nrc.cjs b/.i18nrc.cjs
index f0ed79099..86ce06eaa 100644
--- a/.i18nrc.cjs
+++ b/.i18nrc.cjs
@@ -10,7 +10,7 @@ module.exports = defineConfig({
entry: 'src/locales/en',
entryLocale: 'en',
output: 'src/locales',
- outputLocales: ['zh', 'zh-TW', 'ru', 'ja', 'ko', 'fr', 'es', 'ar', 'tr', 'pt-BR'],
+ outputLocales: ['zh', 'zh-TW', 'ru', 'ja', 'ko', 'fr', 'es', 'ar', 'tr', 'pt-BR', 'fa'],
reference: `Special names to keep untranslated: flux, photomaker, clip, vae, cfg, stable audio, stable cascade, stable zero, controlnet, lora, HiDream, Civitai, Hugging Face.
'latent' is the short form of 'latent space'.
'mask' is in the context of image processing.
@@ -19,5 +19,11 @@ module.exports = defineConfig({
- For 'zh' locale: Use ONLY Simplified Chinese characters (简体中文). Common examples: 节点 (not 節點), 画布 (not 畫布), 图像 (not 圖像), 选择 (not 選擇), 减小 (not 減小).
- For 'zh-TW' locale: Use ONLY Traditional Chinese characters (繁體中文) with Taiwan-specific terminology.
- NEVER mix Simplified and Traditional Chinese characters within the same locale.
+
+ IMPORTANT Persian Translation Guidelines:
+ - For 'fa' locale: Use formal Persian (فارسی رسمی) for professional tone throughout the UI.
+ - Keep commonly used technical terms in English when they are standard in Persian software (e.g., node, workflow).
+ - Use Arabic-Indic numerals (۰-۹) for numbers where appropriate.
+ - Maintain consistency with terminology used in Persian software and design applications.
`
});
diff --git a/src/i18n.ts b/src/i18n.ts
index 46841c16c..e0ee54e2b 100644
--- a/src/i18n.ts
+++ b/src/i18n.ts
@@ -30,6 +30,7 @@ const localeLoaders: Record<
> = {
ar: () => import('./locales/ar/main.json'),
es: () => import('./locales/es/main.json'),
+ fa: () => import('./locales/fa/main.json'),
fr: () => import('./locales/fr/main.json'),
ja: () => import('./locales/ja/main.json'),
ko: () => import('./locales/ko/main.json'),
@@ -46,6 +47,7 @@ const nodeDefsLoaders: Record<
> = {
ar: () => import('./locales/ar/nodeDefs.json'),
es: () => import('./locales/es/nodeDefs.json'),
+ fa: () => import('./locales/fa/nodeDefs.json'),
fr: () => import('./locales/fr/nodeDefs.json'),
ja: () => import('./locales/ja/nodeDefs.json'),
ko: () => import('./locales/ko/nodeDefs.json'),
@@ -62,6 +64,7 @@ const commandsLoaders: Record<
> = {
ar: () => import('./locales/ar/commands.json'),
es: () => import('./locales/es/commands.json'),
+ fa: () => import('./locales/fa/commands.json'),
fr: () => import('./locales/fr/commands.json'),
ja: () => import('./locales/ja/commands.json'),
ko: () => import('./locales/ko/commands.json'),
@@ -78,6 +81,7 @@ const settingsLoaders: Record<
> = {
ar: () => import('./locales/ar/settings.json'),
es: () => import('./locales/es/settings.json'),
+ fa: () => import('./locales/fa/settings.json'),
fr: () => import('./locales/fr/settings.json'),
ja: () => import('./locales/ja/settings.json'),
ko: () => import('./locales/ko/settings.json'),
diff --git a/src/locales/fa/commands.json b/src/locales/fa/commands.json
new file mode 100644
index 000000000..9e26dfeeb
--- /dev/null
+++ b/src/locales/fa/commands.json
@@ -0,0 +1 @@
+{}
\ No newline at end of file
diff --git a/src/locales/fa/main.json b/src/locales/fa/main.json
new file mode 100644
index 000000000..9e26dfeeb
--- /dev/null
+++ b/src/locales/fa/main.json
@@ -0,0 +1 @@
+{}
\ No newline at end of file
diff --git a/src/locales/fa/nodeDefs.json b/src/locales/fa/nodeDefs.json
new file mode 100644
index 000000000..9e26dfeeb
--- /dev/null
+++ b/src/locales/fa/nodeDefs.json
@@ -0,0 +1 @@
+{}
\ No newline at end of file
diff --git a/src/locales/fa/settings.json b/src/locales/fa/settings.json
new file mode 100644
index 000000000..9e26dfeeb
--- /dev/null
+++ b/src/locales/fa/settings.json
@@ -0,0 +1 @@
+{}
\ No newline at end of file
diff --git a/src/platform/settings/constants/coreSettings.ts b/src/platform/settings/constants/coreSettings.ts
index e01141429..80438f4d0 100644
--- a/src/platform/settings/constants/coreSettings.ts
+++ b/src/platform/settings/constants/coreSettings.ts
@@ -410,7 +410,8 @@ export const CORE_SETTINGS: SettingParams[] = [
{ value: 'es', text: 'Español' },
{ value: 'ar', text: 'عربي' },
{ value: 'tr', text: 'Türkçe' },
- { value: 'pt-BR', text: 'Português (BR)' }
+ { value: 'pt-BR', text: 'Português (BR)' },
+ { value: 'fa', text: 'فارسی' }
],
defaultValue: () => navigator.language.split('-')[0] || 'en'
},
From 2d5d18c020201f3b65b6720b1493c90e179e1b81 Mon Sep 17 00:00:00 2001
From: Csongor Czezar <44126075+csongorczezar@users.noreply.github.com>
Date: Sat, 10 Jan 2026 22:09:18 -0800
Subject: [PATCH 12/63] feat: improved playwright comment format (#7882)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
### Description
Improve Playwright PR comment format
### Problem
The current Playwright PR comment format is verbose and doesn't provide
easy access to failing test details.
Developers need to navigate multiple levels deep to:
Find which tests failed
Access test source code
View trace files for debugging
This makes debugging test failures tedious and time-consuming.
### Solution
Improved the Playwright PR comment format to be concise and actionable
by:
Modified extract-playwright-counts.ts to extract detailed failure
information from Playwright JSON reports including test names, file
paths, and trace URLs
Updated pr-playwright-deploy-and-comment.sh to generate concise comments
with failed tests listed upfront
Modified ci-tests-e2e.yaml to pass GITHUB_SHA for source code links
Modified ci-tests-e2e-forks.yaml to pass GITHUB_SHA for forked PR
workflow
**Before:**
Large multi-section layout with emoji-heavy headers
Summary section listing all counts vertically
Browser results displayed prominently with detailed counts
Failed test details only accessible through report links
No direct links to test source code or traces
**After:**
Concise single-line header with status
Single-line summary: "X passed, Y failed, Z flaky, W skipped (Total: N)"
Failed tests section (only shown when tests fail) with:
Direct links to test source code on GitHub
Direct links to trace viewer for each failure
Browser details collapsed in details section
Overall roughly half size reduction in visible text
### Testing
Verified TypeScript extraction logic for parsing Playwright JSON reports
Validated shell script syntax
Confirmed GitHub workflow changes are properly formatted
Will be fully tested on next PR with actual test failures
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7882-feat-improved-playwright-comment-format-2e16d73d365081609078e34773063511)
by [Unito](https://www.unito.io)
---
.github/workflows/ci-tests-e2e-forks.yaml | 5 +-
.github/workflows/ci-tests-e2e.yaml | 3 +-
scripts/cicd/extract-playwright-counts.ts | 146 +++++++++++++++++-
.../cicd/pr-playwright-deploy-and-comment.sh | 100 +++++++-----
4 files changed, 208 insertions(+), 46 deletions(-)
diff --git a/.github/workflows/ci-tests-e2e-forks.yaml b/.github/workflows/ci-tests-e2e-forks.yaml
index c1828b7fb..8f039f1c4 100644
--- a/.github/workflows/ci-tests-e2e-forks.yaml
+++ b/.github/workflows/ci-tests-e2e-forks.yaml
@@ -1,9 +1,9 @@
# Description: Deploys test results from forked PRs (forks can't access deployment secrets)
-name: "CI: Tests E2E (Deploy for Forks)"
+name: 'CI: Tests E2E (Deploy for Forks)'
on:
workflow_run:
- workflows: ["CI: Tests E2E"]
+ workflows: ['CI: Tests E2E']
types: [requested, completed]
env:
@@ -81,6 +81,7 @@ jobs:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
GITHUB_TOKEN: ${{ github.token }}
+ GITHUB_SHA: ${{ github.event.workflow_run.head_sha }}
run: |
# Rename merged report if exists
[ -d "reports/playwright-report-chromium-merged" ] && \
diff --git a/.github/workflows/ci-tests-e2e.yaml b/.github/workflows/ci-tests-e2e.yaml
index 91ae9a481..90bf4112c 100644
--- a/.github/workflows/ci-tests-e2e.yaml
+++ b/.github/workflows/ci-tests-e2e.yaml
@@ -1,5 +1,5 @@
# Description: End-to-end testing with Playwright across multiple browsers, deploys test reports to Cloudflare Pages
-name: "CI: Tests E2E"
+name: 'CI: Tests E2E'
on:
push:
@@ -222,6 +222,7 @@ jobs:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
GITHUB_TOKEN: ${{ github.token }}
+ GITHUB_SHA: ${{ github.event.pull_request.head.sha }}
run: |
bash ./scripts/cicd/pr-playwright-deploy-and-comment.sh \
"${{ github.event.pull_request.number }}" \
diff --git a/scripts/cicd/extract-playwright-counts.ts b/scripts/cicd/extract-playwright-counts.ts
index ff6f44db3..1a7922816 100755
--- a/scripts/cicd/extract-playwright-counts.ts
+++ b/scripts/cicd/extract-playwright-counts.ts
@@ -10,37 +10,158 @@ interface TestStats {
finished?: number
}
+interface TestResult {
+ status: string
+ duration?: number
+ error?: {
+ message?: string
+ stack?: string
+ }
+ attachments?: Array<{
+ name: string
+ path?: string
+ contentType: string
+ }>
+}
+
+interface TestCase {
+ title: string
+ ok: boolean
+ outcome: string
+ results: TestResult[]
+}
+
+interface Suite {
+ title: string
+ file: string
+ suites?: Suite[]
+ tests?: TestCase[]
+}
+
+interface FullReportData {
+ stats?: TestStats
+ suites?: Suite[]
+}
+
interface ReportData {
stats?: TestStats
}
+interface FailedTest {
+ name: string
+ file: string
+ traceUrl?: string
+ error?: string
+}
+
interface TestCounts {
passed: number
failed: number
flaky: number
skipped: number
total: number
+ failures?: FailedTest[]
+}
+
+/**
+ * Extract failed test details from Playwright report
+ */
+function extractFailedTests(
+ reportData: FullReportData,
+ baseUrl?: string
+): FailedTest[] {
+ const failures: FailedTest[] = []
+
+ function processTest(test: TestCase, file: string, suitePath: string[]) {
+ // Check if test failed or is flaky
+ const hasFailed = test.results.some(
+ (r) => r.status === 'failed' || r.status === 'timedOut'
+ )
+ const isFlaky = test.outcome === 'flaky'
+
+ if (hasFailed || isFlaky) {
+ const fullTestName = [...suitePath, test.title]
+ .filter(Boolean)
+ .join(' › ')
+ const failedResult = test.results.find(
+ (r) => r.status === 'failed' || r.status === 'timedOut'
+ )
+
+ // Find trace attachment
+ let traceUrl: string | undefined
+ if (failedResult?.attachments) {
+ const traceAttachment = failedResult.attachments.find(
+ (a) => a.name === 'trace' && a.contentType === 'application/zip'
+ )
+ if (traceAttachment?.path) {
+ // Convert local path to URL path
+ const tracePath = traceAttachment.path.replace(/\\/g, '/')
+ const traceFile = path.basename(tracePath)
+ if (baseUrl) {
+ // Construct trace viewer URL
+ const traceDataUrl = `${baseUrl}/data/${traceFile}`
+ traceUrl = `${baseUrl}/trace/?trace=${encodeURIComponent(traceDataUrl)}`
+ }
+ }
+ }
+
+ failures.push({
+ name: fullTestName,
+ file: file,
+ traceUrl,
+ error: failedResult?.error?.message
+ })
+ }
+ }
+
+ function processSuite(suite: Suite, parentPath: string[] = []) {
+ const suitePath = suite.title ? [...parentPath, suite.title] : parentPath
+
+ // Process tests in this suite
+ if (suite.tests) {
+ for (const test of suite.tests) {
+ processTest(test, suite.file, suitePath)
+ }
+ }
+
+ // Recursively process nested suites
+ if (suite.suites) {
+ for (const childSuite of suite.suites) {
+ processSuite(childSuite, suitePath)
+ }
+ }
+ }
+
+ if (reportData.suites) {
+ for (const suite of reportData.suites) {
+ processSuite(suite)
+ }
+ }
+
+ return failures
}
/**
* Extract test counts from Playwright HTML report
* @param reportDir - Path to the playwright-report directory
- * @returns Test counts { passed, failed, flaky, skipped, total }
+ * @param baseUrl - Base URL of the deployed report (for trace links)
+ * @returns Test counts { passed, failed, flaky, skipped, total, failures }
*/
-function extractTestCounts(reportDir: string): TestCounts {
+function extractTestCounts(reportDir: string, baseUrl?: string): TestCounts {
const counts: TestCounts = {
passed: 0,
failed: 0,
flaky: 0,
skipped: 0,
- total: 0
+ total: 0,
+ failures: []
}
try {
// First, try to find report.json which Playwright generates with JSON reporter
const jsonReportFile = path.join(reportDir, 'report.json')
if (fs.existsSync(jsonReportFile)) {
- const reportJson: ReportData = JSON.parse(
+ const reportJson: FullReportData = JSON.parse(
fs.readFileSync(jsonReportFile, 'utf-8')
)
if (reportJson.stats) {
@@ -54,6 +175,12 @@ function extractTestCounts(reportDir: string): TestCounts {
counts.failed = stats.unexpected || 0
counts.flaky = stats.flaky || 0
counts.skipped = stats.skipped || 0
+
+ // Extract detailed failure information
+ if (counts.failed > 0 || counts.flaky > 0) {
+ counts.failures = extractFailedTests(reportJson, baseUrl)
+ }
+
return counts
}
}
@@ -169,15 +296,18 @@ function extractTestCounts(reportDir: string): TestCounts {
// Main execution
const reportDir = process.argv[2]
+const baseUrl = process.argv[3] // Optional: base URL for trace links
if (!reportDir) {
- console.error('Usage: extract-playwright-counts.ts ')
+ console.error(
+ 'Usage: extract-playwright-counts.ts [base-url]'
+ )
process.exit(1)
}
-const counts = extractTestCounts(reportDir)
+const counts = extractTestCounts(reportDir, baseUrl)
// Output as JSON for easy parsing in shell script
-console.log(JSON.stringify(counts))
+process.stdout.write(JSON.stringify(counts) + '\n')
-export { extractTestCounts }
+export { extractTestCounts, extractFailedTests }
diff --git a/scripts/cicd/pr-playwright-deploy-and-comment.sh b/scripts/cicd/pr-playwright-deploy-and-comment.sh
index 840203f44..9332e60b7 100755
--- a/scripts/cicd/pr-playwright-deploy-and-comment.sh
+++ b/scripts/cicd/pr-playwright-deploy-and-comment.sh
@@ -134,23 +134,22 @@ post_comment() {
# Main execution
if [ "$STATUS" = "starting" ]; then
- # Post starting comment
+ # Post concise starting comment
comment=$(cat < **Tests are starting...**
+Tests started at $START_TIME UTC
-⏰ Started at: $START_TIME UTC
+
+📊 Browser Tests
-### 🚀 Running Tests
-- 🧪 **chromium**: Running tests...
-- 🧪 **chromium-0.5x**: Running tests...
-- 🧪 **chromium-2x**: Running tests...
-- 🧪 **mobile-chrome**: Running tests...
+- **chromium**: Running...
+- **chromium-0.5x**: Running...
+- **chromium-2x**: Running...
+- **mobile-chrome**: Running...
----
-⏱️ Please wait while tests are running...
+
EOF
)
post_comment "$comment"
@@ -189,7 +188,8 @@ else
if command -v tsx > /dev/null 2>&1 && [ -f "$EXTRACT_SCRIPT" ]; then
echo "Extracting counts from $REPORT_DIR using $EXTRACT_SCRIPT" >&2
- counts=$(tsx "$EXTRACT_SCRIPT" "$REPORT_DIR" 2>&1 || echo '{}')
+ # Pass the base URL so we can generate trace links
+ counts=$(tsx "$EXTRACT_SCRIPT" "$REPORT_DIR" "$url" 2>&1 || echo '{}')
echo "Extracted counts for $browser: $counts" >&2
echo "$counts" > "$temp_dir/$i.counts"
else
@@ -286,43 +286,74 @@ else
# Determine overall status
if [ $total_failed -gt 0 ]; then
status_icon="❌"
- status_text="Some tests failed"
+ status_text="Failed"
elif [ $total_flaky -gt 0 ]; then
status_icon="⚠️"
- status_text="Tests passed with flaky tests"
+ status_text="Passed with flaky tests"
elif [ $total_tests -gt 0 ]; then
status_icon="✅"
- status_text="All tests passed!"
+ status_text="Passed"
else
status_icon="🕵🏻"
- status_text="No test results found"
+ status_text="No test results"
fi
- # Generate completion comment
+ # Generate concise completion comment
comment="$COMMENT_MARKER
-## 🎭 Playwright Test Results
-
-$status_icon **$status_text**
-
-⏰ Completed at: $(date -u '+%m/%d/%Y, %I:%M:%S %p') UTC"
+## 🎭 Playwright Tests: $status_icon **$status_text**"
# Add summary counts if we have test data
if [ $total_tests -gt 0 ]; then
comment="$comment
-### 📈 Summary
-- **Total Tests:** $total_tests
-- **Passed:** $total_passed ✅
-- **Failed:** $total_failed $([ $total_failed -gt 0 ] && echo '❌' || echo '')
-- **Flaky:** $total_flaky $([ $total_flaky -gt 0 ] && echo '⚠️' || echo '')
-- **Skipped:** $total_skipped $([ $total_skipped -gt 0 ] && echo '⏭️' || echo '')"
+**Results:** $total_passed passed, $total_failed failed, $total_flaky flaky, $total_skipped skipped (Total: $total_tests)"
+ fi
+
+ # Extract and display failed tests from all browsers
+ if [ $total_failed -gt 0 ] || [ $total_flaky -gt 0 ]; then
+ comment="$comment
+
+### ❌ Failed Tests"
+
+ # Process each browser's failures
+ for counts_json in "${counts_array[@]}"; do
+ [ -z "$counts_json" ] || [ "$counts_json" = "{}" ] && continue
+
+ if command -v jq > /dev/null 2>&1; then
+ # Extract failures array from JSON
+ failures=$(echo "$counts_json" | jq -r '.failures // [] | .[]? | "\(.name)|\(.file)|\(.traceUrl // "")"')
+
+ if [ -n "$failures" ]; then
+ while IFS='|' read -r test_name test_file trace_url; do
+ [ -z "$test_name" ] && continue
+
+ # Convert file path to GitHub URL (relative to repo root)
+ github_file_url="https://github.com/$GITHUB_REPOSITORY/blob/$GITHUB_SHA/$test_file"
+
+ # Build the failed test line
+ test_line="- [$test_name]($github_file_url)"
+
+ if [ -n "$trace_url" ] && [ "$trace_url" != "null" ]; then
+ test_line="$test_line: [View trace]($trace_url)"
+ fi
+
+ comment="$comment
+$test_line"
+ done <<< "$failures"
+ fi
+ fi
+ done
fi
+ # Add browser reports in collapsible section
comment="$comment
-### 📊 Test Reports by Browser"
+
+📊 Browser Reports
+
+"
- # Add browser results with individual counts
+ # Add browser results
i=0
IFS=' ' read -r -a browser_array <<< "$BROWSERS"
IFS=' ' read -r -a url_array <<< "$urls"
@@ -349,7 +380,7 @@ $status_icon **$status_text**
fi
if [ -n "$b_total" ] && [ "$b_total" != "0" ]; then
- counts_str=" • ✅ $b_passed / ❌ $b_failed / ⚠️ $b_flaky / ⏭️ $b_skipped"
+ counts_str=" (✅ $b_passed / ❌ $b_failed / ⚠️ $b_flaky / ⏭️ $b_skipped)"
else
counts_str=""
fi
@@ -358,10 +389,10 @@ $status_icon **$status_text**
fi
comment="$comment
-- ✅ **${browser}**: [View Report](${url})${counts_str}"
+- **${browser}**: [View Report](${url})${counts_str}"
else
comment="$comment
-- ❌ **${browser}**: Deployment failed"
+- **${browser}**: ❌ Deployment failed"
fi
i=$((i + 1))
done
@@ -369,8 +400,7 @@ $status_icon **$status_text**
comment="$comment
----
-🎉 Click on the links above to view detailed test results for each browser configuration."
+ "
post_comment "$comment"
fi
From dcfa53fd7d8d49b2a00e155899121e82193e2355 Mon Sep 17 00:00:00 2001
From: Yourz
Date: Sun, 11 Jan 2026 14:24:43 +0800
Subject: [PATCH 13/63] feat: add dynamic Fuse.js options loading for template
filtering (#7822)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## Summary
PRD:
https://www.notion.so/comfy-org/Implement-Move-search-config-to-templates-repo-for-template-owner-adjustability-2c76d73d365081ad81c4ed33332eda09
Move search config to templates repo for template owner adjustability
## Changes
- **What**:
- Made `fuseOptions` reactive in `useTemplateFiltering` composable to
support dynamic updates
- Added `getFuseOptions()` API method to fetch Fuse.js configuration
from `/templates/fuse_options.json`
- Added `loadFuseOptions()` function to `useTemplateFiltering` that
fetches and applies server-provided options
- Removed unused `templateFuse` computed property from
`workflowTemplatesStore`
- Added comprehensive unit tests covering success, null response, error
handling, and Fuse instance recreation scenarios
- **Breaking**: None
- **Dependencies**: None (uses existing `fuse.js` and `axios`
dependencies)
## Review Focus
- Verify that the API endpoint path `/templates/fuse_options.json` is
correct and accessible
- Confirm that the reactive `fuseOptions` properly triggers Fuse
instance recreation when updated
- Check that error handling gracefully falls back to default options
when server fetch fails
- Ensure the watch on `fuseOptions` is necessary or can be removed
(currently just recreates Fuse via computed)
- Review test coverage to ensure all edge cases are handled
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7822-feat-add-dynamic-Fuse-js-options-loading-for-template-filtering-2db6d73d365081828103d8ee70844b2e)
by [Unito](https://www.unito.io)
---------
Co-authored-by: GitHub Action
---
.../widget/WorkflowTemplateSelectorDialog.vue | 7 +-
src/composables/useTemplateFiltering.test.ts | 123 ++++++++++++++++++
src/composables/useTemplateFiltering.ts | 44 ++++---
.../repositories/workflowTemplatesStore.ts | 20 ---
src/scripts/api.ts | 29 ++++-
5 files changed, 183 insertions(+), 40 deletions(-)
diff --git a/src/components/custom/widget/WorkflowTemplateSelectorDialog.vue b/src/components/custom/widget/WorkflowTemplateSelectorDialog.vue
index 7c76e971d..aeb98971b 100644
--- a/src/components/custom/widget/WorkflowTemplateSelectorDialog.vue
+++ b/src/components/custom/widget/WorkflowTemplateSelectorDialog.vue
@@ -563,7 +563,8 @@ const {
availableRunsOn,
filteredCount,
totalCount,
- resetFilters
+ resetFilters,
+ loadFuseOptions
} = useTemplateFiltering(navigationFilteredTemplates)
/**
@@ -815,10 +816,10 @@ const pageTitle = computed(() => {
// Initialize templates loading with useAsyncState
const { isLoading } = useAsyncState(
async () => {
- // Run all operations in parallel for better performance
await Promise.all([
loadTemplates(),
- workflowTemplatesStore.loadWorkflowTemplates()
+ workflowTemplatesStore.loadWorkflowTemplates(),
+ loadFuseOptions()
])
return true
},
diff --git a/src/composables/useTemplateFiltering.test.ts b/src/composables/useTemplateFiltering.test.ts
index 5f30e5ec1..f6e617cb7 100644
--- a/src/composables/useTemplateFiltering.test.ts
+++ b/src/composables/useTemplateFiltering.test.ts
@@ -1,6 +1,7 @@
import { createPinia, setActivePinia } from 'pinia'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { nextTick, ref } from 'vue'
+import type { IFuseOptions } from 'fuse.js'
import type { TemplateInfo } from '@/platform/workflow/templates/types/template'
@@ -42,6 +43,13 @@ vi.mock('@/platform/telemetry', () => ({
}))
}))
+const mockGetFuseOptions = vi.hoisted(() => vi.fn())
+vi.mock('@/scripts/api', () => ({
+ api: {
+ getFuseOptions: mockGetFuseOptions
+ }
+}))
+
const { useTemplateFiltering } =
await import('@/composables/useTemplateFiltering')
@@ -49,6 +57,7 @@ describe('useTemplateFiltering', () => {
beforeEach(() => {
setActivePinia(createPinia())
vi.clearAllMocks()
+ mockGetFuseOptions.mockResolvedValue(null)
})
afterEach(() => {
@@ -272,4 +281,118 @@ describe('useTemplateFiltering', () => {
'beta-pro'
])
})
+
+ describe('loadFuseOptions', () => {
+ it('updates fuseOptions when getFuseOptions returns valid options', async () => {
+ const templates = ref([
+ {
+ name: 'test-template',
+ description: 'Test template',
+ mediaType: 'image',
+ mediaSubtype: 'png'
+ }
+ ])
+
+ const customFuseOptions: IFuseOptions = {
+ keys: [
+ { name: 'name', weight: 0.5 },
+ { name: 'description', weight: 0.5 }
+ ],
+ threshold: 0.4,
+ includeScore: true
+ }
+
+ mockGetFuseOptions.mockResolvedValueOnce(customFuseOptions)
+
+ const { loadFuseOptions, filteredTemplates } =
+ useTemplateFiltering(templates)
+
+ await loadFuseOptions()
+
+ expect(mockGetFuseOptions).toHaveBeenCalledTimes(1)
+ expect(filteredTemplates.value).toBeDefined()
+ })
+
+ it('does not update fuseOptions when getFuseOptions returns null', async () => {
+ const templates = ref([
+ {
+ name: 'test-template',
+ description: 'Test template',
+ mediaType: 'image',
+ mediaSubtype: 'png'
+ }
+ ])
+
+ mockGetFuseOptions.mockResolvedValueOnce(null)
+
+ const { loadFuseOptions, filteredTemplates } =
+ useTemplateFiltering(templates)
+
+ const initialResults = filteredTemplates.value
+
+ await loadFuseOptions()
+
+ expect(mockGetFuseOptions).toHaveBeenCalledTimes(1)
+ expect(filteredTemplates.value).toEqual(initialResults)
+ })
+
+ it('handles errors when getFuseOptions fails', async () => {
+ const templates = ref([
+ {
+ name: 'test-template',
+ description: 'Test template',
+ mediaType: 'image',
+ mediaSubtype: 'png'
+ }
+ ])
+
+ mockGetFuseOptions.mockRejectedValueOnce(new Error('Network error'))
+
+ const { loadFuseOptions, filteredTemplates } =
+ useTemplateFiltering(templates)
+
+ const initialResults = filteredTemplates.value
+
+ await expect(loadFuseOptions()).rejects.toThrow('Network error')
+ expect(filteredTemplates.value).toEqual(initialResults)
+ })
+
+ it('recreates Fuse instance when fuseOptions change', async () => {
+ const templates = ref([
+ {
+ name: 'searchable-template',
+ description: 'This is a searchable template',
+ mediaType: 'image',
+ mediaSubtype: 'png'
+ },
+ {
+ name: 'another-template',
+ description: 'Another template',
+ mediaType: 'image',
+ mediaSubtype: 'png'
+ }
+ ])
+
+ const { loadFuseOptions, searchQuery, filteredTemplates } =
+ useTemplateFiltering(templates)
+
+ const customFuseOptions = {
+ keys: [{ name: 'name', weight: 1.0 }],
+ threshold: 0.2,
+ includeScore: true,
+ includeMatches: true
+ }
+
+ mockGetFuseOptions.mockResolvedValueOnce(customFuseOptions)
+
+ await loadFuseOptions()
+ await nextTick()
+
+ searchQuery.value = 'searchable'
+ await nextTick()
+
+ expect(filteredTemplates.value.length).toBeGreaterThan(0)
+ expect(mockGetFuseOptions).toHaveBeenCalledTimes(1)
+ })
+ })
})
diff --git a/src/composables/useTemplateFiltering.ts b/src/composables/useTemplateFiltering.ts
index fdb892e07..9dcdeffcc 100644
--- a/src/composables/useTemplateFiltering.ts
+++ b/src/composables/useTemplateFiltering.ts
@@ -1,5 +1,6 @@
import { refDebounced, watchDebounced } from '@vueuse/core'
import Fuse from 'fuse.js'
+import type { IFuseOptions } from 'fuse.js'
import { computed, ref, watch } from 'vue'
import type { Ref } from 'vue'
@@ -8,6 +9,21 @@ import { useTelemetry } from '@/platform/telemetry'
import type { TemplateInfo } from '@/platform/workflow/templates/types/template'
import { useTemplateRankingStore } from '@/stores/templateRankingStore'
import { debounce } from 'es-toolkit/compat'
+import { api } from '@/scripts/api'
+
+// Fuse.js configuration for fuzzy search
+const defaultFuseOptions: IFuseOptions = {
+ keys: [
+ { name: 'name', weight: 0.3 },
+ { name: 'title', weight: 0.3 },
+ { name: 'description', weight: 0.1 },
+ { name: 'tags', weight: 0.2 },
+ { name: 'models', weight: 0.3 }
+ ],
+ threshold: 0.33,
+ includeScore: true,
+ includeMatches: true
+}
export function useTemplateFiltering(
templates: Ref | TemplateInfo[]
@@ -35,26 +51,14 @@ export function useTemplateFiltering(
| 'model-size-low-to-high'
>(settingStore.get('Comfy.Templates.SortBy'))
+ const fuseOptions = ref>(defaultFuseOptions)
+
const templatesArray = computed(() => {
const templateData = 'value' in templates ? templates.value : templates
return Array.isArray(templateData) ? templateData : []
})
- // Fuse.js configuration for fuzzy search
- const fuseOptions = {
- keys: [
- { name: 'name', weight: 0.3 },
- { name: 'title', weight: 0.3 },
- { name: 'description', weight: 0.1 },
- { name: 'tags', weight: 0.2 },
- { name: 'models', weight: 0.3 }
- ],
- threshold: 0.33,
- includeScore: true,
- includeMatches: true
- }
-
- const fuse = computed(() => new Fuse(templatesArray.value, fuseOptions))
+ const fuse = computed(() => new Fuse(templatesArray.value, fuseOptions.value))
const availableModels = computed(() => {
const modelSet = new Set()
@@ -272,6 +276,13 @@ export function useTemplateFiltering(
})
}, 500)
+ const loadFuseOptions = async () => {
+ const fetchedOptions = await api.getFuseOptions()
+ if (fetchedOptions) {
+ fuseOptions.value = fetchedOptions
+ }
+ }
+
// Watch for filter changes and track them
watch(
[searchQuery, selectedModels, selectedUseCases, selectedRunsOn, sortBy],
@@ -344,6 +355,7 @@ export function useTemplateFiltering(
resetFilters,
removeModelFilter,
removeUseCaseFilter,
- removeRunsOnFilter
+ removeRunsOnFilter,
+ loadFuseOptions
}
}
diff --git a/src/platform/workflow/templates/repositories/workflowTemplatesStore.ts b/src/platform/workflow/templates/repositories/workflowTemplatesStore.ts
index 8ce653d4d..f96872f79 100644
--- a/src/platform/workflow/templates/repositories/workflowTemplatesStore.ts
+++ b/src/platform/workflow/templates/repositories/workflowTemplatesStore.ts
@@ -1,4 +1,3 @@
-import Fuse from 'fuse.js'
import { defineStore } from 'pinia'
import { computed, ref, shallowRef } from 'vue'
@@ -250,24 +249,6 @@ export const useWorkflowTemplatesStore = defineStore(
return filteredTemplates
})
- /**
- * Fuse.js instance for advanced template searching and filtering
- */
- const templateFuse = computed(() => {
- const fuseOptions = {
- keys: [
- { name: 'searchableText', weight: 0.4 },
- { name: 'title', weight: 0.3 },
- { name: 'name', weight: 0.2 },
- { name: 'tags', weight: 0.1 }
- ],
- threshold: 0.3,
- includeScore: true
- }
-
- return new Fuse(enhancedTemplates.value, fuseOptions)
- })
-
/**
* Filter templates by category ID using stored filter mappings
*/
@@ -548,7 +529,6 @@ export const useWorkflowTemplatesStore = defineStore(
groupedTemplates,
navGroupedTemplates,
enhancedTemplates,
- templateFuse,
filterTemplatesByCategory,
isLoaded,
loadWorkflowTemplates,
diff --git a/src/scripts/api.ts b/src/scripts/api.ts
index 3061f74bd..822773c0d 100644
--- a/src/scripts/api.ts
+++ b/src/scripts/api.ts
@@ -10,7 +10,10 @@ import type {
} from '@/platform/assets/schemas/assetSchema'
import { isCloud } from '@/platform/distribution/types'
import { useToastStore } from '@/platform/updates/common/toastStore'
-import { type WorkflowTemplates } from '@/platform/workflow/templates/types/template'
+import {
+ type TemplateInfo,
+ type WorkflowTemplates
+} from '@/platform/workflow/templates/types/template'
import type {
ComfyApiWorkflow,
ComfyWorkflowJSON,
@@ -51,6 +54,7 @@ import type { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
import type { AuthHeader } from '@/types/authTypes'
import type { NodeExecutionId } from '@/types/nodeIdentification'
import { fetchHistory } from '@/platform/remote/comfyui/history'
+import type { IFuseOptions } from 'fuse.js'
interface QueuePromptRequestBody {
client_id: string
@@ -1269,6 +1273,29 @@ export class ComfyApi extends EventTarget {
}
}
+ /**
+ * Gets the Fuse options from the server.
+ *
+ * @returns The Fuse options, or null if not found or invalid
+ */
+ async getFuseOptions(): Promise | null> {
+ try {
+ const res = await axios.get(
+ this.fileURL('/templates/fuse_options.json'),
+ {
+ headers: {
+ 'Content-Type': 'application/json'
+ }
+ }
+ )
+ const contentType = res.headers['content-type']
+ return contentType?.includes('application/json') ? res.data : null
+ } catch (error) {
+ console.error('Error loading fuse options:', error)
+ return null
+ }
+ }
+
/**
* Gets the custom nodes i18n data from the server.
*
From 23e9b395937e51b3a941b8a4c8549d76dc14bb90 Mon Sep 17 00:00:00 2001
From: newraina
Date: Sun, 11 Jan 2026 14:37:50 +0800
Subject: [PATCH 14/63] fix: context menu appears at wrong position on first
click after canvas move (#7821)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## Summary
Fixed context menu positioning bug where menu appeared below mouse
cursor on first right-click after moving canvas, causing viewport
overflow.
## Changes
Initialize lastScale/lastOffset* to current canvas transform values when
opening menu, preventing updateMenuPosition from overwriting PrimeVue's
flip-adjusted position on the first RAF tick.
## Review Focus
Fixes #7666
## Screenshots (if applicable)
Please pay attention to the first right-click in each video — that’s
where the fix makes a difference.
**Before**
https://github.com/user-attachments/assets/29621621-a05e-414a-a4cc-5aa5a31b5041
**After**
https://github.com/user-attachments/assets/5f46aa69-97a0-44a4-9894-b205fe3d58ed
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7821-fix-context-menu-appears-at-wrong-position-on-first-click-after-canvas-move-2db6d73d365081e4a8ebc0d91e3f927b)
by [Unito](https://www.unito.io)
Co-authored-by: Alexander Brown
---
src/components/graph/NodeContextMenu.vue | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/src/components/graph/NodeContextMenu.vue b/src/components/graph/NodeContextMenu.vue
index 8382a43ec..97ee1b9c9 100644
--- a/src/components/graph/NodeContextMenu.vue
+++ b/src/components/graph/NodeContextMenu.vue
@@ -220,6 +220,12 @@ function show(event: MouseEvent) {
y: screenY / scale - offset[1]
}
+ // Initialize last* values to current transform to prevent updateMenuPosition
+ // from overwriting PrimeVue's flip-adjusted position on the first RAF tick
+ lastScale = scale
+ lastOffsetX = offset[0]
+ lastOffsetY = offset[1]
+
isOpen.value = true
contextMenu.value?.show(event)
}
From 44c317fd05f12b13a42a6fd265d2573366cddb01 Mon Sep 17 00:00:00 2001
From: AustinMroz
Date: Sat, 10 Jan 2026 23:01:34 -0800
Subject: [PATCH 15/63] Fix reactivity washing in refreshNodeSlots (#7802)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Creating a copy with spread resulted in a copy which was not reactive.
Solves a bug where all widgets on a node in vue mode would cease to be
reactive after any connection is made to the node.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7802-Fix-reactivity-washing-in-refreshNodeSlots-2d96d73d3650819e842ff46030bebfa1)
by [Unito](https://www.unito.io)
---------
Co-authored-by: Alexander Brown
---
src/composables/graph/useGraphNodeManager.ts | 13 ++---
.../graph/useGraphNodeManager.test.ts | 49 +++++++++++++++++++
2 files changed, 52 insertions(+), 10 deletions(-)
create mode 100644 tests-ui/tests/composables/graph/useGraphNodeManager.test.ts
diff --git a/src/composables/graph/useGraphNodeManager.ts b/src/composables/graph/useGraphNodeManager.ts
index 68e74c0b5..786e23f4e 100644
--- a/src/composables/graph/useGraphNodeManager.ts
+++ b/src/composables/graph/useGraphNodeManager.ts
@@ -245,17 +245,10 @@ export function useGraphNodeManager(graph: LGraph): GraphNodeManager {
})
// Update only widgets with new slot metadata, keeping other widget data intact
- const updatedWidgets = currentData.widgets?.map((widget) => {
+ for (const widget of currentData.widgets ?? []) {
const slotInfo = slotMetadata.get(widget.name)
- return slotInfo ? { ...widget, slotMetadata: slotInfo } : widget
- })
-
- vueNodeData.set(nodeId, {
- ...currentData,
- widgets: updatedWidgets,
- inputs: nodeRef.inputs ? [...nodeRef.inputs] : undefined,
- outputs: nodeRef.outputs ? [...nodeRef.outputs] : undefined
- })
+ if (slotInfo) widget.slotMetadata = slotInfo
+ }
}
// Extract safe data from LiteGraph node for Vue consumption
diff --git a/tests-ui/tests/composables/graph/useGraphNodeManager.test.ts b/tests-ui/tests/composables/graph/useGraphNodeManager.test.ts
new file mode 100644
index 000000000..ba22d98cd
--- /dev/null
+++ b/tests-ui/tests/composables/graph/useGraphNodeManager.test.ts
@@ -0,0 +1,49 @@
+import { setActivePinia } from 'pinia'
+import { createTestingPinia } from '@pinia/testing'
+import { describe, expect, it, vi } from 'vitest'
+import { nextTick, watch } from 'vue'
+
+import { useGraphNodeManager } from '@/composables/graph/useGraphNodeManager'
+import { LGraph, LGraphNode } from '@/lib/litegraph/src/litegraph'
+import { NodeSlotType } from '@/lib/litegraph/src/types/globalEnums'
+
+setActivePinia(createTestingPinia())
+
+function createTestGraph() {
+ const graph = new LGraph()
+ const node = new LGraphNode('test')
+ node.addInput('input', 'INT')
+ node.addWidget('number', 'testnum', 2, () => undefined, {})
+ graph.add(node)
+
+ const { vueNodeData } = useGraphNodeManager(graph)
+ const onReactivityUpdate = vi.fn()
+ watch(vueNodeData, onReactivityUpdate)
+
+ return [node, graph, onReactivityUpdate] as const
+}
+
+describe('Node Reactivity', () => {
+ it('should trigger on callback', async () => {
+ const [node, , onReactivityUpdate] = createTestGraph()
+
+ node.widgets![0].callback!(2)
+ await nextTick()
+ expect(onReactivityUpdate).toHaveBeenCalledTimes(1)
+ })
+
+ it('should remain reactive after a connection is made', async () => {
+ const [node, graph, onReactivityUpdate] = createTestGraph()
+
+ graph.trigger('node:slot-links:changed', {
+ nodeId: '1',
+ slotType: NodeSlotType.INPUT
+ })
+ await nextTick()
+ onReactivityUpdate.mockClear()
+
+ node.widgets![0].callback!(2)
+ await nextTick()
+ expect(onReactivityUpdate).toHaveBeenCalledTimes(1)
+ })
+})
From 7b274b74f15edf696aafa15fb4571301934e35a1 Mon Sep 17 00:00:00 2001
From: Godwin Iheuwa
Date: Sun, 11 Jan 2026 12:41:08 +0530
Subject: [PATCH 16/63] fix: add beforeChange/afterChange to convertToSubgraph
for proper undo (#7791)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## Summary
- Adds missing `beforeChange()` and `afterChange()` lifecycle calls to
`convertToSubgraph()` method
## Problem
When converting nodes to a subgraph and then pressing Ctrl+Z to undo,
the node positions were being changed from their original locations
instead of being properly restored.
## Root Cause
The `convertToSubgraph()` method in `LGraph.ts` was missing the
`beforeChange()` and `afterChange()` lifecycle calls that are needed for
proper undo/redo state tracking. These calls record the graph state
before and after modifications.
The inverse operation `unpackSubgraph()` already has these calls (see
line 1742), so this is simply matching the existing pattern.
## Solution
Add `beforeChange()` at the start of the method (after validation) and
`afterChange()` before the return.
## Testing
1. Create a workflow with several nodes positioned in specific locations
2. Select 2-3 nodes
3. Right-click → "Convert Selection to Subgraph"
4. Press Ctrl+Z to undo
5. Verify nodes return to their exact original positions
Fixes comfyanonymous/ComfyUI#11514
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7791-fix-add-beforeChange-afterChange-to-convertToSubgraph-for-proper-undo-2d86d73d36508125a2c4e4a412cced4a)
by [Unito](https://www.unito.io)
---------
Co-authored-by: RUiNtheExtinct
---
src/lib/litegraph/src/LGraph.ts | 34 +++++++++++++++++++++++++++++++--
1 file changed, 32 insertions(+), 2 deletions(-)
diff --git a/src/lib/litegraph/src/LGraph.ts b/src/lib/litegraph/src/LGraph.ts
index b3ad35c99..32494c615 100644
--- a/src/lib/litegraph/src/LGraph.ts
+++ b/src/lib/litegraph/src/LGraph.ts
@@ -1522,6 +1522,22 @@ export class LGraph
} {
if (items.size === 0)
throw new Error('Cannot convert to subgraph: nothing to convert')
+
+ // Record state before conversion for proper undo support
+ this.beforeChange()
+
+ try {
+ return this._convertToSubgraphImpl(items)
+ } finally {
+ // Mark state change complete for proper undo support
+ this.afterChange()
+ }
+ }
+
+ private _convertToSubgraphImpl(items: Set): {
+ subgraph: Subgraph
+ node: SubgraphNode
+ } {
const { state, revision, config } = this
const firstChild = [...items][0]
if (items.size === 1 && firstChild instanceof LGraphGroup) {
@@ -1728,6 +1744,7 @@ export class LGraph
subgraphNode._setConcreteSlots()
subgraphNode.arrange()
+
this.canvasAction((c) =>
c.canvas.dispatchEvent(
new CustomEvent('subgraph-converted', {
@@ -1746,9 +1763,23 @@ export class LGraph
if (!(subgraphNode instanceof SubgraphNode))
throw new Error('Can only unpack Subgraph Nodes')
+ // Record state before unpacking for proper undo support
+ this.beforeChange()
+
+ try {
+ this._unpackSubgraphImpl(subgraphNode, options)
+ } finally {
+ // Mark state change complete for proper undo support
+ this.afterChange()
+ }
+ }
+
+ private _unpackSubgraphImpl(
+ subgraphNode: SubgraphNode,
+ options?: { skipMissingNodes?: boolean }
+ ) {
const skipMissingNodes = options?.skipMissingNodes ?? false
- this.beforeChange()
//NOTE: Create bounds can not be called on positionables directly as the subgraph is not being displayed and boundingRect is not initialized.
//NOTE: NODE_TITLE_HEIGHT is explicitly excluded here
const positionables = [
@@ -2019,7 +2050,6 @@ export class LGraph
}
this.canvasAction((c) => c.selectItems(toSelect))
- this.afterChange()
}
/**
From 97ca9f489e108b3fa7647b0349004578207cf988 Mon Sep 17 00:00:00 2001
From: pythongosssss <125205205+pythongosssss@users.noreply.github.com>
Date: Sun, 11 Jan 2026 07:11:50 +0000
Subject: [PATCH 17/63] Integrated tab bar UI elements (#7853)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## Summary
The current help / feedback is often overlooked by users, this adds a
setting that makes it more visible moving it up into the tab bar and
moves the user login/profile button out of the "action bar" into the tab
bar.
## Changes
- Add 'Comfy.UI.TabBarLayout' setting with Default/Integrated options
- Move Help & User controls to tab bar when Integrated mode is enabled
- Extract help center logic into shared useHelpCenter composable
## Screenshots (if applicable)
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7853-Integrated-tab-bar-UI-elements-2df6d73d365081b1beb8f7c641c2fa43)
by [Unito](https://www.unito.io)
---------
Co-authored-by: GitHub Action
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
---
src/components/TopMenuSection.vue | 12 +-
.../helpcenter/HelpCenterPopups.vue | 134 ++++++++++++
src/components/sidebar/SideToolbar.vue | 7 +-
.../sidebar/SidebarHelpCenterIcon.vue | 200 ++----------------
src/components/topbar/CurrentUserButton.vue | 17 +-
src/components/topbar/LoginButton.vue | 16 +-
src/components/topbar/TopMenuHelpButton.vue | 21 ++
src/components/topbar/WorkflowTabs.vue | 24 +++
src/composables/useHelpCenter.ts | 100 +++++++++
src/locales/en/main.json | 1 +
src/locales/en/settings.json | 8 +
.../settings/constants/coreSettings.ts | 10 +
src/schemas/apiSchema.ts | 1 +
src/stores/helpCenterStore.ts | 12 +-
14 files changed, 364 insertions(+), 199 deletions(-)
create mode 100644 src/components/helpcenter/HelpCenterPopups.vue
create mode 100644 src/components/topbar/TopMenuHelpButton.vue
create mode 100644 src/composables/useHelpCenter.ts
diff --git a/src/components/TopMenuSection.vue b/src/components/TopMenuSection.vue
index 7cdd5418a..28f2411ea 100644
--- a/src/components/TopMenuSection.vue
+++ b/src/components/TopMenuSection.vue
@@ -59,8 +59,11 @@
{{ queuedCount }}
-
-
+
+
+
@@ -54,6 +55,7 @@ import { useResizeObserver } from '@vueuse/core'
import { debounce } from 'es-toolkit/compat'
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
+import HelpCenterPopups from '@/components/helpcenter/HelpCenterPopups.vue'
import ComfyMenuButton from '@/components/sidebar/ComfyMenuButton.vue'
import SidebarBottomPanelToggleButton from '@/components/sidebar/SidebarBottomPanelToggleButton.vue'
import SidebarSettingsButton from '@/components/sidebar/SidebarSettingsButton.vue'
@@ -89,6 +91,9 @@ const sidebarLocation = computed<'left' | 'right'>(() =>
settingStore.get('Comfy.Sidebar.Location')
)
const sidebarStyle = computed(() => settingStore.get('Comfy.Sidebar.Style'))
+const isIntegratedTabBar = computed(
+ () => settingStore.get('Comfy.UI.TabBarLayout') === 'Integrated'
+)
const isConnected = computed(
() =>
selectedTab.value ||
diff --git a/src/components/sidebar/SidebarHelpCenterIcon.vue b/src/components/sidebar/SidebarHelpCenterIcon.vue
index b8d8cea39..b140a1c53 100644
--- a/src/components/sidebar/SidebarHelpCenterIcon.vue
+++ b/src/components/sidebar/SidebarHelpCenterIcon.vue
@@ -1,204 +1,28 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
diff --git a/src/renderer/extensions/vueNodes/widgets/components/WidgetWithControl.vue b/src/renderer/extensions/vueNodes/widgets/components/WidgetWithControl.vue
index 4c2b42b85..f571d6dad 100644
--- a/src/renderer/extensions/vueNodes/widgets/components/WidgetWithControl.vue
+++ b/src/renderer/extensions/vueNodes/widgets/components/WidgetWithControl.vue
@@ -50,7 +50,7 @@ const togglePopover = (event: Event) => {
variant="textonly"
size="sm"
class="h-4 w-7 self-center rounded-xl bg-blue-100/30 p-0"
- @pointerdown.stop.prevent="togglePopover"
+ @click.stop.prevent="togglePopover"
>
From 20d06f92ca4e6437099122a60c3b6aa9dc94e81b Mon Sep 17 00:00:00 2001
From: AustinMroz
Date: Tue, 13 Jan 2026 15:31:55 -0800
Subject: [PATCH 32/63] Fix error on close of combo dropdown (#7804)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Apparently, enabling autofocus causes primevue to also attempt focusing
the searchbox one frame after closing.
I have no idea why this behaviour would ever be beneficial, but this PR
adds an override to prevent this behaviour and clean up the console
spam.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7804-Fix-error-on-close-of-combo-dropdown-2d96d73d365081f8b677e3e4aaadb51b)
by [Unito](https://www.unito.io)
---------
Co-authored-by: Alexander Brown
---
.../primevueOverride/SelectPlus.vue | 19 +++++++++++++++++++
.../widgets/components/WidgetSelect.test.ts | 19 +++++++++----------
.../components/WidgetSelectDefault.vue | 6 +++---
3 files changed, 31 insertions(+), 13 deletions(-)
create mode 100644 src/components/primevueOverride/SelectPlus.vue
diff --git a/src/components/primevueOverride/SelectPlus.vue b/src/components/primevueOverride/SelectPlus.vue
new file mode 100644
index 000000000..dc678dc34
--- /dev/null
+++ b/src/components/primevueOverride/SelectPlus.vue
@@ -0,0 +1,19 @@
+
diff --git a/src/renderer/extensions/vueNodes/widgets/components/WidgetSelect.test.ts b/src/renderer/extensions/vueNodes/widgets/components/WidgetSelect.test.ts
index 4d52d47e2..9505840db 100644
--- a/src/renderer/extensions/vueNodes/widgets/components/WidgetSelect.test.ts
+++ b/src/renderer/extensions/vueNodes/widgets/components/WidgetSelect.test.ts
@@ -1,13 +1,12 @@
import { createTestingPinia } from '@pinia/testing'
import { mount } from '@vue/test-utils'
import PrimeVue from 'primevue/config'
-import Select from 'primevue/select'
import type { SelectProps } from 'primevue/select'
import { beforeEach, describe, expect, it, vi } from 'vitest'
+import SelectPlus from '@/components/primevueOverride/SelectPlus.vue'
import type { ComboInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
-
import WidgetSelect from '@/renderer/extensions/vueNodes/widgets/components/WidgetSelect.vue'
import WidgetSelectDefault from '@/renderer/extensions/vueNodes/widgets/components/WidgetSelectDefault.vue'
import WidgetSelectDropdown from '@/renderer/extensions/vueNodes/widgets/components/WidgetSelectDropdown.vue'
@@ -76,7 +75,7 @@ describe('WidgetSelect Value Binding', () => {
},
global: {
plugins: [PrimeVue, createTestingPinia()],
- components: { Select }
+ components: { SelectPlus }
}
})
}
@@ -85,7 +84,7 @@ describe('WidgetSelect Value Binding', () => {
wrapper: ReturnType,
value: string
) => {
- const select = wrapper.findComponent({ name: 'Select' })
+ const select = wrapper.findComponent({ name: 'SelectPlus' })
await select.setValue(value)
return wrapper.emitted('update:modelValue')
}
@@ -150,7 +149,7 @@ describe('WidgetSelect Value Binding', () => {
const widget = createMockWidget('', { values: [] })
const wrapper = mountComponent(widget, '')
- const select = wrapper.findComponent({ name: 'Select' })
+ const select = wrapper.findComponent({ name: 'SelectPlus' })
expect(select.props('options')).toEqual([])
})
@@ -160,7 +159,7 @@ describe('WidgetSelect Value Binding', () => {
})
const wrapper = mountComponent(widget, 'only_option')
- const select = wrapper.findComponent({ name: 'Select' })
+ const select = wrapper.findComponent({ name: 'SelectPlus' })
const options = select.props('options')
expect(options).toHaveLength(1)
expect(options[0]).toEqual('only_option')
@@ -228,7 +227,7 @@ describe('WidgetSelect Value Binding', () => {
},
global: {
plugins: [PrimeVue, createTestingPinia()],
- components: { Select }
+ components: { SelectPlus }
}
})
@@ -247,7 +246,7 @@ describe('WidgetSelect Value Binding', () => {
},
global: {
plugins: [PrimeVue, createTestingPinia()],
- components: { Select }
+ components: { SelectPlus }
}
})
@@ -271,7 +270,7 @@ describe('WidgetSelect Value Binding', () => {
},
global: {
plugins: [PrimeVue, createTestingPinia()],
- components: { Select }
+ components: { SelectPlus }
}
})
@@ -290,7 +289,7 @@ describe('WidgetSelect Value Binding', () => {
},
global: {
plugins: [PrimeVue, createTestingPinia()],
- components: { Select }
+ components: { SelectPlus }
}
})
diff --git a/src/renderer/extensions/vueNodes/widgets/components/WidgetSelectDefault.vue b/src/renderer/extensions/vueNodes/widgets/components/WidgetSelectDefault.vue
index e996c944e..93428aab5 100644
--- a/src/renderer/extensions/vueNodes/widgets/components/WidgetSelectDefault.vue
+++ b/src/renderer/extensions/vueNodes/widgets/components/WidgetSelectDefault.vue
@@ -1,10 +1,10 @@
-
@@ -111,12 +125,14 @@ const { assets, isSelected } = defineProps<{
const emit = defineEmits<{
(e: 'select-asset', asset: AssetItem): void
+ (e: 'context-menu', event: MouseEvent, asset: AssetItem): void
(e: 'approach-end'): void
}>()
const { t } = useI18n()
const { jobItems } = useJobList()
const hoveredJobId = ref