- remove root item removal functionality

- wrap essentials/extensions
This commit is contained in:
pythongosssss
2026-02-25 04:58:44 -08:00
parent ccced06925
commit d36e52d36f
3 changed files with 141 additions and 19 deletions

View File

@@ -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' })

View File

@@ -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

View File

@@ -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