mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-20 06:20:11 +00:00
- remove root item removal functionality
- wrap essentials/extensions
This commit is contained in:
@@ -130,8 +130,7 @@ describe('NodeSearchCategorySidebar', () => {
|
||||
useNodeDefStore().updateNodeDefs([
|
||||
createMockNodeDef({ name: 'Node1', category: 'sampling' }),
|
||||
createMockNodeDef({ name: 'Node2', category: 'sampling/advanced' }),
|
||||
createMockNodeDef({ name: 'Node3', category: 'sampling/basic' }),
|
||||
createMockNodeDef({ name: 'Node4', category: 'other' })
|
||||
createMockNodeDef({ name: 'Node3', category: 'sampling/basic' })
|
||||
])
|
||||
await nextTick()
|
||||
|
||||
@@ -170,8 +169,7 @@ describe('NodeSearchCategorySidebar', () => {
|
||||
it('should emit update:selectedCategory when subcategory is clicked', async () => {
|
||||
useNodeDefStore().updateNodeDefs([
|
||||
createMockNodeDef({ name: 'Node1', category: 'sampling' }),
|
||||
createMockNodeDef({ name: 'Node2', category: 'sampling/advanced' }),
|
||||
createMockNodeDef({ name: 'Node3', category: 'other' })
|
||||
createMockNodeDef({ name: 'Node2', category: 'sampling/advanced' })
|
||||
])
|
||||
await nextTick()
|
||||
|
||||
@@ -207,8 +205,7 @@ describe('NodeSearchCategorySidebar', () => {
|
||||
it('should emit selected subcategory when expanded', async () => {
|
||||
useNodeDefStore().updateNodeDefs([
|
||||
createMockNodeDef({ name: 'Node1', category: 'sampling' }),
|
||||
createMockNodeDef({ name: 'Node2', category: 'sampling/advanced' }),
|
||||
createMockNodeDef({ name: 'Node3', category: 'other' })
|
||||
createMockNodeDef({ name: 'Node2', category: 'sampling/advanced' })
|
||||
])
|
||||
await nextTick()
|
||||
|
||||
@@ -227,8 +224,7 @@ describe('NodeSearchCategorySidebar', () => {
|
||||
useNodeDefStore().updateNodeDefs([
|
||||
createMockNodeDef({ name: 'Node1', category: 'api' }),
|
||||
createMockNodeDef({ name: 'Node2', category: 'api/image' }),
|
||||
createMockNodeDef({ name: 'Node3', category: 'api/image/BFL' }),
|
||||
createMockNodeDef({ name: 'Node4', category: 'other' })
|
||||
createMockNodeDef({ name: 'Node3', category: 'api/image/BFL' })
|
||||
])
|
||||
await nextTick()
|
||||
|
||||
@@ -257,6 +253,88 @@ describe('NodeSearchCategorySidebar', () => {
|
||||
expect(emitted[emitted.length - 1]).toEqual(['api/image/BFL'])
|
||||
})
|
||||
|
||||
describe('rootLabel wrapping', () => {
|
||||
it('should wrap multiple roots under a parent when rootLabel is provided', async () => {
|
||||
useNodeDefStore().updateNodeDefs([
|
||||
createMockNodeDef({ name: 'Node1', category: 'sampling' }),
|
||||
createMockNodeDef({ name: 'Node2', category: 'loaders' })
|
||||
])
|
||||
await nextTick()
|
||||
|
||||
const wrapper = await createWrapper({
|
||||
hidePresets: true,
|
||||
rootLabel: 'Extensions',
|
||||
rootKey: 'custom'
|
||||
})
|
||||
|
||||
expect(wrapper.text()).toContain('Extensions')
|
||||
})
|
||||
|
||||
it('should auto-expand the synthetic root and keep it expanded when selecting a child', async () => {
|
||||
useNodeDefStore().updateNodeDefs([
|
||||
createMockNodeDef({ name: 'Node1', category: 'sampling' }),
|
||||
createMockNodeDef({ name: 'Node2', category: 'loaders' })
|
||||
])
|
||||
await nextTick()
|
||||
|
||||
const wrapper = await createWrapper({
|
||||
hidePresets: true,
|
||||
rootLabel: 'Extensions',
|
||||
rootKey: 'custom'
|
||||
})
|
||||
|
||||
// Auto-expanded: children should be visible
|
||||
expect(wrapper.text()).toContain('sampling')
|
||||
expect(wrapper.text()).toContain('loaders')
|
||||
|
||||
// Select a child category
|
||||
await clickCategory(wrapper, 'sampling')
|
||||
await nextTick()
|
||||
|
||||
// Parent should stay expanded (children still visible)
|
||||
expect(wrapper.text()).toContain('Extensions')
|
||||
expect(wrapper.text()).toContain('sampling')
|
||||
expect(wrapper.text()).toContain('loaders')
|
||||
})
|
||||
|
||||
it('should prefix child keys with rootKey', async () => {
|
||||
useNodeDefStore().updateNodeDefs([
|
||||
createMockNodeDef({ name: 'Node1', category: 'sampling' }),
|
||||
createMockNodeDef({ name: 'Node2', category: 'loaders' })
|
||||
])
|
||||
await nextTick()
|
||||
|
||||
const wrapper = await createWrapper({
|
||||
hidePresets: true,
|
||||
rootLabel: 'Extensions',
|
||||
rootKey: 'custom'
|
||||
})
|
||||
|
||||
await clickCategory(wrapper, 'sampling')
|
||||
|
||||
const emitted = wrapper.emitted('update:selectedCategory')!
|
||||
expect(emitted[emitted.length - 1]).toEqual(['custom/sampling'])
|
||||
})
|
||||
|
||||
it('should not wrap when there is only one root category', async () => {
|
||||
useNodeDefStore().updateNodeDefs([
|
||||
createMockNodeDef({ name: 'Node1', category: 'sampling' }),
|
||||
createMockNodeDef({ name: 'Node2', category: 'sampling/advanced' })
|
||||
])
|
||||
await nextTick()
|
||||
|
||||
const wrapper = await createWrapper({
|
||||
hidePresets: true,
|
||||
rootLabel: 'Extensions',
|
||||
rootKey: 'custom'
|
||||
})
|
||||
|
||||
// No wrapping — "Extensions" parent should not appear
|
||||
expect(wrapper.text()).not.toContain('Extensions')
|
||||
expect(wrapper.text()).toContain('sampling')
|
||||
})
|
||||
})
|
||||
|
||||
it('should emit category without root/ prefix', async () => {
|
||||
useNodeDefStore().updateNodeDefs([
|
||||
createMockNodeDef({ name: 'Node1', category: 'sampling' })
|
||||
|
||||
@@ -58,7 +58,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import NodeSearchCategoryTreeNode, {
|
||||
@@ -78,11 +78,15 @@ import { cn } from '@/utils/tailwindUtil'
|
||||
const {
|
||||
hideChevrons = false,
|
||||
hidePresets = false,
|
||||
nodeDefs
|
||||
nodeDefs,
|
||||
rootLabel,
|
||||
rootKey
|
||||
} = defineProps<{
|
||||
hideChevrons?: boolean
|
||||
hidePresets?: boolean
|
||||
nodeDefs?: ComfyNodeDefImpl[]
|
||||
rootLabel?: string
|
||||
rootKey?: string
|
||||
}>()
|
||||
|
||||
const selectedCategory = defineModel<string>('selectedCategory', {
|
||||
@@ -138,18 +142,40 @@ const categoryTree = computed<CategoryNode[]>(() => {
|
||||
}
|
||||
}
|
||||
|
||||
let nodes = (tree.children ?? [])
|
||||
const nodes = (tree.children ?? [])
|
||||
.filter((node): node is TreeNode => !node.leaf)
|
||||
.map(mapNode)
|
||||
|
||||
// Skip single root node if it has children
|
||||
if (nodes.length === 1 && nodes[0].children?.length) {
|
||||
nodes = nodes[0].children
|
||||
if (rootLabel && nodes.length > 1) {
|
||||
const key = rootKey ?? rootLabel.toLowerCase()
|
||||
function prefixKeys(node: CategoryNode): CategoryNode {
|
||||
return {
|
||||
key: key + '/' + node.key,
|
||||
label: node.label,
|
||||
...(node.children?.length
|
||||
? { children: node.children.map(prefixKeys) }
|
||||
: {})
|
||||
}
|
||||
}
|
||||
return [{ key, label: rootLabel, children: nodes.map(prefixKeys) }]
|
||||
}
|
||||
|
||||
return nodes
|
||||
})
|
||||
|
||||
const selectedCollapsed = ref(false)
|
||||
|
||||
watch(
|
||||
categoryTree,
|
||||
(nodes) => {
|
||||
if (rootLabel && nodes.length === 1) {
|
||||
selectedCategory.value = nodes[0].key
|
||||
selectedCollapsed.value = false
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
function categoryBtnClass(id: string) {
|
||||
return cn(
|
||||
'cursor-pointer border-none bg-transparent rounded py-2.5 pr-3 text-left text-sm transition-colors',
|
||||
@@ -160,8 +186,6 @@ function categoryBtnClass(id: string) {
|
||||
)
|
||||
}
|
||||
|
||||
const selectedCollapsed = ref(false)
|
||||
|
||||
function selectCategory(categoryId: string) {
|
||||
if (selectedCategory.value === categoryId) {
|
||||
selectedCollapsed.value = !selectedCollapsed.value
|
||||
|
||||
@@ -36,6 +36,8 @@
|
||||
:hide-chevrons="!anyTreeCategoryHasChildren"
|
||||
:hide-presets="rootFilter !== null"
|
||||
:node-defs="rootFilteredNodeDefs"
|
||||
:root-label="rootFilterLabel"
|
||||
:root-key="rootFilter ?? undefined"
|
||||
/>
|
||||
|
||||
<!-- Results list -->
|
||||
@@ -81,6 +83,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, onMounted, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import NodeSearchFilterBar from '@/components/searchbox/v2/NodeSearchFilterBar.vue'
|
||||
import NodeSearchCategorySidebar from '@/components/searchbox/v2/NodeSearchCategorySidebar.vue'
|
||||
@@ -106,6 +109,7 @@ const emit = defineEmits<{
|
||||
|
||||
const BLUEPRINT_CATEGORY = 'Subgraph Blueprints'
|
||||
|
||||
const { t } = useI18n()
|
||||
const nodeDefStore = useNodeDefStore()
|
||||
const nodeFrequencyStore = useNodeFrequencyStore()
|
||||
const nodeBookmarkStore = useNodeBookmarkStore()
|
||||
@@ -138,6 +142,17 @@ const selectedIndex = ref(0)
|
||||
// Root filter from filter bar category buttons (radio toggle)
|
||||
const rootFilter = ref<string | null>(null)
|
||||
|
||||
const rootFilterLabel = computed(() => {
|
||||
switch (rootFilter.value) {
|
||||
case 'essentials':
|
||||
return t('g.essentials')
|
||||
case 'custom':
|
||||
return t('g.extensions')
|
||||
default:
|
||||
return undefined
|
||||
}
|
||||
})
|
||||
|
||||
const rootFilteredNodeDefs = computed(() => {
|
||||
if (!rootFilter.value) return nodeDefStore.visibleNodeDefs
|
||||
const allNodes = nodeDefStore.visibleNodeDefs
|
||||
@@ -245,13 +260,18 @@ const displayedResults = computed<ComfyNodeDefImpl[]>(() => {
|
||||
case 'custom':
|
||||
results = baseNodes.filter(isCustomNode)
|
||||
break
|
||||
default:
|
||||
default: {
|
||||
const prefix = rootFilter.value ? rootFilter.value + '/' : ''
|
||||
const categoryPath = effectiveCategory.value.startsWith(prefix)
|
||||
? effectiveCategory.value.slice(prefix.length)
|
||||
: effectiveCategory.value
|
||||
results = baseNodes.filter(
|
||||
(n) =>
|
||||
n.category === effectiveCategory.value ||
|
||||
n.category.startsWith(effectiveCategory.value + '/')
|
||||
n.category === categoryPath ||
|
||||
n.category.startsWith(categoryPath + '/')
|
||||
)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return filters.length > 0 ? results.filter(matchesFilters) : results
|
||||
|
||||
Reference in New Issue
Block a user