mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-01 22:09:55 +00:00
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>
This commit is contained in:
@@ -119,6 +119,23 @@
|
||||
<div v-if="shouldShowPreviewImg" class="min-h-0 flex-1 px-4">
|
||||
<LivePreview :image-url="latestPreviewUrl || null" />
|
||||
</div>
|
||||
|
||||
<!-- Show advanced inputs button for subgraph nodes -->
|
||||
<div v-if="showAdvancedInputsButton" class="flex justify-center px-3">
|
||||
<button
|
||||
:class="
|
||||
cn(
|
||||
WidgetInputBaseClass,
|
||||
'w-full h-7 flex justify-center items-center gap-2 text-sm px-3 outline-0 ring-0 truncate',
|
||||
'transition-all cursor-pointer hover:bg-accent-background duration-150 active:scale-95'
|
||||
)
|
||||
"
|
||||
@click.stop="handleShowAdvancedInputs"
|
||||
>
|
||||
<i class="icon-[lucide--settings-2] size-4" />
|
||||
<span>{{ t('rightSidePanel.showAdvancedInputsButton') }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -148,6 +165,7 @@ import {
|
||||
LiteGraph,
|
||||
RenderShape
|
||||
} from '@/lib/litegraph/src/litegraph'
|
||||
import { SubgraphNode } from '@/lib/litegraph/src/subgraph/SubgraphNode'
|
||||
import { TitleMode } from '@/lib/litegraph/src/types/globalEnums'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { useTelemetry } from '@/platform/telemetry'
|
||||
@@ -169,6 +187,7 @@ import { app } from '@/scripts/app'
|
||||
import { useExecutionStore } from '@/stores/executionStore'
|
||||
import { useNodeOutputStore } from '@/stores/imagePreviewStore'
|
||||
import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
|
||||
import { useRightSidePanelStore } from '@/stores/workspace/rightSidePanelStore'
|
||||
import { isTransparent } from '@/utils/colorUtil'
|
||||
import {
|
||||
getLocatorIdFromNodeData,
|
||||
@@ -177,6 +196,7 @@ import {
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
import { useNodeResize } from '../interactions/resize/useNodeResize'
|
||||
import { WidgetInputBaseClass } from '../widgets/components/layout'
|
||||
import LivePreview from './LivePreview.vue'
|
||||
import NodeContent from './NodeContent.vue'
|
||||
import NodeHeader from './NodeHeader.vue'
|
||||
@@ -474,6 +494,22 @@ const lgraphNode = computed(() => {
|
||||
return getNodeByLocatorId(app.rootGraph, locatorId)
|
||||
})
|
||||
|
||||
const showAdvancedInputsButton = computed(() => {
|
||||
const node = lgraphNode.value
|
||||
if (!node || !(node instanceof SubgraphNode)) return false
|
||||
|
||||
// Check if there are hidden inputs (widgets not promoted)
|
||||
const interiorNodes = node.subgraph.nodes
|
||||
const allInteriorWidgets = interiorNodes.flatMap((n) => n.widgets ?? [])
|
||||
|
||||
return allInteriorWidgets.some((w) => !w.computedDisabled && !w.promoted)
|
||||
})
|
||||
|
||||
function handleShowAdvancedInputs() {
|
||||
const rightSidePanelStore = useRightSidePanelStore()
|
||||
rightSidePanelStore.focusSection('advanced-inputs')
|
||||
}
|
||||
|
||||
const nodeMedia = computed(() => {
|
||||
const newOutputs = nodeOutputs.nodeOutputs[nodeOutputLocatorId.value]
|
||||
const node = lgraphNode.value
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
<script setup lang="ts">
|
||||
import { refDebounced } from '@vueuse/core'
|
||||
import { ref, toRef, toValue, watch } from 'vue'
|
||||
import type { HTMLAttributes, MaybeRefOrGetter } from 'vue'
|
||||
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
const {
|
||||
searcher = async () => {},
|
||||
updateKey,
|
||||
autofocus = false,
|
||||
class: customClass
|
||||
} = defineProps<{
|
||||
searcher?: (
|
||||
query: string,
|
||||
onCleanup: (cleanupFn: () => void) => void
|
||||
) => Promise<void>
|
||||
updateKey?: MaybeRefOrGetter<unknown>
|
||||
autofocus?: boolean
|
||||
class?: HTMLAttributes['class']
|
||||
}>()
|
||||
|
||||
const searchQuery = defineModel<string>({ default: '' })
|
||||
|
||||
const isQuerying = ref(false)
|
||||
const debouncedSearchQuery = refDebounced(searchQuery, 250, {
|
||||
maxWait: 1000
|
||||
})
|
||||
watch(searchQuery, (value) => {
|
||||
isQuerying.value = value !== debouncedSearchQuery.value
|
||||
})
|
||||
const updateKeyRef = toRef(() => toValue(updateKey))
|
||||
|
||||
watch(
|
||||
[debouncedSearchQuery, updateKeyRef],
|
||||
(_, __, onCleanup) => {
|
||||
let isCleanup = false
|
||||
let cleanupFn: undefined | (() => void)
|
||||
onCleanup(() => {
|
||||
isCleanup = true
|
||||
cleanupFn?.()
|
||||
})
|
||||
|
||||
void searcher(debouncedSearchQuery.value, (cb) => (cleanupFn = cb))
|
||||
.catch((error) => {
|
||||
console.error('[SidePanelSearch] searcher failed', error)
|
||||
})
|
||||
.finally(() => {
|
||||
if (!isCleanup) isQuerying.value = false
|
||||
})
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
function handleFocus(event: FocusEvent) {
|
||||
const target = event.target as HTMLInputElement
|
||||
target.select()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<label
|
||||
:class="
|
||||
cn(
|
||||
'group',
|
||||
'bg-component-node-widget-background rounded-lg transition-all duration-150',
|
||||
'flex-1 flex items-center',
|
||||
'text-base-foreground border-0',
|
||||
'focus-within:ring focus-within:ring-component-node-widget-background-highlighted/80',
|
||||
customClass
|
||||
)
|
||||
"
|
||||
>
|
||||
<i
|
||||
:class="
|
||||
cn(
|
||||
'size-4 ml-2 shrink-0 transition-colors duration-150',
|
||||
isQuerying
|
||||
? 'icon-[lucide--loader-circle] animate-spin'
|
||||
: 'icon-[lucide--search]',
|
||||
searchQuery?.trim() !== ''
|
||||
? 'text-base-foreground'
|
||||
: 'text-muted-foreground group-hover:text-base-foreground group-focus-within:text-base-foreground'
|
||||
)
|
||||
"
|
||||
/>
|
||||
<input
|
||||
v-model="searchQuery"
|
||||
type="text"
|
||||
class="bg-transparent border-0 outline-0 ring-0 h-5 w-full my-1.5 mx-2"
|
||||
:placeholder="$t('g.searchPlaceholder')"
|
||||
:autofocus
|
||||
@focus="handleFocus"
|
||||
/>
|
||||
<button
|
||||
v-if="searchQuery.trim().length > 0"
|
||||
class="text-muted-foreground hover:text-base-foreground bg-transparent shrink-0 border-0 outline-0 ring-0 p-0 m-0 pr-3 pl-1 flex items-center justify-center transition-all duration-150 hover:scale-108"
|
||||
:aria-label="$t('g.clear')"
|
||||
@click="searchQuery = ''"
|
||||
>
|
||||
<i :class="cn('icon-[lucide--delete] size-4 cursor-pointer')" />
|
||||
</button>
|
||||
</label>
|
||||
</template>
|
||||
@@ -1,7 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { refDebounced } from '@vueuse/core'
|
||||
import Popover from 'primevue/popover'
|
||||
import { computed, ref, useTemplateRef, watch } from 'vue'
|
||||
import { computed, ref, useTemplateRef } from 'vue'
|
||||
|
||||
import { t } from '@/i18n'
|
||||
import { useToastStore } from '@/platform/updates/common/toastStore'
|
||||
@@ -68,10 +67,6 @@ const layoutMode = defineModel<LayoutMode>('layoutMode', {
|
||||
const files = defineModel<File[]>('files', { default: [] })
|
||||
const searchQuery = defineModel<string>('searchQuery', { default: '' })
|
||||
|
||||
const debouncedSearchQuery = refDebounced(searchQuery, 700, {
|
||||
maxWait: 700
|
||||
})
|
||||
const isQuerying = ref(false)
|
||||
const toastStore = useToastStore()
|
||||
const popoverRef = ref<InstanceType<typeof Popover>>()
|
||||
const triggerRef = useTemplateRef('triggerRef')
|
||||
@@ -85,36 +80,6 @@ const maxSelectable = computed(() => {
|
||||
|
||||
const filteredItems = ref<DropdownItem[]>([])
|
||||
|
||||
watch(searchQuery, (value) => {
|
||||
isQuerying.value = value !== debouncedSearchQuery.value
|
||||
})
|
||||
|
||||
watch(
|
||||
[debouncedSearchQuery, () => props.items],
|
||||
(_, __, onCleanup) => {
|
||||
let isCleanup = false
|
||||
let cleanupFn: undefined | (() => void)
|
||||
onCleanup(() => {
|
||||
isCleanup = true
|
||||
cleanupFn?.()
|
||||
})
|
||||
|
||||
void props
|
||||
.searcher(
|
||||
debouncedSearchQuery.value,
|
||||
props.items,
|
||||
(cb) => (cleanupFn = cb)
|
||||
)
|
||||
.then((result) => {
|
||||
if (!isCleanup) filteredItems.value = result
|
||||
})
|
||||
.finally(() => {
|
||||
if (!isCleanup) isQuerying.value = false
|
||||
})
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
const defaultSorter = computed<SortOption['sorter']>(() => {
|
||||
const sorter = props.sortOptions.find(
|
||||
(option) => option.id === 'default'
|
||||
@@ -183,6 +148,23 @@ function handleSelection(item: DropdownItem, index: number) {
|
||||
closeDropdown()
|
||||
}
|
||||
}
|
||||
|
||||
async function customSearcher(
|
||||
query: string,
|
||||
onCleanup: (cleanupFn: () => void) => void
|
||||
) {
|
||||
let isCleanup = false
|
||||
let cleanupFn: undefined | (() => void)
|
||||
onCleanup(() => {
|
||||
isCleanup = true
|
||||
cleanupFn?.()
|
||||
})
|
||||
await props
|
||||
.searcher(query, props.items, (cb) => (cleanupFn = cb))
|
||||
.then((results) => {
|
||||
if (!isCleanup) filteredItems.value = results
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -223,7 +205,7 @@ function handleSelection(item: DropdownItem, index: number) {
|
||||
:filter-options="filterOptions"
|
||||
:sort-options="sortOptions"
|
||||
:disabled="disabled"
|
||||
:is-querying="isQuerying"
|
||||
:searcher="customSearcher"
|
||||
:items="sortedItems"
|
||||
:is-selected="internalIsSelected"
|
||||
:max-selectable="maxSelectable"
|
||||
|
||||
@@ -15,9 +15,12 @@ import type {
|
||||
interface Props {
|
||||
items: DropdownItem[]
|
||||
isSelected: (item: DropdownItem, index: number) => boolean
|
||||
isQuerying: boolean
|
||||
filterOptions: FilterOption[]
|
||||
sortOptions: SortOption[]
|
||||
searcher?: (
|
||||
query: string,
|
||||
onCleanup: (cleanupFn: () => void) => void
|
||||
) => Promise<void>
|
||||
}
|
||||
|
||||
defineProps<Props>()
|
||||
@@ -50,7 +53,7 @@ const searchQuery = defineModel<string>('searchQuery')
|
||||
v-model:sort-selected="sortSelected"
|
||||
v-model:search-query="searchQuery"
|
||||
:sort-options="sortOptions"
|
||||
:is-querying="isQuerying"
|
||||
:searcher
|
||||
/>
|
||||
<!-- List -->
|
||||
<div class="relative flex h-full mt-2 overflow-y-scroll">
|
||||
|
||||
@@ -4,10 +4,14 @@ import { ref, useTemplateRef } from 'vue'
|
||||
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
import FormSearchInput from '../FormSearchInput.vue'
|
||||
import type { LayoutMode, OptionId, SortOption } from './types'
|
||||
|
||||
defineProps<{
|
||||
isQuerying: boolean
|
||||
searcher?: (
|
||||
query: string,
|
||||
onCleanup: (cleanupFn: () => void) => void
|
||||
) => Promise<void>
|
||||
sortOptions: SortOption[]
|
||||
}>()
|
||||
|
||||
@@ -46,33 +50,18 @@ function handleSortSelected(item: SortOption) {
|
||||
|
||||
<template>
|
||||
<div class="text-secondary flex gap-2 px-4">
|
||||
<!-- TODO: Replace with a common Search input -->
|
||||
<label
|
||||
<FormSearchInput
|
||||
v-model="searchQuery"
|
||||
:searcher
|
||||
:class="
|
||||
cn(
|
||||
actionButtonStyle,
|
||||
'flex-1 flex px-2 items-center text-base leading-none cursor-text',
|
||||
searchQuery?.trim() !== '' ? 'text-base-foreground' : '',
|
||||
'hover:outline-component-node-widget-background-highlighted/80',
|
||||
'focus-within:outline-component-node-widget-background-highlighted/80'
|
||||
'focus-within:outline-component-node-widget-background-highlighted/80 focus-within:ring-0'
|
||||
)
|
||||
"
|
||||
>
|
||||
<i
|
||||
v-if="isQuerying"
|
||||
class="mr-2 icon-[lucide--loader-circle] size-4 animate-spin"
|
||||
/>
|
||||
<i v-else class="mr-2 icon-[lucide--search] size-4" />
|
||||
<input
|
||||
v-model="searchQuery"
|
||||
type="text"
|
||||
autofocus
|
||||
:class="resetInputStyle"
|
||||
:placeholder="$t('g.search')"
|
||||
/>
|
||||
</label>
|
||||
/>
|
||||
|
||||
<!-- Sort Select -->
|
||||
<button
|
||||
ref="sortTriggerRef"
|
||||
:class="
|
||||
|
||||
Reference in New Issue
Block a user