mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-24 08:19:51 +00:00
Compare commits
2 Commits
feat/node-
...
ticket/bug
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b2c1f1f51a | ||
|
|
eb1153d836 |
@@ -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
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@comfyorg/comfyui-frontend",
|
||||
"version": "1.42.2",
|
||||
"version": "1.42.0",
|
||||
"private": true,
|
||||
"description": "Official front-end implementation of ComfyUI",
|
||||
"homepage": "https://comfy.org",
|
||||
|
||||
@@ -34,7 +34,17 @@
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div ref="actionbarContainerRef" :class="actionbarContainerClass">
|
||||
<div
|
||||
ref="actionbarContainerRef"
|
||||
:class="
|
||||
cn(
|
||||
'actionbar-container pointer-events-auto relative flex h-12 items-center gap-2 rounded-lg border bg-comfy-menu-bg px-2 shadow-interface',
|
||||
hasAnyError
|
||||
? 'border-destructive-background-hover'
|
||||
: 'border-interface-stroke'
|
||||
)
|
||||
"
|
||||
>
|
||||
<ActionBarButtons />
|
||||
<!-- Support for legacy topbar elements attached by custom scripts, hidden if no elements present -->
|
||||
<div
|
||||
@@ -45,7 +55,6 @@
|
||||
<ComfyActionbar
|
||||
:top-menu-container="actionbarContainerRef"
|
||||
:queue-overlay-expanded="isQueueOverlayExpanded"
|
||||
:has-any-error="hasAnyError"
|
||||
@update:progress-target="updateProgressTarget"
|
||||
/>
|
||||
<CurrentUserButton
|
||||
@@ -114,7 +123,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useLocalStorage, useMutationObserver } from '@vueuse/core'
|
||||
import { useLocalStorage } from '@vueuse/core'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
@@ -136,7 +145,6 @@ import { buildTooltipConfig } from '@/composables/useTooltipConfig'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { app } from '@/scripts/app'
|
||||
import { useExecutionErrorStore } from '@/stores/executionErrorStore'
|
||||
import { useActionBarButtonStore } from '@/stores/actionBarButtonStore'
|
||||
import { useQueueUIStore } from '@/stores/queueStore'
|
||||
import { useRightSidePanelStore } from '@/stores/workspace/rightSidePanelStore'
|
||||
import { useWorkspaceStore } from '@/stores/workspaceStore'
|
||||
@@ -160,7 +168,6 @@ const { isLoggedIn } = useCurrentUser()
|
||||
const { t } = useI18n()
|
||||
const { toastErrorHandler } = useErrorHandling()
|
||||
const executionErrorStore = useExecutionErrorStore()
|
||||
const actionBarButtonStore = useActionBarButtonStore()
|
||||
const queueUIStore = useQueueUIStore()
|
||||
const { isOverlayExpanded: isQueueOverlayExpanded } = storeToRefs(queueUIStore)
|
||||
const { shouldShowRedDot: shouldShowConflictRedDot } =
|
||||
@@ -175,43 +182,6 @@ const isActionbarEnabled = computed(
|
||||
const isActionbarFloating = computed(
|
||||
() => isActionbarEnabled.value && !isActionbarDocked.value
|
||||
)
|
||||
/**
|
||||
* Whether the actionbar container has any visible docked buttons
|
||||
* (excluding ComfyActionbar, which uses position:fixed when floating
|
||||
* and does not contribute to the container's visual layout).
|
||||
*/
|
||||
const hasDockedButtons = computed(() => {
|
||||
if (actionBarButtonStore.buttons.length > 0) return true
|
||||
if (hasLegacyContent.value) return true
|
||||
if (isLoggedIn.value && !isIntegratedTabBar.value) return true
|
||||
if (isDesktop && !isIntegratedTabBar.value) return true
|
||||
if (isCloud && flags.workflowSharingEnabled) return true
|
||||
if (!isRightSidePanelOpen.value) return true
|
||||
return false
|
||||
})
|
||||
const isActionbarContainerEmpty = computed(
|
||||
() => isActionbarFloating.value && !hasDockedButtons.value
|
||||
)
|
||||
const actionbarContainerClass = computed(() => {
|
||||
const base =
|
||||
'actionbar-container pointer-events-auto relative flex h-12 items-center gap-2 rounded-lg border bg-comfy-menu-bg shadow-interface'
|
||||
|
||||
if (isActionbarContainerEmpty.value) {
|
||||
return cn(
|
||||
base,
|
||||
'-ml-2 w-0 min-w-0 border-transparent shadow-none',
|
||||
'has-[.border-dashed]:ml-0 has-[.border-dashed]:w-auto has-[.border-dashed]:min-w-auto',
|
||||
'has-[.border-dashed]:border-interface-stroke has-[.border-dashed]:pl-2 has-[.border-dashed]:shadow-interface'
|
||||
)
|
||||
}
|
||||
|
||||
const borderClass =
|
||||
!isActionbarFloating.value && hasAnyError.value
|
||||
? 'border-destructive-background-hover'
|
||||
: 'border-interface-stroke'
|
||||
|
||||
return cn(base, 'px-2', borderClass)
|
||||
})
|
||||
const isIntegratedTabBar = computed(
|
||||
() => settingStore.get('Comfy.UI.TabBarLayout') !== 'Legacy'
|
||||
)
|
||||
@@ -263,25 +233,6 @@ const rightSidePanelTooltipConfig = computed(() =>
|
||||
|
||||
// Maintain support for legacy topbar elements attached by custom scripts
|
||||
const legacyCommandsContainerRef = ref<HTMLElement>()
|
||||
const hasLegacyContent = ref(false)
|
||||
|
||||
function checkLegacyContent() {
|
||||
const el = legacyCommandsContainerRef.value
|
||||
if (!el) {
|
||||
hasLegacyContent.value = false
|
||||
return
|
||||
}
|
||||
// Mirror the CSS: [&:not(:has(*>*:not(:empty)))]:hidden
|
||||
hasLegacyContent.value =
|
||||
el.querySelector(':scope > * > *:not(:empty)') !== null
|
||||
}
|
||||
|
||||
useMutationObserver(legacyCommandsContainerRef, checkLegacyContent, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
characterData: true
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
if (legacyCommandsContainerRef.value) {
|
||||
app.menu.element.style.width = 'fit-content'
|
||||
|
||||
@@ -119,14 +119,9 @@ import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
import ComfyRunButton from './ComfyRunButton'
|
||||
|
||||
const {
|
||||
topMenuContainer,
|
||||
queueOverlayExpanded = false,
|
||||
hasAnyError = false
|
||||
} = defineProps<{
|
||||
const { topMenuContainer, queueOverlayExpanded = false } = defineProps<{
|
||||
topMenuContainer?: HTMLElement | null
|
||||
queueOverlayExpanded?: boolean
|
||||
hasAnyError?: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -440,12 +435,7 @@ const panelClass = computed(() =>
|
||||
isDragging.value && 'pointer-events-none select-none',
|
||||
isDocked.value
|
||||
? 'static border-none bg-transparent p-0'
|
||||
: [
|
||||
'fixed shadow-interface',
|
||||
hasAnyError
|
||||
? 'border-destructive-background-hover'
|
||||
: 'border-interface-stroke'
|
||||
]
|
||||
: 'fixed shadow-interface'
|
||||
)
|
||||
)
|
||||
</script>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -138,7 +138,8 @@ onMounted(async () => {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: t('g.error'),
|
||||
detail: t('toastMessages.failedToFetchLogs')
|
||||
detail: t('toastMessages.failedToFetchLogs'),
|
||||
life: 5000
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -5,7 +5,6 @@ import PrimeVue from 'primevue/config'
|
||||
import Tooltip from 'primevue/tooltip'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
import type * as ColorUtilModule from '@/utils/colorUtil'
|
||||
|
||||
// Import after mocks
|
||||
import ColorPickerButton from '@/components/graph/selectionToolbox/ColorPickerButton.vue'
|
||||
@@ -63,14 +62,9 @@ vi.mock('@/lib/litegraph/src/litegraph', async () => {
|
||||
})
|
||||
|
||||
// Mock the colorUtil module
|
||||
vi.mock('@/utils/colorUtil', async () => {
|
||||
const actual = await vi.importActual<typeof ColorUtilModule>('@/utils/colorUtil')
|
||||
|
||||
return {
|
||||
...actual,
|
||||
adjustColor: vi.fn((color: string) => color + '_light')
|
||||
}
|
||||
})
|
||||
vi.mock('@/utils/colorUtil', () => ({
|
||||
adjustColor: vi.fn((color: string) => color + '_light')
|
||||
}))
|
||||
|
||||
// Mock the litegraphUtil module
|
||||
vi.mock('@/utils/litegraphUtil', () => ({
|
||||
@@ -89,25 +83,11 @@ describe('ColorPickerButton', () => {
|
||||
locale: 'en',
|
||||
messages: {
|
||||
en: {
|
||||
g: {
|
||||
color: 'Color',
|
||||
custom: 'Custom',
|
||||
favorites: 'Favorites',
|
||||
remove: 'Remove'
|
||||
},
|
||||
color: {
|
||||
noColor: 'No Color',
|
||||
red: 'Red',
|
||||
green: 'Green',
|
||||
blue: 'Blue'
|
||||
},
|
||||
shape: {
|
||||
default: 'Default',
|
||||
box: 'Box',
|
||||
CARD: 'Card'
|
||||
},
|
||||
modelLibrary: {
|
||||
sortRecent: 'Recent'
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -158,17 +138,4 @@ describe('ColorPickerButton', () => {
|
||||
await button.trigger('click')
|
||||
expect(wrapper.findComponent({ name: 'SelectButton' }).exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('disables favoriting when the selection has no shared applied color', async () => {
|
||||
canvasStore.selectedItems = [createMockPositionable()]
|
||||
const wrapper = createWrapper()
|
||||
|
||||
await wrapper.find('[data-testid="color-picker-button"]').trigger('click')
|
||||
|
||||
expect(
|
||||
wrapper.find('[data-testid="toggle-favorite-color"]').attributes(
|
||||
'disabled'
|
||||
)
|
||||
).toBeDefined()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
</Button>
|
||||
<div
|
||||
v-if="showColorPicker"
|
||||
class="absolute -top-10 left-1/2 z-10 min-w-44 -translate-x-1/2 rounded-lg border border-border-default bg-interface-panel-surface p-2 shadow-lg"
|
||||
class="absolute -top-10 left-1/2 -translate-x-1/2"
|
||||
>
|
||||
<SelectButton
|
||||
:model-value="selectedColorOption"
|
||||
@@ -41,70 +41,11 @@
|
||||
/>
|
||||
</template>
|
||||
</SelectButton>
|
||||
<div class="mt-2 flex items-center gap-2">
|
||||
<ColorPicker
|
||||
data-testid="custom-color-trigger"
|
||||
:model-value="currentPickerValue"
|
||||
format="hex"
|
||||
:aria-label="t('g.custom')"
|
||||
class="h-8 w-8 overflow-hidden rounded-md border border-border-default bg-secondary-background"
|
||||
:pt="{
|
||||
preview: {
|
||||
class: '!h-full !w-full !rounded-md !border-none'
|
||||
}
|
||||
}"
|
||||
@update:model-value="onCustomColorUpdate"
|
||||
/>
|
||||
<button
|
||||
class="flex size-8 cursor-pointer items-center justify-center rounded-md border border-border-default bg-secondary-background hover:bg-secondary-background-hover disabled:cursor-not-allowed disabled:opacity-50"
|
||||
:title="isCurrentColorFavorite ? t('g.remove') : t('g.favorites')"
|
||||
data-testid="toggle-favorite-color"
|
||||
:disabled="!currentAppliedColor"
|
||||
@click="toggleCurrentColorFavorite"
|
||||
>
|
||||
<i
|
||||
:class="
|
||||
isCurrentColorFavorite
|
||||
? 'icon-[lucide--star] text-yellow-500'
|
||||
: 'icon-[lucide--star-off]'
|
||||
"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="favoriteColors.length" class="mt-2 flex flex-wrap gap-1">
|
||||
<button
|
||||
v-for="color in favoriteColors"
|
||||
:key="`favorite-${color}`"
|
||||
class="flex size-7 cursor-pointer items-center justify-center rounded-md border border-border-default bg-secondary-background hover:bg-secondary-background-hover"
|
||||
:title="`${t('g.favorites')}: ${color.toUpperCase()}`"
|
||||
@click="applySavedCustomColor(color)"
|
||||
>
|
||||
<div
|
||||
class="size-4 rounded-full border border-border-default"
|
||||
:style="{ backgroundColor: color }"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="recentColors.length" class="mt-2 flex flex-wrap gap-1">
|
||||
<button
|
||||
v-for="color in recentColors"
|
||||
:key="`recent-${color}`"
|
||||
class="flex size-7 cursor-pointer items-center justify-center rounded-md border border-border-default bg-secondary-background hover:bg-secondary-background-hover"
|
||||
:title="`${t('modelLibrary.sortRecent')}: ${color.toUpperCase()}`"
|
||||
@click="applySavedCustomColor(color)"
|
||||
>
|
||||
<div
|
||||
class="size-4 rounded-full border border-border-default"
|
||||
:style="{ backgroundColor: color }"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import ColorPicker from 'primevue/colorpicker'
|
||||
import SelectButton from 'primevue/selectbutton'
|
||||
import type { Raw } from 'vue'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
@@ -120,26 +61,16 @@ import {
|
||||
LiteGraph,
|
||||
isColorable
|
||||
} from '@/lib/litegraph/src/litegraph'
|
||||
import { useCustomNodeColorSettings } from '@/composables/graph/useCustomNodeColorSettings'
|
||||
import { useNodeCustomization } from '@/composables/graph/useNodeCustomization'
|
||||
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
|
||||
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||
import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
|
||||
import { adjustColor, toHexFromFormat } from '@/utils/colorUtil'
|
||||
import { adjustColor } from '@/utils/colorUtil'
|
||||
import { getItemsColorOption } from '@/utils/litegraphUtil'
|
||||
import { getDefaultCustomNodeColor } from '@/utils/nodeColorCustomization'
|
||||
|
||||
const { t } = useI18n()
|
||||
const canvasStore = useCanvasStore()
|
||||
const colorPaletteStore = useColorPaletteStore()
|
||||
const workflowStore = useWorkflowStore()
|
||||
const { applyCustomColor, getCurrentAppliedColor } = useNodeCustomization()
|
||||
const {
|
||||
favoriteColors,
|
||||
recentColors,
|
||||
isFavoriteColor,
|
||||
toggleFavoriteColor
|
||||
} = useCustomNodeColorSettings()
|
||||
const isLightTheme = computed(
|
||||
() => colorPaletteStore.completedActivePalette.light_theme
|
||||
)
|
||||
@@ -198,25 +129,16 @@ const applyColor = (colorOption: ColorOption | null) => {
|
||||
}
|
||||
|
||||
const currentColorOption = ref<CanvasColorOption | null>(null)
|
||||
const currentAppliedColor = computed(() => getCurrentAppliedColor())
|
||||
const currentPickerValue = computed(() =>
|
||||
(currentAppliedColor.value ?? getDefaultCustomNodeColor()).replace('#', '')
|
||||
)
|
||||
const currentColor = computed(() =>
|
||||
currentColorOption.value
|
||||
? isLightTheme.value
|
||||
? toLightThemeColor(currentColorOption.value?.bgcolor)
|
||||
: currentColorOption.value?.bgcolor
|
||||
: currentAppliedColor.value
|
||||
: null
|
||||
)
|
||||
|
||||
const localizedCurrentColorName = computed(() => {
|
||||
if (currentAppliedColor.value) {
|
||||
return currentAppliedColor.value.toUpperCase()
|
||||
}
|
||||
if (!currentColorOption.value?.bgcolor) {
|
||||
return null
|
||||
}
|
||||
if (!currentColorOption.value?.bgcolor) return null
|
||||
const colorOption = colorOptions.find(
|
||||
(option) =>
|
||||
option.value.dark === currentColorOption.value?.bgcolor ||
|
||||
@@ -224,26 +146,6 @@ const localizedCurrentColorName = computed(() => {
|
||||
)
|
||||
return colorOption?.localizedName ?? NO_COLOR_OPTION.localizedName
|
||||
})
|
||||
|
||||
async function applySavedCustomColor(color: string) {
|
||||
currentColorOption.value = null
|
||||
await applyCustomColor(color)
|
||||
showColorPicker.value = false
|
||||
}
|
||||
|
||||
async function onCustomColorUpdate(value: string) {
|
||||
await applySavedCustomColor(toHexFromFormat(value, 'hex'))
|
||||
}
|
||||
|
||||
async function toggleCurrentColorFavorite() {
|
||||
if (!currentAppliedColor.value) return
|
||||
await toggleFavoriteColor(currentAppliedColor.value)
|
||||
}
|
||||
|
||||
const isCurrentColorFavorite = computed(() =>
|
||||
isFavoriteColor(currentAppliedColor.value)
|
||||
)
|
||||
|
||||
const updateColorSelectionFromNode = (
|
||||
newSelectedItems: Raw<Positionable[]>
|
||||
) => {
|
||||
|
||||
@@ -1,99 +0,0 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import ColorPicker from 'primevue/colorpicker'
|
||||
import PrimeVue from 'primevue/config'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import type { MenuOption } from '@/composables/graph/useMoreOptionsMenu'
|
||||
|
||||
import ColorPickerMenu from './ColorPickerMenu.vue'
|
||||
|
||||
vi.mock('@/composables/graph/useNodeCustomization', () => ({
|
||||
useNodeCustomization: () => ({
|
||||
getCurrentShape: () => null
|
||||
})
|
||||
}))
|
||||
|
||||
describe('ColorPickerMenu', () => {
|
||||
it('renders a compact PrimeVue picker panel for custom color submenu entries', async () => {
|
||||
const onColorPick = vi.fn()
|
||||
const option: MenuOption = {
|
||||
label: 'Color',
|
||||
hasSubmenu: true,
|
||||
action: () => {},
|
||||
submenu: [
|
||||
{
|
||||
label: 'Custom',
|
||||
action: () => {},
|
||||
pickerValue: '112233',
|
||||
onColorPick
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
const wrapper = mount(ColorPickerMenu, {
|
||||
props: { option },
|
||||
global: {
|
||||
plugins: [PrimeVue],
|
||||
stubs: {
|
||||
Popover: {
|
||||
template: '<div><slot /></div>'
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const picker = wrapper.findComponent(ColorPicker)
|
||||
expect(picker.exists()).toBe(true)
|
||||
expect(picker.props('modelValue')).toBe('112233')
|
||||
expect(picker.props('inline')).toBe(true)
|
||||
expect(wrapper.text()).toContain('#112233')
|
||||
|
||||
picker.vm.$emit('update:model-value', 'fedcba')
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
expect(onColorPick).toHaveBeenCalledWith('#fedcba')
|
||||
})
|
||||
|
||||
it('shows preset swatches in a compact grid when color presets are available', () => {
|
||||
const option: MenuOption = {
|
||||
label: 'Color',
|
||||
hasSubmenu: true,
|
||||
action: () => {},
|
||||
submenu: [
|
||||
{
|
||||
label: 'Custom',
|
||||
action: () => {},
|
||||
pickerValue: '112233',
|
||||
onColorPick: vi.fn()
|
||||
},
|
||||
{
|
||||
label: 'Red',
|
||||
action: () => {},
|
||||
color: '#ff0000'
|
||||
},
|
||||
{
|
||||
label: 'Green',
|
||||
action: () => {},
|
||||
color: '#00ff00'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
const wrapper = mount(ColorPickerMenu, {
|
||||
props: { option },
|
||||
global: {
|
||||
plugins: [PrimeVue],
|
||||
stubs: {
|
||||
Popover: {
|
||||
template: '<div><slot /></div>'
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
expect(wrapper.findAll('button[title]').map((node) => node.attributes('title'))).toEqual([
|
||||
'Red',
|
||||
'Green'
|
||||
])
|
||||
})
|
||||
})
|
||||
@@ -20,135 +20,49 @@
|
||||
}"
|
||||
>
|
||||
<div
|
||||
v-if="isCompactColorPanel"
|
||||
class="w-[15.5rem] rounded-2xl border border-border-default bg-interface-panel-surface p-2.5 shadow-lg"
|
||||
>
|
||||
<div class="mb-2 flex items-center justify-between gap-3">
|
||||
<div class="min-w-0">
|
||||
<p class="text-[11px] font-medium uppercase tracking-[0.18em] text-muted-foreground">
|
||||
{{ option.label }}
|
||||
</p>
|
||||
<p class="mt-0.5 truncate text-sm font-medium text-base-foreground">
|
||||
{{ pickerOption?.label ?? 'Custom' }}
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
class="rounded-md border border-border-default bg-secondary-background px-2 py-1 font-mono text-[10px] text-muted-foreground"
|
||||
>
|
||||
{{ selectedPickerColor }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ColorPicker
|
||||
v-if="pickerOption"
|
||||
data-testid="color-picker-inline"
|
||||
:model-value="pickerOption.pickerValue"
|
||||
inline
|
||||
format="hex"
|
||||
:aria-label="pickerOption.label"
|
||||
class="w-full"
|
||||
:pt="{
|
||||
root: { class: '!w-full' },
|
||||
content: {
|
||||
class: '!border-none !bg-transparent !p-0 !shadow-none'
|
||||
},
|
||||
colorSelector: {
|
||||
class: '!h-32 !w-full overflow-hidden !rounded-xl'
|
||||
},
|
||||
colorBackground: {
|
||||
class: '!rounded-xl'
|
||||
},
|
||||
colorHandle: {
|
||||
class:
|
||||
'!h-3.5 !w-3.5 !rounded-full !border-2 !border-black/70 !shadow-sm'
|
||||
},
|
||||
hue: {
|
||||
class:
|
||||
'!mt-2 !h-3 !overflow-hidden !rounded-full !border !border-border-default'
|
||||
},
|
||||
hueHandle: {
|
||||
class:
|
||||
'!h-3.5 !w-3.5 !-translate-x-1/2 !rounded-full !border-2 !border-white !shadow-sm'
|
||||
}
|
||||
}"
|
||||
@update:model-value="handleColorPickerUpdate(pickerOption, $event)"
|
||||
/>
|
||||
|
||||
<div
|
||||
v-if="swatchOptions.length"
|
||||
class="mt-2 rounded-xl border border-border-default bg-secondary-background p-2"
|
||||
>
|
||||
<div class="-mx-0.5 flex gap-1.5 overflow-x-auto px-0.5 pb-0.5">
|
||||
<button
|
||||
v-for="subOption in swatchOptions"
|
||||
:key="subOption.label"
|
||||
type="button"
|
||||
class="flex size-8 shrink-0 items-center justify-center rounded-xl border border-transparent transition-transform hover:scale-[1.04] hover:border-border-default hover:bg-secondary-background-hover"
|
||||
:title="subOption.label"
|
||||
@click="handleSubmenuClick(subOption)"
|
||||
>
|
||||
<div
|
||||
:class="
|
||||
cn(
|
||||
'size-5 rounded-full border transition-shadow',
|
||||
isSelectedSwatch(subOption)
|
||||
? 'border-white shadow-[0_0_0_2px_rgba(255,255,255,0.18)]'
|
||||
: 'border-border-default'
|
||||
)
|
||||
"
|
||||
:style="{ backgroundColor: subOption.color }"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-else
|
||||
:class="
|
||||
isColorSubmenu
|
||||
? 'flex flex-col gap-1 p-2'
|
||||
: 'flex min-w-40 flex-col p-2'
|
||||
"
|
||||
>
|
||||
<template v-for="subOption in option.submenu" :key="subOption.label">
|
||||
<div
|
||||
v-for="subOption in option.submenu"
|
||||
:key="subOption.label"
|
||||
:class="
|
||||
cn(
|
||||
'cursor-pointer rounded-sm hover:bg-secondary-background-hover',
|
||||
isColorSubmenu
|
||||
? 'flex size-7 items-center justify-center'
|
||||
: 'flex items-center gap-2 px-3 py-1.5 text-sm',
|
||||
subOption.disabled
|
||||
? 'pointer-events-none cursor-not-allowed text-node-icon-disabled'
|
||||
: 'hover:bg-secondary-background-hover'
|
||||
)
|
||||
"
|
||||
:title="subOption.label"
|
||||
@click="handleSubmenuClick(subOption)"
|
||||
>
|
||||
<div
|
||||
:class="
|
||||
cn(
|
||||
'cursor-pointer rounded-sm hover:bg-secondary-background-hover',
|
||||
isColorSubmenu
|
||||
? 'flex size-7 items-center justify-center'
|
||||
: 'flex items-center gap-2 px-3 py-1.5 text-sm',
|
||||
subOption.disabled
|
||||
? 'pointer-events-none cursor-not-allowed text-node-icon-disabled'
|
||||
: 'hover:bg-secondary-background-hover'
|
||||
)
|
||||
"
|
||||
:title="subOption.label"
|
||||
@click="handleSubmenuClick(subOption)"
|
||||
>
|
||||
<div
|
||||
v-if="subOption.color"
|
||||
class="size-5 rounded-full border border-border-default"
|
||||
:style="{ backgroundColor: subOption.color }"
|
||||
v-if="subOption.color"
|
||||
class="size-5 rounded-full border border-border-default"
|
||||
:style="{ backgroundColor: subOption.color }"
|
||||
/>
|
||||
<template v-else-if="!subOption.color">
|
||||
<i
|
||||
v-if="isShapeSelected(subOption)"
|
||||
class="icon-[lucide--check] size-4 shrink-0"
|
||||
/>
|
||||
<template v-else-if="!subOption.color">
|
||||
<i
|
||||
v-if="isShapeSelected(subOption)"
|
||||
class="icon-[lucide--check] size-4 shrink-0"
|
||||
/>
|
||||
<div v-else class="w-4 shrink-0" />
|
||||
<span>{{ subOption.label }}</span>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
<div v-else class="w-4 shrink-0" />
|
||||
<span>{{ subOption.label }}</span>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</Popover>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { cn } from '@comfyorg/tailwind-utils'
|
||||
import ColorPicker from 'primevue/colorpicker'
|
||||
import Popover from 'primevue/popover'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
@@ -188,43 +102,6 @@ const handleSubmenuClick = (subOption: SubMenuOption) => {
|
||||
popoverRef.value?.hide()
|
||||
}
|
||||
|
||||
const isPickerOption = (subOption: SubMenuOption): boolean =>
|
||||
typeof subOption.pickerValue === 'string' &&
|
||||
typeof subOption.onColorPick === 'function'
|
||||
|
||||
const pickerOption = computed(
|
||||
() => props.option.submenu?.find(isPickerOption) ?? null
|
||||
)
|
||||
|
||||
const swatchOptions = computed(() =>
|
||||
(props.option.submenu ?? []).filter(
|
||||
(subOption) => Boolean(subOption.color) && !isPickerOption(subOption)
|
||||
)
|
||||
)
|
||||
|
||||
const selectedPickerColor = computed(() =>
|
||||
pickerOption.value?.pickerValue
|
||||
? `#${pickerOption.value.pickerValue.toUpperCase()}`
|
||||
: '#000000'
|
||||
)
|
||||
|
||||
const isCompactColorPanel = computed(() => Boolean(pickerOption.value))
|
||||
|
||||
async function handleColorPickerUpdate(
|
||||
subOption: SubMenuOption,
|
||||
value: string
|
||||
) {
|
||||
if (!isPickerOption(subOption) || !value) return
|
||||
|
||||
await subOption.onColorPick?.(`#${value}`)
|
||||
}
|
||||
|
||||
function isSelectedSwatch(subOption: SubMenuOption): boolean {
|
||||
return (
|
||||
subOption.color?.toLowerCase() === selectedPickerColor.value.toLowerCase()
|
||||
)
|
||||
}
|
||||
|
||||
const isShapeSelected = (subOption: SubMenuOption): boolean => {
|
||||
if (subOption.color) return false
|
||||
|
||||
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,12 +72,12 @@
|
||||
/>
|
||||
</div>
|
||||
<SliderControl
|
||||
v-model="brushSizeSliderValue"
|
||||
v-model="brushSize"
|
||||
class="flex-1"
|
||||
label=""
|
||||
:min="0"
|
||||
:max="1"
|
||||
:step="0.001"
|
||||
:min="1"
|
||||
:max="250"
|
||||
:step="1"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -182,26 +182,6 @@ 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,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() {
|
||||
|
||||
@@ -1,22 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
import ColorPicker from 'primevue/colorpicker'
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import type {
|
||||
ColorOption,
|
||||
LGraphGroup,
|
||||
LGraphNode
|
||||
} from '@/lib/litegraph/src/litegraph'
|
||||
import { useCustomNodeColorSettings } from '@/composables/graph/useCustomNodeColorSettings'
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
|
||||
import { LGraphCanvas, LiteGraph } from '@/lib/litegraph/src/litegraph'
|
||||
import type { ColorOption } from '@/lib/litegraph/src/litegraph'
|
||||
import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
|
||||
import { adjustColor } from '@/utils/colorUtil'
|
||||
import {
|
||||
applyCustomColorToItems,
|
||||
getDefaultCustomNodeColor,
|
||||
getSharedAppliedColor
|
||||
} from '@/utils/nodeColorCustomization'
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
import LayoutField from './LayoutField.vue'
|
||||
@@ -26,7 +16,7 @@ import LayoutField from './LayoutField.vue'
|
||||
* Here, we only care about the getColorOption and setColorOption methods,
|
||||
* and do not concern ourselves with other methods.
|
||||
*/
|
||||
type PickedNode = LGraphNode | LGraphGroup
|
||||
type PickedNode = Pick<LGraphNode, 'getColorOption' | 'setColorOption'>
|
||||
|
||||
const { nodes } = defineProps<{ nodes: PickedNode[] }>()
|
||||
const emit = defineEmits<{ (e: 'changed'): void }>()
|
||||
@@ -34,14 +24,6 @@ const emit = defineEmits<{ (e: 'changed'): void }>()
|
||||
const { t } = useI18n()
|
||||
|
||||
const colorPaletteStore = useColorPaletteStore()
|
||||
const {
|
||||
darkerHeader,
|
||||
favoriteColors,
|
||||
isFavoriteColor,
|
||||
recentColors,
|
||||
rememberRecentColor,
|
||||
toggleFavoriteColor
|
||||
} = useCustomNodeColorSettings()
|
||||
|
||||
type NodeColorOption = {
|
||||
name: string
|
||||
@@ -120,127 +102,43 @@ const nodeColor = computed<NodeColorOption['name'] | null>({
|
||||
emit('changed')
|
||||
}
|
||||
})
|
||||
|
||||
const currentAppliedColor = computed(() => getSharedAppliedColor(nodes))
|
||||
const currentPickerValue = computed(() =>
|
||||
(currentAppliedColor.value ?? getDefaultCustomNodeColor()).replace('#', '')
|
||||
)
|
||||
|
||||
async function applySavedCustomColor(color: string) {
|
||||
applyCustomColorToItems(nodes, color, {
|
||||
darkerHeader: darkerHeader.value
|
||||
})
|
||||
await rememberRecentColor(color)
|
||||
emit('changed')
|
||||
}
|
||||
|
||||
async function toggleCurrentColorFavorite() {
|
||||
if (!currentAppliedColor.value) return
|
||||
await toggleFavoriteColor(currentAppliedColor.value)
|
||||
}
|
||||
|
||||
const isCurrentColorFavorite = computed(() =>
|
||||
isFavoriteColor(currentAppliedColor.value)
|
||||
)
|
||||
|
||||
async function onCustomColorUpdate(value: string) {
|
||||
await applySavedCustomColor(`#${value}`)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<LayoutField :label="t('rightSidePanel.color')">
|
||||
<div class="space-y-2">
|
||||
<div
|
||||
class="grid grid-cols-5 justify-items-center gap-1 rounded-lg border-none bg-secondary-background p-1"
|
||||
<div
|
||||
class="grid grid-cols-5 justify-items-center gap-1 rounded-lg border-none bg-secondary-background p-1"
|
||||
>
|
||||
<button
|
||||
v-for="option of colorOptions"
|
||||
:key="option.name"
|
||||
:class="
|
||||
cn(
|
||||
'flex size-8 cursor-pointer items-center justify-center rounded-lg border-0 bg-transparent text-left ring-0 outline-0',
|
||||
option.name === nodeColor
|
||||
? 'bg-interface-menu-component-surface-selected'
|
||||
: 'hover:bg-interface-menu-component-surface-selected'
|
||||
)
|
||||
"
|
||||
@click="nodeColor = option.name"
|
||||
>
|
||||
<button
|
||||
v-for="option of colorOptions"
|
||||
:key="option.name"
|
||||
:class="
|
||||
cn(
|
||||
'flex size-8 cursor-pointer items-center justify-center rounded-lg border-0 bg-transparent text-left ring-0 outline-0',
|
||||
<div
|
||||
v-tooltip.top="option.localizedName()"
|
||||
:class="cn('size-4 rounded-full ring-2 ring-gray-500/10')"
|
||||
:style="{
|
||||
backgroundColor: isLightTheme
|
||||
? option.value.light
|
||||
: option.value.dark,
|
||||
'--tw-ring-color':
|
||||
option.name === nodeColor
|
||||
? 'bg-interface-menu-component-surface-selected'
|
||||
: 'hover:bg-interface-menu-component-surface-selected'
|
||||
)
|
||||
"
|
||||
@click="nodeColor = option.name"
|
||||
>
|
||||
<div
|
||||
v-tooltip.top="option.localizedName()"
|
||||
:class="cn('size-4 rounded-full ring-2 ring-gray-500/10')"
|
||||
:style="{
|
||||
backgroundColor: isLightTheme
|
||||
? option.value.light
|
||||
: option.value.dark,
|
||||
'--tw-ring-color':
|
||||
option.name === nodeColor
|
||||
? isLightTheme
|
||||
? option.value.ringLight
|
||||
: option.value.ringDark
|
||||
: undefined
|
||||
}"
|
||||
:data-testid="option.name"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<ColorPicker
|
||||
:model-value="currentPickerValue"
|
||||
format="hex"
|
||||
:aria-label="t('g.custom')"
|
||||
class="h-8 w-8 overflow-hidden rounded-md border border-border-default bg-secondary-background"
|
||||
:pt="{
|
||||
preview: {
|
||||
class: '!h-full !w-full !rounded-md !border-none'
|
||||
}
|
||||
? isLightTheme
|
||||
? option.value.ringLight
|
||||
: option.value.ringDark
|
||||
: undefined
|
||||
}"
|
||||
@update:model-value="onCustomColorUpdate"
|
||||
:data-testid="option.name"
|
||||
/>
|
||||
<button
|
||||
class="flex size-8 cursor-pointer items-center justify-center rounded-md border border-border-default bg-secondary-background hover:bg-secondary-background-hover disabled:cursor-not-allowed disabled:opacity-50"
|
||||
:title="isCurrentColorFavorite ? t('g.remove') : t('g.favorites')"
|
||||
:disabled="!currentAppliedColor"
|
||||
@click="toggleCurrentColorFavorite"
|
||||
>
|
||||
<i
|
||||
:class="
|
||||
isCurrentColorFavorite
|
||||
? 'icon-[lucide--star] text-yellow-500'
|
||||
: 'icon-[lucide--star-off]'
|
||||
"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="favoriteColors.length" class="flex flex-wrap gap-1">
|
||||
<button
|
||||
v-for="color in favoriteColors"
|
||||
:key="`favorite-${color}`"
|
||||
class="flex size-7 cursor-pointer items-center justify-center rounded-md border border-border-default bg-secondary-background hover:bg-secondary-background-hover"
|
||||
:title="`${t('g.favorites')}: ${color.toUpperCase()}`"
|
||||
@click="applySavedCustomColor(color)"
|
||||
>
|
||||
<div
|
||||
class="size-4 rounded-full border border-border-default"
|
||||
:style="{ backgroundColor: color }"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="recentColors.length" class="flex flex-wrap gap-1">
|
||||
<button
|
||||
v-for="color in recentColors"
|
||||
:key="`recent-${color}`"
|
||||
class="flex size-7 cursor-pointer items-center justify-center rounded-md border border-border-default bg-secondary-background hover:bg-secondary-background-hover"
|
||||
:title="`${t('modelLibrary.sortRecent')}: ${color.toUpperCase()}`"
|
||||
@click="applySavedCustomColor(color)"
|
||||
>
|
||||
<div
|
||||
class="size-4 rounded-full border border-border-default"
|
||||
:style="{ backgroundColor: color }"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</LayoutField>
|
||||
</template>
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
tabindex="0"
|
||||
:aria-label="
|
||||
t('assetBrowser.ariaLabel.assetCard', {
|
||||
name: getAssetDisplayName(item.asset),
|
||||
name: item.asset.name,
|
||||
type: getAssetMediaType(item.asset)
|
||||
})
|
||||
"
|
||||
@@ -44,7 +44,7 @@
|
||||
)
|
||||
"
|
||||
:preview-url="getAssetPreviewUrl(item.asset)"
|
||||
:preview-alt="getAssetDisplayName(item.asset)"
|
||||
:preview-alt="item.asset.name"
|
||||
:icon-name="iconForMediaType(getAssetMediaType(item.asset))"
|
||||
:is-video-preview="isVideoAsset(item.asset)"
|
||||
:primary-text="getAssetPrimaryText(item.asset)"
|
||||
@@ -133,12 +133,8 @@ const listGridStyle = {
|
||||
gap: '0.5rem'
|
||||
}
|
||||
|
||||
function getAssetDisplayName(asset: AssetItem): string {
|
||||
return asset.display_name || asset.name
|
||||
}
|
||||
|
||||
function getAssetPrimaryText(asset: AssetItem): string {
|
||||
return truncateFilename(getAssetDisplayName(asset))
|
||||
return truncateFilename(asset.name)
|
||||
}
|
||||
|
||||
function getAssetMediaType(asset: AssetItem) {
|
||||
|
||||
@@ -569,7 +569,7 @@ const handleZoomClick = (asset: AssetItem) => {
|
||||
const dialogStore = useDialogStore()
|
||||
dialogStore.showDialog({
|
||||
key: 'asset-3d-viewer',
|
||||
title: asset.display_name || asset.name,
|
||||
title: asset.name,
|
||||
component: Load3dViewerContent,
|
||||
props: {
|
||||
modelUrl: asset.preview_url || ''
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
})
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import {
|
||||
NODE_COLOR_DARKER_HEADER_SETTING_ID,
|
||||
NODE_COLOR_FAVORITES_SETTING_ID,
|
||||
NODE_COLOR_RECENTS_SETTING_ID,
|
||||
normalizeNodeColor,
|
||||
toggleFavoriteNodeColor,
|
||||
upsertRecentNodeColor
|
||||
} from '@/utils/nodeColorCustomization'
|
||||
|
||||
export function useCustomNodeColorSettings() {
|
||||
const settingStore = useSettingStore()
|
||||
|
||||
const favoriteColors = computed(() =>
|
||||
settingStore.get(NODE_COLOR_FAVORITES_SETTING_ID) ?? []
|
||||
)
|
||||
const recentColors = computed(() =>
|
||||
settingStore.get(NODE_COLOR_RECENTS_SETTING_ID) ?? []
|
||||
)
|
||||
const darkerHeader = computed(() =>
|
||||
settingStore.get(NODE_COLOR_DARKER_HEADER_SETTING_ID) ?? true
|
||||
)
|
||||
|
||||
async function rememberRecentColor(color: string) {
|
||||
const nextColors = upsertRecentNodeColor(recentColors.value, color)
|
||||
await settingStore.set(NODE_COLOR_RECENTS_SETTING_ID, nextColors)
|
||||
}
|
||||
|
||||
async function toggleFavoriteColor(color: string) {
|
||||
const nextColors = toggleFavoriteNodeColor(favoriteColors.value, color)
|
||||
await settingStore.set(NODE_COLOR_FAVORITES_SETTING_ID, nextColors)
|
||||
}
|
||||
|
||||
function isFavoriteColor(color: string | null | undefined) {
|
||||
if (!color) return false
|
||||
return favoriteColors.value.includes(normalizeNodeColor(color))
|
||||
}
|
||||
|
||||
return {
|
||||
favoriteColors,
|
||||
recentColors,
|
||||
darkerHeader,
|
||||
rememberRecentColor,
|
||||
toggleFavoriteColor,
|
||||
isFavoriteColor
|
||||
}
|
||||
}
|
||||
@@ -1,159 +0,0 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import type * as VueI18nModule from 'vue-i18n'
|
||||
|
||||
import { LGraphGroup, LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import type * as NodeColorCustomizationModule from '@/utils/nodeColorCustomization'
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
refreshCanvas: vi.fn(),
|
||||
rememberRecentColor: vi.fn().mockResolvedValue(undefined),
|
||||
selectedItems: [] as unknown[]
|
||||
}))
|
||||
|
||||
vi.mock('vue-i18n', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof VueI18nModule>()
|
||||
|
||||
return {
|
||||
...actual,
|
||||
useI18n: () => ({
|
||||
t: (key: string) => key
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('@/renderer/core/canvas/canvasStore', () => ({
|
||||
useCanvasStore: () => ({
|
||||
selectedItems: mocks.selectedItems,
|
||||
canvas: {
|
||||
setDirty: vi.fn()
|
||||
}
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('@/platform/settings/settingStore', () => ({
|
||||
useSettingStore: () => ({
|
||||
get: vi.fn()
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('@/platform/workflow/management/stores/workflowStore', () => ({
|
||||
useWorkflowStore: () => ({
|
||||
activeWorkflow: null
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('@/composables/graph/useCanvasRefresh', () => ({
|
||||
useCanvasRefresh: () => ({
|
||||
refreshCanvas: mocks.refreshCanvas
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('@/composables/graph/useCustomNodeColorSettings', () => ({
|
||||
useCustomNodeColorSettings: () => ({
|
||||
darkerHeader: { value: true },
|
||||
favoriteColors: { value: ['#abcdef'] },
|
||||
recentColors: { value: [] },
|
||||
rememberRecentColor: mocks.rememberRecentColor
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('@/composables/graph/useNodeCustomization', () => ({
|
||||
useNodeCustomization: () => ({
|
||||
colorOptions: [
|
||||
{
|
||||
name: 'noColor',
|
||||
localizedName: 'color.noColor',
|
||||
value: {
|
||||
dark: '#353535',
|
||||
light: '#6f6f6f'
|
||||
}
|
||||
}
|
||||
],
|
||||
isLightTheme: { value: false },
|
||||
shapeOptions: []
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('@/utils/nodeColorCustomization', async () =>
|
||||
vi.importActual<typeof NodeColorCustomizationModule>(
|
||||
'@/utils/nodeColorCustomization'
|
||||
)
|
||||
)
|
||||
|
||||
function createNode() {
|
||||
return Object.assign(Object.create(LGraphNode.prototype), {
|
||||
color: undefined,
|
||||
bgcolor: undefined,
|
||||
getColorOption: () => null
|
||||
}) as LGraphNode
|
||||
}
|
||||
|
||||
function createGroup(color?: string) {
|
||||
return Object.assign(Object.create(LGraphGroup.prototype), {
|
||||
color,
|
||||
getColorOption: () => null
|
||||
}) as LGraphGroup
|
||||
}
|
||||
|
||||
describe('useGroupMenuOptions', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mocks.selectedItems = []
|
||||
})
|
||||
|
||||
it('applies saved custom colors to the group context only', async () => {
|
||||
const selectedNode = createNode()
|
||||
const groupContext = createGroup()
|
||||
mocks.selectedItems = [selectedNode, groupContext]
|
||||
|
||||
const { useGroupMenuOptions } = await import('./useGroupMenuOptions')
|
||||
const { getGroupColorOptions } = useGroupMenuOptions()
|
||||
const bump = vi.fn()
|
||||
|
||||
const colorMenu = getGroupColorOptions(groupContext, bump)
|
||||
const favoriteEntry = colorMenu.submenu?.find((entry) =>
|
||||
entry.label.includes('#ABCDEF')
|
||||
)
|
||||
|
||||
expect(favoriteEntry).toBeDefined()
|
||||
|
||||
await favoriteEntry?.action()
|
||||
|
||||
expect(groupContext.color).toBe('#abcdef')
|
||||
expect(selectedNode.bgcolor).toBeUndefined()
|
||||
expect(mocks.refreshCanvas).toHaveBeenCalledOnce()
|
||||
expect(mocks.rememberRecentColor).toHaveBeenCalledWith('#abcdef')
|
||||
expect(bump).toHaveBeenCalledOnce()
|
||||
expect(mocks.rememberRecentColor.mock.invocationCallOrder[0]).toBeLessThan(
|
||||
bump.mock.invocationCallOrder[0]
|
||||
)
|
||||
})
|
||||
|
||||
it('seeds the PrimeVue custom picker from the clicked group color', async () => {
|
||||
const selectedNode = createNode()
|
||||
selectedNode.bgcolor = '#445566'
|
||||
const groupContext = createGroup('#112233')
|
||||
mocks.selectedItems = [selectedNode, groupContext]
|
||||
|
||||
const { useGroupMenuOptions } = await import('./useGroupMenuOptions')
|
||||
const { getGroupColorOptions } = useGroupMenuOptions()
|
||||
const bump = vi.fn()
|
||||
const colorMenu = getGroupColorOptions(groupContext, bump)
|
||||
const customEntry = colorMenu.submenu?.find(
|
||||
(entry) => entry.label === 'g.custom'
|
||||
)
|
||||
|
||||
expect(customEntry).toBeDefined()
|
||||
expect(customEntry?.color).toBe('#112233')
|
||||
expect(customEntry?.pickerValue).toBe('112233')
|
||||
|
||||
await customEntry?.onColorPick?.('#fedcba')
|
||||
|
||||
expect(groupContext.color).toBe('#fedcba')
|
||||
expect(selectedNode.bgcolor).toBe('#445566')
|
||||
expect(mocks.rememberRecentColor).toHaveBeenCalledWith('#fedcba')
|
||||
expect(mocks.rememberRecentColor.mock.invocationCallOrder[0]).toBeLessThan(
|
||||
bump.mock.invocationCallOrder[0]
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -1,16 +1,10 @@
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { useCustomNodeColorSettings } from '@/composables/graph/useCustomNodeColorSettings'
|
||||
import { LGraphEventMode } from '@/lib/litegraph/src/litegraph'
|
||||
import type { LGraphGroup, LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
|
||||
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||
import {
|
||||
applyCustomColorToItems,
|
||||
getDefaultCustomNodeColor,
|
||||
getSharedAppliedColor
|
||||
} from '@/utils/nodeColorCustomization'
|
||||
|
||||
import { useCanvasRefresh } from './useCanvasRefresh'
|
||||
import type { MenuOption } from './useMoreOptionsMenu'
|
||||
@@ -25,24 +19,7 @@ export function useGroupMenuOptions() {
|
||||
const workflowStore = useWorkflowStore()
|
||||
const settingStore = useSettingStore()
|
||||
const canvasRefresh = useCanvasRefresh()
|
||||
const { darkerHeader, favoriteColors, recentColors, rememberRecentColor } =
|
||||
useCustomNodeColorSettings()
|
||||
const {
|
||||
colorOptions,
|
||||
isLightTheme,
|
||||
shapeOptions
|
||||
} = useNodeCustomization()
|
||||
|
||||
const applyCustomColorToGroup = async (
|
||||
groupContext: LGraphGroup,
|
||||
color: string
|
||||
) => {
|
||||
applyCustomColorToItems([groupContext], color, {
|
||||
darkerHeader: darkerHeader.value
|
||||
})
|
||||
canvasRefresh.refreshCanvas()
|
||||
await rememberRecentColor(color)
|
||||
}
|
||||
const { shapeOptions, colorOptions, isLightTheme } = useNodeCustomization()
|
||||
|
||||
const getFitGroupToNodesOption = (groupContext: LGraphGroup): MenuOption => ({
|
||||
label: 'Fit Group To Nodes',
|
||||
@@ -88,68 +65,19 @@ export function useGroupMenuOptions() {
|
||||
label: t('contextMenu.Color'),
|
||||
icon: 'icon-[lucide--palette]',
|
||||
hasSubmenu: true,
|
||||
submenu: (() => {
|
||||
const currentAppliedColor = getSharedAppliedColor([groupContext])
|
||||
const presetEntries = colorOptions.map((colorOption) => ({
|
||||
label: colorOption.localizedName,
|
||||
color: isLightTheme.value
|
||||
submenu: colorOptions.map((colorOption) => ({
|
||||
label: colorOption.localizedName,
|
||||
color: isLightTheme.value
|
||||
? colorOption.value.light
|
||||
: colorOption.value.dark,
|
||||
action: () => {
|
||||
groupContext.color = isLightTheme.value
|
||||
? colorOption.value.light
|
||||
: colorOption.value.dark,
|
||||
action: () => {
|
||||
groupContext.color = isLightTheme.value
|
||||
? colorOption.value.light
|
||||
: colorOption.value.dark
|
||||
canvasRefresh.refreshCanvas()
|
||||
bump()
|
||||
}
|
||||
}))
|
||||
|
||||
const presetColors = new Set(
|
||||
colorOptions.map((colorOption) => colorOption.value.dark.toLowerCase())
|
||||
)
|
||||
const customEntries = [
|
||||
...favoriteColors.value.map((color) => ({
|
||||
label: `${t('g.favorites')}: ${color.toUpperCase()}`,
|
||||
color
|
||||
})),
|
||||
...recentColors.value.map((color) => ({
|
||||
label: `${t('modelLibrary.sortRecent')}: ${color.toUpperCase()}`,
|
||||
color
|
||||
}))
|
||||
]
|
||||
.filter((entry, index, entries) => {
|
||||
return (
|
||||
entries.findIndex((candidate) => candidate.color === entry.color) ===
|
||||
index
|
||||
)
|
||||
})
|
||||
.filter((entry) => !presetColors.has(entry.color.toLowerCase()))
|
||||
.map((entry) => ({
|
||||
...entry,
|
||||
action: async () => {
|
||||
await applyCustomColorToGroup(groupContext, entry.color)
|
||||
bump()
|
||||
}
|
||||
}))
|
||||
|
||||
return [
|
||||
...presetEntries,
|
||||
...customEntries,
|
||||
{
|
||||
label: t('g.custom'),
|
||||
color: currentAppliedColor ?? undefined,
|
||||
pickerValue: (currentAppliedColor ?? getDefaultCustomNodeColor()).replace(
|
||||
'#',
|
||||
''
|
||||
),
|
||||
onColorPick: async (color: string) => {
|
||||
await applyCustomColorToGroup(groupContext, color)
|
||||
bump()
|
||||
},
|
||||
action: () => {}
|
||||
}
|
||||
]
|
||||
})()
|
||||
: colorOption.value.dark
|
||||
canvasRefresh.refreshCanvas()
|
||||
bump()
|
||||
}
|
||||
}))
|
||||
})
|
||||
|
||||
const getGroupModeOptions = (
|
||||
|
||||
@@ -41,8 +41,6 @@ export interface SubMenuOption {
|
||||
action: () => void
|
||||
color?: string
|
||||
disabled?: boolean
|
||||
pickerValue?: string
|
||||
onColorPick?: (color: string) => void | Promise<void>
|
||||
}
|
||||
|
||||
export enum BadgeVariant {
|
||||
|
||||
@@ -11,12 +11,7 @@ import {
|
||||
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||
import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
|
||||
import { adjustColor } from '@/utils/colorUtil'
|
||||
import {
|
||||
applyCustomColorToItems,
|
||||
getSharedAppliedColor
|
||||
} from '@/utils/nodeColorCustomization'
|
||||
|
||||
import { useCustomNodeColorSettings } from './useCustomNodeColorSettings'
|
||||
import { useCanvasRefresh } from './useCanvasRefresh'
|
||||
|
||||
interface ColorOption {
|
||||
@@ -41,12 +36,6 @@ export function useNodeCustomization() {
|
||||
const { t } = useI18n()
|
||||
const canvasStore = useCanvasStore()
|
||||
const colorPaletteStore = useColorPaletteStore()
|
||||
const {
|
||||
favoriteColors,
|
||||
recentColors,
|
||||
darkerHeader,
|
||||
rememberRecentColor
|
||||
} = useCustomNodeColorSettings()
|
||||
const canvasRefresh = useCanvasRefresh()
|
||||
const isLightTheme = computed(
|
||||
() => colorPaletteStore.completedActivePalette.light_theme
|
||||
@@ -112,19 +101,6 @@ export function useNodeCustomization() {
|
||||
canvasRefresh.refreshCanvas()
|
||||
}
|
||||
|
||||
const applyCustomColor = async (color: string) => {
|
||||
const normalized = applyCustomColorToItems(
|
||||
canvasStore.selectedItems,
|
||||
color,
|
||||
{
|
||||
darkerHeader: darkerHeader.value
|
||||
}
|
||||
)
|
||||
|
||||
canvasRefresh.refreshCanvas()
|
||||
await rememberRecentColor(normalized)
|
||||
}
|
||||
|
||||
const applyShape = (shapeOption: ShapeOption) => {
|
||||
const selectedNodes = Array.from(canvasStore.selectedItems).filter(
|
||||
(item): item is LGraphNode => item instanceof LGraphNode
|
||||
@@ -179,20 +155,13 @@ export function useNodeCustomization() {
|
||||
)
|
||||
}
|
||||
|
||||
const getCurrentAppliedColor = (): string | null =>
|
||||
getSharedAppliedColor(Array.from(canvasStore.selectedItems))
|
||||
|
||||
return {
|
||||
colorOptions,
|
||||
shapeOptions,
|
||||
applyColor,
|
||||
applyCustomColor,
|
||||
applyShape,
|
||||
getCurrentColor,
|
||||
getCurrentAppliedColor,
|
||||
getCurrentShape,
|
||||
favoriteColors,
|
||||
recentColors,
|
||||
isLightTheme
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,93 +0,0 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import type * as VueI18nModule from 'vue-i18n'
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
applyShape: vi.fn(),
|
||||
applyColor: vi.fn(),
|
||||
applyCustomColor: vi.fn(),
|
||||
adjustNodeSize: vi.fn(),
|
||||
toggleNodeCollapse: vi.fn(),
|
||||
toggleNodePin: vi.fn(),
|
||||
toggleNodeBypass: vi.fn(),
|
||||
runBranch: vi.fn(),
|
||||
getCurrentAppliedColor: vi.fn<() => string | null>(() => null)
|
||||
}))
|
||||
|
||||
vi.mock('vue-i18n', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof VueI18nModule>()
|
||||
|
||||
return {
|
||||
...actual,
|
||||
useI18n: () => ({
|
||||
t: (key: string) => key
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('./useNodeCustomization', () => ({
|
||||
useNodeCustomization: () => ({
|
||||
shapeOptions: [],
|
||||
applyShape: mocks.applyShape,
|
||||
applyColor: mocks.applyColor,
|
||||
applyCustomColor: mocks.applyCustomColor,
|
||||
colorOptions: [
|
||||
{
|
||||
name: 'noColor',
|
||||
localizedName: 'color.noColor',
|
||||
value: {
|
||||
dark: '#353535',
|
||||
light: '#6f6f6f'
|
||||
}
|
||||
}
|
||||
],
|
||||
favoriteColors: { value: [] },
|
||||
recentColors: { value: [] },
|
||||
getCurrentAppliedColor: mocks.getCurrentAppliedColor,
|
||||
isLightTheme: { value: false }
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('./useSelectedNodeActions', () => ({
|
||||
useSelectedNodeActions: () => ({
|
||||
adjustNodeSize: mocks.adjustNodeSize,
|
||||
toggleNodeCollapse: mocks.toggleNodeCollapse,
|
||||
toggleNodePin: mocks.toggleNodePin,
|
||||
toggleNodeBypass: mocks.toggleNodeBypass,
|
||||
runBranch: mocks.runBranch
|
||||
})
|
||||
}))
|
||||
|
||||
describe('useNodeMenuOptions', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mocks.getCurrentAppliedColor.mockReturnValue(null)
|
||||
})
|
||||
|
||||
it('keeps the custom node color entry unset when there is no shared applied color', async () => {
|
||||
const { useNodeMenuOptions } = await import('./useNodeMenuOptions')
|
||||
const { colorSubmenu } = useNodeMenuOptions()
|
||||
|
||||
const customEntry = colorSubmenu.value.find(
|
||||
(entry) => entry.label === 'g.custom'
|
||||
)
|
||||
|
||||
expect(customEntry).toBeDefined()
|
||||
expect(customEntry?.color).toBeUndefined()
|
||||
expect(customEntry?.pickerValue).toBe('353535')
|
||||
})
|
||||
|
||||
it('preserves the shared applied color for the custom node color entry', async () => {
|
||||
mocks.getCurrentAppliedColor.mockReturnValue('#abcdef')
|
||||
|
||||
const { useNodeMenuOptions } = await import('./useNodeMenuOptions')
|
||||
const { colorSubmenu } = useNodeMenuOptions()
|
||||
|
||||
const customEntry = colorSubmenu.value.find(
|
||||
(entry) => entry.label === 'g.custom'
|
||||
)
|
||||
|
||||
expect(customEntry).toBeDefined()
|
||||
expect(customEntry?.color).toBe('#abcdef')
|
||||
expect(customEntry?.pickerValue).toBe('abcdef')
|
||||
})
|
||||
})
|
||||
@@ -1,8 +1,6 @@
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { getDefaultCustomNodeColor } from '@/utils/nodeColorCustomization'
|
||||
|
||||
import type { MenuOption } from './useMoreOptionsMenu'
|
||||
import { useNodeCustomization } from './useNodeCustomization'
|
||||
import { useSelectedNodeActions } from './useSelectedNodeActions'
|
||||
@@ -13,17 +11,8 @@ import type { NodeSelectionState } from './useSelectionState'
|
||||
*/
|
||||
export function useNodeMenuOptions() {
|
||||
const { t } = useI18n()
|
||||
const {
|
||||
shapeOptions,
|
||||
applyShape,
|
||||
applyColor,
|
||||
applyCustomColor,
|
||||
colorOptions,
|
||||
favoriteColors,
|
||||
recentColors,
|
||||
getCurrentAppliedColor,
|
||||
isLightTheme
|
||||
} = useNodeCustomization()
|
||||
const { shapeOptions, applyShape, applyColor, colorOptions, isLightTheme } =
|
||||
useNodeCustomization()
|
||||
const {
|
||||
adjustNodeSize,
|
||||
toggleNodeCollapse,
|
||||
@@ -40,8 +29,7 @@ export function useNodeMenuOptions() {
|
||||
)
|
||||
|
||||
const colorSubmenu = computed(() => {
|
||||
const currentAppliedColor = getCurrentAppliedColor()
|
||||
const presetEntries = colorOptions.map((colorOption) => ({
|
||||
return colorOptions.map((colorOption) => ({
|
||||
label: colorOption.localizedName,
|
||||
color: isLightTheme.value
|
||||
? colorOption.value.light
|
||||
@@ -49,48 +37,6 @@ export function useNodeMenuOptions() {
|
||||
action: () =>
|
||||
applyColor(colorOption.name === 'noColor' ? null : colorOption)
|
||||
}))
|
||||
|
||||
const presetColors = new Set(
|
||||
colorOptions.map((colorOption) => colorOption.value.dark.toLowerCase())
|
||||
)
|
||||
const customEntries = [
|
||||
...favoriteColors.value.map((color) => ({
|
||||
label: `${t('g.favorites')}: ${color.toUpperCase()}`,
|
||||
color
|
||||
})),
|
||||
...recentColors.value.map((color) => ({
|
||||
label: `${t('modelLibrary.sortRecent')}: ${color.toUpperCase()}`,
|
||||
color
|
||||
}))
|
||||
]
|
||||
.filter((entry, index, entries) => {
|
||||
return (
|
||||
entries.findIndex((candidate) => candidate.color === entry.color) ===
|
||||
index
|
||||
)
|
||||
})
|
||||
.filter((entry) => !presetColors.has(entry.color.toLowerCase()))
|
||||
.map((entry) => ({
|
||||
...entry,
|
||||
action: () => {
|
||||
void applyCustomColor(entry.color)
|
||||
}
|
||||
}))
|
||||
|
||||
return [
|
||||
...presetEntries,
|
||||
...customEntries,
|
||||
{
|
||||
label: t('g.custom'),
|
||||
color: currentAppliedColor ?? undefined,
|
||||
pickerValue: (currentAppliedColor ?? getDefaultCustomNodeColor()).replace(
|
||||
'#',
|
||||
''
|
||||
),
|
||||
onColorPick: applyCustomColor,
|
||||
action: () => {}
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
const getAdjustSizeOption = (): MenuOption => ({
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 []
|
||||
}
|
||||
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,400 +0,0 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
vi.mock('@/i18n', () => ({
|
||||
st: (_key: string, fallback: string) => fallback
|
||||
}))
|
||||
|
||||
import type { ContextMenu } from './ContextMenu'
|
||||
import { LGraphCanvas } from './LGraphCanvas'
|
||||
import { LGraphGroup } from './LGraphGroup'
|
||||
import { LGraphNode } from './LGraphNode'
|
||||
import { LiteGraph } from './litegraph'
|
||||
|
||||
describe('LGraphCanvas.onMenuNodeColors', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('does not add a custom color entry to the legacy submenu', () => {
|
||||
const node = Object.assign(Object.create(LGraphNode.prototype), {
|
||||
color: undefined,
|
||||
bgcolor: undefined
|
||||
}) as LGraphNode
|
||||
|
||||
const canvas = {
|
||||
selectedItems: new Set([node]),
|
||||
setDirty: vi.fn()
|
||||
}
|
||||
LGraphCanvas.active_canvas = canvas as unknown as LGraphCanvas
|
||||
|
||||
let capturedValues:
|
||||
| ReadonlyArray<{ content?: string } | string | null>
|
||||
| undefined
|
||||
const originalContextMenu = LiteGraph.ContextMenu
|
||||
class MockContextMenu {
|
||||
constructor(values: ReadonlyArray<{ content?: string } | string | null>) {
|
||||
capturedValues = values
|
||||
}
|
||||
}
|
||||
LiteGraph.ContextMenu =
|
||||
MockContextMenu as unknown as typeof LiteGraph.ContextMenu
|
||||
|
||||
try {
|
||||
LGraphCanvas.onMenuNodeColors(
|
||||
{ content: 'Colors', value: null },
|
||||
{} as never,
|
||||
new MouseEvent('contextmenu'),
|
||||
{} as ContextMenu<string | null>,
|
||||
node
|
||||
)
|
||||
|
||||
const contents = capturedValues
|
||||
?.filter(
|
||||
(value): value is { content?: string } =>
|
||||
typeof value === 'object' && value !== null
|
||||
)
|
||||
.map((value) => value.content ?? '')
|
||||
|
||||
expect(contents).not.toEqual(
|
||||
expect.arrayContaining([expect.stringContaining('Custom...')])
|
||||
)
|
||||
} finally {
|
||||
LiteGraph.ContextMenu = originalContextMenu
|
||||
}
|
||||
})
|
||||
|
||||
it('uses group preset colors for legacy group menu swatches', () => {
|
||||
const group = Object.assign(Object.create(LGraphGroup.prototype), {
|
||||
color: undefined
|
||||
}) as LGraphGroup
|
||||
|
||||
const canvas = {
|
||||
selectedItems: new Set([group]),
|
||||
setDirty: vi.fn()
|
||||
}
|
||||
LGraphCanvas.active_canvas = canvas as unknown as LGraphCanvas
|
||||
|
||||
let capturedValues:
|
||||
| ReadonlyArray<{ content?: string } | string | null>
|
||||
| undefined
|
||||
const originalContextMenu = LiteGraph.ContextMenu
|
||||
class MockContextMenu {
|
||||
constructor(values: ReadonlyArray<{ content?: string } | string | null>) {
|
||||
capturedValues = values
|
||||
}
|
||||
}
|
||||
LiteGraph.ContextMenu =
|
||||
MockContextMenu as unknown as typeof LiteGraph.ContextMenu
|
||||
|
||||
try {
|
||||
LGraphCanvas.onMenuNodeColors(
|
||||
{ content: 'Colors', value: null },
|
||||
{} as never,
|
||||
new MouseEvent('contextmenu'),
|
||||
{} as ContextMenu<string | null>,
|
||||
group
|
||||
)
|
||||
|
||||
const contents = capturedValues
|
||||
?.filter(
|
||||
(value): value is { content?: string } =>
|
||||
typeof value === 'object' && value !== null
|
||||
)
|
||||
.map((value) => value.content ?? '')
|
||||
|
||||
expect(contents).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.stringContaining(LGraphCanvas.node_colors.red.groupcolor)
|
||||
])
|
||||
)
|
||||
} finally {
|
||||
LiteGraph.ContextMenu = originalContextMenu
|
||||
}
|
||||
})
|
||||
|
||||
it('sanitizes legacy menu markup for extension-provided labels and colors', () => {
|
||||
const node = Object.assign(Object.create(LGraphNode.prototype), {
|
||||
color: undefined,
|
||||
bgcolor: undefined
|
||||
}) as LGraphNode
|
||||
|
||||
const canvas = {
|
||||
selectedItems: new Set([node]),
|
||||
setDirty: vi.fn()
|
||||
}
|
||||
LGraphCanvas.active_canvas = canvas as unknown as LGraphCanvas
|
||||
|
||||
let capturedValues:
|
||||
| ReadonlyArray<{ content?: string } | string | null>
|
||||
| undefined
|
||||
const originalContextMenu = LiteGraph.ContextMenu
|
||||
const originalNodeColors = LGraphCanvas.node_colors
|
||||
class MockContextMenu {
|
||||
constructor(values: ReadonlyArray<{ content?: string } | string | null>) {
|
||||
capturedValues = values
|
||||
}
|
||||
}
|
||||
LiteGraph.ContextMenu =
|
||||
MockContextMenu as unknown as typeof LiteGraph.ContextMenu
|
||||
LGraphCanvas.node_colors = {
|
||||
...originalNodeColors,
|
||||
'<img src=x onerror=1>': {
|
||||
color: '#000',
|
||||
bgcolor: 'not-a-color',
|
||||
groupcolor: '#fff'
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
LGraphCanvas.onMenuNodeColors(
|
||||
{ content: 'Colors', value: null },
|
||||
{} as never,
|
||||
new MouseEvent('contextmenu'),
|
||||
{} as ContextMenu<string | null>,
|
||||
node
|
||||
)
|
||||
|
||||
const escapedEntry = capturedValues
|
||||
?.filter(
|
||||
(value): value is { content?: string } =>
|
||||
typeof value === 'object' && value !== null
|
||||
)
|
||||
.map((value) => value.content ?? '')
|
||||
.find((content) => content.includes('<img src=x onerror=1>'))
|
||||
|
||||
expect(escapedEntry).toBeDefined()
|
||||
expect(escapedEntry).not.toContain('<img src=x onerror=1>')
|
||||
expect(escapedEntry).not.toContain('background-color:not-a-color')
|
||||
} finally {
|
||||
LiteGraph.ContextMenu = originalContextMenu
|
||||
LGraphCanvas.node_colors = originalNodeColors
|
||||
}
|
||||
})
|
||||
|
||||
it('applies preset colors to selected nodes and groups in legacy mode', () => {
|
||||
const graph = {
|
||||
beforeChange: vi.fn(),
|
||||
afterChange: vi.fn()
|
||||
}
|
||||
|
||||
const node = Object.assign(Object.create(LGraphNode.prototype), {
|
||||
graph,
|
||||
color: undefined,
|
||||
bgcolor: undefined
|
||||
}) as LGraphNode
|
||||
const group = Object.assign(Object.create(LGraphGroup.prototype), {
|
||||
graph,
|
||||
color: undefined
|
||||
}) as LGraphGroup
|
||||
|
||||
const canvas = {
|
||||
selectedItems: new Set([node, group]),
|
||||
setDirty: vi.fn()
|
||||
}
|
||||
LGraphCanvas.active_canvas = canvas as unknown as LGraphCanvas
|
||||
|
||||
let callback: ((value: { value?: unknown }) => void) | undefined
|
||||
const originalContextMenu = LiteGraph.ContextMenu
|
||||
class MockContextMenu {
|
||||
constructor(
|
||||
_values: ReadonlyArray<{ content?: string } | string | null>,
|
||||
options: { callback?: (value: { value?: unknown }) => void }
|
||||
) {
|
||||
callback = options.callback
|
||||
}
|
||||
}
|
||||
LiteGraph.ContextMenu =
|
||||
MockContextMenu as unknown as typeof LiteGraph.ContextMenu
|
||||
|
||||
try {
|
||||
LGraphCanvas.onMenuNodeColors(
|
||||
{ content: 'Colors', value: null },
|
||||
{} as never,
|
||||
new MouseEvent('contextmenu'),
|
||||
{} as ContextMenu<string | null>,
|
||||
node
|
||||
)
|
||||
|
||||
callback?.({
|
||||
value: 'red'
|
||||
})
|
||||
|
||||
expect(node.bgcolor).toBe(LGraphCanvas.node_colors.red.bgcolor)
|
||||
expect(group.color).toBe(LGraphCanvas.node_colors.red.groupcolor)
|
||||
expect(graph.beforeChange).toHaveBeenCalled()
|
||||
expect(graph.afterChange).toHaveBeenCalled()
|
||||
expect(canvas.setDirty).toHaveBeenCalledWith(true, true)
|
||||
} finally {
|
||||
LiteGraph.ContextMenu = originalContextMenu
|
||||
}
|
||||
})
|
||||
|
||||
it('does not fan out legacy preset actions to an unrelated single selection', () => {
|
||||
const graph = {
|
||||
beforeChange: vi.fn(),
|
||||
afterChange: vi.fn()
|
||||
}
|
||||
|
||||
const selectedNode = Object.assign(Object.create(LGraphNode.prototype), {
|
||||
graph,
|
||||
color: undefined,
|
||||
bgcolor: undefined
|
||||
}) as LGraphNode
|
||||
const targetNode = Object.assign(Object.create(LGraphNode.prototype), {
|
||||
graph,
|
||||
color: undefined,
|
||||
bgcolor: undefined
|
||||
}) as LGraphNode
|
||||
|
||||
const canvas = {
|
||||
selectedItems: new Set([selectedNode]),
|
||||
setDirty: vi.fn()
|
||||
}
|
||||
LGraphCanvas.active_canvas = canvas as unknown as LGraphCanvas
|
||||
|
||||
let callback: ((value: { value?: unknown }) => void) | undefined
|
||||
const originalContextMenu = LiteGraph.ContextMenu
|
||||
class MockContextMenu {
|
||||
constructor(
|
||||
_values: ReadonlyArray<{ content?: string } | string | null>,
|
||||
options: { callback?: (value: { value?: unknown }) => void }
|
||||
) {
|
||||
callback = options.callback
|
||||
}
|
||||
}
|
||||
LiteGraph.ContextMenu =
|
||||
MockContextMenu as unknown as typeof LiteGraph.ContextMenu
|
||||
|
||||
try {
|
||||
LGraphCanvas.onMenuNodeColors(
|
||||
{ content: 'Colors', value: null },
|
||||
{} as never,
|
||||
new MouseEvent('contextmenu'),
|
||||
{} as ContextMenu<string | null>,
|
||||
targetNode
|
||||
)
|
||||
|
||||
callback?.({
|
||||
value: 'red'
|
||||
})
|
||||
|
||||
expect(targetNode.bgcolor).toBe(LGraphCanvas.node_colors.red.bgcolor)
|
||||
expect(selectedNode.bgcolor).toBeUndefined()
|
||||
} finally {
|
||||
LiteGraph.ContextMenu = originalContextMenu
|
||||
}
|
||||
})
|
||||
|
||||
it('keeps legacy group color actions scoped to the clicked group', () => {
|
||||
const graph = {
|
||||
beforeChange: vi.fn(),
|
||||
afterChange: vi.fn()
|
||||
}
|
||||
|
||||
const selectedNode = Object.assign(Object.create(LGraphNode.prototype), {
|
||||
graph,
|
||||
color: undefined,
|
||||
bgcolor: undefined
|
||||
}) as LGraphNode
|
||||
const targetGroup = Object.assign(Object.create(LGraphGroup.prototype), {
|
||||
graph,
|
||||
color: undefined
|
||||
}) as LGraphGroup
|
||||
|
||||
const canvas = {
|
||||
selectedItems: new Set([selectedNode, targetGroup]),
|
||||
setDirty: vi.fn()
|
||||
}
|
||||
LGraphCanvas.active_canvas = canvas as unknown as LGraphCanvas
|
||||
|
||||
let callback: ((value: { value?: unknown }) => void) | undefined
|
||||
const originalContextMenu = LiteGraph.ContextMenu
|
||||
class MockContextMenu {
|
||||
constructor(
|
||||
_values: ReadonlyArray<{ content?: string } | string | null>,
|
||||
options: { callback?: (value: { value?: unknown }) => void }
|
||||
) {
|
||||
callback = options.callback
|
||||
}
|
||||
}
|
||||
LiteGraph.ContextMenu =
|
||||
MockContextMenu as unknown as typeof LiteGraph.ContextMenu
|
||||
|
||||
try {
|
||||
LGraphCanvas.onMenuNodeColors(
|
||||
{ content: 'Colors', value: null },
|
||||
{} as never,
|
||||
new MouseEvent('contextmenu'),
|
||||
{} as ContextMenu<string | null>,
|
||||
targetGroup
|
||||
)
|
||||
|
||||
callback?.({
|
||||
value: 'red'
|
||||
})
|
||||
|
||||
expect(targetGroup.color).toBe(LGraphCanvas.node_colors.red.groupcolor)
|
||||
expect(selectedNode.bgcolor).toBeUndefined()
|
||||
} finally {
|
||||
LiteGraph.ContextMenu = originalContextMenu
|
||||
}
|
||||
})
|
||||
|
||||
it('balances graph change lifecycle if applying a legacy preset throws', () => {
|
||||
const graph = {
|
||||
beforeChange: vi.fn(),
|
||||
afterChange: vi.fn()
|
||||
}
|
||||
|
||||
const node = Object.assign(Object.create(LGraphNode.prototype), {
|
||||
graph,
|
||||
setColorOption: vi.fn(() => {
|
||||
throw new Error('boom')
|
||||
})
|
||||
}) as LGraphNode
|
||||
|
||||
const canvas = {
|
||||
selectedItems: new Set([node]),
|
||||
setDirty: vi.fn()
|
||||
}
|
||||
LGraphCanvas.active_canvas = canvas as unknown as LGraphCanvas
|
||||
|
||||
let callback:
|
||||
| ((value: string | { value?: unknown } | null) => void)
|
||||
| undefined
|
||||
const originalContextMenu = LiteGraph.ContextMenu
|
||||
const consoleErrorSpy = vi
|
||||
.spyOn(console, 'error')
|
||||
.mockImplementation(() => undefined)
|
||||
class MockContextMenu {
|
||||
constructor(
|
||||
_values: ReadonlyArray<{ content?: string } | string | null>,
|
||||
options: {
|
||||
callback?: (value: string | { value?: unknown } | null) => void
|
||||
}
|
||||
) {
|
||||
callback = options.callback
|
||||
}
|
||||
}
|
||||
LiteGraph.ContextMenu =
|
||||
MockContextMenu as unknown as typeof LiteGraph.ContextMenu
|
||||
|
||||
try {
|
||||
LGraphCanvas.onMenuNodeColors(
|
||||
{ content: 'Colors', value: null },
|
||||
{} as never,
|
||||
new MouseEvent('contextmenu'),
|
||||
{} as ContextMenu<string | null>,
|
||||
node
|
||||
)
|
||||
|
||||
expect(() => callback?.('red')).not.toThrow()
|
||||
expect(graph.beforeChange).toHaveBeenCalledOnce()
|
||||
expect(graph.afterChange).toHaveBeenCalledOnce()
|
||||
expect(consoleErrorSpy).toHaveBeenCalled()
|
||||
} finally {
|
||||
LiteGraph.ContextMenu = originalContextMenu
|
||||
consoleErrorSpy.mockRestore()
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -2,7 +2,6 @@ import { toString } from 'es-toolkit/compat'
|
||||
import { toValue } from 'vue'
|
||||
|
||||
import { PREFIX, SEPARATOR } from '@/constants/groupNodeConstants'
|
||||
import { st } from '@/i18n'
|
||||
import { MovingInputLink } from '@/lib/litegraph/src/canvas/MovingInputLink'
|
||||
import { LitegraphLinkAdapter } from '@/renderer/core/canvas/litegraph/litegraphLinkAdapter'
|
||||
import type { LinkRenderContext } from '@/renderer/core/canvas/litegraph/litegraphLinkAdapter'
|
||||
@@ -157,52 +156,6 @@ interface ICreateDefaultNodeOptions extends ICreateNodeOptions {
|
||||
posSizeFix?: Point
|
||||
}
|
||||
|
||||
type LegacyColorTarget = (LGraphNode | LGraphGroup) & IColorable & Positionable
|
||||
|
||||
function isLegacyColorTarget(item: unknown): item is LegacyColorTarget {
|
||||
return item instanceof LGraphNode || item instanceof LGraphGroup
|
||||
}
|
||||
|
||||
function getLegacyColorTargets(target: LegacyColorTarget): LegacyColorTarget[] {
|
||||
if (target instanceof LGraphGroup) {
|
||||
return [target]
|
||||
}
|
||||
|
||||
const selected = Array.from(LGraphCanvas.active_canvas.selectedItems).filter(
|
||||
isLegacyColorTarget
|
||||
)
|
||||
|
||||
return selected.length > 1 && selected.includes(target) ? selected : [target]
|
||||
}
|
||||
|
||||
function createLegacyColorMenuContent(label: string, color?: string): string {
|
||||
const escapedLabel = label
|
||||
.replaceAll('&', '&')
|
||||
.replaceAll('<', '<')
|
||||
.replaceAll('>', '>')
|
||||
.replaceAll('"', '"')
|
||||
.replaceAll("'", ''')
|
||||
const safeColor = getSafeLegacyMenuColor(color)
|
||||
|
||||
if (!safeColor) {
|
||||
return `<span style='display: block; padding-left: 4px;'>${escapedLabel}</span>`
|
||||
}
|
||||
|
||||
return (
|
||||
`<span style='display: block; color: #fff; padding-left: 4px;` +
|
||||
` border-left: 8px solid ${safeColor}; background-color:${safeColor}'>${escapedLabel}</span>`
|
||||
)
|
||||
}
|
||||
|
||||
function getSafeLegacyMenuColor(color?: string): string | undefined {
|
||||
if (!color) return undefined
|
||||
|
||||
const trimmed = color.trim()
|
||||
return /^#(?:[\da-fA-F]{3,4}|[\da-fA-F]{6}|[\da-fA-F]{8})$/.test(trimmed)
|
||||
? trimmed
|
||||
: undefined
|
||||
}
|
||||
|
||||
interface HasShowSearchCallback {
|
||||
/** See {@link LGraphCanvas.showSearchBox} */
|
||||
showSearchBox: (
|
||||
@@ -1696,70 +1649,61 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
|
||||
/** @param value Parameter is never used */
|
||||
static onMenuNodeColors(
|
||||
_value: IContextMenuValue<string | null>,
|
||||
value: IContextMenuValue<string | null>,
|
||||
_options: IContextMenuOptions,
|
||||
e: MouseEvent,
|
||||
menu: ContextMenu<string | null>,
|
||||
node: LGraphNode | LGraphGroup
|
||||
node: LGraphNode
|
||||
): boolean {
|
||||
if (!node || !isLegacyColorTarget(node)) throw 'no node for color'
|
||||
const values: (IContextMenuValue<string | null> | null)[] = [
|
||||
{
|
||||
value: null,
|
||||
content: createLegacyColorMenuContent(st('color.noColor', 'No color'))
|
||||
}
|
||||
]
|
||||
if (!node) throw 'no node for color'
|
||||
|
||||
for (const [presetName, colorOption] of Object.entries(
|
||||
LGraphCanvas.node_colors
|
||||
)) {
|
||||
values.push({
|
||||
value: presetName,
|
||||
content: createLegacyColorMenuContent(
|
||||
st(`color.${presetName}`, presetName),
|
||||
node instanceof LGraphGroup
|
||||
? (colorOption.groupcolor ?? colorOption.bgcolor)
|
||||
: colorOption.bgcolor
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
new LiteGraph.ContextMenu<string | null>(values, {
|
||||
event: e,
|
||||
callback: (value) => {
|
||||
try {
|
||||
innerClicked(value)
|
||||
} catch (error) {
|
||||
console.error('Failed to apply legacy node color selection.', error)
|
||||
}
|
||||
},
|
||||
parentMenu: menu,
|
||||
...(node instanceof LGraphNode ? { node } : {})
|
||||
const values: IContextMenuValue<
|
||||
string | null,
|
||||
unknown,
|
||||
{ value: string | null }
|
||||
>[] = []
|
||||
values.push({
|
||||
value: null,
|
||||
content:
|
||||
"<span style='display: block; padding-left: 4px;'>No color</span>"
|
||||
})
|
||||
|
||||
function innerClicked(
|
||||
value: string | IContextMenuValue<string | null> | null | undefined
|
||||
) {
|
||||
if (!node || !isLegacyColorTarget(node)) return
|
||||
const presetName =
|
||||
value == null ? null : typeof value === 'string' ? value : value.value
|
||||
for (const i in LGraphCanvas.node_colors) {
|
||||
const color = LGraphCanvas.node_colors[i]
|
||||
value = {
|
||||
value: i,
|
||||
content:
|
||||
`<span style='display: block; color: #999; padding-left: 4px;` +
|
||||
` border-left: 8px solid ${color.color}; background-color:${color.bgcolor}'>${i}</span>`
|
||||
}
|
||||
values.push(value)
|
||||
}
|
||||
new LiteGraph.ContextMenu<string | null>(values, {
|
||||
event: e,
|
||||
callback: inner_clicked,
|
||||
parentMenu: menu,
|
||||
node
|
||||
})
|
||||
|
||||
const canvas = LGraphCanvas.active_canvas
|
||||
const targets = getLegacyColorTargets(node)
|
||||
const graphInfo = node instanceof LGraphNode ? node : undefined
|
||||
function inner_clicked(v: IContextMenuValue<string>) {
|
||||
if (!node) return
|
||||
|
||||
node.graph?.beforeChange(graphInfo)
|
||||
try {
|
||||
const colorOption = presetName
|
||||
? LGraphCanvas.node_colors[presetName]
|
||||
: null
|
||||
for (const target of targets) {
|
||||
target.setColorOption(colorOption)
|
||||
}
|
||||
} finally {
|
||||
node.graph?.afterChange(graphInfo)
|
||||
const fApplyColor = function (item: IColorable) {
|
||||
const colorOption = v.value ? LGraphCanvas.node_colors[v.value] : null
|
||||
item.setColorOption(colorOption)
|
||||
}
|
||||
|
||||
const canvas = LGraphCanvas.active_canvas
|
||||
if (
|
||||
!canvas.selected_nodes ||
|
||||
Object.keys(canvas.selected_nodes).length <= 1
|
||||
) {
|
||||
fApplyColor(node)
|
||||
} else {
|
||||
for (const i in canvas.selected_nodes) {
|
||||
fApplyColor(canvas.selected_nodes[i])
|
||||
}
|
||||
}
|
||||
canvas.setDirty(true, true)
|
||||
}
|
||||
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -400,7 +400,7 @@
|
||||
"name": "تخطيط شريط التبويبات",
|
||||
"options": {
|
||||
"Default": "افتراضي",
|
||||
"Legacy": "تقليدي"
|
||||
"Integrated": "مُدمج"
|
||||
},
|
||||
"tooltip": "يتحكم في تخطيط شريط التبويبات. \"مُدمج\" ينقل عناصر المساعدة والتحكمات الخاصة بالمستخدم إلى منطقة شريط التبويبات."
|
||||
},
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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."
|
||||
},
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -400,7 +400,7 @@
|
||||
"name": "چیدمان نوار تب",
|
||||
"options": {
|
||||
"Default": "پیشفرض",
|
||||
"Legacy": "قدیمی"
|
||||
"Integrated": "یکپارچه"
|
||||
},
|
||||
"tooltip": "چیدمان نوار تب را کنترل میکند. «یکپارچه» کنترلهای راهنما و کاربر را به ناحیه نوار تب منتقل میکند."
|
||||
},
|
||||
|
||||
@@ -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 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",
|
||||
"Legacy": "Héritage"
|
||||
"Integrated": "Intégrée"
|
||||
},
|
||||
"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,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": {
|
||||
|
||||
@@ -400,7 +400,7 @@
|
||||
"name": "タブバーのレイアウト",
|
||||
"options": {
|
||||
"Default": "デフォルト",
|
||||
"Legacy": "レガシー"
|
||||
"Integrated": "統合"
|
||||
},
|
||||
"tooltip": "タブバーのレイアウトを制御します。「統合」を選択すると、ヘルプとユーザーコントロールがタブバーエリアに移動します。"
|
||||
},
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -400,7 +400,7 @@
|
||||
"name": "탭 바 레이아웃",
|
||||
"options": {
|
||||
"Default": "기본값",
|
||||
"Legacy": "레거시"
|
||||
"Integrated": "통합"
|
||||
},
|
||||
"tooltip": "탭 바의 레이아웃을 제어합니다. \"통합\"을 선택하면 도움말과 사용자 컨트롤이 탭 바 영역으로 이동합니다."
|
||||
},
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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."
|
||||
},
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -400,7 +400,7 @@
|
||||
"name": "Макет панели вкладок",
|
||||
"options": {
|
||||
"Default": "По умолчанию",
|
||||
"Legacy": "Классический"
|
||||
"Integrated": "Интегрированный"
|
||||
},
|
||||
"tooltip": "Управляет расположением панели вкладок. «Интегрированный» перемещает элементы управления Справкой и Пользователем в область панели вкладок."
|
||||
},
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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."
|
||||
},
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -400,7 +400,7 @@
|
||||
"name": "分頁列佈局",
|
||||
"options": {
|
||||
"Default": "預設",
|
||||
"Legacy": "傳統"
|
||||
"Integrated": "整合"
|
||||
},
|
||||
"tooltip": "控制分頁列的佈局。「整合」會將說明和使用者控制項移至分頁列區域。"
|
||||
},
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -400,7 +400,7 @@
|
||||
"name": "标签栏布局",
|
||||
"options": {
|
||||
"Default": "默认",
|
||||
"Legacy": "传统"
|
||||
"Integrated": "集成"
|
||||
},
|
||||
"tooltip": "控制标签栏的布局。“集成”会将帮助和用户控件移动到标签栏区域。"
|
||||
},
|
||||
|
||||
@@ -186,7 +186,7 @@ const tooltipDelay = computed<number>(() =>
|
||||
|
||||
const { isLoading, error } = useImage({
|
||||
src: asset.preview_url ?? '',
|
||||
alt: asset.display_name || asset.name
|
||||
alt: asset.name
|
||||
})
|
||||
|
||||
function handleSelect() {
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
:aria-label="
|
||||
asset
|
||||
? $t('assetBrowser.ariaLabel.assetCard', {
|
||||
name: asset.display_name || asset.name,
|
||||
name: asset.name,
|
||||
type: fileKind
|
||||
})
|
||||
: $t('assetBrowser.ariaLabel.loadingAsset')
|
||||
@@ -225,7 +225,7 @@ const canInspect = computed(() => isPreviewableMediaType(fileKind.value))
|
||||
|
||||
// Get filename without extension
|
||||
const fileName = computed(() => {
|
||||
return getFilenameDetails(asset?.display_name || asset?.name || '').filename
|
||||
return getFilenameDetails(asset?.name || '').filename
|
||||
})
|
||||
|
||||
// Adapt AssetItem to legacy AssetMeta format for existing components
|
||||
@@ -234,7 +234,6 @@ const adaptedAsset = computed(() => {
|
||||
return {
|
||||
id: asset.id,
|
||||
name: asset.name,
|
||||
display_name: asset.display_name,
|
||||
kind: fileKind.value,
|
||||
src: asset.preview_url || '',
|
||||
size: asset.size,
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<img
|
||||
v-if="!error"
|
||||
:src="asset.src"
|
||||
:alt="asset.display_name || asset.name"
|
||||
:alt="asset.name"
|
||||
class="size-full object-contain transition-transform duration-300 group-hover:scale-105 group-data-[selected=true]:scale-105"
|
||||
/>
|
||||
<div
|
||||
@@ -34,7 +34,7 @@ const emit = defineEmits<{
|
||||
|
||||
const { state, error, isReady } = useImage({
|
||||
src: asset.src ?? '',
|
||||
alt: asset.display_name || asset.name
|
||||
alt: asset.name
|
||||
})
|
||||
|
||||
whenever(
|
||||
|
||||
@@ -39,7 +39,6 @@ export function mapTaskOutputToAssetItem(
|
||||
return {
|
||||
id: taskItem.jobId,
|
||||
name: output.filename,
|
||||
display_name: output.display_name,
|
||||
size: 0,
|
||||
created_at: taskItem.executionStartTimestamp
|
||||
? new Date(taskItem.executionStartTimestamp).toISOString()
|
||||
|
||||
@@ -68,7 +68,7 @@ export function useMediaAssetActions() {
|
||||
if (!targetAsset) return
|
||||
|
||||
try {
|
||||
const filename = targetAsset.display_name || targetAsset.name
|
||||
const filename = targetAsset.name
|
||||
// Prefer preview_url (already includes subfolder) with getAssetUrl as fallback
|
||||
const downloadUrl = targetAsset.preview_url || getAssetUrl(targetAsset)
|
||||
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -109,7 +110,7 @@ export function useMediaAssetActions() {
|
||||
|
||||
try {
|
||||
assets.forEach((asset) => {
|
||||
const filename = asset.display_name || asset.name
|
||||
const filename = asset.name
|
||||
const downloadUrl = asset.preview_url || getAssetUrl(asset)
|
||||
downloadFile(downloadUrl, filename)
|
||||
})
|
||||
@@ -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
|
||||
|
||||
@@ -9,7 +9,6 @@ const zAsset = z.object({
|
||||
mime_type: z.string().nullish(),
|
||||
tags: z.array(z.string()).optional().default([]),
|
||||
preview_id: z.string().nullable().optional(),
|
||||
display_name: z.string().optional(),
|
||||
preview_url: z.string().optional(),
|
||||
created_at: z.string().optional(),
|
||||
updated_at: z.string().optional(),
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -20,7 +20,6 @@ type OutputOverrides = Partial<{
|
||||
subfolder: string
|
||||
nodeId: string
|
||||
url: string
|
||||
display_name: string
|
||||
}>
|
||||
|
||||
function createOutput(overrides: OutputOverrides = {}): ResultItemImpl {
|
||||
@@ -33,8 +32,7 @@ function createOutput(overrides: OutputOverrides = {}): ResultItemImpl {
|
||||
}
|
||||
return {
|
||||
...merged,
|
||||
previewUrl: merged.url,
|
||||
display_name: merged.display_name
|
||||
previewUrl: merged.url
|
||||
} as ResultItemImpl
|
||||
}
|
||||
|
||||
@@ -127,48 +125,6 @@ describe('resolveOutputAssetItems', () => {
|
||||
])
|
||||
})
|
||||
|
||||
it('propagates display_name from output to asset item', async () => {
|
||||
const output = createOutput({
|
||||
filename: 'abc123hash.png',
|
||||
nodeId: '1',
|
||||
url: 'https://example.com/abc123hash.png',
|
||||
display_name: 'ComfyUI_00001_.png'
|
||||
})
|
||||
const metadata: OutputAssetMetadata = {
|
||||
jobId: 'job-dn',
|
||||
nodeId: '1',
|
||||
subfolder: 'sub',
|
||||
outputCount: 1,
|
||||
allOutputs: [output]
|
||||
}
|
||||
|
||||
const results = await resolveOutputAssetItems(metadata)
|
||||
|
||||
expect(results).toHaveLength(1)
|
||||
expect(results[0].name).toBe('abc123hash.png')
|
||||
expect(results[0].display_name).toBe('ComfyUI_00001_.png')
|
||||
})
|
||||
|
||||
it('omits display_name when not present in output', async () => {
|
||||
const output = createOutput({
|
||||
filename: 'file.png',
|
||||
nodeId: '1',
|
||||
url: 'https://example.com/file.png'
|
||||
})
|
||||
const metadata: OutputAssetMetadata = {
|
||||
jobId: 'job-nodn',
|
||||
nodeId: '1',
|
||||
subfolder: 'sub',
|
||||
outputCount: 1,
|
||||
allOutputs: [output]
|
||||
}
|
||||
|
||||
const results = await resolveOutputAssetItems(metadata)
|
||||
|
||||
expect(results).toHaveLength(1)
|
||||
expect(results[0].display_name).toBeUndefined()
|
||||
})
|
||||
|
||||
it('keeps root outputs with empty subfolders', async () => {
|
||||
const output = createOutput({
|
||||
filename: 'root.png',
|
||||
|
||||
@@ -69,7 +69,6 @@ function mapOutputsToAssetItems({
|
||||
items.push({
|
||||
id: `${jobId}-${outputKey}`,
|
||||
name: output.filename,
|
||||
display_name: output.display_name,
|
||||
size: 0,
|
||||
created_at: createdAtValue,
|
||||
tags: ['output'],
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -23,8 +23,7 @@ const zPreviewOutput = z.object({
|
||||
subfolder: z.string(),
|
||||
type: resultItemType,
|
||||
nodeId: z.string(),
|
||||
mediaType: z.string(),
|
||||
display_name: z.string().optional()
|
||||
mediaType: z.string()
|
||||
})
|
||||
|
||||
/**
|
||||
|
||||
@@ -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
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -922,27 +922,6 @@ export const CORE_SETTINGS: SettingParams[] = [
|
||||
defaultValue: {} as ColorPalettes,
|
||||
versionModified: '1.6.7'
|
||||
},
|
||||
{
|
||||
id: 'Comfy.NodeColor.Favorites',
|
||||
name: 'Favorite node colors',
|
||||
type: 'hidden',
|
||||
defaultValue: [] as string[],
|
||||
versionAdded: '1.25.0'
|
||||
},
|
||||
{
|
||||
id: 'Comfy.NodeColor.Recents',
|
||||
name: 'Recent node colors',
|
||||
type: 'hidden',
|
||||
defaultValue: [] as string[],
|
||||
versionAdded: '1.25.0'
|
||||
},
|
||||
{
|
||||
id: 'Comfy.NodeColor.DarkerHeader',
|
||||
name: 'Use a darker node header for custom colors',
|
||||
type: 'hidden',
|
||||
defaultValue: true,
|
||||
versionAdded: '1.25.0'
|
||||
},
|
||||
{
|
||||
id: 'Comfy.WidgetControlMode',
|
||||
category: ['Comfy', 'Node Widget', 'WidgetControlMode'],
|
||||
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -484,7 +484,8 @@ describe('ShareWorkflowDialogContent', () => {
|
||||
expect(mockToast.add).toHaveBeenCalledWith({
|
||||
severity: 'error',
|
||||
summary: 'Error',
|
||||
detail: 'Publish failed'
|
||||
detail: 'Publish failed',
|
||||
life: 5000
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
|
||||
@@ -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
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -543,7 +543,8 @@ async function handleCopyInviteLink(invite: PendingInvite) {
|
||||
} catch {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: t('g.error')
|
||||
summary: t('g.error'),
|
||||
life: 3000
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ import {
|
||||
useInfiniteScroll,
|
||||
useResizeObserver
|
||||
} from '@vueuse/core'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import type { ComponentPublicInstance } from 'vue'
|
||||
import {
|
||||
computed,
|
||||
@@ -27,13 +26,11 @@ 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()
|
||||
@@ -159,10 +156,8 @@ watch(
|
||||
const inProgress = store.activeWorkflowInProgressItems
|
||||
if (inProgress.length > 0) {
|
||||
store.selectAsLatest(`slot:${inProgress[0].id}`)
|
||||
} else if (hasOutputs.value) {
|
||||
selectFirstHistory()
|
||||
} else {
|
||||
store.selectAsLatest(null)
|
||||
selectFirstHistory()
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
@@ -185,13 +180,13 @@ watch(
|
||||
: undefined
|
||||
|
||||
if (!sv || sv.kind !== 'history') {
|
||||
if (hasOutputs.value) selectFirstHistory()
|
||||
selectFirstHistory()
|
||||
return
|
||||
}
|
||||
|
||||
const wasFirst = sv.assetId === oldAssets[0]?.id
|
||||
if (wasFirst || !newAssets.some((a) => a.id === sv.assetId)) {
|
||||
if (hasOutputs.value) selectFirstHistory()
|
||||
selectFirstHistory()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
@@ -219,7 +219,6 @@ 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,
|
||||
@@ -256,7 +255,7 @@ describe(useOutputHistory, () => {
|
||||
expect(outputs[0].filename).toBe('b.png')
|
||||
})
|
||||
|
||||
it('returns empty when no output nodes are selected', () => {
|
||||
it('returns all outputs when no output nodes are selected', () => {
|
||||
const results = [makeResult('a.png', '1'), makeResult('b.png', '2')]
|
||||
const asset = makeAsset('a1', 'job-1', {
|
||||
allOutputs: results,
|
||||
@@ -266,7 +265,7 @@ describe(useOutputHistory, () => {
|
||||
const { allOutputs } = useOutputHistory()
|
||||
const outputs = allOutputs(asset)
|
||||
|
||||
expect(outputs).toHaveLength(0)
|
||||
expect(outputs).toHaveLength(2)
|
||||
})
|
||||
|
||||
it('returns consistent filtered outputs across repeated calls', () => {
|
||||
@@ -289,7 +288,6 @@ describe(useOutputHistory, () => {
|
||||
})
|
||||
|
||||
it('returns in-progress outputs for pending resolve jobs', () => {
|
||||
useAppModeStore().selectedOutputs.push('1')
|
||||
pendingResolveRef.value = new Set(['job-1'])
|
||||
inProgressItemsRef.value = [
|
||||
{
|
||||
@@ -316,7 +314,6 @@ describe(useOutputHistory, () => {
|
||||
})
|
||||
|
||||
it('fetches full job detail for multi-output jobs', async () => {
|
||||
useAppModeStore().selectedOutputs.push('1')
|
||||
jobDetailResults.set('job-1', {
|
||||
outputs: {
|
||||
'1': {
|
||||
@@ -345,7 +342,6 @@ 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,
|
||||
@@ -364,7 +360,6 @@ 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 []
|
||||
if (!nodeIds.length) return items
|
||||
return items.filter((r) =>
|
||||
nodeIds.some((id) => String(id) === String(r.nodeId))
|
||||
)
|
||||
|
||||
@@ -203,6 +203,7 @@ const handleDownload = () => {
|
||||
severity: 'error',
|
||||
summary: 'Error',
|
||||
detail: t('g.failedToDownloadVideo'),
|
||||
life: 3000,
|
||||
group: 'video-preview'
|
||||
})
|
||||
}
|
||||
|
||||
@@ -233,6 +233,7 @@ const handleDownload = () => {
|
||||
severity: 'error',
|
||||
summary: 'Error',
|
||||
detail: t('g.failedToDownloadImage'),
|
||||
life: 3000,
|
||||
group: 'image-preview'
|
||||
})
|
||||
}
|
||||
|
||||
@@ -208,7 +208,8 @@ const handleDownload = () => {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: t('g.error'),
|
||||
detail: t('g.failedToDownloadFile')
|
||||
detail: t('g.failedToDownloadFile'),
|
||||
life: 3000
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user