Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions eslint.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -320,5 +320,15 @@ export default defineConfig([
}
]
}
},

// Storybook-only mock components (__stories__/**/*.vue)
// These are not shipped to production and do not require i18n or strict Vue patterns.
{
files: ['**/__stories__/**/*.vue'],
rules: {
'@intlify/vue-i18n/no-raw-text': 'off',
'vue/no-unused-properties': 'off'
}
}
])
2 changes: 2 additions & 0 deletions src/components/TopMenuSection.vue
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@
</Button>
</div>
</div>
<ErrorOverlay />
<QueueProgressOverlay
v-if="isQueueProgressOverlayEnabled"
v-model:expanded="isQueueOverlayExpanded"
Expand Down Expand Up @@ -156,6 +157,7 @@ import StatusBadge from '@/components/common/StatusBadge.vue'
import QueueInlineProgressSummary from '@/components/queue/QueueInlineProgressSummary.vue'
import QueueNotificationBannerHost from '@/components/queue/QueueNotificationBannerHost.vue'
import QueueProgressOverlay from '@/components/queue/QueueProgressOverlay.vue'
import ErrorOverlay from '@/components/error/ErrorOverlay.vue'
import ActionBarButtons from '@/components/topbar/ActionBarButtons.vue'
import CurrentUserButton from '@/components/topbar/CurrentUserButton.vue'
import LoginButton from '@/components/topbar/LoginButton.vue'
Expand Down
100 changes: 100 additions & 0 deletions src/components/error/ErrorOverlay.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
<template>
<Transition
enter-active-class="transition-all duration-300 ease-out"
enter-from-class="-translate-y-3 opacity-0"
enter-to-class="translate-y-0 opacity-100"
>
<div v-if="isVisible" class="flex justify-end w-full pointer-events-none">
<div
class="pointer-events-auto flex w-80 min-w-72 flex-col overflow-hidden rounded-lg border border-interface-stroke bg-comfy-menu-bg shadow-interface transition-colors duration-200 ease-in-out"
>
<!-- Header -->
<div class="flex h-12 items-center gap-2 px-4">
<span class="flex-1 text-sm font-bold text-destructive-background">
{{ errorCountLabel }}
</span>
<Button
variant="muted-textonly"
size="icon-sm"
:aria-label="t('g.close')"
@click="dismiss"
>
<i class="icon-[lucide--x] block size-5 leading-none" />
</Button>
</div>

<!-- Body -->
<div class="px-4 pb-3">
<ul class="m-0 flex list-none flex-col gap-1.5 p-0">
<li
v-for="(message, idx) in groupedErrorMessages"
:key="idx"
class="flex items-baseline gap-2 text-sm leading-snug text-muted-foreground"
>
<span
class="mt-1.5 size-1 shrink-0 rounded-full bg-muted-foreground"
/>
<span>{{ message }}</span>
</li>
</ul>
</div>

<!-- Footer -->
<div class="flex items-center justify-end gap-4 px-4 py-3">
<Button variant="muted-textonly" size="unset" @click="dismiss">
{{ t('g.dismiss') }}
</Button>
<Button variant="secondary" size="lg" @click="seeErrors">
{{ t('errorOverlay.seeErrors') }}
</Button>
</div>
</div>
</div>
</Transition>
</template>

<script setup lang="ts">
import { computed, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { storeToRefs } from 'pinia'

import Button from '@/components/ui/button/Button.vue'
import { useExecutionStore } from '@/stores/executionStore'
import { useRightSidePanelStore } from '@/stores/workspace/rightSidePanelStore'
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
import { useErrorGroups } from '@/components/rightSidePanel/errors/useErrorGroups'

const { t } = useI18n()
const executionStore = useExecutionStore()
const rightSidePanelStore = useRightSidePanelStore()
const canvasStore = useCanvasStore()

const { totalErrorCount, isErrorOverlayOpen } = storeToRefs(executionStore)
const { groupedErrorMessages } = useErrorGroups(ref(''), t)

const errorCountLabel = computed(() =>
t(
'errorOverlay.errorCount',
{ count: totalErrorCount.value },
totalErrorCount.value
)
)

const isVisible = computed(
() => isErrorOverlayOpen.value && totalErrorCount.value > 0
)

function dismiss() {
executionStore.dismissErrorOverlay()
}

function seeErrors() {
if (canvasStore.canvas) {
canvasStore.canvas.deselectAll()
canvasStore.updateSelectedItems()
}

rightSidePanelStore.openPanel('errors')
executionStore.dismissErrorOverlay()
}
</script>
48 changes: 24 additions & 24 deletions src/components/rightSidePanel/RightSidePanel.vue
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ import { useRightSidePanelStore } from '@/stores/workspace/rightSidePanelStore'
import type { RightSidePanelTab } from '@/stores/workspace/rightSidePanelStore'
import { resolveNodeDisplayName } from '@/utils/nodeTitleUtil'
import { cn } from '@/utils/tailwindUtil'
import { isGroupNode } from '@/utils/executableGroupNodeDto'

import TabError from './TabError.vue'
import TabInfo from './info/TabInfo.vue'
import TabGlobalParameters from './parameters/TabGlobalParameters.vue'
import TabNodes from './parameters/TabNodes.vue'
Expand All @@ -41,7 +41,7 @@ const rightSidePanelStore = useRightSidePanelStore()
const settingStore = useSettingStore()
const { t } = useI18n()

const { hasAnyError } = storeToRefs(executionStore)
const { hasAnyError, allErrorExecutionIds } = storeToRefs(executionStore)

const { findParentGroup } = useGraphHierarchy()

Expand Down Expand Up @@ -96,30 +96,31 @@ type RightSidePanelTabList = Array<{
icon?: string
}>

//FIXME all errors if nothing selected?
const selectedNodeErrors = computed(() =>
selectedNodes.value
.map((node) => executionStore.getNodeErrors(`${node.id}`))
.filter((nodeError) => !!nodeError)
const hasDirectNodeError = computed(() =>
selectedNodes.value.some((node) =>
executionStore.activeGraphErrorNodeIds.has(String(node.id))
)
)

const hasContainerInternalError = computed(() => {
if (allErrorExecutionIds.value.length === 0) return false
return selectedNodes.value.some((node) => {
if (!(node instanceof SubgraphNode || isGroupNode(node))) return false
return executionStore.hasInternalErrorForNode(node.id)
})
})

const hasRelevantErrors = computed(() => {
if (!hasSelection.value) return hasAnyError.value
return hasDirectNodeError.value || hasContainerInternalError.value
})

const tabs = computed<RightSidePanelTabList>(() => {
const list: RightSidePanelTabList = []
if (
selectedNodeErrors.value.length &&
settingStore.get('Comfy.RightSidePanel.ShowErrorsTab')
) {
list.push({
label: () => t('g.error'),
value: 'error',
icon: 'icon-[lucide--octagon-alert] bg-node-stroke-error ml-1'
})
}

if (
hasAnyError.value &&
!hasSelection.value &&
settingStore.get('Comfy.RightSidePanel.ShowErrorsTab')
settingStore.get('Comfy.RightSidePanel.ShowErrorsTab') &&
hasRelevantErrors.value
) {
list.push({
label: () => t('rightSidePanel.errors'),
Expand Down Expand Up @@ -315,9 +316,9 @@ function handleProxyWidgetsUpdate(value: ProxyWidgetsProperty) {

<!-- Panel Content -->
<div class="scrollbar-thin flex-1 overflow-y-auto">
<template v-if="!hasSelection">
<TabErrors v-if="activeTab === 'errors'" />
<TabGlobalParameters v-else-if="activeTab === 'parameters'" />
<TabErrors v-if="activeTab === 'errors'" />
<template v-else-if="!hasSelection">
<TabGlobalParameters v-if="activeTab === 'parameters'" />
<TabNodes v-else-if="activeTab === 'nodes'" />
<TabGlobalSettings v-else-if="activeTab === 'settings'" />
</template>
Expand All @@ -326,7 +327,6 @@ function handleProxyWidgetsUpdate(value: ProxyWidgetsProperty) {
:node="selectedSingleNode"
/>
<template v-else>
<TabError v-if="activeTab === 'error'" :errors="selectedNodeErrors" />
<TabSubgraphInputs
v-if="activeTab === 'parameters' && isSingleSubgraphNode"
:node="selectedSingleNode as SubgraphNode"
Expand Down
30 changes: 0 additions & 30 deletions src/components/rightSidePanel/TabError.vue

This file was deleted.

38 changes: 30 additions & 8 deletions src/components/rightSidePanel/errors/ErrorNodeCard.vue
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
<template>
<div class="overflow-hidden">
<!-- Card Header (Node ID & Actions) -->
<div v-if="card.nodeId" class="flex flex-wrap items-center gap-2 py-2">
<!-- Card Header -->
<div
v-if="card.nodeId && !compact"
class="flex flex-wrap items-center gap-2 py-2"
>
<span
v-if="showNodeIdBadge"
class="shrink-0 rounded-md bg-secondary-background-selected px-2 py-0.5 text-[10px] font-mono text-muted-foreground font-bold"
class="shrink-0 rounded-md bg-secondary-background-selected px-2 py-0.5 text-xs font-mono text-muted-foreground font-bold"
>
#{{ card.nodeId }}
</span>
Expand All @@ -19,15 +22,16 @@
variant="secondary"
size="sm"
class="rounded-lg text-sm shrink-0"
@click.stop="emit('enterSubgraph', card.nodeId ?? '')"
@click.stop="handleEnterSubgraph"
>
{{ t('rightSidePanel.enterSubgraph') }}
</Button>
<Button
variant="textonly"
size="icon-sm"
class="size-7 text-muted-foreground hover:text-base-foreground shrink-0"
@click.stop="emit('locateNode', card.nodeId ?? '')"
:aria-label="t('rightSidePanel.locateNode')"
@click.stop="handleLocateNode"
>
<i class="icon-[lucide--locate] size-3.5" />
</Button>
Expand All @@ -43,7 +47,7 @@
>
<!-- Error Message -->
<p
v-if="error.message"
v-if="error.message && !compact"
class="m-0 text-sm break-words whitespace-pre-wrap leading-relaxed px-0.5"
>
{{ error.message }}
Expand All @@ -69,7 +73,7 @@
<Button
variant="secondary"
size="sm"
class="w-full justify-center gap-2 h-8 text-[11px]"
class="w-full justify-center gap-2 h-8 text-xs"
@click="handleCopyError(error)"
>
<i class="icon-[lucide--copy] size-3.5" />
Expand All @@ -88,9 +92,15 @@ import { cn } from '@/utils/tailwindUtil'

import type { ErrorCardData, ErrorItem } from './types'

const { card, showNodeIdBadge = false } = defineProps<{
const {
card,
showNodeIdBadge = false,
compact = false
} = defineProps<{
card: ErrorCardData
showNodeIdBadge?: boolean
/** Hide card header and error message (used in single-node selection mode) */
compact?: boolean
}>()

const emit = defineEmits<{
Expand All @@ -101,6 +111,18 @@ const emit = defineEmits<{

const { t } = useI18n()

function handleLocateNode() {
if (card.nodeId) {
emit('locateNode', card.nodeId)
}
}

function handleEnterSubgraph() {
if (card.nodeId) {
emit('enterSubgraph', card.nodeId)
}
}

function handleCopyError(error: ErrorItem) {
emit(
'copyToClipboard',
Expand Down
Loading
Loading