mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-03-09 15:10:17 +00:00
feat: add collapse/expand all toggle to right panel tabs (#9333)
## Summary Adds a collapse/expand all toggle button to all parameter and error tabs in the right side panel, letting users quickly collapse or expand all accordion sections at once. ## Changes - **New component**: `CollapseToggleButton.vue` — a reusable icon button (list-collapse / list-tree icon) with tooltip, bound via `v-model` - **Error tab**: Toggle collapses/expands all error groups; per-group state is now managed through `isSectionCollapsed` / `setSectionCollapsed` helpers - **Nodes tab** (`TabNodes.vue`): Per-node `collapseMap`; toggle overrides per-section state; defaults to collapsed (nodes tab default was already collapsed) - **Normal inputs tab** (`TabNormalInputs.vue`): Per-node `collapseMap` + `advancedCollapsed`; toggle covers both normal and advanced sections; defaults to collapsed when multiple nodes selected - **Subgraph inputs tab** (`TabSubgraphInputs.vue`): Toggle covers both main and advanced inputs sections - **Global parameters tab** (`TabGlobalParameters.vue`): Toggle bound to single `SectionWidgets` - **i18n**: Added `g.collapseAll` and `g.expandAll` keys ## Review Focus - `isAllCollapsed` getter in each tab: reads from the same per-section state so the toggle accurately reflects current state rather than being independently tracked - `TabNormalInputs`: multi-node selection default collapse behaviour is preserved through `isSectionCollapsed` fallback logic ## Screenshots <img width="778" height="643" alt="image" src="https://github.com/user-attachments/assets/04d07f32-5135-47f9-b029-78ca78a996fb" /> ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-9333-feat-add-collapse-expand-all-toggle-to-right-panel-tabs-3176d73d36508123ba22d6e81983bb1b) by [Unito](https://www.unito.io)
This commit is contained in:
@@ -1,38 +1,43 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col h-full min-w-0">
|
<div class="flex flex-col h-full min-w-0">
|
||||||
<!-- Search bar -->
|
<!-- Search bar + collapse toggle -->
|
||||||
<div
|
<div
|
||||||
class="px-4 pt-1 pb-4 flex gap-2 border-b border-interface-stroke shrink-0 min-w-0"
|
class="px-4 pt-1 pb-4 flex items-center border-b border-interface-stroke shrink-0 min-w-0"
|
||||||
>
|
>
|
||||||
<FormSearchInput v-model="searchQuery" />
|
<FormSearchInput v-model="searchQuery" class="flex-1" />
|
||||||
|
<CollapseToggleButton
|
||||||
|
v-model="isAllCollapsed"
|
||||||
|
:show="!isSearching && tabErrorGroups.length > 1"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Scrollable content -->
|
<!-- Scrollable content -->
|
||||||
<div class="flex-1 overflow-y-auto min-w-0">
|
<div class="flex-1 overflow-y-auto min-w-0">
|
||||||
<div
|
<TransitionGroup tag="div" name="list-scale" class="relative">
|
||||||
v-if="filteredGroups.length === 0"
|
<div
|
||||||
class="text-sm text-muted-foreground px-4 text-center pt-5 pb-15"
|
v-if="filteredGroups.length === 0"
|
||||||
>
|
key="empty"
|
||||||
{{
|
class="text-sm text-muted-foreground px-4 text-center pt-5 pb-15"
|
||||||
searchQuery.trim()
|
>
|
||||||
? t('rightSidePanel.noneSearchDesc')
|
{{
|
||||||
: t('rightSidePanel.noErrors')
|
searchQuery.trim()
|
||||||
}}
|
? t('rightSidePanel.noneSearchDesc')
|
||||||
</div>
|
: t('rightSidePanel.noErrors')
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div v-else>
|
|
||||||
<!-- Group by Class Type -->
|
<!-- Group by Class Type -->
|
||||||
<PropertiesAccordionItem
|
<PropertiesAccordionItem
|
||||||
v-for="group in filteredGroups"
|
v-for="group in filteredGroups"
|
||||||
:key="group.title"
|
:key="group.title"
|
||||||
:collapse="collapseState[group.title] ?? false"
|
:collapse="isSectionCollapsed(group.title) && !isSearching"
|
||||||
class="border-b border-interface-stroke"
|
class="border-b border-interface-stroke"
|
||||||
:size="
|
:size="
|
||||||
group.type === 'missing_node' || group.type === 'swap_nodes'
|
group.type === 'missing_node' || group.type === 'swap_nodes'
|
||||||
? 'lg'
|
? 'lg'
|
||||||
: 'default'
|
: 'default'
|
||||||
"
|
"
|
||||||
@update:collapse="collapseState[group.title] = $event"
|
@update:collapse="setSectionCollapsed(group.title, $event)"
|
||||||
>
|
>
|
||||||
<template #label>
|
<template #label>
|
||||||
<div class="flex items-center gap-2 flex-1 min-w-0">
|
<div class="flex items-center gap-2 flex-1 min-w-0">
|
||||||
@@ -126,7 +131,7 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</PropertiesAccordionItem>
|
</PropertiesAccordionItem>
|
||||||
</div>
|
</TransitionGroup>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Fixed Footer: Help Links -->
|
<!-- Fixed Footer: Help Links -->
|
||||||
@@ -177,6 +182,7 @@ import { ManagerTab } from '@/workbench/extensions/manager/types/comfyManagerTyp
|
|||||||
import { NodeBadgeMode } from '@/types/nodeSource'
|
import { NodeBadgeMode } from '@/types/nodeSource'
|
||||||
|
|
||||||
import PropertiesAccordionItem from '../layout/PropertiesAccordionItem.vue'
|
import PropertiesAccordionItem from '../layout/PropertiesAccordionItem.vue'
|
||||||
|
import CollapseToggleButton from '../layout/CollapseToggleButton.vue'
|
||||||
import FormSearchInput from '@/renderer/extensions/vueNodes/widgets/components/form/FormSearchInput.vue'
|
import FormSearchInput from '@/renderer/extensions/vueNodes/widgets/components/form/FormSearchInput.vue'
|
||||||
import ErrorNodeCard from './ErrorNodeCard.vue'
|
import ErrorNodeCard from './ErrorNodeCard.vue'
|
||||||
import MissingNodeCard from './MissingNodeCard.vue'
|
import MissingNodeCard from './MissingNodeCard.vue'
|
||||||
@@ -203,6 +209,7 @@ const { isInstalling: isInstallingAll, installAllPacks: installAll } =
|
|||||||
const { replaceGroup, replaceAllGroups } = useNodeReplacement()
|
const { replaceGroup, replaceAllGroups } = useNodeReplacement()
|
||||||
|
|
||||||
const searchQuery = ref('')
|
const searchQuery = ref('')
|
||||||
|
const isSearching = computed(() => searchQuery.value.trim() !== '')
|
||||||
|
|
||||||
const showNodeIdBadge = computed(
|
const showNodeIdBadge = computed(
|
||||||
() =>
|
() =>
|
||||||
@@ -212,6 +219,7 @@ const showNodeIdBadge = computed(
|
|||||||
|
|
||||||
const {
|
const {
|
||||||
allErrorGroups,
|
allErrorGroups,
|
||||||
|
tabErrorGroups,
|
||||||
filteredGroups,
|
filteredGroups,
|
||||||
collapseState,
|
collapseState,
|
||||||
isSingleNodeSelected,
|
isSingleNodeSelected,
|
||||||
@@ -221,6 +229,26 @@ const {
|
|||||||
swapNodeGroups
|
swapNodeGroups
|
||||||
} = useErrorGroups(searchQuery, t)
|
} = useErrorGroups(searchQuery, t)
|
||||||
|
|
||||||
|
const isAllCollapsed = computed({
|
||||||
|
get() {
|
||||||
|
return filteredGroups.value.every((g) => isSectionCollapsed(g.title))
|
||||||
|
},
|
||||||
|
set(collapse: boolean) {
|
||||||
|
for (const group of tabErrorGroups.value) {
|
||||||
|
setSectionCollapsed(group.title, collapse)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
function isSectionCollapsed(title: string): boolean {
|
||||||
|
// Defaults to expanded when not explicitly set by the user
|
||||||
|
return collapseState[title] ?? false
|
||||||
|
}
|
||||||
|
|
||||||
|
function setSectionCollapsed(title: string, collapsed: boolean) {
|
||||||
|
collapseState[title] = collapsed
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* When an external trigger (e.g. "See Error" button in SectionWidgets)
|
* When an external trigger (e.g. "See Error" button in SectionWidgets)
|
||||||
* sets focusedErrorNodeId, expand only the group containing the target
|
* sets focusedErrorNodeId, expand only the group containing the target
|
||||||
@@ -240,7 +268,7 @@ watch(
|
|||||||
card.graphNodeId === graphNodeId ||
|
card.graphNodeId === graphNodeId ||
|
||||||
(card.nodeId?.startsWith(prefix) ?? false)
|
(card.nodeId?.startsWith(prefix) ?? false)
|
||||||
)
|
)
|
||||||
collapseState[group.title] = !hasMatch
|
setSectionCollapsed(group.title, !hasMatch)
|
||||||
}
|
}
|
||||||
rightSidePanelStore.focusedErrorNodeId = null
|
rightSidePanelStore.focusedErrorNodeId = null
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
<template>
|
||||||
|
<Transition
|
||||||
|
enter-active-class="transition-all duration-300 ease-in-out"
|
||||||
|
enter-from-class="max-w-0 opacity-0 ml-0"
|
||||||
|
enter-to-class="max-w-10 opacity-100 ml-2"
|
||||||
|
leave-active-class="transition-all duration-300 ease-in-out"
|
||||||
|
leave-from-class="max-w-10 opacity-100 ml-2"
|
||||||
|
leave-to-class="max-w-0 opacity-0 ml-0"
|
||||||
|
>
|
||||||
|
<div v-if="show" class="overflow-hidden flex items-center ml-2">
|
||||||
|
<Button
|
||||||
|
v-tooltip.bottom="
|
||||||
|
isAllCollapsed ? t('g.expandAll') : t('g.collapseAll')
|
||||||
|
"
|
||||||
|
:aria-label="isAllCollapsed ? t('g.expandAll') : t('g.collapseAll')"
|
||||||
|
variant="textonly"
|
||||||
|
size="icon-sm"
|
||||||
|
class="size-8 shrink-0 text-muted-foreground hover:text-base-foreground"
|
||||||
|
@click="toggle"
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
:class="
|
||||||
|
cn(
|
||||||
|
'size-4',
|
||||||
|
isAllCollapsed
|
||||||
|
? 'icon-[lucide--list-tree]'
|
||||||
|
: 'icon-[lucide--list-collapse]'
|
||||||
|
)
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Transition>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import { cn } from '@/utils/tailwindUtil'
|
||||||
|
import Button from '@/components/ui/button/Button.vue'
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
|
||||||
|
const isAllCollapsed = defineModel<boolean>({ required: true })
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
show: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
|
function toggle() {
|
||||||
|
isAllCollapsed.value = !isAllCollapsed.value
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -108,14 +108,22 @@ onMounted(() => {
|
|||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
draggableList.value?.dispose()
|
draggableList.value?.dispose()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
function onCollapseUpdate() {
|
||||||
|
// Rebuild draggable list after the section header is toggled
|
||||||
|
nextTick(setDraggableState)
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="px-4 pt-1 pb-4 flex gap-2 border-b border-interface-stroke">
|
<div
|
||||||
|
class="px-4 pt-1 pb-4 flex items-center border-b border-interface-stroke"
|
||||||
|
>
|
||||||
<FormSearchInput
|
<FormSearchInput
|
||||||
v-model="searchQuery"
|
v-model="searchQuery"
|
||||||
:searcher
|
:searcher
|
||||||
:update-key="favoritedWidgets"
|
:update-key="favoritedWidgets"
|
||||||
|
class="flex-1"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<SectionWidgets
|
<SectionWidgets
|
||||||
@@ -127,7 +135,7 @@ onBeforeUnmount(() => {
|
|||||||
show-node-name
|
show-node-name
|
||||||
enable-empty-state
|
enable-empty-state
|
||||||
class="border-b border-interface-stroke"
|
class="border-b border-interface-stroke"
|
||||||
@update:collapse="nextTick(setDraggableState)"
|
@update:collapse="onCollapseUpdate"
|
||||||
>
|
>
|
||||||
<template #empty>
|
<template #empty>
|
||||||
<div class="text-sm text-muted-foreground px-4 text-center py-10">
|
<div class="text-sm text-muted-foreground px-4 text-center py-10">
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { storeToRefs } from 'pinia'
|
import { storeToRefs } from 'pinia'
|
||||||
import { computed, ref, shallowRef } from 'vue'
|
import { computed, reactive, ref, shallowRef, watch } from 'vue'
|
||||||
|
|
||||||
|
import CollapseToggleButton from '@/components/rightSidePanel/layout/CollapseToggleButton.vue'
|
||||||
|
|
||||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||||
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
|
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
|
||||||
@@ -30,6 +32,40 @@ const searchedWidgetsSectionDataList = shallowRef<NodeWidgetsListList>(
|
|||||||
widgetsSectionDataList.value
|
widgetsSectionDataList.value
|
||||||
)
|
)
|
||||||
const isSearching = ref(false)
|
const isSearching = ref(false)
|
||||||
|
|
||||||
|
const collapseMap = reactive<Record<string, boolean>>({})
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => workflowStore.activeWorkflow?.path,
|
||||||
|
() => {
|
||||||
|
for (const key of Object.keys(collapseMap)) {
|
||||||
|
delete collapseMap[key]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
function isSectionCollapsed(nodeId: string): boolean {
|
||||||
|
// Defaults to collapsed when not explicitly set by the user
|
||||||
|
return collapseMap[nodeId] ?? true
|
||||||
|
}
|
||||||
|
|
||||||
|
function setSectionCollapsed(nodeId: string, collapsed: boolean) {
|
||||||
|
collapseMap[nodeId] = collapsed
|
||||||
|
}
|
||||||
|
|
||||||
|
const isAllCollapsed = computed({
|
||||||
|
get() {
|
||||||
|
return searchedWidgetsSectionDataList.value.every(({ node }) =>
|
||||||
|
isSectionCollapsed(String(node.id))
|
||||||
|
)
|
||||||
|
},
|
||||||
|
set(collapse: boolean) {
|
||||||
|
for (const { node } of widgetsSectionDataList.value) {
|
||||||
|
setSectionCollapsed(String(node.id), collapse)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
async function searcher(query: string) {
|
async function searcher(query: string) {
|
||||||
const list = widgetsSectionDataList.value
|
const list = widgetsSectionDataList.value
|
||||||
const target = searchedWidgetsSectionDataList
|
const target = searchedWidgetsSectionDataList
|
||||||
@@ -39,11 +75,18 @@ async function searcher(query: string) {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="px-4 pt-1 pb-4 flex gap-2 border-b border-interface-stroke">
|
<div
|
||||||
|
class="px-4 pt-1 pb-4 flex items-center border-b border-interface-stroke"
|
||||||
|
>
|
||||||
<FormSearchInput
|
<FormSearchInput
|
||||||
v-model="searchQuery"
|
v-model="searchQuery"
|
||||||
:searcher
|
:searcher
|
||||||
:update-key="widgetsSectionDataList"
|
:update-key="widgetsSectionDataList"
|
||||||
|
class="flex-1"
|
||||||
|
/>
|
||||||
|
<CollapseToggleButton
|
||||||
|
v-model="isAllCollapsed"
|
||||||
|
:show="!isSearching && widgetsSectionDataList.length > 1"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<TransitionGroup tag="div" name="list-scale" class="relative">
|
<TransitionGroup tag="div" name="list-scale" class="relative">
|
||||||
@@ -58,7 +101,7 @@ async function searcher(query: string) {
|
|||||||
:key="node.id"
|
:key="node.id"
|
||||||
:node
|
:node
|
||||||
:widgets
|
:widgets
|
||||||
:collapse="!isSearching"
|
:collapse="isSectionCollapsed(String(node.id)) && !isSearching"
|
||||||
:tooltip="
|
:tooltip="
|
||||||
isSearching || widgets.length
|
isSearching || widgets.length
|
||||||
? ''
|
? ''
|
||||||
@@ -66,6 +109,7 @@ async function searcher(query: string) {
|
|||||||
"
|
"
|
||||||
show-locate-button
|
show-locate-button
|
||||||
class="border-b border-interface-stroke"
|
class="border-b border-interface-stroke"
|
||||||
|
@update:collapse="setSectionCollapsed(String(node.id), $event)"
|
||||||
/>
|
/>
|
||||||
</TransitionGroup>
|
</TransitionGroup>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { storeToRefs } from 'pinia'
|
import { storeToRefs } from 'pinia'
|
||||||
import { computed, ref, shallowRef } from 'vue'
|
import { computed, reactive, ref, shallowRef, watch } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||||
|
import CollapseToggleButton from '@/components/rightSidePanel/layout/CollapseToggleButton.vue'
|
||||||
import FormSearchInput from '@/renderer/extensions/vueNodes/widgets/components/form/FormSearchInput.vue'
|
import FormSearchInput from '@/renderer/extensions/vueNodes/widgets/components/form/FormSearchInput.vue'
|
||||||
|
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
|
||||||
import { useRightSidePanelStore } from '@/stores/workspace/rightSidePanelStore'
|
import { useRightSidePanelStore } from '@/stores/workspace/rightSidePanelStore'
|
||||||
|
|
||||||
import { computedSectionDataList, searchWidgetsAndNodes } from '../shared'
|
import { computedSectionDataList, searchWidgetsAndNodes } from '../shared'
|
||||||
@@ -17,6 +19,7 @@ const { nodes, mustShowNodeTitle } = defineProps<{
|
|||||||
}>()
|
}>()
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
const workflowStore = useWorkflowStore()
|
||||||
|
|
||||||
const rightSidePanelStore = useRightSidePanelStore()
|
const rightSidePanelStore = useRightSidePanelStore()
|
||||||
const { searchQuery } = storeToRefs(rightSidePanelStore)
|
const { searchQuery } = storeToRefs(rightSidePanelStore)
|
||||||
@@ -52,6 +55,46 @@ const searchedWidgetsSectionDataList = shallowRef<NodeWidgetsListList>(
|
|||||||
)
|
)
|
||||||
const isSearching = ref(false)
|
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: string): boolean {
|
||||||
|
// When not explicitly set, sections are collapsed if multiple nodes are selected
|
||||||
|
return collapseMap[nodeId] ?? isMultipleNodesSelected.value
|
||||||
|
}
|
||||||
|
|
||||||
|
function setSectionCollapsed(nodeId: string, collapsed: boolean) {
|
||||||
|
collapseMap[nodeId] = collapsed
|
||||||
|
}
|
||||||
|
|
||||||
|
const isAllCollapsed = computed({
|
||||||
|
get() {
|
||||||
|
const normalAllCollapsed = searchedWidgetsSectionDataList.value.every(
|
||||||
|
({ node }) => isSectionCollapsed(String(node.id))
|
||||||
|
)
|
||||||
|
const hasAdvanced = advancedWidgetsSectionDataList.value.length > 0
|
||||||
|
return hasAdvanced
|
||||||
|
? normalAllCollapsed && advancedCollapsed.value
|
||||||
|
: normalAllCollapsed
|
||||||
|
},
|
||||||
|
set(collapse: boolean) {
|
||||||
|
for (const { node } of widgetsSectionDataList.value) {
|
||||||
|
setSectionCollapsed(String(node.id), collapse)
|
||||||
|
}
|
||||||
|
advancedCollapsed.value = collapse
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
async function searcher(query: string) {
|
async function searcher(query: string) {
|
||||||
const list = widgetsSectionDataList.value
|
const list = widgetsSectionDataList.value
|
||||||
const target = searchedWidgetsSectionDataList
|
const target = searchedWidgetsSectionDataList
|
||||||
@@ -76,11 +119,22 @@ const advancedLabel = computed(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="px-4 pt-1 pb-4 flex gap-2 border-b border-interface-stroke">
|
<div
|
||||||
|
class="px-4 pt-1 pb-4 flex items-center border-b border-interface-stroke"
|
||||||
|
>
|
||||||
<FormSearchInput
|
<FormSearchInput
|
||||||
v-model="searchQuery"
|
v-model="searchQuery"
|
||||||
:searcher
|
:searcher
|
||||||
:update-key="widgetsSectionDataList"
|
:update-key="widgetsSectionDataList"
|
||||||
|
class="flex-1"
|
||||||
|
/>
|
||||||
|
<CollapseToggleButton
|
||||||
|
v-model="isAllCollapsed"
|
||||||
|
:show="
|
||||||
|
!isSearching &&
|
||||||
|
widgetsSectionDataList.length + advancedWidgetsSectionDataList.length >
|
||||||
|
1
|
||||||
|
"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<TransitionGroup tag="div" name="list-scale" class="relative">
|
<TransitionGroup tag="div" name="list-scale" class="relative">
|
||||||
@@ -100,7 +154,7 @@ const advancedLabel = computed(() => {
|
|||||||
:node
|
:node
|
||||||
:label
|
:label
|
||||||
:widgets
|
:widgets
|
||||||
:collapse="isMultipleNodesSelected && !isSearching"
|
:collapse="isSectionCollapsed(String(node.id)) && !isSearching"
|
||||||
:show-locate-button="isMultipleNodesSelected"
|
:show-locate-button="isMultipleNodesSelected"
|
||||||
:tooltip="
|
:tooltip="
|
||||||
isSearching || widgets.length
|
isSearching || widgets.length
|
||||||
@@ -108,13 +162,14 @@ const advancedLabel = computed(() => {
|
|||||||
: t('rightSidePanel.inputsNoneTooltip')
|
: t('rightSidePanel.inputsNoneTooltip')
|
||||||
"
|
"
|
||||||
class="border-b border-interface-stroke"
|
class="border-b border-interface-stroke"
|
||||||
|
@update:collapse="setSectionCollapsed(String(node.id), $event)"
|
||||||
/>
|
/>
|
||||||
</TransitionGroup>
|
</TransitionGroup>
|
||||||
<template v-if="advancedWidgetsSectionDataList.length > 0 && !isSearching">
|
<template v-if="advancedWidgetsSectionDataList.length > 0 && !isSearching">
|
||||||
<SectionWidgets
|
<SectionWidgets
|
||||||
v-for="{ widgets, node } in advancedWidgetsSectionDataList"
|
v-for="{ widgets, node } in advancedWidgetsSectionDataList"
|
||||||
:key="`advanced-${node.id}`"
|
:key="`advanced-${node.id}`"
|
||||||
:collapse="true"
|
v-model:collapse="advancedCollapsed"
|
||||||
:node
|
:node
|
||||||
:label="advancedLabel"
|
:label="advancedLabel"
|
||||||
:widgets
|
:widgets
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import { isPromotedWidgetView } from '@/core/graph/subgraph/promotedWidgetTypes'
|
|||||||
import type { SubgraphNode } from '@/lib/litegraph/src/subgraph/SubgraphNode'
|
import type { SubgraphNode } from '@/lib/litegraph/src/subgraph/SubgraphNode'
|
||||||
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||||
import FormSearchInput from '@/renderer/extensions/vueNodes/widgets/components/form/FormSearchInput.vue'
|
import FormSearchInput from '@/renderer/extensions/vueNodes/widgets/components/form/FormSearchInput.vue'
|
||||||
|
import CollapseToggleButton from '@/components/rightSidePanel/layout/CollapseToggleButton.vue'
|
||||||
import { DraggableList } from '@/scripts/ui/draggableList'
|
import { DraggableList } from '@/scripts/ui/draggableList'
|
||||||
import { usePromotionStore } from '@/stores/promotionStore'
|
import { usePromotionStore } from '@/stores/promotionStore'
|
||||||
import { useRightSidePanelStore } from '@/stores/workspace/rightSidePanelStore'
|
import { useRightSidePanelStore } from '@/stores/workspace/rightSidePanelStore'
|
||||||
@@ -36,6 +37,19 @@ const rightSidePanelStore = useRightSidePanelStore()
|
|||||||
const { focusedSection, searchQuery } = storeToRefs(rightSidePanelStore)
|
const { focusedSection, searchQuery } = storeToRefs(rightSidePanelStore)
|
||||||
|
|
||||||
const advancedInputsCollapsed = ref(true)
|
const advancedInputsCollapsed = ref(true)
|
||||||
|
const firstSectionCollapsed = ref(false)
|
||||||
|
const isAllCollapsed = computed({
|
||||||
|
get() {
|
||||||
|
const hasAdvanced = advancedInputsWidgets.value.length > 0
|
||||||
|
return hasAdvanced
|
||||||
|
? firstSectionCollapsed.value && advancedInputsCollapsed.value
|
||||||
|
: firstSectionCollapsed.value
|
||||||
|
},
|
||||||
|
set(collapse: boolean) {
|
||||||
|
firstSectionCollapsed.value = collapse
|
||||||
|
advancedInputsCollapsed.value = collapse
|
||||||
|
}
|
||||||
|
})
|
||||||
const draggableList = ref<DraggableList | undefined>(undefined)
|
const draggableList = ref<DraggableList | undefined>(undefined)
|
||||||
const sectionWidgetsRef = useTemplateRef('sectionWidgetsRef')
|
const sectionWidgetsRef = useTemplateRef('sectionWidgetsRef')
|
||||||
const advancedInputsSectionRef = useTemplateRef('advancedInputsSectionRef')
|
const advancedInputsSectionRef = useTemplateRef('advancedInputsSectionRef')
|
||||||
@@ -186,15 +200,23 @@ const label = computed(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="px-4 pt-1 pb-4 flex gap-2 border-b border-interface-stroke">
|
<div
|
||||||
|
class="px-4 pt-1 pb-4 flex items-center border-b border-interface-stroke"
|
||||||
|
>
|
||||||
<FormSearchInput
|
<FormSearchInput
|
||||||
v-model="searchQuery"
|
v-model="searchQuery"
|
||||||
:searcher
|
:searcher
|
||||||
:update-key="widgetsList"
|
:update-key="widgetsList"
|
||||||
|
class="flex-1"
|
||||||
|
/>
|
||||||
|
<CollapseToggleButton
|
||||||
|
v-model="isAllCollapsed"
|
||||||
|
:show="!isSearching && advancedInputsWidgets.length > 0"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<SectionWidgets
|
<SectionWidgets
|
||||||
ref="sectionWidgetsRef"
|
ref="sectionWidgetsRef"
|
||||||
|
:collapse="firstSectionCollapsed && !isSearching"
|
||||||
:node
|
:node
|
||||||
:label
|
:label
|
||||||
:parents
|
:parents
|
||||||
@@ -207,7 +229,12 @@ const label = computed(() => {
|
|||||||
: t('rightSidePanel.inputsNoneTooltip')
|
: t('rightSidePanel.inputsNoneTooltip')
|
||||||
"
|
"
|
||||||
class="border-b border-interface-stroke"
|
class="border-b border-interface-stroke"
|
||||||
@update:collapse="nextTick(setDraggableState)"
|
@update:collapse="
|
||||||
|
(v) => {
|
||||||
|
firstSectionCollapsed = v
|
||||||
|
nextTick(setDraggableState)
|
||||||
|
}
|
||||||
|
"
|
||||||
>
|
>
|
||||||
<template #empty>
|
<template #empty>
|
||||||
<div class="text-sm text-muted-foreground px-4 text-center pt-5 pb-15">
|
<div class="text-sm text-muted-foreground px-4 text-center pt-5 pb-15">
|
||||||
|
|||||||
@@ -305,7 +305,9 @@
|
|||||||
"profile": "Profile",
|
"profile": "Profile",
|
||||||
"noItems": "No items",
|
"noItems": "No items",
|
||||||
"recents": "Recents",
|
"recents": "Recents",
|
||||||
"partner": "Partner"
|
"partner": "Partner",
|
||||||
|
"collapseAll": "Collapse all",
|
||||||
|
"expandAll": "Expand all"
|
||||||
},
|
},
|
||||||
"manager": {
|
"manager": {
|
||||||
"title": "Nodes Manager",
|
"title": "Nodes Manager",
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ watch(
|
|||||||
cleanupFn?.()
|
cleanupFn?.()
|
||||||
})
|
})
|
||||||
|
|
||||||
void searcher(debouncedSearchQuery.value, (cb) => (cleanupFn = cb))
|
searcher(debouncedSearchQuery.value, (cb) => (cleanupFn = cb))
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('[SidePanelSearch] searcher failed', error)
|
console.error('[SidePanelSearch] searcher failed', error)
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user