Files
ComfyUI_frontend/src/components/rightSidePanel/RightSidePanel.vue
Rizumu Ayaka b1b2fd8a4f feat: right side panel favorites, no selection state, and more... (#7812)
Most of the features in this pull request are completed and can be
reviewed and merged.

## TODO

- [x] no selection panel
- [x] group selected panel
- [x] tabs 
  - [x] favorites tab
  - [x] global settings tab
  - [x] nodes tab
- [x] widget actions menu 
  - [x] [Bug]: style bugs
- [x] button zoom to the node on canvas.
- [x] rename widgets on widget actions
  - [ ] [Bug]: the canvas has not been updated after renaming. 
- [x] global settings
  - [ ] setting item: "show advanced parameters"
    - blocked by other things. skip for now.
  - [x] setting item: show toolbox on selection 
  - [x] setting item: nodes 2.0
  - [ ] setting item: "background color"
    - blocked by other things. skip for now.
  - [x] setting item: grid spacing
  - [x] setting item: snap nodes to grid
  - [x] setting item: link shape
  - [x] setting item: show connected links
  - [x] form style reuses the form style of node widgets
- [x] group node cases
  - [x] group node settings
  - [x] show all nodes in group
  - [x] show frame name on nodes when multiple selections are made
  - [x] group multiple selections
- [x] [Bug]: nodes without widgets cannot display the location and their
group
  - [x] [Bug]: labels layout
- [x] favorites
  - [x] the indicator on widgets
  - [x] favorite and unfavorite buttons on widgets
- [x] [Bug]: show node name in favorite widgets + improve labels layout
- [ ] [Bug]: After canceling the like, the like list will not be updated
immediately.
- [x] [Bug]: The favorite function does not work for the project on
Subgraph.
- [x] subgraph
- [x] add the node name from where this parameter comes from when node
is subgraph
  - [x] show and hide directly on Inputs
    - [x] some bugs need to be fixed.
- [x] advanced widgets 
  - [x] button: show advanced inputs
- Clicking button expands the "Advanced Inputs" section on the right
side panel, regardless of whether the panel is open or not
    - [x] [Bug]: style bugs
  - [x] advanced inputs section when node is subgraph
- [x] inputs tab rearranging
  - [x] favorited inputs rearranging
  - [x] subgraph inputs rearranging
- [ ] review and reconstruction to improve complexity and architecture

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7812-feat-right-side-panel-favorites-no-selection-state-and-more-2da6d73d36508134b503d676f9b3d248)
by [Unito](https://www.unito.io)

---------

Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: bymyself <cbyrne@comfy.org>
2026-01-13 20:37:17 -07:00

296 lines
8.8 KiB
Vue

<script setup lang="ts">
import { storeToRefs } from 'pinia'
import { computed, provide, ref, watchEffect } from 'vue'
import { useI18n } from 'vue-i18n'
import EditableText from '@/components/common/EditableText.vue'
import Tab from '@/components/tab/Tab.vue'
import TabList from '@/components/tab/TabList.vue'
import Button from '@/components/ui/button/Button.vue'
import { useGraphHierarchy } from '@/composables/graph/useGraphHierarchy'
import { SubgraphNode } from '@/lib/litegraph/src/litegraph'
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
import { useSettingStore } from '@/platform/settings/settingStore'
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
import { useRightSidePanelStore } from '@/stores/workspace/rightSidePanelStore'
import type { RightSidePanelTab } from '@/stores/workspace/rightSidePanelStore'
import { cn } from '@/utils/tailwindUtil'
import TabInfo from './info/TabInfo.vue'
import TabGlobalParameters from './parameters/TabGlobalParameters.vue'
import TabNodes from './parameters/TabNodes.vue'
import TabNormalInputs from './parameters/TabNormalInputs.vue'
import TabSubgraphInputs from './parameters/TabSubgraphInputs.vue'
import TabGlobalSettings from './settings/TabGlobalSettings.vue'
import TabSettings from './settings/TabSettings.vue'
import {
GetNodeParentGroupKey,
useFlatAndCategorizeSelectedItems
} from './shared'
import SubgraphEditor from './subgraph/SubgraphEditor.vue'
const canvasStore = useCanvasStore()
const rightSidePanelStore = useRightSidePanelStore()
const settingStore = useSettingStore()
const { t } = useI18n()
const { findParentGroup } = useGraphHierarchy()
const { selectedItems: directlySelectedItems } = storeToRefs(canvasStore)
const { activeTab, isEditingSubgraph } = storeToRefs(rightSidePanelStore)
const sidebarLocation = computed<'left' | 'right'>(() =>
settingStore.get('Comfy.Sidebar.Location')
)
// Panel is on the left when sidebar is on the right, and vice versa
const panelIcon = computed(() =>
sidebarLocation.value === 'right'
? 'icon-[lucide--panel-left]'
: 'icon-[lucide--panel-right]'
)
const { flattedItems, selectedNodes, selectedGroups, nodeToParentGroup } =
useFlatAndCategorizeSelectedItems(directlySelectedItems)
const shouldShowGroupNames = computed(() => {
return !(
directlySelectedItems.value.length === 1 &&
(selectedGroups.value.length === 1 || selectedNodes.value.length === 1)
)
})
provide(GetNodeParentGroupKey, (node: LGraphNode) => {
if (!shouldShowGroupNames.value) return null
return nodeToParentGroup.value.get(node) ?? findParentGroup(node)
})
const hasSelection = computed(() => flattedItems.value.length > 0)
const selectedSingleNode = computed(() => {
return selectedNodes.value.length === 1 && flattedItems.value.length === 1
? selectedNodes.value[0]
: null
})
const isSingleSubgraphNode = computed(() => {
return selectedSingleNode.value instanceof SubgraphNode
})
function closePanel() {
rightSidePanelStore.closePanel()
}
type RightSidePanelTabList = Array<{
label: () => string
value: RightSidePanelTab
}>
const tabs = computed<RightSidePanelTabList>(() => {
const list: RightSidePanelTabList = []
list.push({
label: () =>
flattedItems.value.length > 1
? t('rightSidePanel.nodes')
: t('rightSidePanel.parameters'),
value: 'parameters'
})
if (!hasSelection.value) {
list.push({
label: () => t('rightSidePanel.nodes'),
value: 'nodes'
})
}
if (hasSelection.value) {
if (selectedSingleNode.value && !isSingleSubgraphNode.value) {
list.push({
label: () => t('rightSidePanel.info'),
value: 'info'
})
}
}
list.push({
label: () =>
hasSelection.value
? t('g.settings')
: t('rightSidePanel.globalSettings.title'),
value: 'settings'
})
return list
})
// Use global state for activeTab and ensure it's valid
watchEffect(() => {
if (
!tabs.value.some((tab) => tab.value === activeTab.value) &&
!(activeTab.value === 'subgraph' && isSingleSubgraphNode.value)
) {
rightSidePanelStore.openPanel(tabs.value[0].value)
}
})
function resolveTitle() {
const items = flattedItems.value
const nodes = selectedNodes.value
const groups = selectedGroups.value
if (items.length === 0) {
return t('rightSidePanel.workflowOverview')
}
if (directlySelectedItems.value.length === 1) {
if (groups.length === 1) {
return groups[0].title || t('rightSidePanel.fallbackGroupTitle')
}
if (nodes.length === 1) {
return (
nodes[0].title || nodes[0].type || t('rightSidePanel.fallbackNodeTitle')
)
}
}
return t('rightSidePanel.title', { count: items.length })
}
const panelTitle = ref(resolveTitle())
watchEffect(() => (panelTitle.value = resolveTitle()))
const isEditing = ref(false)
const allowTitleEdit = computed(() => {
return (
directlySelectedItems.value.length === 1 &&
(selectedGroups.value.length === 1 || selectedNodes.value.length === 1)
)
})
function handleTitleEdit(newTitle: string) {
isEditing.value = false
const trimmedTitle = newTitle.trim()
if (!trimmedTitle) return
const node = selectedGroups.value[0] || selectedNodes.value[0]
if (!node) return
if (trimmedTitle === node.title) return
node.title = trimmedTitle
panelTitle.value = trimmedTitle
canvasStore.canvas?.setDirty(true, true)
}
function handleTitleCancel() {
isEditing.value = false
}
</script>
<template>
<div
data-testid="properties-panel"
class="flex size-full flex-col bg-comfy-menu-bg"
>
<!-- Panel Header -->
<section class="pt-1">
<div class="flex items-center justify-between pl-4 pr-3">
<h3 class="my-3.5 text-sm font-semibold line-clamp-2 cursor-default">
<template v-if="allowTitleEdit">
<EditableText
:model-value="panelTitle"
:is-editing="isEditing"
:input-attrs="{ 'data-testid': 'node-title-input' }"
class="cursor-text"
@edit="handleTitleEdit"
@cancel="handleTitleCancel"
@click="isEditing = true"
/>
<i
v-if="!isEditing"
class="icon-[lucide--pencil] size-4 text-muted-foreground ml-2 content-center relative top-[2px] hover:text-base-foreground cursor-pointer shrink-0"
@click="isEditing = true"
/>
</template>
<template v-else>
{{ panelTitle }}
</template>
</h3>
<div class="flex gap-2">
<Button
v-if="isSingleSubgraphNode"
variant="secondary"
size="icon"
:class="cn(isEditingSubgraph && 'bg-secondary-background-selected')"
@click="
rightSidePanelStore.openPanel(
isEditingSubgraph ? 'parameters' : 'subgraph'
)
"
>
<i class="icon-[lucide--settings-2]" />
</Button>
<Button
variant="secondary"
size="icon"
:aria-pressed="rightSidePanelStore.isOpen"
:aria-label="t('rightSidePanel.togglePanel')"
@click="closePanel"
>
<i :class="cn(panelIcon, 'size-4')" />
</Button>
</div>
</div>
<nav class="px-4 pb-2 pt-1 overflow-x-auto">
<TabList
:model-value="activeTab"
@update:model-value="
(newTab: RightSidePanelTab) => {
rightSidePanelStore.openPanel(newTab)
}
"
>
<Tab
v-for="tab in tabs"
:key="tab.value"
class="text-sm py-1 px-2 font-inter transition-all active:scale-95"
:value="tab.value"
>
{{ tab.label() }}
</Tab>
</TabList>
</nav>
</section>
<!-- Panel Content -->
<div class="scrollbar-thin flex-1 overflow-y-auto">
<template v-if="!hasSelection">
<TabGlobalParameters v-if="activeTab === 'parameters'" />
<TabNodes v-else-if="activeTab === 'nodes'" />
<TabGlobalSettings v-else-if="activeTab === 'settings'" />
</template>
<SubgraphEditor
v-else-if="isSingleSubgraphNode && isEditingSubgraph"
:node="selectedSingleNode"
/>
<template v-else>
<TabSubgraphInputs
v-if="activeTab === 'parameters' && isSingleSubgraphNode"
:node="selectedSingleNode as SubgraphNode"
/>
<TabNormalInputs
v-else-if="activeTab === 'parameters'"
:nodes="selectedNodes"
:must-show-node-title="selectedGroups.length > 0"
/>
<TabInfo v-else-if="activeTab === 'info'" :nodes="selectedNodes" />
<TabSettings
v-else-if="activeTab === 'settings'"
:nodes="flattedItems"
/>
</template>
</div>
</div>
</template>