Skip to content

Commit 2bd00a0

Browse files
LukasHirtclaude
andauthored
fix(frontend): align add-node button consistently across node types (#28)
The "+" add-next button was duplicated across TriggerNode, LlmNode and ActionNode with an arbitrary -28px offset from the card, leaving it floating away from Vue Flow's own connection handle at the card's edge instead of being coordinated with it. Consolidate the button into a shared NodeAddButton component and anchor it the same way Vue Flow anchors its handle (right: 0; translate(50%, -50%)) so both sit at the same point, consistently, regardless of a node's border-radius or width. Adds a Playwright regression test asserting the button's vertical centering and its offset from the card's right edge stay consistent across trigger/llm/action node types. Signed-off-by: Lukas Hirt <info@hirt.cz> Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
1 parent fdb8c46 commit 2bd00a0

6 files changed

Lines changed: 106 additions & 26 deletions

File tree

frontend/src/components/nodes/ActionNode.vue

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,21 +14,15 @@
1414
<span class="workflows-node-card-subtitle">{{ subtitle }}</span>
1515
</div>
1616
<Handle type="source" :position="Position.Right" />
17-
<button
18-
type="button"
19-
class="workflows-node-add-button"
20-
:aria-label="$gettext('Add next step')"
21-
@click.stop="$emit('add-next')"
22-
>
23-
+
24-
</button>
17+
<NodeAddButton @add-next="$emit('add-next')" />
2518
</div>
2619
</template>
2720

2821
<script lang="ts" setup>
2922
import { computed } from 'vue'
3023
import { Handle, Position } from '@vue-flow/core'
3124
import { useGettext } from 'vue3-gettext'
25+
import NodeAddButton from './NodeAddButton.vue'
3226
import { findNodeTypeForNode } from '../../nodeTypes'
3327
import type { WorkflowNodeData } from '../../types/workflow'
3428

frontend/src/components/nodes/LlmNode.vue

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,21 +14,15 @@
1414
<span class="workflows-node-card-subtitle">{{ subtitle }}</span>
1515
</div>
1616
<Handle type="source" :position="Position.Right" />
17-
<button
18-
type="button"
19-
class="workflows-node-add-button"
20-
:aria-label="$gettext('Add next step')"
21-
@click.stop="$emit('add-next')"
22-
>
23-
+
24-
</button>
17+
<NodeAddButton @add-next="$emit('add-next')" />
2518
</div>
2619
</template>
2720

2821
<script lang="ts" setup>
2922
import { computed } from 'vue'
3023
import { Handle, Position } from '@vue-flow/core'
3124
import { useGettext } from 'vue3-gettext'
25+
import NodeAddButton from './NodeAddButton.vue'
3226
import type { WorkflowNodeData } from '../../types/workflow'
3327
3428
const props = defineProps<{ id: string; data: WorkflowNodeData }>()
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<template>
2+
<button
3+
type="button"
4+
class="workflows-node-add-button"
5+
:aria-label="$gettext('Add next step')"
6+
@click.stop="$emit('add-next')"
7+
>
8+
+
9+
</button>
10+
</template>
11+
12+
<script lang="ts" setup>
13+
import { useGettext } from 'vue3-gettext'
14+
15+
defineEmits<{ (e: 'add-next'): void }>()
16+
const { $gettext } = useGettext()
17+
</script>

frontend/src/components/nodes/TriggerNode.vue

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,21 +13,15 @@
1313
<span class="workflows-node-card-subtitle">{{ subtitle }}</span>
1414
</div>
1515
<Handle type="source" :position="Position.Right" />
16-
<button
17-
type="button"
18-
class="workflows-node-add-button"
19-
:aria-label="$gettext('Add next step')"
20-
@click.stop="$emit('add-next')"
21-
>
22-
+
23-
</button>
16+
<NodeAddButton @add-next="$emit('add-next')" />
2417
</div>
2518
</template>
2619

2720
<script lang="ts" setup>
2821
import { computed } from 'vue'
2922
import { Handle, Position } from '@vue-flow/core'
3023
import { useGettext } from 'vue3-gettext'
24+
import NodeAddButton from './NodeAddButton.vue'
3125
import { findNodeTypeForNode } from '../../nodeTypes'
3226
import type { WorkflowNodeData } from '../../types/workflow'
3327

frontend/src/styles/canvas.css

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,13 +37,28 @@
3737
text-overflow: ellipsis;
3838
max-width: 180px;
3939
}
40+
/*
41+
* Anchored the same way Vue Flow anchors its own connection handle
42+
* (.vue-flow__handle-right: right: 0; top: 50%; transform: translate(50%, -50%)) so the
43+
* "+" button straddles the card's right edge at exactly the same point as the handle,
44+
* regardless of a node's border-radius (e.g. the trigger's pill-shaped left corners
45+
* don't affect the right edge, but anchoring to the handle's own point rather than an
46+
* arbitrary fixed offset keeps the two coordinated even if that ever changes) or width.
47+
* It paints after the Handle in the DOM, so it visually sits on top of the small handle
48+
* dot instead of floating a fixed 28px away from it, giving one clear affordance instead
49+
* of two dis-connected circles.
50+
*/
4051
.workflows-node-add-button {
4152
position: absolute;
42-
right: -28px;
53+
right: 0;
4354
top: 50%;
44-
transform: translateY(-50%);
55+
transform: translate(50%, -50%);
56+
display: flex;
57+
align-items: center;
58+
justify-content: center;
4559
width: 22px;
4660
height: 22px;
61+
padding: 0;
4762
border-radius: 50%;
4863
border: 1px solid var(--oc-color-border, #ccc);
4964
background: var(--oc-color-swatch-brand-contrastText, #fff);
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { test, expect } from '@playwright/test'
2+
import { login } from './support/auth'
3+
4+
test('the "+" add-next button aligns consistently across node types', async ({ page }) => {
5+
await login(page)
6+
7+
await page.goto('/workflows/workflows')
8+
await page.getByRole('button', { name: 'Add workflow' }).click()
9+
await page.waitForURL(/\/workflows\/workflows\/new$/)
10+
11+
// Chain a trigger -> LLM -> action, matching build-workflow.spec.ts, so we get one
12+
// instance of each of the three node components that render the shared add-button.
13+
await page.getByRole('button', { name: 'Add trigger' }).click()
14+
await page.getByRole('button', { name: 'Manual Trigger', exact: true }).click()
15+
await expect(page.locator('.workflows-node-trigger')).toBeVisible()
16+
17+
await page.locator('.workflows-node-trigger .workflows-node-add-button').click()
18+
await page.getByRole('button', { name: 'LLM Prompt', exact: true }).click()
19+
await expect(page.locator('.workflows-node-llm')).toBeVisible()
20+
21+
await page.locator('.workflows-node-llm .workflows-node-add-button').click()
22+
await page.getByRole('button', { name: 'Add Tag', exact: true }).click()
23+
await expect(page.locator('.workflows-node-action')).toBeVisible()
24+
25+
const centerOffsets: number[] = []
26+
27+
for (const cardClass of ['workflows-node-trigger', 'workflows-node-llm', 'workflows-node-action']) {
28+
const card = page.locator(`.${cardClass}`)
29+
const cardBox = await card.boundingBox()
30+
const buttonBox = await card.locator('.workflows-node-add-button').boundingBox()
31+
expect(cardBox, `${cardClass} card should have a bounding box`).not.toBeNull()
32+
expect(buttonBox, `${cardClass} add-button should have a bounding box`).not.toBeNull()
33+
if (!cardBox || !buttonBox) continue
34+
35+
// The button must be vertically centered on its own card, regardless of the
36+
// card's height or its border-radius shape (e.g. the trigger's pill-shaped left
37+
// corners must not throw off the button sitting on the right edge).
38+
const cardCenterY = cardBox.y + cardBox.height / 2
39+
const buttonCenterY = buttonBox.y + buttonBox.height / 2
40+
expect(
41+
Math.abs(buttonCenterY - cardCenterY),
42+
`${cardClass} add-button should be vertically centered on the card`
43+
).toBeLessThanOrEqual(1)
44+
45+
// The button must sit essentially at the card's right edge -- coordinated with Vue
46+
// Flow's own connection handle there -- rather than floating away from the card
47+
// with a visible, disconnected-looking gap.
48+
const buttonCenterX = buttonBox.x + buttonBox.width / 2
49+
const cardRightX = cardBox.x + cardBox.width
50+
const offsetFromEdge = buttonCenterX - cardRightX
51+
expect(
52+
Math.abs(offsetFromEdge),
53+
`${cardClass} add-button should sit right at the card's edge, not floating away from it`
54+
).toBeLessThanOrEqual(6)
55+
56+
centerOffsets.push(offsetFromEdge)
57+
}
58+
59+
// And that offset must be identical (within a tight tolerance) across all three node
60+
// types, so the button can't silently drift out of sync between the three node
61+
// components again.
62+
const [first, ...rest] = centerOffsets
63+
for (const offset of rest) {
64+
expect(Math.abs(offset - first)).toBeLessThanOrEqual(1)
65+
}
66+
})

0 commit comments

Comments
 (0)