From c090d189f08d8c953f18a9a0f23e56aeca0bcccd Mon Sep 17 00:00:00 2001 From: pythongosssss <125205205+pythongosssss@users.noreply.github.com> Date: Fri, 27 Feb 2026 10:32:44 +0000 Subject: [PATCH] Render app builder in arrange mode (#9260) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 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) image ┆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) --- .../LiteGraphCanvasSplitterOverlay.vue | 30 ++++-- src/components/builder/AppBuilder.vue | 60 +++++++++++- .../rightSidePanel/parameters/WidgetItem.vue | 7 +- src/composables/useAppMode.ts | 6 +- src/constants/splitterConstants.ts | 11 +++ src/views/LinearView.vue | 97 ++++++++++++++++--- 6 files changed, 183 insertions(+), 28 deletions(-) create mode 100644 src/constants/splitterConstants.ts diff --git a/src/components/LiteGraphCanvasSplitterOverlay.vue b/src/components/LiteGraphCanvasSplitterOverlay.vue index cbf257146b..316fc258d3 100644 --- a/src/components/LiteGraphCanvasSplitterOverlay.vue +++ b/src/components/LiteGraphCanvasSplitterOverlay.vue @@ -18,7 +18,7 @@ @@ -35,8 +35,10 @@ ) : 'bg-comfy-menu-bg pointer-events-auto' " - :min-size="sidebarLocation === 'left' ? 10 : 15" - :size="20" + :min-size=" + sidebarLocation === 'left' ? SIDEBAR_MIN_SIZE : BUILDER_MIN_SIZE + " + :size="SIDE_PANEL_SIZE" :style="firstPanelStyle" :role="sidebarLocation === 'left' ? 'complementary' : undefined" :aria-label=" @@ -54,7 +56,7 @@ - + import { remove } from 'es-toolkit' -import { computed, ref, toValue } from 'vue' +import { computed, provide, ref, toValue, watchEffect } from 'vue' import type { MaybeRef } from 'vue' import { useI18n } from 'vue-i18n' import DraggableList from '@/components/common/DraggableList.vue' import IoItem from '@/components/builder/IoItem.vue' import PropertiesAccordionItem from '@/components/rightSidePanel/layout/PropertiesAccordionItem.vue' +import WidgetItem from '@/components/rightSidePanel/parameters/WidgetItem.vue' import Button from '@/components/ui/button/Button.vue' import { LiteGraph } from '@/lib/litegraph/src/litegraph' import type { LGraphNode, NodeId } from '@/lib/litegraph/src/LGraphNode' @@ -23,8 +24,10 @@ import TransformPane from '@/renderer/core/layout/transform/TransformPane.vue' import { app } from '@/scripts/app' import { DOMWidgetImpl } from '@/scripts/domWidget' import { useDialogService } from '@/services/dialogService' +import { useAppMode } from '@/composables/useAppMode' import { useAppModeStore } from '@/stores/appModeStore' import { cn } from '@/utils/tailwindUtil' +import { HideLayoutFieldKey } from '@/types/widgetTypes' type BoundStyle = { top: string; left: string; width: string; height: string } @@ -36,10 +39,36 @@ const workflowStore = useWorkflowStore() const { t } = useI18n() const canvas: LGraphCanvas = canvasStore.getCanvas() +const { mode, isArrangeMode } = useAppMode() const hoveringSelectable = ref(false) +provide(HideLayoutFieldKey, true) + workflowStore.activeWorkflow?.changeTracker?.reset() +// Prune stale entries whose node/widget no longer exists, so the +// DraggableList model always matches the rendered items. +watchEffect(() => { + const valid = appModeStore.selectedInputs.filter(([nodeId, widgetName]) => { + const node = app.rootGraph.getNodeById(nodeId) + return node?.widgets?.some((w) => w.name === widgetName) + }) + if (valid.length < appModeStore.selectedInputs.length) { + appModeStore.selectedInputs = valid + } +}) + +const arrangeInputs = computed(() => + appModeStore.selectedInputs + .map(([nodeId, widgetName]) => { + const node = app.rootGraph.getNodeById(nodeId) + const widget = node?.widgets?.find((w) => w.name === widgetName) + if (!node || !widget) return null + return { nodeId, widgetName, node, widget } + }) + .filter((item): item is NonNullable => item !== null) +) + const inputsWithState = computed(() => appModeStore.selectedInputs.map(([nodeId, widgetName]) => { const node = app.rootGraph.getNodeById(nodeId) @@ -179,12 +208,36 @@ const renderedInputs = computed<[string, MaybeRef | undefined][]>(