Compare commits

...

6 Commits

Author SHA1 Message Date
snomiao
2691df34ec chore: enhance telemetry CI scan to detect Mixpanel and Impact
Expand the existing CI telemetry scan workflow to detect additional
telemetry libraries beyond Google Tag Manager.

Added detection patterns for:
- Mixpanel (mixpanel-browser, api/cdn domains, init/track/identify methods)
- Impact Analytics (impactcdn.com, tracking ID)

Also improved error messaging to:
- List all telemetry providers being checked
- Provide troubleshooting guidance
- Reference PR #8311 for historical context

This prevents accidental inclusion of telemetry code in OSS builds,
similar to the GTM incident in PR #8311.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-14 12:56:47 +00:00
Christian Byrne
5f7a6e7aba fix: clear draft on workflow close to prevent stale state on reopen (#8854)
## Summary

Clear the workflow draft from localStorage when any workflow tab is
closed, preventing stale cached state from being served when the
workflow is re-opened.

## Changes

- **What**: `closeWorkflow()` in `workflowStore.ts` now calls
`removeDraft()` for all workflows, not just temporary ones.
`closeWorkflow()` in `workflowService.ts` removes the draft before
switching tabs, preventing `beforeLoadNewGraph()` from re-saving it.

## Review Focus

- Draft is removed before the tab switch in
`workflowService.closeWorkflow()` to prevent `beforeLoadNewGraph()` from
re-saving it during the switch
- Crash recovery is preserved: drafts are only cleared on explicit
close, not on unload/crash
- Tab restore on restart is unaffected: drafts for intentionally-open
tabs are saved on graph change events, not on close

Fixes #8778
Fixes https://github.com/Comfy-Org/ComfyUI/issues/12323

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8854-fix-clear-draft-on-workflow-close-to-prevent-stale-state-on-reopen-3066d73d365081a2a633c9b352d0b0d1)
by [Unito](https://www.unito.io)
2026-02-14 02:50:05 -08:00
Comfy Org PR Bot
2c07bedbb1 1.40.3 (#8859)
Patch version increment to 1.40.3

**Base branch:** `main`

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8859-1-40-3-3076d73d36508130ab36d2b00a9fb1f3)
by [Unito](https://www.unito.io)

---------

Co-authored-by: christian-byrne <72887196+christian-byrne@users.noreply.github.com>
Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: Alexander Brown <drjkl@comfy.org>
2026-02-14 02:41:47 -08:00
Terry Jia
78635294ce feat: add hideOutputImages flag for nodes with custom preview (#8857)
## Summary

Prerequisite for upcoming native color correction nodes (ColorCorrect,
ColorBalance, ColorCurves) which render their own WebGL preview and need
to suppress the default output image display while keeping data in the
store for downstream nodes.

## Screenshots (if applicable)
before
<img width="980" height="1580" alt="image"
src="https://github.com/user-attachments/assets/2e08869f-1cad-4637-8174-96d034da524c"
/>

after
<img width="783" height="1276" alt="image"
src="https://github.com/user-attachments/assets/3f9b50ee-268c-48f4-9e63-89ef8d732157"
/>

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8857-feat-add-hideOutputImages-flag-for-nodes-with-custom-preview-3076d73d365081a2aa55d34280601b47)
by [Unito](https://www.unito.io)

Co-authored-by: Alexander Brown <drjkl@comfy.org>
2026-02-14 03:55:36 -05:00
Alexander Brown
2f09c6321e Regenerate images (#8866)
```
```

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8866-Regenerate-images-3076d73d365081018009ff8e79c5a418)
by [Unito](https://www.unito.io)

---------

Co-authored-by: github-actions <github-actions@github.com>
2026-02-14 00:43:04 -08:00
Christian Byrne
38edba7024 fix: exclude missing assets from cloud mode dropdown (COM-14333) (#8747)
## Summary

Fixes a bug where non-existent images appeared in the asset search
dropdown when loading workflows that reference images the user doesn't
have in cloud mode.

## Changes

- Add `displayItems` prop to `FormDropdown` and `FormDropdownInput` for
showing selected values that aren't in the dropdown list
- Exclude `missingValueItem` from cloud asset mode `dropdownItems` while
still displaying it in the input field via `displayItems`
- Use localized error messages in `ImagePreview` for missing images
(`g.imageDoesNotExist`, `g.unknownFile`)
- Add tests for cloud asset mode behavior in
`WidgetSelectDropdown.test.ts`

## Context

The `missingValueItem` was originally added in PR #8276 for template
workflows. This fix keeps that behavior for local mode but excludes it
from cloud asset mode dropdown. Cloud users can't access files they
don't own, so showing them as search results causes confusion.

## Testing

- Added unit tests for cloud asset mode behavior
- Verified existing tests pass
- All quality gates pass: typecheck, lint, format, tests

Fixes COM-14333



┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8747-fix-exclude-missing-assets-from-cloud-mode-dropdown-COM-14333-3016d73d365081e3ab47c326d791257e)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Alexander Brown <drjkl@comfy.org>
Co-authored-by: github-actions <github-actions@github.com>
2026-02-13 14:30:55 -08:00
27 changed files with 273 additions and 20 deletions

View File

@@ -45,8 +45,29 @@ jobs:
-e '(?i)\bgtm\.js\b' \
-e '(?i)googletagmanager\.com/gtm\.js\\?id=' \
-e '(?i)googletagmanager\.com/ns\.html\\?id=' \
-e 'mixpanel-browser' \
-e '(?i)api\.mixpanel\.com' \
-e '(?i)cdn\.mxpnl\.com' \
-e '(?i)mixpanel\.init' \
-e '(?i)mixpanel\.track' \
-e '(?i)mixpanel\.identify' \
-e '(?i)mixpanel\.people' \
-e '(?i)impactcdn\.com' \
-e 'A6951770-3747-434a-9ac7-4e582e67d91f1' \
dist; then
echo 'Telemetry references found in dist assets.'
echo 'Telemetry references found in dist assets.'
echo ''
echo 'This CI check scans for telemetry libraries that should not be included in OSS builds:'
echo ' - Google Tag Manager (GTM)'
echo ' - Mixpanel'
echo ' - Impact Analytics'
echo ''
echo 'If you see this error:'
echo ' 1. Check your build configuration to ensure telemetry code is properly excluded'
echo ' 2. Verify conditional imports are working correctly'
echo ' 3. Review the matched lines above to identify the source'
echo ''
echo 'For context, see PR #8311 which accidentally shipped GTM code to OSS builds.'
exit 1
fi
echo 'No telemetry references found in dist assets.'
echo 'No telemetry references found in dist assets.'

Binary file not shown.

Before

Width:  |  Height:  |  Size: 79 KiB

After

Width:  |  Height:  |  Size: 80 KiB

View File

@@ -1,6 +1,6 @@
{
"name": "@comfyorg/comfyui-frontend",
"version": "1.40.2",
"version": "1.40.3",
"private": true,
"description": "Official front-end implementation of ComfyUI",
"homepage": "https://comfy.org",

View File

@@ -847,6 +847,7 @@
"hideLeftPanel": "إخفاء اللوحة اليسرى",
"hideRightPanel": "إخفاء اللوحة اليمنى",
"icon": "أيقونة",
"imageDoesNotExist": "الصورة غير موجودة",
"imageFailedToLoad": "فشل تحميل الصورة",
"imagePreview": "معاينة الصورة - استخدم مفاتيح الأسهم للتنقل بين الصور",
"imageUrl": "رابط الصورة",
@@ -996,6 +997,7 @@
"title": "العنوان",
"triggerPhrase": "عبارة التشغيل",
"unknownError": "خطأ غير معروف",
"unknownFile": "ملف غير معروف",
"untitled": "بدون عنوان",
"update": "تحديث",
"updateAvailable": "تحديث متاح",

View File

@@ -94,6 +94,8 @@
"openNewIssue": "Open New Issue",
"showReport": "Show Report",
"imageFailedToLoad": "Image failed to load",
"imageDoesNotExist": "Image does not exist",
"unknownFile": "Unknown file",
"reconnecting": "Reconnecting",
"reconnected": "Reconnected",
"delete": "Delete",

View File

@@ -14727,7 +14727,7 @@
},
"offloading": {
"name": "offloading",
"tooltip": "Depth level for gradient checkpointing."
"tooltip": "Offload the Model to RAM. Requires Bypass Mode."
},
"existing_lora": {
"name": "existing_lora",

View File

@@ -847,6 +847,7 @@
"hideLeftPanel": "Ocultar panel izquierdo",
"hideRightPanel": "Ocultar panel derecho",
"icon": "Icono",
"imageDoesNotExist": "La imagen no existe",
"imageFailedToLoad": "Falló la carga de la imagen",
"imagePreview": "Vista previa de imagen - Usa las teclas de flecha para navegar entre imágenes",
"imageUrl": "URL de la imagen",
@@ -996,6 +997,7 @@
"title": "Título",
"triggerPhrase": "Frase de activación",
"unknownError": "Error desconocido",
"unknownFile": "Archivo desconocido",
"untitled": "Sin título",
"update": "Actualizar",
"updateAvailable": "Actualización Disponible",

View File

@@ -847,6 +847,7 @@
"hideLeftPanel": "پنهان کردن پنل چپ",
"hideRightPanel": "پنهان کردن پنل راست",
"icon": "آیکون",
"imageDoesNotExist": "تصویر وجود ندارد",
"imageFailedToLoad": "بارگذاری تصویر ناموفق بود",
"imagePreview": "پیش‌نمایش تصویر - برای جابجایی بین تصاویر از کلیدهای جهت‌دار استفاده کنید",
"imageUrl": "آدرس تصویر",
@@ -996,6 +997,7 @@
"title": "عنوان",
"triggerPhrase": "عبارت trigger",
"unknownError": "خطای ناشناخته",
"unknownFile": "فایل ناشناخته",
"untitled": "بدون عنوان",
"update": "به‌روزرسانی",
"updateAvailable": "به‌روزرسانی موجود است",

View File

@@ -847,6 +847,7 @@
"hideLeftPanel": "Masquer le panneau de gauche",
"hideRightPanel": "Masquer le panneau de droite",
"icon": "Icône",
"imageDoesNotExist": "Limage nexiste pas",
"imageFailedToLoad": "Échec du chargement de l'image",
"imagePreview": "Aperçu de l'image - Utilisez les flèches pour naviguer entre les images",
"imageUrl": "URL de l'image",
@@ -996,6 +997,7 @@
"title": "Titre",
"triggerPhrase": "Phrase déclencheuse",
"unknownError": "Erreur inconnue",
"unknownFile": "Fichier inconnu",
"untitled": "Sans titre",
"update": "Mettre à jour",
"updateAvailable": "Mise à jour disponible",

View File

@@ -847,6 +847,7 @@
"hideLeftPanel": "左パネルを非表示",
"hideRightPanel": "右パネルを非表示",
"icon": "アイコン",
"imageDoesNotExist": "画像が存在しません",
"imageFailedToLoad": "画像の読み込みに失敗しました",
"imagePreview": "画像プレビュー - 矢印キーで画像を切り替え",
"imageUrl": "画像URL",
@@ -996,6 +997,7 @@
"title": "タイトル",
"triggerPhrase": "トリガーフレーズ",
"unknownError": "不明なエラー",
"unknownFile": "不明なファイル",
"untitled": "無題",
"update": "更新",
"updateAvailable": "更新が利用可能",

View File

@@ -847,6 +847,7 @@
"hideLeftPanel": "왼쪽 패널 숨기기",
"hideRightPanel": "오른쪽 패널 숨기기",
"icon": "아이콘",
"imageDoesNotExist": "이미지가 존재하지 않습니다",
"imageFailedToLoad": "이미지를 로드하지 못했습니다.",
"imagePreview": "이미지 미리보기 - 화살표 키를 사용하여 이미지 간 이동",
"imageUrl": "이미지 URL",
@@ -996,6 +997,7 @@
"title": "제목",
"triggerPhrase": "트리거 문구",
"unknownError": "알 수 없는 오류",
"unknownFile": "알 수 없는 파일",
"untitled": "제목 없음",
"update": "업데이트",
"updateAvailable": "업데이트 가능",

View File

@@ -847,6 +847,7 @@
"hideLeftPanel": "Ocultar painel esquerdo",
"hideRightPanel": "Ocultar painel direito",
"icon": "Ícone",
"imageDoesNotExist": "Imagem não existe",
"imageFailedToLoad": "Falha ao carregar imagem",
"imagePreview": "Pré-visualização da imagem - Use as setas para navegar entre as imagens",
"imageUrl": "URL da imagem",
@@ -996,6 +997,7 @@
"title": "Título",
"triggerPhrase": "Frase de gatilho",
"unknownError": "Erro desconhecido",
"unknownFile": "Arquivo desconhecido",
"untitled": "Sem título",
"update": "Atualizar",
"updateAvailable": "Atualização disponível",

View File

@@ -847,6 +847,7 @@
"hideLeftPanel": "Скрыть левую панель",
"hideRightPanel": "Скрыть правую панель",
"icon": "Иконка",
"imageDoesNotExist": "Изображение не существует",
"imageFailedToLoad": "Не удалось загрузить изображение",
"imagePreview": "Предварительный просмотр изображения - Используйте клавиши со стрелками для навигации между изображениями",
"imageUrl": "URL изображения",
@@ -996,6 +997,7 @@
"title": "Заголовок",
"triggerPhrase": "Триггерная фраза",
"unknownError": "Неизвестная ошибка",
"unknownFile": "Неизвестный файл",
"untitled": "Без названия",
"update": "Обновить",
"updateAvailable": "Доступно обновление",

View File

@@ -847,6 +847,7 @@
"hideLeftPanel": "Sol paneli gizle",
"hideRightPanel": "Sağ paneli gizle",
"icon": "Simge",
"imageDoesNotExist": "Görsel mevcut değil",
"imageFailedToLoad": "Görsel yüklenemedi",
"imagePreview": "Görüntü önizlemesi - Görüntüler arasında gezinmek için ok tuşlarını kullanın",
"imageUrl": "Görsel URL'si",
@@ -996,6 +997,7 @@
"title": "Başlık",
"triggerPhrase": "Tetikleyici ifade",
"unknownError": "Bilinmeyen hata",
"unknownFile": "Bilinmeyen dosya",
"untitled": "Başlıksız",
"update": "Güncelle",
"updateAvailable": "Güncelleme Mevcut",

View File

@@ -847,6 +847,7 @@
"hideLeftPanel": "隱藏左側面板",
"hideRightPanel": "隱藏右側面板",
"icon": "圖示",
"imageDoesNotExist": "圖像不存在",
"imageFailedToLoad": "無法載入圖片",
"imagePreview": "圖片預覽 - 使用方向鍵在圖片間導航",
"imageUrl": "圖片網址",
@@ -996,6 +997,7 @@
"title": "標題",
"triggerPhrase": "觸發詞",
"unknownError": "未知錯誤",
"unknownFile": "未知檔案",
"untitled": "未命名",
"update": "更新",
"updateAvailable": "有可用更新",

View File

@@ -847,6 +847,7 @@
"hideLeftPanel": "隐藏左侧面板",
"hideRightPanel": "隐藏右侧面板",
"icon": "图标",
"imageDoesNotExist": "图像不存在",
"imageFailedToLoad": "图像加载失败",
"imagePreview": "图片预览 - 使用方向键切换图片",
"imageUrl": "图片网址",
@@ -996,6 +997,7 @@
"title": "标题",
"triggerPhrase": "触发短语",
"unknownError": "未知错误",
"unknownFile": "未知文件",
"untitled": "无标题",
"update": "更新",
"updateAvailable": "有更新可用",

View File

@@ -215,6 +215,8 @@ export const useWorkflowService = () => {
}
}
workflowDraftStore.removeDraft(workflow.path)
// If this is the last workflow, create a new default temporary workflow
if (workflowStore.openWorkflows.length === 1) {
await loadDefaultWorkflow()

View File

@@ -12,6 +12,7 @@ import {
useWorkflowBookmarkStore,
useWorkflowStore
} from '@/platform/workflow/management/stores/workflowStore'
import { useWorkflowDraftStore } from '@/platform/workflow/persistence/stores/workflowDraftStore'
import { api } from '@/scripts/api'
import { app as comfyApp } from '@/scripts/app'
import { defaultGraph, defaultGraphJSON } from '@/scripts/defaultGraph'
@@ -911,4 +912,41 @@ describe('useWorkflowStore', () => {
expect(mostRecent).toBeNull()
})
})
describe('closeWorkflow draft cleanup', () => {
it('should remove draft for persisted workflows on close', async () => {
const draftStore = useWorkflowDraftStore()
await syncRemoteWorkflows(['a.json'])
const workflow = store.getWorkflowByPath('workflows/a.json')!
draftStore.saveDraft('workflows/a.json', {
data: '{"dirty":true}',
updatedAt: Date.now(),
name: 'a.json',
isTemporary: false
})
expect(draftStore.getDraft('workflows/a.json')).toBeDefined()
await store.closeWorkflow(workflow)
expect(draftStore.getDraft('workflows/a.json')).toBeUndefined()
})
it('should remove draft for temporary workflows on close', async () => {
const draftStore = useWorkflowDraftStore()
const workflow = store.createTemporary('temp.json')
draftStore.saveDraft(workflow.path, {
data: '{"dirty":true}',
updatedAt: Date.now(),
name: 'temp.json',
isTemporary: true
})
expect(draftStore.getDraft(workflow.path)).toBeDefined()
await store.closeWorkflow(workflow)
expect(draftStore.getDraft(workflow.path)).toBeUndefined()
})
})
})

View File

@@ -320,11 +320,9 @@ export const useWorkflowStore = defineStore('workflow', () => {
openWorkflowPaths.value = openWorkflowPaths.value.filter(
(path) => path !== workflow.path
)
useWorkflowDraftStore().removeDraft(workflow.path)
if (workflow.isTemporary) {
// Clear thumbnail when temporary workflow is closed
clearThumbnail(workflow.key)
// Clear draft when unsaved workflow tab is closed
useWorkflowDraftStore().removeDraft(workflow.path)
delete workflowLookup.value[workflow.path]
} else {
workflow.unload()

View File

@@ -29,6 +29,8 @@ const i18n = createI18n({
failedToDownloadImage: 'Failed to download image',
calculatingDimensions: 'Calculating dimensions',
imageFailedToLoad: 'Image failed to load',
imageDoesNotExist: 'Image does not exist',
unknownFile: 'Unknown file',
loading: 'Loading'
}
}

View File

@@ -321,10 +321,11 @@ const handleKeyDown = (event: KeyboardEvent) => {
}
const getImageFilename = (url: string): string => {
if (!url) return t('g.imageDoesNotExist')
try {
return new URL(url).searchParams.get('filename') || 'Unknown file'
return new URL(url).searchParams.get('filename') || t('g.unknownFile')
} catch {
return 'Invalid URL'
return t('g.imageDoesNotExist')
}
}
</script>

View File

@@ -656,7 +656,8 @@ const nodeMedia = computed(() => {
const newOutputs = nodeOutputs.nodeOutputs[nodeOutputLocatorId.value]
const node = lgraphNode.value
if (!node || !newOutputs?.images?.length) return undefined
if (!node || !newOutputs?.images?.length || node.hideOutputImages)
return undefined
const urls = nodeOutputs.getNodeImageUrls(node)
if (!urls?.length) return undefined

View File

@@ -2,16 +2,31 @@ import { createTestingPinia } from '@pinia/testing'
import { mount } from '@vue/test-utils'
import type { VueWrapper } from '@vue/test-utils'
import PrimeVue from 'primevue/config'
import { computed } from 'vue'
import type { ComponentPublicInstance } from 'vue'
import { describe, expect, it, vi } from 'vitest'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { createI18n } from 'vue-i18n'
import type { AssetItem } from '@/platform/assets/schemas/assetSchema'
import type { FormDropdownItem } from '@/renderer/extensions/vueNodes/widgets/components/form/dropdown/types'
import type { ComboInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
import WidgetSelectDropdown from '@/renderer/extensions/vueNodes/widgets/components/WidgetSelectDropdown.vue'
const mockAssetsData = vi.hoisted(() => ({ items: [] as AssetItem[] }))
vi.mock(
'@/renderer/extensions/vueNodes/widgets/composables/useAssetWidgetData',
() => ({
useAssetWidgetData: () => ({
category: computed(() => 'checkpoints'),
assets: computed(() => mockAssetsData.items),
isLoading: computed(() => false),
error: computed(() => null)
})
})
)
const i18n = createI18n({
legacy: false,
locale: 'en',
@@ -306,3 +321,133 @@ describe('WidgetSelectDropdown custom label mapping', () => {
})
})
})
describe('WidgetSelectDropdown cloud asset mode (COM-14333)', () => {
interface CloudModeInstance extends ComponentPublicInstance {
dropdownItems: FormDropdownItem[]
displayItems: FormDropdownItem[]
selectedSet: Set<string>
}
const createTestAsset = (
id: string,
name: string,
preview_url: string
): AssetItem => ({
id,
name,
preview_url,
tags: []
})
const createCloudModeWidget = (
value: string = 'model.safetensors'
): SimplifiedWidget<string | undefined> => ({
name: 'test_model_select',
type: 'combo',
value,
options: {
values: [],
nodeType: 'CheckpointLoaderSimple'
}
})
const mountCloudComponent = (
widget: SimplifiedWidget<string | undefined>,
modelValue: string | undefined
): VueWrapper<CloudModeInstance> => {
return mount(WidgetSelectDropdown, {
props: {
widget,
modelValue,
assetKind: 'model',
isAssetMode: true,
nodeType: 'CheckpointLoaderSimple'
},
global: {
plugins: [PrimeVue, createTestingPinia(), i18n]
}
}) as unknown as VueWrapper<CloudModeInstance>
}
beforeEach(() => {
mockAssetsData.items = []
})
it('does not include missing items in cloud asset mode dropdown', () => {
mockAssetsData.items = [
createTestAsset(
'asset-1',
'existing_model.safetensors',
'https://example.com/preview.jpg'
)
]
const widget = createCloudModeWidget('missing_model.safetensors')
const wrapper = mountCloudComponent(widget, 'missing_model.safetensors')
const dropdownItems = wrapper.vm.dropdownItems
expect(dropdownItems).toHaveLength(1)
expect(dropdownItems[0].name).toBe('existing_model.safetensors')
expect(
dropdownItems.some((item) => item.name === 'missing_model.safetensors')
).toBe(false)
})
it('shows only available cloud assets in dropdown', () => {
mockAssetsData.items = [
createTestAsset(
'asset-1',
'model_a.safetensors',
'https://example.com/a.jpg'
),
createTestAsset(
'asset-2',
'model_b.safetensors',
'https://example.com/b.jpg'
)
]
const widget = createCloudModeWidget('model_a.safetensors')
const wrapper = mountCloudComponent(widget, 'model_a.safetensors')
const dropdownItems = wrapper.vm.dropdownItems
expect(dropdownItems).toHaveLength(2)
expect(dropdownItems.map((item) => item.name)).toEqual([
'model_a.safetensors',
'model_b.safetensors'
])
})
it('returns empty dropdown when no cloud assets available', () => {
mockAssetsData.items = []
const widget = createCloudModeWidget('missing_model.safetensors')
const wrapper = mountCloudComponent(widget, 'missing_model.safetensors')
const dropdownItems = wrapper.vm.dropdownItems
expect(dropdownItems).toHaveLength(0)
})
it('includes missing cloud asset in displayItems for input field visibility', () => {
mockAssetsData.items = [
createTestAsset(
'asset-1',
'existing_model.safetensors',
'https://example.com/preview.jpg'
)
]
const widget = createCloudModeWidget('missing_model.safetensors')
const wrapper = mountCloudComponent(widget, 'missing_model.safetensors')
const displayItems = wrapper.vm.displayItems
expect(displayItems).toHaveLength(2)
expect(displayItems[0].name).toBe('missing_model.safetensors')
expect(displayItems[0].id).toBe('missing-missing_model.safetensors')
expect(displayItems[1].name).toBe('existing_model.safetensors')
const selectedSet = wrapper.vm.selectedSet
expect(selectedSet.has('missing-missing_model.safetensors')).toBe(true)
})
})

View File

@@ -254,9 +254,8 @@ const baseModelFilteredAssetItems = computed<FormDropdownItem[]>(() =>
const allItems = computed<FormDropdownItem[]>(() => {
if (props.isAssetMode && assetData) {
if (missingValueItem.value) {
return [missingValueItem.value, ...baseModelFilteredAssetItems.value]
}
// Cloud assets not in user's library shouldn't appear as search results (COM-14333).
// Unlike local mode, cloud users can't access files they don't own.
return baseModelFilteredAssetItems.value
}
return [
@@ -282,6 +281,17 @@ const dropdownItems = computed<FormDropdownItem[]>(() => {
}
})
/**
* Items used for display in the input field. In cloud mode, includes
* missing items so users can see their selected value even if not in library.
*/
const displayItems = computed<FormDropdownItem[]>(() => {
if (props.isAssetMode && assetData && missingValueItem.value) {
return [missingValueItem.value, ...baseModelFilteredAssetItems.value]
}
return dropdownItems.value
})
const mediaPlaceholder = computed(() => {
const options = props.widget.options
@@ -332,18 +342,20 @@ const acceptTypes = computed(() => {
const layoutMode = ref<LayoutMode>(props.defaultLayoutMode ?? 'grid')
watch(
[modelValue, dropdownItems],
([currentValue, _dropdownItems]) => {
[modelValue, displayItems],
([currentValue]) => {
if (currentValue === undefined) {
selectedSet.value.clear()
return
}
const item = dropdownItems.value.find((item) => item.name === currentValue)
if (item) {
const item = displayItems.value.find((item) => item.name === currentValue)
if (!item) {
selectedSet.value.clear()
selectedSet.value.add(item.id)
return
}
selectedSet.value.clear()
selectedSet.value.add(item.id)
},
{ immediate: true }
)
@@ -461,6 +473,7 @@ function getMediaUrl(
v-model:ownership-selected="ownershipSelected"
v-model:base-model-selected="baseModelSelected"
:items="dropdownItems"
:display-items="displayItems"
:placeholder="mediaPlaceholder"
:multiple="false"
:uploadable

View File

@@ -18,6 +18,8 @@ import type { FormDropdownItem, LayoutMode, SortOption } from './types'
interface Props {
items: FormDropdownItem[]
/** Items used for display in the input field. Falls back to items if not provided. */
displayItems?: FormDropdownItem[]
placeholder?: string
/**
* If true, allows multiple selections. If a number is provided,
@@ -193,6 +195,7 @@ async function customSearcher(
:is-open
:placeholder="placeholderText"
:items
:display-items
:max-selectable
:selected
:uploadable

View File

@@ -10,6 +10,8 @@ interface Props {
isOpen?: boolean
placeholder?: string
items: FormDropdownItem[]
/** Items used for display in the input field. Falls back to items if not provided. */
displayItems?: FormDropdownItem[]
selected: Set<string>
maxSelectable: number
uploadable: boolean
@@ -28,7 +30,8 @@ const emit = defineEmits<{
}>()
const selectedItems = computed(() => {
return props.items.filter((item) => props.selected.has(item.id))
const itemsToSearch = props.displayItems ?? props.items
return itemsToSearch.filter((item) => props.selected.has(item.id))
})
const theButtonStyle = computed(() =>

View File

@@ -177,6 +177,8 @@ declare module '@/lib/litegraph/src/litegraph' {
isLoading?: boolean
/** The content type of the node's preview media */
previewMediaType?: 'image' | 'video' | 'audio' | 'model'
/** If true, output images are stored but not rendered below the node */
hideOutputImages?: boolean
preview: string[]
/** Index of the currently selected image on a multi-image node such as Preview Image */