mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-03 22:59:14 +00:00
Merge remote-tracking branch 'origin/main' into bl-update-slots
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
7
.github/workflows/backport.yaml
vendored
7
.github/workflows/backport.yaml
vendored
@@ -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}"
|
||||
|
||||
|
||||
11
package.json
11
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",
|
||||
|
||||
@@ -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
|
||||
})
|
||||
|
||||
|
||||
@@ -168,13 +168,18 @@ interface LGraphNodeProps {
|
||||
const props = defineProps<LGraphNodeProps>()
|
||||
|
||||
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 = () => {
|
||||
|
||||
@@ -26,7 +26,11 @@ export function useNodeEventHandlers(nodeManager: Ref<NodeManager | null>) {
|
||||
* 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<NodeManager | null>) {
|
||||
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<NodeManager | null>) {
|
||||
// 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<NodeManager | null>) {
|
||||
|
||||
// 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<NodeManager | null>) {
|
||||
metaKey: event.metaKey,
|
||||
bubbles: true
|
||||
})
|
||||
handleNodeSelect(syntheticEvent, nodeData)
|
||||
handleNodeSelect(syntheticEvent, nodeData, false)
|
||||
}
|
||||
|
||||
// Set drag data for potential drop operations
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
*/
|
||||
import { computed, inject } from 'vue'
|
||||
|
||||
import { SelectedNodeIdsKey } from '@/renderer/core/canvas/injectionKeys'
|
||||
import { TransformStateKey } from '@/renderer/core/layout/injectionKeys'
|
||||
import { useLayoutMutations } from '@/renderer/core/layout/operations/layoutMutations'
|
||||
import { layoutStore } from '@/renderer/core/layout/store/layoutStore'
|
||||
@@ -50,6 +51,9 @@ export function useNodeLayout(nodeId: string) {
|
||||
let isDragging = false
|
||||
let dragStartPos: Point | null = null
|
||||
let dragStartMouse: Point | null = null
|
||||
let otherSelectedNodesStartPositions: Map<string, Point> | null = null
|
||||
|
||||
const selectedNodeIds = inject(SelectedNodeIdsKey, null)
|
||||
|
||||
/**
|
||||
* Start dragging the node
|
||||
@@ -61,6 +65,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)
|
||||
|
||||
@@ -91,7 +113,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
|
||||
@@ -99,6 +121,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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -110,6 +146,7 @@ export function useNodeLayout(nodeId: string) {
|
||||
isDragging = false
|
||||
dragStartPos = null
|
||||
dragStartMouse = null
|
||||
otherSelectedNodesStartPositions = null
|
||||
|
||||
// Release pointer
|
||||
const target = event.target as HTMLElement
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user