Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 8 additions & 36 deletions src/components/rightSidePanel/parameters/SectionWidgets.vue
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,7 @@ import {
import { useI18n } from 'vue-i18n'

import Button from '@/components/ui/button/Button.vue'
import { widgetPromotedSource } from '@/core/graph/subgraph/promotedInputWidget'
import { resolvePromotedWidgetSource } from '@/core/graph/subgraph/resolvePromotedWidgetSource'
import { isWidgetPromotedOnSubgraphNode } from '@/core/graph/subgraph/promotionUtils'
import type { LGraphGroup, LGraphNode } from '@/lib/litegraph/src/litegraph'
import { SubgraphNode } from '@/lib/litegraph/src/litegraph'
import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets'
Expand Down Expand Up @@ -45,12 +43,12 @@ const {
isDraggable = false,
hiddenFavoriteIndicator = false,
showNodeName = false,
parents = [],
host,
enableEmptyState = false,
tooltip
} = defineProps<{
label?: string
parents?: SubgraphNode[]
host?: SubgraphNode
node?: LGraphNode
widgets: { widget: IBaseWidget; node: LGraphNode }[]
showLocateButton?: boolean
Expand Down Expand Up @@ -145,31 +143,6 @@ const { t } = useI18n()

const getNodeParentGroup = inject(GetNodeParentGroupKey, null)

function isWidgetShownOnParents(
widgetNode: LGraphNode,
widget: IBaseWidget
): boolean {
const source = widgetPromotedSource(widgetNode, widget)
return parents.some((parent) => {
if (source) {
const widgetNodeId = widgetNode.id
const interiorNodeId =
String(widgetNode.id) === String(parent.id)
? source.nodeId
: widgetNodeId

return isWidgetPromotedOnSubgraphNode(parent, {
sourceNodeId: interiorNodeId,
sourceWidgetName: source.widgetName
})
}
return isWidgetPromotedOnSubgraphNode(parent, {
sourceNodeId: widgetNode.id,
sourceWidgetName: widget.name
})
})
}

const isEmpty = computed(() => widgets.value.length === 0)

const displayLabel = computed(
Expand Down Expand Up @@ -399,13 +372,12 @@ defineExpose({
<WidgetItem
v-for="{ widget, node } in widgets"
:key="getStableWidgetRenderKey(widget)"
:widget="widget"
:node="node"
:is-draggable="isDraggable"
:hidden-favorite-indicator="hiddenFavoriteIndicator"
:show-node-name="showNodeName"
:parents="parents"
:is-shown-on-parents="isWidgetShownOnParents(node, widget)"
:widget
:node
:is-draggable
:hidden-favorite-indicator
:show-node-name
:host
@update:widget-value="handleWidgetValueUpdate(node, widget, $event)"
@reset-to-default="handleWidgetReset(node, widget, $event)"
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ const captured: { rows: { node: LGraphNode; widget: IBaseWidget }[] } = {
}

const SectionWidgetsStub = {
props: ['widgets', 'node', 'parents'],
props: ['widgets', 'node', 'host'],
setup(props: Record<string, unknown>) {
captured.rows = props.widgets as {
node: LGraphNode
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import { useI18n } from 'vue-i18n'

import { promotedInputWidgets } from '@/core/graph/subgraph/promotedInputWidget'
import {
getWidgetName,
isWidgetPromotedOnSubgraphNode,
reorderSubgraphInputsByWidgetOrder
} from '@/core/graph/subgraph/promotionUtils'
Expand Down Expand Up @@ -83,13 +82,11 @@ const advancedInputsWidgets = computed((): NodeWidgetsList => {
({ node: interiorNode, widget }) =>
!isWidgetPromotedOnSubgraphNode(node, {
sourceNodeId: interiorNode.id,
sourceWidgetName: getWidgetName(widget)
sourceWidgetName: widget.name
})
)
})

const parents = computed<SubgraphNode[]>(() => [node])

const searchedWidgetsList = shallowRef<NodeWidgetsList>(widgetsList.value)
const isSearching = ref(false)

Expand Down Expand Up @@ -140,7 +137,7 @@ const label = computed(() => {
:collapse="firstSectionCollapsed && !isSearching"
:node
:label
:parents
:host="node"
:widgets="searchedWidgetsList"
:is-draggable="!isSearching"
:enable-empty-state="isSearching"
Expand All @@ -164,7 +161,7 @@ const label = computed(() => {
ref="advancedInputsSectionRef"
v-model:collapse="advancedInputsCollapsed"
:label="t('rightSidePanel.advancedInputs')"
:parents="parents"
:host="node"
:widgets="advancedInputsWidgets"
show-node-name
class="border-b border-interface-stroke"
Expand Down
74 changes: 65 additions & 9 deletions src/components/rightSidePanel/parameters/WidgetActions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,21 @@ import { h } from 'vue'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { createI18n } from 'vue-i18n'

import { promoteWidget } from '@/core/graph/subgraph/promotionUtils'
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
import type { SubgraphNode } from '@/lib/litegraph/src/subgraph/SubgraphNode'
import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets'
import WidgetActions from './WidgetActions.vue'

const { mockGetInputSpecForWidget } = vi.hoisted(() => ({
mockGetInputSpecForWidget: vi.fn()
}))
const { mockGetInputSpecForWidget, mockIsFavorited, mockToggleFavorite } =
vi.hoisted(() => ({
mockGetInputSpecForWidget: vi.fn(),
mockIsFavorited: vi.fn(),
mockToggleFavorite: vi.fn()
}))

vi.mock('@/core/graph/subgraph/promotionUtils', () => ({
demoteWidget: vi.fn(),
promoteWidget: vi.fn(),
isLinkedPromotion: vi.fn(() => false)
promoteWidget: vi.fn()
}))

vi.mock('@/stores/nodeDefStore', () => ({
Expand All @@ -36,8 +39,8 @@ vi.mock('@/renderer/core/canvas/canvasStore', () => ({

vi.mock('@/stores/workspace/favoritedWidgetsStore', () => ({
useFavoritedWidgetsStore: () => ({
isFavorited: vi.fn().mockReturnValue(false),
toggleFavorite: vi.fn()
isFavorited: mockIsFavorited,
toggleFavorite: mockToggleFavorite
})
}))

Expand All @@ -62,7 +65,6 @@ const i18n = createI18n({
enterNewName: 'Enter new name'
},
rightSidePanel: {
hideInput: 'Hide input',
showInput: 'Show input',
addFavorite: 'Favorite',
removeFavorite: 'Unfavorite',
Expand All @@ -76,6 +78,7 @@ describe('WidgetActions', () => {
beforeEach(() => {
setActivePinia(createTestingPinia({ stubActions: false }))
vi.resetAllMocks()
mockIsFavorited.mockReturnValue(false)
mockGetInputSpecForWidget.mockReturnValue({
type: 'INT',
default: 42
Expand Down Expand Up @@ -205,4 +208,57 @@ describe('WidgetActions', () => {

expect(onResetToDefault).toHaveBeenCalledWith('option1')
})

it('promotes the widget into the host when "Show input" is clicked', async () => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: (non-blocking) there is no integration-level test that uses a real SubgraphNode + promoteValueWidgetViaSubgraphInput, then renders WidgetActions and asserts "Show input" is absent for a linked promotion and present for an unlinked one. The deleted isLinkedPromotion tests partially covered this invariant. One test at this level would guard the new inputForWidget(node, widget)?.widgetId != null check against future regressions.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I’m deferring this ⏸️📝 The suggested test exercises the previous promotion-chain model 🔗🪦, while this PR intentionally makes widget behavior node-local 📍 and independent 🧩✨ The focused tests cover the intended boundary 🎯🧪✅

const widget = createMockWidget()
const node = createMockNode()
const host = fromAny<SubgraphNode, unknown>({ id: 2 })

const { user } = renderWidgetActions(widget, node, { host })

await user.click(screen.getByRole('button', { name: /Show input/ }))

expect(promoteWidget).toHaveBeenCalledWith(node, widget, [host])
})

it('does not offer "Show input" without a host', () => {
renderWidgetActions(createMockWidget(), createMockNode())

expect(
screen.queryByRole('button', { name: /Show input/ })
).not.toBeInTheDocument()
})

it('does not offer "Show input" when the host input is already linked', () => {
const widget = createMockWidget()
const node = fromAny<LGraphNode, unknown>({
id: 1,
type: 'TestNode',
rootGraph: { id: 'graph-test' },
isSubgraphNode: () => true,
getSlotFromWidget: (candidate: IBaseWidget) =>
candidate.name === 'test_widget'
? { widgetId: 'graph-test:1:test_widget' }
: undefined
})
const host = fromAny<SubgraphNode, unknown>({ id: 2 })

renderWidgetActions(widget, node, { host })

expect(
screen.queryByRole('button', { name: /Show input/ })
).not.toBeInTheDocument()
})

it("toggles the favorite for the row's node", async () => {
const widget = createMockWidget()
const node = createMockNode()
const host = fromAny<SubgraphNode, unknown>({ id: 2 })

const { user } = renderWidgetActions(widget, node, { host })

await user.click(screen.getByRole('button', { name: /Favorite/ }))

expect(mockToggleFavorite).toHaveBeenCalledWith(node, 'test_widget')
})
Comment thread
coderabbitai[bot] marked this conversation as resolved.
})
74 changes: 14 additions & 60 deletions src/components/rightSidePanel/parameters/WidgetActions.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,33 +5,21 @@ import { useI18n } from 'vue-i18n'

import MoreButton from '@/components/button/MoreButton.vue'
import Button from '@/components/ui/button/Button.vue'
import { widgetPromotedSource } from '@/core/graph/subgraph/promotedInputWidget'
import {
demotePromotedInput,
demoteWidget,
isLinkedPromotion,
promoteWidget
} from '@/core/graph/subgraph/promotionUtils'
import { inputForWidget } from '@/core/graph/subgraph/promotedInputWidget'
import { promoteWidget } from '@/core/graph/subgraph/promotionUtils'
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
import type { SubgraphNode } from '@/lib/litegraph/src/subgraph/SubgraphNode'
import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets'
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
import { useNodeDefStore } from '@/stores/nodeDefStore'
import { useWidgetValueStore } from '@/stores/widgetValueStore'
import { useFavoritedWidgetsStore } from '@/stores/workspace/favoritedWidgetsStore'
import { getWidgetDefaultValue, promptWidgetLabel } from '@/utils/widgetUtil'
import type { WidgetValue } from '@/utils/widgetUtil'

const {
widget,
node,
parents = [],
isShownOnParents = false
} = defineProps<{
const { widget, node, host } = defineProps<{
widget: IBaseWidget
node: LGraphNode
parents?: SubgraphNode[]
isShownOnParents?: boolean
host?: SubgraphNode
}>()

const emit = defineEmits<{
Expand All @@ -40,24 +28,17 @@ const emit = defineEmits<{

const label = defineModel<string>('label', { required: true })

const canvasStore = useCanvasStore()
const favoritedWidgetsStore = useFavoritedWidgetsStore()
const nodeDefStore = useNodeDefStore()
const { t } = useI18n()

const hasParents = computed(() => parents?.length > 0)
const isLinked = computed(() => {
if (!node.isSubgraphNode()) return false
const source = widgetPromotedSource(node, widget)
if (!source) return false
return isLinkedPromotion(node, source.nodeId, source.widgetName)
return inputForWidget(node, widget)?.widgetId != null
})
const canToggleVisibility = computed(() => hasParents.value && !isLinked.value)
const favoriteNode = computed(() =>
isShownOnParents && hasParents.value ? parents[0] : node
)
const canShowInput = computed(() => host != null && !isLinked.value)
const isFavorited = computed(() =>
favoritedWidgetsStore.isFavorited(favoriteNode.value, widget.name)
favoritedWidgetsStore.isFavorited(node, widget.name)
Comment thread
coderabbitai[bot] marked this conversation as resolved.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue: handleHideInput and the hide/show toggle are removed entirely, but the "Hide input" action was reachable for non-linked physically-promoted widgets. Git history shows bb96e3c (#10648) explicitly restored this path (canToggleVisibility = hasParents && !isLinked). After this PR, a user inside a subgraph can no longer demote a promoted widget from the Parameters tab three-dot menu.

Is the intent to remove this action completely, or should the demote path (demoteWidget(node, widget, [host])) be kept and gated on a separate condition?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove until we have better handling. The previous path had special cases for "promoted" vs "linked to Subgraph input". Now that we have Subgraph Link Only Promotion, hiding becomes a difficult UX/product issue.

)

const inputSpec = computed(() =>
Expand Down Expand Up @@ -85,33 +66,13 @@ async function handleRename() {
if (newLabel !== null) label.value = newLabel
}

function handleHideInput() {
if (!parents?.length) return

const source = widgetPromotedSource(node, widget)
if (source) {
const currentNodeId = node.id
for (const parent of parents) {
const sourceNodeId =
String(node.id) === String(parent.id) ? source.nodeId : currentNodeId
demotePromotedInput(parent, {
sourceNodeId,
sourceWidgetName: source.widgetName
})
}
canvasStore.canvas?.setDirty(true, true)
} else {
demoteWidget(node, widget, parents)
}
}

function handleShowInput() {
if (!parents?.length) return
promoteWidget(node, widget, parents)
if (!host) return
promoteWidget(node, widget, [host])
}

function handleToggleFavorite() {
favoritedWidgetsStore.toggleFavorite(favoriteNode.value, widget.name)
favoritedWidgetsStore.toggleFavorite(node, widget.name)
}

function handleResetToDefault() {
Expand Down Expand Up @@ -143,26 +104,19 @@ function handleResetToDefault() {
</Button>

<Button
v-if="canToggleVisibility"
v-if="canShowInput"
variant="textonly"
size="unset"
class="flex w-full items-center gap-2 rounded-sm px-3 py-2 text-sm transition-all active:scale-95"
@click="
() => {
if (isShownOnParents) handleHideInput()
else handleShowInput()
handleShowInput()
close()
}
"
>
<template v-if="isShownOnParents">
<i class="icon-[lucide--eye-off] size-4" />
<span>{{ t('rightSidePanel.hideInput') }}</span>
</template>
<template v-else>
<i class="icon-[lucide--eye] size-4" />
<span>{{ t('rightSidePanel.showInput') }}</span>
</template>
<i class="icon-[lucide--eye] size-4" />
<span>{{ t('rightSidePanel.showInput') }}</span>
</Button>

<Button
Expand Down
Loading
Loading