From ef7575b8d6c521c2ee161968379e87fc30f3bc02 Mon Sep 17 00:00:00 2001 From: Alexander Brown Date: Thu, 11 Sep 2025 17:07:19 -0700 Subject: [PATCH 1/4] Revert "chore(lint): make ESLint concurrency configurable via pnpm config" (#5499) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Revert "chore(lint): make ESLint concurrency configurable via pnpm config (#5…" This reverts commit 999705329081d9f8bb2ec75cac3afaa4d95077bd. * chore: Remove --concurrency --- .env_example | 1 - package.json | 11 ++++------- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/.env_example b/.env_example index a6987ae26..520521fe0 100644 --- a/.env_example +++ b/.env_example @@ -33,4 +33,3 @@ DISABLE_VUE_PLUGINS=false # Algolia credentials required for developing with the new custom node manager. ALGOLIA_APP_ID=4E0RO38HS8 ALGOLIA_API_KEY=684d998c36b67a9a9fce8fc2d8860579 - diff --git a/package.json b/package.json index 49884f34c..f4e6035d3 100644 --- a/package.json +++ b/package.json @@ -25,10 +25,10 @@ "preinstall": "npx only-allow pnpm", "prepare": "husky || true && git config blame.ignoreRevsFile .git-blame-ignore-revs || true", "preview": "nx preview", - "lint": "eslint src --cache --concurrency=$npm_package_config_eslint_concurrency", - "lint:fix": "eslint src --fix --cache --concurrency=$npm_package_config_eslint_concurrency", - "lint:no-cache": "eslint src --concurrency=$npm_package_config_eslint_concurrency", - "lint:fix:no-cache": "eslint src --fix --concurrency=$npm_package_config_eslint_concurrency", + "lint": "eslint src --cache", + "lint:fix": "eslint src --cache --fix", + "lint:no-cache": "eslint src", + "lint:fix:no-cache": "eslint src --fix", "knip": "knip --cache", "knip:no-cache": "knip", "locale": "lobe-i18n locale", @@ -37,9 +37,6 @@ "storybook": "nx storybook -p 6006", "build-storybook": "storybook build" }, - "config": { - "eslint_concurrency": "4" - }, "devDependencies": { "@eslint/js": "^9.8.0", "@iconify-json/lucide": "^1.2.66", From 54cbf42a84cb9132eef5d3afeb1f3916bc3dee20 Mon Sep 17 00:00:00 2001 From: Alexander Brown Date: Thu, 11 Sep 2025 17:11:55 -0700 Subject: [PATCH 2/4] fix: Missing `.value` led to the release dot always showing (#5500) --- src/components/sidebar/SidebarHelpCenterIcon.vue | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/sidebar/SidebarHelpCenterIcon.vue b/src/components/sidebar/SidebarHelpCenterIcon.vue index 06662f581..f778d3ee6 100644 --- a/src/components/sidebar/SidebarHelpCenterIcon.vue +++ b/src/components/sidebar/SidebarHelpCenterIcon.vue @@ -88,8 +88,8 @@ const { shouldShowRedDot: shouldShowConflictRedDot, markConflictsAsSeen } = useConflictAcknowledgment() // Use either release red dot or conflict red dot -const shouldShowRedDot = computed(() => { - const releaseRedDot = showReleaseRedDot +const shouldShowRedDot = computed((): boolean => { + const releaseRedDot = showReleaseRedDot.value return releaseRedDot || shouldShowConflictRedDot.value }) From c051c3a507e6002715399c43116f645524b449ee Mon Sep 17 00:00:00 2001 From: Johnpaul Chiwetelu <49923152+Myestery@users.noreply.github.com> Date: Fri, 12 Sep 2025 01:25:23 +0100 Subject: [PATCH 3/4] Drag Multiple Vue Nodes (#5459) * feat: enhance dragging functionality to support multiple selected nodes * feat: enhance node selection handling to support drag state detection * feat: enhance node selection handling to support drag state detection * fix: update event trigger from pointer down to pointer up in LGraphNode tests --- .../vueNodes/components/LGraphNode.vue | 21 ++++++++-- .../composables/useNodeEventHandlers.ts | 19 ++++++--- .../vueNodes/layout/useNodeLayout.ts | 39 ++++++++++++++++++- .../vueNodes/components/LGraphNode.spec.ts | 6 +-- .../composables/useNodeEventHandlers.test.ts | 16 ++++---- 5 files changed, 79 insertions(+), 22 deletions(-) diff --git a/src/renderer/extensions/vueNodes/components/LGraphNode.vue b/src/renderer/extensions/vueNodes/components/LGraphNode.vue index 54632c087..2b01e3260 100644 --- a/src/renderer/extensions/vueNodes/components/LGraphNode.vue +++ b/src/renderer/extensions/vueNodes/components/LGraphNode.vue @@ -168,13 +168,18 @@ interface LGraphNodeProps { const props = defineProps() const emit = defineEmits<{ - 'node-click': [event: PointerEvent, nodeData: VueNodeData] + 'node-click': [ + event: PointerEvent, + nodeData: VueNodeData, + wasDragging: boolean + ] 'slot-click': [ event: PointerEvent, nodeData: VueNodeData, slotIndex: number, isInput: boolean ] + dragStart: [event: DragEvent, nodeData: VueNodeData] 'update:collapsed': [nodeId: string, collapsed: boolean] 'update:title': [nodeId: string, newTitle: string] }>() @@ -231,6 +236,10 @@ const isDragging = ref(false) const dragStyle = computed(() => ({ cursor: isDragging.value ? 'grabbing' : 'grab' })) +const lastY = ref(0) +const lastX = ref(0) +// Treat tiny pointer jitter as a click, not a drag +const DRAG_THRESHOLD_PX = 4 // Track collapsed state const isCollapsed = ref(props.nodeData.flags?.collapsed ?? false) @@ -276,9 +285,8 @@ const handlePointerDown = (event: PointerEvent) => { // Start drag using layout system isDragging.value = true startDrag(event) - - // Emit node-click for selection handling in GraphCanvas - emit('node-click', event, props.nodeData) + lastY.value = event.clientY + lastX.value = event.clientX } const handlePointerMove = (event: PointerEvent) => { @@ -292,6 +300,11 @@ const handlePointerUp = (event: PointerEvent) => { isDragging.value = false void endDrag(event) } + // Emit node-click for selection handling in GraphCanvas + const dx = event.clientX - lastX.value + const dy = event.clientY - lastY.value + const wasDragging = Math.hypot(dx, dy) > DRAG_THRESHOLD_PX + emit('node-click', event, props.nodeData, wasDragging) } const handleCollapse = () => { diff --git a/src/renderer/extensions/vueNodes/composables/useNodeEventHandlers.ts b/src/renderer/extensions/vueNodes/composables/useNodeEventHandlers.ts index 5b354bf25..8cd7e2e5a 100644 --- a/src/renderer/extensions/vueNodes/composables/useNodeEventHandlers.ts +++ b/src/renderer/extensions/vueNodes/composables/useNodeEventHandlers.ts @@ -26,7 +26,11 @@ export function useNodeEventHandlers(nodeManager: Ref) { * Handle node selection events * Supports single selection and multi-select with Ctrl/Cmd */ - const handleNodeSelect = (event: PointerEvent, nodeData: VueNodeData) => { + const handleNodeSelect = ( + event: PointerEvent, + nodeData: VueNodeData, + wasDragging: boolean + ) => { if (!canvasStore.canvas || !nodeManager.value) return const node = nodeManager.value.getNode(nodeData.id) @@ -42,9 +46,12 @@ export function useNodeEventHandlers(nodeManager: Ref) { canvasStore.canvas.select(node) } } else { + // If it wasn't a drag: single-select the node + if (!wasDragging) { + canvasStore.canvas.deselectAll() + canvasStore.canvas.select(node) + } // Regular click -> single select - canvasStore.canvas.deselectAll() - canvasStore.canvas.select(node) } // Bring node to front when clicked (similar to LiteGraph behavior) @@ -107,7 +114,7 @@ export function useNodeEventHandlers(nodeManager: Ref) { // TODO: add custom double-click behavior here // For now, ensure node is selected if (!node.selected) { - handleNodeSelect(event, nodeData) + handleNodeSelect(event, nodeData, false) } } @@ -126,7 +133,7 @@ export function useNodeEventHandlers(nodeManager: Ref) { // Select the node if not already selected if (!node.selected) { - handleNodeSelect(event, nodeData) + handleNodeSelect(event, nodeData, false) } // Let LiteGraph handle the context menu @@ -151,7 +158,7 @@ export function useNodeEventHandlers(nodeManager: Ref) { metaKey: event.metaKey, bubbles: true }) - handleNodeSelect(syntheticEvent, nodeData) + handleNodeSelect(syntheticEvent, nodeData, false) } // Set drag data for potential drop operations diff --git a/src/renderer/extensions/vueNodes/layout/useNodeLayout.ts b/src/renderer/extensions/vueNodes/layout/useNodeLayout.ts index 407a14243..995d83d6f 100644 --- a/src/renderer/extensions/vueNodes/layout/useNodeLayout.ts +++ b/src/renderer/extensions/vueNodes/layout/useNodeLayout.ts @@ -6,6 +6,7 @@ */ import { computed, inject } from 'vue' +import { SelectedNodeIdsKey } from '@/renderer/core/canvas/injectionKeys' import { useLayoutMutations } from '@/renderer/core/layout/operations/layoutMutations' import { layoutStore } from '@/renderer/core/layout/store/layoutStore' import { LayoutSource, type Point } from '@/renderer/core/layout/types' @@ -54,6 +55,9 @@ export function useNodeLayout(nodeId: string) { let isDragging = false let dragStartPos: Point | null = null let dragStartMouse: Point | null = null + let otherSelectedNodesStartPositions: Map | null = null + + const selectedNodeIds = inject(SelectedNodeIdsKey, null) /** * Start dragging the node @@ -65,6 +69,24 @@ export function useNodeLayout(nodeId: string) { dragStartPos = { ...position.value } dragStartMouse = { x: event.clientX, y: event.clientY } + // capture the starting positions of all other selected nodes + if (selectedNodeIds?.value?.has(nodeId) && selectedNodeIds.value.size > 1) { + otherSelectedNodesStartPositions = new Map() + + // Iterate through all selected node IDs + for (const id of selectedNodeIds.value) { + // Skip the current node being dragged + if (id === nodeId) continue + + const nodeLayout = layoutStore.getNodeLayoutRef(id).value + if (nodeLayout) { + otherSelectedNodesStartPositions.set(id, { ...nodeLayout.position }) + } + } + } else { + otherSelectedNodesStartPositions = null + } + // Set mutation source mutations.setSource(LayoutSource.Vue) @@ -95,7 +117,7 @@ export function useNodeLayout(nodeId: string) { y: canvasWithDelta.y - canvasOrigin.y } - // Calculate new position + // Calculate new position for the current node const newPosition = { x: dragStartPos.x + canvasDelta.x, y: dragStartPos.y + canvasDelta.y @@ -103,6 +125,20 @@ export function useNodeLayout(nodeId: string) { // Apply mutation through the layout system mutations.moveNode(nodeId, newPosition) + + // If we're dragging multiple selected nodes, move them all together + if ( + otherSelectedNodesStartPositions && + otherSelectedNodesStartPositions.size > 0 + ) { + for (const [otherNodeId, startPos] of otherSelectedNodesStartPositions) { + const newOtherPosition = { + x: startPos.x + canvasDelta.x, + y: startPos.y + canvasDelta.y + } + mutations.moveNode(otherNodeId, newOtherPosition) + } + } } /** @@ -114,6 +150,7 @@ export function useNodeLayout(nodeId: string) { isDragging = false dragStartPos = null dragStartMouse = null + otherSelectedNodesStartPositions = null // Release pointer const target = event.target as HTMLElement diff --git a/tests-ui/tests/renderer/extensions/vueNodes/components/LGraphNode.spec.ts b/tests-ui/tests/renderer/extensions/vueNodes/components/LGraphNode.spec.ts index 7acebce00..861a301ab 100644 --- a/tests-ui/tests/renderer/extensions/vueNodes/components/LGraphNode.spec.ts +++ b/tests-ui/tests/renderer/extensions/vueNodes/components/LGraphNode.spec.ts @@ -103,13 +103,13 @@ describe('LGraphNode', () => { expect(wrapper.classes()).toContain('animate-pulse') }) - it('should emit node-click event on pointer down', async () => { + it('should emit node-click event on pointer up', async () => { const wrapper = mountLGraphNode({ nodeData: mockNodeData }) - await wrapper.trigger('pointerdown') + await wrapper.trigger('pointerup') expect(wrapper.emitted('node-click')).toHaveLength(1) - expect(wrapper.emitted('node-click')?.[0]).toHaveLength(2) + expect(wrapper.emitted('node-click')?.[0]).toHaveLength(3) expect(wrapper.emitted('node-click')?.[0][1]).toEqual(mockNodeData) }) }) diff --git a/tests-ui/tests/renderer/extensions/vueNodes/composables/useNodeEventHandlers.test.ts b/tests-ui/tests/renderer/extensions/vueNodes/composables/useNodeEventHandlers.test.ts index 51a0ae235..6d33ad9b7 100644 --- a/tests-ui/tests/renderer/extensions/vueNodes/composables/useNodeEventHandlers.test.ts +++ b/tests-ui/tests/renderer/extensions/vueNodes/composables/useNodeEventHandlers.test.ts @@ -110,7 +110,7 @@ describe('useNodeEventHandlers', () => { metaKey: false }) - handleNodeSelect(event, testNodeData) + handleNodeSelect(event, testNodeData, false) expect(mockCanvas.deselectAll).toHaveBeenCalledOnce() expect(mockCanvas.select).toHaveBeenCalledWith(mockNode) @@ -130,7 +130,7 @@ describe('useNodeEventHandlers', () => { metaKey: false }) - handleNodeSelect(ctrlClickEvent, testNodeData) + handleNodeSelect(ctrlClickEvent, testNodeData, false) expect(mockCanvas.deselectAll).not.toHaveBeenCalled() expect(mockCanvas.select).toHaveBeenCalledWith(mockNode) @@ -149,7 +149,7 @@ describe('useNodeEventHandlers', () => { metaKey: false }) - handleNodeSelect(ctrlClickEvent, testNodeData) + handleNodeSelect(ctrlClickEvent, testNodeData, false) expect(mockCanvas.deselect).toHaveBeenCalledWith(mockNode) expect(mockCanvas.select).not.toHaveBeenCalled() @@ -167,7 +167,7 @@ describe('useNodeEventHandlers', () => { metaKey: true }) - handleNodeSelect(metaClickEvent, testNodeData) + handleNodeSelect(metaClickEvent, testNodeData, false) expect(mockCanvas.select).toHaveBeenCalledWith(mockNode) expect(mockCanvas.deselectAll).not.toHaveBeenCalled() @@ -180,7 +180,7 @@ describe('useNodeEventHandlers', () => { mockNode.flags.pinned = false const event = new PointerEvent('pointerdown') - handleNodeSelect(event, testNodeData) + handleNodeSelect(event, testNodeData, false) expect(mockLayoutMutations.bringNodeToFront).toHaveBeenCalledWith( 'node-1' @@ -194,7 +194,7 @@ describe('useNodeEventHandlers', () => { mockNode.flags.pinned = true const event = new PointerEvent('pointerdown') - handleNodeSelect(event, testNodeData) + handleNodeSelect(event, testNodeData, false) expect(mockLayoutMutations.bringNodeToFront).not.toHaveBeenCalled() }) @@ -207,7 +207,7 @@ describe('useNodeEventHandlers', () => { const event = new PointerEvent('pointerdown') expect(() => { - handleNodeSelect(event, testNodeData) + handleNodeSelect(event, testNodeData, false) }).not.toThrow() expect(mockCanvas.select).not.toHaveBeenCalled() @@ -227,7 +227,7 @@ describe('useNodeEventHandlers', () => { } as any expect(() => { - handleNodeSelect(event, nodeData) + handleNodeSelect(event, nodeData, false) }).not.toThrow() expect(mockCanvas.select).not.toHaveBeenCalled() From dfcbbec2b95ba4392365ce9418618e1017b82240 Mon Sep 17 00:00:00 2001 From: Benjamin Lu Date: Thu, 11 Sep 2025 17:33:25 -0700 Subject: [PATCH 4/4] Use env (#5501) --- .github/workflows/backport.yaml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/.github/workflows/backport.yaml b/.github/workflows/backport.yaml index 3f4b93242..907695e57 100644 --- a/.github/workflows/backport.yaml +++ b/.github/workflows/backport.yaml @@ -133,11 +133,10 @@ jobs: if: steps.check-existing.outputs.skip != 'true' && steps.backport.outputs.success env: GH_TOKEN: ${{ secrets.PR_GH_TOKEN }} + PR_TITLE: ${{ github.event.pull_request.title }} + PR_NUMBER: ${{ github.event.pull_request.number }} + PR_AUTHOR: ${{ github.event.pull_request.user.login }} run: | - PR_TITLE="${{ github.event.pull_request.title }}" - PR_NUMBER="${{ github.event.pull_request.number }}" - PR_AUTHOR="${{ github.event.pull_request.user.login }}" - for backport in ${{ steps.backport.outputs.success }}; do IFS=':' read -r version branch <<< "${backport}"