Compare commits

..

2 Commits

Author SHA1 Message Date
bymyself
b2c1f1f51a fix: forward watcher cleanup into searcher
Addresses review feedback:
https://github.com/Comfy-Org/ComfyUI_frontend/pull/9551#discussion_r2901111651
2026-03-07 18:39:05 -08:00
bymyself
eb1153d836 fix: update FormDropdown filteredItems when items prop changes
Replace computed itemsKey with a Symbol ref and add a watcher that
re-runs the searcher whenever the items prop changes. This fixes
the Load Video node not showing videos in the Media Assets panel
dropdown when switching between asset types with identical or empty
item sets.
2026-03-07 16:21:38 -08:00
77 changed files with 373 additions and 201 deletions

View File

@@ -4,7 +4,7 @@
<template v-if="filter.tasks.length === 0">
<!-- Empty filter -->
<Divider />
<p class="w-full text-center text-neutral-400">
<p class="text-neutral-400 w-full text-center">
{{ $t('maintenance.allOk') }}
</p>
</template>
@@ -25,7 +25,7 @@
<!-- Display: Cards -->
<template v-else>
<div class="pad-y my-4 flex flex-wrap justify-evenly gap-8">
<div class="flex flex-wrap justify-evenly gap-8 pad-y my-4">
<TaskCard
v-for="task in filter.tasks"
:key="task.id"
@@ -45,8 +45,7 @@ import { useConfirm, useToast } from 'primevue'
import ConfirmPopup from 'primevue/confirmpopup'
import Divider from 'primevue/divider'
import { useI18n } from 'vue-i18n'
import { t } from '@/i18n'
import { useMaintenanceTaskStore } from '@/stores/maintenanceTaskStore'
import type {
MaintenanceFilter,
@@ -56,7 +55,6 @@ import type {
import TaskCard from './TaskCard.vue'
import TaskListItem from './TaskListItem.vue'
const { t } = useI18n()
const toast = useToast()
const confirm = useConfirm()
const taskStore = useMaintenanceTaskStore()
@@ -82,7 +80,8 @@ const executeTask = async (task: MaintenanceTask) => {
toast.add({
severity: 'error',
summary: t('maintenance.error.toastTitle'),
detail: message ?? t('maintenance.error.defaultDescription')
detail: message ?? t('maintenance.error.defaultDescription'),
life: 10_000
})
}

View File

@@ -189,7 +189,8 @@ const completeValidation = async () => {
toast.add({
severity: 'error',
summary: t('g.error'),
detail: t('maintenance.error.cannotContinue')
detail: t('maintenance.error.cannotContinue'),
life: 5_000
})
}
}

View File

@@ -1,8 +1,8 @@
<template>
<BaseViewTemplate dark hide-language-selector>
<div class="flex h-full flex-col items-center justify-center p-8 2xl:p-16">
<div class="h-full p-8 2xl:p-16 flex flex-col items-center justify-center">
<div
class="flex w-full max-w-[600px] flex-col gap-6 rounded-lg bg-neutral-800 p-6 shadow-lg"
class="bg-neutral-800 rounded-lg shadow-lg p-6 w-full max-w-[600px] flex flex-col gap-6"
>
<h2 class="text-3xl font-semibold text-neutral-100">
{{ $t('install.helpImprove') }}
@@ -15,7 +15,7 @@
<a
href="https://comfy.org/privacy"
target="_blank"
class="text-blue-400 underline hover:text-blue-300"
class="text-blue-400 hover:text-blue-300 underline"
>
{{ $t('install.privacyPolicy') }} </a
>.
@@ -33,7 +33,7 @@
}}
</span>
</div>
<div class="flex justify-end pt-6">
<div class="flex pt-6 justify-end">
<Button
:label="$t('g.ok')"
icon="pi pi-check"
@@ -72,7 +72,8 @@ const updateConsent = async () => {
toast.add({
severity: 'error',
summary: t('install.settings.errorUpdatingConsent'),
detail: t('install.settings.errorUpdatingConsentDetail')
detail: t('install.settings.errorUpdatingConsentDetail'),
life: 3000
})
} finally {
isUpdating.value = false

View File

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

View File

@@ -10,6 +10,7 @@ import PropertiesAccordionItem from '@/components/rightSidePanel/layout/Properti
import WidgetItem from '@/components/rightSidePanel/parameters/WidgetItem.vue'
import { LiteGraph } from '@/lib/litegraph/src/litegraph'
import type { LGraphNode, NodeId } from '@/lib/litegraph/src/LGraphNode'
import type { INodeInputSlot } from '@/lib/litegraph/src/interfaces'
import type { LGraphCanvas } from '@/lib/litegraph/src/LGraphCanvas'
import {
LGraphEventMode,
@@ -24,7 +25,7 @@ import { useCanvasInteractions } from '@/renderer/core/canvas/useCanvasInteracti
import TransformPane from '@/renderer/core/layout/transform/TransformPane.vue'
import { app } from '@/scripts/app'
import { DOMWidgetImpl } from '@/scripts/domWidget'
import { promptRenameWidget } from '@/utils/widgetUtil'
import { useDialogService } from '@/services/dialogService'
import { useAppMode } from '@/composables/useAppMode'
import { nodeTypeValidForApp, useAppModeStore } from '@/stores/appModeStore'
import { resolveNode } from '@/utils/litegraphUtil'
@@ -72,12 +73,15 @@ const inputsWithState = computed(() =>
}
}
const input = node.inputs.find((i) => i.widget?.name === widget.name)
const rename = input && (() => renameWidget(widget, input))
return {
nodeId,
widgetName,
label: widget.label,
subLabel: node.title,
rename: () => promptRenameWidget(widget, node, t)
rename
}
})
)
@@ -88,6 +92,20 @@ const outputsWithState = computed<[NodeId, string][]>(() =>
])
)
async function renameWidget(widget: IBaseWidget, input: INodeInputSlot) {
const newLabel = await useDialogService().prompt({
title: t('g.rename'),
message: t('g.enterNewNamePrompt'),
defaultValue: widget.label,
placeholder: widget.name
})
if (newLabel === null) return
widget.label = newLabel || undefined
input.label = newLabel || undefined
widget.callback?.(widget.value)
useCanvasStore().canvas?.setDirty(true)
}
function getHovered(
e: MouseEvent
): undefined | [LGraphNode, undefined] | [LGraphNode, IBaseWidget] {
@@ -248,7 +266,7 @@ const renderedInputs = computed<[string, MaybeRef<BoundStyle> | undefined][]>(
<template #label>
<div class="flex gap-3">
{{ t('nodeHelpPage.inputs') }}
<i class="icon-[lucide--info] bg-muted-foreground" />
<i class="icon-[lucide--circle-alert] bg-muted-foreground" />
</div>
</template>
<template #empty>
@@ -303,7 +321,7 @@ const renderedInputs = computed<[string, MaybeRef<BoundStyle> | undefined][]>(
<template #label>
<div class="flex gap-3">
{{ t('nodeHelpPage.outputs') }}
<i class="icon-[lucide--info] bg-muted-foreground" />
<i class="icon-[lucide--circle-alert] bg-muted-foreground" />
</div>
</template>
<template #empty>

View File

@@ -138,7 +138,8 @@ onMounted(async () => {
toast.add({
severity: 'error',
summary: t('g.error'),
detail: t('toastMessages.failedToFetchLogs')
detail: t('toastMessages.failedToFetchLogs'),
life: 5000
})
}
})

View File

@@ -275,7 +275,8 @@ async function handleBuy() {
toast.add({
severity: 'error',
summary: t('credits.topUp.purchaseError'),
detail: t('credits.topUp.purchaseErrorDetail', { error: errorMessage })
detail: t('credits.topUp.purchaseErrorDetail', { error: errorMessage }),
life: 5000
})
} finally {
loading.value = false

View File

@@ -98,7 +98,8 @@ async function onConfirmCancel() {
toast.add({
severity: 'error',
summary: t('subscription.cancelDialog.failed'),
detail: error instanceof Error ? error.message : t('g.unknownError')
detail: error instanceof Error ? error.message : t('g.unknownError'),
life: 5000
})
} finally {
isLoading.value = false

View File

@@ -579,7 +579,8 @@ const onUpdateComfyUI = async (): Promise<void> => {
toast.add({
severity: 'error',
summary: t('g.error'),
detail: error.value || t('helpCenter.updateComfyUIFailed')
detail: error.value || t('helpCenter.updateComfyUIFailed'),
life: 5000
})
return
}
@@ -596,7 +597,8 @@ const onUpdateComfyUI = async (): Promise<void> => {
toast.add({
severity: 'error',
summary: t('g.error'),
detail: err instanceof Error ? err.message : t('g.unknownError')
detail: err instanceof Error ? err.message : t('g.unknownError'),
life: 5000
})
}
}

View File

@@ -15,9 +15,10 @@ import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
import type { SubgraphNode } from '@/lib/litegraph/src/subgraph/SubgraphNode'
import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets'
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
import { useDialogService } from '@/services/dialogService'
import { useNodeDefStore } from '@/stores/nodeDefStore'
import { useFavoritedWidgetsStore } from '@/stores/workspace/favoritedWidgetsStore'
import { getWidgetDefaultValue, promptWidgetLabel } from '@/utils/widgetUtil'
import { getWidgetDefaultValue } from '@/utils/widgetUtil'
import type { WidgetValue } from '@/utils/widgetUtil'
const {
@@ -41,6 +42,7 @@ const label = defineModel<string>('label', { required: true })
const canvasStore = useCanvasStore()
const favoritedWidgetsStore = useFavoritedWidgetsStore()
const nodeDefStore = useNodeDefStore()
const dialogService = useDialogService()
const { t } = useI18n()
const hasParents = computed(() => parents?.length > 0)
@@ -65,8 +67,15 @@ const isCurrentValueDefault = computed(() => {
})
async function handleRename() {
const newLabel = await promptWidgetLabel(widget, t)
if (newLabel !== null) label.value = newLabel
const newLabel = await dialogService.prompt({
title: t('g.rename'),
message: t('g.enterNewNamePrompt'),
defaultValue: widget.label,
placeholder: widget.name
})
if (newLabel === null) return
label.value = newLabel
}
function handleHideInput() {

View File

@@ -615,7 +615,8 @@ const enterFolderView = async (asset: AssetItem) => {
toast.add({
severity: 'error',
summary: t('sideToolbar.folderView.errorSummary'),
detail: t('sideToolbar.folderView.errorDetail')
detail: t('sideToolbar.folderView.errorDetail'),
life: 5000
})
exitFolderView()
}
@@ -661,7 +662,8 @@ const copyJobId = async () => {
toast.add({
severity: 'error',
summary: t('mediaAsset.jobIdToast.error'),
detail: t('mediaAsset.jobIdToast.jobIdCopyFailed')
detail: t('mediaAsset.jobIdToast.jobIdCopyFailed'),
life: 3000
})
}
}

View File

@@ -10,9 +10,8 @@
@mouseup="handleMouseUp"
@click="handleClick"
>
<i v-if="isBuilderState" class="bg-text-subtle icon-[lucide--hammer]" />
<i
v-else-if="workflowOption.workflow.initialMode === 'app'"
v-if="workflowOption.workflow.initialMode === 'app'"
class="icon-[lucide--panels-top-left] bg-primary-background"
/>
<span
@@ -150,11 +149,6 @@ const shouldShowStatusIndicator = computed(() => {
return false
})
const isBuilderState = computed(() => {
const currentMode = props.workflowOption.workflow.activeMode
return typeof currentMode === 'string' && currentMode.startsWith('builder:')
})
const isActiveTab = computed(() => {
return workflowStore.activeWorkflow?.key === props.workflowOption.workflow.key
})

View File

@@ -397,7 +397,8 @@ export function useCoreCommands(): ComfyCommand[] {
if (app.canvas.empty) {
toastStore.add({
severity: 'error',
summary: t('toastMessages.emptyCanvas')
summary: t('toastMessages.emptyCanvas'),
life: 3000
})
return
}
@@ -556,7 +557,8 @@ export function useCoreCommands(): ComfyCommand[] {
toastStore.add({
severity: 'error',
summary: t('toastMessages.nothingToQueue'),
detail: t('toastMessages.pleaseSelectOutputNodes')
detail: t('toastMessages.pleaseSelectOutputNodes'),
life: 3000
})
return
}
@@ -569,7 +571,8 @@ export function useCoreCommands(): ComfyCommand[] {
toastStore.add({
severity: 'error',
summary: t('toastMessages.failedToQueue'),
detail: t('toastMessages.failedExecutionPathResolution')
detail: t('toastMessages.failedExecutionPathResolution'),
life: 3000
})
return
}
@@ -599,7 +602,8 @@ export function useCoreCommands(): ComfyCommand[] {
toastStore.add({
severity: 'error',
summary: t('toastMessages.nothingToGroup'),
detail: t('toastMessages.pleaseSelectNodesToGroup')
detail: t('toastMessages.pleaseSelectNodesToGroup'),
life: 3000
})
return
}
@@ -958,7 +962,8 @@ export function useCoreCommands(): ComfyCommand[] {
toastStore.add({
severity: 'error',
summary: t('g.error'),
detail: t('manager.notAvailable')
detail: t('manager.notAvailable'),
life: 3000
})
return
}
@@ -1043,7 +1048,8 @@ export function useCoreCommands(): ComfyCommand[] {
toastStore.add({
severity: 'error',
summary: t('toastMessages.cannotCreateSubgraph'),
detail: t('toastMessages.failedToConvertToSubgraph')
detail: t('toastMessages.failedToConvertToSubgraph'),
life: 3000
})
return
}
@@ -1252,7 +1258,8 @@ export function useCoreCommands(): ComfyCommand[] {
summary: t('g.error'),
detail: t('g.commandProhibited', {
command: 'Comfy.Memory.UnloadModels'
})
}),
life: 3000
})
return
}
@@ -1271,7 +1278,8 @@ export function useCoreCommands(): ComfyCommand[] {
summary: t('g.error'),
detail: t('g.commandProhibited', {
command: 'Comfy.Memory.UnloadModelsAndExecutionCache'
})
}),
life: 3000
})
return
}

View File

@@ -81,7 +81,8 @@ function getParentNodes(): SubgraphNode[] {
useToastStore().add({
severity: 'error',
summary: t('g.error'),
detail: t('subgraphStore.promoteOutsideSubgraph')
detail: t('subgraphStore.promoteOutsideSubgraph'),
life: 2000
})
return []
}

View File

@@ -204,7 +204,8 @@ import { electronAPI as getElectronAPI } from '@/utils/envUtil'
toastStore.add({
severity: 'error',
summary: t('g.error'),
detail: t('desktopUpdate.errorInstallingUpdate')
detail: t('desktopUpdate.errorInstallingUpdate'),
life: 10_000
})
}
}
@@ -213,7 +214,8 @@ import { electronAPI as getElectronAPI } from '@/utils/envUtil'
toastStore.add({
severity: 'error',
summary: t('g.error'),
detail: t('desktopUpdate.errorCheckingUpdate')
detail: t('desktopUpdate.errorCheckingUpdate'),
life: 10_000
})
}
}

View File

@@ -952,7 +952,6 @@
"collapseAll": "طي الكل",
"color": "اللون",
"comfy": "Comfy",
"comfyCloud": "Comfy Cloud",
"comfyOrgLogoAlt": "شعار ComfyOrg",
"comingSoon": "قريباً",
"command": "أمر",
@@ -1460,14 +1459,12 @@
"unknownWidget": "عنصر الواجهة غير مرئي"
},
"cancelThisRun": "إلغاء هذا التشغيل",
"deleteAllAssets": "حذف جميع الأصول من هذه الجلسة",
"downloadAll": "تنزيل الكل",
"dragAndDropImage": "اسحب وأسقط صورة",
"emptyWorkflowExplanation": "سير العمل الخاص بك فارغ. تحتاج إلى بعض العقد أولاً لبدء بناء التطبيق.",
"enterNodeGraph": "دخول مخطط العقد",
"giveFeedback": "إعطاء ملاحظات",
"graphMode": "وضع الرسم البياني",
"hasCreditCost": "يتطلب أرصدة إضافية",
"linearMode": "وضع التطبيق",
"loadTemplate": "تحميل قالب",
"mobileControls": "تعديل وتشغيل",
@@ -3376,7 +3373,6 @@
"addedToWorkspace": "تمت إضافتك إلى {workspaceName}",
"inviteAccepted": "تم قبول الدعوة",
"inviteFailed": "فشل في قبول الدعوة",
"switchFailed": "فشل في تبديل مساحة العمل. يرجى المحاولة مرة أخرى.",
"viewWorkspace": "عرض مساحة العمل"
},
"workspaceAuth": {

View File

@@ -400,7 +400,7 @@
"name": "تخطيط شريط التبويبات",
"options": {
"Default": "افتراضي",
"Legacy": "تقليدي"
"Integrated": "مُدمج"
},
"tooltip": "يتحكم في تخطيط شريط التبويبات. \"مُدمج\" ينقل عناصر المساعدة والتحكمات الخاصة بالمستخدم إلى منطقة شريط التبويبات."
},

View File

@@ -398,10 +398,10 @@
},
"Comfy_UI_TabBarLayout": {
"name": "Tab Bar Layout",
"tooltip": "Controls the elements contained in the integrated tab bar.",
"tooltip": "Controls the layout of the tab bar. \"Integrated\" moves Help and User controls into the tab bar area.",
"options": {
"Default": "Default",
"Legacy": "Legacy"
"Integrated": "Integrated"
}
},
"Comfy_UseNewMenu": {

View File

@@ -952,7 +952,6 @@
"collapseAll": "Colapsar todo",
"color": "Color",
"comfy": "Comfy",
"comfyCloud": "Comfy Cloud",
"comfyOrgLogoAlt": "Logo de ComfyOrg",
"comingSoon": "Próximamente",
"command": "Comando",
@@ -1460,14 +1459,12 @@
"unknownWidget": "Widget no visible"
},
"cancelThisRun": "Cancelar esta ejecución",
"deleteAllAssets": "Eliminar todos los recursos de esta ejecución",
"downloadAll": "Descargar todo",
"dragAndDropImage": "Arrastra y suelta una imagen",
"emptyWorkflowExplanation": "Tu flujo de trabajo está vacío. Necesitas algunos nodos primero para empezar a construir una aplicación.",
"enterNodeGraph": "Entrar al grafo de nodos",
"giveFeedback": "Enviar comentarios",
"graphMode": "Modo gráfico",
"hasCreditCost": "Requiere créditos adicionales",
"linearMode": "Modo App",
"loadTemplate": "Cargar una plantilla",
"mobileControls": "Editar y ejecutar",
@@ -3376,7 +3373,6 @@
"addedToWorkspace": "Has sido añadido a {workspaceName}",
"inviteAccepted": "Invitación aceptada",
"inviteFailed": "No se pudo aceptar la invitación",
"switchFailed": "No se pudo cambiar de espacio de trabajo. Por favor, inténtalo de nuevo.",
"viewWorkspace": "Ver espacio de trabajo"
},
"workspaceAuth": {

View File

@@ -400,7 +400,7 @@
"name": "Diseño de barra de pestañas",
"options": {
"Default": "Predeterminado",
"Legacy": "Clásico"
"Integrated": "Integrado"
},
"tooltip": "Controla el diseño de la barra de pestañas. \"Integrado\" mueve los controles de Ayuda y Usuario al área de la barra de pestañas."
},

View File

@@ -952,7 +952,6 @@
"collapseAll": "بستن همه",
"color": "رنگ",
"comfy": "Comfy",
"comfyCloud": "Comfy Cloud",
"comfyOrgLogoAlt": "لوگوی ComfyOrg",
"comingSoon": "به‌زودی",
"command": "دستور",
@@ -1460,14 +1459,12 @@
"unknownWidget": "ویجت قابل مشاهده نیست"
},
"cancelThisRun": "لغو این اجرا",
"deleteAllAssets": "حذف همه دارایی‌ها از این اجرا",
"downloadAll": "دانلود همه",
"dragAndDropImage": "تصویر را بکشید و رها کنید",
"emptyWorkflowExplanation": "جریان کاری شما خالی است. ابتدا باید چند node اضافه کنید تا بتوانید یک برنامه بسازید.",
"enterNodeGraph": "ورود به گراف node",
"giveFeedback": "ارسال بازخورد",
"graphMode": "حالت گراف",
"hasCreditCost": "نیازمند اعتبار اضافی",
"linearMode": "حالت برنامه",
"loadTemplate": "بارگذاری قالب",
"mobileControls": "ویرایش و اجرا",
@@ -3388,7 +3385,6 @@
"addedToWorkspace": "شما به {workspaceName} اضافه شدید",
"inviteAccepted": "دعوت پذیرفته شد",
"inviteFailed": "پذیرش دعوت ناموفق بود",
"switchFailed": "تغییر ورک‌اسپیس ناموفق بود. لطفاً دوباره تلاش کنید.",
"viewWorkspace": "مشاهده workspace"
},
"workspaceAuth": {

View File

@@ -400,7 +400,7 @@
"name": "چیدمان نوار تب",
"options": {
"Default": "پیش‌فرض",
"Legacy": "قدیمی"
"Integrated": "یکپارچه"
},
"tooltip": "چیدمان نوار تب را کنترل می‌کند. «یکپارچه» کنترل‌های راهنما و کاربر را به ناحیه نوار تب منتقل می‌کند."
},

View File

@@ -952,7 +952,6 @@
"collapseAll": "Tout réduire",
"color": "Couleur",
"comfy": "Comfy",
"comfyCloud": "Comfy Cloud",
"comfyOrgLogoAlt": "Logo ComfyOrg",
"comingSoon": "Bientôt disponible",
"command": "Commande",
@@ -1460,14 +1459,12 @@
"unknownWidget": "Widget non visible"
},
"cancelThisRun": "Annuler cette exécution",
"deleteAllAssets": "Supprimer toutes les ressources de cette exécution",
"downloadAll": "Tout télécharger",
"dragAndDropImage": "Glissez-déposez une image",
"emptyWorkflowExplanation": "Votre workflow est vide. Vous devez d'abord ajouter des nodes pour commencer à créer une application.",
"enterNodeGraph": "Entrer dans le graphique de nœuds",
"giveFeedback": "Donner un avis",
"graphMode": "Mode graphique",
"hasCreditCost": "Nécessite des crédits supplémentaires",
"linearMode": "Mode App",
"loadTemplate": "Charger un modèle",
"mobileControls": "Éditer & Exécuter",
@@ -3376,7 +3373,6 @@
"addedToWorkspace": "Vous avez été ajouté à {workspaceName}",
"inviteAccepted": "Invitation acceptée",
"inviteFailed": "Échec de l'acceptation de l'invitation",
"switchFailed": "Échec du changement despace de travail. Veuillez réessayer.",
"viewWorkspace": "Voir lespace de travail"
},
"workspaceAuth": {

View File

@@ -400,7 +400,7 @@
"name": "Disposition de la barre donglets",
"options": {
"Default": "Par défaut",
"Legacy": "Héritage"
"Integrated": "Intégrée"
},
"tooltip": "Contrôle la disposition de la barre donglets. « Intégrée » déplace les contrôles Aide et Utilisateur dans la zone de la barre donglets."
},

View File

@@ -952,7 +952,6 @@
"collapseAll": "すべて折りたたむ",
"color": "色",
"comfy": "Comfy",
"comfyCloud": "Comfy Cloud",
"comfyOrgLogoAlt": "ComfyOrgロゴ",
"comingSoon": "近日公開",
"command": "コマンド",
@@ -1460,14 +1459,12 @@
"unknownWidget": "ウィジェットが表示されていません"
},
"cancelThisRun": "この実行をキャンセル",
"deleteAllAssets": "この実行からすべてのアセットを削除",
"downloadAll": "すべてダウンロード",
"dragAndDropImage": "画像をドラッグ&ドロップ",
"emptyWorkflowExplanation": "ワークフローが空です。アプリを作成するには、まずノードを追加してください。",
"enterNodeGraph": "ノードグラフに入る",
"giveFeedback": "フィードバックを送る",
"graphMode": "グラフモード",
"hasCreditCost": "追加クレジットが必要です",
"linearMode": "アプリモード",
"loadTemplate": "テンプレートを読み込む",
"mobileControls": "編集と実行",
@@ -3376,7 +3373,6 @@
"addedToWorkspace": "{workspaceName}に追加されました",
"inviteAccepted": "招待を承諾しました",
"inviteFailed": "招待の承諾に失敗しました",
"switchFailed": "ワークスペースの切り替えに失敗しました。もう一度お試しください。",
"viewWorkspace": "ワークスペースを見る"
},
"workspaceAuth": {

View File

@@ -400,7 +400,7 @@
"name": "タブバーのレイアウト",
"options": {
"Default": "デフォルト",
"Legacy": "レガシー"
"Integrated": "統合"
},
"tooltip": "タブバーのレイアウトを制御します。「統合」を選択すると、ヘルプとユーザーコントロールがタブバーエリアに移動します。"
},

View File

@@ -952,7 +952,6 @@
"collapseAll": "모두 접기",
"color": "색상",
"comfy": "Comfy",
"comfyCloud": "Comfy Cloud",
"comfyOrgLogoAlt": "ComfyOrg 로고",
"comingSoon": "곧 출시 예정",
"command": "명령",
@@ -1460,14 +1459,12 @@
"unknownWidget": "위젯이 표시되지 않습니다"
},
"cancelThisRun": "이 실행 취소",
"deleteAllAssets": "이 실행에서 모든 에셋 삭제",
"downloadAll": "모두 다운로드",
"dragAndDropImage": "이미지를 드래그 앤 드롭하세요",
"emptyWorkflowExplanation": "워크플로우가 비어 있습니다. 앱을 만들려면 먼저 노드를 추가해야 합니다.",
"enterNodeGraph": "노드 그래프로 진입",
"giveFeedback": "피드백 보내기",
"graphMode": "그래프 모드",
"hasCreditCost": "추가 크레딧 필요",
"linearMode": "앱 모드",
"loadTemplate": "템플릿 불러오기",
"mobileControls": "편집 및 실행",
@@ -3376,7 +3373,6 @@
"addedToWorkspace": "{workspaceName} 워크스페이스에 추가되었습니다",
"inviteAccepted": "초대 수락됨",
"inviteFailed": "초대 수락에 실패했습니다",
"switchFailed": "워크스페이스 전환에 실패했습니다. 다시 시도해 주세요.",
"viewWorkspace": "워크스페이스 보기"
},
"workspaceAuth": {

View File

@@ -400,7 +400,7 @@
"name": "탭 바 레이아웃",
"options": {
"Default": "기본값",
"Legacy": "레거시"
"Integrated": "통합"
},
"tooltip": "탭 바의 레이아웃을 제어합니다. \"통합\"을 선택하면 도움말과 사용자 컨트롤이 탭 바 영역으로 이동합니다."
},

View File

@@ -952,7 +952,6 @@
"collapseAll": "Recolher tudo",
"color": "Cor",
"comfy": "Comfy",
"comfyCloud": "Comfy Cloud",
"comfyOrgLogoAlt": "Logo do ComfyOrg",
"comingSoon": "Em breve",
"command": "Comando",
@@ -1460,14 +1459,12 @@
"unknownWidget": "Widget não visível"
},
"cancelThisRun": "Cancelar esta execução",
"deleteAllAssets": "Excluir todos os ativos desta execução",
"downloadAll": "Baixar tudo",
"dragAndDropImage": "Arraste e solte uma imagem",
"emptyWorkflowExplanation": "Seu fluxo de trabalho está vazio. Você precisa adicionar alguns nós primeiro para começar a construir um app.",
"enterNodeGraph": "Entrar no grafo de nós",
"giveFeedback": "Enviar feedback",
"graphMode": "Modo Gráfico",
"hasCreditCost": "Requer créditos adicionais",
"linearMode": "Modo App",
"loadTemplate": "Carregar um modelo",
"mobileControls": "Editar e Executar",
@@ -3388,7 +3385,6 @@
"addedToWorkspace": "Você foi adicionado ao {workspaceName}",
"inviteAccepted": "Convite aceito",
"inviteFailed": "Falha ao aceitar convite",
"switchFailed": "Falha ao alternar o workspace. Por favor, tente novamente.",
"viewWorkspace": "Ver workspace"
},
"workspaceAuth": {

View File

@@ -400,7 +400,7 @@
"name": "Layout da Barra de Abas",
"options": {
"Default": "Padrão",
"Legacy": "Legado"
"Integrated": "Integrado"
},
"tooltip": "Controla o layout da barra de abas. \"Integrado\" move os controles de Ajuda e Usuário para a área da barra de abas."
},

View File

@@ -952,7 +952,6 @@
"collapseAll": "Свернуть все",
"color": "Цвет",
"comfy": "Comfy",
"comfyCloud": "Comfy Cloud",
"comfyOrgLogoAlt": "Логотип ComfyOrg",
"comingSoon": "Скоро будет",
"command": "Команда",
@@ -1460,14 +1459,12 @@
"unknownWidget": "Виджет не отображается"
},
"cancelThisRun": "Отменить этот запуск",
"deleteAllAssets": "Удалить все ресурсы из этого запуска",
"downloadAll": "Скачать всё",
"dragAndDropImage": "Перетащите изображение",
"emptyWorkflowExplanation": "Ваш рабочий процесс пуст. Сначала добавьте несколько узлов, чтобы начать создавать приложение.",
"enterNodeGraph": "Войти в граф узлов",
"giveFeedback": "Оставить отзыв",
"graphMode": "Графовый режим",
"hasCreditCost": "Требуются дополнительные кредиты",
"linearMode": "Режим приложения",
"loadTemplate": "Загрузить шаблон",
"mobileControls": "Редактировать и запустить",
@@ -3376,7 +3373,6 @@
"addedToWorkspace": "Вы были добавлены в {workspaceName}",
"inviteAccepted": "Приглашение принято",
"inviteFailed": "Не удалось принять приглашение",
"switchFailed": "Не удалось переключить рабочее пространство. Пожалуйста, попробуйте еще раз.",
"viewWorkspace": "Просмотреть рабочее пространство"
},
"workspaceAuth": {

View File

@@ -400,7 +400,7 @@
"name": "Макет панели вкладок",
"options": {
"Default": "По умолчанию",
"Legacy": "Классический"
"Integrated": "Интегрированный"
},
"tooltip": "Управляет расположением панели вкладок. «Интегрированный» перемещает элементы управления Справкой и Пользователем в область панели вкладок."
},

View File

@@ -952,7 +952,6 @@
"collapseAll": "Hepsini daralt",
"color": "Renk",
"comfy": "Comfy",
"comfyCloud": "Comfy Cloud",
"comfyOrgLogoAlt": "ComfyOrg Logosu",
"comingSoon": "Çok Yakında",
"command": "Komut",
@@ -1460,14 +1459,12 @@
"unknownWidget": "Widget görünür değil"
},
"cancelThisRun": "Bu çalıştırmayı iptal et",
"deleteAllAssets": "Bu çalışmadaki tüm varlıkları sil",
"downloadAll": "Tümünü İndir",
"dragAndDropImage": "Bir görseli sürükleyip bırakın",
"emptyWorkflowExplanation": "Çalışma akışınız boş. Bir uygulama oluşturmaya başlamak için önce bazı düğümler eklemelisiniz.",
"enterNodeGraph": "Düğüm grafiğine gir",
"giveFeedback": "Geri bildirim ver",
"graphMode": "Grafik Modu",
"hasCreditCost": "Ekstra kredi gerektirir",
"linearMode": "Uygulama Modu",
"loadTemplate": "Şablon yükle",
"mobileControls": "Düzenle ve Çalıştır",
@@ -3376,7 +3373,6 @@
"addedToWorkspace": "{workspaceName} çalışma alanına eklendiniz",
"inviteAccepted": "Davet kabul edildi",
"inviteFailed": "Davet kabul edilemedi",
"switchFailed": "Çalışma alanı değiştirilemedi. Lütfen tekrar deneyin.",
"viewWorkspace": "Çalışma alanını görüntüle"
},
"workspaceAuth": {

View File

@@ -400,7 +400,7 @@
"name": "Sekme Çubuğu Düzeni",
"options": {
"Default": "Varsayılan",
"Legacy": "Klasik"
"Integrated": "Entegre"
},
"tooltip": "Sekme çubuğu düzenini kontrol eder. \"Entegre\" seçeneği, Yardım ve Kullanıcı kontrollerini sekme çubuğu alanına taşır."
},

View File

@@ -952,7 +952,6 @@
"collapseAll": "全部摺疊",
"color": "顏色",
"comfy": "Comfy",
"comfyCloud": "Comfy Cloud",
"comfyOrgLogoAlt": "ComfyOrg 標誌",
"comingSoon": "即將推出",
"command": "指令",
@@ -1460,14 +1459,12 @@
"unknownWidget": "元件不可見"
},
"cancelThisRun": "取消本次執行",
"deleteAllAssets": "刪除本次運行的所有資產",
"downloadAll": "全部下載",
"dragAndDropImage": "拖曳圖片到此",
"emptyWorkflowExplanation": "您的工作流程目前是空的。您需要先新增一些節點,才能開始建立應用程式。",
"enterNodeGraph": "進入節點圖",
"giveFeedback": "提供回饋",
"graphMode": "圖形模式",
"hasCreditCost": "需要額外點數",
"linearMode": "App 模式",
"loadTemplate": "載入範本",
"mobileControls": "編輯與執行",
@@ -3376,7 +3373,6 @@
"addedToWorkspace": "你已被加入 {workspaceName}",
"inviteAccepted": "已接受邀請",
"inviteFailed": "接受邀請失敗",
"switchFailed": "切換工作區失敗。請再試一次。",
"viewWorkspace": "檢視工作區"
},
"workspaceAuth": {

View File

@@ -400,7 +400,7 @@
"name": "分頁列佈局",
"options": {
"Default": "預設",
"Legacy": "傳統"
"Integrated": "整合"
},
"tooltip": "控制分頁列的佈局。「整合」會將說明和使用者控制項移至分頁列區域。"
},

View File

@@ -952,7 +952,6 @@
"collapseAll": "全部折叠",
"color": "颜色",
"comfy": "舒适",
"comfyCloud": "Comfy Cloud",
"comfyOrgLogoAlt": "ComfyOrg 徽标",
"comingSoon": "即将推出",
"command": "指令",
@@ -1460,14 +1459,12 @@
"unknownWidget": "组件不可见"
},
"cancelThisRun": "取消本次运行",
"deleteAllAssets": "删除本次运行的所有资源",
"downloadAll": "全部下载",
"dragAndDropImage": "拖拽图片到此处",
"emptyWorkflowExplanation": "你的工作流为空。你需要先添加一些节点,才能开始构建应用。",
"enterNodeGraph": "进入节点图",
"giveFeedback": "提供反馈",
"graphMode": "图形模式",
"hasCreditCost": "需要额外积分",
"linearMode": "App 模式",
"loadTemplate": "加载模板",
"mobileControls": "编辑与运行",
@@ -3388,7 +3385,6 @@
"addedToWorkspace": "您已被加入 {workspaceName}",
"inviteAccepted": "邀请已接受",
"inviteFailed": "接受邀请失败",
"switchFailed": "切换工作区失败。请重试。",
"viewWorkspace": "查看工作区"
},
"workspaceAuth": {

View File

@@ -400,7 +400,7 @@
"name": "标签栏布局",
"options": {
"Default": "默认",
"Legacy": "传统"
"Integrated": "集成"
},
"tooltip": "控制标签栏的布局。“集成”会将帮助和用户控件移动到标签栏区域。"
},

View File

@@ -84,7 +84,8 @@ export function useMediaAssetActions() {
toast.add({
severity: 'error',
summary: t('g.error'),
detail: t('g.failedToDownloadImage')
detail: t('g.failedToDownloadImage'),
life: 3000
})
}
}
@@ -125,7 +126,8 @@ export function useMediaAssetActions() {
toast.add({
severity: 'error',
summary: t('g.error'),
detail: t('g.failedToDownloadImage')
detail: t('g.failedToDownloadImage'),
life: 3000
})
}
}
@@ -180,7 +182,8 @@ export function useMediaAssetActions() {
toast.add({
severity: 'error',
summary: t('g.error'),
detail: t('exportToast.exportFailedSingle')
detail: t('exportToast.exportFailedSingle'),
life: 3000
})
}
}
@@ -235,7 +238,8 @@ export function useMediaAssetActions() {
toast.add({
severity: 'error',
summary: t('g.error'),
detail: t('mediaAsset.nodeTypeNotFound', { nodeType })
detail: t('mediaAsset.nodeTypeNotFound', { nodeType }),
life: 3000
})
return
}
@@ -248,7 +252,8 @@ export function useMediaAssetActions() {
toast.add({
severity: 'error',
summary: t('g.error'),
detail: t('mediaAsset.failedToCreateNode')
detail: t('mediaAsset.failedToCreateNode'),
life: 3000
})
return
}
@@ -438,7 +443,8 @@ export function useMediaAssetActions() {
toast.add({
severity: 'error',
summary: t('g.error'),
detail: t('mediaAsset.selection.failedToAddNodes')
detail: t('mediaAsset.selection.failedToAddNodes'),
life: 3000
})
} else {
toast.add({
@@ -670,7 +676,8 @@ export function useMediaAssetActions() {
summary: t('g.error'),
detail: isSingle
? t('mediaAsset.failedToDeleteAsset')
: t('mediaAsset.selection.failedToDeleteAssets')
: t('mediaAsset.selection.failedToDeleteAssets'),
life: 3000
})
} else {
// Partial success (only possible with multiple assets)
@@ -691,7 +698,8 @@ export function useMediaAssetActions() {
summary: t('g.error'),
detail: isSingle
? t('mediaAsset.failedToDeleteAsset')
: t('mediaAsset.selection.failedToDeleteAssets')
: t('mediaAsset.selection.failedToDeleteAssets'),
life: 3000
})
} finally {
// Hide loading overlay for all assets

View File

@@ -73,7 +73,8 @@ export function createAssetWidget(
toastStore.add({
severity: 'error',
summary: t('assetBrowser.invalidAsset'),
detail: t('assetBrowser.invalidAssetDetail')
detail: t('assetBrowser.invalidAssetDetail'),
life: 5000
})
return
}
@@ -91,7 +92,8 @@ export function createAssetWidget(
toastStore.add({
severity: 'error',
summary: t('assetBrowser.invalidFilename'),
detail: t('assetBrowser.invalidFilenameDetail')
detail: t('assetBrowser.invalidFilenameDetail'),
life: 5000
})
return
}

View File

@@ -317,7 +317,8 @@ export function useNodeReplacement() {
toastStore.add({
severity: 'error',
summary: t('g.error', 'Error'),
detail: t('nodeReplacement.replaceFailed', 'Failed to replace nodes')
detail: t('nodeReplacement.replaceFailed', 'Failed to replace nodes'),
life: 5000
})
return replacedTypes
} finally {

View File

@@ -83,7 +83,8 @@ describe('useSecrets', () => {
expect(mockAdd).toHaveBeenCalledWith({
severity: 'error',
summary: 'g.error',
detail: 'Network error'
detail: 'Network error',
life: 5000
})
})
})
@@ -129,7 +130,8 @@ describe('useSecrets', () => {
expect(mockAdd).toHaveBeenCalledWith({
severity: 'error',
summary: 'g.error',
detail: 'Delete failed'
detail: 'Delete failed',
life: 5000
})
})
})

View File

@@ -33,14 +33,16 @@ export function useSecrets() {
toastStore.add({
severity: 'error',
summary: t('g.error'),
detail: err.message
detail: err.message,
life: 5000
})
} else {
console.error('Unexpected error fetching secrets:', err)
toastStore.add({
severity: 'error',
summary: t('g.error'),
detail: t('g.unknownError')
detail: t('g.unknownError'),
life: 5000
})
}
} finally {
@@ -58,14 +60,16 @@ export function useSecrets() {
toastStore.add({
severity: 'error',
summary: t('g.error'),
detail: err.message
detail: err.message,
life: 5000
})
} else {
console.error('Unexpected error deleting secret:', err)
toastStore.add({
severity: 'error',
summary: t('g.error'),
detail: t('g.unknownError')
detail: t('g.unknownError'),
life: 5000
})
}
} finally {

View File

@@ -370,7 +370,8 @@ export const useWorkflowService = () => {
toastStore.add({
severity: 'error',
summary: t('g.error'),
detail: t('toastMessages.failedToSaveDraft')
detail: t('toastMessages.failedToSaveDraft'),
life: 3000
})
}
}

View File

@@ -112,7 +112,8 @@ export function useWorkflowPersistenceV2() {
toast.add({
severity: 'error',
summary: t('g.error'),
detail: t('toastMessages.failedToSaveDraft')
detail: t('toastMessages.failedToSaveDraft'),
life: 3000
})
return
}

View File

@@ -484,7 +484,8 @@ describe('ShareWorkflowDialogContent', () => {
expect(mockToast.add).toHaveBeenCalledWith({
severity: 'error',
summary: 'Error',
detail: 'Publish failed'
detail: 'Publish failed',
life: 5000
})
})

View File

@@ -352,7 +352,8 @@ const { isLoading: isSaving, execute: handleSave } = useAsyncState(
toast.add({
severity: 'error',
summary: t('shareWorkflow.saveFailedTitle'),
detail: t('shareWorkflow.saveFailedDescription')
detail: t('shareWorkflow.saveFailedDescription'),
life: 5000
})
}
}
@@ -390,7 +391,8 @@ const {
toast.add({
severity: 'error',
summary: t('g.error'),
detail: error instanceof Error ? error.message : t('g.error')
detail: error instanceof Error ? error.message : t('g.error'),
life: 5000
})
}
}

View File

@@ -183,7 +183,8 @@ async function handleCreate() {
toast.add({
severity: 'error',
summary: t('g.error'),
detail: error instanceof Error ? error.message : t('g.error')
detail: error instanceof Error ? error.message : t('g.error'),
life: 5000
})
} finally {
isCreating.value = false

View File

@@ -338,7 +338,8 @@ describe('useSharedWorkflowUrlLoader', () => {
expect(mockToastAdd).toHaveBeenCalledWith({
severity: 'error',
summary: 'Error',
detail: 'Failed to load shared workflow'
detail: 'Failed to load shared workflow',
life: 3000
})
expect(mockRouterReplace).toHaveBeenCalledWith({ query: {} })
expect(preservedQueryMocks.clearPreservedQuery).toHaveBeenCalledWith(

View File

@@ -118,7 +118,8 @@ export function useSharedWorkflowUrlLoader() {
toast.add({
severity: 'error',
summary: t('g.error'),
detail: t('shareWorkflow.loadFailed')
detail: t('shareWorkflow.loadFailed'),
life: 3000
})
cleanupUrlParams()
clearPreservedQuery(SHARE_NAMESPACE)
@@ -147,7 +148,8 @@ export function useSharedWorkflowUrlLoader() {
toast.add({
severity: 'error',
summary: t('g.error'),
detail: t('shareWorkflow.loadFailed')
detail: t('shareWorkflow.loadFailed'),
life: 5000
})
return 'failed'
}

View File

@@ -145,7 +145,8 @@ describe('useTemplateUrlLoader', () => {
expect(mockToastAdd).toHaveBeenCalledWith({
severity: 'error',
summary: 'Error',
detail: 'Template "invalid-template" not found'
detail: 'Template "invalid-template" not found',
life: 3000
})
})
@@ -238,7 +239,8 @@ describe('useTemplateUrlLoader', () => {
expect(mockToastAdd).toHaveBeenCalledWith({
severity: 'error',
summary: 'Error',
detail: 'Failed to load template'
detail: 'Failed to load template',
life: 3000
})
})

View File

@@ -117,7 +117,8 @@ export function useTemplateUrlLoader() {
summary: t('g.error'),
detail: t('templateWorkflows.error.templateNotFound', {
templateName: templateParam
})
}),
life: 3000
})
} else if (modeParam === 'linear') {
// Set linear mode after successful template load
@@ -131,7 +132,8 @@ export function useTemplateUrlLoader() {
toast.add({
severity: 'error',
summary: t('g.error'),
detail: t('g.errorLoadingTemplate')
detail: t('g.errorLoadingTemplate'),
life: 3000
})
} finally {
cleanupUrlParams()

View File

@@ -428,7 +428,8 @@ async function handleResubscribe() {
toast.add({
severity: 'error',
summary: t('g.error'),
detail: message
detail: message,
life: 5000
})
} finally {
isResubscribing.value = false

View File

@@ -148,7 +148,8 @@ async function handleSubscribeClick(payload: {
toast.add({
severity: 'error',
summary: 'Unable to subscribe',
detail: 'This plan is not available'
detail: 'This plan is not available',
life: 5000
})
return
}
@@ -158,7 +159,8 @@ async function handleSubscribeClick(payload: {
toast.add({
severity: 'error',
summary: 'Unable to subscribe',
detail: response?.reason || 'This plan is not available'
detail: response?.reason || 'This plan is not available',
life: 5000
})
return
}
@@ -173,7 +175,8 @@ async function handleSubscribeClick(payload: {
toast.add({
severity: 'error',
summary: 'Error',
detail: message
detail: message,
life: 5000
})
} finally {
isLoadingPreview.value = false
@@ -233,7 +236,8 @@ async function handleAddCreditCard() {
toast.add({
severity: 'error',
summary: 'Error',
detail: message
detail: message,
life: 5000
})
} finally {
isSubscribing.value = false
@@ -287,7 +291,8 @@ async function handleConfirmTransition() {
toast.add({
severity: 'error',
summary: 'Error',
detail: message
detail: message,
life: 5000
})
} finally {
isSubscribing.value = false
@@ -311,7 +316,8 @@ async function handleResubscribe() {
toast.add({
severity: 'error',
summary: 'Error',
detail: message
detail: message,
life: 5000
})
} finally {
isResubscribing.value = false

View File

@@ -273,7 +273,8 @@ async function handleBuy() {
toast.add({
severity: 'error',
summary: t('credits.topUp.purchaseError'),
detail: t('credits.topUp.unknownError')
detail: t('credits.topUp.unknownError'),
life: 5000
})
}
} catch (error) {
@@ -284,7 +285,8 @@ async function handleBuy() {
toast.add({
severity: 'error',
summary: t('credits.topUp.purchaseError'),
detail: t('credits.topUp.purchaseErrorDetail', { error: errorMessage })
detail: t('credits.topUp.purchaseErrorDetail', { error: errorMessage }),
life: 5000
})
} finally {
loading.value = false

View File

@@ -102,7 +102,8 @@ async function onCreate() {
toast.add({
severity: 'error',
summary: t('workspacePanel.toast.failedToCreateWorkspace'),
detail: error instanceof Error ? error.message : t('g.unknownError')
detail: error instanceof Error ? error.message : t('g.unknownError'),
life: 5000
})
} finally {
loading.value = false

View File

@@ -79,7 +79,8 @@ async function onDelete() {
toast.add({
severity: 'error',
summary: t('workspacePanel.toast.failedToDeleteWorkspace'),
detail: error instanceof Error ? error.message : t('g.unknownError')
detail: error instanceof Error ? error.message : t('g.unknownError'),
life: 5000
})
} finally {
loading.value = false

View File

@@ -94,7 +94,8 @@ async function onSave() {
toast.add({
severity: 'error',
summary: t('workspacePanel.toast.failedToUpdateWorkspace'),
detail: error instanceof Error ? error.message : t('g.unknownError')
detail: error instanceof Error ? error.message : t('g.unknownError'),
life: 5000
})
} finally {
loading.value = false

View File

@@ -138,7 +138,8 @@ async function onCreateLink() {
toast.add({
severity: 'error',
summary: t('workspacePanel.inviteMemberDialog.linkCopyFailed'),
detail: error instanceof Error ? error.message : undefined
detail: error instanceof Error ? error.message : undefined,
life: 3000
})
} finally {
loading.value = false
@@ -160,7 +161,8 @@ async function onCopyLink() {
} catch {
toast.add({
severity: 'error',
summary: t('workspacePanel.inviteMemberDialog.linkCopyFailed')
summary: t('workspacePanel.inviteMemberDialog.linkCopyFailed'),
life: 3000
})
}
}

View File

@@ -68,7 +68,8 @@ async function onLeave() {
toast.add({
severity: 'error',
summary: t('workspacePanel.toast.failedToLeaveWorkspace'),
detail: error instanceof Error ? error.message : t('g.unknownError')
detail: error instanceof Error ? error.message : t('g.unknownError'),
life: 5000
})
} finally {
loading.value = false

View File

@@ -73,7 +73,8 @@ async function onRemove() {
} catch {
toast.add({
severity: 'error',
summary: t('workspacePanel.removeMemberDialog.error')
summary: t('workspacePanel.removeMemberDialog.error'),
life: 3000
})
} finally {
loading.value = false

View File

@@ -69,7 +69,8 @@ async function onRevoke() {
toast.add({
severity: 'error',
summary: t('g.error'),
detail: error instanceof Error ? error.message : undefined
detail: error instanceof Error ? error.message : undefined,
life: 3000
})
} finally {
loading.value = false

View File

@@ -543,7 +543,8 @@ async function handleCopyInviteLink(invite: PendingInvite) {
} catch {
toast.add({
severity: 'error',
summary: t('g.error')
summary: t('g.error'),
life: 3000
})
}
}

View File

@@ -151,7 +151,8 @@ describe('useInviteUrlLoader', () => {
expect(mockToastAdd).toHaveBeenCalledWith({
severity: 'error',
summary: 'Failed to Accept Invite',
detail: 'Invalid invite'
detail: 'Invalid invite',
life: 5000
})
})
@@ -210,7 +211,8 @@ describe('useInviteUrlLoader', () => {
expect(mockToastAdd).toHaveBeenCalledWith({
severity: 'error',
summary: 'Failed to Accept Invite',
detail: 'Invalid token'
detail: 'Invalid token',
life: 5000
})
})

View File

@@ -97,7 +97,8 @@ export function useInviteUrlLoader() {
toast.add({
severity: 'error',
summary: t('workspace.inviteFailed'),
detail: error instanceof Error ? error.message : t('g.unknownError')
detail: error instanceof Error ? error.message : t('g.unknownError'),
life: 5000
})
} finally {
cleanupUrlParams()

View File

@@ -219,7 +219,8 @@ describe('billingOperationStore', () => {
expect(mockToastAdd).toHaveBeenCalledWith({
severity: 'error',
summary: 'billingOperation.subscriptionFailed',
detail: errorMessage
detail: errorMessage,
life: 5000
})
})
@@ -238,7 +239,8 @@ describe('billingOperationStore', () => {
expect(mockToastAdd).toHaveBeenCalledWith({
severity: 'error',
summary: 'billingOperation.topupFailed',
detail: undefined
detail: undefined,
life: 5000
})
})
})
@@ -265,7 +267,8 @@ describe('billingOperationStore', () => {
expect(mockToastAdd).toHaveBeenCalledWith({
severity: 'error',
summary: 'billingOperation.subscriptionTimeout'
summary: 'billingOperation.subscriptionTimeout',
life: 5000
})
})
@@ -284,7 +287,8 @@ describe('billingOperationStore', () => {
expect(mockToastAdd).toHaveBeenCalledWith({
severity: 'error',
summary: 'billingOperation.topupTimeout'
summary: 'billingOperation.topupTimeout',
life: 5000
})
})
})

View File

@@ -173,7 +173,8 @@ export const useBillingOperationStore = defineStore('billingOperation', () => {
useToastStore().add({
severity: 'error',
summary: defaultMessage,
detail: errorMessage ?? undefined
detail: errorMessage ?? undefined,
life: 5000
})
}
@@ -191,7 +192,8 @@ export const useBillingOperationStore = defineStore('billingOperation', () => {
useToastStore().add({
severity: 'error',
summary: message
summary: message,
life: 5000
})
}

View File

@@ -203,6 +203,7 @@ const handleDownload = () => {
severity: 'error',
summary: 'Error',
detail: t('g.failedToDownloadVideo'),
life: 3000,
group: 'video-preview'
})
}

View File

@@ -233,6 +233,7 @@ const handleDownload = () => {
severity: 'error',
summary: 'Error',
detail: t('g.failedToDownloadImage'),
life: 3000,
group: 'image-preview'
})
}

View File

@@ -208,7 +208,8 @@ const handleDownload = () => {
toast.add({
severity: 'error',
summary: t('g.error'),
detail: t('g.failedToDownloadFile')
detail: t('g.failedToDownloadFile'),
life: 3000
})
}
}

View File

@@ -0,0 +1,124 @@
import { mount } from '@vue/test-utils'
import PrimeVue from 'primevue/config'
import { createI18n } from 'vue-i18n'
import { describe, expect, it, vi } from 'vitest'
import { defineComponent, h, nextTick } from 'vue'
import FormDropdown from './FormDropdown.vue'
import type { FormDropdownItem } from './types'
function createItem(id: string, name: string): FormDropdownItem {
return { id, preview_url: '', name, label: name }
}
const i18n = createI18n({ legacy: false, locale: 'en', messages: { en: {} } })
vi.mock('@/platform/updates/common/toastStore', () => ({
useToastStore: () => ({
addAlert: vi.fn()
})
}))
const MockFormDropdownMenu = defineComponent({
name: 'FormDropdownMenu',
props: {
items: { type: Array as () => FormDropdownItem[], default: () => [] },
updateKey: { type: null, default: undefined },
searcher: { type: Function, default: undefined },
isSelected: { type: Function, default: undefined },
filterOptions: { type: Array, default: () => [] },
sortOptions: { type: Array, default: () => [] },
maxSelectable: { type: Number, default: 1 },
disabled: { type: Boolean, default: false },
showOwnershipFilter: { type: Boolean, default: false },
ownershipOptions: { type: Array, default: () => [] },
showBaseModelFilter: { type: Boolean, default: false },
baseModelOptions: { type: Array, default: () => [] }
},
setup() {
return () => h('div', { class: 'mock-menu' })
}
})
function mountDropdown(items: FormDropdownItem[]) {
return mount(FormDropdown, {
props: { items },
global: {
plugins: [PrimeVue, i18n],
stubs: {
FormDropdownInput: true,
Popover: { template: '<div><slot /></div>' },
FormDropdownMenu: MockFormDropdownMenu
}
}
})
}
function getMenuItems(
wrapper: ReturnType<typeof mountDropdown>
): FormDropdownItem[] {
return wrapper
.findComponent(MockFormDropdownMenu)
.props('items') as FormDropdownItem[]
}
describe('FormDropdown', () => {
describe('filteredItems updates when items prop changes', () => {
it('updates displayed items when items prop changes', async () => {
const wrapper = mountDropdown([
createItem('input-0', 'video1.mp4'),
createItem('input-1', 'video2.mp4')
])
await nextTick()
await nextTick()
expect(getMenuItems(wrapper)).toHaveLength(2)
await wrapper.setProps({
items: [
createItem('output-0', 'rendered1.mp4'),
createItem('output-1', 'rendered2.mp4')
]
})
await nextTick()
await nextTick()
const menuItems = getMenuItems(wrapper)
expect(menuItems).toHaveLength(2)
expect(menuItems[0].name).toBe('rendered1.mp4')
})
it('updates when items change but IDs stay the same', async () => {
const wrapper = mountDropdown([createItem('1', 'alpha')])
await nextTick()
await nextTick()
await wrapper.setProps({ items: [createItem('1', 'beta')] })
await nextTick()
await nextTick()
expect(getMenuItems(wrapper)[0].name).toBe('beta')
})
it('updates when switching between empty and non-empty items', async () => {
const wrapper = mountDropdown([])
await nextTick()
await nextTick()
expect(getMenuItems(wrapper)).toHaveLength(0)
await wrapper.setProps({ items: [createItem('1', 'video.mp4')] })
await nextTick()
await nextTick()
expect(getMenuItems(wrapper)).toHaveLength(1)
expect(getMenuItems(wrapper)[0].name).toBe('video.mp4')
await wrapper.setProps({ items: [] })
await nextTick()
await nextTick()
expect(getMenuItems(wrapper)).toHaveLength(0)
})
})
})

View File

@@ -1,6 +1,6 @@
<script setup lang="ts">
import Popover from 'primevue/popover'
import { computed, ref, useTemplateRef } from 'vue'
import { computed, ref, useTemplateRef, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { useToastStore } from '@/platform/updates/common/toastStore'
@@ -101,10 +101,30 @@ const maxSelectable = computed(() => {
return 1
})
const itemsKey = computed(() => items.map((item) => item.id).join('|'))
const itemsKey = ref<symbol>(Symbol())
const filteredItems = ref<FormDropdownItem[]>([])
watch(
() => items,
async (newItems, _old, onCleanup) => {
itemsKey.value = Symbol()
let cancelled = false
let cleanupFn: (() => void) | undefined
onCleanup(() => {
cancelled = true
cleanupFn?.()
})
const results = await searcher(
searchQuery.value,
newItems,
(cb) => (cleanupFn = cb)
)
if (!cancelled) filteredItems.value = results
},
{ immediate: true }
)
const defaultSorter = computed<SortOption['sorter']>(() => {
const sorter = sortOptions.find((option) => option.id === 'default')?.sorter
return sorter || (({ items: i }) => i.slice())

View File

@@ -1315,13 +1315,15 @@ export class ComfyApi extends EventTarget {
useToastStore().add({
severity: 'error',
summary:
'Unloading of models failed. Installed ComfyUI may be an outdated version.'
'Unloading of models failed. Installed ComfyUI may be an outdated version.',
life: 5000
})
}
} catch (error) {
useToastStore().add({
severity: 'error',
summary: 'An error occurred while trying to unload models.'
summary: 'An error occurred while trying to unload models.',
life: 5000
})
}
}

View File

@@ -70,7 +70,7 @@ function mapHistoryToAssets(historyItems: JobListItem[]): AssetItem[] {
assetItem.user_metadata = {
...assetItem.user_metadata,
outputCount: task.previewableOutputs.length,
outputCount: job.outputs_count,
allOutputs: task.previewableOutputs
}

View File

@@ -267,7 +267,8 @@ export const useSubgraphStore = defineStore('subgraph', () => {
useToastStore().add({
severity: 'error',
summary: t('subgraphStore.loadFailure'),
detail: errors.length > 3 ? `x${errors.length}` : `${errors}`
detail: errors.length > 3 ? `x${errors.length}` : `${errors}`,
life: 6000
})
}
}

View File

@@ -3,9 +3,7 @@ import { resolvePromotedWidgetSource } from '@/core/graph/subgraph/resolvePromot
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
import type { SubgraphNode } from '@/lib/litegraph/src/subgraph/SubgraphNode'
import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets'
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
import type { InputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
import { useDialogService } from '@/services/dialogService'
export type WidgetValue = boolean | number | string | object | undefined
@@ -77,34 +75,3 @@ export function renameWidget(
return true
}
export async function promptWidgetLabel(
widget: IBaseWidget,
t: (key: string) => string
): Promise<string | null> {
return useDialogService().prompt({
title: t('g.rename'),
message: t('g.enterNewNamePrompt'),
defaultValue: widget.label,
placeholder: widget.name
})
}
export async function promptRenameWidget(
widget: IBaseWidget,
node: LGraphNode,
t: (key: string) => string,
parents?: SubgraphNode[]
): Promise<string | null> {
const rawLabel = await promptWidgetLabel(widget, t)
if (rawLabel === null) return null
const normalizedLabel = rawLabel.trim()
if (!normalizedLabel) return null
if (!renameWidget(widget, node, normalizedLabel, parents)) return null
widget.callback?.(widget.value)
useCanvasStore().canvas?.setDirty(true)
return normalizedLabel
}

View File

@@ -168,7 +168,8 @@ export function useManagerState() {
useToastStore().add({
severity: 'error',
summary: t('g.error'),
detail: t('manager.legacyMenuNotAvailable')
detail: t('manager.legacyMenuNotAvailable'),
life: 3000
})
}
// Fallback to extensions panel if not showing toast
@@ -184,7 +185,8 @@ export function useManagerState() {
useToastStore().add({
severity: 'error',
summary: t('g.error'),
detail: t('manager.legacyMenuNotAvailable')
detail: t('manager.legacyMenuNotAvailable'),
life: 3000
})
} else {
managerDialog.show(options?.initialTab, options?.initialPackId)