mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-03-05 13:10:24 +00:00
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:
@@ -39,7 +39,8 @@ const workflowStore = useWorkflowStore()
|
||||
const { t } = useI18n()
|
||||
const canvas: LGraphCanvas = canvasStore.getCanvas()
|
||||
|
||||
const { isSelectMode, isArrangeMode } = useAppMode()
|
||||
const { isSelectMode, isSelectInputsMode, isSelectOutputsMode, isArrangeMode } =
|
||||
useAppMode()
|
||||
const hoveringSelectable = ref(false)
|
||||
|
||||
provide(HideLayoutFieldKey, true)
|
||||
@@ -161,6 +162,7 @@ function handleClick(e: MouseEvent) {
|
||||
if (!node) return canvasInteractions.forwardEventToCanvas(e)
|
||||
|
||||
if (!widget) {
|
||||
if (!isSelectOutputsMode.value) return
|
||||
if (!node.constructor.nodeData?.output_node)
|
||||
return canvasInteractions.forwardEventToCanvas(e)
|
||||
const index = appModeStore.selectedOutputs.findIndex((id) => id == node.id)
|
||||
@@ -168,6 +170,7 @@ function handleClick(e: MouseEvent) {
|
||||
else appModeStore.selectedOutputs.splice(index, 1)
|
||||
return
|
||||
}
|
||||
if (!isSelectInputsMode.value) return
|
||||
|
||||
const index = appModeStore.selectedInputs.findIndex(
|
||||
([nodeId, widgetName]) => node.id == nodeId && widget.name === widgetName
|
||||
@@ -234,7 +237,7 @@ const renderedInputs = computed<[string, MaybeRef<BoundStyle> | undefined][]>(
|
||||
</div>
|
||||
</DraggableList>
|
||||
<PropertiesAccordionItem
|
||||
v-else
|
||||
v-if="isSelectInputsMode"
|
||||
:label="t('nodeHelpPage.inputs')"
|
||||
enable-empty-state
|
||||
:disabled="!appModeStore.selectedInputs.length"
|
||||
@@ -283,7 +286,7 @@ const renderedInputs = computed<[string, MaybeRef<BoundStyle> | undefined][]>(
|
||||
</DraggableList>
|
||||
</PropertiesAccordionItem>
|
||||
<PropertiesAccordionItem
|
||||
v-if="!isArrangeMode"
|
||||
v-if="isSelectOutputsMode"
|
||||
:label="t('nodeHelpPage.outputs')"
|
||||
enable-empty-state
|
||||
:disabled="!appModeStore.selectedOutputs.length"
|
||||
@@ -344,42 +347,46 @@ const renderedInputs = computed<[string, MaybeRef<BoundStyle> | undefined][]>(
|
||||
@wheel="canvasInteractions.forwardEventToCanvas"
|
||||
>
|
||||
<TransformPane :canvas="canvasStore.getCanvas()">
|
||||
<div
|
||||
v-for="[key, style] in renderedInputs"
|
||||
:key
|
||||
:style="toValue(style)"
|
||||
class="fixed bg-primary-background/30 rounded-lg"
|
||||
/>
|
||||
<div
|
||||
v-for="[key, style, isSelected] in renderedOutputs"
|
||||
:key
|
||||
:style="toValue(style)"
|
||||
: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"
|
||||
@click.stop="
|
||||
remove(appModeStore.selectedOutputs, (k) => k == key)
|
||||
"
|
||||
@pointerdown.stop
|
||||
>
|
||||
<i class="icon-[lucide--check] bg-text-foreground size-full" />
|
||||
<template v-if="isSelectInputsMode">
|
||||
<div
|
||||
v-for="[key, style] in renderedInputs"
|
||||
:key
|
||||
:style="toValue(style)"
|
||||
class="fixed bg-primary-background/30 rounded-lg"
|
||||
/>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div
|
||||
v-for="[key, style, isSelected] in renderedOutputs"
|
||||
:key
|
||||
:style="toValue(style)"
|
||||
: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"
|
||||
@click.stop="
|
||||
remove(appModeStore.selectedOutputs, (k) => k == key)
|
||||
"
|
||||
@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
|
||||
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>
|
||||
</template>
|
||||
</TransformPane>
|
||||
</div>
|
||||
</Teleport>
|
||||
|
||||
@@ -63,7 +63,7 @@ describe('BuilderFooterToolbar', () => {
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia())
|
||||
vi.clearAllMocks()
|
||||
mockState.mode = 'builder:select'
|
||||
mockState.mode = 'builder:inputs'
|
||||
mockHasOutputs.value = true
|
||||
mockState.settingView = false
|
||||
})
|
||||
@@ -87,7 +87,7 @@ describe('BuilderFooterToolbar', () => {
|
||||
}
|
||||
|
||||
it('disables back on the first step', () => {
|
||||
mockState.mode = 'builder:select'
|
||||
mockState.mode = 'builder:inputs'
|
||||
const { back } = getButtons(mountComponent())
|
||||
expect(back.attributes('disabled')).toBeDefined()
|
||||
})
|
||||
@@ -111,8 +111,8 @@ describe('BuilderFooterToolbar', () => {
|
||||
expect(next.attributes('disabled')).toBeDefined()
|
||||
})
|
||||
|
||||
it('enables next on select step', () => {
|
||||
mockState.mode = 'builder:select'
|
||||
it('enables next on inputs step', () => {
|
||||
mockState.mode = 'builder:inputs'
|
||||
const { next } = getButtons(mountComponent())
|
||||
expect(next.attributes('disabled')).toBeUndefined()
|
||||
})
|
||||
@@ -121,14 +121,14 @@ describe('BuilderFooterToolbar', () => {
|
||||
mockState.mode = 'builder:arrange'
|
||||
const { back } = getButtons(mountComponent())
|
||||
await back.trigger('click')
|
||||
expect(mockSetMode).toHaveBeenCalledWith('builder:select')
|
||||
expect(mockSetMode).toHaveBeenCalledWith('builder:outputs')
|
||||
})
|
||||
|
||||
it('calls setMode on next click from select step', async () => {
|
||||
mockState.mode = 'builder:select'
|
||||
it('calls setMode on next click from inputs step', async () => {
|
||||
mockState.mode = 'builder:inputs'
|
||||
const { next } = getButtons(mountComponent())
|
||||
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 () => {
|
||||
|
||||
@@ -6,17 +6,14 @@
|
||||
<div
|
||||
class="inline-flex items-center gap-1 rounded-2xl border border-border-default bg-base-background p-2 shadow-interface"
|
||||
>
|
||||
<template
|
||||
v-for="(step, index) in [selectStep, arrangeStep]"
|
||||
:key="step.id"
|
||||
>
|
||||
<template v-for="(step, index) in steps" :key="step.id">
|
||||
<button
|
||||
:class="
|
||||
cn(
|
||||
stepClasses,
|
||||
activeStep === step.id && 'bg-interface-builder-mode-background',
|
||||
activeStep !== step.id &&
|
||||
'hover:bg-secondary-background bg-transparent'
|
||||
activeStep === step.id
|
||||
? 'bg-interface-builder-mode-background'
|
||||
: 'hover:bg-secondary-background bg-transparent'
|
||||
)
|
||||
"
|
||||
:aria-current="activeStep === step.id ? 'step' : undefined"
|
||||
@@ -32,13 +29,13 @@
|
||||
<!-- Default view -->
|
||||
<ConnectOutputPopover
|
||||
v-if="!hasOutputs"
|
||||
:is-select-active="activeStep === 'builder:select'"
|
||||
@switch="navigateToStep('builder:select')"
|
||||
:is-select-active="isSelectStep"
|
||||
@switch="navigateToStep('builder:outputs')"
|
||||
>
|
||||
<button :class="cn(stepClasses, 'opacity-30 bg-transparent')">
|
||||
<StepBadge
|
||||
:step="defaultViewStep"
|
||||
:index="2"
|
||||
:index="steps.length"
|
||||
:model-value="activeStep"
|
||||
/>
|
||||
<StepLabel :step="defaultViewStep" />
|
||||
@@ -58,7 +55,7 @@
|
||||
>
|
||||
<StepBadge
|
||||
:step="defaultViewStep"
|
||||
:index="2"
|
||||
:index="steps.length"
|
||||
:model-value="activeStep"
|
||||
/>
|
||||
<StepLabel :step="defaultViewStep" />
|
||||
@@ -84,15 +81,22 @@ import { useBuilderSteps } from './useBuilderSteps'
|
||||
const { t } = useI18n()
|
||||
const appModeStore = useAppModeStore()
|
||||
const { hasOutputs } = storeToRefs(appModeStore)
|
||||
const { activeStep, navigateToStep } = useBuilderSteps()
|
||||
const { activeStep, isSelectStep, navigateToStep } = useBuilderSteps()
|
||||
|
||||
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'
|
||||
|
||||
const selectStep: BuilderToolbarStep<BuilderStepId> = {
|
||||
id: 'builder:select',
|
||||
title: t('builderToolbar.select'),
|
||||
subtitle: t('builderToolbar.selectDescription'),
|
||||
const selectInputsStep: BuilderToolbarStep<BuilderStepId> = {
|
||||
id: 'builder:inputs',
|
||||
title: t('builderToolbar.inputs'),
|
||||
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]'
|
||||
}
|
||||
|
||||
@@ -109,4 +113,5 @@ const defaultViewStep: BuilderToolbarStep<BuilderStepId> = {
|
||||
subtitle: t('builderToolbar.defaultViewDescription'),
|
||||
icon: 'icon-[lucide--eye]'
|
||||
}
|
||||
const steps = [selectInputsStep, selectOutputsStep, arrangeStep]
|
||||
</script>
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
</PopoverClose>
|
||||
<PopoverClose as-child>
|
||||
<Button variant="secondary" size="md" @click="emit('switch')">
|
||||
{{ t('builderToolbar.switchToSelect') }}
|
||||
{{ t('builderToolbar.switchToOutputs') }}
|
||||
</Button>
|
||||
</PopoverClose>
|
||||
</template>
|
||||
|
||||
@@ -7,7 +7,8 @@ import { useAppMode } from '@/composables/useAppMode'
|
||||
import { useAppSetDefaultView } from './useAppSetDefaultView'
|
||||
|
||||
const BUILDER_STEPS = [
|
||||
'builder:select',
|
||||
'builder:inputs',
|
||||
'builder:outputs',
|
||||
'builder:arrange',
|
||||
'setDefaultView'
|
||||
] as const
|
||||
@@ -25,7 +26,7 @@ export function useBuilderSteps(options?: { hasOutputs?: Ref<boolean> }) {
|
||||
if (isBuilderMode.value) {
|
||||
return mode.value as BuilderStepId
|
||||
}
|
||||
return 'builder:select'
|
||||
return 'builder:inputs'
|
||||
})
|
||||
|
||||
const activeStepIndex = computed(() =>
|
||||
@@ -40,6 +41,12 @@ export function useBuilderSteps(options?: { hasOutputs?: Ref<boolean> }) {
|
||||
return activeStepIndex.value >= BUILDER_STEPS.length - 1
|
||||
})
|
||||
|
||||
const isSelectStep = computed(
|
||||
() =>
|
||||
activeStep.value === 'builder:inputs' ||
|
||||
activeStep.value === 'builder:outputs'
|
||||
)
|
||||
|
||||
function navigateToStep(stepId: BuilderStepId) {
|
||||
if (stepId === 'setDefaultView') {
|
||||
setMode('builder:arrange')
|
||||
@@ -64,6 +71,7 @@ export function useBuilderSteps(options?: { hasOutputs?: Ref<boolean> }) {
|
||||
activeStepIndex,
|
||||
isFirstStep,
|
||||
isLastStep,
|
||||
isSelectStep,
|
||||
navigateToStep,
|
||||
goBack,
|
||||
goNext
|
||||
|
||||
@@ -39,8 +39,8 @@
|
||||
<BottomPanel />
|
||||
</template>
|
||||
<template v-if="showUI" #right-side-panel>
|
||||
<AppBuilder v-if="mode === 'builder:select'" />
|
||||
<NodePropertiesPanel v-else-if="!isBuilderMode" />
|
||||
<AppBuilder v-if="isBuilderMode" />
|
||||
<NodePropertiesPanel v-else />
|
||||
</template>
|
||||
<template #graph-canvas-panel>
|
||||
<GraphCanvasMenu
|
||||
@@ -204,7 +204,7 @@ const nodeSearchboxPopoverRef = shallowRef<InstanceType<
|
||||
const settingStore = useSettingStore()
|
||||
const nodeDefStore = useNodeDefStore()
|
||||
const workspaceStore = useWorkspaceStore()
|
||||
const { mode, isBuilderMode } = useAppMode()
|
||||
const { isBuilderMode } = useAppMode()
|
||||
const canvasStore = useCanvasStore()
|
||||
const workflowStore = useWorkflowStore()
|
||||
const executionStore = useExecutionStore()
|
||||
|
||||
@@ -2,7 +2,12 @@ import { computed, ref } from 'vue'
|
||||
|
||||
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)
|
||||
|
||||
@@ -18,13 +23,17 @@ export function useAppMode() {
|
||||
const isBuilderMode = computed(
|
||||
() => 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 isAppMode = computed(
|
||||
() => mode.value === 'app' || mode.value === 'builder:arrange'
|
||||
)
|
||||
const isGraphMode = computed(
|
||||
() => mode.value === 'graph' || mode.value === 'builder:select'
|
||||
() => mode.value === 'graph' || isSelectMode.value
|
||||
)
|
||||
|
||||
function setMode(newMode: AppMode) {
|
||||
@@ -39,6 +48,8 @@ export function useAppMode() {
|
||||
enableAppBuilder,
|
||||
isBuilderMode,
|
||||
isSelectMode,
|
||||
isSelectInputsMode,
|
||||
isSelectOutputsMode,
|
||||
isArrangeMode,
|
||||
isAppMode,
|
||||
isGraphMode,
|
||||
|
||||
@@ -3051,11 +3051,11 @@
|
||||
},
|
||||
"arrange": {
|
||||
"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.",
|
||||
"atLeastOne": "at least one",
|
||||
"outputExamples": "Examples: 'Save Image' or 'Save Video'",
|
||||
"switchToSelectButton": "Switch to Select",
|
||||
"switchToOutputsButton": "Switch to Outputs",
|
||||
"outputs": "Outputs",
|
||||
"resultsLabel": "Results generated from the selected output node(s) will be shown here after running this app"
|
||||
},
|
||||
@@ -3359,16 +3359,18 @@
|
||||
},
|
||||
"builderToolbar": {
|
||||
"label": "App Builder",
|
||||
"select": "Select",
|
||||
"selectDescription": "Choose inputs/outputs",
|
||||
"inputs": "Inputs",
|
||||
"outputs": "Outputs",
|
||||
"inputsDescription": "Choose inputs",
|
||||
"outputsDescription": "Choose outputs",
|
||||
"arrange": "Preview",
|
||||
"arrangeDescription": "Review app layout",
|
||||
"defaultView": "Set a default view",
|
||||
"defaultViewDescription": "Choose how this opens",
|
||||
"connectOutput": "Connect an output",
|
||||
"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.",
|
||||
"switchToSelect": "Switch to Select",
|
||||
"connectOutputBody2": "Switch to the 'Output' step and click on output nodes to add them here.",
|
||||
"switchToOutputs": "Switch to Outputs",
|
||||
"defaultViewTitle": "Set the default view for this workflow",
|
||||
"defaultViewLabel": "By default, this workflow will open as:",
|
||||
"app": "App",
|
||||
|
||||
@@ -365,14 +365,14 @@ describe('useWorkflowService', () => {
|
||||
})
|
||||
const workflow2 = createModeTestWorkflow({
|
||||
path: 'workflows/two.json',
|
||||
activeMode: 'builder:select'
|
||||
activeMode: 'builder:inputs'
|
||||
})
|
||||
|
||||
workflowStore.activeWorkflow = workflow1
|
||||
expect(appMode.mode.value).toBe('app')
|
||||
|
||||
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', () => {
|
||||
const workflow1 = createModeTestWorkflow({
|
||||
path: 'workflows/one.json',
|
||||
activeMode: 'builder:select'
|
||||
activeMode: 'builder:inputs'
|
||||
})
|
||||
const workflow2 = createModeTestWorkflow({
|
||||
path: 'workflows/two.json',
|
||||
@@ -515,13 +515,13 @@ describe('useWorkflowService', () => {
|
||||
})
|
||||
|
||||
workflowStore.activeWorkflow = workflow1
|
||||
expect(appMode.mode.value).toBe('builder:select')
|
||||
expect(appMode.mode.value).toBe('builder:inputs')
|
||||
|
||||
workflowStore.activeWorkflow = workflow2
|
||||
expect(appMode.mode.value).toBe('app')
|
||||
|
||||
workflowStore.activeWorkflow = workflow1
|
||||
expect(appMode.mode.value).toBe('builder:select')
|
||||
expect(appMode.mode.value).toBe('builder:inputs')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -8,7 +8,7 @@ import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
const { id, name } = defineProps<{
|
||||
id: string
|
||||
isSelectMode: boolean
|
||||
isSelectInputsMode: boolean
|
||||
name: string
|
||||
}>()
|
||||
|
||||
@@ -25,7 +25,7 @@ function togglePromotion() {
|
||||
</script>
|
||||
<template>
|
||||
<div
|
||||
v-if="isSelectMode"
|
||||
v-if="isSelectInputsMode"
|
||||
class="col-span-2 flex flex-row pointer-events-auto cursor-pointer gap-1 relative"
|
||||
@pointerdown.capture.stop.prevent="togglePromotion"
|
||||
@click.capture.stop.prevent
|
||||
|
||||
@@ -37,7 +37,7 @@ const { hasOutputs } = storeToRefs(useAppModeStore())
|
||||
</p>
|
||||
|
||||
<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">
|
||||
<template #atLeastOne>
|
||||
@@ -50,8 +50,8 @@ const { hasOutputs } = storeToRefs(useAppModeStore())
|
||||
<p class="mt-0 p-0">{{ t('linearMode.arrange.outputExamples') }}</p>
|
||||
</div>
|
||||
<div class="flex flex-row gap-2">
|
||||
<Button variant="primary" size="lg" @click="setMode('builder:select')">
|
||||
{{ t('linearMode.arrange.switchToSelectButton') }}
|
||||
<Button variant="primary" size="lg" @click="setMode('builder:outputs')">
|
||||
{{ t('linearMode.arrange.switchToOutputsButton') }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -51,7 +51,9 @@
|
||||
@drop.stop.prevent="handleDrop"
|
||||
>
|
||||
<AppOutput
|
||||
v-if="lgraphNode?.constructor?.nodeData?.output_node && isSelectMode"
|
||||
v-if="
|
||||
lgraphNode?.constructor?.nodeData?.output_node && isSelectOutputsMode
|
||||
"
|
||||
:id="nodeData.id"
|
||||
/>
|
||||
<div
|
||||
@@ -337,7 +339,7 @@ const { nodeData, error = null } = defineProps<LGraphNodeProps>()
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const { isSelectMode } = useAppMode()
|
||||
const { isSelectMode, isSelectOutputsMode } = useAppMode()
|
||||
const settingStore = useSettingStore()
|
||||
|
||||
const { handleNodeCollapse, handleNodeTitleUpdate, handleNodeRightClick } =
|
||||
|
||||
@@ -53,7 +53,7 @@
|
||||
/>
|
||||
</div>
|
||||
<!-- Widget Component -->
|
||||
<AppInput :id="widget.id" :name="widget.name" :is-select-mode>
|
||||
<AppInput :id="widget.id" :name="widget.name" :is-select-inputs-mode>
|
||||
<component
|
||||
:is="widget.vueComponent"
|
||||
v-model="widget.value"
|
||||
@@ -123,7 +123,7 @@ const { nodeData } = defineProps<NodeWidgetsProps>()
|
||||
|
||||
const { shouldHandleNodePointerEvents, forwardEventToCanvas } =
|
||||
useCanvasInteractions()
|
||||
const { isSelectMode } = useAppMode()
|
||||
const { isSelectInputsMode } = useAppMode()
|
||||
const canvasStore = useCanvasStore()
|
||||
const { bringNodeToFront } = useNodeZIndex()
|
||||
const promotionStore = usePromotionStore()
|
||||
|
||||
@@ -50,7 +50,7 @@ vi.mock('@/components/builder/useEmptyWorkflowDialog', () => ({
|
||||
import { useAppModeStore } from './appModeStore'
|
||||
|
||||
function createBuilderWorkflow(
|
||||
activeMode: string = 'builder:select'
|
||||
activeMode: string = 'builder:inputs'
|
||||
): LoadedComfyWorkflow {
|
||||
const workflow = new ComfyWorkflowClass({
|
||||
path: 'workflows/test.json',
|
||||
@@ -88,29 +88,29 @@ describe('appModeStore', () => {
|
||||
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')
|
||||
|
||||
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')
|
||||
store.selectedOutputs.push(1)
|
||||
|
||||
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')
|
||||
|
||||
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', () => {
|
||||
@@ -141,7 +141,7 @@ describe('appModeStore', () => {
|
||||
const options = getDialogOptions()
|
||||
|
||||
// Move to builder so onDismiss must actually transition back
|
||||
workflowStore.activeWorkflow!.activeMode = 'builder:select'
|
||||
workflowStore.activeWorkflow!.activeMode = 'builder:inputs'
|
||||
|
||||
options.onDismiss()
|
||||
|
||||
@@ -156,7 +156,7 @@ describe('appModeStore', () => {
|
||||
|
||||
options.onEnterBuilder()
|
||||
|
||||
expect(workflowStore.activeWorkflow!.activeMode).toBe('builder:select')
|
||||
expect(workflowStore.activeWorkflow!.activeMode).toBe('builder:inputs')
|
||||
})
|
||||
|
||||
it('onEnterBuilder shows dialog again when no nodes', () => {
|
||||
|
||||
@@ -13,7 +13,7 @@ import { resolveNode } from '@/utils/litegraphUtil'
|
||||
export const useAppModeStore = defineStore('appMode', () => {
|
||||
const { getCanvas } = useCanvasStore()
|
||||
const workflowStore = useWorkflowStore()
|
||||
const { mode, setMode, isBuilderMode } = useAppMode()
|
||||
const { mode, setMode, isBuilderMode, isSelectMode } = useAppMode()
|
||||
const emptyWorkflowDialog = useEmptyWorkflowDialog()
|
||||
|
||||
const selectedInputs = reactive<[NodeId, string][]>([])
|
||||
@@ -78,20 +78,17 @@ export const useAppModeStore = defineStore('appMode', () => {
|
||||
)
|
||||
|
||||
let unwatch: () => void | undefined
|
||||
watch(
|
||||
() => mode.value === 'builder:select',
|
||||
(inSelect) => {
|
||||
const { state } = getCanvas()
|
||||
if (!state) return
|
||||
state.readOnly = inSelect
|
||||
unwatch?.()
|
||||
if (inSelect)
|
||||
unwatch = watch(
|
||||
() => state.readOnly,
|
||||
() => (state.readOnly = true)
|
||||
)
|
||||
}
|
||||
)
|
||||
watch(isSelectMode, (inSelect) => {
|
||||
const { state } = getCanvas()
|
||||
if (!state) return
|
||||
state.readOnly = inSelect
|
||||
unwatch?.()
|
||||
if (inSelect)
|
||||
unwatch = watch(
|
||||
() => state.readOnly,
|
||||
() => (state.readOnly = true)
|
||||
)
|
||||
})
|
||||
|
||||
function enterBuilder() {
|
||||
if (!app.rootGraph?.nodes?.length) {
|
||||
@@ -105,7 +102,7 @@ export const useAppModeStore = defineStore('appMode', () => {
|
||||
setMode(
|
||||
mode.value === 'app' && hasOutputs.value
|
||||
? 'builder:arrange'
|
||||
: 'builder:select'
|
||||
: 'builder:inputs'
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user