Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create node from port button #11836

Merged
merged 5 commits into from
Dec 18, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions app/gui/integration-test/project-view/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand Down
22 changes: 9 additions & 13 deletions app/gui/integration-test/project-view/componentBrowser.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')
})

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
4 changes: 2 additions & 2 deletions app/gui/integration-test/project-view/locate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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]')
Expand Down
8 changes: 4 additions & 4 deletions app/gui/integration-test/project-view/nodeComments.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
})

Expand Down Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion app/gui/src/project-view/components/ComponentBrowser.vue
Original file line number Diff line number Diff line change
Expand Up @@ -380,7 +380,7 @@ const handler = componentBrowserBindings.handler({
:nodeSize="inputSize"
:nodePosition="nodePosition"
:scale="1"
:isCircularMenuVisible="false"
:isComponentMenuVisible="false"
:isFullscreen="false"
:isFullscreenAllowed="false"
:isResizable="false"
Expand Down
4 changes: 2 additions & 2 deletions app/gui/src/project-view/components/ComponentMenu.vue
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ const isDropdownOpened = ref(false)

<template>
<div
class="CircularMenu"
class="ComponentMenu"
:class="{
menu: !componentButtons.pickColor.state,
openedDropdown: isDropdownOpened,
Expand Down Expand Up @@ -58,7 +58,7 @@ const isDropdownOpened = ref(false)
</template>

<style scoped>
.CircularMenu {
.ComponentMenu {
position: absolute;
left: -36px;
bottom: -36px;
Expand Down
15 changes: 9 additions & 6 deletions app/gui/src/project-view/components/GraphEditor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import { keyboardBusy, keyboardBusyExceptIn, unrefElement, useEvent } from '@/co
import { groupColorVar } from '@/composables/nodeColors'
import type { PlacementStrategy } from '@/composables/nodeCreation'
import { provideGraphEditorLayers } from '@/providers/graphEditorLayers'
import { provideGraphEditorState } from '@/providers/graphEditorState'
import type { GraphNavigator } from '@/providers/graphNavigator'
import { provideGraphNavigator } from '@/providers/graphNavigator'
import { provideNodeColors } from '@/providers/graphNodeColors'
Expand Down Expand Up @@ -286,7 +287,7 @@ const graphBindingsHandler = graphBindings.handler({
projectStore.lsRpcConnection.profilingStop()
},
openComponentBrowser() {
if (graphNavigator.sceneMousePos != null && !componentBrowserVisible.value) {
if (graphNavigator.sceneMousePos != null && !componentBrowserOpened.value) {
createWithComponentBrowser(fromSelection() ?? { placement: { type: 'mouse' } })
}
},
Expand Down Expand Up @@ -390,23 +391,25 @@ const documentationEditorHandler = documentationEditorBindings.handler({

// === Component Browser ===

const componentBrowserVisible = ref(false)
const { componentBrowserOpened } = provideGraphEditorState({
componentBrowserOpened: ref(false),
})
const componentBrowserNodePosition = ref<Vec2>(Vec2.Zero)
const componentBrowserUsage = ref<Usage>({ type: 'newNode' })

watch(componentBrowserVisible, (v) =>
watch(componentBrowserOpened, (v) =>
rightDock.setStorageMode(v ? StorageMode.ComponentBrowser : StorageMode.Default),
)

function openComponentBrowser(usage: Usage, position: Vec2) {
componentBrowserUsage.value = usage
componentBrowserNodePosition.value = position
componentBrowserVisible.value = true
componentBrowserOpened.value = true
}

function hideComponentBrowser() {
graphStore.editedNodeInfo = undefined
componentBrowserVisible.value = false
componentBrowserOpened.value = false
displayedDocs.value = undefined
}

Expand Down Expand Up @@ -642,7 +645,7 @@ const groupColors = computed(() => {
/>
<GraphEdges :navigator="graphNavigator" @createNodeFromEdge="handleEdgeDrop" />
<ComponentBrowser
v-if="componentBrowserVisible"
v-if="componentBrowserOpened"
ref="componentBrowser"
:navigator="graphNavigator"
:nodePosition="componentBrowserNodePosition"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
<script setup lang="ts">
import { ref } from 'vue'
import { AstId } from 'ydoc-shared/ast'

const props = defineProps<{ portId: AstId }>()
const emit = defineEmits<{ click: [] }>()
const hovered = ref(false)
</script>

<template>
<g :class="{ CreateNodeFromPortButton: true, hovered }" @click="emit('click')">
<rect :class="{ connection: true }" fill="currentColor"></rect>
<g :class="{ plusIcon: true }">
<mask :id="`${props.portId}_add_node_clip_path`">
<rect class="maskBackground"></rect>
<rect class="plusV"></rect>
<rect class="plusH"></rect>
</mask>
<circle :mask="`url(#${props.portId}_add_node_clip_path)`" fill="currentColor"></circle>
</g>
<rect class="hoverArea" @pointerenter="hovered = true" @pointerleave="hovered = false"></rect>
</g>
</template>

<style scoped>
.CreateNodeFromPortButton {
--radius: 6px;
--maskSize: calc(var(--radius) * 2);
--strokeWidth: 1.5px;
--leftOffset: 16px;
--topOffset: 40px;
--color-dimmed: color-mix(in oklab, var(--color-node-primary) 60%, white 40%);
--color: var(--color-node-primary);
}

.connection {
--width: 4px;
--direct-hover-offset: calc(
var(--output-port-hovered-extra-width) * var(--direct-hover-animation)
);
width: var(--width);
height: calc((var(--topOffset) - var(--direct-hover-offset) + 2px) * var(--hover-animation));
transform: translate(
calc(var(--port-clip-start) * (100% + 1px) + var(--leftOffset) - var(--width) / 2),
calc(var(--node-size-y) + var(--direct-hover-offset) - var(--output-port-overlap))
);
cursor: pointer;
color: var(--color-dimmed);
transition: color 0.2s ease;
}

.hovered * {
color: var(--color);
}

.plusIcon {
transform: translate(
calc(var(--port-clip-start) * (100% + 1px) + var(--leftOffset) - var(--radius)),
calc(
var(--node-size-y) + var(--output-port-max-width) + var(--node-vertical-gap) +
var(--topOffset)
)
);
color: var(--color-dimmed);
cursor: pointer;
& .maskBackground {
fill: white;
width: var(--maskSize);
height: var(--maskSize);
}
& .plusV {
x: calc(var(--maskSize) / 2 - var(--strokeWidth) / 2);
y: calc(var(--radius) / 2);
width: var(--strokeWidth);
height: var(--radius);
fill: black;
}
& .plusH {
x: calc(var(--radius) / 2);
y: calc(var(--maskSize) / 2 - var(--strokeWidth) / 2);
width: var(--radius);
height: var(--strokeWidth);
fill: black;
}
& circle {
cx: var(--radius);
cy: var(--radius);
r: calc(var(--radius) * var(--hover-animation));
transition: color 0.2s ease;
}
}

.hoverArea {
--margin: 4px;
--width: calc(var(--radius) * 2 + var(--margin) * 2);
fill: transparent;
width: var(--width);
height: calc(
var(--node-vertical-gap) + var(--output-port-max-width) + var(--margin) * 2 + var(--topOffset) +
var(--radius)
);
transform: translate(
calc(var(--port-clip-start) * (100% + 1px) + var(--leftOffset) - var(--width) / 2),
calc(var(--node-size-y) + var(--output-port-max-width))
);
cursor: pointer;
}
</style>
13 changes: 5 additions & 8 deletions app/gui/src/project-view/components/GraphEditor/GraphNode.vue
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ import GraphNodeOutputPorts from '@/components/GraphEditor/GraphNodeOutputPorts.
import GraphVisualization from '@/components/GraphEditor/GraphVisualization.vue'
import type { NodeCreationOptions } from '@/components/GraphEditor/nodeCreation'
import PointFloatingMenu from '@/components/PointFloatingMenu.vue'
import SmallPlusButton from '@/components/SmallPlusButton.vue'
import SvgIcon from '@/components/SvgIcon.vue'
import { useDoubleClick } from '@/composables/doubleClick'
import { usePointer, useResizeObserver } from '@/composables/events'
Expand Down Expand Up @@ -499,7 +498,7 @@ const showMenuAt = ref<{ x: number; y: number }>()
:nodeSize="nodeSize"
:scale="navigator?.scale ?? 1"
:nodePosition="nodePosition"
:isCircularMenuVisible="menuVisible"
:isComponentMenuVisible="menuVisible"
:currentType="props.node.vis?.identifier"
:dataSource="dataSource"
:typename="expressionInfo?.typename"
Expand Down Expand Up @@ -561,17 +560,15 @@ const showMenuAt = ref<{ x: number; y: number }>()
v-if="props.node.type !== 'output'"
:nodeId="nodeId"
:forceVisible="nodeHovered"
@newNodeClick="
setSoleSelected(), emit('createNodes', [{ commit: false, content: undefined }])
"
@portClick="(...args) => emit('outputPortClick', ...args)"
@portDoubleClick="(...args) => emit('outputPortDoubleClick', ...args)"
@update:hoverAnim="emit('update:hoverAnim', $event)"
@update:nodeHovered="outputHovered = $event"
/>
</svg>
<SmallPlusButton
v-if="menuVisible"
:class="isVisualizationVisible ? 'afterNode' : 'belowMenu'"
@createNodes="setSoleSelected(), emit('createNodes', $event)"
/>
</div>
<PointFloatingMenu v-if="showMenuAt" :point="showMenuAt" @close="showMenuAt = undefined">
<ComponentContextMenu @close="showMenuAt = undefined" />
Expand Down Expand Up @@ -640,7 +637,7 @@ const showMenuAt = ref<{ x: number; y: number }>()
opacity: 1;
}

.CircularMenu {
.ComponentMenu {
z-index: 25;
&.partial {
z-index: 1;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<script setup lang="ts">
import { useApproach } from '@/composables/animation'
import { useDoubleClick } from '@/composables/doubleClick'
import { useGraphEditorState } from '@/providers/graphEditorState'
import { useGraphStore, type NodeId } from '@/stores/graph'
import { isDef } from '@vueuse/core'
import { setIfUndefined } from 'lib0/map'
Expand All @@ -14,12 +15,14 @@ import {
type EffectScope,
} from 'vue'
import type { AstId } from 'ydoc-shared/ast'
import CreateNodeFromPortButton from './CreateNodeFromPortButton.vue'

const props = defineProps<{ nodeId: NodeId; forceVisible: boolean }>()

const emit = defineEmits<{
portClick: [event: PointerEvent, portId: AstId]
portDoubleClick: [event: PointerEvent, portId: AstId]
newNodeClick: [portId: AstId]
'update:hoverAnim': [progress: number]
'update:nodeHovered': [hovered: boolean]
}>()
Expand All @@ -43,6 +46,8 @@ const outputPortsSet = computed(() => {
return bindings
})

const { componentBrowserOpened } = useGraphEditorState()

const outputPorts = computed((): PortData[] => {
const ports = outputPortsSet.value
const numPorts = ports.size
Expand Down Expand Up @@ -122,6 +127,7 @@ function portGroupStyle(port: PortData) {
'--direct-hover-animation': hoverAnimations.get(port.portId)?.[0].value ?? 0,
'--port-clip-start': start,
'--port-clip-end': end,
'--port-label-transform-x': `calc(${((end - start) / 2 + start) * 100}%)`,
transform: 'var(--output-port-transform)',
}
}
Expand All @@ -144,6 +150,11 @@ graph.suggestEdgeFromOutput(outputHovered)
<rect class="outputPort" />
</g>
<text class="outputPortLabel">{{ port.label }}</text>
<CreateNodeFromPortButton
v-if="!componentBrowserOpened"
:portId="port.portId"
@click="emit('newNodeClick', port.portId)"
/>
</g>
</template>
</template>
Expand Down Expand Up @@ -205,6 +216,9 @@ graph.suggestEdgeFromOutput(outputHovered)
text-anchor: middle;
opacity: calc(var(--hover-animation) * var(--hover-animation));
fill: var(--color-node-primary);
transform: translate(50%, calc(var(--node-size-y) + var(--output-port-max-width) + 16px));
transform: translate(
var(--port-label-transform-x),
calc(var(--node-size-y) + var(--output-port-max-width) + 16px)
);
}
</style>
Loading
Loading