mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-22 23:39:45 +00:00
Compare commits
6 Commits
drjkl/esli
...
chore/enha
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2691df34ec | ||
|
|
5f7a6e7aba | ||
|
|
2c07bedbb1 | ||
|
|
78635294ce | ||
|
|
2f09c6321e | ||
|
|
38edba7024 |
25
.github/workflows/ci-dist-telemetry-scan.yaml
vendored
25
.github/workflows/ci-dist-telemetry-scan.yaml
vendored
@@ -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 |
@@ -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",
|
||||
|
||||
@@ -847,6 +847,7 @@
|
||||
"hideLeftPanel": "إخفاء اللوحة اليسرى",
|
||||
"hideRightPanel": "إخفاء اللوحة اليمنى",
|
||||
"icon": "أيقونة",
|
||||
"imageDoesNotExist": "الصورة غير موجودة",
|
||||
"imageFailedToLoad": "فشل تحميل الصورة",
|
||||
"imagePreview": "معاينة الصورة - استخدم مفاتيح الأسهم للتنقل بين الصور",
|
||||
"imageUrl": "رابط الصورة",
|
||||
@@ -996,6 +997,7 @@
|
||||
"title": "العنوان",
|
||||
"triggerPhrase": "عبارة التشغيل",
|
||||
"unknownError": "خطأ غير معروف",
|
||||
"unknownFile": "ملف غير معروف",
|
||||
"untitled": "بدون عنوان",
|
||||
"update": "تحديث",
|
||||
"updateAvailable": "تحديث متاح",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -847,6 +847,7 @@
|
||||
"hideLeftPanel": "پنهان کردن پنل چپ",
|
||||
"hideRightPanel": "پنهان کردن پنل راست",
|
||||
"icon": "آیکون",
|
||||
"imageDoesNotExist": "تصویر وجود ندارد",
|
||||
"imageFailedToLoad": "بارگذاری تصویر ناموفق بود",
|
||||
"imagePreview": "پیشنمایش تصویر - برای جابجایی بین تصاویر از کلیدهای جهتدار استفاده کنید",
|
||||
"imageUrl": "آدرس تصویر",
|
||||
@@ -996,6 +997,7 @@
|
||||
"title": "عنوان",
|
||||
"triggerPhrase": "عبارت trigger",
|
||||
"unknownError": "خطای ناشناخته",
|
||||
"unknownFile": "فایل ناشناخته",
|
||||
"untitled": "بدون عنوان",
|
||||
"update": "بهروزرسانی",
|
||||
"updateAvailable": "بهروزرسانی موجود است",
|
||||
|
||||
@@ -847,6 +847,7 @@
|
||||
"hideLeftPanel": "Masquer le panneau de gauche",
|
||||
"hideRightPanel": "Masquer le panneau de droite",
|
||||
"icon": "Icône",
|
||||
"imageDoesNotExist": "L’image n’existe 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",
|
||||
|
||||
@@ -847,6 +847,7 @@
|
||||
"hideLeftPanel": "左パネルを非表示",
|
||||
"hideRightPanel": "右パネルを非表示",
|
||||
"icon": "アイコン",
|
||||
"imageDoesNotExist": "画像が存在しません",
|
||||
"imageFailedToLoad": "画像の読み込みに失敗しました",
|
||||
"imagePreview": "画像プレビュー - 矢印キーで画像を切り替え",
|
||||
"imageUrl": "画像URL",
|
||||
@@ -996,6 +997,7 @@
|
||||
"title": "タイトル",
|
||||
"triggerPhrase": "トリガーフレーズ",
|
||||
"unknownError": "不明なエラー",
|
||||
"unknownFile": "不明なファイル",
|
||||
"untitled": "無題",
|
||||
"update": "更新",
|
||||
"updateAvailable": "更新が利用可能",
|
||||
|
||||
@@ -847,6 +847,7 @@
|
||||
"hideLeftPanel": "왼쪽 패널 숨기기",
|
||||
"hideRightPanel": "오른쪽 패널 숨기기",
|
||||
"icon": "아이콘",
|
||||
"imageDoesNotExist": "이미지가 존재하지 않습니다",
|
||||
"imageFailedToLoad": "이미지를 로드하지 못했습니다.",
|
||||
"imagePreview": "이미지 미리보기 - 화살표 키를 사용하여 이미지 간 이동",
|
||||
"imageUrl": "이미지 URL",
|
||||
@@ -996,6 +997,7 @@
|
||||
"title": "제목",
|
||||
"triggerPhrase": "트리거 문구",
|
||||
"unknownError": "알 수 없는 오류",
|
||||
"unknownFile": "알 수 없는 파일",
|
||||
"untitled": "제목 없음",
|
||||
"update": "업데이트",
|
||||
"updateAvailable": "업데이트 가능",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -847,6 +847,7 @@
|
||||
"hideLeftPanel": "Скрыть левую панель",
|
||||
"hideRightPanel": "Скрыть правую панель",
|
||||
"icon": "Иконка",
|
||||
"imageDoesNotExist": "Изображение не существует",
|
||||
"imageFailedToLoad": "Не удалось загрузить изображение",
|
||||
"imagePreview": "Предварительный просмотр изображения - Используйте клавиши со стрелками для навигации между изображениями",
|
||||
"imageUrl": "URL изображения",
|
||||
@@ -996,6 +997,7 @@
|
||||
"title": "Заголовок",
|
||||
"triggerPhrase": "Триггерная фраза",
|
||||
"unknownError": "Неизвестная ошибка",
|
||||
"unknownFile": "Неизвестный файл",
|
||||
"untitled": "Без названия",
|
||||
"update": "Обновить",
|
||||
"updateAvailable": "Доступно обновление",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -847,6 +847,7 @@
|
||||
"hideLeftPanel": "隱藏左側面板",
|
||||
"hideRightPanel": "隱藏右側面板",
|
||||
"icon": "圖示",
|
||||
"imageDoesNotExist": "圖像不存在",
|
||||
"imageFailedToLoad": "無法載入圖片",
|
||||
"imagePreview": "圖片預覽 - 使用方向鍵在圖片間導航",
|
||||
"imageUrl": "圖片網址",
|
||||
@@ -996,6 +997,7 @@
|
||||
"title": "標題",
|
||||
"triggerPhrase": "觸發詞",
|
||||
"unknownError": "未知錯誤",
|
||||
"unknownFile": "未知檔案",
|
||||
"untitled": "未命名",
|
||||
"update": "更新",
|
||||
"updateAvailable": "有可用更新",
|
||||
|
||||
@@ -847,6 +847,7 @@
|
||||
"hideLeftPanel": "隐藏左侧面板",
|
||||
"hideRightPanel": "隐藏右侧面板",
|
||||
"icon": "图标",
|
||||
"imageDoesNotExist": "图像不存在",
|
||||
"imageFailedToLoad": "图像加载失败",
|
||||
"imagePreview": "图片预览 - 使用方向键切换图片",
|
||||
"imageUrl": "图片网址",
|
||||
@@ -996,6 +997,7 @@
|
||||
"title": "标题",
|
||||
"triggerPhrase": "触发短语",
|
||||
"unknownError": "未知错误",
|
||||
"unknownFile": "未知文件",
|
||||
"untitled": "无标题",
|
||||
"update": "更新",
|
||||
"updateAvailable": "有更新可用",
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(() =>
|
||||
|
||||
2
src/types/litegraph-augmentation.d.ts
vendored
2
src/types/litegraph-augmentation.d.ts
vendored
@@ -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 */
|
||||
|
||||
Reference in New Issue
Block a user