From 7a92d1c78e31078702d5b11be45963183725ec00 Mon Sep 17 00:00:00 2001 From: Ilya Bogdanov Date: Wed, 18 Dec 2024 16:49:59 +0400 Subject: [PATCH] Create node from port button (#11836) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #11508 https://github.com/user-attachments/assets/7943f63d-4d34-4909-ac47-bf8ea4dd3eb7 A few notes regarding implementation: - The button is drawn as SVG and embedded into the output port because it is relatively easy to position it correctly and seamlessly blend it with the surrounding output port. - We have a ready SVG `plus` icon, but the initial design suggested we want to make it transparent in the center, so I used a mask to clip the plus shape from the background. This is relatively unimportant for the implementation and can be replaced by a separate SVG if we want. - I fixed the positioning for “port labels.” They are only visible when multiple output ports are present, so it is not important. - There is a hover area extending around the actual button and its “connection”, so it is easy to click. It is a nuance for the tests, though, so I was forced to use `force: true`. - The connection is not a real connection, but has the same dimensions and color scheme. Implementing this visual part as a “fake” edge is extremely tricky, so I chose not to do that. - These buttons must be hidden when the component browser is connected, or they will visually conflict with the normal edge. There is no clear way to check if the component browser is connected to _that_ port, so _every_ button is hidden when the component browser is opened. - As an unrelated change, I renamed `circular menu` → `component menu`. --- CHANGELOG.md | 7 ++ .../integration-test/project-view/actions.ts | 4 +- .../project-view/componentBrowser.spec.ts | 22 ++-- .../graphNodeVisualization.spec.ts | 2 +- .../integration-test/project-view/locate.ts | 4 +- .../project-view/nodeComments.spec.ts | 8 +- .../project-view/typesOnNodeHover.spec.ts | 8 +- .../components/ComponentBrowser.vue | 2 +- .../project-view/components/ComponentMenu.vue | 4 +- .../project-view/components/GraphEditor.vue | 15 ++- .../GraphEditor/CreateNodeFromPortButton.vue | 108 ++++++++++++++++++ .../components/GraphEditor/GraphNode.vue | 13 +-- .../GraphEditor/GraphNodeOutputPorts.vue | 16 ++- .../GraphEditor/GraphVisualization.vue | 4 +- .../components/SmallPlusButton.vue | 35 ------ .../providers/graphEditorState.ts | 15 +++ 16 files changed, 186 insertions(+), 81 deletions(-) create mode 100644 app/gui/src/project-view/components/GraphEditor/CreateNodeFromPortButton.vue delete mode 100644 app/gui/src/project-view/components/SmallPlusButton.vue create mode 100644 app/gui/src/project-view/providers/graphEditorState.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 4df253fba6e9..5ac8c6d31d0d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Next Next Release +#### Enso IDE + +- [Round ‘Add component’ button under the component menu replaced by a small + button protruding from the output port.][11836]. + +[11836]: https://github.com/enso-org/enso/pull/11836 + #### Enso Language & Runtime - [Intersection types & type checks][11600] diff --git a/app/gui/integration-test/project-view/actions.ts b/app/gui/integration-test/project-view/actions.ts index 96743db1f575..00522b0410aa 100644 --- a/app/gui/integration-test/project-view/actions.ts +++ b/app/gui/integration-test/project-view/actions.ts @@ -66,9 +66,9 @@ export async function dragNodeByBinding(page: Page, nodeBinding: string, x: numb } /** Move mouse away to avoid random hover events and wait for any circular menus to disappear. */ -export async function ensureNoCircularMenusVisibleDueToHovering(page: Page) { +export async function ensureNoComponentMenusVisibleDueToHovering(page: Page) { await page.mouse.move(-1000, 0) - await expect(locate.circularMenu(page)).toBeHidden() + await expect(locate.componentMenu(page)).toBeHidden() } /** Ensure no nodes are selected. */ diff --git a/app/gui/integration-test/project-view/componentBrowser.spec.ts b/app/gui/integration-test/project-view/componentBrowser.spec.ts index 3e49c831d834..95607c23504e 100644 --- a/app/gui/integration-test/project-view/componentBrowser.spec.ts +++ b/app/gui/integration-test/project-view/componentBrowser.spec.ts @@ -73,25 +73,21 @@ test('Different ways of opening Component Browser', async ({ page }) => { await expectAndCancelBrowser(page, '', 'selected') }) -test('Opening Component Browser with small plus buttons', async ({ page }) => { +test('Opening Component Browser from output port buttons', async ({ page }) => { await actions.goToGraph(page) // Small (+) button shown when node is hovered - await page.keyboard.press('Escape') - await page.mouse.move(100, 80) - await expect(locate.smallPlusButton(page)).toBeHidden() - await locate.graphNodeIcon(locate.graphNodeByBinding(page, 'selected')).hover() - await expect(locate.smallPlusButton(page)).toBeVisible() - await locate.smallPlusButton(page).click() + const node = locate.graphNodeByBinding(page, 'selected') + await locate.graphNodeIcon(node).hover() + await expect(locate.createNodeFromPort(node)).toBeVisible() + await locate.createNodeFromPort(node).click({ force: true }) await expectAndCancelBrowser(page, '', 'selected') - // Small (+) button shown when node is sole selection + // Small (+) button shown when node is selected await page.keyboard.press('Escape') - await page.mouse.move(300, 300) - await expect(locate.smallPlusButton(page)).toBeHidden() - await locate.graphNodeByBinding(page, 'selected').click() - await expect(locate.smallPlusButton(page)).toBeVisible() - await locate.smallPlusButton(page).click() + await node.click() + await expect(locate.createNodeFromPort(node)).toBeVisible() + await locate.createNodeFromPort(node).click({ force: true }) await expectAndCancelBrowser(page, '', 'selected') }) diff --git a/app/gui/integration-test/project-view/graphNodeVisualization.spec.ts b/app/gui/integration-test/project-view/graphNodeVisualization.spec.ts index bf8556275791..af27ca9159c9 100644 --- a/app/gui/integration-test/project-view/graphNodeVisualization.spec.ts +++ b/app/gui/integration-test/project-view/graphNodeVisualization.spec.ts @@ -9,7 +9,7 @@ test('Node can open and load visualization', async ({ page }) => { await actions.goToGraph(page) const node = locate.graphNode(page).last() await node.click({ position: { x: 8, y: 8 } }) - await expect(locate.circularMenu(page)).toExist() + await expect(locate.componentMenu(page)).toExist() await locate.toggleVisualizationButton(page).click() await expect(locate.anyVisualization(page)).toExist() await expect(locate.loadingVisualization(page)).toHaveCount(0) diff --git a/app/gui/integration-test/project-view/locate.ts b/app/gui/integration-test/project-view/locate.ts index e57546a852a2..e8a4a04b769f 100644 --- a/app/gui/integration-test/project-view/locate.ts +++ b/app/gui/integration-test/project-view/locate.ts @@ -79,11 +79,11 @@ export const graphEditor = componentLocator('.GraphEditor') export const codeEditor = componentLocator('.CodeEditor') export const anyVisualization = componentLocator('.GraphVisualization') export const loadingVisualization = componentLocator('.LoadingVisualization') -export const circularMenu = componentLocator('.CircularMenu') +export const componentMenu = componentLocator('.ComponentMenu') export const addNewNodeButton = componentLocator('.PlusButton') export const componentBrowser = componentLocator('.ComponentBrowser') export const nodeOutputPort = componentLocator('.outputPortHoverArea') -export const smallPlusButton = componentLocator('.SmallPlusButton') +export const createNodeFromPort = componentLocator('.CreateNodeFromPortButton .plusIcon') export const editorRoot = componentLocator('.CodeMirror') export const nodeComment = componentLocator('.GraphNodeComment') export const nodeCommentContent = componentLocator('.GraphNodeComment div[contentEditable]') diff --git a/app/gui/integration-test/project-view/nodeComments.spec.ts b/app/gui/integration-test/project-view/nodeComments.spec.ts index c9e386cae4e0..d48c1f7ece7e 100644 --- a/app/gui/integration-test/project-view/nodeComments.spec.ts +++ b/app/gui/integration-test/project-view/nodeComments.spec.ts @@ -22,8 +22,8 @@ test('Start editing comment via menu', async ({ page }) => { await actions.goToGraph(page) const node = locate.graphNodeByBinding(page, 'final') await node.click() - await locate.circularMenu(node).getByRole('button', { name: 'More' }).click() - await locate.circularMenu(node).getByRole('button', { name: 'Comment' }).click() + await locate.componentMenu(node).getByRole('button', { name: 'More' }).click() + await locate.componentMenu(node).getByRole('button', { name: 'Comment' }).click() await expect(locate.nodeCommentContent(node)).toBeFocused() }) @@ -60,8 +60,8 @@ test('Add new comment via menu', async ({ page }) => { const nodeComment = locate.nodeCommentContent(node) await node.click() - await locate.circularMenu(node).getByRole('button', { name: 'More' }).click() - await locate.circularMenu(node).getByRole('button', { name: 'Comment' }).click() + await locate.componentMenu(node).getByRole('button', { name: 'More' }).click() + await locate.componentMenu(node).getByRole('button', { name: 'Comment' }).click() await expect(locate.nodeCommentContent(node)).toBeFocused() const NEW_COMMENT = 'New comment text' await nodeComment.fill(NEW_COMMENT) diff --git a/app/gui/integration-test/project-view/typesOnNodeHover.spec.ts b/app/gui/integration-test/project-view/typesOnNodeHover.spec.ts index 32811c648382..eea37fd602d7 100644 --- a/app/gui/integration-test/project-view/typesOnNodeHover.spec.ts +++ b/app/gui/integration-test/project-view/typesOnNodeHover.spec.ts @@ -15,17 +15,17 @@ async function assertTypeLabelOnNode( ) { // Ensure the visualization button won't be covered by any other parts of another node (e.g. a comment). await bringNodeToFront(page, node) - await node.hover({ position: { x: 8, y: 8 } }) - await locate.toggleVisualizationButton(node).click() + await node.hover({ position: { x: 8, y: 8 }, force: true }) + await locate.toggleVisualizationButton(node).click({ force: true }) const targetLabel = node.locator('.node-type').first() await expect(targetLabel).toHaveText(type.short) await expect(targetLabel).toHaveAttribute('title', type.full) - await locate.toggleVisualizationButton(node).click() + await locate.toggleVisualizationButton(node).click({ force: true }) await actions.deselectNodes(page) } async function bringNodeToFront(page: Page, node: Locator) { - await node.click({ position: { x: 8, y: 8 } }) + await node.click({ position: { x: 0, y: 8 }, force: true }) await page.keyboard.press('Escape') } diff --git a/app/gui/src/project-view/components/ComponentBrowser.vue b/app/gui/src/project-view/components/ComponentBrowser.vue index 29bfd8842f31..097ea1418282 100644 --- a/app/gui/src/project-view/components/ComponentBrowser.vue +++ b/app/gui/src/project-view/components/ComponentBrowser.vue @@ -378,7 +378,7 @@ const handler = componentBrowserBindings.handler({ :nodeSize="inputSize" :nodePosition="nodePosition" :scale="1" - :isCircularMenuVisible="false" + :isComponentMenuVisible="false" :isFullscreen="false" :isFullscreenAllowed="false" :isResizable="false" diff --git a/app/gui/src/project-view/components/ComponentMenu.vue b/app/gui/src/project-view/components/ComponentMenu.vue index 8911369d59ee..e40d7ceaa84b 100644 --- a/app/gui/src/project-view/components/ComponentMenu.vue +++ b/app/gui/src/project-view/components/ComponentMenu.vue @@ -13,7 +13,7 @@ const isDropdownOpened = ref(false)