Skip to content

Commit 9d4ae0d

Browse files
pythongosssssDrJKL
authored andcommitted
Render app builder in arrange mode (#9260)
## Summary Adds app builder in arrange/preview mode with re-orderable widgets, maintaining size (as much as possible) between the select + preview steps ## Changes - **What**: - Extract sidebar size constants for sharing between canvas splitter + app mode - Add widget list using DraggableList and render inert WidgetItems ## Screenshots (if applicable) <img width="1391" height="923" alt="image" src="https://github.com/user-attachments/assets/3e17eafe-db1e-40a3-83b5-15a7d0672909" /> ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-9260-Render-app-builder-in-arrange-mode-3136d73d365081ef875acab683d01d9e) by [Unito](https://www.unito.io)
1 parent b9c4675 commit 9d4ae0d

File tree

6 files changed

+183
-28
lines changed

6 files changed

+183
-28
lines changed

src/components/LiteGraphCanvasSplitterOverlay.vue

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
<Splitter
1919
:key="splitterRefreshKey"
2020
class="bg-transparent pointer-events-none border-none flex-1 overflow-hidden"
21-
:state-key="sidebarStateKey"
21+
:state-key="isSelectMode ? 'builder-splitter' : sidebarStateKey"
2222
state-storage="local"
2323
@resizestart="onResizestart"
2424
>
@@ -35,8 +35,10 @@
3535
)
3636
: 'bg-comfy-menu-bg pointer-events-auto'
3737
"
38-
:min-size="sidebarLocation === 'left' ? 10 : 15"
39-
:size="20"
38+
:min-size="
39+
sidebarLocation === 'left' ? SIDEBAR_MIN_SIZE : BUILDER_MIN_SIZE
40+
"
41+
:size="SIDE_PANEL_SIZE"
4042
:style="firstPanelStyle"
4143
:role="sidebarLocation === 'left' ? 'complementary' : undefined"
4244
:aria-label="
@@ -54,7 +56,7 @@
5456
</SplitterPanel>
5557

5658
<!-- Main panel (always present) -->
57-
<SplitterPanel :size="80" class="flex flex-col">
59+
<SplitterPanel :size="CENTER_PANEL_SIZE" class="flex flex-col">
5860
<slot name="topmenu" :sidebar-panel-visible />
5961

6062
<Splitter
@@ -95,8 +97,10 @@
9597
)
9698
: 'bg-comfy-menu-bg pointer-events-auto'
9799
"
98-
:min-size="sidebarLocation === 'right' ? 10 : 15"
99-
:size="20"
100+
:min-size="
101+
sidebarLocation === 'right' ? SIDEBAR_MIN_SIZE : BUILDER_MIN_SIZE
102+
"
103+
:size="SIDE_PANEL_SIZE"
100104
:style="lastPanelStyle"
101105
:role="sidebarLocation === 'right' ? 'complementary' : undefined"
102106
:aria-label="
@@ -123,8 +127,14 @@ import SplitterPanel from 'primevue/splitterpanel'
123127
import { computed } from 'vue'
124128
import { useI18n } from 'vue-i18n'
125129
126-
import { useSettingStore } from '@/platform/settings/settingStore'
127130
import { useAppMode } from '@/composables/useAppMode'
131+
import {
132+
BUILDER_MIN_SIZE,
133+
CENTER_PANEL_SIZE,
134+
SIDEBAR_MIN_SIZE,
135+
SIDE_PANEL_SIZE
136+
} from '@/constants/splitterConstants'
137+
import { useSettingStore } from '@/platform/settings/settingStore'
128138
import { useBottomPanelStore } from '@/stores/workspace/bottomPanelStore'
129139
import { useRightSidePanelStore } from '@/stores/workspace/rightSidePanelStore'
130140
import { useSidebarTabStore } from '@/stores/workspace/sidebarTabStore'
@@ -145,12 +155,12 @@ const unifiedWidth = computed(() =>
145155
146156
const { focusMode } = storeToRefs(workspaceStore)
147157
148-
const { mode } = useAppMode()
158+
const { isSelectMode } = useAppMode()
149159
const { activeSidebarTabId, activeSidebarTab } = storeToRefs(sidebarTabStore)
150160
const { bottomPanelVisible } = storeToRefs(useBottomPanelStore())
151161
const { isOpen: rightSidePanelVisible } = storeToRefs(rightSidePanelStore)
152162
const showOffsideSplitter = computed(
153-
() => rightSidePanelVisible.value || mode.value === 'builder:select'
163+
() => rightSidePanelVisible.value || isSelectMode.value
154164
)
155165
156166
const sidebarPanelVisible = computed(() => activeSidebarTab.value !== null)
@@ -174,7 +184,7 @@ function onResizestart({ originalEvent: event }: SplitterResizeStartEvent) {
174184
* to recalculate the width and panel order
175185
*/
176186
const splitterRefreshKey = computed(() => {
177-
return `main-splitter${rightSidePanelVisible.value ? '-with-right-panel' : ''}-${sidebarLocation.value}`
187+
return `main-splitter${rightSidePanelVisible.value ? '-with-right-panel' : ''}${isSelectMode.value ? '-builder' : ''}-${sidebarLocation.value}`
178188
})
179189
180190
const firstPanelStyle = computed(() => {

src/components/builder/AppBuilder.vue

Lines changed: 57 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
<script setup lang="ts">
22
import { remove } from 'es-toolkit'
3-
import { computed, ref, toValue } from 'vue'
3+
import { computed, provide, ref, toValue, watchEffect } from 'vue'
44
import type { MaybeRef } from 'vue'
55
import { useI18n } from 'vue-i18n'
66
77
import DraggableList from '@/components/common/DraggableList.vue'
88
import IoItem from '@/components/builder/IoItem.vue'
99
import PropertiesAccordionItem from '@/components/rightSidePanel/layout/PropertiesAccordionItem.vue'
10+
import WidgetItem from '@/components/rightSidePanel/parameters/WidgetItem.vue'
1011
import Button from '@/components/ui/button/Button.vue'
1112
import { LiteGraph } from '@/lib/litegraph/src/litegraph'
1213
import type { LGraphNode, NodeId } from '@/lib/litegraph/src/LGraphNode'
@@ -23,8 +24,10 @@ import TransformPane from '@/renderer/core/layout/transform/TransformPane.vue'
2324
import { app } from '@/scripts/app'
2425
import { DOMWidgetImpl } from '@/scripts/domWidget'
2526
import { useDialogService } from '@/services/dialogService'
27+
import { useAppMode } from '@/composables/useAppMode'
2628
import { useAppModeStore } from '@/stores/appModeStore'
2729
import { cn } from '@/utils/tailwindUtil'
30+
import { HideLayoutFieldKey } from '@/types/widgetTypes'
2831
2932
type BoundStyle = { top: string; left: string; width: string; height: string }
3033
@@ -36,10 +39,36 @@ const workflowStore = useWorkflowStore()
3639
const { t } = useI18n()
3740
const canvas: LGraphCanvas = canvasStore.getCanvas()
3841
42+
const { mode, isArrangeMode } = useAppMode()
3943
const hoveringSelectable = ref(false)
4044
45+
provide(HideLayoutFieldKey, true)
46+
4147
workflowStore.activeWorkflow?.changeTracker?.reset()
4248
49+
// Prune stale entries whose node/widget no longer exists, so the
50+
// DraggableList model always matches the rendered items.
51+
watchEffect(() => {
52+
const valid = appModeStore.selectedInputs.filter(([nodeId, widgetName]) => {
53+
const node = app.rootGraph.getNodeById(nodeId)
54+
return node?.widgets?.some((w) => w.name === widgetName)
55+
})
56+
if (valid.length < appModeStore.selectedInputs.length) {
57+
appModeStore.selectedInputs = valid
58+
}
59+
})
60+
61+
const arrangeInputs = computed(() =>
62+
appModeStore.selectedInputs
63+
.map(([nodeId, widgetName]) => {
64+
const node = app.rootGraph.getNodeById(nodeId)
65+
const widget = node?.widgets?.find((w) => w.name === widgetName)
66+
if (!node || !widget) return null
67+
return { nodeId, widgetName, node, widget }
68+
})
69+
.filter((item): item is NonNullable<typeof item> => item !== null)
70+
)
71+
4372
const inputsWithState = computed(() =>
4473
appModeStore.selectedInputs.map(([nodeId, widgetName]) => {
4574
const node = app.rootGraph.getNodeById(nodeId)
@@ -179,12 +208,36 @@ const renderedInputs = computed<[string, MaybeRef<BoundStyle> | undefined][]>(
179208
</script>
180209
<template>
181210
<div class="flex font-bold p-2 border-border-subtle border-b items-center">
182-
{{ t('linearMode.builder.title') }}
211+
{{
212+
isArrangeMode ? t('nodeHelpPage.inputs') : t('linearMode.builder.title')
213+
}}
183214
<Button class="ml-auto" @click="appModeStore.exitBuilder">
184215
{{ t('linearMode.builder.exit') }}
185216
</Button>
186217
</div>
218+
<DraggableList
219+
v-if="isArrangeMode"
220+
v-slot="{ dragClass }"
221+
v-model="appModeStore.selectedInputs"
222+
>
223+
<div
224+
v-for="{ nodeId, widgetName, node, widget } in arrangeInputs"
225+
:key="`${nodeId}: ${widgetName}`"
226+
:class="cn(dragClass, 'p-2 my-2 pointer-events-auto')"
227+
:aria-label="`${widget.label ?? widgetName} — ${node.title}`"
228+
>
229+
<div class="pointer-events-none" inert>
230+
<WidgetItem
231+
:widget="widget"
232+
:node="node"
233+
show-node-name
234+
hidden-widget-actions
235+
/>
236+
</div>
237+
</div>
238+
</DraggableList>
187239
<PropertiesAccordionItem
240+
v-else
188241
:label="t('nodeHelpPage.inputs')"
189242
enable-empty-state
190243
:disabled="!appModeStore.selectedInputs.length"
@@ -232,6 +285,7 @@ const renderedInputs = computed<[string, MaybeRef<BoundStyle> | undefined][]>(
232285
</DraggableList>
233286
</PropertiesAccordionItem>
234287
<PropertiesAccordionItem
288+
v-if="!isArrangeMode"
235289
:label="t('nodeHelpPage.outputs')"
236290
enable-empty-state
237291
:disabled="!appModeStore.selectedOutputs.length"
@@ -274,7 +328,7 @@ const renderedInputs = computed<[string, MaybeRef<BoundStyle> | undefined][]>(
274328
</DraggableList>
275329
</PropertiesAccordionItem>
276330

277-
<Teleport to="body">
331+
<Teleport v-if="mode === 'builder:select'" to="body">
278332
<div
279333
:class="
280334
cn(

src/components/rightSidePanel/parameters/WidgetItem.vue

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ const {
3434
node,
3535
isDraggable = false,
3636
hiddenFavoriteIndicator = false,
37+
hiddenWidgetActions = false,
3738
showNodeName = false,
3839
parents = [],
3940
isShownOnParents = false
@@ -42,6 +43,7 @@ const {
4243
node: LGraphNode
4344
isDraggable?: boolean
4445
hiddenFavoriteIndicator?: boolean
46+
hiddenWidgetActions?: boolean
4547
showNodeName?: boolean
4648
parents?: SubgraphNode[]
4749
isShownOnParents?: boolean
@@ -170,7 +172,10 @@ const displayLabel = customRef((track, trigger) => {
170172
>
171173
{{ sourceNodeName }}
172174
</span>
173-
<div class="flex items-center gap-1 shrink-0 pointer-events-auto">
175+
<div
176+
v-if="!hiddenWidgetActions"
177+
class="flex items-center gap-1 shrink-0 pointer-events-auto"
178+
>
174179
<WidgetActions
175180
v-model:label="displayLabel"
176181
:widget="widget"

src/composables/useAppMode.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,10 @@ export function useAppMode() {
1616
)
1717

1818
const isBuilderMode = computed(
19-
() => mode.value === 'builder:select' || mode.value === 'builder:arrange'
19+
() => isSelectMode.value || isArrangeMode.value
2020
)
21+
const isSelectMode = computed(() => mode.value === 'builder:select')
22+
const isArrangeMode = computed(() => mode.value === 'builder:arrange')
2123
const isAppMode = computed(
2224
() => mode.value === 'app' || mode.value === 'builder:arrange'
2325
)
@@ -36,6 +38,8 @@ export function useAppMode() {
3638
mode,
3739
enableAppBuilder,
3840
isBuilderMode,
41+
isSelectMode,
42+
isArrangeMode,
3943
isAppMode,
4044
isGraphMode,
4145
setMode

src/constants/splitterConstants.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
/** Default panel size (%) for sidebar and builder panels */
2+
export const SIDE_PANEL_SIZE = 20
3+
4+
/** Default panel size (%) for the center/main panel */
5+
export const CENTER_PANEL_SIZE = 80
6+
7+
/** Minimum panel size (%) for the sidebar */
8+
export const SIDEBAR_MIN_SIZE = 10
9+
10+
/** Minimum panel size (%) for the builder panel */
11+
export const BUILDER_MIN_SIZE = 15

0 commit comments

Comments
 (0)