mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-28 02:02:08 +00:00
Node library search filters (#636)
* Add search filters to node library * Fix * Dont close on add * Fix wildcard --------- Co-authored-by: Chenlei Hu <chenlei.hu@mail.utoronto.ca>
This commit is contained in:
@@ -1,20 +1,45 @@
|
|||||||
<template>
|
<template>
|
||||||
<IconField :class="props.class">
|
<div :class="props.class">
|
||||||
<InputIcon :class="props.icon" />
|
<IconField>
|
||||||
<InputText
|
<InputIcon :class="props.icon" />
|
||||||
class="search-box-input"
|
<InputText
|
||||||
@input="handleInput"
|
class="search-box-input"
|
||||||
:modelValue="props.modelValue"
|
:class="{ ['with-filter']: props.filterIcon }"
|
||||||
:placeholder="props.placeholder"
|
@input="handleInput"
|
||||||
/>
|
:modelValue="props.modelValue"
|
||||||
</IconField>
|
:placeholder="props.placeholder"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
v-if="props.filterIcon"
|
||||||
|
class="p-inputicon"
|
||||||
|
:icon="props.filterIcon"
|
||||||
|
text
|
||||||
|
severity="contrast"
|
||||||
|
@click="$emit('showFilter', $event)"
|
||||||
|
/>
|
||||||
|
</IconField>
|
||||||
|
<div class="search-filters" v-if="filters">
|
||||||
|
<SearchFilterChip
|
||||||
|
v-for="filter in filters"
|
||||||
|
:key="filter.id"
|
||||||
|
:text="filter.text"
|
||||||
|
:badge="filter.badge"
|
||||||
|
:badge-class="filter.badgeClass"
|
||||||
|
@remove="$emit('removeFilter', filter)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts" generic="TFilter extends SearchFilter">
|
||||||
|
import type { SearchFilter } from './SearchFilterChip.vue'
|
||||||
import { debounce } from 'lodash'
|
import { debounce } from 'lodash'
|
||||||
import IconField from 'primevue/iconfield'
|
import IconField from 'primevue/iconfield'
|
||||||
import InputIcon from 'primevue/inputicon'
|
import InputIcon from 'primevue/inputicon'
|
||||||
import InputText from 'primevue/inputtext'
|
import InputText from 'primevue/inputtext'
|
||||||
|
import Button from 'primevue/button'
|
||||||
|
import SearchFilterChip from './SearchFilterChip.vue'
|
||||||
|
import { toRefs } from 'vue'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
class?: string
|
class?: string
|
||||||
@@ -22,6 +47,8 @@ interface Props {
|
|||||||
placeholder?: string
|
placeholder?: string
|
||||||
icon?: string
|
icon?: string
|
||||||
debounceTime?: number
|
debounceTime?: number
|
||||||
|
filterIcon?: string
|
||||||
|
filters?: TFilter[]
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
@@ -30,10 +57,17 @@ const props = withDefaults(defineProps<Props>(), {
|
|||||||
debounceTime: 300
|
debounceTime: 300
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits(['update:modelValue', 'search'])
|
const { filters } = toRefs(props)
|
||||||
|
|
||||||
|
const emit = defineEmits([
|
||||||
|
'update:modelValue',
|
||||||
|
'search',
|
||||||
|
'showFilter',
|
||||||
|
'removeFilter'
|
||||||
|
])
|
||||||
|
|
||||||
const emitSearch = debounce((value: string) => {
|
const emitSearch = debounce((value: string) => {
|
||||||
emit('search', value)
|
emit('search', value, props.filters)
|
||||||
}, props.debounceTime)
|
}, props.debounceTime)
|
||||||
|
|
||||||
const handleInput = (event: Event) => {
|
const handleInput = (event: Event) => {
|
||||||
@@ -46,5 +80,20 @@ const handleInput = (event: Event) => {
|
|||||||
<style scoped>
|
<style scoped>
|
||||||
.search-box-input {
|
.search-box-input {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
padding-left: 36px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-box-input.with-filter {
|
||||||
|
padding-right: 36px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.p-button.p-inputicon {
|
||||||
|
padding: 0;
|
||||||
|
width: auto;
|
||||||
|
border: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-filters {
|
||||||
|
@apply pt-2 flex flex-wrap gap-2;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
41
src/components/common/SearchFilterChip.vue
Normal file
41
src/components/common/SearchFilterChip.vue
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
<template>
|
||||||
|
<Chip removable @remove="$emit('remove', $event)">
|
||||||
|
<Badge size="small" :class="badgeClass">
|
||||||
|
{{ badge }}
|
||||||
|
</Badge>
|
||||||
|
{{ text }}
|
||||||
|
</Chip>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import Chip from 'primevue/chip'
|
||||||
|
import Badge from 'primevue/badge'
|
||||||
|
|
||||||
|
export interface SearchFilter {
|
||||||
|
text: string
|
||||||
|
badge: string
|
||||||
|
badgeClass: string
|
||||||
|
id: string | number
|
||||||
|
}
|
||||||
|
|
||||||
|
defineProps<Omit<SearchFilter, 'id'>>()
|
||||||
|
defineEmits(['remove'])
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
:deep(.i-badge) {
|
||||||
|
@apply bg-green-500 text-white;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.o-badge) {
|
||||||
|
@apply bg-red-500 text-white;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.c-badge) {
|
||||||
|
@apply bg-blue-500 text-white;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.s-badge) {
|
||||||
|
@apply bg-yellow-500;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -7,7 +7,22 @@
|
|||||||
v-if="hoveredSuggestion"
|
v-if="hoveredSuggestion"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<NodeSearchFilter @addFilter="onAddFilter" />
|
|
||||||
|
<Button
|
||||||
|
icon="pi pi-filter"
|
||||||
|
severity="secondary"
|
||||||
|
class="_filter-button"
|
||||||
|
@click="nodeSearchFilterVisible = true"
|
||||||
|
/>
|
||||||
|
<Dialog v-model:visible="nodeSearchFilterVisible" class="_dialog">
|
||||||
|
<template #header>
|
||||||
|
<h3>Add node filter condition</h3>
|
||||||
|
</template>
|
||||||
|
<div class="_dialog-body">
|
||||||
|
<NodeSearchFilter @addFilter="onAddFilter"></NodeSearchFilter>
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
<AutoCompletePlus
|
<AutoCompletePlus
|
||||||
:model-value="props.filters"
|
:model-value="props.filters"
|
||||||
class="comfy-vue-node-search-box"
|
class="comfy-vue-node-search-box"
|
||||||
@@ -56,12 +71,12 @@
|
|||||||
</template>
|
</template>
|
||||||
<!-- FilterAndValue -->
|
<!-- FilterAndValue -->
|
||||||
<template v-slot:chip="{ value }">
|
<template v-slot:chip="{ value }">
|
||||||
<Chip removable @remove="onRemoveFilter($event, value)">
|
<SearchFilterChip
|
||||||
<Badge size="small" :class="value[0].invokeSequence + '-badge'">
|
@remove="onRemoveFilter($event, value)"
|
||||||
{{ value[0].invokeSequence.toUpperCase() }}
|
:text="value[1]"
|
||||||
</Badge>
|
:badge="value[0].invokeSequence.toUpperCase()"
|
||||||
{{ value[1] }}
|
:badge-class="value[0].invokeSequence + '-badge'"
|
||||||
</Chip>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</AutoCompletePlus>
|
</AutoCompletePlus>
|
||||||
</div>
|
</div>
|
||||||
@@ -70,9 +85,9 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onMounted, ref } from 'vue'
|
import { computed, onMounted, ref } from 'vue'
|
||||||
import AutoCompletePlus from '@/components/primevueOverride/AutoCompletePlus.vue'
|
import AutoCompletePlus from '@/components/primevueOverride/AutoCompletePlus.vue'
|
||||||
import Chip from 'primevue/chip'
|
|
||||||
import Badge from 'primevue/badge'
|
|
||||||
import Tag from 'primevue/tag'
|
import Tag from 'primevue/tag'
|
||||||
|
import Dialog from 'primevue/dialog'
|
||||||
|
import Button from 'primevue/button'
|
||||||
import NodeSearchFilter from '@/components/searchbox/NodeSearchFilter.vue'
|
import NodeSearchFilter from '@/components/searchbox/NodeSearchFilter.vue'
|
||||||
import NodeSourceChip from '@/components/node/NodeSourceChip.vue'
|
import NodeSourceChip from '@/components/node/NodeSourceChip.vue'
|
||||||
import { type FilterAndValue } from '@/services/nodeSearchService'
|
import { type FilterAndValue } from '@/services/nodeSearchService'
|
||||||
@@ -80,6 +95,7 @@ import NodePreview from '@/components/node/NodePreview.vue'
|
|||||||
import { ComfyNodeDefImpl, useNodeDefStore } from '@/stores/nodeDefStore'
|
import { ComfyNodeDefImpl, useNodeDefStore } from '@/stores/nodeDefStore'
|
||||||
import { useSettingStore } from '@/stores/settingStore'
|
import { useSettingStore } from '@/stores/settingStore'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import SearchFilterChip from '../common/SearchFilterChip.vue'
|
||||||
|
|
||||||
const settingStore = useSettingStore()
|
const settingStore = useSettingStore()
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
@@ -101,6 +117,7 @@ const props = defineProps({
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const nodeSearchFilterVisible = ref(false)
|
||||||
const inputId = `comfy-vue-node-search-box-input-${Math.random()}`
|
const inputId = `comfy-vue-node-search-box-input-${Math.random()}`
|
||||||
const suggestions = ref<ComfyNodeDefImpl[]>([])
|
const suggestions = ref<ComfyNodeDefImpl[]>([])
|
||||||
const hoveredSuggestion = ref<ComfyNodeDefImpl | null>(null)
|
const hoveredSuggestion = ref<ComfyNodeDefImpl | null>(null)
|
||||||
@@ -136,6 +153,7 @@ const reFocusInput = () => {
|
|||||||
|
|
||||||
onMounted(reFocusInput)
|
onMounted(reFocusInput)
|
||||||
const onAddFilter = (filterAndValue: FilterAndValue) => {
|
const onAddFilter = (filterAndValue: FilterAndValue) => {
|
||||||
|
nodeSearchFilterVisible.value = false
|
||||||
emit('addFilter', filterAndValue)
|
emit('addFilter', filterAndValue)
|
||||||
reFocusInput()
|
reFocusInput()
|
||||||
}
|
}
|
||||||
@@ -188,22 +206,6 @@ const setHoverSuggestion = (index: number) => {
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.i-badge {
|
|
||||||
@apply bg-green-500 text-white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.o-badge {
|
|
||||||
@apply bg-red-500 text-white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.c-badge {
|
|
||||||
@apply bg-blue-500 text-white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.s-badge {
|
|
||||||
@apply bg-yellow-500;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.highlight) {
|
:deep(.highlight) {
|
||||||
background-color: var(--p-primary-color);
|
background-color: var(--p-primary-color);
|
||||||
color: var(--p-primary-contrast-color);
|
color: var(--p-primary-contrast-color);
|
||||||
@@ -212,4 +214,12 @@ const setHoverSuggestion = (index: number) => {
|
|||||||
padding: 0rem 0.125rem;
|
padding: 0rem 0.125rem;
|
||||||
margin: -0.125rem 0.125rem;
|
margin: -0.125rem 0.125rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
._filter-button {
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
._dialog {
|
||||||
|
@apply min-w-96;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,48 +1,35 @@
|
|||||||
<template>
|
<template>
|
||||||
<Button
|
<div class="_content">
|
||||||
icon="pi pi-filter"
|
<SelectButton
|
||||||
severity="secondary"
|
v-model="selectedFilter"
|
||||||
class="_filter-button"
|
:options="filters"
|
||||||
@click="showModal"
|
:allowEmpty="false"
|
||||||
/>
|
optionLabel="name"
|
||||||
<Dialog v-model:visible="visible" class="_dialog">
|
@change="updateSelectedFilterValue"
|
||||||
<template #header>
|
/>
|
||||||
<h3>Add node filter condition</h3>
|
<AutoComplete
|
||||||
</template>
|
v-model="selectedFilterValue"
|
||||||
<div class="_dialog-body">
|
:suggestions="filterValues"
|
||||||
<SelectButton
|
:min-length="0"
|
||||||
v-model="selectedFilter"
|
@complete="(event) => updateFilterValues(event.query)"
|
||||||
:options="filters"
|
completeOnFocus
|
||||||
:allowEmpty="false"
|
forceSelection
|
||||||
optionLabel="name"
|
dropdown
|
||||||
@change="updateSelectedFilterValue"
|
></AutoComplete>
|
||||||
/>
|
</div>
|
||||||
<AutoComplete
|
<div class="_footer">
|
||||||
v-model="selectedFilterValue"
|
<Button type="button" label="Add" @click="submit"></Button>
|
||||||
:suggestions="filterValues"
|
</div>
|
||||||
:min-length="0"
|
|
||||||
@complete="(event) => updateFilterValues(event.query)"
|
|
||||||
completeOnFocus
|
|
||||||
forceSelection
|
|
||||||
dropdown
|
|
||||||
></AutoComplete>
|
|
||||||
</div>
|
|
||||||
<template #footer>
|
|
||||||
<Button type="button" label="Add" @click="submit"></Button>
|
|
||||||
</template>
|
|
||||||
</Dialog>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { NodeFilter, type FilterAndValue } from '@/services/nodeSearchService'
|
import { NodeFilter, type FilterAndValue } from '@/services/nodeSearchService'
|
||||||
import Button from 'primevue/button'
|
import Button from 'primevue/button'
|
||||||
import Dialog from 'primevue/dialog'
|
|
||||||
import SelectButton from 'primevue/selectbutton'
|
import SelectButton from 'primevue/selectbutton'
|
||||||
import AutoComplete from 'primevue/autocomplete'
|
import AutoComplete from 'primevue/autocomplete'
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
import { useNodeDefStore } from '@/stores/nodeDefStore'
|
import { useNodeDefStore } from '@/stores/nodeDefStore'
|
||||||
|
|
||||||
const visible = ref<boolean>(false)
|
|
||||||
const filters = ref<NodeFilter[]>([])
|
const filters = ref<NodeFilter[]>([])
|
||||||
const selectedFilter = ref<NodeFilter>()
|
const selectedFilter = ref<NodeFilter>()
|
||||||
const filterValues = ref<string[]>([])
|
const filterValues = ref<string[]>([])
|
||||||
@@ -69,29 +56,21 @@ const updateFilterValues = (query: string) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const submit = () => {
|
const submit = () => {
|
||||||
visible.value = false
|
|
||||||
emit('addFilter', [
|
emit('addFilter', [
|
||||||
selectedFilter.value,
|
selectedFilter.value,
|
||||||
selectedFilterValue.value
|
selectedFilterValue.value
|
||||||
] as FilterAndValue)
|
] as FilterAndValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
const showModal = () => {
|
onMounted(updateSelectedFilterValue)
|
||||||
updateSelectedFilterValue()
|
|
||||||
visible.value = true
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
._filter-button {
|
._content {
|
||||||
z-index: 10;
|
|
||||||
}
|
|
||||||
|
|
||||||
._dialog {
|
|
||||||
@apply min-w-96;
|
|
||||||
}
|
|
||||||
|
|
||||||
._dialog-body {
|
|
||||||
@apply flex flex-col space-y-2;
|
@apply flex flex-col space-y-2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
._footer {
|
||||||
|
@apply flex flex-col pt-4 items-end;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -23,8 +23,17 @@
|
|||||||
class="node-lib-search-box"
|
class="node-lib-search-box"
|
||||||
v-model:modelValue="searchQuery"
|
v-model:modelValue="searchQuery"
|
||||||
@search="handleSearch"
|
@search="handleSearch"
|
||||||
|
@show-filter="($event) => searchFilter.toggle($event)"
|
||||||
|
@remove-filter="onRemoveFilter"
|
||||||
:placeholder="$t('searchNodes') + '...'"
|
:placeholder="$t('searchNodes') + '...'"
|
||||||
|
filter-icon="pi pi-filter"
|
||||||
|
:filters
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<Popover ref="searchFilter" class="node-lib-filter-popup">
|
||||||
|
<NodeSearchFilter @addFilter="onAddFilter" />
|
||||||
|
</Popover>
|
||||||
|
|
||||||
<Tree
|
<Tree
|
||||||
class="node-lib-tree"
|
class="node-lib-tree"
|
||||||
v-model:expandedKeys="expandedKeys"
|
v-model:expandedKeys="expandedKeys"
|
||||||
@@ -102,8 +111,9 @@ import {
|
|||||||
ComfyNodeDefImpl,
|
ComfyNodeDefImpl,
|
||||||
useNodeDefStore
|
useNodeDefStore
|
||||||
} from '@/stores/nodeDefStore'
|
} from '@/stores/nodeDefStore'
|
||||||
import { computed, ref, nextTick } from 'vue'
|
import { computed, ref, nextTick, Ref } from 'vue'
|
||||||
import type { TreeNode } from 'primevue/treenode'
|
import type { TreeNode } from 'primevue/treenode'
|
||||||
|
import Popover from 'primevue/popover'
|
||||||
import NodeTreeLeaf from './nodeLibrary/NodeTreeLeaf.vue'
|
import NodeTreeLeaf from './nodeLibrary/NodeTreeLeaf.vue'
|
||||||
import NodeTreeFolder from './nodeLibrary/NodeTreeFolder.vue'
|
import NodeTreeFolder from './nodeLibrary/NodeTreeFolder.vue'
|
||||||
import Tree from 'primevue/tree'
|
import Tree from 'primevue/tree'
|
||||||
@@ -116,17 +126,20 @@ import SidebarTabTemplate from '@/components/sidebar/tabs/SidebarTabTemplate.vue
|
|||||||
import { useSettingStore } from '@/stores/settingStore'
|
import { useSettingStore } from '@/stores/settingStore'
|
||||||
import { app } from '@/scripts/app'
|
import { app } from '@/scripts/app'
|
||||||
import { findNodeByKey, sortedTree } from '@/utils/treeUtil'
|
import { findNodeByKey, sortedTree } from '@/utils/treeUtil'
|
||||||
import _ from 'lodash'
|
|
||||||
import { useTreeExpansion } from '@/hooks/treeHooks'
|
import { useTreeExpansion } from '@/hooks/treeHooks'
|
||||||
import type { MenuItem } from 'primevue/menuitem'
|
import type { MenuItem } from 'primevue/menuitem'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { useToast } from 'primevue/usetoast'
|
import { useToast } from 'primevue/usetoast'
|
||||||
|
import NodeSearchFilter from '@/components/searchbox/NodeSearchFilter.vue'
|
||||||
|
import { FilterAndValue } from '@/services/nodeSearchService'
|
||||||
|
import { SearchFilter } from '@/components/common/SearchFilterChip.vue'
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
const nodeDefStore = useNodeDefStore()
|
const nodeDefStore = useNodeDefStore()
|
||||||
const { expandedKeys, expandNode, toggleNodeOnEvent } = useTreeExpansion()
|
const { expandedKeys, expandNode, toggleNodeOnEvent } = useTreeExpansion()
|
||||||
|
|
||||||
|
const searchFilter = ref(null)
|
||||||
const alphabeticalSort = ref(false)
|
const alphabeticalSort = ref(false)
|
||||||
const hoveredComfyNodeName = ref<string | null>(null)
|
const hoveredComfyNodeName = ref<string | null>(null)
|
||||||
const hoveredComfyNode = computed<ComfyNodeDefImpl | null>(() => {
|
const hoveredComfyNode = computed<ComfyNodeDefImpl | null>(() => {
|
||||||
@@ -240,16 +253,26 @@ const insertNode = (nodeDef: ComfyNodeDefImpl) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const filteredRoot = ref<TreeNode | null>(null)
|
const filteredRoot = ref<TreeNode | null>(null)
|
||||||
|
const filters: Ref<Array<SearchFilter & { filter: FilterAndValue<string> }>> =
|
||||||
|
ref([])
|
||||||
const handleSearch = (query: string) => {
|
const handleSearch = (query: string) => {
|
||||||
if (query.length < 3) {
|
if (query.length < 3 && !filters.value.length) {
|
||||||
filteredRoot.value = null
|
filteredRoot.value = null
|
||||||
expandedKeys.value = {}
|
expandedKeys.value = {}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const matchedNodes = nodeDefStore.nodeSearchService.searchNode(query, [], {
|
const f = filters.value.map((f) => f.filter as FilterAndValue<string>)
|
||||||
limit: 64
|
const matchedNodes = nodeDefStore.nodeSearchService.searchNode(
|
||||||
})
|
query,
|
||||||
|
f,
|
||||||
|
{
|
||||||
|
limit: 64
|
||||||
|
},
|
||||||
|
{
|
||||||
|
matchWildcards: false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
filteredRoot.value = buildNodeDefTree(matchedNodes)
|
filteredRoot.value = buildNodeDefTree(matchedNodes)
|
||||||
expandNode(filteredRoot.value)
|
expandNode(filteredRoot.value)
|
||||||
@@ -353,6 +376,26 @@ const updateCustomization = (icon: string, color: string) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const onAddFilter = (filterAndValue: FilterAndValue) => {
|
||||||
|
filters.value.push({
|
||||||
|
filter: filterAndValue,
|
||||||
|
badge: filterAndValue[0].invokeSequence.toUpperCase(),
|
||||||
|
badgeClass: filterAndValue[0].invokeSequence + '-badge',
|
||||||
|
text: filterAndValue[1],
|
||||||
|
id: +new Date()
|
||||||
|
})
|
||||||
|
|
||||||
|
handleSearch(searchQuery.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onRemoveFilter = (filterAndValue) => {
|
||||||
|
const index = filters.value.findIndex((f) => f === filterAndValue)
|
||||||
|
if (index !== -1) {
|
||||||
|
filters.value.splice(index, 1)
|
||||||
|
}
|
||||||
|
handleSearch(searchQuery.value)
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
@@ -362,6 +405,10 @@ const updateCustomization = (icon: string, color: string) => {
|
|||||||
margin-left: var(--p-tree-node-gap);
|
margin-left: var(--p-tree-node-gap);
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.node-lib-filter-popup {
|
||||||
|
margin-left: -13px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@@ -5,6 +5,10 @@ import _ from 'lodash'
|
|||||||
|
|
||||||
type SearchAuxScore = [number, number, number, number]
|
type SearchAuxScore = [number, number, number, number]
|
||||||
|
|
||||||
|
interface ExtraSearchOptions {
|
||||||
|
matchWildcards?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
export class FuseSearch<T> {
|
export class FuseSearch<T> {
|
||||||
private fuse: Fuse<T>
|
private fuse: Fuse<T>
|
||||||
private readonly keys: string[]
|
private readonly keys: string[]
|
||||||
@@ -145,13 +149,19 @@ export abstract class NodeFilter<FilterOptionT = string> {
|
|||||||
|
|
||||||
public abstract getNodeOptions(node: ComfyNodeDefImpl): FilterOptionT[]
|
public abstract getNodeOptions(node: ComfyNodeDefImpl): FilterOptionT[]
|
||||||
|
|
||||||
public matches(node: ComfyNodeDefImpl, value: FilterOptionT): boolean {
|
public matches(
|
||||||
if (value === '*') {
|
node: ComfyNodeDefImpl,
|
||||||
|
value: FilterOptionT,
|
||||||
|
extraOptions?: ExtraSearchOptions
|
||||||
|
): boolean {
|
||||||
|
const matchWildcards = extraOptions?.matchWildcards !== false
|
||||||
|
if (matchWildcards && value === '*') {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
const options = this.getNodeOptions(node)
|
const options = this.getNodeOptions(node)
|
||||||
return (
|
return (
|
||||||
options.includes(value) || _.some(options, (option) => option === '*')
|
options.includes(value) ||
|
||||||
|
(matchWildcards && _.some(options, (option) => option === '*'))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -242,14 +252,15 @@ export class NodeSearchService {
|
|||||||
public searchNode(
|
public searchNode(
|
||||||
query: string,
|
query: string,
|
||||||
filters: FilterAndValue<string>[] = [],
|
filters: FilterAndValue<string>[] = [],
|
||||||
options?: FuseSearchOptions
|
options?: FuseSearchOptions,
|
||||||
|
extraOptions?: ExtraSearchOptions
|
||||||
): ComfyNodeDefImpl[] {
|
): ComfyNodeDefImpl[] {
|
||||||
const matchedNodes = this.nodeFuseSearch.search(query)
|
const matchedNodes = this.nodeFuseSearch.search(query)
|
||||||
|
|
||||||
const results = matchedNodes.filter((node) => {
|
const results = matchedNodes.filter((node) => {
|
||||||
return _.every(filters, (filterAndValue) => {
|
return _.every(filters, (filterAndValue) => {
|
||||||
const [filter, value] = filterAndValue
|
const [filter, value] = filterAndValue
|
||||||
return filter.matches(node, value)
|
return filter.matches(node, value, extraOptions)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user