mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-07-15 11:44:10 +00:00
Compare commits
23 Commits
DynamicGro
...
whatdreams
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5b3b6aefc1 | ||
|
|
7fb280885e | ||
|
|
99835e765b | ||
|
|
d084a9c556 | ||
|
|
409ddc88fa | ||
|
|
dbbadbdc09 | ||
|
|
f1b24f308e | ||
|
|
25fb1725e6 | ||
|
|
9d72802750 | ||
|
|
a4a4d7e527 | ||
|
|
f9e0809ce7 | ||
|
|
7561d3f4bd | ||
|
|
b40b03c8ae | ||
|
|
b8fd3ec1dc | ||
|
|
93ec57e8d2 | ||
|
|
73e1fed080 | ||
|
|
711aad704a | ||
|
|
d10decbd96 | ||
|
|
cb8d3186f9 | ||
|
|
83a412112e | ||
|
|
ecb2a745a5 | ||
|
|
6262358361 | ||
|
|
0f04cec48c |
@@ -29,4 +29,27 @@
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@keyframes highlight-animation {
|
||||
0% {
|
||||
background-color: transparent;
|
||||
box-shadow: 0 0 0 6px transparent;
|
||||
border-radius: 4px;
|
||||
}
|
||||
6%,
|
||||
63% {
|
||||
background-color: color-mix(in srgb, var(--fg-color) 25%, transparent);
|
||||
box-shadow: 0 0 0 6px color-mix(in srgb, var(--fg-color) 25%, transparent);
|
||||
border-radius: 4px;
|
||||
}
|
||||
100% {
|
||||
background-color: transparent;
|
||||
box-shadow: 0 0 0 6px transparent;
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.animate-highlight {
|
||||
animation: highlight-animation 0.9s ease-out;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,13 +6,24 @@ import LayoutField from './LayoutField.vue'
|
||||
defineProps<{
|
||||
label: string
|
||||
tooltip?: string
|
||||
highlighted?: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'animationend'): void
|
||||
}>()
|
||||
|
||||
const modelValue = defineModel<boolean>({ default: false })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<LayoutField singleline :label :tooltip>
|
||||
<LayoutField
|
||||
singleline
|
||||
:label
|
||||
:tooltip
|
||||
:class="{ 'animate-highlight': highlighted }"
|
||||
@animationend="emit('animationend')"
|
||||
>
|
||||
<ToggleSwitch
|
||||
v-model="modelValue"
|
||||
class="transition-transform active:scale-90"
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
import { cleanup, render, fireEvent, screen } from '@testing-library/vue'
|
||||
import { describe, expect, it, vi, afterEach } from 'vitest'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
import PrimeVue from 'primevue/config'
|
||||
import { createTestingPinia } from '@pinia/testing'
|
||||
import TabGlobalSettings from '@/components/rightSidePanel/settings/TabGlobalSettings.vue'
|
||||
import { useRightSidePanelStore } from '@/stores/workspace/rightSidePanelStore'
|
||||
|
||||
const i18n = createI18n({
|
||||
legacy: false,
|
||||
locale: 'en',
|
||||
messages: {
|
||||
en: {
|
||||
rightSidePanel: {
|
||||
globalSettings: {
|
||||
nodes: 'Nodes',
|
||||
showAdvanced: 'Show Advanced Parameters'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const { mockSettings } = vi.hoisted(() => {
|
||||
const mockSettings: Record<string, unknown> = {
|
||||
'Comfy.Node.AlwaysShowAdvancedWidgets': false,
|
||||
'Comfy.Canvas.SelectionToolbox': false,
|
||||
'Comfy.VueNodes.Enabled': false,
|
||||
'Comfy.SnapToGrid.GridSize': 10,
|
||||
'pysssss.SnapToGrid': false,
|
||||
'Comfy.Graph.LinkMarkers': 'None',
|
||||
'Comfy.LinkRenderMode': 'Spline'
|
||||
}
|
||||
return { mockSettings }
|
||||
})
|
||||
|
||||
vi.mock('@/platform/settings/settingStore', () => ({
|
||||
useSettingStore: () => ({
|
||||
get: vi.fn((key: string) => mockSettings[key]),
|
||||
set: vi.fn((key: string, val: unknown) => {
|
||||
mockSettings[key] = val
|
||||
})
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('@/platform/settings/composables/useSettingsDialog', () => ({
|
||||
useSettingsDialog: () => ({
|
||||
show: vi.fn()
|
||||
})
|
||||
}))
|
||||
|
||||
describe('TabGlobalSettings', () => {
|
||||
afterEach(() => {
|
||||
cleanup()
|
||||
})
|
||||
|
||||
it('adds and removes highlight class when highlight gets triggered and animation ends', async () => {
|
||||
const pinia = createTestingPinia({ stubActions: false })
|
||||
render(TabGlobalSettings, {
|
||||
global: {
|
||||
plugins: [i18n, pinia, PrimeVue]
|
||||
}
|
||||
})
|
||||
|
||||
const rightSidePanelStore = useRightSidePanelStore()
|
||||
|
||||
const switchEl = screen.getByTestId('advanced-widgets-switch')
|
||||
expect(switchEl).not.toHaveClass('animate-highlight')
|
||||
|
||||
rightSidePanelStore.triggerHighlight('Comfy.Node.AlwaysShowAdvancedWidgets')
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 0))
|
||||
|
||||
expect(switchEl).toHaveClass('animate-highlight')
|
||||
|
||||
await fireEvent.animationEnd(switchEl)
|
||||
|
||||
expect(switchEl).not.toHaveClass('animate-highlight')
|
||||
})
|
||||
})
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import InputNumber from 'primevue/inputnumber'
|
||||
import Select from 'primevue/select'
|
||||
import { computed } from 'vue'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
@@ -18,9 +18,29 @@ import PropertiesAccordionItem from '../layout/PropertiesAccordionItem.vue'
|
||||
import FieldSwitch from './FieldSwitch.vue'
|
||||
import LayoutField from './LayoutField.vue'
|
||||
|
||||
import { useRightSidePanelStore } from '@/stores/workspace/rightSidePanelStore'
|
||||
|
||||
const { t } = useI18n()
|
||||
const settingStore = useSettingStore()
|
||||
const settingsDialog = useSettingsDialog()
|
||||
const rightSidePanelStore = useRightSidePanelStore()
|
||||
|
||||
const isHighlightingAdvanced = ref(false)
|
||||
|
||||
watch(
|
||||
() => rightSidePanelStore.highlightGlobalSetting,
|
||||
(newVal) => {
|
||||
if (
|
||||
newVal &&
|
||||
rightSidePanelStore.consumeHighlight(
|
||||
'Comfy.Node.AlwaysShowAdvancedWidgets'
|
||||
)
|
||||
) {
|
||||
isHighlightingAdvanced.value = true
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
// NODES settings
|
||||
const showAdvancedParameters = computed({
|
||||
@@ -106,8 +126,11 @@ function openFullSettings() {
|
||||
<div class="space-y-4 px-4 py-3">
|
||||
<FieldSwitch
|
||||
v-model="showAdvancedParameters"
|
||||
data-testid="advanced-widgets-switch"
|
||||
:label="t('rightSidePanel.globalSettings.showAdvanced')"
|
||||
:tooltip="t('settings.Comfy_Node_AlwaysShowAdvancedWidgets.tooltip')"
|
||||
:highlighted="isHighlightingAdvanced"
|
||||
@animationend="isHighlightingAdvanced = false"
|
||||
/>
|
||||
<FieldSwitch
|
||||
v-model="showToolbox"
|
||||
|
||||
@@ -3816,6 +3816,7 @@
|
||||
"parameters": "Parameters",
|
||||
"nodes": "Nodes",
|
||||
"info": "Info",
|
||||
"advancedWidgetSettings": "Advanced widget settings",
|
||||
"infoFor": "Info for {item}",
|
||||
"color": "Node color",
|
||||
"pinned": "Pinned",
|
||||
|
||||
@@ -1,12 +1,50 @@
|
||||
import { render, screen } from '@testing-library/vue'
|
||||
import { cleanup, render, screen, within } from '@testing-library/vue'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import {
|
||||
beforeEach,
|
||||
describe,
|
||||
expect,
|
||||
it,
|
||||
vi,
|
||||
afterEach,
|
||||
afterAll
|
||||
} from 'vitest'
|
||||
import { ref } from 'vue'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
import PrimeVue from 'primevue/config'
|
||||
import { createTestingPinia } from '@pinia/testing'
|
||||
import { fromAny } from '@total-typescript/shoehorn'
|
||||
|
||||
import { RenderShape } from '@/lib/litegraph/src/litegraph'
|
||||
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||
import NodeFooter from '@/renderer/extensions/vueNodes/components/NodeFooter.vue'
|
||||
|
||||
vi.hoisted(() => {
|
||||
const localStorageMock = (() => {
|
||||
let store: Record<string, string> = {}
|
||||
return {
|
||||
getItem: (key: string) => store[key] || null,
|
||||
setItem: (key: string, value: string) => {
|
||||
store[key] = value.toString()
|
||||
},
|
||||
removeItem: (key: string) => {
|
||||
delete store[key]
|
||||
},
|
||||
clear: () => {
|
||||
store = {}
|
||||
}
|
||||
}
|
||||
})()
|
||||
vi.stubGlobal('localStorage', localStorageMock)
|
||||
})
|
||||
|
||||
const mockSettingsDialogShow = vi.fn()
|
||||
vi.mock('@/platform/settings/composables/useSettingsDialog', () => ({
|
||||
useSettingsDialog: () => ({
|
||||
show: mockSettingsDialogShow
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('@/renderer/core/layout/store/layoutStore', () => {
|
||||
const isDraggingVueNodes = ref(false)
|
||||
return { layoutStore: { isDraggingVueNodes } }
|
||||
@@ -51,24 +89,35 @@ const baseProps: Props = {
|
||||
}
|
||||
|
||||
function renderFooter(overrides: Partial<Props> = {}) {
|
||||
const pinia = createTestingPinia({ stubActions: false })
|
||||
return render(NodeFooter, {
|
||||
global: { plugins: [i18n] },
|
||||
global: { plugins: [i18n, pinia, PrimeVue] },
|
||||
props: { ...baseProps, ...overrides }
|
||||
})
|
||||
}
|
||||
|
||||
function allButtonClasses(): string {
|
||||
return screen
|
||||
.getAllByRole('button')
|
||||
function allButtonClasses(container: Element): string {
|
||||
return within(container as HTMLElement)
|
||||
.queryAllByRole('button')
|
||||
.map((b) => b.className)
|
||||
.join(' ')
|
||||
}
|
||||
|
||||
describe('NodeFooter', () => {
|
||||
afterEach(() => {
|
||||
cleanup()
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
vi.unstubAllGlobals()
|
||||
})
|
||||
|
||||
describe('rendering branches', () => {
|
||||
it('renders nothing when no relevant flags are set', () => {
|
||||
renderFooter()
|
||||
expect(screen.queryAllByRole('button')).toHaveLength(0)
|
||||
const { container } = renderFooter()
|
||||
expect(
|
||||
within(container as HTMLElement).queryAllByRole('button')
|
||||
).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('renders error + enter tabs for subgraph with error (Case 1)', () => {
|
||||
@@ -182,25 +231,34 @@ describe('NodeFooter', () => {
|
||||
|
||||
describe('shape-based radius classes (getBottomRadius)', () => {
|
||||
it('BOX shape renders no rounded-b* class on the single-tab footer', () => {
|
||||
renderFooter({ isSubgraph: true, shape: RenderShape.BOX })
|
||||
expect(allButtonClasses()).not.toMatch(/rounded-b/)
|
||||
const { container } = renderFooter({
|
||||
isSubgraph: true,
|
||||
shape: RenderShape.BOX
|
||||
})
|
||||
expect(allButtonClasses(container)).not.toMatch(/rounded-b/)
|
||||
})
|
||||
|
||||
it('CARD shape emits rounded-br variant on the single-tab footer', () => {
|
||||
renderFooter({ isSubgraph: true, shape: RenderShape.CARD })
|
||||
const classes = allButtonClasses()
|
||||
const { container } = renderFooter({
|
||||
isSubgraph: true,
|
||||
shape: RenderShape.CARD
|
||||
})
|
||||
const classes = allButtonClasses(container)
|
||||
expect(classes).toMatch(/rounded-br-\[17px\]/)
|
||||
expect(classes).not.toMatch(/rounded-b-\[/)
|
||||
})
|
||||
|
||||
it('default shape emits rounded-b variant on the single-tab footer', () => {
|
||||
renderFooter({ isSubgraph: true })
|
||||
expect(allButtonClasses()).toMatch(/rounded-b-\[17px\]/)
|
||||
const { container } = renderFooter({ isSubgraph: true })
|
||||
expect(allButtonClasses(container)).toMatch(/rounded-b-\[17px\]/)
|
||||
})
|
||||
|
||||
it('upgrades to 20px radius when the error tab is present', () => {
|
||||
renderFooter({ hasAnyError: true, showErrorsTabEnabled: true })
|
||||
expect(allButtonClasses()).toMatch(/rounded-b-\[20px\]/)
|
||||
const { container } = renderFooter({
|
||||
hasAnyError: true,
|
||||
showErrorsTabEnabled: true
|
||||
})
|
||||
expect(allButtonClasses(container)).toMatch(/rounded-b-\[20px\]/)
|
||||
})
|
||||
|
||||
it('enter tab uses right-only rounding in dual-tab mode (Case 1)', () => {
|
||||
@@ -212,6 +270,53 @@ describe('NodeFooter', () => {
|
||||
const enterBtn = screen.getByTestId('subgraph-enter-button')
|
||||
expect(enterBtn.className).toMatch(/rounded-br-\[20px\]/)
|
||||
})
|
||||
|
||||
it('error tab uses both-corner rounding in dual-tab mode (Case 1)', () => {
|
||||
renderFooter({
|
||||
isSubgraph: true,
|
||||
hasAnyError: true,
|
||||
showErrorsTabEnabled: true
|
||||
})
|
||||
const errorBtn = screen.getByRole('button', { name: /error/i })
|
||||
expect(errorBtn.className).toMatch(/rounded-b-\[20px\]/)
|
||||
})
|
||||
|
||||
it('error tab uses rounded-br rounding for CARD shape in dual-tab mode', () => {
|
||||
renderFooter({
|
||||
isSubgraph: true,
|
||||
hasAnyError: true,
|
||||
showErrorsTabEnabled: true,
|
||||
shape: RenderShape.CARD
|
||||
})
|
||||
const errorBtn = screen.getByRole('button', { name: /error/i })
|
||||
expect(errorBtn.className).toMatch(/rounded-br-\[20px\]/)
|
||||
expect(errorBtn.className).not.toMatch(/rounded-bl-\[/)
|
||||
expect(errorBtn.className).not.toMatch(/rounded-b-\[/)
|
||||
})
|
||||
|
||||
it('enter tab uses rounded-br rounding for CARD shape in dual-tab mode', () => {
|
||||
renderFooter({
|
||||
isSubgraph: true,
|
||||
hasAnyError: true,
|
||||
showErrorsTabEnabled: true,
|
||||
shape: RenderShape.CARD
|
||||
})
|
||||
const enterBtn = screen.getByTestId('subgraph-enter-button')
|
||||
expect(enterBtn.className).toMatch(/rounded-br-\[20px\]/)
|
||||
})
|
||||
|
||||
it('BOX shape renders no rounded corners in dual-tab mode', () => {
|
||||
renderFooter({
|
||||
isSubgraph: true,
|
||||
hasAnyError: true,
|
||||
showErrorsTabEnabled: true,
|
||||
shape: RenderShape.BOX
|
||||
})
|
||||
const errorBtn = screen.getByRole('button', { name: /error/i })
|
||||
const enterBtn = screen.getByTestId('subgraph-enter-button')
|
||||
expect(errorBtn.className).not.toMatch(/rounded-b/)
|
||||
expect(enterBtn.className).not.toMatch(/rounded-b/)
|
||||
})
|
||||
})
|
||||
|
||||
describe('headerColor style', () => {
|
||||
@@ -231,4 +336,75 @@ describe('NodeFooter', () => {
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('advanced settings button', () => {
|
||||
let user: ReturnType<typeof userEvent.setup>
|
||||
|
||||
beforeEach(() => {
|
||||
user = userEvent.setup()
|
||||
vi.resetAllMocks()
|
||||
})
|
||||
|
||||
it('renders the gear settings button next to advanced inputs button when expanded', () => {
|
||||
renderFooter({ showAdvancedInputsButton: true, showAdvancedState: true })
|
||||
expect(screen.getByTestId('advanced-settings-button')).toBeTruthy()
|
||||
})
|
||||
|
||||
it('opens settings dialog when clicked in legacy menu mode', async () => {
|
||||
renderFooter({ showAdvancedInputsButton: true, showAdvancedState: true })
|
||||
const settingStore = (
|
||||
await import('@/platform/settings/settingStore')
|
||||
).useSettingStore()
|
||||
vi.mocked(settingStore.get).mockImplementation((key) => {
|
||||
if (key === 'Comfy.UseNewMenu') return 'Disabled'
|
||||
return false
|
||||
})
|
||||
|
||||
const settingsBtn = screen.getByTestId('advanced-settings-button')
|
||||
await user.click(settingsBtn)
|
||||
|
||||
expect(mockSettingsDialogShow).toHaveBeenCalledWith(
|
||||
undefined,
|
||||
'Comfy.Node.AlwaysShowAdvancedWidgets'
|
||||
)
|
||||
})
|
||||
|
||||
it('opens right side settings panel when clicked in new menu mode', async () => {
|
||||
const deselectAllSpy = vi.fn()
|
||||
|
||||
renderFooter({
|
||||
showAdvancedInputsButton: true,
|
||||
showAdvancedState: true
|
||||
})
|
||||
|
||||
const canvasStore = useCanvasStore()
|
||||
const originalCanvas = canvasStore.canvas
|
||||
canvasStore.canvas = fromAny({
|
||||
deselectAll: deselectAllSpy
|
||||
})
|
||||
|
||||
try {
|
||||
const settingStore = (
|
||||
await import('@/platform/settings/settingStore')
|
||||
).useSettingStore()
|
||||
vi.mocked(settingStore.get).mockImplementation((key) => {
|
||||
if (key === 'Comfy.UseNewMenu') return 'Top'
|
||||
return false
|
||||
})
|
||||
|
||||
const rightSidePanelStore = (
|
||||
await import('@/stores/workspace/rightSidePanelStore')
|
||||
).useRightSidePanelStore()
|
||||
const openPanelSpy = vi.spyOn(rightSidePanelStore, 'openPanel')
|
||||
|
||||
const settingsBtn = screen.getByTestId('advanced-settings-button')
|
||||
await user.click(settingsBtn)
|
||||
|
||||
expect(deselectAllSpy).toHaveBeenCalledOnce()
|
||||
expect(openPanelSpy).toHaveBeenCalledWith('settings')
|
||||
} finally {
|
||||
canvasStore.canvas = originalCanvas
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
:class="
|
||||
cn(
|
||||
tabStyles,
|
||||
'-ml-5 box-border w-[calc(50%+20px)] rounded-none bg-node-component-header-surface pt-9 pb-4 pl-5',
|
||||
'-ml-5 box-border w-[calc(50%+20px)] rounded-none bg-node-component-header-surface pt-9 pr-0 pb-4 pl-5',
|
||||
enterRadiusClass
|
||||
)
|
||||
"
|
||||
@@ -72,12 +72,17 @@
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
as="div"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
variant="textonly"
|
||||
data-testid="advanced-inputs-button"
|
||||
:class="
|
||||
cn(
|
||||
tabStyles,
|
||||
'-ml-5 box-border w-[calc(50%+20px)] rounded-none bg-node-component-header-surface pt-9 pb-4 pl-5',
|
||||
'relative z-0 -ml-5 box-border w-[calc(50%+20px)] rounded-none bg-node-component-header-surface pt-9 pb-4 pl-5',
|
||||
showAdvancedState ? 'pr-0' : 'pr-4',
|
||||
'has-[.node-footer-settings-btn:hover]:hover:bg-node-component-header-surface',
|
||||
enterRadiusClass
|
||||
)
|
||||
"
|
||||
@@ -85,20 +90,31 @@
|
||||
@pointerup="snapshotDragOnPointerUp"
|
||||
@click.stop="emitIfNotDragged('toggleAdvanced')"
|
||||
>
|
||||
<div class="flex size-full items-center justify-center gap-2">
|
||||
<span class="truncate">{{
|
||||
<div
|
||||
:class="
|
||||
cn(
|
||||
'flex size-full min-w-0 items-center justify-center gap-2',
|
||||
showAdvancedState ? 'pr-10' : 'pr-0'
|
||||
)
|
||||
"
|
||||
>
|
||||
<span class="min-w-0 truncate">{{
|
||||
showAdvancedState
|
||||
? t('rightSidePanel.hideAdvancedShort')
|
||||
: t('rightSidePanel.showAdvancedShort')
|
||||
}}</span>
|
||||
<i
|
||||
:class="
|
||||
showAdvancedState
|
||||
? 'icon-[lucide--chevron-up] size-4 shrink-0'
|
||||
: 'icon-[lucide--settings-2] size-4 shrink-0'
|
||||
"
|
||||
v-if="!showAdvancedState"
|
||||
class="icon-[lucide--settings-2] size-4 shrink-0"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<NodeFooterAdvancedSettingsButton
|
||||
v-if="showAdvancedState"
|
||||
class="border-interface-stroke-muted/20 absolute inset-y-0 right-0 flex h-full w-10 shrink-0 items-center justify-center rounded-none border-l bg-node-component-header-surface pt-9 pb-4"
|
||||
:class="cn(tabStyles, enterRadiusClass)"
|
||||
:style="headerColorStyle"
|
||||
/>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -164,7 +180,8 @@
|
||||
:class="
|
||||
cn(
|
||||
footerWrapperBase,
|
||||
hasAnyError ? '-mx-1 -mb-2 w-[calc(100%+8px)] pb-1' : 'w-full'
|
||||
hasAnyError ? '-mx-1 -mb-2 w-[calc(100%+8px)] pb-1' : 'w-full',
|
||||
'relative flex items-center'
|
||||
)
|
||||
"
|
||||
>
|
||||
@@ -174,7 +191,7 @@
|
||||
:class="
|
||||
cn(
|
||||
tabStyles,
|
||||
'box-border w-full rounded-none bg-node-component-header-surface',
|
||||
'box-border w-full rounded-none bg-node-component-header-surface px-0',
|
||||
hasAnyError ? 'pt-9 pb-4' : 'pt-8 pb-4',
|
||||
footerRadiusClass
|
||||
)
|
||||
@@ -183,21 +200,40 @@
|
||||
@pointerup="snapshotDragOnPointerUp"
|
||||
@click.stop="emitIfNotDragged('toggleAdvanced')"
|
||||
>
|
||||
<div class="flex size-full items-center justify-center gap-2">
|
||||
<div
|
||||
:class="
|
||||
cn(
|
||||
'flex size-full min-w-0 items-center justify-center gap-2',
|
||||
showAdvancedState ? 'px-10' : 'px-4'
|
||||
)
|
||||
"
|
||||
>
|
||||
<template v-if="showAdvancedState">
|
||||
<span class="truncate">{{
|
||||
<span class="min-w-0 truncate">{{
|
||||
t('rightSidePanel.hideAdvancedInputsButton')
|
||||
}}</span>
|
||||
<i class="icon-[lucide--chevron-up] size-4 shrink-0" />
|
||||
</template>
|
||||
<template v-else>
|
||||
<span class="truncate">{{
|
||||
<span class="min-w-0 truncate">{{
|
||||
t('rightSidePanel.showAdvancedInputsButton')
|
||||
}}</span>
|
||||
<i class="icon-[lucide--settings-2] size-4 shrink-0" />
|
||||
</template>
|
||||
</div>
|
||||
</Button>
|
||||
|
||||
<NodeFooterAdvancedSettingsButton
|
||||
v-if="showAdvancedState"
|
||||
class="border-interface-stroke-muted/20 absolute inset-y-0 right-0 flex h-full w-10 shrink-0 items-center justify-center rounded-none border-l bg-node-component-header-surface"
|
||||
:class="
|
||||
cn(
|
||||
tabStyles,
|
||||
footerRadiusRightClass,
|
||||
hasAnyError ? 'pt-9 pb-4' : 'pt-8 pb-4'
|
||||
)
|
||||
"
|
||||
:style="headerColorStyle"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -205,6 +241,7 @@
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import NodeFooterAdvancedSettingsButton from './NodeFooterAdvancedSettingsButton.vue'
|
||||
import { RenderShape } from '@/lib/litegraph/src/litegraph'
|
||||
import { layoutStore } from '@/renderer/core/layout/store/layoutStore'
|
||||
import { cn } from '@comfyorg/tailwind-utils'
|
||||
@@ -258,19 +295,30 @@ const RADIUS_CLASS = {
|
||||
'rounded-b-17': 'rounded-b-[17px]',
|
||||
'rounded-b-20': 'rounded-b-[20px]',
|
||||
'rounded-br-17': 'rounded-br-[17px]',
|
||||
'rounded-br-20': 'rounded-br-[20px]'
|
||||
'rounded-br-20': 'rounded-br-[20px]',
|
||||
'rounded-bl-17': 'rounded-bl-[17px]',
|
||||
'rounded-bl-20': 'rounded-bl-[20px]'
|
||||
} as const
|
||||
|
||||
const SHAPE_CORNER_PREFIX_MAP: Record<
|
||||
string | number,
|
||||
Record<'both' | 'right' | 'left', string>
|
||||
> = {
|
||||
[RenderShape.BOX]: { both: '', right: '', left: '' },
|
||||
[RenderShape.CARD]: { both: 'rounded-br', right: 'rounded-br', left: '' },
|
||||
default: { both: 'rounded-b', right: 'rounded-br', left: 'rounded-bl' }
|
||||
}
|
||||
|
||||
function getBottomRadius(
|
||||
nodeShape: RenderShape | undefined,
|
||||
size: '17px' | '20px',
|
||||
corners: 'both' | 'right' = 'both'
|
||||
corners: 'both' | 'right' | 'left' = 'both'
|
||||
): string {
|
||||
if (nodeShape === RenderShape.BOX) return ''
|
||||
const prefix =
|
||||
nodeShape === RenderShape.CARD || corners === 'right'
|
||||
? 'rounded-br'
|
||||
: 'rounded-b'
|
||||
const map =
|
||||
SHAPE_CORNER_PREFIX_MAP[nodeShape ?? 'default'] ??
|
||||
SHAPE_CORNER_PREFIX_MAP.default
|
||||
const prefix = map[corners]
|
||||
if (!prefix) return ''
|
||||
const key =
|
||||
`${prefix}-${size === '17px' ? '17' : '20'}` as keyof typeof RADIUS_CLASS
|
||||
return RADIUS_CLASS[key]
|
||||
@@ -280,6 +328,10 @@ const footerRadiusClass = computed(() =>
|
||||
getBottomRadius(shape, hasAnyError ? '20px' : '17px')
|
||||
)
|
||||
|
||||
const footerRadiusRightClass = computed(() =>
|
||||
getBottomRadius(shape, hasAnyError ? '20px' : '17px', 'right')
|
||||
)
|
||||
|
||||
const errorRadiusClass = computed(() => getBottomRadius(shape, '20px'))
|
||||
|
||||
const enterRadiusClass = computed(() => getBottomRadius(shape, '20px', 'right'))
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import { useTooltipConfig } from '@/renderer/extensions/vueNodes/composables/useNodeTooltips'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { useSettingsDialog } from '@/platform/settings/composables/useSettingsDialog'
|
||||
import { useRightSidePanelStore } from '@/stores/workspace/rightSidePanelStore'
|
||||
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||
|
||||
const { t } = useI18n()
|
||||
const settingStore = useSettingStore()
|
||||
const settingsDialog = useSettingsDialog()
|
||||
const rightSidePanelStore = useRightSidePanelStore()
|
||||
const canvasStore = useCanvasStore()
|
||||
const { createTooltipConfig } = useTooltipConfig()
|
||||
|
||||
const tooltipConfig = computed(() =>
|
||||
createTooltipConfig(t('rightSidePanel.advancedWidgetSettings'))
|
||||
)
|
||||
|
||||
function openSettings() {
|
||||
const isLegacyMenu = settingStore.get('Comfy.UseNewMenu') === 'Disabled'
|
||||
if (isLegacyMenu) {
|
||||
settingsDialog.show(undefined, 'Comfy.Node.AlwaysShowAdvancedWidgets')
|
||||
} else {
|
||||
if (canvasStore.canvas) {
|
||||
canvasStore.canvas.deselectAll()
|
||||
}
|
||||
rightSidePanelStore.openPanel('settings')
|
||||
rightSidePanelStore.triggerHighlight('Comfy.Node.AlwaysShowAdvancedWidgets')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Button
|
||||
v-tooltip.bottom="tooltipConfig"
|
||||
variant="textonly"
|
||||
class="node-footer-settings-btn group"
|
||||
data-testid="advanced-settings-button"
|
||||
:aria-label="t('rightSidePanel.advancedWidgetSettings')"
|
||||
@pointerdown.stop
|
||||
@pointerup.stop
|
||||
@mousedown.stop
|
||||
@mouseup.stop
|
||||
@click.stop="openSettings"
|
||||
>
|
||||
<i
|
||||
class="group-hover:text-foreground icon-[lucide--settings] size-4 text-muted-foreground"
|
||||
/>
|
||||
</Button>
|
||||
</template>
|
||||
@@ -11,6 +11,10 @@ import { useNodeDefStore } from '@/stores/nodeDefStore'
|
||||
|
||||
import { useNodeTooltips } from './useNodeTooltips'
|
||||
|
||||
vi.mock('@/scripts/app', () => ({
|
||||
app: {}
|
||||
}))
|
||||
|
||||
const jsonTooltip =
|
||||
'Positive point prompts as JSON [{"x": int, "y": int}, ...] (pixel coords)'
|
||||
|
||||
|
||||
@@ -82,22 +82,63 @@ function setupGlobalTooltipHiding() {
|
||||
globalTooltipState.listenersSetup = true
|
||||
}
|
||||
|
||||
/**
|
||||
* Composable for creating the themed tooltip configuration object.
|
||||
*/
|
||||
export function useTooltipConfig() {
|
||||
const settingStore = useSettingStore()
|
||||
|
||||
const tooltipsEnabled = computed(() =>
|
||||
settingStore.get('Comfy.EnableTooltips')
|
||||
)
|
||||
|
||||
const createTooltipConfig = (text: string): TooltipOptions => {
|
||||
const tooltipDelay = settingStore.get('LiteGraph.Node.TooltipDelay')
|
||||
const tooltipText = text || ''
|
||||
|
||||
return {
|
||||
value: tooltipText,
|
||||
showDelay: tooltipDelay as number,
|
||||
hideDelay: 0, // Immediate hiding
|
||||
disabled:
|
||||
!tooltipsEnabled.value ||
|
||||
!tooltipText ||
|
||||
tooltipsTemporarilyDisabled.value, // this reactive value works but only on next mount,
|
||||
// so if the tooltip is already visible changing this will not hide it
|
||||
pt: {
|
||||
text: {
|
||||
class:
|
||||
'border-node-component-tooltip-border bg-node-component-tooltip-surface border rounded-md px-4 py-2 text-node-component-tooltip text-sm font-normal leading-tight max-w-75 shadow-none'
|
||||
},
|
||||
arrow: ({ context }: TooltipPassThroughMethodOptions) => ({
|
||||
class: cn(
|
||||
context.top && 'border-t-node-component-tooltip-border',
|
||||
context.bottom && 'border-b-node-component-tooltip-border',
|
||||
context.left && 'border-l-node-component-tooltip-border',
|
||||
context.right && 'border-r-node-component-tooltip-border'
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
tooltipsEnabled,
|
||||
createTooltipConfig
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Composable for managing Vue node tooltips
|
||||
* Provides tooltip text for node headers, slots, and widgets
|
||||
*/
|
||||
export function useNodeTooltips(nodeType: MaybeRef<string>) {
|
||||
const nodeDefStore = useNodeDefStore()
|
||||
const settingsStore = useSettingStore()
|
||||
const { tooltipsEnabled, createTooltipConfig } = useTooltipConfig()
|
||||
|
||||
// Setup global pointerdown listener once
|
||||
setupGlobalTooltipHiding()
|
||||
|
||||
// Check if tooltips are globally enabled
|
||||
const tooltipsEnabled = computed(() =>
|
||||
settingsStore.get('Comfy.EnableTooltips')
|
||||
)
|
||||
|
||||
// Get node definition for tooltip data
|
||||
const nodeDef = computed(() => nodeDefStore.nodeDefsByName[unref(nodeType)])
|
||||
|
||||
@@ -149,40 +190,6 @@ export function useNodeTooltips(nodeType: MaybeRef<string>) {
|
||||
return stRaw(key, inputTooltip)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create tooltip configuration object for v-tooltip directive
|
||||
* Components wrap this in computed() for reactivity
|
||||
*/
|
||||
const createTooltipConfig = (text: string): TooltipOptions => {
|
||||
const tooltipDelay = settingsStore.get('LiteGraph.Node.TooltipDelay')
|
||||
const tooltipText = text || ''
|
||||
|
||||
return {
|
||||
value: tooltipText,
|
||||
showDelay: tooltipDelay as number,
|
||||
hideDelay: 0, // Immediate hiding
|
||||
disabled:
|
||||
!tooltipsEnabled.value ||
|
||||
!tooltipText ||
|
||||
tooltipsTemporarilyDisabled.value, // this reactive value works but only on next mount,
|
||||
// so if the tooltip is already visible changing this will not hide it
|
||||
pt: {
|
||||
text: {
|
||||
class:
|
||||
'border-node-component-tooltip-border bg-node-component-tooltip-surface border rounded-md px-4 py-2 text-node-component-tooltip text-sm font-normal leading-tight max-w-75 shadow-none'
|
||||
},
|
||||
arrow: ({ context }: TooltipPassThroughMethodOptions) => ({
|
||||
class: cn(
|
||||
context.top && 'border-t-node-component-tooltip-border',
|
||||
context.bottom && 'border-b-node-component-tooltip-border',
|
||||
context.left && 'border-l-node-component-tooltip-border',
|
||||
context.right && 'border-r-node-component-tooltip-border'
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
tooltipsEnabled,
|
||||
getNodeDescription,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { computed, readonly, ref, watch } from 'vue'
|
||||
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
|
||||
@@ -81,6 +81,20 @@ export const useRightSidePanelStore = defineStore('rightSidePanel', () => {
|
||||
focusedSection.value = null
|
||||
}
|
||||
|
||||
const highlightGlobalSetting = ref<string | null>(null)
|
||||
|
||||
function triggerHighlight(settingName: string) {
|
||||
highlightGlobalSetting.value = settingName
|
||||
}
|
||||
|
||||
function consumeHighlight(settingName: string): boolean {
|
||||
if (highlightGlobalSetting.value === settingName) {
|
||||
highlightGlobalSetting.value = null
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
return {
|
||||
isOpen,
|
||||
activeTab,
|
||||
@@ -88,10 +102,13 @@ export const useRightSidePanelStore = defineStore('rightSidePanel', () => {
|
||||
focusedSection,
|
||||
focusedErrorNodeId,
|
||||
searchQuery,
|
||||
highlightGlobalSetting: readonly(highlightGlobalSetting),
|
||||
openPanel,
|
||||
closePanel,
|
||||
togglePanel,
|
||||
focusSection,
|
||||
clearFocusedSection
|
||||
clearFocusedSection,
|
||||
triggerHighlight,
|
||||
consumeHighlight
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user