mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-30 11:11:53 +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 { 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>
|
||||||
|
|||||||
@@ -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 () => {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 } =
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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', () => {
|
||||||
|
|||||||
@@ -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'
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user