mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-27 17:52:16 +00:00
Compare commits
11 Commits
ticket/bug
...
fix/image-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a724bd9b43 | ||
|
|
45acf75393 | ||
|
|
c6c3e69241 | ||
|
|
892a9cf2c5 | ||
|
|
308c22efc6 | ||
|
|
5728d240da | ||
|
|
acf2f4280c | ||
|
|
7ad6994d01 | ||
|
|
2829f78579 | ||
|
|
c4156d7059 | ||
|
|
725a0a2b89 |
@@ -4,7 +4,7 @@
|
||||
<template v-if="filter.tasks.length === 0">
|
||||
<!-- Empty filter -->
|
||||
<Divider />
|
||||
<p class="text-neutral-400 w-full text-center">
|
||||
<p class="w-full text-center text-neutral-400">
|
||||
{{ $t('maintenance.allOk') }}
|
||||
</p>
|
||||
</template>
|
||||
@@ -25,7 +25,7 @@
|
||||
|
||||
<!-- Display: Cards -->
|
||||
<template v-else>
|
||||
<div class="flex flex-wrap justify-evenly gap-8 pad-y my-4">
|
||||
<div class="pad-y my-4 flex flex-wrap justify-evenly gap-8">
|
||||
<TaskCard
|
||||
v-for="task in filter.tasks"
|
||||
:key="task.id"
|
||||
@@ -45,7 +45,8 @@ import { useConfirm, useToast } from 'primevue'
|
||||
import ConfirmPopup from 'primevue/confirmpopup'
|
||||
import Divider from 'primevue/divider'
|
||||
|
||||
import { t } from '@/i18n'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { useMaintenanceTaskStore } from '@/stores/maintenanceTaskStore'
|
||||
import type {
|
||||
MaintenanceFilter,
|
||||
@@ -55,6 +56,7 @@ import type {
|
||||
import TaskCard from './TaskCard.vue'
|
||||
import TaskListItem from './TaskListItem.vue'
|
||||
|
||||
const { t } = useI18n()
|
||||
const toast = useToast()
|
||||
const confirm = useConfirm()
|
||||
const taskStore = useMaintenanceTaskStore()
|
||||
@@ -80,8 +82,7 @@ const executeTask = async (task: MaintenanceTask) => {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: t('maintenance.error.toastTitle'),
|
||||
detail: message ?? t('maintenance.error.defaultDescription'),
|
||||
life: 10_000
|
||||
detail: message ?? t('maintenance.error.defaultDescription')
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -189,8 +189,7 @@ const completeValidation = async () => {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: t('g.error'),
|
||||
detail: t('maintenance.error.cannotContinue'),
|
||||
life: 5_000
|
||||
detail: t('maintenance.error.cannotContinue')
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<BaseViewTemplate dark hide-language-selector>
|
||||
<div class="h-full p-8 2xl:p-16 flex flex-col items-center justify-center">
|
||||
<div class="flex h-full flex-col items-center justify-center p-8 2xl:p-16">
|
||||
<div
|
||||
class="bg-neutral-800 rounded-lg shadow-lg p-6 w-full max-w-[600px] flex flex-col gap-6"
|
||||
class="flex w-full max-w-[600px] flex-col gap-6 rounded-lg bg-neutral-800 p-6 shadow-lg"
|
||||
>
|
||||
<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 hover:text-blue-300 underline"
|
||||
class="text-blue-400 underline hover:text-blue-300"
|
||||
>
|
||||
{{ $t('install.privacyPolicy') }} </a
|
||||
>.
|
||||
@@ -33,7 +33,7 @@
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex pt-6 justify-end">
|
||||
<div class="flex justify-end pt-6">
|
||||
<Button
|
||||
:label="$t('g.ok')"
|
||||
icon="pi pi-check"
|
||||
@@ -72,8 +72,7 @@ const updateConsent = async () => {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: t('install.settings.errorUpdatingConsent'),
|
||||
detail: t('install.settings.errorUpdatingConsentDetail'),
|
||||
life: 3000
|
||||
detail: t('install.settings.errorUpdatingConsentDetail')
|
||||
})
|
||||
} finally {
|
||||
isUpdating.value = false
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@comfyorg/comfyui-frontend",
|
||||
"version": "1.42.0",
|
||||
"version": "1.42.2",
|
||||
"private": true,
|
||||
"description": "Official front-end implementation of ComfyUI",
|
||||
"homepage": "https://comfy.org",
|
||||
|
||||
@@ -10,7 +10,6 @@ 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,
|
||||
@@ -25,7 +24,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 { useDialogService } from '@/services/dialogService'
|
||||
import { promptRenameWidget } from '@/utils/widgetUtil'
|
||||
import { useAppMode } from '@/composables/useAppMode'
|
||||
import { nodeTypeValidForApp, useAppModeStore } from '@/stores/appModeStore'
|
||||
import { resolveNode } from '@/utils/litegraphUtil'
|
||||
@@ -73,15 +72,12 @@ 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
|
||||
rename: () => promptRenameWidget(widget, node, t)
|
||||
}
|
||||
})
|
||||
)
|
||||
@@ -92,20 +88,6 @@ 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] {
|
||||
@@ -266,7 +248,7 @@ const renderedInputs = computed<[string, MaybeRef<BoundStyle> | undefined][]>(
|
||||
<template #label>
|
||||
<div class="flex gap-3">
|
||||
{{ t('nodeHelpPage.inputs') }}
|
||||
<i class="icon-[lucide--circle-alert] bg-muted-foreground" />
|
||||
<i class="icon-[lucide--info] bg-muted-foreground" />
|
||||
</div>
|
||||
</template>
|
||||
<template #empty>
|
||||
@@ -321,7 +303,7 @@ const renderedInputs = computed<[string, MaybeRef<BoundStyle> | undefined][]>(
|
||||
<template #label>
|
||||
<div class="flex gap-3">
|
||||
{{ t('nodeHelpPage.outputs') }}
|
||||
<i class="icon-[lucide--circle-alert] bg-muted-foreground" />
|
||||
<i class="icon-[lucide--info] bg-muted-foreground" />
|
||||
</div>
|
||||
</template>
|
||||
<template #empty>
|
||||
|
||||
@@ -138,8 +138,7 @@ onMounted(async () => {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: t('g.error'),
|
||||
detail: t('toastMessages.failedToFetchLogs'),
|
||||
life: 5000
|
||||
detail: t('toastMessages.failedToFetchLogs')
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
@@ -275,8 +275,7 @@ async function handleBuy() {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: t('credits.topUp.purchaseError'),
|
||||
detail: t('credits.topUp.purchaseErrorDetail', { error: errorMessage }),
|
||||
life: 5000
|
||||
detail: t('credits.topUp.purchaseErrorDetail', { error: errorMessage })
|
||||
})
|
||||
} finally {
|
||||
loading.value = false
|
||||
|
||||
@@ -98,8 +98,7 @@ async function onConfirmCancel() {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: t('subscription.cancelDialog.failed'),
|
||||
detail: error instanceof Error ? error.message : t('g.unknownError'),
|
||||
life: 5000
|
||||
detail: error instanceof Error ? error.message : t('g.unknownError')
|
||||
})
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
|
||||
@@ -579,8 +579,7 @@ const onUpdateComfyUI = async (): Promise<void> => {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: t('g.error'),
|
||||
detail: error.value || t('helpCenter.updateComfyUIFailed'),
|
||||
life: 5000
|
||||
detail: error.value || t('helpCenter.updateComfyUIFailed')
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -597,8 +596,7 @@ const onUpdateComfyUI = async (): Promise<void> => {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: t('g.error'),
|
||||
detail: err instanceof Error ? err.message : t('g.unknownError'),
|
||||
life: 5000
|
||||
detail: err instanceof Error ? err.message : t('g.unknownError')
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,12 +72,12 @@
|
||||
/>
|
||||
</div>
|
||||
<SliderControl
|
||||
v-model="brushSize"
|
||||
v-model="brushSizeSliderValue"
|
||||
class="flex-1"
|
||||
label=""
|
||||
:min="1"
|
||||
:max="250"
|
||||
:step="1"
|
||||
:min="0"
|
||||
:max="1"
|
||||
:step="0.001"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -182,6 +182,26 @@ const brushSize = computed({
|
||||
set: (value: number) => store.setBrushSize(value)
|
||||
})
|
||||
|
||||
const rawSliderValue = ref<number | null>(null)
|
||||
|
||||
const brushSizeSliderValue = computed({
|
||||
get: () => {
|
||||
if (rawSliderValue.value !== null) {
|
||||
const cachedSize = Math.round(Math.pow(250, rawSliderValue.value))
|
||||
if (cachedSize === brushSize.value) {
|
||||
return rawSliderValue.value
|
||||
}
|
||||
}
|
||||
|
||||
return Math.log(brushSize.value) / Math.log(250)
|
||||
},
|
||||
set: (value: number) => {
|
||||
rawSliderValue.value = value
|
||||
const size = Math.round(Math.pow(250, value))
|
||||
store.setBrushSize(size)
|
||||
}
|
||||
})
|
||||
|
||||
const brushOpacity = computed({
|
||||
get: () => store.brushSettings.opacity,
|
||||
set: (value: number) => store.setBrushOpacity(value)
|
||||
|
||||
@@ -15,10 +15,9 @@ 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 } from '@/utils/widgetUtil'
|
||||
import { getWidgetDefaultValue, promptWidgetLabel } from '@/utils/widgetUtil'
|
||||
import type { WidgetValue } from '@/utils/widgetUtil'
|
||||
|
||||
const {
|
||||
@@ -42,7 +41,6 @@ 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)
|
||||
@@ -67,15 +65,8 @@ const isCurrentValueDefault = computed(() => {
|
||||
})
|
||||
|
||||
async function handleRename() {
|
||||
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
|
||||
const newLabel = await promptWidgetLabel(widget, t)
|
||||
if (newLabel !== null) label.value = newLabel
|
||||
}
|
||||
|
||||
function handleHideInput() {
|
||||
|
||||
@@ -615,8 +615,7 @@ const enterFolderView = async (asset: AssetItem) => {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: t('sideToolbar.folderView.errorSummary'),
|
||||
detail: t('sideToolbar.folderView.errorDetail'),
|
||||
life: 5000
|
||||
detail: t('sideToolbar.folderView.errorDetail')
|
||||
})
|
||||
exitFolderView()
|
||||
}
|
||||
@@ -662,8 +661,7 @@ const copyJobId = async () => {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: t('mediaAsset.jobIdToast.error'),
|
||||
detail: t('mediaAsset.jobIdToast.jobIdCopyFailed'),
|
||||
life: 3000
|
||||
detail: t('mediaAsset.jobIdToast.jobIdCopyFailed')
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,8 +10,9 @@
|
||||
@mouseup="handleMouseUp"
|
||||
@click="handleClick"
|
||||
>
|
||||
<i v-if="isBuilderState" class="bg-text-subtle icon-[lucide--hammer]" />
|
||||
<i
|
||||
v-if="workflowOption.workflow.initialMode === 'app'"
|
||||
v-else-if="workflowOption.workflow.initialMode === 'app'"
|
||||
class="icon-[lucide--panels-top-left] bg-primary-background"
|
||||
/>
|
||||
<span
|
||||
@@ -149,6 +150,11 @@ 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
|
||||
})
|
||||
|
||||
@@ -397,8 +397,7 @@ export function useCoreCommands(): ComfyCommand[] {
|
||||
if (app.canvas.empty) {
|
||||
toastStore.add({
|
||||
severity: 'error',
|
||||
summary: t('toastMessages.emptyCanvas'),
|
||||
life: 3000
|
||||
summary: t('toastMessages.emptyCanvas')
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -557,8 +556,7 @@ export function useCoreCommands(): ComfyCommand[] {
|
||||
toastStore.add({
|
||||
severity: 'error',
|
||||
summary: t('toastMessages.nothingToQueue'),
|
||||
detail: t('toastMessages.pleaseSelectOutputNodes'),
|
||||
life: 3000
|
||||
detail: t('toastMessages.pleaseSelectOutputNodes')
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -571,8 +569,7 @@ export function useCoreCommands(): ComfyCommand[] {
|
||||
toastStore.add({
|
||||
severity: 'error',
|
||||
summary: t('toastMessages.failedToQueue'),
|
||||
detail: t('toastMessages.failedExecutionPathResolution'),
|
||||
life: 3000
|
||||
detail: t('toastMessages.failedExecutionPathResolution')
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -602,8 +599,7 @@ export function useCoreCommands(): ComfyCommand[] {
|
||||
toastStore.add({
|
||||
severity: 'error',
|
||||
summary: t('toastMessages.nothingToGroup'),
|
||||
detail: t('toastMessages.pleaseSelectNodesToGroup'),
|
||||
life: 3000
|
||||
detail: t('toastMessages.pleaseSelectNodesToGroup')
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -962,8 +958,7 @@ export function useCoreCommands(): ComfyCommand[] {
|
||||
toastStore.add({
|
||||
severity: 'error',
|
||||
summary: t('g.error'),
|
||||
detail: t('manager.notAvailable'),
|
||||
life: 3000
|
||||
detail: t('manager.notAvailable')
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -1048,8 +1043,7 @@ export function useCoreCommands(): ComfyCommand[] {
|
||||
toastStore.add({
|
||||
severity: 'error',
|
||||
summary: t('toastMessages.cannotCreateSubgraph'),
|
||||
detail: t('toastMessages.failedToConvertToSubgraph'),
|
||||
life: 3000
|
||||
detail: t('toastMessages.failedToConvertToSubgraph')
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -1258,8 +1252,7 @@ export function useCoreCommands(): ComfyCommand[] {
|
||||
summary: t('g.error'),
|
||||
detail: t('g.commandProhibited', {
|
||||
command: 'Comfy.Memory.UnloadModels'
|
||||
}),
|
||||
life: 3000
|
||||
})
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -1278,8 +1271,7 @@ export function useCoreCommands(): ComfyCommand[] {
|
||||
summary: t('g.error'),
|
||||
detail: t('g.commandProhibited', {
|
||||
command: 'Comfy.Memory.UnloadModelsAndExecutionCache'
|
||||
}),
|
||||
life: 3000
|
||||
})
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -81,8 +81,7 @@ function getParentNodes(): SubgraphNode[] {
|
||||
useToastStore().add({
|
||||
severity: 'error',
|
||||
summary: t('g.error'),
|
||||
detail: t('subgraphStore.promoteOutsideSubgraph'),
|
||||
life: 2000
|
||||
detail: t('subgraphStore.promoteOutsideSubgraph')
|
||||
})
|
||||
return []
|
||||
}
|
||||
|
||||
@@ -204,8 +204,7 @@ import { electronAPI as getElectronAPI } from '@/utils/envUtil'
|
||||
toastStore.add({
|
||||
severity: 'error',
|
||||
summary: t('g.error'),
|
||||
detail: t('desktopUpdate.errorInstallingUpdate'),
|
||||
life: 10_000
|
||||
detail: t('desktopUpdate.errorInstallingUpdate')
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -214,8 +213,7 @@ import { electronAPI as getElectronAPI } from '@/utils/envUtil'
|
||||
toastStore.add({
|
||||
severity: 'error',
|
||||
summary: t('g.error'),
|
||||
detail: t('desktopUpdate.errorCheckingUpdate'),
|
||||
life: 10_000
|
||||
detail: t('desktopUpdate.errorCheckingUpdate')
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -952,6 +952,7 @@
|
||||
"collapseAll": "طي الكل",
|
||||
"color": "اللون",
|
||||
"comfy": "Comfy",
|
||||
"comfyCloud": "Comfy Cloud",
|
||||
"comfyOrgLogoAlt": "شعار ComfyOrg",
|
||||
"comingSoon": "قريباً",
|
||||
"command": "أمر",
|
||||
@@ -1459,12 +1460,14 @@
|
||||
"unknownWidget": "عنصر الواجهة غير مرئي"
|
||||
},
|
||||
"cancelThisRun": "إلغاء هذا التشغيل",
|
||||
"deleteAllAssets": "حذف جميع الأصول من هذه الجلسة",
|
||||
"downloadAll": "تنزيل الكل",
|
||||
"dragAndDropImage": "اسحب وأسقط صورة",
|
||||
"emptyWorkflowExplanation": "سير العمل الخاص بك فارغ. تحتاج إلى بعض العقد أولاً لبدء بناء التطبيق.",
|
||||
"enterNodeGraph": "دخول مخطط العقد",
|
||||
"giveFeedback": "إعطاء ملاحظات",
|
||||
"graphMode": "وضع الرسم البياني",
|
||||
"hasCreditCost": "يتطلب أرصدة إضافية",
|
||||
"linearMode": "وضع التطبيق",
|
||||
"loadTemplate": "تحميل قالب",
|
||||
"mobileControls": "تعديل وتشغيل",
|
||||
@@ -3373,6 +3376,7 @@
|
||||
"addedToWorkspace": "تمت إضافتك إلى {workspaceName}",
|
||||
"inviteAccepted": "تم قبول الدعوة",
|
||||
"inviteFailed": "فشل في قبول الدعوة",
|
||||
"switchFailed": "فشل في تبديل مساحة العمل. يرجى المحاولة مرة أخرى.",
|
||||
"viewWorkspace": "عرض مساحة العمل"
|
||||
},
|
||||
"workspaceAuth": {
|
||||
|
||||
@@ -400,7 +400,7 @@
|
||||
"name": "تخطيط شريط التبويبات",
|
||||
"options": {
|
||||
"Default": "افتراضي",
|
||||
"Integrated": "مُدمج"
|
||||
"Legacy": "تقليدي"
|
||||
},
|
||||
"tooltip": "يتحكم في تخطيط شريط التبويبات. \"مُدمج\" ينقل عناصر المساعدة والتحكمات الخاصة بالمستخدم إلى منطقة شريط التبويبات."
|
||||
},
|
||||
|
||||
@@ -398,10 +398,10 @@
|
||||
},
|
||||
"Comfy_UI_TabBarLayout": {
|
||||
"name": "Tab Bar Layout",
|
||||
"tooltip": "Controls the layout of the tab bar. \"Integrated\" moves Help and User controls into the tab bar area.",
|
||||
"tooltip": "Controls the elements contained in the integrated tab bar.",
|
||||
"options": {
|
||||
"Default": "Default",
|
||||
"Integrated": "Integrated"
|
||||
"Legacy": "Legacy"
|
||||
}
|
||||
},
|
||||
"Comfy_UseNewMenu": {
|
||||
|
||||
@@ -952,6 +952,7 @@
|
||||
"collapseAll": "Colapsar todo",
|
||||
"color": "Color",
|
||||
"comfy": "Comfy",
|
||||
"comfyCloud": "Comfy Cloud",
|
||||
"comfyOrgLogoAlt": "Logo de ComfyOrg",
|
||||
"comingSoon": "Próximamente",
|
||||
"command": "Comando",
|
||||
@@ -1459,12 +1460,14 @@
|
||||
"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",
|
||||
@@ -3373,6 +3376,7 @@
|
||||
"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": {
|
||||
|
||||
@@ -400,7 +400,7 @@
|
||||
"name": "Diseño de barra de pestañas",
|
||||
"options": {
|
||||
"Default": "Predeterminado",
|
||||
"Integrated": "Integrado"
|
||||
"Legacy": "Clásico"
|
||||
},
|
||||
"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."
|
||||
},
|
||||
|
||||
@@ -952,6 +952,7 @@
|
||||
"collapseAll": "بستن همه",
|
||||
"color": "رنگ",
|
||||
"comfy": "Comfy",
|
||||
"comfyCloud": "Comfy Cloud",
|
||||
"comfyOrgLogoAlt": "لوگوی ComfyOrg",
|
||||
"comingSoon": "بهزودی",
|
||||
"command": "دستور",
|
||||
@@ -1459,12 +1460,14 @@
|
||||
"unknownWidget": "ویجت قابل مشاهده نیست"
|
||||
},
|
||||
"cancelThisRun": "لغو این اجرا",
|
||||
"deleteAllAssets": "حذف همه داراییها از این اجرا",
|
||||
"downloadAll": "دانلود همه",
|
||||
"dragAndDropImage": "تصویر را بکشید و رها کنید",
|
||||
"emptyWorkflowExplanation": "جریان کاری شما خالی است. ابتدا باید چند node اضافه کنید تا بتوانید یک برنامه بسازید.",
|
||||
"enterNodeGraph": "ورود به گراف node",
|
||||
"giveFeedback": "ارسال بازخورد",
|
||||
"graphMode": "حالت گراف",
|
||||
"hasCreditCost": "نیازمند اعتبار اضافی",
|
||||
"linearMode": "حالت برنامه",
|
||||
"loadTemplate": "بارگذاری قالب",
|
||||
"mobileControls": "ویرایش و اجرا",
|
||||
@@ -3385,6 +3388,7 @@
|
||||
"addedToWorkspace": "شما به {workspaceName} اضافه شدید",
|
||||
"inviteAccepted": "دعوت پذیرفته شد",
|
||||
"inviteFailed": "پذیرش دعوت ناموفق بود",
|
||||
"switchFailed": "تغییر ورکاسپیس ناموفق بود. لطفاً دوباره تلاش کنید.",
|
||||
"viewWorkspace": "مشاهده workspace"
|
||||
},
|
||||
"workspaceAuth": {
|
||||
|
||||
@@ -400,7 +400,7 @@
|
||||
"name": "چیدمان نوار تب",
|
||||
"options": {
|
||||
"Default": "پیشفرض",
|
||||
"Integrated": "یکپارچه"
|
||||
"Legacy": "قدیمی"
|
||||
},
|
||||
"tooltip": "چیدمان نوار تب را کنترل میکند. «یکپارچه» کنترلهای راهنما و کاربر را به ناحیه نوار تب منتقل میکند."
|
||||
},
|
||||
|
||||
@@ -952,6 +952,7 @@
|
||||
"collapseAll": "Tout réduire",
|
||||
"color": "Couleur",
|
||||
"comfy": "Comfy",
|
||||
"comfyCloud": "Comfy Cloud",
|
||||
"comfyOrgLogoAlt": "Logo ComfyOrg",
|
||||
"comingSoon": "Bientôt disponible",
|
||||
"command": "Commande",
|
||||
@@ -1459,12 +1460,14 @@
|
||||
"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",
|
||||
@@ -3373,6 +3376,7 @@
|
||||
"addedToWorkspace": "Vous avez été ajouté à {workspaceName}",
|
||||
"inviteAccepted": "Invitation acceptée",
|
||||
"inviteFailed": "Échec de l'acceptation de l'invitation",
|
||||
"switchFailed": "Échec du changement d’espace de travail. Veuillez réessayer.",
|
||||
"viewWorkspace": "Voir l’espace de travail"
|
||||
},
|
||||
"workspaceAuth": {
|
||||
|
||||
@@ -400,7 +400,7 @@
|
||||
"name": "Disposition de la barre d’onglets",
|
||||
"options": {
|
||||
"Default": "Par défaut",
|
||||
"Integrated": "Intégrée"
|
||||
"Legacy": "Héritage"
|
||||
},
|
||||
"tooltip": "Contrôle la disposition de la barre d’onglets. « Intégrée » déplace les contrôles Aide et Utilisateur dans la zone de la barre d’onglets."
|
||||
},
|
||||
|
||||
@@ -952,6 +952,7 @@
|
||||
"collapseAll": "すべて折りたたむ",
|
||||
"color": "色",
|
||||
"comfy": "Comfy",
|
||||
"comfyCloud": "Comfy Cloud",
|
||||
"comfyOrgLogoAlt": "ComfyOrgロゴ",
|
||||
"comingSoon": "近日公開",
|
||||
"command": "コマンド",
|
||||
@@ -1459,12 +1460,14 @@
|
||||
"unknownWidget": "ウィジェットが表示されていません"
|
||||
},
|
||||
"cancelThisRun": "この実行をキャンセル",
|
||||
"deleteAllAssets": "この実行からすべてのアセットを削除",
|
||||
"downloadAll": "すべてダウンロード",
|
||||
"dragAndDropImage": "画像をドラッグ&ドロップ",
|
||||
"emptyWorkflowExplanation": "ワークフローが空です。アプリを作成するには、まずノードを追加してください。",
|
||||
"enterNodeGraph": "ノードグラフに入る",
|
||||
"giveFeedback": "フィードバックを送る",
|
||||
"graphMode": "グラフモード",
|
||||
"hasCreditCost": "追加クレジットが必要です",
|
||||
"linearMode": "アプリモード",
|
||||
"loadTemplate": "テンプレートを読み込む",
|
||||
"mobileControls": "編集と実行",
|
||||
@@ -3373,6 +3376,7 @@
|
||||
"addedToWorkspace": "{workspaceName}に追加されました",
|
||||
"inviteAccepted": "招待を承諾しました",
|
||||
"inviteFailed": "招待の承諾に失敗しました",
|
||||
"switchFailed": "ワークスペースの切り替えに失敗しました。もう一度お試しください。",
|
||||
"viewWorkspace": "ワークスペースを見る"
|
||||
},
|
||||
"workspaceAuth": {
|
||||
|
||||
@@ -400,7 +400,7 @@
|
||||
"name": "タブバーのレイアウト",
|
||||
"options": {
|
||||
"Default": "デフォルト",
|
||||
"Integrated": "統合"
|
||||
"Legacy": "レガシー"
|
||||
},
|
||||
"tooltip": "タブバーのレイアウトを制御します。「統合」を選択すると、ヘルプとユーザーコントロールがタブバーエリアに移動します。"
|
||||
},
|
||||
|
||||
@@ -952,6 +952,7 @@
|
||||
"collapseAll": "모두 접기",
|
||||
"color": "색상",
|
||||
"comfy": "Comfy",
|
||||
"comfyCloud": "Comfy Cloud",
|
||||
"comfyOrgLogoAlt": "ComfyOrg 로고",
|
||||
"comingSoon": "곧 출시 예정",
|
||||
"command": "명령",
|
||||
@@ -1459,12 +1460,14 @@
|
||||
"unknownWidget": "위젯이 표시되지 않습니다"
|
||||
},
|
||||
"cancelThisRun": "이 실행 취소",
|
||||
"deleteAllAssets": "이 실행에서 모든 에셋 삭제",
|
||||
"downloadAll": "모두 다운로드",
|
||||
"dragAndDropImage": "이미지를 드래그 앤 드롭하세요",
|
||||
"emptyWorkflowExplanation": "워크플로우가 비어 있습니다. 앱을 만들려면 먼저 노드를 추가해야 합니다.",
|
||||
"enterNodeGraph": "노드 그래프로 진입",
|
||||
"giveFeedback": "피드백 보내기",
|
||||
"graphMode": "그래프 모드",
|
||||
"hasCreditCost": "추가 크레딧 필요",
|
||||
"linearMode": "앱 모드",
|
||||
"loadTemplate": "템플릿 불러오기",
|
||||
"mobileControls": "편집 및 실행",
|
||||
@@ -3373,6 +3376,7 @@
|
||||
"addedToWorkspace": "{workspaceName} 워크스페이스에 추가되었습니다",
|
||||
"inviteAccepted": "초대 수락됨",
|
||||
"inviteFailed": "초대 수락에 실패했습니다",
|
||||
"switchFailed": "워크스페이스 전환에 실패했습니다. 다시 시도해 주세요.",
|
||||
"viewWorkspace": "워크스페이스 보기"
|
||||
},
|
||||
"workspaceAuth": {
|
||||
|
||||
@@ -400,7 +400,7 @@
|
||||
"name": "탭 바 레이아웃",
|
||||
"options": {
|
||||
"Default": "기본값",
|
||||
"Integrated": "통합"
|
||||
"Legacy": "레거시"
|
||||
},
|
||||
"tooltip": "탭 바의 레이아웃을 제어합니다. \"통합\"을 선택하면 도움말과 사용자 컨트롤이 탭 바 영역으로 이동합니다."
|
||||
},
|
||||
|
||||
@@ -952,6 +952,7 @@
|
||||
"collapseAll": "Recolher tudo",
|
||||
"color": "Cor",
|
||||
"comfy": "Comfy",
|
||||
"comfyCloud": "Comfy Cloud",
|
||||
"comfyOrgLogoAlt": "Logo do ComfyOrg",
|
||||
"comingSoon": "Em breve",
|
||||
"command": "Comando",
|
||||
@@ -1459,12 +1460,14 @@
|
||||
"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",
|
||||
@@ -3385,6 +3388,7 @@
|
||||
"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": {
|
||||
|
||||
@@ -400,7 +400,7 @@
|
||||
"name": "Layout da Barra de Abas",
|
||||
"options": {
|
||||
"Default": "Padrão",
|
||||
"Integrated": "Integrado"
|
||||
"Legacy": "Legado"
|
||||
},
|
||||
"tooltip": "Controla o layout da barra de abas. \"Integrado\" move os controles de Ajuda e Usuário para a área da barra de abas."
|
||||
},
|
||||
|
||||
@@ -952,6 +952,7 @@
|
||||
"collapseAll": "Свернуть все",
|
||||
"color": "Цвет",
|
||||
"comfy": "Comfy",
|
||||
"comfyCloud": "Comfy Cloud",
|
||||
"comfyOrgLogoAlt": "Логотип ComfyOrg",
|
||||
"comingSoon": "Скоро будет",
|
||||
"command": "Команда",
|
||||
@@ -1459,12 +1460,14 @@
|
||||
"unknownWidget": "Виджет не отображается"
|
||||
},
|
||||
"cancelThisRun": "Отменить этот запуск",
|
||||
"deleteAllAssets": "Удалить все ресурсы из этого запуска",
|
||||
"downloadAll": "Скачать всё",
|
||||
"dragAndDropImage": "Перетащите изображение",
|
||||
"emptyWorkflowExplanation": "Ваш рабочий процесс пуст. Сначала добавьте несколько узлов, чтобы начать создавать приложение.",
|
||||
"enterNodeGraph": "Войти в граф узлов",
|
||||
"giveFeedback": "Оставить отзыв",
|
||||
"graphMode": "Графовый режим",
|
||||
"hasCreditCost": "Требуются дополнительные кредиты",
|
||||
"linearMode": "Режим приложения",
|
||||
"loadTemplate": "Загрузить шаблон",
|
||||
"mobileControls": "Редактировать и запустить",
|
||||
@@ -3373,6 +3376,7 @@
|
||||
"addedToWorkspace": "Вы были добавлены в {workspaceName}",
|
||||
"inviteAccepted": "Приглашение принято",
|
||||
"inviteFailed": "Не удалось принять приглашение",
|
||||
"switchFailed": "Не удалось переключить рабочее пространство. Пожалуйста, попробуйте еще раз.",
|
||||
"viewWorkspace": "Просмотреть рабочее пространство"
|
||||
},
|
||||
"workspaceAuth": {
|
||||
|
||||
@@ -400,7 +400,7 @@
|
||||
"name": "Макет панели вкладок",
|
||||
"options": {
|
||||
"Default": "По умолчанию",
|
||||
"Integrated": "Интегрированный"
|
||||
"Legacy": "Классический"
|
||||
},
|
||||
"tooltip": "Управляет расположением панели вкладок. «Интегрированный» перемещает элементы управления Справкой и Пользователем в область панели вкладок."
|
||||
},
|
||||
|
||||
@@ -952,6 +952,7 @@
|
||||
"collapseAll": "Hepsini daralt",
|
||||
"color": "Renk",
|
||||
"comfy": "Comfy",
|
||||
"comfyCloud": "Comfy Cloud",
|
||||
"comfyOrgLogoAlt": "ComfyOrg Logosu",
|
||||
"comingSoon": "Çok Yakında",
|
||||
"command": "Komut",
|
||||
@@ -1459,12 +1460,14 @@
|
||||
"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",
|
||||
@@ -3373,6 +3376,7 @@
|
||||
"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": {
|
||||
|
||||
@@ -400,7 +400,7 @@
|
||||
"name": "Sekme Çubuğu Düzeni",
|
||||
"options": {
|
||||
"Default": "Varsayılan",
|
||||
"Integrated": "Entegre"
|
||||
"Legacy": "Klasik"
|
||||
},
|
||||
"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."
|
||||
},
|
||||
|
||||
@@ -952,6 +952,7 @@
|
||||
"collapseAll": "全部摺疊",
|
||||
"color": "顏色",
|
||||
"comfy": "Comfy",
|
||||
"comfyCloud": "Comfy Cloud",
|
||||
"comfyOrgLogoAlt": "ComfyOrg 標誌",
|
||||
"comingSoon": "即將推出",
|
||||
"command": "指令",
|
||||
@@ -1459,12 +1460,14 @@
|
||||
"unknownWidget": "元件不可見"
|
||||
},
|
||||
"cancelThisRun": "取消本次執行",
|
||||
"deleteAllAssets": "刪除本次運行的所有資產",
|
||||
"downloadAll": "全部下載",
|
||||
"dragAndDropImage": "拖曳圖片到此",
|
||||
"emptyWorkflowExplanation": "您的工作流程目前是空的。您需要先新增一些節點,才能開始建立應用程式。",
|
||||
"enterNodeGraph": "進入節點圖",
|
||||
"giveFeedback": "提供回饋",
|
||||
"graphMode": "圖形模式",
|
||||
"hasCreditCost": "需要額外點數",
|
||||
"linearMode": "App 模式",
|
||||
"loadTemplate": "載入範本",
|
||||
"mobileControls": "編輯與執行",
|
||||
@@ -3373,6 +3376,7 @@
|
||||
"addedToWorkspace": "你已被加入 {workspaceName}",
|
||||
"inviteAccepted": "已接受邀請",
|
||||
"inviteFailed": "接受邀請失敗",
|
||||
"switchFailed": "切換工作區失敗。請再試一次。",
|
||||
"viewWorkspace": "檢視工作區"
|
||||
},
|
||||
"workspaceAuth": {
|
||||
|
||||
@@ -400,7 +400,7 @@
|
||||
"name": "分頁列佈局",
|
||||
"options": {
|
||||
"Default": "預設",
|
||||
"Integrated": "整合"
|
||||
"Legacy": "傳統"
|
||||
},
|
||||
"tooltip": "控制分頁列的佈局。「整合」會將說明和使用者控制項移至分頁列區域。"
|
||||
},
|
||||
|
||||
@@ -952,6 +952,7 @@
|
||||
"collapseAll": "全部折叠",
|
||||
"color": "颜色",
|
||||
"comfy": "舒适",
|
||||
"comfyCloud": "Comfy Cloud",
|
||||
"comfyOrgLogoAlt": "ComfyOrg 徽标",
|
||||
"comingSoon": "即将推出",
|
||||
"command": "指令",
|
||||
@@ -1459,12 +1460,14 @@
|
||||
"unknownWidget": "组件不可见"
|
||||
},
|
||||
"cancelThisRun": "取消本次运行",
|
||||
"deleteAllAssets": "删除本次运行的所有资源",
|
||||
"downloadAll": "全部下载",
|
||||
"dragAndDropImage": "拖拽图片到此处",
|
||||
"emptyWorkflowExplanation": "你的工作流为空。你需要先添加一些节点,才能开始构建应用。",
|
||||
"enterNodeGraph": "进入节点图",
|
||||
"giveFeedback": "提供反馈",
|
||||
"graphMode": "图形模式",
|
||||
"hasCreditCost": "需要额外积分",
|
||||
"linearMode": "App 模式",
|
||||
"loadTemplate": "加载模板",
|
||||
"mobileControls": "编辑与运行",
|
||||
@@ -3385,6 +3388,7 @@
|
||||
"addedToWorkspace": "您已被加入 {workspaceName}",
|
||||
"inviteAccepted": "邀请已接受",
|
||||
"inviteFailed": "接受邀请失败",
|
||||
"switchFailed": "切换工作区失败。请重试。",
|
||||
"viewWorkspace": "查看工作区"
|
||||
},
|
||||
"workspaceAuth": {
|
||||
|
||||
@@ -400,7 +400,7 @@
|
||||
"name": "标签栏布局",
|
||||
"options": {
|
||||
"Default": "默认",
|
||||
"Integrated": "集成"
|
||||
"Legacy": "传统"
|
||||
},
|
||||
"tooltip": "控制标签栏的布局。“集成”会将帮助和用户控件移动到标签栏区域。"
|
||||
},
|
||||
|
||||
@@ -84,8 +84,7 @@ export function useMediaAssetActions() {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: t('g.error'),
|
||||
detail: t('g.failedToDownloadImage'),
|
||||
life: 3000
|
||||
detail: t('g.failedToDownloadImage')
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -126,8 +125,7 @@ export function useMediaAssetActions() {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: t('g.error'),
|
||||
detail: t('g.failedToDownloadImage'),
|
||||
life: 3000
|
||||
detail: t('g.failedToDownloadImage')
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -182,8 +180,7 @@ export function useMediaAssetActions() {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: t('g.error'),
|
||||
detail: t('exportToast.exportFailedSingle'),
|
||||
life: 3000
|
||||
detail: t('exportToast.exportFailedSingle')
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -238,8 +235,7 @@ export function useMediaAssetActions() {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: t('g.error'),
|
||||
detail: t('mediaAsset.nodeTypeNotFound', { nodeType }),
|
||||
life: 3000
|
||||
detail: t('mediaAsset.nodeTypeNotFound', { nodeType })
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -252,8 +248,7 @@ export function useMediaAssetActions() {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: t('g.error'),
|
||||
detail: t('mediaAsset.failedToCreateNode'),
|
||||
life: 3000
|
||||
detail: t('mediaAsset.failedToCreateNode')
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -443,8 +438,7 @@ export function useMediaAssetActions() {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: t('g.error'),
|
||||
detail: t('mediaAsset.selection.failedToAddNodes'),
|
||||
life: 3000
|
||||
detail: t('mediaAsset.selection.failedToAddNodes')
|
||||
})
|
||||
} else {
|
||||
toast.add({
|
||||
@@ -676,8 +670,7 @@ export function useMediaAssetActions() {
|
||||
summary: t('g.error'),
|
||||
detail: isSingle
|
||||
? t('mediaAsset.failedToDeleteAsset')
|
||||
: t('mediaAsset.selection.failedToDeleteAssets'),
|
||||
life: 3000
|
||||
: t('mediaAsset.selection.failedToDeleteAssets')
|
||||
})
|
||||
} else {
|
||||
// Partial success (only possible with multiple assets)
|
||||
@@ -698,8 +691,7 @@ export function useMediaAssetActions() {
|
||||
summary: t('g.error'),
|
||||
detail: isSingle
|
||||
? t('mediaAsset.failedToDeleteAsset')
|
||||
: t('mediaAsset.selection.failedToDeleteAssets'),
|
||||
life: 3000
|
||||
: t('mediaAsset.selection.failedToDeleteAssets')
|
||||
})
|
||||
} finally {
|
||||
// Hide loading overlay for all assets
|
||||
|
||||
@@ -73,8 +73,7 @@ export function createAssetWidget(
|
||||
toastStore.add({
|
||||
severity: 'error',
|
||||
summary: t('assetBrowser.invalidAsset'),
|
||||
detail: t('assetBrowser.invalidAssetDetail'),
|
||||
life: 5000
|
||||
detail: t('assetBrowser.invalidAssetDetail')
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -92,8 +91,7 @@ export function createAssetWidget(
|
||||
toastStore.add({
|
||||
severity: 'error',
|
||||
summary: t('assetBrowser.invalidFilename'),
|
||||
detail: t('assetBrowser.invalidFilenameDetail'),
|
||||
life: 5000
|
||||
detail: t('assetBrowser.invalidFilenameDetail')
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -317,8 +317,7 @@ export function useNodeReplacement() {
|
||||
toastStore.add({
|
||||
severity: 'error',
|
||||
summary: t('g.error', 'Error'),
|
||||
detail: t('nodeReplacement.replaceFailed', 'Failed to replace nodes'),
|
||||
life: 5000
|
||||
detail: t('nodeReplacement.replaceFailed', 'Failed to replace nodes')
|
||||
})
|
||||
return replacedTypes
|
||||
} finally {
|
||||
|
||||
@@ -83,8 +83,7 @@ describe('useSecrets', () => {
|
||||
expect(mockAdd).toHaveBeenCalledWith({
|
||||
severity: 'error',
|
||||
summary: 'g.error',
|
||||
detail: 'Network error',
|
||||
life: 5000
|
||||
detail: 'Network error'
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -130,8 +129,7 @@ describe('useSecrets', () => {
|
||||
expect(mockAdd).toHaveBeenCalledWith({
|
||||
severity: 'error',
|
||||
summary: 'g.error',
|
||||
detail: 'Delete failed',
|
||||
life: 5000
|
||||
detail: 'Delete failed'
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -33,16 +33,14 @@ export function useSecrets() {
|
||||
toastStore.add({
|
||||
severity: 'error',
|
||||
summary: t('g.error'),
|
||||
detail: err.message,
|
||||
life: 5000
|
||||
detail: err.message
|
||||
})
|
||||
} else {
|
||||
console.error('Unexpected error fetching secrets:', err)
|
||||
toastStore.add({
|
||||
severity: 'error',
|
||||
summary: t('g.error'),
|
||||
detail: t('g.unknownError'),
|
||||
life: 5000
|
||||
detail: t('g.unknownError')
|
||||
})
|
||||
}
|
||||
} finally {
|
||||
@@ -60,16 +58,14 @@ export function useSecrets() {
|
||||
toastStore.add({
|
||||
severity: 'error',
|
||||
summary: t('g.error'),
|
||||
detail: err.message,
|
||||
life: 5000
|
||||
detail: err.message
|
||||
})
|
||||
} else {
|
||||
console.error('Unexpected error deleting secret:', err)
|
||||
toastStore.add({
|
||||
severity: 'error',
|
||||
summary: t('g.error'),
|
||||
detail: t('g.unknownError'),
|
||||
life: 5000
|
||||
detail: t('g.unknownError')
|
||||
})
|
||||
}
|
||||
} finally {
|
||||
|
||||
@@ -370,8 +370,7 @@ export const useWorkflowService = () => {
|
||||
toastStore.add({
|
||||
severity: 'error',
|
||||
summary: t('g.error'),
|
||||
detail: t('toastMessages.failedToSaveDraft'),
|
||||
life: 3000
|
||||
detail: t('toastMessages.failedToSaveDraft')
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,8 +112,7 @@ export function useWorkflowPersistenceV2() {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: t('g.error'),
|
||||
detail: t('toastMessages.failedToSaveDraft'),
|
||||
life: 3000
|
||||
detail: t('toastMessages.failedToSaveDraft')
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -484,8 +484,7 @@ describe('ShareWorkflowDialogContent', () => {
|
||||
expect(mockToast.add).toHaveBeenCalledWith({
|
||||
severity: 'error',
|
||||
summary: 'Error',
|
||||
detail: 'Publish failed',
|
||||
life: 5000
|
||||
detail: 'Publish failed'
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -352,8 +352,7 @@ const { isLoading: isSaving, execute: handleSave } = useAsyncState(
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: t('shareWorkflow.saveFailedTitle'),
|
||||
detail: t('shareWorkflow.saveFailedDescription'),
|
||||
life: 5000
|
||||
detail: t('shareWorkflow.saveFailedDescription')
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -391,8 +390,7 @@ const {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: t('g.error'),
|
||||
detail: error instanceof Error ? error.message : t('g.error'),
|
||||
life: 5000
|
||||
detail: error instanceof Error ? error.message : t('g.error')
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -183,8 +183,7 @@ async function handleCreate() {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: t('g.error'),
|
||||
detail: error instanceof Error ? error.message : t('g.error'),
|
||||
life: 5000
|
||||
detail: error instanceof Error ? error.message : t('g.error')
|
||||
})
|
||||
} finally {
|
||||
isCreating.value = false
|
||||
|
||||
@@ -338,8 +338,7 @@ describe('useSharedWorkflowUrlLoader', () => {
|
||||
expect(mockToastAdd).toHaveBeenCalledWith({
|
||||
severity: 'error',
|
||||
summary: 'Error',
|
||||
detail: 'Failed to load shared workflow',
|
||||
life: 3000
|
||||
detail: 'Failed to load shared workflow'
|
||||
})
|
||||
expect(mockRouterReplace).toHaveBeenCalledWith({ query: {} })
|
||||
expect(preservedQueryMocks.clearPreservedQuery).toHaveBeenCalledWith(
|
||||
|
||||
@@ -118,8 +118,7 @@ export function useSharedWorkflowUrlLoader() {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: t('g.error'),
|
||||
detail: t('shareWorkflow.loadFailed'),
|
||||
life: 3000
|
||||
detail: t('shareWorkflow.loadFailed')
|
||||
})
|
||||
cleanupUrlParams()
|
||||
clearPreservedQuery(SHARE_NAMESPACE)
|
||||
@@ -148,8 +147,7 @@ export function useSharedWorkflowUrlLoader() {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: t('g.error'),
|
||||
detail: t('shareWorkflow.loadFailed'),
|
||||
life: 5000
|
||||
detail: t('shareWorkflow.loadFailed')
|
||||
})
|
||||
return 'failed'
|
||||
}
|
||||
|
||||
@@ -145,8 +145,7 @@ describe('useTemplateUrlLoader', () => {
|
||||
expect(mockToastAdd).toHaveBeenCalledWith({
|
||||
severity: 'error',
|
||||
summary: 'Error',
|
||||
detail: 'Template "invalid-template" not found',
|
||||
life: 3000
|
||||
detail: 'Template "invalid-template" not found'
|
||||
})
|
||||
})
|
||||
|
||||
@@ -239,8 +238,7 @@ describe('useTemplateUrlLoader', () => {
|
||||
expect(mockToastAdd).toHaveBeenCalledWith({
|
||||
severity: 'error',
|
||||
summary: 'Error',
|
||||
detail: 'Failed to load template',
|
||||
life: 3000
|
||||
detail: 'Failed to load template'
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -117,8 +117,7 @@ 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
|
||||
@@ -132,8 +131,7 @@ export function useTemplateUrlLoader() {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: t('g.error'),
|
||||
detail: t('g.errorLoadingTemplate'),
|
||||
life: 3000
|
||||
detail: t('g.errorLoadingTemplate')
|
||||
})
|
||||
} finally {
|
||||
cleanupUrlParams()
|
||||
|
||||
@@ -428,8 +428,7 @@ async function handleResubscribe() {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: t('g.error'),
|
||||
detail: message,
|
||||
life: 5000
|
||||
detail: message
|
||||
})
|
||||
} finally {
|
||||
isResubscribing.value = false
|
||||
|
||||
@@ -148,8 +148,7 @@ async function handleSubscribeClick(payload: {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Unable to subscribe',
|
||||
detail: 'This plan is not available',
|
||||
life: 5000
|
||||
detail: 'This plan is not available'
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -159,8 +158,7 @@ async function handleSubscribeClick(payload: {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Unable to subscribe',
|
||||
detail: response?.reason || 'This plan is not available',
|
||||
life: 5000
|
||||
detail: response?.reason || 'This plan is not available'
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -175,8 +173,7 @@ async function handleSubscribeClick(payload: {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Error',
|
||||
detail: message,
|
||||
life: 5000
|
||||
detail: message
|
||||
})
|
||||
} finally {
|
||||
isLoadingPreview.value = false
|
||||
@@ -236,8 +233,7 @@ async function handleAddCreditCard() {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Error',
|
||||
detail: message,
|
||||
life: 5000
|
||||
detail: message
|
||||
})
|
||||
} finally {
|
||||
isSubscribing.value = false
|
||||
@@ -291,8 +287,7 @@ async function handleConfirmTransition() {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Error',
|
||||
detail: message,
|
||||
life: 5000
|
||||
detail: message
|
||||
})
|
||||
} finally {
|
||||
isSubscribing.value = false
|
||||
@@ -316,8 +311,7 @@ async function handleResubscribe() {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Error',
|
||||
detail: message,
|
||||
life: 5000
|
||||
detail: message
|
||||
})
|
||||
} finally {
|
||||
isResubscribing.value = false
|
||||
|
||||
@@ -273,8 +273,7 @@ async function handleBuy() {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: t('credits.topUp.purchaseError'),
|
||||
detail: t('credits.topUp.unknownError'),
|
||||
life: 5000
|
||||
detail: t('credits.topUp.unknownError')
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -285,8 +284,7 @@ async function handleBuy() {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: t('credits.topUp.purchaseError'),
|
||||
detail: t('credits.topUp.purchaseErrorDetail', { error: errorMessage }),
|
||||
life: 5000
|
||||
detail: t('credits.topUp.purchaseErrorDetail', { error: errorMessage })
|
||||
})
|
||||
} finally {
|
||||
loading.value = false
|
||||
|
||||
@@ -102,8 +102,7 @@ async function onCreate() {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: t('workspacePanel.toast.failedToCreateWorkspace'),
|
||||
detail: error instanceof Error ? error.message : t('g.unknownError'),
|
||||
life: 5000
|
||||
detail: error instanceof Error ? error.message : t('g.unknownError')
|
||||
})
|
||||
} finally {
|
||||
loading.value = false
|
||||
|
||||
@@ -79,8 +79,7 @@ async function onDelete() {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: t('workspacePanel.toast.failedToDeleteWorkspace'),
|
||||
detail: error instanceof Error ? error.message : t('g.unknownError'),
|
||||
life: 5000
|
||||
detail: error instanceof Error ? error.message : t('g.unknownError')
|
||||
})
|
||||
} finally {
|
||||
loading.value = false
|
||||
|
||||
@@ -94,8 +94,7 @@ async function onSave() {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: t('workspacePanel.toast.failedToUpdateWorkspace'),
|
||||
detail: error instanceof Error ? error.message : t('g.unknownError'),
|
||||
life: 5000
|
||||
detail: error instanceof Error ? error.message : t('g.unknownError')
|
||||
})
|
||||
} finally {
|
||||
loading.value = false
|
||||
|
||||
@@ -138,8 +138,7 @@ async function onCreateLink() {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: t('workspacePanel.inviteMemberDialog.linkCopyFailed'),
|
||||
detail: error instanceof Error ? error.message : undefined,
|
||||
life: 3000
|
||||
detail: error instanceof Error ? error.message : undefined
|
||||
})
|
||||
} finally {
|
||||
loading.value = false
|
||||
@@ -161,8 +160,7 @@ async function onCopyLink() {
|
||||
} catch {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: t('workspacePanel.inviteMemberDialog.linkCopyFailed'),
|
||||
life: 3000
|
||||
summary: t('workspacePanel.inviteMemberDialog.linkCopyFailed')
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,8 +68,7 @@ async function onLeave() {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: t('workspacePanel.toast.failedToLeaveWorkspace'),
|
||||
detail: error instanceof Error ? error.message : t('g.unknownError'),
|
||||
life: 5000
|
||||
detail: error instanceof Error ? error.message : t('g.unknownError')
|
||||
})
|
||||
} finally {
|
||||
loading.value = false
|
||||
|
||||
@@ -73,8 +73,7 @@ async function onRemove() {
|
||||
} catch {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: t('workspacePanel.removeMemberDialog.error'),
|
||||
life: 3000
|
||||
summary: t('workspacePanel.removeMemberDialog.error')
|
||||
})
|
||||
} finally {
|
||||
loading.value = false
|
||||
|
||||
@@ -69,8 +69,7 @@ async function onRevoke() {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: t('g.error'),
|
||||
detail: error instanceof Error ? error.message : undefined,
|
||||
life: 3000
|
||||
detail: error instanceof Error ? error.message : undefined
|
||||
})
|
||||
} finally {
|
||||
loading.value = false
|
||||
|
||||
@@ -543,8 +543,7 @@ async function handleCopyInviteLink(invite: PendingInvite) {
|
||||
} catch {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: t('g.error'),
|
||||
life: 3000
|
||||
summary: t('g.error')
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -151,8 +151,7 @@ describe('useInviteUrlLoader', () => {
|
||||
expect(mockToastAdd).toHaveBeenCalledWith({
|
||||
severity: 'error',
|
||||
summary: 'Failed to Accept Invite',
|
||||
detail: 'Invalid invite',
|
||||
life: 5000
|
||||
detail: 'Invalid invite'
|
||||
})
|
||||
})
|
||||
|
||||
@@ -211,8 +210,7 @@ describe('useInviteUrlLoader', () => {
|
||||
expect(mockToastAdd).toHaveBeenCalledWith({
|
||||
severity: 'error',
|
||||
summary: 'Failed to Accept Invite',
|
||||
detail: 'Invalid token',
|
||||
life: 5000
|
||||
detail: 'Invalid token'
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -97,8 +97,7 @@ export function useInviteUrlLoader() {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: t('workspace.inviteFailed'),
|
||||
detail: error instanceof Error ? error.message : t('g.unknownError'),
|
||||
life: 5000
|
||||
detail: error instanceof Error ? error.message : t('g.unknownError')
|
||||
})
|
||||
} finally {
|
||||
cleanupUrlParams()
|
||||
|
||||
@@ -219,8 +219,7 @@ describe('billingOperationStore', () => {
|
||||
expect(mockToastAdd).toHaveBeenCalledWith({
|
||||
severity: 'error',
|
||||
summary: 'billingOperation.subscriptionFailed',
|
||||
detail: errorMessage,
|
||||
life: 5000
|
||||
detail: errorMessage
|
||||
})
|
||||
})
|
||||
|
||||
@@ -239,8 +238,7 @@ describe('billingOperationStore', () => {
|
||||
expect(mockToastAdd).toHaveBeenCalledWith({
|
||||
severity: 'error',
|
||||
summary: 'billingOperation.topupFailed',
|
||||
detail: undefined,
|
||||
life: 5000
|
||||
detail: undefined
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -267,8 +265,7 @@ describe('billingOperationStore', () => {
|
||||
|
||||
expect(mockToastAdd).toHaveBeenCalledWith({
|
||||
severity: 'error',
|
||||
summary: 'billingOperation.subscriptionTimeout',
|
||||
life: 5000
|
||||
summary: 'billingOperation.subscriptionTimeout'
|
||||
})
|
||||
})
|
||||
|
||||
@@ -287,8 +284,7 @@ describe('billingOperationStore', () => {
|
||||
|
||||
expect(mockToastAdd).toHaveBeenCalledWith({
|
||||
severity: 'error',
|
||||
summary: 'billingOperation.topupTimeout',
|
||||
life: 5000
|
||||
summary: 'billingOperation.topupTimeout'
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -173,8 +173,7 @@ export const useBillingOperationStore = defineStore('billingOperation', () => {
|
||||
useToastStore().add({
|
||||
severity: 'error',
|
||||
summary: defaultMessage,
|
||||
detail: errorMessage ?? undefined,
|
||||
life: 5000
|
||||
detail: errorMessage ?? undefined
|
||||
})
|
||||
}
|
||||
|
||||
@@ -192,8 +191,7 @@ export const useBillingOperationStore = defineStore('billingOperation', () => {
|
||||
|
||||
useToastStore().add({
|
||||
severity: 'error',
|
||||
summary: message,
|
||||
life: 5000
|
||||
summary: message
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
useInfiniteScroll,
|
||||
useResizeObserver
|
||||
} from '@vueuse/core'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import type { ComponentPublicInstance } from 'vue'
|
||||
import {
|
||||
computed,
|
||||
@@ -26,11 +27,13 @@ import type {
|
||||
import OutputPreviewItem from '@/renderer/extensions/linearMode/OutputPreviewItem.vue'
|
||||
import { useOutputHistory } from '@/renderer/extensions/linearMode/useOutputHistory'
|
||||
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
|
||||
import { useAppModeStore } from '@/stores/appModeStore'
|
||||
import { useQueueStore } from '@/stores/queueStore'
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
const { outputs, allOutputs, selectFirstHistory, mayBeActiveWorkflowPending } =
|
||||
useOutputHistory()
|
||||
const { hasOutputs } = storeToRefs(useAppModeStore())
|
||||
const queueStore = useQueueStore()
|
||||
const store = useLinearOutputStore()
|
||||
const workflowStore = useWorkflowStore()
|
||||
@@ -156,8 +159,10 @@ watch(
|
||||
const inProgress = store.activeWorkflowInProgressItems
|
||||
if (inProgress.length > 0) {
|
||||
store.selectAsLatest(`slot:${inProgress[0].id}`)
|
||||
} else {
|
||||
} else if (hasOutputs.value) {
|
||||
selectFirstHistory()
|
||||
} else {
|
||||
store.selectAsLatest(null)
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
@@ -180,13 +185,13 @@ watch(
|
||||
: undefined
|
||||
|
||||
if (!sv || sv.kind !== 'history') {
|
||||
selectFirstHistory()
|
||||
if (hasOutputs.value) selectFirstHistory()
|
||||
return
|
||||
}
|
||||
|
||||
const wasFirst = sv.assetId === oldAssets[0]?.id
|
||||
if (wasFirst || !newAssets.some((a) => a.id === sv.assetId)) {
|
||||
selectFirstHistory()
|
||||
if (hasOutputs.value) selectFirstHistory()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
@@ -1,85 +1,40 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { flattenNodeOutput } from '@/renderer/extensions/linearMode/flattenNodeOutput'
|
||||
import type { NodeExecutionOutput } from '@/schemas/apiSchema'
|
||||
|
||||
function makeOutput(
|
||||
overrides: Partial<NodeExecutionOutput> = {}
|
||||
): NodeExecutionOutput {
|
||||
return { ...overrides }
|
||||
}
|
||||
vi.mock('@/scripts/api', () => ({
|
||||
api: {
|
||||
apiURL: vi.fn((path: string) => `/api${path}`),
|
||||
addEventListener: vi.fn(),
|
||||
removeEventListener: vi.fn()
|
||||
}
|
||||
}))
|
||||
|
||||
describe(flattenNodeOutput, () => {
|
||||
it('returns empty array for output with no known media types', () => {
|
||||
const result = flattenNodeOutput(['1', makeOutput({ text: 'hello' })])
|
||||
expect(result).toEqual([])
|
||||
})
|
||||
|
||||
it('flattens images into ResultItemImpl instances', () => {
|
||||
const output = makeOutput({
|
||||
images: [
|
||||
{ filename: 'a.png', subfolder: '', type: 'output' },
|
||||
{ filename: 'b.png', subfolder: 'sub', type: 'output' }
|
||||
]
|
||||
})
|
||||
it('delegates to shared parser and returns ResultItemImpl instances', () => {
|
||||
const output: NodeExecutionOutput = {
|
||||
images: [{ filename: 'a.png', subfolder: '', type: 'output' }]
|
||||
}
|
||||
|
||||
const result = flattenNodeOutput(['42', output])
|
||||
|
||||
expect(result).toHaveLength(2)
|
||||
expect(result).toHaveLength(1)
|
||||
expect(result[0].filename).toBe('a.png')
|
||||
expect(result[0].nodeId).toBe('42')
|
||||
expect(result[0].mediaType).toBe('images')
|
||||
expect(result[1].filename).toBe('b.png')
|
||||
expect(result[1].subfolder).toBe('sub')
|
||||
})
|
||||
|
||||
it('flattens audio outputs', () => {
|
||||
const output = makeOutput({
|
||||
audio: [{ filename: 'sound.wav', subfolder: '', type: 'output' }]
|
||||
})
|
||||
it('supports non-standard output keys', () => {
|
||||
const output = {
|
||||
a_images: [{ filename: 'before.png', subfolder: '', type: 'output' }],
|
||||
b_images: [{ filename: 'after.png', subfolder: '', type: 'output' }]
|
||||
} as unknown as NodeExecutionOutput
|
||||
|
||||
const result = flattenNodeOutput([7, output])
|
||||
|
||||
expect(result).toHaveLength(1)
|
||||
expect(result[0].mediaType).toBe('audio')
|
||||
expect(result[0].nodeId).toBe(7)
|
||||
})
|
||||
|
||||
it('flattens multiple media types in a single output', () => {
|
||||
const output = makeOutput({
|
||||
images: [{ filename: 'img.png', subfolder: '', type: 'output' }],
|
||||
video: [{ filename: 'vid.mp4', subfolder: '', type: 'output' }]
|
||||
})
|
||||
|
||||
const result = flattenNodeOutput(['1', output])
|
||||
const result = flattenNodeOutput(['10', output])
|
||||
|
||||
expect(result).toHaveLength(2)
|
||||
const types = result.map((r) => r.mediaType)
|
||||
expect(types).toContain('images')
|
||||
expect(types).toContain('video')
|
||||
})
|
||||
|
||||
it('handles gifs and 3d output types', () => {
|
||||
const output = makeOutput({
|
||||
gifs: [
|
||||
{ filename: 'anim.gif', subfolder: '', type: 'output' }
|
||||
] as NodeExecutionOutput['gifs'],
|
||||
'3d': [
|
||||
{ filename: 'model.glb', subfolder: '', type: 'output' }
|
||||
] as NodeExecutionOutput['3d']
|
||||
})
|
||||
|
||||
const result = flattenNodeOutput(['5', output])
|
||||
|
||||
expect(result).toHaveLength(2)
|
||||
const types = result.map((r) => r.mediaType)
|
||||
expect(types).toContain('gifs')
|
||||
expect(types).toContain('3d')
|
||||
})
|
||||
|
||||
it('ignores empty arrays', () => {
|
||||
const output = makeOutput({ images: [], audio: [] })
|
||||
const result = flattenNodeOutput(['1', output])
|
||||
expect(result).toEqual([])
|
||||
expect(result.map((r) => r.filename)).toContain('before.png')
|
||||
expect(result.map((r) => r.filename)).toContain('after.png')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,20 +1,10 @@
|
||||
import type { NodeExecutionOutput, ResultItem } from '@/schemas/apiSchema'
|
||||
import { ResultItemImpl } from '@/stores/queueStore'
|
||||
import type { NodeExecutionOutput } from '@/schemas/apiSchema'
|
||||
import { flattenNodeExecutionOutput } from '@/stores/resultItemParsing'
|
||||
import type { ResultItemImpl } from '@/stores/queueStore'
|
||||
|
||||
export function flattenNodeOutput([nodeId, nodeOutput]: [
|
||||
string | number,
|
||||
NodeExecutionOutput
|
||||
]): ResultItemImpl[] {
|
||||
const knownOutputs: Record<string, ResultItem[]> = {}
|
||||
if (nodeOutput.audio) knownOutputs.audio = nodeOutput.audio
|
||||
if (nodeOutput.images) knownOutputs.images = nodeOutput.images
|
||||
if (nodeOutput.video) knownOutputs.video = nodeOutput.video
|
||||
if (nodeOutput.gifs) knownOutputs.gifs = nodeOutput.gifs as ResultItem[]
|
||||
if (nodeOutput['3d']) knownOutputs['3d'] = nodeOutput['3d'] as ResultItem[]
|
||||
|
||||
return Object.entries(knownOutputs).flatMap(([mediaType, outputs]) =>
|
||||
outputs.map(
|
||||
(output) => new ResultItemImpl({ ...output, mediaType, nodeId })
|
||||
)
|
||||
)
|
||||
return flattenNodeExecutionOutput(nodeId, nodeOutput)
|
||||
}
|
||||
|
||||
@@ -219,6 +219,7 @@ describe(useOutputHistory, () => {
|
||||
})
|
||||
|
||||
it('returns outputs from metadata allOutputs when count matches', () => {
|
||||
useAppModeStore().selectedOutputs.push('1')
|
||||
const results = [makeResult('a.png'), makeResult('b.png')]
|
||||
const asset = makeAsset('a1', 'job-1', {
|
||||
allOutputs: results,
|
||||
@@ -255,7 +256,7 @@ describe(useOutputHistory, () => {
|
||||
expect(outputs[0].filename).toBe('b.png')
|
||||
})
|
||||
|
||||
it('returns all outputs when no output nodes are selected', () => {
|
||||
it('returns empty when no output nodes are selected', () => {
|
||||
const results = [makeResult('a.png', '1'), makeResult('b.png', '2')]
|
||||
const asset = makeAsset('a1', 'job-1', {
|
||||
allOutputs: results,
|
||||
@@ -265,7 +266,7 @@ describe(useOutputHistory, () => {
|
||||
const { allOutputs } = useOutputHistory()
|
||||
const outputs = allOutputs(asset)
|
||||
|
||||
expect(outputs).toHaveLength(2)
|
||||
expect(outputs).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('returns consistent filtered outputs across repeated calls', () => {
|
||||
@@ -288,6 +289,7 @@ describe(useOutputHistory, () => {
|
||||
})
|
||||
|
||||
it('returns in-progress outputs for pending resolve jobs', () => {
|
||||
useAppModeStore().selectedOutputs.push('1')
|
||||
pendingResolveRef.value = new Set(['job-1'])
|
||||
inProgressItemsRef.value = [
|
||||
{
|
||||
@@ -314,6 +316,7 @@ describe(useOutputHistory, () => {
|
||||
})
|
||||
|
||||
it('fetches full job detail for multi-output jobs', async () => {
|
||||
useAppModeStore().selectedOutputs.push('1')
|
||||
jobDetailResults.set('job-1', {
|
||||
outputs: {
|
||||
'1': {
|
||||
@@ -342,6 +345,7 @@ describe(useOutputHistory, () => {
|
||||
|
||||
describe('watchEffect resolve loop', () => {
|
||||
it('resolves pending jobs when history outputs load', async () => {
|
||||
useAppModeStore().selectedOutputs.push('1')
|
||||
const results = [makeResult('a.png')]
|
||||
const asset = makeAsset('a1', 'job-1', {
|
||||
allOutputs: results,
|
||||
@@ -360,6 +364,7 @@ describe(useOutputHistory, () => {
|
||||
})
|
||||
|
||||
it('does not select first history when a selection exists', async () => {
|
||||
useAppModeStore().selectedOutputs.push('1')
|
||||
const results = [makeResult('a.png')]
|
||||
const asset = makeAsset('a1', 'job-1', {
|
||||
allOutputs: results,
|
||||
|
||||
@@ -65,7 +65,7 @@ export function useOutputHistory(): {
|
||||
|
||||
function filterByOutputNodes(items: ResultItemImpl[]): ResultItemImpl[] {
|
||||
const nodeIds = appModeStore.selectedOutputs
|
||||
if (!nodeIds.length) return items
|
||||
if (!nodeIds.length) return []
|
||||
return items.filter((r) =>
|
||||
nodeIds.some((id) => String(id) === String(r.nodeId))
|
||||
)
|
||||
|
||||
@@ -203,7 +203,6 @@ const handleDownload = () => {
|
||||
severity: 'error',
|
||||
summary: 'Error',
|
||||
detail: t('g.failedToDownloadVideo'),
|
||||
life: 3000,
|
||||
group: 'video-preview'
|
||||
})
|
||||
}
|
||||
|
||||
@@ -233,7 +233,6 @@ const handleDownload = () => {
|
||||
severity: 'error',
|
||||
summary: 'Error',
|
||||
detail: t('g.failedToDownloadImage'),
|
||||
life: 3000,
|
||||
group: 'image-preview'
|
||||
})
|
||||
}
|
||||
|
||||
@@ -208,8 +208,7 @@ const handleDownload = () => {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: t('g.error'),
|
||||
detail: t('g.failedToDownloadFile'),
|
||||
life: 3000
|
||||
detail: t('g.failedToDownloadFile')
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1315,15 +1315,13 @@ export class ComfyApi extends EventTarget {
|
||||
useToastStore().add({
|
||||
severity: 'error',
|
||||
summary:
|
||||
'Unloading of models failed. Installed ComfyUI may be an outdated version.',
|
||||
life: 5000
|
||||
'Unloading of models failed. Installed ComfyUI may be an outdated version.'
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
useToastStore().add({
|
||||
severity: 'error',
|
||||
summary: 'An error occurred while trying to unload models.',
|
||||
life: 5000
|
||||
summary: 'An error occurred while trying to unload models.'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,10 +11,10 @@ import QuickLRU from '@alloc/quick-lru'
|
||||
import type { JobDetail } from '@/platform/remote/comfyui/jobs/jobTypes'
|
||||
import { extractWorkflow } from '@/platform/remote/comfyui/jobs/fetchJobs'
|
||||
import type { ComfyWorkflowJSON } from '@/platform/workflow/validation/schemas/workflowSchema'
|
||||
import { resultItemType } from '@/schemas/apiSchema'
|
||||
import type { ResultItem, TaskOutput } from '@/schemas/apiSchema'
|
||||
import type { TaskOutput } from '@/schemas/apiSchema'
|
||||
import { api } from '@/scripts/api'
|
||||
import { ResultItemImpl } from '@/stores/queueStore'
|
||||
import { flattenTaskOutputs } from '@/stores/resultItemParsing'
|
||||
import type { TaskItemImpl } from '@/stores/queueStore'
|
||||
|
||||
const MAX_TASK_CACHE_SIZE = 50
|
||||
@@ -78,66 +78,7 @@ export async function getOutputsForTask(
|
||||
}
|
||||
|
||||
function getPreviewableOutputs(outputs?: TaskOutput): ResultItemImpl[] {
|
||||
if (!outputs) return []
|
||||
const resultItems = Object.entries(outputs).flatMap(([nodeId, nodeOutputs]) =>
|
||||
Object.entries(nodeOutputs)
|
||||
.filter(([mediaType, _]) => mediaType !== 'animated')
|
||||
.flatMap(([mediaType, items]) => {
|
||||
if (!Array.isArray(items)) {
|
||||
return []
|
||||
}
|
||||
|
||||
return items.filter(isResultItemLike).map(
|
||||
(item) =>
|
||||
new ResultItemImpl({
|
||||
...item,
|
||||
nodeId,
|
||||
mediaType
|
||||
})
|
||||
)
|
||||
})
|
||||
)
|
||||
|
||||
return ResultItemImpl.filterPreviewable(resultItems)
|
||||
}
|
||||
|
||||
function isResultItemLike(item: unknown): item is ResultItem {
|
||||
if (!item || typeof item !== 'object' || Array.isArray(item)) {
|
||||
return false
|
||||
}
|
||||
|
||||
const candidate = item as Record<string, unknown>
|
||||
|
||||
if (
|
||||
candidate.filename !== undefined &&
|
||||
typeof candidate.filename !== 'string'
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (
|
||||
candidate.subfolder !== undefined &&
|
||||
typeof candidate.subfolder !== 'string'
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (
|
||||
candidate.type !== undefined &&
|
||||
!resultItemType.safeParse(candidate.type).success
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (
|
||||
candidate.filename === undefined &&
|
||||
candidate.subfolder === undefined &&
|
||||
candidate.type === undefined
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
return ResultItemImpl.filterPreviewable(flattenTaskOutputs(outputs))
|
||||
}
|
||||
|
||||
export function getPreviewableOutputsFromJobDetail(
|
||||
|
||||
@@ -70,7 +70,7 @@ function mapHistoryToAssets(historyItems: JobListItem[]): AssetItem[] {
|
||||
|
||||
assetItem.user_metadata = {
|
||||
...assetItem.user_metadata,
|
||||
outputCount: job.outputs_count,
|
||||
outputCount: task.outputsCount ?? task.previewableOutputs.length,
|
||||
allOutputs: task.previewableOutputs
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ import type {
|
||||
StatusWsMessageStatus,
|
||||
TaskOutput
|
||||
} from '@/schemas/apiSchema'
|
||||
import { flattenTaskOutputs } from '@/stores/resultItemParsing'
|
||||
import { appendCloudResParam } from '@/platform/distribution/cloudPreviewUtil'
|
||||
import { api } from '@/scripts/api'
|
||||
import type { ComfyApp } from '@/scripts/app'
|
||||
@@ -259,21 +260,7 @@ export class TaskItemImpl {
|
||||
}
|
||||
|
||||
calculateFlatOutputs(): ReadonlyArray<ResultItemImpl> {
|
||||
if (!this.outputs) {
|
||||
return []
|
||||
}
|
||||
return Object.entries(this.outputs).flatMap(([nodeId, nodeOutputs]) =>
|
||||
Object.entries(nodeOutputs).flatMap(([mediaType, items]) =>
|
||||
(items as ResultItem[]).map(
|
||||
(item: ResultItem) =>
|
||||
new ResultItemImpl({
|
||||
...item,
|
||||
nodeId,
|
||||
mediaType
|
||||
})
|
||||
)
|
||||
)
|
||||
)
|
||||
return flattenTaskOutputs(this.outputs)
|
||||
}
|
||||
|
||||
/** All outputs that support preview (images, videos, audio, 3D) */
|
||||
|
||||
229
src/stores/resultItemParsing.test.ts
Normal file
229
src/stores/resultItemParsing.test.ts
Normal file
@@ -0,0 +1,229 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import type { NodeExecutionOutput, TaskOutput } from '@/schemas/apiSchema'
|
||||
import {
|
||||
flattenNodeExecutionOutput,
|
||||
flattenTaskOutputs,
|
||||
isResultItemLike
|
||||
} from '@/stores/resultItemParsing'
|
||||
|
||||
vi.mock('@/scripts/api', () => ({
|
||||
api: {
|
||||
apiURL: vi.fn((path: string) => `/api${path}`),
|
||||
addEventListener: vi.fn(),
|
||||
removeEventListener: vi.fn()
|
||||
}
|
||||
}))
|
||||
|
||||
describe(isResultItemLike, () => {
|
||||
it('accepts valid result items', () => {
|
||||
expect(
|
||||
isResultItemLike({ filename: 'a.png', subfolder: '', type: 'output' })
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
it('accepts items without type', () => {
|
||||
expect(isResultItemLike({ filename: 'a.png', subfolder: '' })).toBe(true)
|
||||
})
|
||||
|
||||
it('rejects null/undefined/primitives', () => {
|
||||
expect(isResultItemLike(null)).toBe(false)
|
||||
expect(isResultItemLike(undefined)).toBe(false)
|
||||
expect(isResultItemLike('string')).toBe(false)
|
||||
expect(isResultItemLike(42)).toBe(false)
|
||||
})
|
||||
|
||||
it('rejects arrays', () => {
|
||||
expect(isResultItemLike([1, 2, 3])).toBe(false)
|
||||
})
|
||||
|
||||
it('rejects objects missing filename', () => {
|
||||
expect(isResultItemLike({ subfolder: '', type: 'output' })).toBe(false)
|
||||
})
|
||||
|
||||
it('rejects objects missing subfolder', () => {
|
||||
expect(isResultItemLike({ filename: 'a.png', type: 'output' })).toBe(false)
|
||||
})
|
||||
|
||||
it('rejects objects with non-string filename', () => {
|
||||
expect(isResultItemLike({ filename: 123, subfolder: '' })).toBe(false)
|
||||
})
|
||||
|
||||
it('rejects objects with non-string subfolder', () => {
|
||||
expect(isResultItemLike({ filename: 'a.png', subfolder: 42 })).toBe(false)
|
||||
})
|
||||
|
||||
it('rejects objects with invalid type', () => {
|
||||
expect(
|
||||
isResultItemLike({
|
||||
filename: 'a.png',
|
||||
subfolder: '',
|
||||
type: 'invalid_type'
|
||||
})
|
||||
).toBe(false)
|
||||
})
|
||||
|
||||
it('rejects objects with only type (no filename/subfolder)', () => {
|
||||
expect(isResultItemLike({ type: 'output' })).toBe(false)
|
||||
})
|
||||
|
||||
it('rejects empty objects', () => {
|
||||
expect(isResultItemLike({})).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe(flattenNodeExecutionOutput, () => {
|
||||
it('flattens standard image outputs', () => {
|
||||
const output: NodeExecutionOutput = {
|
||||
images: [
|
||||
{ filename: 'a.png', subfolder: '', type: 'output' },
|
||||
{ filename: 'b.png', subfolder: 'sub', type: 'output' }
|
||||
]
|
||||
}
|
||||
|
||||
const result = flattenNodeExecutionOutput('42', output)
|
||||
|
||||
expect(result).toHaveLength(2)
|
||||
expect(result[0].filename).toBe('a.png')
|
||||
expect(result[0].nodeId).toBe('42')
|
||||
expect(result[0].mediaType).toBe('images')
|
||||
expect(result[1].subfolder).toBe('sub')
|
||||
})
|
||||
|
||||
it('flattens non-standard output keys', () => {
|
||||
const output = {
|
||||
a_images: [{ filename: 'before.png', subfolder: '', type: 'output' }],
|
||||
b_images: [{ filename: 'after.png', subfolder: '', type: 'output' }]
|
||||
} as unknown as NodeExecutionOutput
|
||||
|
||||
const result = flattenNodeExecutionOutput('10', output)
|
||||
|
||||
expect(result).toHaveLength(2)
|
||||
expect(result.map((r) => r.filename)).toContain('before.png')
|
||||
expect(result.map((r) => r.filename)).toContain('after.png')
|
||||
})
|
||||
|
||||
it('flattens multiple media types', () => {
|
||||
const output: NodeExecutionOutput = {
|
||||
images: [{ filename: 'img.png', subfolder: '', type: 'output' }],
|
||||
video: [{ filename: 'vid.mp4', subfolder: '', type: 'output' }]
|
||||
}
|
||||
|
||||
const result = flattenNodeExecutionOutput('1', output)
|
||||
|
||||
expect(result).toHaveLength(2)
|
||||
expect(result.map((r) => r.mediaType)).toContain('images')
|
||||
expect(result.map((r) => r.mediaType)).toContain('video')
|
||||
})
|
||||
|
||||
it('excludes animated key', () => {
|
||||
const output: NodeExecutionOutput = {
|
||||
images: [{ filename: 'img.png', subfolder: '', type: 'output' }],
|
||||
animated: [true]
|
||||
}
|
||||
|
||||
const result = flattenNodeExecutionOutput('1', output)
|
||||
|
||||
expect(result).toHaveLength(1)
|
||||
expect(result[0].mediaType).toBe('images')
|
||||
})
|
||||
|
||||
it('skips non-array values like text strings', () => {
|
||||
const output: NodeExecutionOutput = {
|
||||
images: [{ filename: 'img.png', subfolder: '', type: 'output' }],
|
||||
text: 'hello'
|
||||
}
|
||||
|
||||
const result = flattenNodeExecutionOutput('1', output)
|
||||
|
||||
expect(result).toHaveLength(1)
|
||||
expect(result[0].mediaType).toBe('images')
|
||||
})
|
||||
|
||||
it('filters out non-ResultItem array items', () => {
|
||||
const output = {
|
||||
images: [{ filename: 'img.png', subfolder: '', type: 'output' }],
|
||||
custom_data: [{ randomKey: 123 }]
|
||||
} as unknown as NodeExecutionOutput
|
||||
|
||||
const result = flattenNodeExecutionOutput('1', output)
|
||||
|
||||
expect(result).toHaveLength(1)
|
||||
expect(result[0].mediaType).toBe('images')
|
||||
})
|
||||
|
||||
it('returns empty array for output with no valid media', () => {
|
||||
const result = flattenNodeExecutionOutput('1', { text: 'hello' })
|
||||
expect(result).toEqual([])
|
||||
})
|
||||
|
||||
it('returns empty array for empty arrays', () => {
|
||||
const output: NodeExecutionOutput = { images: [], audio: [] }
|
||||
const result = flattenNodeExecutionOutput('1', output)
|
||||
expect(result).toEqual([])
|
||||
})
|
||||
|
||||
it('accepts numeric nodeId', () => {
|
||||
const output: NodeExecutionOutput = {
|
||||
audio: [{ filename: 'sound.wav', subfolder: '', type: 'output' }]
|
||||
}
|
||||
|
||||
const result = flattenNodeExecutionOutput(7, output)
|
||||
|
||||
expect(result).toHaveLength(1)
|
||||
expect(result[0].nodeId).toBe(7)
|
||||
})
|
||||
})
|
||||
|
||||
describe(flattenTaskOutputs, () => {
|
||||
it('returns empty array for undefined outputs', () => {
|
||||
expect(flattenTaskOutputs(undefined)).toEqual([])
|
||||
})
|
||||
|
||||
it('flattens outputs from multiple nodes', () => {
|
||||
const outputs: TaskOutput = {
|
||||
'node-1': {
|
||||
images: [{ filename: 'a.png', subfolder: '', type: 'output' }]
|
||||
},
|
||||
'node-2': {
|
||||
video: [{ filename: 'b.mp4', subfolder: '', type: 'output' }]
|
||||
}
|
||||
}
|
||||
|
||||
const result = flattenTaskOutputs(outputs)
|
||||
|
||||
expect(result).toHaveLength(2)
|
||||
expect(result[0].nodeId).toBe('node-1')
|
||||
expect(result[1].nodeId).toBe('node-2')
|
||||
})
|
||||
|
||||
it('filters animated and non-ResultItem values across nodes', () => {
|
||||
const outputs: TaskOutput = {
|
||||
'node-1': {
|
||||
images: [{ filename: 'img.png', subfolder: '', type: 'output' }],
|
||||
animated: [true],
|
||||
text: 'hello'
|
||||
}
|
||||
}
|
||||
|
||||
const result = flattenTaskOutputs(outputs)
|
||||
|
||||
expect(result).toHaveLength(1)
|
||||
expect(result[0].filename).toBe('img.png')
|
||||
})
|
||||
|
||||
it('supports non-standard output keys across nodes', () => {
|
||||
const outputs = {
|
||||
'node-1': {
|
||||
a_images: [{ filename: 'before.png', subfolder: '', type: 'output' }],
|
||||
b_images: [{ filename: 'after.png', subfolder: '', type: 'output' }]
|
||||
}
|
||||
} as unknown as TaskOutput
|
||||
|
||||
const result = flattenTaskOutputs(outputs)
|
||||
|
||||
expect(result).toHaveLength(2)
|
||||
expect(result.map((r) => r.filename)).toContain('before.png')
|
||||
expect(result.map((r) => r.filename)).toContain('after.png')
|
||||
})
|
||||
})
|
||||
74
src/stores/resultItemParsing.ts
Normal file
74
src/stores/resultItemParsing.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import type {
|
||||
NodeExecutionOutput,
|
||||
ResultItem,
|
||||
TaskOutput
|
||||
} from '@/schemas/apiSchema'
|
||||
import { resultItemType } from '@/schemas/apiSchema'
|
||||
import { ResultItemImpl } from '@/stores/queueStore'
|
||||
|
||||
const EXCLUDED_KEYS = new Set(['animated'])
|
||||
|
||||
/**
|
||||
* Strict domain guard for result items.
|
||||
*
|
||||
* The wire-format schema (zOutputs) is intentionally permissive via
|
||||
* `.passthrough()` to accept arbitrary keys from custom nodes. This guard
|
||||
* is strict: it requires the fields needed to construct a valid UI model
|
||||
* (ResultItemImpl) that can build preview URLs.
|
||||
*/
|
||||
export function isResultItemLike(item: unknown): item is ResultItem {
|
||||
if (!item || typeof item !== 'object' || Array.isArray(item)) {
|
||||
return false
|
||||
}
|
||||
|
||||
const candidate = item as Record<string, unknown>
|
||||
|
||||
if (typeof candidate.filename !== 'string') {
|
||||
return false
|
||||
}
|
||||
|
||||
if (typeof candidate.subfolder !== 'string') {
|
||||
return false
|
||||
}
|
||||
|
||||
if (
|
||||
candidate.type !== undefined &&
|
||||
!resultItemType.safeParse(candidate.type).success
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Flattens a single node's execution output into ResultItemImpl instances.
|
||||
*
|
||||
* Iterates all output keys dynamically (to support custom node keys like
|
||||
* `a_images`, `b_images`, `gifs`, etc.) and validates each item with the
|
||||
* strict domain guard before constructing ResultItemImpl.
|
||||
*/
|
||||
export function flattenNodeExecutionOutput(
|
||||
nodeId: string | number,
|
||||
nodeOutput: NodeExecutionOutput
|
||||
): ResultItemImpl[] {
|
||||
return Object.entries(nodeOutput)
|
||||
.filter(([key, value]) => !EXCLUDED_KEYS.has(key) && Array.isArray(value))
|
||||
.flatMap(([mediaType, items]) =>
|
||||
(items as unknown[])
|
||||
.filter(isResultItemLike)
|
||||
.map((item) => new ResultItemImpl({ ...item, mediaType, nodeId }))
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Flattens all nodes' outputs from a TaskOutput into ResultItemImpl instances.
|
||||
*/
|
||||
export function flattenTaskOutputs(
|
||||
outputs?: TaskOutput
|
||||
): ReadonlyArray<ResultItemImpl> {
|
||||
if (!outputs) return []
|
||||
return Object.entries(outputs).flatMap(([nodeId, nodeOutput]) =>
|
||||
flattenNodeExecutionOutput(nodeId, nodeOutput)
|
||||
)
|
||||
}
|
||||
@@ -267,8 +267,7 @@ export const useSubgraphStore = defineStore('subgraph', () => {
|
||||
useToastStore().add({
|
||||
severity: 'error',
|
||||
summary: t('subgraphStore.loadFailure'),
|
||||
detail: errors.length > 3 ? `x${errors.length}` : `${errors}`,
|
||||
life: 6000
|
||||
detail: errors.length > 3 ? `x${errors.length}` : `${errors}`
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,9 @@ 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
|
||||
|
||||
@@ -75,3 +77,34 @@ 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
|
||||
}
|
||||
|
||||
@@ -168,8 +168,7 @@ export function useManagerState() {
|
||||
useToastStore().add({
|
||||
severity: 'error',
|
||||
summary: t('g.error'),
|
||||
detail: t('manager.legacyMenuNotAvailable'),
|
||||
life: 3000
|
||||
detail: t('manager.legacyMenuNotAvailable')
|
||||
})
|
||||
}
|
||||
// Fallback to extensions panel if not showing toast
|
||||
@@ -185,8 +184,7 @@ export function useManagerState() {
|
||||
useToastStore().add({
|
||||
severity: 'error',
|
||||
summary: t('g.error'),
|
||||
detail: t('manager.legacyMenuNotAvailable'),
|
||||
life: 3000
|
||||
detail: t('manager.legacyMenuNotAvailable')
|
||||
})
|
||||
} else {
|
||||
managerDialog.show(options?.initialTab, options?.initialPackId)
|
||||
|
||||
Reference in New Issue
Block a user