mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-03-14 01:20:03 +00:00
Cleanup: Properties Panel (#7137)
## Summary - Code cleanup - Copy, padding, color, alignment of components - Subgraph Edit mode changes - Partial fix for the Node Info location (need to do context menu still) - Editing node title ### Still to-do - Bi-directionality in values ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-7137-WIP-Cleanup-Properties-Panel-2be6d73d3650813e9430f6bcb09dfb4d) by [Unito](https://www.unito.io) --------- Co-authored-by: github-actions <github-actions@github.com>
This commit is contained in:
@@ -1,15 +1,17 @@
|
||||
<script setup lang="ts">
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { computed, watchEffect } from 'vue'
|
||||
import { computed, ref, toValue, watchEffect } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import IconButton from '@/components/button/IconButton.vue'
|
||||
import EditableText from '@/components/common/EditableText.vue'
|
||||
import Tab from '@/components/tab/Tab.vue'
|
||||
import TabList from '@/components/tab/TabList.vue'
|
||||
import { SubgraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||
import { useRightSidePanelStore } from '@/stores/workspace/rightSidePanelStore'
|
||||
import type { RightSidePanelTab } from '@/stores/workspace/rightSidePanelStore'
|
||||
import { isLGraphNode } from '@/utils/litegraphUtil'
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
@@ -27,8 +29,8 @@ const { activeTab, isEditingSubgraph } = storeToRefs(rightSidePanelStore)
|
||||
|
||||
const hasSelection = computed(() => selectedItems.value.length > 0)
|
||||
|
||||
const selectedNodes = computed(() => {
|
||||
return selectedItems.value.filter(isLGraphNode) as LGraphNode[]
|
||||
const selectedNodes = computed((): LGraphNode[] => {
|
||||
return selectedItems.value.filter(isLGraphNode)
|
||||
})
|
||||
|
||||
const isSubgraphNode = computed(() => {
|
||||
@@ -44,25 +46,29 @@ const selectedNode = computed(() => {
|
||||
const selectionCount = computed(() => selectedItems.value.length)
|
||||
|
||||
const panelTitle = computed(() => {
|
||||
if (!hasSelection.value) return t('rightSidePanel.properties')
|
||||
if (isSingleNodeSelected.value && selectedNode.value) {
|
||||
return selectedNode.value.title || selectedNode.value.type || 'Node'
|
||||
}
|
||||
return t('rightSidePanel.multipleSelection', { count: selectionCount.value })
|
||||
return t('rightSidePanel.title', { count: selectionCount.value })
|
||||
})
|
||||
|
||||
function closePanel() {
|
||||
rightSidePanelStore.closePanel()
|
||||
}
|
||||
|
||||
const tabs = computed<{ label: () => string; value: string }[]>(() => {
|
||||
const list = [
|
||||
type RightSidePanelTabList = Array<{
|
||||
label: () => string
|
||||
value: RightSidePanelTab
|
||||
}>
|
||||
|
||||
const tabs = computed<RightSidePanelTabList>(() => {
|
||||
const list: RightSidePanelTabList = [
|
||||
{
|
||||
label: () => t('rightSidePanel.parameters'),
|
||||
value: 'parameters'
|
||||
},
|
||||
{
|
||||
label: () => t('rightSidePanel.settings'),
|
||||
label: () => t('g.settings'),
|
||||
value: 'settings'
|
||||
}
|
||||
]
|
||||
@@ -80,19 +86,57 @@ const tabs = computed<{ label: () => string; value: string }[]>(() => {
|
||||
|
||||
// Use global state for activeTab and ensure it's valid
|
||||
watchEffect(() => {
|
||||
if (!tabs.value.some((tab) => tab.value === activeTab.value)) {
|
||||
activeTab.value = tabs.value[0].value as 'parameters' | 'settings' | 'info'
|
||||
if (
|
||||
!tabs.value.some((tab) => tab.value === activeTab.value) &&
|
||||
!(activeTab.value === 'subgraph' && isSubgraphNode.value)
|
||||
) {
|
||||
rightSidePanelStore.openPanel(tabs.value[0].value)
|
||||
}
|
||||
})
|
||||
|
||||
const isEditing = ref(false)
|
||||
|
||||
function handleTitleEdit(newTitle: string) {
|
||||
isEditing.value = false
|
||||
|
||||
const trimmedTitle = newTitle.trim()
|
||||
if (!trimmedTitle) return
|
||||
|
||||
const node = toValue(selectedNode)
|
||||
if (!node) return
|
||||
|
||||
if (trimmedTitle === node.title) return
|
||||
|
||||
node.title = trimmedTitle
|
||||
canvasStore.canvas?.setDirty(true, false)
|
||||
}
|
||||
|
||||
function handleTitleCancel() {
|
||||
isEditing.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex h-full w-full flex-col bg-interface-panel-surface">
|
||||
<div
|
||||
data-testid="properties-panel"
|
||||
class="flex size-full flex-col bg-interface-panel-surface"
|
||||
>
|
||||
<!-- Panel Header -->
|
||||
<div class="border-b border-interface-stroke pt-1">
|
||||
<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">
|
||||
{{ panelTitle }}
|
||||
<EditableText
|
||||
v-if="isSingleNodeSelected"
|
||||
:model-value="panelTitle"
|
||||
:is-editing="isEditing"
|
||||
:input-attrs="{ 'data-testid': 'node-title-input' }"
|
||||
@edit="handleTitleEdit"
|
||||
@cancel="handleTitleCancel"
|
||||
@dblclick="isEditing = true"
|
||||
/>
|
||||
<template v-else>
|
||||
{{ panelTitle }}
|
||||
</template>
|
||||
</h3>
|
||||
|
||||
<div class="flex gap-2">
|
||||
@@ -100,36 +144,41 @@ watchEffect(() => {
|
||||
v-if="isSubgraphNode"
|
||||
type="transparent"
|
||||
size="sm"
|
||||
class="bg-secondary-background hover:bg-secondary-background-hover"
|
||||
:class="
|
||||
cn(
|
||||
'bg-secondary-background hover:bg-secondary-background-hover',
|
||||
isEditingSubgraph
|
||||
? 'bg-secondary-background-selected'
|
||||
: 'bg-secondary-background'
|
||||
'bg-secondary-background hover:bg-secondary-background-hover text-base-foreground',
|
||||
isEditingSubgraph && 'bg-secondary-background-selected'
|
||||
)
|
||||
"
|
||||
@click="
|
||||
rightSidePanelStore.openPanel(
|
||||
isEditingSubgraph ? 'parameters' : 'subgraph'
|
||||
)
|
||||
"
|
||||
@click="isEditingSubgraph = !isEditingSubgraph"
|
||||
>
|
||||
<i class="icon-[lucide--settings-2]" />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
type="transparent"
|
||||
size="sm"
|
||||
class="bg-secondary-background hover:bg-secondary-background-hover"
|
||||
class="bg-secondary-background hover:bg-secondary-background-hover text-base-foreground"
|
||||
:aria-pressed="rightSidePanelStore.isOpen"
|
||||
:aria-label="t('rightSidePanel.togglePanel')"
|
||||
@click="closePanel"
|
||||
>
|
||||
<i class="icon-[lucide--panel-right]" />
|
||||
<i class="icon-[lucide--panel-right] size-4" />
|
||||
</IconButton>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="hasSelection && !(isSubgraphNode && isEditingSubgraph)"
|
||||
class="px-4 pb-2 pt-1"
|
||||
>
|
||||
<TabList v-model="activeTab">
|
||||
<nav v-if="hasSelection" class="px-4 pb-2 pt-1">
|
||||
<TabList
|
||||
:model-value="activeTab"
|
||||
@update:model-value="
|
||||
(newTab: RightSidePanelTab) => {
|
||||
rightSidePanelStore.openPanel(newTab)
|
||||
}
|
||||
"
|
||||
>
|
||||
<Tab
|
||||
v-for="tab in tabs"
|
||||
:key="tab.value"
|
||||
@@ -139,23 +188,21 @@ watchEffect(() => {
|
||||
{{ tab.label() }}
|
||||
</Tab>
|
||||
</TabList>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</section>
|
||||
|
||||
<!-- Panel Content -->
|
||||
<div class="scrollbar-thin flex-1 overflow-y-auto">
|
||||
<div
|
||||
v-if="!hasSelection"
|
||||
class="flex size-full p-4 items-start justify-start text-sm text-muted-foreground"
|
||||
>
|
||||
{{ $t('rightSidePanel.noSelection') }}
|
||||
</div>
|
||||
<SubgraphEditor
|
||||
v-if="isSubgraphNode && isEditingSubgraph"
|
||||
v-else-if="isSubgraphNode && isEditingSubgraph"
|
||||
:node="selectedNode"
|
||||
/>
|
||||
<div
|
||||
v-else-if="!hasSelection"
|
||||
class="flex h-full items-center justify-center text-center"
|
||||
>
|
||||
<div class="px-4 text-sm text-base-foreground-muted">
|
||||
{{ $t('rightSidePanel.noSelection') }}
|
||||
</div>
|
||||
</div>
|
||||
<template v-else>
|
||||
<TabParameters
|
||||
v-if="activeTab === 'parameters'"
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, watch } from 'vue'
|
||||
import { whenever } from '@vueuse/core'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import NodeHelpContent from '@/components/node/NodeHelpContent.vue'
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import { useNodeDefStore } from '@/stores/nodeDefStore'
|
||||
import { useNodeHelpStore } from '@/stores/workspace/nodeHelpStore'
|
||||
|
||||
const props = defineProps<{
|
||||
const { nodes } = defineProps<{
|
||||
nodes: LGraphNode[]
|
||||
}>()
|
||||
const node = computed(() => props.nodes[0])
|
||||
const node = computed(() => nodes[0])
|
||||
|
||||
const nodeDefStore = useNodeDefStore()
|
||||
const nodeHelpStore = useNodeHelpStore()
|
||||
@@ -19,19 +20,17 @@ const nodeInfo = computed(() => {
|
||||
})
|
||||
|
||||
// Open node help when the selected node changes
|
||||
watch(
|
||||
whenever(
|
||||
nodeInfo,
|
||||
(info) => {
|
||||
if (info) {
|
||||
nodeHelpStore.openHelp(info)
|
||||
}
|
||||
nodeHelpStore.openHelp(info)
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="nodeInfo" class="rounded-lg bg-interface-surface p-3">
|
||||
<div v-if="nodeInfo" class="p-3">
|
||||
<NodeHelpContent :node="nodeInfo" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
<script lang="ts" setup>
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
defineProps<{
|
||||
isEmpty?: boolean
|
||||
}>()
|
||||
|
||||
const isCollapse = defineModel<boolean>('collapse', { default: false })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col bg-interface-panel-surface">
|
||||
<div
|
||||
class="sticky top-0 z-10 flex items-center justify-between backdrop-blur-xl bg-inherit"
|
||||
>
|
||||
<button
|
||||
v-tooltip="
|
||||
isEmpty
|
||||
? {
|
||||
value: $t('rightSidePanel.inputsNoneTooltip'),
|
||||
showDelay: 1_000
|
||||
}
|
||||
: undefined
|
||||
"
|
||||
type="button"
|
||||
:class="
|
||||
cn(
|
||||
'group min-h-12 bg-transparent border-0 outline-0 ring-0 w-full text-left flex items-center justify-between pl-4 pr-3',
|
||||
!isEmpty && 'cursor-pointer'
|
||||
)
|
||||
"
|
||||
:disabled="isEmpty"
|
||||
@click="isCollapse = !isCollapse"
|
||||
>
|
||||
<span class="text-sm font-semibold line-clamp-2">
|
||||
<slot name="label" />
|
||||
</span>
|
||||
|
||||
<i
|
||||
v-if="!isEmpty"
|
||||
:class="
|
||||
cn(
|
||||
'text-muted-foreground group-hover:text-base-foreground group-focus:text-base-foreground icon-[lucide--chevron-up] size-4 transition-all',
|
||||
isCollapse && '-rotate-180'
|
||||
)
|
||||
"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="!isCollapse && !isEmpty" class="pb-4">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,51 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import { watch } from 'vue'
|
||||
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
const props = defineProps<{
|
||||
label?: string
|
||||
defaultCollapse?: boolean
|
||||
}>()
|
||||
const isCollapse = defineModel<boolean>('collapse', { default: false })
|
||||
|
||||
if (props.defaultCollapse) {
|
||||
isCollapse.value = true
|
||||
}
|
||||
watch(
|
||||
() => props.defaultCollapse,
|
||||
(value) => (isCollapse.value = value)
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col">
|
||||
<div
|
||||
class="sticky top-0 z-10 flex items-center justify-between backdrop-blur-xl min-h-12"
|
||||
>
|
||||
<button
|
||||
class="group min-h-12 bg-transparent border-0 outline-0 ring-0 w-full text-left flex items-center justify-between pl-4 pr-3 cursor-pointer"
|
||||
@click="isCollapse = !isCollapse"
|
||||
>
|
||||
<span class="text-sm font-semibold line-clamp-2">
|
||||
<slot name="label">
|
||||
{{ props.label ?? '' }}
|
||||
</slot>
|
||||
</span>
|
||||
|
||||
<i
|
||||
:class="
|
||||
cn(
|
||||
'icon-[lucide--chevron-down] size-5 min-w-5 transition-all',
|
||||
isCollapse && 'rotate-90'
|
||||
)
|
||||
"
|
||||
class="relative top-px text-xs leading-none text-node-component-header-icon group-hover:text-base-foreground"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="!isCollapse" class="pb-4">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,39 +1,31 @@
|
||||
<script setup lang="ts">
|
||||
import { refDebounced } from '@vueuse/core'
|
||||
import { ref, toRef, watch } from 'vue'
|
||||
import { ref, toRef, toValue, watch } from 'vue'
|
||||
import type { MaybeRefOrGetter } from 'vue'
|
||||
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
searcher?: (
|
||||
query: string,
|
||||
onCleanup: (cleanupFn: () => void) => void
|
||||
) => Promise<void>
|
||||
updateKey?: (() => unknown) | unknown
|
||||
}>(),
|
||||
{
|
||||
searcher: async () => {}
|
||||
}
|
||||
)
|
||||
const { searcher = async () => {}, updateKey } = defineProps<{
|
||||
searcher?: (
|
||||
query: string,
|
||||
onCleanup: (cleanupFn: () => void) => void
|
||||
) => Promise<void>
|
||||
updateKey?: MaybeRefOrGetter<unknown>
|
||||
}>()
|
||||
|
||||
const searchQuery = defineModel<string>({ default: '' })
|
||||
|
||||
const isQuerying = ref(false)
|
||||
const debouncedSearchQuery = refDebounced(searchQuery, 700, {
|
||||
maxWait: 700
|
||||
const debouncedSearchQuery = refDebounced(searchQuery, 100, {
|
||||
maxWait: 100
|
||||
})
|
||||
watch(searchQuery, (value) => {
|
||||
isQuerying.value = value !== debouncedSearchQuery.value
|
||||
})
|
||||
|
||||
const updateKey =
|
||||
typeof props.updateKey === 'function'
|
||||
? props.updateKey
|
||||
: toRef(props, 'updateKey')
|
||||
const updateKeyRef = toRef(() => toValue(updateKey))
|
||||
|
||||
watch(
|
||||
[debouncedSearchQuery, updateKey],
|
||||
[debouncedSearchQuery, updateKeyRef],
|
||||
(_, __, onCleanup) => {
|
||||
let isCleanup = false
|
||||
let cleanupFn: undefined | (() => void)
|
||||
@@ -42,8 +34,10 @@ watch(
|
||||
cleanupFn?.()
|
||||
})
|
||||
|
||||
void props
|
||||
.searcher(debouncedSearchQuery.value, (cb) => (cleanupFn = cb))
|
||||
void searcher(debouncedSearchQuery.value, (cb) => (cleanupFn = cb))
|
||||
.catch((error) => {
|
||||
console.error('[SidePanelSearch] searcher failed', error)
|
||||
})
|
||||
.finally(() => {
|
||||
if (!isCleanup) isQuerying.value = false
|
||||
})
|
||||
@@ -56,24 +50,28 @@ watch(
|
||||
<label
|
||||
:class="
|
||||
cn(
|
||||
'h-8 bg-zinc-500/20 rounded-lg outline outline-offset-[-1px] outline-node-component-border transition-all duration-150',
|
||||
'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'
|
||||
'mt-1 py-1.5 bg-secondary-background rounded-lg transition-all duration-150',
|
||||
'flex-1 flex gap-2 px-2 items-center',
|
||||
'text-base-foreground border-0',
|
||||
'focus-within:ring focus-within:ring-component-node-widget-background-highlighted/80'
|
||||
)
|
||||
"
|
||||
>
|
||||
<i
|
||||
v-if="isQuerying"
|
||||
class="mr-2 icon-[lucide--loader-circle] size-4 animate-spin"
|
||||
:class="
|
||||
cn(
|
||||
'size-4 text-muted-foreground',
|
||||
isQuerying
|
||||
? 'icon-[lucide--loader-circle] animate-spin'
|
||||
: 'icon-[lucide--search]'
|
||||
)
|
||||
"
|
||||
/>
|
||||
<i v-else class="mr-2 icon-[lucide--search] size-4" />
|
||||
<input
|
||||
v-model="searchQuery"
|
||||
type="text"
|
||||
class="bg-transparent border-0 outline-0 ring-0 text-left"
|
||||
:placeholder="$t('g.search')"
|
||||
class="bg-transparent border-0 outline-0 ring-0 h-5"
|
||||
:placeholder="$t('g.searchPlaceholder')"
|
||||
/>
|
||||
</label>
|
||||
</template>
|
||||
|
||||
@@ -1,15 +1,20 @@
|
||||
<script setup lang="ts">
|
||||
import { provide } from 'vue'
|
||||
import { computed, provide } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets'
|
||||
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||
import WidgetLegacy from '@/renderer/extensions/vueNodes/widgets/components/WidgetLegacy.vue'
|
||||
import { getComponent } from '@/renderer/extensions/vueNodes/widgets/registry/widgetRegistry'
|
||||
import {
|
||||
getComponent,
|
||||
shouldExpand
|
||||
} from '@/renderer/extensions/vueNodes/widgets/registry/widgetRegistry'
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
import RightPanelSection from '../layout/RightPanelSection.vue'
|
||||
import PropertiesAccordionItem from '../layout/PropertiesAccordionItem.vue'
|
||||
|
||||
defineProps<{
|
||||
const { label, widgets } = defineProps<{
|
||||
label?: string
|
||||
widgets: { widget: IBaseWidget; node: LGraphNode }[]
|
||||
}>()
|
||||
@@ -17,6 +22,7 @@ defineProps<{
|
||||
provide('hideLayoutField', true)
|
||||
|
||||
const canvasStore = useCanvasStore()
|
||||
const { t } = useI18n()
|
||||
|
||||
function getWidgetComponent(widget: IBaseWidget) {
|
||||
const component = getComponent(widget.type, widget.name)
|
||||
@@ -31,17 +37,27 @@ function onWidgetValueChange(
|
||||
widget.callback?.(value)
|
||||
canvasStore.canvas?.setDirty(true, true)
|
||||
}
|
||||
|
||||
const isEmpty = computed(() => widgets.length === 0)
|
||||
|
||||
const displayLabel = computed(
|
||||
() =>
|
||||
label ??
|
||||
(isEmpty.value
|
||||
? t('rightSidePanel.inputsNone')
|
||||
: t('rightSidePanel.inputs'))
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<RightPanelSection>
|
||||
<PropertiesAccordionItem :is-empty>
|
||||
<template #label>
|
||||
<slot name="label">
|
||||
{{ label ?? $t('rightSidePanel.inputs') }}
|
||||
{{ displayLabel }}
|
||||
</slot>
|
||||
</template>
|
||||
|
||||
<div class="space-y-4 rounded-lg bg-interface-surface px-4">
|
||||
<div v-if="!isEmpty" class="space-y-4 rounded-lg bg-interface-surface px-4">
|
||||
<div
|
||||
v-for="({ widget, node }, index) in widgets"
|
||||
:key="`widget-${index}-${widget.name}`"
|
||||
@@ -58,7 +74,7 @@ function onWidgetValueChange(
|
||||
:model-value="widget.value"
|
||||
:node-id="String(node.id)"
|
||||
:node-type="node.type"
|
||||
class="col-span-1"
|
||||
:class="cn('col-span-1', shouldExpand(widget.type) && 'min-h-36')"
|
||||
@update:model-value="
|
||||
(value: string | number | boolean | object) =>
|
||||
onWidgetValueChange(widget, value)
|
||||
@@ -66,5 +82,5 @@ function onWidgetValueChange(
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</RightPanelSection>
|
||||
</PropertiesAccordionItem>
|
||||
</template>
|
||||
|
||||
@@ -7,35 +7,30 @@ import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets'
|
||||
import SidePanelSearch from '../layout/SidePanelSearch.vue'
|
||||
import SectionWidgets from './SectionWidgets.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
const { nodes } = defineProps<{
|
||||
nodes: LGraphNode[]
|
||||
}>()
|
||||
|
||||
const widgetsSectionDataList = computed(() => {
|
||||
const list: {
|
||||
widgets: { node: LGraphNode; widget: IBaseWidget }[]
|
||||
node: LGraphNode
|
||||
}[] = []
|
||||
for (const node of props.nodes) {
|
||||
const shownWidgets: IBaseWidget[] = []
|
||||
for (const widget of node.widgets ?? []) {
|
||||
if (widget.options?.canvasOnly || widget.options?.hidden) continue
|
||||
shownWidgets.push(widget)
|
||||
}
|
||||
list.push({
|
||||
widgets: shownWidgets?.map((widget) => ({ node, widget })) ?? [],
|
||||
type NodeWidgetsList = Array<{ node: LGraphNode; widget: IBaseWidget }>
|
||||
type NodeWidgetsListList = Array<{
|
||||
node: LGraphNode
|
||||
widgets: NodeWidgetsList
|
||||
}>
|
||||
|
||||
const widgetsSectionDataList = computed((): NodeWidgetsListList => {
|
||||
return nodes.map((node) => {
|
||||
const { widgets = [] } = node
|
||||
const shownWidgets = widgets
|
||||
.filter((w) => !(w.options?.canvasOnly || w.options?.hidden))
|
||||
.map((widget) => ({ node, widget }))
|
||||
return {
|
||||
widgets: shownWidgets,
|
||||
node
|
||||
})
|
||||
}
|
||||
return list
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
const searchedWidgetsSectionDataList = shallowRef<
|
||||
{
|
||||
widgets: { node: LGraphNode; widget: IBaseWidget }[]
|
||||
node: LGraphNode
|
||||
}[]
|
||||
>([])
|
||||
const searchedWidgetsSectionDataList = shallowRef<NodeWidgetsListList>([])
|
||||
|
||||
/**
|
||||
* Searches widgets in all selected nodes and returns search results.
|
||||
@@ -72,7 +67,7 @@ async function searcher(query: string) {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="p-4 flex gap-2 border-b border-interface-stroke">
|
||||
<div class="px-4 pb-4 flex gap-2 border-b border-interface-stroke">
|
||||
<SidePanelSearch :searcher :update-key="widgetsSectionDataList" />
|
||||
</div>
|
||||
<SectionWidgets
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<div class="space-y-4 rounded-lg bg-interface-surface p-3">
|
||||
<div class="space-y-4 p-3 text-sm text-muted-foreground">
|
||||
<!-- Node State -->
|
||||
<div class="flex flex-col gap-2">
|
||||
<span class="text-sm text-text-secondary">
|
||||
<span>
|
||||
{{ t('rightSidePanel.nodeState') }}
|
||||
</span>
|
||||
<FormSelectButton
|
||||
@@ -27,11 +27,11 @@
|
||||
|
||||
<!-- Color Picker -->
|
||||
<div class="flex flex-col gap-2">
|
||||
<span class="text-sm text-text-secondary">
|
||||
<span>
|
||||
{{ t('rightSidePanel.color') }}
|
||||
</span>
|
||||
<div
|
||||
class="bg-component-node-widget-background text-component-node-foreground border-none rounded-lg p-1 grid grid-cols-5 gap-1 justify-items-center"
|
||||
class="bg-secondary-background border-none rounded-lg p-1 grid grid-cols-5 gap-1 justify-items-center"
|
||||
>
|
||||
<button
|
||||
v-for="option of colorOptions"
|
||||
@@ -39,12 +39,9 @@
|
||||
:class="
|
||||
cn(
|
||||
'size-8 rounded-lg bg-transparent border-0 outline-0 ring-0 text-left flex justify-center items-center cursor-pointer',
|
||||
{
|
||||
'bg-interface-menu-component-surface-selected':
|
||||
option.name === nodeColor,
|
||||
'hover:bg-interface-menu-component-surface-selected':
|
||||
option.name !== nodeColor
|
||||
}
|
||||
option.name === nodeColor
|
||||
? 'bg-interface-menu-component-surface-selected'
|
||||
: 'hover:bg-interface-menu-component-surface-selected'
|
||||
)
|
||||
"
|
||||
@click="nodeColor = option.name"
|
||||
@@ -71,7 +68,7 @@
|
||||
|
||||
<!-- Pinned Toggle -->
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-sm text-text-secondary">
|
||||
<span>
|
||||
{{ t('rightSidePanel.pinned') }}
|
||||
</span>
|
||||
<ToggleSwitch v-model="isPinned" />
|
||||
@@ -81,7 +78,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import ToggleSwitch from 'primevue/toggleswitch'
|
||||
import { computed, shallowRef, triggerRef, watchEffect } from 'vue'
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { LGraphCanvas, LiteGraph } from '@/lib/litegraph/src/litegraph'
|
||||
@@ -93,8 +90,7 @@ import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
|
||||
import { adjustColor } from '@/utils/colorUtil'
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
const props = defineProps<{
|
||||
node?: LGraphNode
|
||||
const { nodes = [] } = defineProps<{
|
||||
nodes?: LGraphNode[]
|
||||
}>()
|
||||
|
||||
@@ -106,36 +102,25 @@ const isLightTheme = computed(
|
||||
() => colorPaletteStore.completedActivePalette.light_theme
|
||||
)
|
||||
|
||||
const targetNodes = shallowRef<LGraphNode[]>([])
|
||||
watchEffect(() => {
|
||||
if (props.node) {
|
||||
targetNodes.value = [props.node]
|
||||
} else {
|
||||
targetNodes.value = props.nodes || []
|
||||
}
|
||||
})
|
||||
|
||||
const nodeState = computed({
|
||||
get() {
|
||||
let mode: LGraphNode['mode'] | null = null
|
||||
get(): LGraphNode['mode'] | null {
|
||||
if (!nodes.length) return null
|
||||
if (nodes.length === 1) {
|
||||
return nodes[0].mode
|
||||
}
|
||||
|
||||
// For multiple nodes, if all nodes have the same mode, return that mode, otherwise return null
|
||||
if (targetNodes.value.length > 1) {
|
||||
mode = targetNodes.value[0].mode
|
||||
if (!targetNodes.value.every((node) => node.mode === mode)) {
|
||||
mode = null
|
||||
}
|
||||
} else {
|
||||
mode = targetNodes.value[0].mode
|
||||
const mode: LGraphNode['mode'] = nodes[0].mode
|
||||
if (!nodes.every((node) => node.mode === mode)) {
|
||||
return null
|
||||
}
|
||||
|
||||
return mode
|
||||
},
|
||||
set(value: LGraphNode['mode']) {
|
||||
targetNodes.value.forEach((node) => {
|
||||
nodes.forEach((node) => {
|
||||
node.mode = value
|
||||
})
|
||||
triggerRef(targetNodes)
|
||||
canvasStore.canvas?.setDirty(true, true)
|
||||
}
|
||||
})
|
||||
@@ -143,11 +128,10 @@ const nodeState = computed({
|
||||
// Pinned state
|
||||
const isPinned = computed<boolean>({
|
||||
get() {
|
||||
return targetNodes.value.some((node) => node.pinned)
|
||||
return nodes.some((node) => node.pinned)
|
||||
},
|
||||
set(value) {
|
||||
targetNodes.value.forEach((node) => node.pin(value))
|
||||
triggerRef(targetNodes)
|
||||
nodes.forEach((node) => node.pin(value))
|
||||
canvasStore.canvas?.setDirty(true, true)
|
||||
}
|
||||
})
|
||||
@@ -191,10 +175,8 @@ const colorOptions: NodeColorOption[] = [
|
||||
|
||||
const nodeColor = computed<NodeColorOption['name'] | null>({
|
||||
get() {
|
||||
if (targetNodes.value.length === 0) return null
|
||||
const theColorOptions = targetNodes.value.map((item) =>
|
||||
item.getColorOption()
|
||||
)
|
||||
if (nodes.length === 0) return null
|
||||
const theColorOptions = nodes.map((item) => item.getColorOption())
|
||||
|
||||
let colorOption: ColorOption | null | false = theColorOptions[0]
|
||||
if (!theColorOptions.every((option) => option === colorOption)) {
|
||||
@@ -220,10 +202,9 @@ const nodeColor = computed<NodeColorOption['name'] | null>({
|
||||
? null
|
||||
: LGraphCanvas.node_colors[colorName]
|
||||
|
||||
for (const item of targetNodes.value) {
|
||||
for (const item of nodes) {
|
||||
item.setColorOption(canvasColorOption)
|
||||
}
|
||||
triggerRef(targetNodes)
|
||||
canvasStore.canvas?.setDirty(true, true)
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user