Split selection into an inputs and outputs step (#9362)

When building an app, selecting inputs and selecting outputs are now 2
separate steps. This prevents confusion where clicking on the widget of
an output node will select that widget instead of the entire output.

<img width="1673" height="773" alt="image"
src="https://github.com/user-attachments/assets/e5994479-6fcf-4572-b58b-bf8cecfb7d55"
/>

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9362-Split-selection-into-an-inputs-and-outputs-step-3196d73d36508187b4a1e51c73f1c54c)
by [Unito](https://www.unito.io)

---------

Co-authored-by: GitHub Action <action@github.com>
This commit is contained in:
AustinMroz
2026-03-04 15:18:16 -08:00
committed by GitHub
parent 316a05c77f
commit 57a919fad2
15 changed files with 147 additions and 115 deletions

View File

@@ -39,7 +39,8 @@ const workflowStore = useWorkflowStore()
const { t } = useI18n() const { t } = useI18n()
const canvas: LGraphCanvas = canvasStore.getCanvas() const canvas: LGraphCanvas = canvasStore.getCanvas()
const { isSelectMode, isArrangeMode } = useAppMode() const { isSelectMode, isSelectInputsMode, isSelectOutputsMode, isArrangeMode } =
useAppMode()
const hoveringSelectable = ref(false) const hoveringSelectable = ref(false)
provide(HideLayoutFieldKey, true) provide(HideLayoutFieldKey, true)
@@ -161,6 +162,7 @@ function handleClick(e: MouseEvent) {
if (!node) return canvasInteractions.forwardEventToCanvas(e) if (!node) return canvasInteractions.forwardEventToCanvas(e)
if (!widget) { if (!widget) {
if (!isSelectOutputsMode.value) return
if (!node.constructor.nodeData?.output_node) if (!node.constructor.nodeData?.output_node)
return canvasInteractions.forwardEventToCanvas(e) return canvasInteractions.forwardEventToCanvas(e)
const index = appModeStore.selectedOutputs.findIndex((id) => id == node.id) const index = appModeStore.selectedOutputs.findIndex((id) => id == node.id)
@@ -168,6 +170,7 @@ function handleClick(e: MouseEvent) {
else appModeStore.selectedOutputs.splice(index, 1) else appModeStore.selectedOutputs.splice(index, 1)
return return
} }
if (!isSelectInputsMode.value) return
const index = appModeStore.selectedInputs.findIndex( const index = appModeStore.selectedInputs.findIndex(
([nodeId, widgetName]) => node.id == nodeId && widget.name === widgetName ([nodeId, widgetName]) => node.id == nodeId && widget.name === widgetName
@@ -234,7 +237,7 @@ const renderedInputs = computed<[string, MaybeRef<BoundStyle> | undefined][]>(
</div> </div>
</DraggableList> </DraggableList>
<PropertiesAccordionItem <PropertiesAccordionItem
v-else v-if="isSelectInputsMode"
:label="t('nodeHelpPage.inputs')" :label="t('nodeHelpPage.inputs')"
enable-empty-state enable-empty-state
:disabled="!appModeStore.selectedInputs.length" :disabled="!appModeStore.selectedInputs.length"
@@ -283,7 +286,7 @@ const renderedInputs = computed<[string, MaybeRef<BoundStyle> | undefined][]>(
</DraggableList> </DraggableList>
</PropertiesAccordionItem> </PropertiesAccordionItem>
<PropertiesAccordionItem <PropertiesAccordionItem
v-if="!isArrangeMode" v-if="isSelectOutputsMode"
:label="t('nodeHelpPage.outputs')" :label="t('nodeHelpPage.outputs')"
enable-empty-state enable-empty-state
:disabled="!appModeStore.selectedOutputs.length" :disabled="!appModeStore.selectedOutputs.length"
@@ -344,42 +347,46 @@ const renderedInputs = computed<[string, MaybeRef<BoundStyle> | undefined][]>(
@wheel="canvasInteractions.forwardEventToCanvas" @wheel="canvasInteractions.forwardEventToCanvas"
> >
<TransformPane :canvas="canvasStore.getCanvas()"> <TransformPane :canvas="canvasStore.getCanvas()">
<div <template v-if="isSelectInputsMode">
v-for="[key, style] in renderedInputs" <div
:key v-for="[key, style] in renderedInputs"
:style="toValue(style)" :key
class="fixed bg-primary-background/30 rounded-lg" :style="toValue(style)"
/> class="fixed bg-primary-background/30 rounded-lg"
<div />
v-for="[key, style, isSelected] in renderedOutputs" </template>
:key <template v-else>
:style="toValue(style)" <div
:class=" v-for="[key, style, isSelected] in renderedOutputs"
cn( :key
'fixed ring-warning-background ring-5 rounded-2xl', :style="toValue(style)"
!isSelected && 'ring-warning-background/50' :class="
) cn(
" 'fixed ring-warning-background ring-5 rounded-2xl',
> !isSelected && 'ring-warning-background/50'
<div class="absolute top-0 right-0 size-8"> )
<div "
v-if="isSelected" >
class="absolute -top-1/2 -right-1/2 size-full p-2 bg-warning-background rounded-lg cursor-pointer pointer-events-auto" <div class="absolute top-0 right-0 size-8">
@click.stop=" <div
remove(appModeStore.selectedOutputs, (k) => k == key) v-if="isSelected"
" class="absolute -top-1/2 -right-1/2 size-full p-2 bg-warning-background rounded-lg cursor-pointer pointer-events-auto"
@pointerdown.stop @click.stop="
> remove(appModeStore.selectedOutputs, (k) => k == key)
<i class="icon-[lucide--check] bg-text-foreground size-full" /> "
@pointerdown.stop
>
<i class="icon-[lucide--check] bg-text-foreground size-full" />
</div>
<div
v-else
class="absolute -top-1/2 -right-1/2 size-full ring-warning-background/50 ring-4 ring-inset bg-component-node-background rounded-lg cursor-pointer pointer-events-auto"
@click.stop="appModeStore.selectedOutputs.push(key)"
@pointerdown.stop
/>
</div> </div>
<div
v-else
class="absolute -top-1/2 -right-1/2 size-full ring-warning-background/50 ring-4 ring-inset bg-component-node-background rounded-lg cursor-pointer pointer-events-auto"
@click.stop="appModeStore.selectedOutputs.push(key)"
@pointerdown.stop
/>
</div> </div>
</div> </template>
</TransformPane> </TransformPane>
</div> </div>
</Teleport> </Teleport>

View File

@@ -63,7 +63,7 @@ describe('BuilderFooterToolbar', () => {
beforeEach(() => { beforeEach(() => {
setActivePinia(createPinia()) setActivePinia(createPinia())
vi.clearAllMocks() vi.clearAllMocks()
mockState.mode = 'builder:select' mockState.mode = 'builder:inputs'
mockHasOutputs.value = true mockHasOutputs.value = true
mockState.settingView = false mockState.settingView = false
}) })
@@ -87,7 +87,7 @@ describe('BuilderFooterToolbar', () => {
} }
it('disables back on the first step', () => { it('disables back on the first step', () => {
mockState.mode = 'builder:select' mockState.mode = 'builder:inputs'
const { back } = getButtons(mountComponent()) const { back } = getButtons(mountComponent())
expect(back.attributes('disabled')).toBeDefined() expect(back.attributes('disabled')).toBeDefined()
}) })
@@ -111,8 +111,8 @@ describe('BuilderFooterToolbar', () => {
expect(next.attributes('disabled')).toBeDefined() expect(next.attributes('disabled')).toBeDefined()
}) })
it('enables next on select step', () => { it('enables next on inputs step', () => {
mockState.mode = 'builder:select' mockState.mode = 'builder:inputs'
const { next } = getButtons(mountComponent()) const { next } = getButtons(mountComponent())
expect(next.attributes('disabled')).toBeUndefined() expect(next.attributes('disabled')).toBeUndefined()
}) })
@@ -121,14 +121,14 @@ describe('BuilderFooterToolbar', () => {
mockState.mode = 'builder:arrange' mockState.mode = 'builder:arrange'
const { back } = getButtons(mountComponent()) const { back } = getButtons(mountComponent())
await back.trigger('click') await back.trigger('click')
expect(mockSetMode).toHaveBeenCalledWith('builder:select') expect(mockSetMode).toHaveBeenCalledWith('builder:outputs')
}) })
it('calls setMode on next click from select step', async () => { it('calls setMode on next click from inputs step', async () => {
mockState.mode = 'builder:select' mockState.mode = 'builder:inputs'
const { next } = getButtons(mountComponent()) const { next } = getButtons(mountComponent())
await next.trigger('click') await next.trigger('click')
expect(mockSetMode).toHaveBeenCalledWith('builder:arrange') expect(mockSetMode).toHaveBeenCalledWith('builder:outputs')
}) })
it('opens default view dialog on next click from arrange step', async () => { it('opens default view dialog on next click from arrange step', async () => {

View File

@@ -6,17 +6,14 @@
<div <div
class="inline-flex items-center gap-1 rounded-2xl border border-border-default bg-base-background p-2 shadow-interface" class="inline-flex items-center gap-1 rounded-2xl border border-border-default bg-base-background p-2 shadow-interface"
> >
<template <template v-for="(step, index) in steps" :key="step.id">
v-for="(step, index) in [selectStep, arrangeStep]"
:key="step.id"
>
<button <button
:class=" :class="
cn( cn(
stepClasses, stepClasses,
activeStep === step.id && 'bg-interface-builder-mode-background', activeStep === step.id
activeStep !== step.id && ? 'bg-interface-builder-mode-background'
'hover:bg-secondary-background bg-transparent' : 'hover:bg-secondary-background bg-transparent'
) )
" "
:aria-current="activeStep === step.id ? 'step' : undefined" :aria-current="activeStep === step.id ? 'step' : undefined"
@@ -32,13 +29,13 @@
<!-- Default view --> <!-- Default view -->
<ConnectOutputPopover <ConnectOutputPopover
v-if="!hasOutputs" v-if="!hasOutputs"
:is-select-active="activeStep === 'builder:select'" :is-select-active="isSelectStep"
@switch="navigateToStep('builder:select')" @switch="navigateToStep('builder:outputs')"
> >
<button :class="cn(stepClasses, 'opacity-30 bg-transparent')"> <button :class="cn(stepClasses, 'opacity-30 bg-transparent')">
<StepBadge <StepBadge
:step="defaultViewStep" :step="defaultViewStep"
:index="2" :index="steps.length"
:model-value="activeStep" :model-value="activeStep"
/> />
<StepLabel :step="defaultViewStep" /> <StepLabel :step="defaultViewStep" />
@@ -58,7 +55,7 @@
> >
<StepBadge <StepBadge
:step="defaultViewStep" :step="defaultViewStep"
:index="2" :index="steps.length"
:model-value="activeStep" :model-value="activeStep"
/> />
<StepLabel :step="defaultViewStep" /> <StepLabel :step="defaultViewStep" />
@@ -84,15 +81,22 @@ import { useBuilderSteps } from './useBuilderSteps'
const { t } = useI18n() const { t } = useI18n()
const appModeStore = useAppModeStore() const appModeStore = useAppModeStore()
const { hasOutputs } = storeToRefs(appModeStore) const { hasOutputs } = storeToRefs(appModeStore)
const { activeStep, navigateToStep } = useBuilderSteps() const { activeStep, isSelectStep, navigateToStep } = useBuilderSteps()
const stepClasses = const stepClasses =
'inline-flex h-14 min-h-8 cursor-pointer items-center gap-3 rounded-lg py-2 pr-4 pl-2 transition-colors border-none' 'inline-flex h-14 min-h-8 cursor-pointer items-center gap-3 rounded-lg py-2 pr-4 pl-2 transition-colors border-none'
const selectStep: BuilderToolbarStep<BuilderStepId> = { const selectInputsStep: BuilderToolbarStep<BuilderStepId> = {
id: 'builder:select', id: 'builder:inputs',
title: t('builderToolbar.select'), title: t('builderToolbar.inputs'),
subtitle: t('builderToolbar.selectDescription'), subtitle: t('builderToolbar.inputsDescription'),
icon: 'icon-[lucide--mouse-pointer-click]'
}
const selectOutputsStep: BuilderToolbarStep<BuilderStepId> = {
id: 'builder:outputs',
title: t('builderToolbar.outputs'),
subtitle: t('builderToolbar.outputsDescription'),
icon: 'icon-[lucide--mouse-pointer-click]' icon: 'icon-[lucide--mouse-pointer-click]'
} }
@@ -109,4 +113,5 @@ const defaultViewStep: BuilderToolbarStep<BuilderStepId> = {
subtitle: t('builderToolbar.defaultViewDescription'), subtitle: t('builderToolbar.defaultViewDescription'),
icon: 'icon-[lucide--eye]' icon: 'icon-[lucide--eye]'
} }
const steps = [selectInputsStep, selectOutputsStep, arrangeStep]
</script> </script>

View File

@@ -46,7 +46,7 @@
</PopoverClose> </PopoverClose>
<PopoverClose as-child> <PopoverClose as-child>
<Button variant="secondary" size="md" @click="emit('switch')"> <Button variant="secondary" size="md" @click="emit('switch')">
{{ t('builderToolbar.switchToSelect') }} {{ t('builderToolbar.switchToOutputs') }}
</Button> </Button>
</PopoverClose> </PopoverClose>
</template> </template>

View File

@@ -7,7 +7,8 @@ import { useAppMode } from '@/composables/useAppMode'
import { useAppSetDefaultView } from './useAppSetDefaultView' import { useAppSetDefaultView } from './useAppSetDefaultView'
const BUILDER_STEPS = [ const BUILDER_STEPS = [
'builder:select', 'builder:inputs',
'builder:outputs',
'builder:arrange', 'builder:arrange',
'setDefaultView' 'setDefaultView'
] as const ] as const
@@ -25,7 +26,7 @@ export function useBuilderSteps(options?: { hasOutputs?: Ref<boolean> }) {
if (isBuilderMode.value) { if (isBuilderMode.value) {
return mode.value as BuilderStepId return mode.value as BuilderStepId
} }
return 'builder:select' return 'builder:inputs'
}) })
const activeStepIndex = computed(() => const activeStepIndex = computed(() =>
@@ -40,6 +41,12 @@ export function useBuilderSteps(options?: { hasOutputs?: Ref<boolean> }) {
return activeStepIndex.value >= BUILDER_STEPS.length - 1 return activeStepIndex.value >= BUILDER_STEPS.length - 1
}) })
const isSelectStep = computed(
() =>
activeStep.value === 'builder:inputs' ||
activeStep.value === 'builder:outputs'
)
function navigateToStep(stepId: BuilderStepId) { function navigateToStep(stepId: BuilderStepId) {
if (stepId === 'setDefaultView') { if (stepId === 'setDefaultView') {
setMode('builder:arrange') setMode('builder:arrange')
@@ -64,6 +71,7 @@ export function useBuilderSteps(options?: { hasOutputs?: Ref<boolean> }) {
activeStepIndex, activeStepIndex,
isFirstStep, isFirstStep,
isLastStep, isLastStep,
isSelectStep,
navigateToStep, navigateToStep,
goBack, goBack,
goNext goNext

View File

@@ -39,8 +39,8 @@
<BottomPanel /> <BottomPanel />
</template> </template>
<template v-if="showUI" #right-side-panel> <template v-if="showUI" #right-side-panel>
<AppBuilder v-if="mode === 'builder:select'" /> <AppBuilder v-if="isBuilderMode" />
<NodePropertiesPanel v-else-if="!isBuilderMode" /> <NodePropertiesPanel v-else />
</template> </template>
<template #graph-canvas-panel> <template #graph-canvas-panel>
<GraphCanvasMenu <GraphCanvasMenu
@@ -204,7 +204,7 @@ const nodeSearchboxPopoverRef = shallowRef<InstanceType<
const settingStore = useSettingStore() const settingStore = useSettingStore()
const nodeDefStore = useNodeDefStore() const nodeDefStore = useNodeDefStore()
const workspaceStore = useWorkspaceStore() const workspaceStore = useWorkspaceStore()
const { mode, isBuilderMode } = useAppMode() const { isBuilderMode } = useAppMode()
const canvasStore = useCanvasStore() const canvasStore = useCanvasStore()
const workflowStore = useWorkflowStore() const workflowStore = useWorkflowStore()
const executionStore = useExecutionStore() const executionStore = useExecutionStore()

View File

@@ -2,7 +2,12 @@ import { computed, ref } from 'vue'
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore' import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
export type AppMode = 'graph' | 'app' | 'builder:select' | 'builder:arrange' export type AppMode =
| 'graph'
| 'app'
| 'builder:inputs'
| 'builder:outputs'
| 'builder:arrange'
const enableAppBuilder = ref(true) const enableAppBuilder = ref(true)
@@ -18,13 +23,17 @@ export function useAppMode() {
const isBuilderMode = computed( const isBuilderMode = computed(
() => isSelectMode.value || isArrangeMode.value () => isSelectMode.value || isArrangeMode.value
) )
const isSelectMode = computed(() => mode.value === 'builder:select') const isSelectInputsMode = computed(() => mode.value === 'builder:inputs')
const isSelectOutputsMode = computed(() => mode.value === 'builder:outputs')
const isSelectMode = computed(
() => isSelectInputsMode.value || isSelectOutputsMode.value
)
const isArrangeMode = computed(() => mode.value === 'builder:arrange') const isArrangeMode = computed(() => mode.value === 'builder:arrange')
const isAppMode = computed( const isAppMode = computed(
() => mode.value === 'app' || mode.value === 'builder:arrange' () => mode.value === 'app' || mode.value === 'builder:arrange'
) )
const isGraphMode = computed( const isGraphMode = computed(
() => mode.value === 'graph' || mode.value === 'builder:select' () => mode.value === 'graph' || isSelectMode.value
) )
function setMode(newMode: AppMode) { function setMode(newMode: AppMode) {
@@ -39,6 +48,8 @@ export function useAppMode() {
enableAppBuilder, enableAppBuilder,
isBuilderMode, isBuilderMode,
isSelectMode, isSelectMode,
isSelectInputsMode,
isSelectOutputsMode,
isArrangeMode, isArrangeMode,
isAppMode, isAppMode,
isGraphMode, isGraphMode,

View File

@@ -3051,11 +3051,11 @@
}, },
"arrange": { "arrange": {
"noOutputs": "No outputs added yet", "noOutputs": "No outputs added yet",
"switchToSelect": "Switch to the 'Select' step and click on output nodes to add them here.", "switchToOutputs": "Switch to the 'Output' step and click on output nodes to add them here.",
"connectAtLeastOne": "Connect {atLeastOne} output node so users can see results after running.", "connectAtLeastOne": "Connect {atLeastOne} output node so users can see results after running.",
"atLeastOne": "at least one", "atLeastOne": "at least one",
"outputExamples": "Examples: 'Save Image' or 'Save Video'", "outputExamples": "Examples: 'Save Image' or 'Save Video'",
"switchToSelectButton": "Switch to Select", "switchToOutputsButton": "Switch to Outputs",
"outputs": "Outputs", "outputs": "Outputs",
"resultsLabel": "Results generated from the selected output node(s) will be shown here after running this app" "resultsLabel": "Results generated from the selected output node(s) will be shown here after running this app"
}, },
@@ -3359,16 +3359,18 @@
}, },
"builderToolbar": { "builderToolbar": {
"label": "App Builder", "label": "App Builder",
"select": "Select", "inputs": "Inputs",
"selectDescription": "Choose inputs/outputs", "outputs": "Outputs",
"inputsDescription": "Choose inputs",
"outputsDescription": "Choose outputs",
"arrange": "Preview", "arrange": "Preview",
"arrangeDescription": "Review app layout", "arrangeDescription": "Review app layout",
"defaultView": "Set a default view", "defaultView": "Set a default view",
"defaultViewDescription": "Choose how this opens", "defaultViewDescription": "Choose how this opens",
"connectOutput": "Connect an output", "connectOutput": "Connect an output",
"connectOutputBody1": "Your app needs at least one output to be connected before it can be saved.", "connectOutputBody1": "Your app needs at least one output to be connected before it can be saved.",
"connectOutputBody2": "Switch to the 'Select' step and click on output nodes to add them here.", "connectOutputBody2": "Switch to the 'Output' step and click on output nodes to add them here.",
"switchToSelect": "Switch to Select", "switchToOutputs": "Switch to Outputs",
"defaultViewTitle": "Set the default view for this workflow", "defaultViewTitle": "Set the default view for this workflow",
"defaultViewLabel": "By default, this workflow will open as:", "defaultViewLabel": "By default, this workflow will open as:",
"app": "App", "app": "App",

View File

@@ -365,14 +365,14 @@ describe('useWorkflowService', () => {
}) })
const workflow2 = createModeTestWorkflow({ const workflow2 = createModeTestWorkflow({
path: 'workflows/two.json', path: 'workflows/two.json',
activeMode: 'builder:select' activeMode: 'builder:inputs'
}) })
workflowStore.activeWorkflow = workflow1 workflowStore.activeWorkflow = workflow1
expect(appMode.mode.value).toBe('app') expect(appMode.mode.value).toBe('app')
workflowStore.activeWorkflow = workflow2 workflowStore.activeWorkflow = workflow2
expect(appMode.mode.value).toBe('builder:select') expect(appMode.mode.value).toBe('builder:inputs')
}) })
}) })
@@ -507,7 +507,7 @@ describe('useWorkflowService', () => {
it('each workflow retains its own mode across tab switches', () => { it('each workflow retains its own mode across tab switches', () => {
const workflow1 = createModeTestWorkflow({ const workflow1 = createModeTestWorkflow({
path: 'workflows/one.json', path: 'workflows/one.json',
activeMode: 'builder:select' activeMode: 'builder:inputs'
}) })
const workflow2 = createModeTestWorkflow({ const workflow2 = createModeTestWorkflow({
path: 'workflows/two.json', path: 'workflows/two.json',
@@ -515,13 +515,13 @@ describe('useWorkflowService', () => {
}) })
workflowStore.activeWorkflow = workflow1 workflowStore.activeWorkflow = workflow1
expect(appMode.mode.value).toBe('builder:select') expect(appMode.mode.value).toBe('builder:inputs')
workflowStore.activeWorkflow = workflow2 workflowStore.activeWorkflow = workflow2
expect(appMode.mode.value).toBe('app') expect(appMode.mode.value).toBe('app')
workflowStore.activeWorkflow = workflow1 workflowStore.activeWorkflow = workflow1
expect(appMode.mode.value).toBe('builder:select') expect(appMode.mode.value).toBe('builder:inputs')
}) })
}) })
}) })

View File

@@ -8,7 +8,7 @@ import { cn } from '@/utils/tailwindUtil'
const { id, name } = defineProps<{ const { id, name } = defineProps<{
id: string id: string
isSelectMode: boolean isSelectInputsMode: boolean
name: string name: string
}>() }>()
@@ -25,7 +25,7 @@ function togglePromotion() {
</script> </script>
<template> <template>
<div <div
v-if="isSelectMode" v-if="isSelectInputsMode"
class="col-span-2 flex flex-row pointer-events-auto cursor-pointer gap-1 relative" class="col-span-2 flex flex-row pointer-events-auto cursor-pointer gap-1 relative"
@pointerdown.capture.stop.prevent="togglePromotion" @pointerdown.capture.stop.prevent="togglePromotion"
@click.capture.stop.prevent @click.capture.stop.prevent

View File

@@ -37,7 +37,7 @@ const { hasOutputs } = storeToRefs(useAppModeStore())
</p> </p>
<div class="flex flex-col gap-1 text-muted-foreground w-lg text-[14px]"> <div class="flex flex-col gap-1 text-muted-foreground w-lg text-[14px]">
<p class="mt-0 p-0">{{ t('linearMode.arrange.switchToSelect') }}</p> <p class="mt-0 p-0">{{ t('linearMode.arrange.switchToOutputs') }}</p>
<i18n-t keypath="linearMode.arrange.connectAtLeastOne" tag="div"> <i18n-t keypath="linearMode.arrange.connectAtLeastOne" tag="div">
<template #atLeastOne> <template #atLeastOne>
@@ -50,8 +50,8 @@ const { hasOutputs } = storeToRefs(useAppModeStore())
<p class="mt-0 p-0">{{ t('linearMode.arrange.outputExamples') }}</p> <p class="mt-0 p-0">{{ t('linearMode.arrange.outputExamples') }}</p>
</div> </div>
<div class="flex flex-row gap-2"> <div class="flex flex-row gap-2">
<Button variant="primary" size="lg" @click="setMode('builder:select')"> <Button variant="primary" size="lg" @click="setMode('builder:outputs')">
{{ t('linearMode.arrange.switchToSelectButton') }} {{ t('linearMode.arrange.switchToOutputsButton') }}
</Button> </Button>
</div> </div>
</div> </div>

View File

@@ -51,7 +51,9 @@
@drop.stop.prevent="handleDrop" @drop.stop.prevent="handleDrop"
> >
<AppOutput <AppOutput
v-if="lgraphNode?.constructor?.nodeData?.output_node && isSelectMode" v-if="
lgraphNode?.constructor?.nodeData?.output_node && isSelectOutputsMode
"
:id="nodeData.id" :id="nodeData.id"
/> />
<div <div
@@ -337,7 +339,7 @@ const { nodeData, error = null } = defineProps<LGraphNodeProps>()
const { t } = useI18n() const { t } = useI18n()
const { isSelectMode } = useAppMode() const { isSelectMode, isSelectOutputsMode } = useAppMode()
const settingStore = useSettingStore() const settingStore = useSettingStore()
const { handleNodeCollapse, handleNodeTitleUpdate, handleNodeRightClick } = const { handleNodeCollapse, handleNodeTitleUpdate, handleNodeRightClick } =

View File

@@ -53,7 +53,7 @@
/> />
</div> </div>
<!-- Widget Component --> <!-- Widget Component -->
<AppInput :id="widget.id" :name="widget.name" :is-select-mode> <AppInput :id="widget.id" :name="widget.name" :is-select-inputs-mode>
<component <component
:is="widget.vueComponent" :is="widget.vueComponent"
v-model="widget.value" v-model="widget.value"
@@ -123,7 +123,7 @@ const { nodeData } = defineProps<NodeWidgetsProps>()
const { shouldHandleNodePointerEvents, forwardEventToCanvas } = const { shouldHandleNodePointerEvents, forwardEventToCanvas } =
useCanvasInteractions() useCanvasInteractions()
const { isSelectMode } = useAppMode() const { isSelectInputsMode } = useAppMode()
const canvasStore = useCanvasStore() const canvasStore = useCanvasStore()
const { bringNodeToFront } = useNodeZIndex() const { bringNodeToFront } = useNodeZIndex()
const promotionStore = usePromotionStore() const promotionStore = usePromotionStore()

View File

@@ -50,7 +50,7 @@ vi.mock('@/components/builder/useEmptyWorkflowDialog', () => ({
import { useAppModeStore } from './appModeStore' import { useAppModeStore } from './appModeStore'
function createBuilderWorkflow( function createBuilderWorkflow(
activeMode: string = 'builder:select' activeMode: string = 'builder:inputs'
): LoadedComfyWorkflow { ): LoadedComfyWorkflow {
const workflow = new ComfyWorkflowClass({ const workflow = new ComfyWorkflowClass({
path: 'workflows/test.json', path: 'workflows/test.json',
@@ -88,29 +88,29 @@ describe('appModeStore', () => {
expect(workflowStore.activeWorkflow!.activeMode).toBe('builder:arrange') expect(workflowStore.activeWorkflow!.activeMode).toBe('builder:arrange')
}) })
it('navigates to builder:select when in app mode without outputs', () => { it('navigates to builder:inputs when in app mode without outputs', () => {
workflowStore.activeWorkflow = createBuilderWorkflow('app') workflowStore.activeWorkflow = createBuilderWorkflow('app')
store.enterBuilder() store.enterBuilder()
expect(workflowStore.activeWorkflow!.activeMode).toBe('builder:select') expect(workflowStore.activeWorkflow!.activeMode).toBe('builder:inputs')
}) })
it('navigates to builder:select when in graph mode with outputs', () => { it('navigates to builder:inputs when in graph mode with outputs', () => {
workflowStore.activeWorkflow = createBuilderWorkflow('graph') workflowStore.activeWorkflow = createBuilderWorkflow('graph')
store.selectedOutputs.push(1) store.selectedOutputs.push(1)
store.enterBuilder() store.enterBuilder()
expect(workflowStore.activeWorkflow!.activeMode).toBe('builder:select') expect(workflowStore.activeWorkflow!.activeMode).toBe('builder:inputs')
}) })
it('navigates to builder:select when in graph mode without outputs', () => { it('navigates to builder:inputs when in graph mode without outputs', () => {
workflowStore.activeWorkflow = createBuilderWorkflow('graph') workflowStore.activeWorkflow = createBuilderWorkflow('graph')
store.enterBuilder() store.enterBuilder()
expect(workflowStore.activeWorkflow!.activeMode).toBe('builder:select') expect(workflowStore.activeWorkflow!.activeMode).toBe('builder:inputs')
}) })
it('shows empty workflow dialog when graph has no nodes', () => { it('shows empty workflow dialog when graph has no nodes', () => {
@@ -141,7 +141,7 @@ describe('appModeStore', () => {
const options = getDialogOptions() const options = getDialogOptions()
// Move to builder so onDismiss must actually transition back // Move to builder so onDismiss must actually transition back
workflowStore.activeWorkflow!.activeMode = 'builder:select' workflowStore.activeWorkflow!.activeMode = 'builder:inputs'
options.onDismiss() options.onDismiss()
@@ -156,7 +156,7 @@ describe('appModeStore', () => {
options.onEnterBuilder() options.onEnterBuilder()
expect(workflowStore.activeWorkflow!.activeMode).toBe('builder:select') expect(workflowStore.activeWorkflow!.activeMode).toBe('builder:inputs')
}) })
it('onEnterBuilder shows dialog again when no nodes', () => { it('onEnterBuilder shows dialog again when no nodes', () => {

View File

@@ -13,7 +13,7 @@ import { resolveNode } from '@/utils/litegraphUtil'
export const useAppModeStore = defineStore('appMode', () => { export const useAppModeStore = defineStore('appMode', () => {
const { getCanvas } = useCanvasStore() const { getCanvas } = useCanvasStore()
const workflowStore = useWorkflowStore() const workflowStore = useWorkflowStore()
const { mode, setMode, isBuilderMode } = useAppMode() const { mode, setMode, isBuilderMode, isSelectMode } = useAppMode()
const emptyWorkflowDialog = useEmptyWorkflowDialog() const emptyWorkflowDialog = useEmptyWorkflowDialog()
const selectedInputs = reactive<[NodeId, string][]>([]) const selectedInputs = reactive<[NodeId, string][]>([])
@@ -78,20 +78,17 @@ export const useAppModeStore = defineStore('appMode', () => {
) )
let unwatch: () => void | undefined let unwatch: () => void | undefined
watch( watch(isSelectMode, (inSelect) => {
() => mode.value === 'builder:select', const { state } = getCanvas()
(inSelect) => { if (!state) return
const { state } = getCanvas() state.readOnly = inSelect
if (!state) return unwatch?.()
state.readOnly = inSelect if (inSelect)
unwatch?.() unwatch = watch(
if (inSelect) () => state.readOnly,
unwatch = watch( () => (state.readOnly = true)
() => state.readOnly, )
() => (state.readOnly = true) })
)
}
)
function enterBuilder() { function enterBuilder() {
if (!app.rootGraph?.nodes?.length) { if (!app.rootGraph?.nodes?.length) {
@@ -105,7 +102,7 @@ export const useAppModeStore = defineStore('appMode', () => {
setMode( setMode(
mode.value === 'app' && hasOutputs.value mode.value === 'app' && hasOutputs.value
? 'builder:arrange' ? 'builder:arrange'
: 'builder:select' : 'builder:inputs'
) )
} }