mirror of
https://github.com/n8n-io/n8n.git
synced 2026-10-17 05:29:44 +02:00
✨ Make group bodies clickable
This commit is contained in:
parent
b82083528f
commit
47ba7b079a
|
|
@ -374,6 +374,94 @@ describe('Canvas', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('group body click', () => {
|
||||
const setupGroups = async () => {
|
||||
workflowDocumentStore.setNodeGroups([{ id: 'g1', name: 'Group 1', nodeIds: ['node-1'] }]);
|
||||
const nodeGroupView = createNodeGroupViewMock(false);
|
||||
const rendered = renderComponent({
|
||||
props: {
|
||||
nodes: [createCanvasNodeElement({ id: 'node-1' }), createCanvasGroupNode({ id: 'g1' })],
|
||||
},
|
||||
global: { provide: { [NodeGroupViewKey as symbol]: nodeGroupView } },
|
||||
});
|
||||
|
||||
await waitFor(() =>
|
||||
expect(rendered.container.querySelectorAll('.vue-flow__node')).toHaveLength(2),
|
||||
);
|
||||
|
||||
const frame = rendered.container.querySelector<HTMLElement>(
|
||||
'[data-id="group:g1"] [data-test-id="canvas-node-group-frame"]',
|
||||
)!;
|
||||
|
||||
return { ...rendered, frame, nodeGroupView, vueFlow: useVueFlow(canvasId) };
|
||||
};
|
||||
|
||||
it('selects the whole group when its body is clicked, without toggling collapse', async () => {
|
||||
const { frame, emitted, nodeGroupView, vueFlow } = await setupGroups();
|
||||
|
||||
await fireEvent.pointerDown(frame);
|
||||
await fireEvent.click(frame);
|
||||
|
||||
// Title bar selected; the reconciler extends it to the members.
|
||||
await waitFor(() =>
|
||||
expect(vueFlow.getSelectedNodes.value.map(({ id }) => id).sort()).toEqual([
|
||||
'group:g1',
|
||||
'node-1',
|
||||
]),
|
||||
);
|
||||
expect(nodeGroupView.isGroupCollapsed('g1')).toBe(false);
|
||||
expect(emitted()['click:pane']).toBeUndefined();
|
||||
});
|
||||
|
||||
it('selects the group on a body click while one of its members is selected', async () => {
|
||||
const { frame, vueFlow } = await setupGroups();
|
||||
|
||||
vueFlow.addSelectedNodes([vueFlow.findNode('node-1')!]);
|
||||
await waitFor(() =>
|
||||
expect(vueFlow.getSelectedNodes.value.map(({ id }) => id)).toContain('node-1'),
|
||||
);
|
||||
|
||||
await fireEvent.pointerDown(frame);
|
||||
await fireEvent.click(frame);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(vueFlow.getSelectedNodes.value.map(({ id }) => id).sort()).toEqual([
|
||||
'group:g1',
|
||||
'node-1',
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it('keeps the group selected when its body is clicked again', async () => {
|
||||
const { frame, vueFlow } = await setupGroups();
|
||||
|
||||
await fireEvent.pointerDown(frame);
|
||||
await fireEvent.click(frame);
|
||||
await waitFor(() =>
|
||||
expect(vueFlow.getSelectedNodes.value.map(({ id }) => id)).toContain('group:g1'),
|
||||
);
|
||||
|
||||
await fireEvent.pointerDown(frame);
|
||||
await fireEvent.click(frame);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(vueFlow.getSelectedNodes.value.map(({ id }) => id)).toContain('group:g1'),
|
||||
);
|
||||
});
|
||||
|
||||
it('treats modifier body clicks as selection gestures without toggling collapse', async () => {
|
||||
const { frame, nodeGroupView, vueFlow } = await setupGroups();
|
||||
|
||||
await fireEvent.pointerDown(frame, { metaKey: true });
|
||||
await fireEvent.click(frame, { metaKey: true });
|
||||
|
||||
await waitFor(() =>
|
||||
expect(vueFlow.getSelectedNodes.value.map(({ id }) => id)).toContain('group:g1'),
|
||||
);
|
||||
expect(nodeGroupView.isGroupCollapsed('g1')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('expanded group selection', () => {
|
||||
const setupExpandedGroup = async (initialCollapsed = false) => {
|
||||
workflowDocumentStore.setNodeGroups([
|
||||
|
|
|
|||
|
|
@ -827,7 +827,14 @@ function onPointerDownCapture() {
|
|||
|
||||
function onNodeClick({ event, node }: NodeMouseEvent) {
|
||||
if (isCanvasGroupNode(node)) {
|
||||
// Modifier clicks keep VueFlow's multi-select behavior (cmd/ctrl+click).
|
||||
// Clicks on the group body (frame) are plain selection gestures —
|
||||
// VueFlow's native click handling already selected the group (plain
|
||||
// click) or toggled its selection (cmd/ctrl+click).
|
||||
const isBodyClick =
|
||||
event.target instanceof Element && event.target.closest('[data-canvas-node-group-body]');
|
||||
if (isBodyClick) return;
|
||||
|
||||
// Modifier clicks on the header keep VueFlow's multi-select behavior.
|
||||
if (event.ctrlKey || event.metaKey || event.shiftKey) return;
|
||||
|
||||
// A plain click on the title bar toggles collapse instead of selecting —
|
||||
|
|
@ -1766,6 +1773,15 @@ defineExpose({
|
|||
width: var(--canvas-selection-box--width) !important;
|
||||
height: var(--canvas-selection-box--height) !important;
|
||||
}
|
||||
|
||||
// Group title bars (and their pointer-interactive frames) must stay behind
|
||||
// canvas nodes at all times. VueFlow raises selected nodes by +1000, which
|
||||
// would float a selected group's frame above any overlapping node that
|
||||
// isn't part of the same selection — swallowing its pointer events (dead
|
||||
// clicks, hover flicker).
|
||||
:global(.vue-flow__node-canvas-node-group) {
|
||||
z-index: -1 !important;
|
||||
}
|
||||
/* stylelint-enable declaration-no-important */
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -316,6 +316,7 @@ function onWrapperPointerDown(event: PointerEvent) {
|
|||
v-if="!isCollapsed"
|
||||
:class="$style.frame"
|
||||
:style="frameStyle"
|
||||
data-canvas-node-group-body
|
||||
data-test-id="canvas-node-group-frame"
|
||||
/>
|
||||
|
||||
|
|
@ -487,6 +488,10 @@ function onWrapperPointerDown(event: PointerEvent) {
|
|||
opacity: 1;
|
||||
}
|
||||
|
||||
// The frame is a pointer surface: clicking empty group space selects the
|
||||
// group and dragging it moves the group, through the same VueFlow node
|
||||
// machinery as the title bar. Member nodes and edges render above it, so
|
||||
// only clicks on truly empty group space land here.
|
||||
.frame {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
|
|
@ -495,7 +500,6 @@ function onWrapperPointerDown(event: PointerEvent) {
|
|||
@include styles.canvas-node-border(dashed);
|
||||
border-top: none;
|
||||
border-radius: 0 0 var(--radius--lg) var(--radius--lg);
|
||||
pointer-events: none;
|
||||
box-sizing: border-box;
|
||||
z-index: 0;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user