diff --git a/CHANGELOG.md b/CHANGELOG.md
index ff571c06f58e..9dba2e7c5e78 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,6 +9,7 @@
- [Quick Fix Import Button][12051].
- [Fixed nodes being selected after deleting other nodes or connections.][11902]
- [Redo stack is no longer lost when interacting with text literals][11908].
+- [Copy button on error message is fixed][12133].
- [Tooltips are hidden when clicking on a button][12067].
- [Fixed bug when clicking header in Table Editor Widget didn't start editing
it][12064]
@@ -21,6 +22,7 @@
[12051]: https://github.com/enso-org/enso/pull/12051
[11902]: https://github.com/enso-org/enso/pull/11902
[11908]: https://github.com/enso-org/enso/pull/11908
+[12133]: https://github.com/enso-org/enso/pull/12133
[12067]: https://github.com/enso-org/enso/pull/12067
[12064]: https://github.com/enso-org/enso/pull/12064
[12129]: https://github.com/enso-org/enso/pull/12129
diff --git a/app/gui/integration-test/project-view/actions.ts b/app/gui/integration-test/project-view/actions.ts
index 00522b0410aa..500fe10cc6bd 100644
--- a/app/gui/integration-test/project-view/actions.ts
+++ b/app/gui/integration-test/project-view/actions.ts
@@ -36,7 +36,7 @@ export async function expectNodePositionsInitialized(page: Page, yPos: number) {
// Wait until edges are initialized and displayed correctly.
await expect(page.getByTestId('broken-edge')).toBeHidden()
// Wait until node sizes are initialized.
- await expect(locate.graphNode(page).first().locator('.bgFill')).toBeVisible()
+ await expect(locate.graphNode(page).first().locator('.nodeBackground')).toBeVisible()
// TODO: The yPos should not need to be a variable. Instead, first automatically positioned nodes
// should always have constant known position. This is a bug caused by incorrect layout after
// entering a function. To be fixed with #9255
diff --git a/app/gui/integration-test/project-view/componentBrowser.spec.ts b/app/gui/integration-test/project-view/componentBrowser.spec.ts
index 95607c23504e..81f06b6c86f6 100644
--- a/app/gui/integration-test/project-view/componentBrowser.spec.ts
+++ b/app/gui/integration-test/project-view/componentBrowser.spec.ts
@@ -59,7 +59,10 @@ test('Different ways of opening Component Browser', async ({ page }) => {
await locate.graphEditor(page).press('Enter')
await expectAndCancelBrowser(page, '', 'selected')
// Dragging out an edge
- let outputPort = await locate.outputPortCoordinates(locate.graphNodeByBinding(page, 'selected'))
+ let outputPort = await locate.outputPortCoordinates(
+ page,
+ locate.graphNodeByBinding(page, 'selected'),
+ )
await page.mouse.click(outputPort.x, outputPort.y)
await locate.graphEditor(page).click({ position: { x: 100, y: 500 } })
await expectAndCancelBrowser(page, '', 'selected')
@@ -67,7 +70,7 @@ test('Different ways of opening Component Browser', async ({ page }) => {
// TODO[ao] Without timeout, even the first click would be treated as double due to previous
// event. Probably we need a better way to simulate double clicks.
await page.waitForTimeout(600)
- outputPort = await locate.outputPortCoordinates(locate.graphNodeByBinding(page, 'selected'))
+ outputPort = await locate.outputPortCoordinates(page, locate.graphNodeByBinding(page, 'selected'))
await page.mouse.click(outputPort.x, outputPort.y)
await page.mouse.click(outputPort.x, outputPort.y)
await expectAndCancelBrowser(page, '', 'selected')
@@ -79,15 +82,16 @@ test('Opening Component Browser from output port buttons', async ({ page }) => {
// Small (+) button shown when node is hovered
const node = locate.graphNodeByBinding(page, 'selected')
await locate.graphNodeIcon(node).hover()
- await expect(locate.createNodeFromPort(node)).toBeVisible()
- await locate.createNodeFromPort(node).click({ force: true })
+ const createNodeFromPortButton = await locate.createNodeFromPortButton(page, node)
+ await expect(createNodeFromPortButton).toBeVisible()
+ await createNodeFromPortButton.click({ force: true })
await expectAndCancelBrowser(page, '', 'selected')
// Small (+) button shown when node is selected
await page.keyboard.press('Escape')
await node.click()
- await expect(locate.createNodeFromPort(node)).toBeVisible()
- await locate.createNodeFromPort(node).click({ force: true })
+ await expect(createNodeFromPortButton).toBeVisible()
+ await createNodeFromPortButton.click({ force: true })
await expectAndCancelBrowser(page, '', 'selected')
})
@@ -111,7 +115,10 @@ test('Graph Editor pans to Component Browser', async ({ page }) => {
await page.mouse.move(100, 280)
await page.mouse.up({ button: 'middle' })
await expect(locate.graphNodeByBinding(page, 'five')).toBeInViewport()
- const outputPort = await locate.outputPortCoordinates(locate.graphNodeByBinding(page, 'final'))
+ const outputPort = await locate.outputPortCoordinates(
+ page,
+ locate.graphNodeByBinding(page, 'final'),
+ )
await page.mouse.click(outputPort.x, outputPort.y)
await locate.graphEditor(page).click({ position: { x: 100, y: 1700 } })
await expect(locate.graphNodeByBinding(page, 'five')).not.toBeInViewport()
diff --git a/app/gui/integration-test/project-view/edgeInteractions.spec.ts b/app/gui/integration-test/project-view/edgeInteractions.spec.ts
index 574e68db9553..72e313795220 100644
--- a/app/gui/integration-test/project-view/edgeInteractions.spec.ts
+++ b/app/gui/integration-test/project-view/edgeInteractions.spec.ts
@@ -82,7 +82,7 @@ test('Conditional ports: Disabled', async ({ page }) => {
// When a port is disabled, it doesn't react to hovering with a disconnected edge,
// and any attempt to connect to it should open the CB.
- const outputPort = await outputPortCoordinates(graphNodeByBinding(page, 'final'))
+ const outputPort = await outputPortCoordinates(page, graphNodeByBinding(page, 'final'))
await page.mouse.click(outputPort.x, outputPort.y)
await conditionalPort.hover()
await expect(conditionalPort).not.toHaveClass(/isTarget/)
@@ -101,7 +101,7 @@ test('Conditional ports: Enabled', async ({ page }) => {
await page.keyboard.down(CONTROL_KEY)
await expect(conditionalPort).toHaveClass(/enabled/)
- const outputPort = await outputPortCoordinates(graphNodeByBinding(page, 'final'))
+ const outputPort = await outputPortCoordinates(page, graphNodeByBinding(page, 'final'))
await page.mouse.click(outputPort.x, outputPort.y)
await conditionalPort.hover()
await expect(conditionalPort).toHaveClass(/isTarget/)
diff --git a/app/gui/integration-test/project-view/graphNodeVisualization.spec.ts b/app/gui/integration-test/project-view/graphNodeVisualization.spec.ts
index 6b8d9ddd0861..271641addc76 100644
--- a/app/gui/integration-test/project-view/graphNodeVisualization.spec.ts
+++ b/app/gui/integration-test/project-view/graphNodeVisualization.spec.ts
@@ -29,7 +29,7 @@ test('Node can open and load visualization', async ({ page }) => {
test('Previewing visualization', async ({ page }) => {
await actions.goToGraph(page)
const node = locate.graphNode(page).last()
- const port = await locate.outputPortCoordinates(node)
+ const port = await locate.outputPortCoordinates(page, node)
await page.keyboard.down('Meta')
await page.keyboard.down('Control')
await expect(locate.anyVisualization(page)).toBeHidden()
diff --git a/app/gui/integration-test/project-view/locate.ts b/app/gui/integration-test/project-view/locate.ts
index e8a4a04b769f..d9c699ba898b 100644
--- a/app/gui/integration-test/project-view/locate.ts
+++ b/app/gui/integration-test/project-view/locate.ts
@@ -83,7 +83,6 @@ export const componentMenu = componentLocator('.ComponentMenu')
export const addNewNodeButton = componentLocator('.PlusButton')
export const componentBrowser = componentLocator('.ComponentBrowser')
export const nodeOutputPort = componentLocator('.outputPortHoverArea')
-export const createNodeFromPort = componentLocator('.CreateNodeFromPortButton .plusIcon')
export const editorRoot = componentLocator('.CodeMirror')
export const nodeComment = componentLocator('.GraphNodeComment')
export const nodeCommentContent = componentLocator('.GraphNodeComment div[contentEditable]')
@@ -185,11 +184,22 @@ export async function edgesToNode(page: Page, node: Locator) {
* Returns a location that can be clicked to activate an output port.
* Using a `Locator` would be better, but `position` option of `click` doesn't work.
*/
-export async function outputPortCoordinates(node: Locator) {
- const outputPortArea = await node.locator('.outputPortHoverArea').boundingBox()
+export async function outputPortCoordinates(page: Page, node: Locator) {
+ const nodeId = await node.getAttribute('data-node-id')
+ const outputPortArea = await page
+ .locator(`.GraphNodeOutputPorts[data-output-ports-node-id="${nodeId}"] .outputPortHoverArea`)
+ .boundingBox()
expect(outputPortArea).not.toBeNull()
assert(outputPortArea)
const centerX = outputPortArea.x + outputPortArea.width / 2
const bottom = outputPortArea.y + outputPortArea.height
return { x: centerX, y: bottom - 2.0 }
}
+
+/** Returns a locator for the create node from port button. */
+export async function createNodeFromPortButton(page: Page, node: Locator) {
+ const nodeId = await node.getAttribute('data-node-id')
+ return page.locator(
+ `.GraphNodeOutputPorts[data-output-ports-node-id="${nodeId}"] .CreateNodeFromPortButton .plusIcon`,
+ )
+}
diff --git a/app/gui/src/App.vue b/app/gui/src/App.vue
index c81288cfaf3e..8bbf7ffb29ea 100644
--- a/app/gui/src/App.vue
+++ b/app/gui/src/App.vue
@@ -5,7 +5,7 @@ import ProjectView from '@/ProjectView.vue'
import { provideAppClassSet } from '@/providers/appClass'
import { provideGuiConfig } from '@/providers/guiConfig'
import { provideTooltipRegistry } from '@/providers/tooltipRegistry'
-import { registerAutoBlurHandler } from '@/util/autoBlur'
+import { registerAutoBlurHandler, registerGlobalBlurHandler } from '@/util/autoBlur'
import { baseConfig, configValue, mergeConfig, type ApplicationConfigValue } from '@/util/config'
import { urlParams } from '@/util/urlParams'
import { useQueryClient } from '@tanstack/vue-query'
@@ -38,6 +38,7 @@ const queryClient = useQueryClient()
provideGuiConfig(appConfigValue)
registerAutoBlurHandler()
+registerGlobalBlurHandler()
onMounted(() => {
if (appConfigValue.value.window.vibrancy) {
diff --git a/app/gui/src/project-view/components/GraphEditor.vue b/app/gui/src/project-view/components/GraphEditor.vue
index 59975a230874..90df2722a36a 100644
--- a/app/gui/src/project-view/components/GraphEditor.vue
+++ b/app/gui/src/project-view/components/GraphEditor.vue
@@ -180,6 +180,7 @@ const nodeSelection = provideGraphSelection(
{
isValid: (id) => graphStore.db.isNodeId(id),
onSelected: (id) => graphStore.db.moveNodeToTop(id),
+ onSoleSelected: (id) => graphStore.db.moveNodeToTop(id),
toSorted: (ids) => {
const idsSet = new Set(ids)
const inputNodes = [
@@ -644,7 +645,12 @@ const groupColors = computed(() => {
@createNodes="createNodesFromSource"
@toggleDocPanel="toggleRightDockHelpPanel"
/>
-
+
-import { ref } from 'vue'
import { AstId } from 'ydoc-shared/ast'
const props = defineProps<{ portId: AstId }>()
-const emit = defineEmits<{ click: [] }>()
-const hovered = ref(false)
-
+
@@ -18,7 +15,7 @@ const hovered = ref(false)
-
+
@@ -31,6 +28,7 @@ const hovered = ref(false)
--topOffset: 40px;
--color-dimmed: color-mix(in oklab, var(--color-node-primary) 60%, white 40%);
--color: var(--color-node-primary);
+ pointer-events: all;
}
.connection {
diff --git a/app/gui/src/project-view/components/GraphEditor/GraphEdge.vue b/app/gui/src/project-view/components/GraphEditor/GraphEdge.vue
index eb5659f8767b..bcc0bc5c9a5e 100644
--- a/app/gui/src/project-view/components/GraphEditor/GraphEdge.vue
+++ b/app/gui/src/project-view/components/GraphEditor/GraphEdge.vue
@@ -126,7 +126,7 @@ const sourceMask = computed(() => {
if (!nodeRect) return
const animProgress =
startsInPort.value ?
- ((sourceNode.value && graph.nodeHoverAnimations.get(sourceNode.value)) ?? 0)
+ ((sourceNode.value && graph.nodeOutputHoverAnimations.get(sourceNode.value)) ?? 0)
: 0
const padding = animProgress * VISIBLE_PORT_MASK_PADDING
if (!maskSource && padding === 0) return
@@ -287,7 +287,7 @@ const arrowPath = [
const sourceHoverAnimationStyle = computed(() => {
if (!animateFromSourceHover || !base.value || !sourceNode.value) return {}
- const progress = graph.nodeHoverAnimations.get(sourceNode.value) ?? 0
+ const progress = graph.nodeOutputHoverAnimations.get(sourceNode.value) ?? 0
if (progress === 1) return {}
const currentLength = progress * base.value.getTotalLength()
return {
diff --git a/app/gui/src/project-view/components/GraphEditor/GraphEdges.vue b/app/gui/src/project-view/components/GraphEditor/GraphEdges.vue
index 05b1e6d8969b..9faa93e0ba9a 100644
--- a/app/gui/src/project-view/components/GraphEditor/GraphEdges.vue
+++ b/app/gui/src/project-view/components/GraphEditor/GraphEdges.vue
@@ -1,5 +1,7 @@
@@ -125,6 +135,19 @@ function createEdge(source: AstId, target: PortId) {
:edge="graph.outputSuggestedEdge"
animateFromSourceHover
/>
+
+ graph.createEdgeFromOutput(portId, event)"
+ @portDoubleClick="(_event, portId) => emit('outputPortDoubleClick', portId)"
+ @update:hoverAnim="graph.updateNodeOutputHoverAnim(id, $event)"
+ />
+