fix: update for reviews

This commit is contained in:
Yourz
2026-02-09 22:26:49 +08:00
parent 193d4827af
commit 99d31a7376
11 changed files with 163 additions and 157 deletions

View File

@@ -33,7 +33,9 @@ describe('BadgePill', () => {
const wrapper = mount(BadgePill, {
props: { text: 'Default' }
})
expect(wrapper.attributes('style')).toContain('border-color: #525252')
expect(wrapper.attributes('style')).toContain(
'border-color: var(--border-color)'
)
})
it('applies solid border color when borderStyle is a color', () => {
@@ -64,18 +66,18 @@ describe('BadgePill', () => {
expect(style).toContain('color: #f59e0b')
})
it('has white text when not filled', () => {
it('has foreground text when not filled', () => {
const wrapper = mount(BadgePill, {
props: { text: 'Not Filled', borderStyle: '#f59e0b' }
})
expect(wrapper.classes()).toContain('text-white')
expect(wrapper.classes()).toContain('text-foreground')
})
it('does not have white text class when filled', () => {
it('does not have foreground text class when filled', () => {
const wrapper = mount(BadgePill, {
props: { text: 'Filled', borderStyle: '#f59e0b', filled: true }
})
expect(wrapper.classes()).not.toContain('text-white')
expect(wrapper.classes()).not.toContain('text-foreground')
})
it('renders slot content', () => {

View File

@@ -23,12 +23,12 @@ const { borderStyle, filled } = defineProps<{
}>()
const textColorClass = computed(() =>
borderStyle && filled ? '' : 'text-white'
borderStyle && filled ? '' : 'text-foreground'
)
const customStyle = computed(() => {
if (!borderStyle) {
return { borderColor: '#525252' }
return { borderColor: 'var(--border-color)' }
}
const isGradient = borderStyle.includes('linear-gradient')

View File

@@ -16,7 +16,6 @@
>
<TreeExplorerV2Node
:item="item as FlattenedItem<RenderedTreeExplorerNode>"
:show-context-menu="showContextMenu"
@node-click="
(node: RenderedTreeExplorerNode, e: MouseEvent) =>
emit('nodeClick', node, e)

View File

@@ -145,10 +145,9 @@ describe('TreeExplorerV2Node', () => {
).toBe(true)
})
it('does not render ContextMenuTrigger when showContextMenu is false', () => {
it('does not render ContextMenuTrigger for folder items', () => {
const { wrapper } = mountComponent({
item: createMockItem('node'),
showContextMenu: false
item: createMockItem('folder')
})
expect(

View File

@@ -6,14 +6,10 @@
as-child
>
<!-- Node with context menu -->
<ContextMenuTrigger
v-if="showContextMenu && item.value.type === 'node'"
as-child
>
<ContextMenuTrigger v-if="item.value.type === 'node'" as-child>
<div
class="group/tree-node flex w-full cursor-pointer select-none items-center gap-3 overflow-hidden py-2 outline-none hover:bg-highlight"
:class="{ 'bg-highlight': isSelected }"
:style="{ paddingLeft: `${16 + (item.level - 1) * 24}px` }"
:class="cn(ROW_CLASS, isSelected && 'bg-highlight')"
:style="rowStyle"
@click.stop="handleClick($event, handleToggle, handleSelect)"
@contextmenu="handleContextMenu"
@mouseenter="handleMouseEnter"
@@ -28,30 +24,11 @@
</div>
</ContextMenuTrigger>
<!-- Node without context menu -->
<div
v-else-if="item.value.type === 'node'"
class="group/tree-node flex w-full cursor-pointer select-none items-center gap-3 overflow-hidden py-2 outline-none hover:bg-highlight"
:class="{ 'bg-highlight': isSelected }"
:style="{ paddingLeft: `${16 + (item.level - 1) * 24}px` }"
@click.stop="handleClick($event, handleToggle, handleSelect)"
@mouseenter="handleMouseEnter"
@mouseleave="handleMouseLeave"
>
<i class="icon-[comfy--node] size-4 shrink-0 text-muted-foreground" />
<span class="min-w-0 flex-1 truncate text-sm text-foreground">
<slot name="node" :node="item.value">
{{ item.value.label }}
</slot>
</span>
</div>
<!-- Folder -->
<div
v-else
class="group/tree-node flex w-full cursor-pointer select-none items-center gap-3 overflow-hidden py-2 outline-none hover:bg-highlight"
:class="{ 'bg-highlight': isSelected }"
:style="{ paddingLeft: `${16 + (item.level - 1) * 24}px` }"
:class="cn(ROW_CLASS, isSelected && 'bg-highlight')"
:style="rowStyle"
@click.stop="handleClick($event, handleToggle, handleSelect)"
>
<i
@@ -96,9 +73,11 @@ import { InjectKeyContextMenuNode } from '@/types/treeExplorerTypes'
import type { RenderedTreeExplorerNode } from '@/types/treeExplorerTypes'
import { cn } from '@/utils/tailwindUtil'
const { item, showContextMenu = false } = defineProps<{
const ROW_CLASS =
'group/tree-node flex w-full cursor-pointer select-none items-center gap-3 overflow-hidden py-2 outline-none hover:bg-highlight'
const { item } = defineProps<{
item: FlattenedItem<RenderedTreeExplorerNode>
showContextMenu?: boolean
}>()
const emit = defineEmits<{
@@ -118,6 +97,10 @@ const nodePreviewStyle = ref<CSSProperties>({
zIndex: 1001
})
const rowStyle = computed(() => ({
paddingLeft: `${16 + (item.level - 1) * 24}px`
}))
const sidebarLocation = computed<'left' | 'right'>(() =>
settingStore.get('Comfy.Sidebar.Location')
)
@@ -151,26 +134,20 @@ function handleMouseEnter(e: MouseEvent) {
const viewportHeight = window.innerHeight
const viewportWidth = window.innerWidth
// Calculate horizontal position based on sidebar location
let left: number
if (sidebarLocation.value === 'left') {
left = rect.right + PREVIEW_MARGIN
// If preview would overflow right edge, flip to left side
if (left + PREVIEW_WIDTH > viewportWidth) {
left = rect.left - PREVIEW_MARGIN - PREVIEW_WIDTH
}
} else {
left = rect.left - PREVIEW_MARGIN - PREVIEW_WIDTH
// If preview would overflow left edge, flip to right side
if (left < 0) {
left = rect.right + PREVIEW_MARGIN
}
}
// Calculate mouse Y position (center of hovered item)
const mouseY = rect.top + rect.height / 2
// Initial top position - will be adjusted after render
let top = rect.top
nodePreviewStyle.value = {
@@ -182,17 +159,13 @@ function handleMouseEnter(e: MouseEvent) {
}
isHovered.value = true
// After render, adjust position to ensure mouse is within preview height
requestAnimationFrame(() => {
if (previewRef.value) {
const previewRect = previewRef.value.getBoundingClientRect()
const previewHeight = previewRect.height
// Ensure mouse Y is within preview's vertical range
// Position preview so mouse is roughly in the upper third
top = mouseY - previewHeight * 0.3
// Clamp to viewport bounds
const minTop = PREVIEW_MARGIN
const maxTop = viewportHeight - previewHeight - PREVIEW_MARGIN
top = Math.max(minTop, Math.min(top, maxTop))

View File

@@ -1,94 +1,130 @@
import { computed, ref } from 'vue'
import { describe, expect, it } from 'vitest'
import { mount } from '@vue/test-utils'
import { createTestingPinia } from '@pinia/testing'
import { TabsContent, TabsList, TabsRoot, TabsTrigger } from 'reka-ui'
import { ref } from 'vue'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { createI18n } from 'vue-i18n'
import type { TabId } from '@/types/nodeOrganizationTypes'
import NodeLibrarySidebarTabV2 from './NodeLibrarySidebarTabV2.vue'
describe('NodeLibrarySidebarTabV2 expandedKeys logic', () => {
describe('per-tab expandedKeys', () => {
function createExpandedKeysState(initialTab: TabId = 'essentials') {
const selectedTab = ref<TabId>(initialTab)
const expandedKeysByTab = ref<Record<TabId, string[]>>({
essentials: [],
all: [],
custom: []
})
const expandedKeys = computed({
get: () => expandedKeysByTab.value[selectedTab.value],
set: (value) => {
expandedKeysByTab.value[selectedTab.value] = value
vi.mock('@vueuse/core', async () => {
const actual = await vi.importActual('@vueuse/core')
return {
...actual,
useLocalStorage: vi.fn((_key: string, defaultValue: unknown) =>
ref(defaultValue)
)
}
})
vi.mock('@/composables/node/useNodeDragToCanvas', () => ({
useNodeDragToCanvas: () => ({
isDragging: { value: false },
draggedNode: { value: null },
cursorPosition: { value: { x: 0, y: 0 } },
startDrag: vi.fn(),
cancelDrag: vi.fn(),
setupGlobalListeners: vi.fn(),
cleanupGlobalListeners: vi.fn()
})
}))
vi.mock('@/services/nodeOrganizationService', () => ({
DEFAULT_TAB_ID: 'essentials',
nodeOrganizationService: {
organizeNodesByTab: vi.fn(() => [])
}
}))
vi.mock('./nodeLibrary/AllNodesPanel.vue', () => ({
default: {
name: 'AllNodesPanel',
template: '<div data-testid="all-panel"><slot /></div>',
props: ['sections', 'expandedKeys', 'fillNodeInfo']
}
}))
vi.mock('./nodeLibrary/CustomNodesPanel.vue', () => ({
default: {
name: 'CustomNodesPanel',
template: '<div data-testid="custom-panel"><slot /></div>',
props: ['sections', 'expandedKeys']
}
}))
vi.mock('./nodeLibrary/EssentialNodesPanel.vue', () => ({
default: {
name: 'EssentialNodesPanel',
template: '<div data-testid="essential-panel"><slot /></div>',
props: ['root', 'expandedKeys']
}
}))
vi.mock('./nodeLibrary/NodeDragPreview.vue', () => ({
default: {
name: 'NodeDragPreview',
template: '<div />'
}
}))
vi.mock('@/components/common/SearchBoxV2.vue', () => ({
default: {
name: 'SearchBox',
template: '<input data-testid="search-box" />',
props: ['modelValue', 'placeholder'],
setup() {
return { focus: vi.fn() }
},
expose: ['focus']
}
}))
const i18n = createI18n({
legacy: false,
locale: 'en',
messages: { en: {} }
})
describe('NodeLibrarySidebarTabV2', () => {
beforeEach(() => {
vi.clearAllMocks()
})
function mountComponent() {
return mount(NodeLibrarySidebarTabV2, {
global: {
plugins: [createTestingPinia({ stubActions: false }), i18n],
components: {
TabsRoot,
TabsList,
TabsTrigger,
TabsContent
},
stubs: {
teleport: true
}
})
return { selectedTab, expandedKeysByTab, expandedKeys }
}
it('should initialize with empty arrays for all tabs', () => {
const { expandedKeysByTab } = createExpandedKeysState()
expect(expandedKeysByTab.value.essentials).toEqual([])
expect(expandedKeysByTab.value.all).toEqual([])
expect(expandedKeysByTab.value.custom).toEqual([])
}
})
}
it('should return keys for the current tab', () => {
const { selectedTab, expandedKeysByTab, expandedKeys } =
createExpandedKeysState('essentials')
it('should render with tabs', () => {
const wrapper = mountComponent()
expandedKeysByTab.value.essentials = ['key1', 'key2']
expandedKeysByTab.value.all = ['key3']
const triggers = wrapper.findAllComponents(TabsTrigger)
expect(triggers.length).toBe(3)
})
expect(expandedKeys.value).toEqual(['key1', 'key2'])
it('should render search box', () => {
const wrapper = mountComponent()
selectedTab.value = 'all'
expect(expandedKeys.value).toEqual(['key3'])
})
expect(wrapper.find('[data-testid="search-box"]').exists()).toBe(true)
})
it('should set keys only for the current tab', () => {
const { expandedKeysByTab, expandedKeys } =
createExpandedKeysState('essentials')
it('should render all panel components', () => {
const wrapper = mountComponent()
expandedKeys.value = ['new-key1', 'new-key2']
expect(expandedKeysByTab.value.essentials).toEqual([
'new-key1',
'new-key2'
])
expect(expandedKeysByTab.value.all).toEqual([])
expect(expandedKeysByTab.value.custom).toEqual([])
})
it('should preserve keys when switching tabs', () => {
const { selectedTab, expandedKeysByTab, expandedKeys } =
createExpandedKeysState('essentials')
expandedKeys.value = ['essentials-key']
selectedTab.value = 'all'
expandedKeys.value = ['all-key']
selectedTab.value = 'custom'
expandedKeys.value = ['custom-key']
expect(expandedKeysByTab.value.essentials).toEqual(['essentials-key'])
expect(expandedKeysByTab.value.all).toEqual(['all-key'])
expect(expandedKeysByTab.value.custom).toEqual(['custom-key'])
selectedTab.value = 'essentials'
expect(expandedKeys.value).toEqual(['essentials-key'])
})
it('should not share keys between tabs', () => {
const { selectedTab, expandedKeys } =
createExpandedKeysState('essentials')
expandedKeys.value = ['shared-key']
selectedTab.value = 'all'
expect(expandedKeys.value).toEqual([])
selectedTab.value = 'custom'
expect(expandedKeys.value).toEqual([])
selectedTab.value = 'essentials'
expect(expandedKeys.value).toEqual(['shared-key'])
})
expect(wrapper.find('[data-testid="essential-panel"]').exists()).toBe(true)
expect(wrapper.find('[data-testid="all-panel"]').exists()).toBe(true)
expect(wrapper.find('[data-testid="custom-panel"]').exists()).toBe(true)
})
})

View File

@@ -126,13 +126,15 @@ const filteredNodeDefs = computed(() => {
)
})
const sections = computed(() => {
const nodes =
filteredNodeDefs.value.length > 0
? filteredNodeDefs.value
: nodeDefStore.visibleNodeDefs
const activeNodes = computed(() =>
filteredNodeDefs.value.length > 0
? filteredNodeDefs.value
: nodeDefStore.visibleNodeDefs
)
return nodeOrganizationService.organizeNodesByTab(nodes, 'all')
const sections = computed(() => {
if (selectedTab.value !== 'all') return []
return nodeOrganizationService.organizeNodesByTab(activeNodes.value, 'all')
})
function getFolderIcon(node: TreeNode): string {
@@ -183,11 +185,11 @@ const renderedSections = computed(() => {
})
const essentialSections = computed(() => {
const nodes =
filteredNodeDefs.value.length > 0
? filteredNodeDefs.value
: nodeDefStore.visibleNodeDefs
return nodeOrganizationService.organizeNodesByTab(nodes, 'essentials')
if (selectedTab.value !== 'essentials') return []
return nodeOrganizationService.organizeNodesByTab(
activeNodes.value,
'essentials'
)
})
const renderedEssentialRoot = computed(() => {
@@ -198,12 +200,8 @@ const renderedEssentialRoot = computed(() => {
})
const customSections = computed(() => {
const nodes =
filteredNodeDefs.value.length > 0
? filteredNodeDefs.value
: nodeDefStore.visibleNodeDefs
return nodeOrganizationService.organizeNodesByTab(nodes, 'custom')
if (selectedTab.value !== 'custom') return []
return nodeOrganizationService.organizeNodesByTab(activeNodes.value, 'custom')
})
const renderedCustomSections = computed(() => {

View File

@@ -19,9 +19,9 @@
<div v-for="(section, index) in sections" :key="section.title ?? index">
<h3
v-if="section.title"
class="px-4 py-2 text-xs font-medium uppercase tracking-wide text-muted-foreground"
class="px-4 py-2 text-xs font-medium tracking-wide text-muted-foreground"
>
{{ $t(section.title) }}
{{ section.title }}
</h3>
<TreeExplorerV2
v-model:expanded-keys="expandedKeys"

View File

@@ -4,9 +4,9 @@
<!-- Section header -->
<h3
v-if="section.title"
class="px-4 py-2 text-xs font-medium uppercase tracking-wide text-muted-foreground"
class="px-4 py-2 text-xs font-medium tracking-wide text-muted-foreground"
>
{{ $t(section.title) }}
{{ section.title }}
</h3>
<!-- Section tree -->
<TreeExplorerV2

View File

@@ -1,5 +1,5 @@
<template>
<TabsContent value="essential" class="min-h-0 flex-1 overflow-y-auto">
<TabsContent value="essentials" class="min-h-0 flex-1 overflow-y-auto">
<TreeExplorerV2
v-model:expanded-keys="expandedKeys"
:root="root"

View File

@@ -793,8 +793,7 @@
"alphabeticalDesc": "Sort alphabetically within groups"
},
"sections": {
"favorites": "Favorites",
"basics": "Basics"
"favorites": "Favorites"
}
},
"modelLibrary": "Model Library",