Files
ComfyUI_frontend/src/components/rightSidePanel/parameters/TabNormalInputs.vue
Rizumu Ayaka f429e1e0c4 refactor: promote FormSearchInput to shared ui as AsyncSearchInput (#12185)
## Summary

Follow-up to #12183: move the debounced, searcher-driven search input
out of `src/renderer/...` and into the shared primitives folder, so both
the graph (form dropdown node widget) and the shell UI (templates
dialog, right side panel tabs) can use it without crossing the renderer
layer.

## Changes

- **What**: Renamed and relocated `FormSearchInput` → `AsyncSearchInput`
at `src/components/ui/search-input/AsyncSearchInput.vue`, joining the
existing `SearchInput` / `SearchAutocomplete` siblings.
- **What**: Updated all 9 callers (graph form dropdown, right side panel
tabs, templates dialog) to import from the new path/name. Test file
moved alongside the component.
- **Breaking**: None — pure rename + relocate, behavior is identical.

## Review Focus

- New name reflects the component's distinguishing feature (the async
`searcher` lifecycle: debounce + cancellation + loading state); see
Slack thread.
- Slack thread captured the discussion — splitting from #12183 so the
perf fix can backport cleanly to the release line.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-12185-refactor-promote-FormSearchInput-to-shared-ui-as-AsyncSearchInput-35e6d73d365081c585d8d421ea4353fa)
by [Unito](https://www.unito.io)

Co-authored-by: Christian Byrne <cbyrne@comfy.org>
2026-05-14 21:14:22 +00:00

181 lines
5.4 KiB
Vue

<script setup lang="ts">
import { storeToRefs } from 'pinia'
import { computed, reactive, ref, shallowRef, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import type { LGraphNode, NodeId } from '@/lib/litegraph/src/litegraph'
import CollapseToggleButton from '@/components/rightSidePanel/layout/CollapseToggleButton.vue'
import AsyncSearchInput from '@/components/ui/search-input/AsyncSearchInput.vue'
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
import { useRightSidePanelStore } from '@/stores/workspace/rightSidePanelStore'
import { computedSectionDataList, searchWidgetsAndNodes } from '../shared'
import type { NodeWidgetsListList } from '../shared'
import SectionWidgets from './SectionWidgets.vue'
const { nodes, mustShowNodeTitle } = defineProps<{
mustShowNodeTitle?: boolean
nodes: LGraphNode[]
}>()
const { t } = useI18n()
const workflowStore = useWorkflowStore()
const rightSidePanelStore = useRightSidePanelStore()
const { searchQuery } = storeToRefs(rightSidePanelStore)
const { widgetsSectionDataList, includesAdvanced } = computedSectionDataList(
() => nodes
)
const advancedWidgetsSectionDataList = computed((): NodeWidgetsListList => {
if (includesAdvanced.value) {
return []
}
return nodes
.map((node) => {
const { widgets = [] } = node
const advancedWidgets = widgets
.filter(
(w) =>
!(w.options?.canvasOnly || w.options?.hidden) && w.options?.advanced
)
.map((widget) => ({ node, widget }))
return { widgets: advancedWidgets, node }
})
.filter(({ widgets }) => widgets.length > 0)
})
const isMultipleNodesSelected = computed(
() => widgetsSectionDataList.value.length > 1
)
const searchedWidgetsSectionDataList = shallowRef<NodeWidgetsListList>(
widgetsSectionDataList.value
)
const isSearching = ref(false)
const collapseMap = reactive<Record<string, boolean>>({})
const advancedCollapsed = ref(true)
watch(
() => workflowStore.activeWorkflow?.path,
() => {
for (const key of Object.keys(collapseMap)) {
delete collapseMap[key]
}
advancedCollapsed.value = true
}
)
function isSectionCollapsed(nodeId: NodeId): boolean {
// When not explicitly set, sections are collapsed if multiple nodes are selected
return collapseMap[nodeId] ?? isMultipleNodesSelected.value
}
function setSectionCollapsed(nodeId: NodeId, collapsed: boolean) {
collapseMap[nodeId] = collapsed
}
const isAllCollapsed = computed({
get() {
const normalAllCollapsed = searchedWidgetsSectionDataList.value.every(
({ node }) => isSectionCollapsed(node.id)
)
const hasAdvanced = advancedWidgetsSectionDataList.value.length > 0
return hasAdvanced
? normalAllCollapsed && advancedCollapsed.value
: normalAllCollapsed
},
set(collapse: boolean) {
for (const { node } of widgetsSectionDataList.value) {
setSectionCollapsed(node.id, collapse)
}
advancedCollapsed.value = collapse
}
})
async function searcher(query: string) {
const list = widgetsSectionDataList.value
const target = searchedWidgetsSectionDataList
isSearching.value = query.trim() !== ''
target.value = searchWidgetsAndNodes(list, query)
}
const label = computed(() => {
const sections = widgetsSectionDataList.value
return !mustShowNodeTitle && sections.length === 1
? sections[0].widgets.length !== 0
? t('rightSidePanel.inputs')
: t('rightSidePanel.inputsNone')
: undefined // SectionWidgets display node titles by default
})
const advancedLabel = computed(() => {
return !mustShowNodeTitle && !isMultipleNodesSelected.value
? t('rightSidePanel.advancedInputs')
: undefined // SectionWidgets display node titles by default
})
</script>
<template>
<div
class="flex items-center border-b border-interface-stroke px-4 pt-1 pb-4"
>
<AsyncSearchInput
v-model="searchQuery"
:searcher
:update-key="widgetsSectionDataList"
class="flex-1"
/>
<CollapseToggleButton
v-model="isAllCollapsed"
:show="
!isSearching &&
widgetsSectionDataList.length + advancedWidgetsSectionDataList.length >
1
"
/>
</div>
<TransitionGroup tag="div" name="list-scale" class="relative">
<div
v-if="searchedWidgetsSectionDataList.length === 0"
class="px-4 py-10 text-center text-sm text-muted-foreground"
>
{{
isSearching
? t('rightSidePanel.noneSearchDesc')
: t('rightSidePanel.nodesNoneDesc')
}}
</div>
<SectionWidgets
v-for="{ widgets, node } in searchedWidgetsSectionDataList"
:key="node.id"
:node
:label
:widgets
:collapse="isSectionCollapsed(node.id) && !isSearching"
:show-locate-button="isMultipleNodesSelected"
:tooltip="
isSearching || widgets.length
? ''
: t('rightSidePanel.inputsNoneTooltip')
"
class="border-b border-interface-stroke"
@update:collapse="setSectionCollapsed(node.id, $event)"
/>
</TransitionGroup>
<template v-if="advancedWidgetsSectionDataList.length > 0 && !isSearching">
<SectionWidgets
v-for="{ widgets, node } in advancedWidgetsSectionDataList"
:key="`advanced-${node.id}`"
v-model:collapse="advancedCollapsed"
:node
:label="advancedLabel"
:widgets
:show-locate-button="isMultipleNodesSelected"
class="border-b border-interface-stroke"
/>
</template>
</template>