mirror of
https://github.com/enso-org/enso.git
synced 2024-12-23 17:34:10 +03:00
Move node type to visualization container (#9784)
This commit is contained in:
parent
e81e3661b2
commit
6655d5fbb2
@ -1,28 +1,30 @@
|
|||||||
import { expect, test, type Locator, type Page } from '@playwright/test'
|
import { test, type Locator, type Page } from '@playwright/test'
|
||||||
import assert from 'assert'
|
|
||||||
import * as actions from './actions'
|
import * as actions from './actions'
|
||||||
|
import { expect } from './customExpect'
|
||||||
import { mockExpressionUpdate } from './expressionUpdates'
|
import { mockExpressionUpdate } from './expressionUpdates'
|
||||||
import * as locate from './locate'
|
import * as locate from './locate'
|
||||||
|
|
||||||
const DUMMY_INT_TYPE = 'Standard.Base.Data.Numbers.Integer'
|
const DUMMY_INT_TYPE = { full: 'Standard.Base.Data.Numbers.Integer', short: 'Integer' }
|
||||||
const DUMMY_STRING_TYPE = 'Standard.Base.Data.Text.Text'
|
const DUMMY_STRING_TYPE = { full: 'Standard.Base.Data.Text.Text', short: 'Text' }
|
||||||
const DUMMY_FLOAT_TYPE = 'Standard.Base.Data.Numbers.Float'
|
const DUMMY_FLOAT_TYPE = { full: 'Standard.Base.Data.Numbers.Float', short: 'Float' }
|
||||||
const UNKNOWN_TYPE = 'Unknown'
|
const UNKNOWN_TYPE = { full: 'Unknown', short: 'Unknown' }
|
||||||
async function assertTypeLabelOnNode(page: Page, node: Locator, type: string) {
|
async function assertTypeLabelOnNode(
|
||||||
const targetLabel = node.locator('.outputPortLabel').first()
|
page: Page,
|
||||||
await expect(targetLabel).toHaveText(type)
|
node: Locator,
|
||||||
await expect(targetLabel).toHaveCSS('opacity', '0')
|
type: { full: string; short: string },
|
||||||
|
) {
|
||||||
const outputPortArea = await node.locator('.outputPortHoverArea').boundingBox()
|
await node.hover({ position: { x: 8, y: 8 } })
|
||||||
assert(outputPortArea, 'The outputPortArea of the node is null')
|
await locate.toggleVisualizationButton(node).click()
|
||||||
const outputPortX = outputPortArea.x + outputPortArea.width / 2.0
|
const targetLabel = node.locator('.node-type').first()
|
||||||
const outputPortY = outputPortArea.y + outputPortArea.height - 2.0
|
await expect(targetLabel).toHaveText(type.short)
|
||||||
await page.mouse.move(outputPortX, outputPortY)
|
await expect(targetLabel).toHaveAttribute('title', type.full)
|
||||||
await expect(targetLabel).toBeVisible()
|
|
||||||
await expect(targetLabel).toHaveCSS('opacity', '1')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function assertTypeLabelOnNodeByBinding(page: Page, label: string, type: string) {
|
async function assertTypeLabelOnNodeByBinding(
|
||||||
|
page: Page,
|
||||||
|
label: string,
|
||||||
|
type: { full: string; short: string },
|
||||||
|
) {
|
||||||
const node = locate.graphNodeByBinding(page, label)
|
const node = locate.graphNodeByBinding(page, label)
|
||||||
await assertTypeLabelOnNode(page, node, type)
|
await assertTypeLabelOnNode(page, node, type)
|
||||||
}
|
}
|
||||||
@ -31,10 +33,10 @@ test('shows the correct type when hovering a node', async ({ page }) => {
|
|||||||
await actions.goToGraph(page)
|
await actions.goToGraph(page)
|
||||||
|
|
||||||
// Note that the types don't have to make sense, they just have to be applied.
|
// Note that the types don't have to make sense, they just have to be applied.
|
||||||
await mockExpressionUpdate(page, 'five', { type: DUMMY_INT_TYPE })
|
await mockExpressionUpdate(page, 'five', { type: DUMMY_INT_TYPE.full })
|
||||||
await mockExpressionUpdate(page, 'ten', { type: DUMMY_STRING_TYPE })
|
await mockExpressionUpdate(page, 'ten', { type: DUMMY_STRING_TYPE.full })
|
||||||
await mockExpressionUpdate(page, 'sum', { type: DUMMY_FLOAT_TYPE })
|
await mockExpressionUpdate(page, 'sum', { type: DUMMY_FLOAT_TYPE.full })
|
||||||
await mockExpressionUpdate(page, 'prod', { type: DUMMY_INT_TYPE })
|
await mockExpressionUpdate(page, 'prod', { type: DUMMY_INT_TYPE.full })
|
||||||
|
|
||||||
await assertTypeLabelOnNodeByBinding(page, 'five', DUMMY_INT_TYPE)
|
await assertTypeLabelOnNodeByBinding(page, 'five', DUMMY_INT_TYPE)
|
||||||
await assertTypeLabelOnNodeByBinding(page, 'ten', DUMMY_STRING_TYPE)
|
await assertTypeLabelOnNodeByBinding(page, 'ten', DUMMY_STRING_TYPE)
|
||||||
|
@ -373,7 +373,7 @@ const handleNodeClick = useDoubleClick(
|
|||||||
|
|
||||||
interface PortData {
|
interface PortData {
|
||||||
clipRange: [number, number]
|
clipRange: [number, number]
|
||||||
label: string
|
label: string | undefined
|
||||||
portId: AstId
|
portId: AstId
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -381,12 +381,9 @@ const outputPorts = computed((): PortData[] => {
|
|||||||
const ports = outputPortsSet.value
|
const ports = outputPortsSet.value
|
||||||
const numPorts = ports.size
|
const numPorts = ports.size
|
||||||
return Array.from(ports, (portId, index): PortData => {
|
return Array.from(ports, (portId, index): PortData => {
|
||||||
const labelIdent = numPorts > 1 ? graph.db.getOutputPortIdentifier(portId) + ': ' : ''
|
|
||||||
const labelType =
|
|
||||||
graph.db.getExpressionInfo(numPorts > 1 ? portId : nodeId.value)?.typename ?? 'Unknown'
|
|
||||||
return {
|
return {
|
||||||
clipRange: [index / numPorts, (index + 1) / numPorts],
|
clipRange: [index / numPorts, (index + 1) / numPorts],
|
||||||
label: labelIdent + labelType,
|
label: numPorts > 1 ? graph.db.getOutputPortIdentifier(portId) : undefined,
|
||||||
portId,
|
portId,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -293,6 +293,9 @@ provideVisualizationConfig({
|
|||||||
get icon() {
|
get icon() {
|
||||||
return icon.value
|
return icon.value
|
||||||
},
|
},
|
||||||
|
get nodeType() {
|
||||||
|
return props.typename
|
||||||
|
},
|
||||||
hide: () => emit('update:visible', false),
|
hide: () => emit('update:visible', false),
|
||||||
updateType: (id) => emit('update:id', id),
|
updateType: (id) => emit('update:id', id),
|
||||||
createNodes: (...options) => emit('createNodes', options),
|
createNodes: (...options) => emit('createNodes', options),
|
||||||
|
@ -4,7 +4,8 @@ import SvgIcon from '@/components/SvgIcon.vue'
|
|||||||
import VisualizationSelector from '@/components/VisualizationSelector.vue'
|
import VisualizationSelector from '@/components/VisualizationSelector.vue'
|
||||||
import { PointerButtonMask, isTriggeredByKeyboard, usePointer } from '@/composables/events'
|
import { PointerButtonMask, isTriggeredByKeyboard, usePointer } from '@/composables/events'
|
||||||
import { useVisualizationConfig } from '@/providers/visualizationConfig'
|
import { useVisualizationConfig } from '@/providers/visualizationConfig'
|
||||||
import { onMounted, ref, watchEffect } from 'vue'
|
import { isQualifiedName, qnLastSegment } from '@/util/qualifiedName'
|
||||||
|
import { computed, onMounted, ref, watchEffect } from 'vue'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
/** If true, the visualization should be `overflow: visible` instead of `overflow: hidden`. */
|
/** If true, the visualization should be `overflow: visible` instead of `overflow: hidden`. */
|
||||||
@ -55,39 +56,34 @@ function hideSelector() {
|
|||||||
requestAnimationFrame(() => (isSelectorVisible.value = false))
|
requestAnimationFrame(() => (isSelectorVisible.value = false))
|
||||||
}
|
}
|
||||||
|
|
||||||
const resizeRight = usePointer((pos, _, type) => {
|
function resizeHandler(resizeX: boolean, resizeY: boolean) {
|
||||||
if (type !== 'move' || pos.delta.x === 0) {
|
return usePointer((pos, _, type) => {
|
||||||
return
|
|
||||||
}
|
|
||||||
const width =
|
|
||||||
(pos.absolute.x - (contentNode.value?.getBoundingClientRect().left ?? 0)) / config.scale
|
|
||||||
config.width = Math.max(width, MIN_WIDTH_PX)
|
|
||||||
}, PointerButtonMask.Main)
|
|
||||||
|
|
||||||
const resizeBottom = usePointer((pos, _, type) => {
|
|
||||||
if (type !== 'move' || pos.delta.y === 0) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const height =
|
|
||||||
(pos.absolute.y - (contentNode.value?.getBoundingClientRect().top ?? 0)) / config.scale
|
|
||||||
config.height = Math.max(0, height)
|
|
||||||
}, PointerButtonMask.Main)
|
|
||||||
|
|
||||||
const resizeBottomRight = usePointer((pos, _, type) => {
|
|
||||||
if (type !== 'move') {
|
if (type !== 'move') {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (pos.delta.x !== 0) {
|
if (resizeX && pos.delta.x !== 0) {
|
||||||
const width =
|
const width =
|
||||||
(pos.absolute.x - (contentNode.value?.getBoundingClientRect().left ?? 0)) / config.scale
|
(pos.absolute.x - (contentNode.value?.getBoundingClientRect().left ?? 0)) / config.scale
|
||||||
config.width = Math.max(0, width)
|
config.width = Math.max(0, width)
|
||||||
}
|
}
|
||||||
if (pos.delta.y !== 0) {
|
if (resizeY && pos.delta.y !== 0) {
|
||||||
const height =
|
const height =
|
||||||
(pos.absolute.y - (contentNode.value?.getBoundingClientRect().top ?? 0)) / config.scale
|
(pos.absolute.y - (contentNode.value?.getBoundingClientRect().top ?? 0)) / config.scale
|
||||||
config.height = Math.max(0, height)
|
config.height = Math.max(0, height)
|
||||||
}
|
}
|
||||||
}, PointerButtonMask.Main)
|
}, PointerButtonMask.Main)
|
||||||
|
}
|
||||||
|
|
||||||
|
const resizeRight = resizeHandler(true, false)
|
||||||
|
const resizeBottom = resizeHandler(false, true)
|
||||||
|
const resizeBottomRight = resizeHandler(true, true)
|
||||||
|
|
||||||
|
const UNKNOWN_TYPE = 'Unknown'
|
||||||
|
const nodeShortType = computed(() =>
|
||||||
|
config.nodeType != null && isQualifiedName(config.nodeType) ?
|
||||||
|
qnLastSegment(config.nodeType)
|
||||||
|
: UNKNOWN_TYPE,
|
||||||
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -186,6 +182,11 @@ const resizeBottomRight = usePointer((pos, _, type) => {
|
|||||||
<div v-if="$slots.toolbar" class="visualization-defined-toolbars">
|
<div v-if="$slots.toolbar" class="visualization-defined-toolbars">
|
||||||
<div class="toolbar"><slot name="toolbar"></slot></div>
|
<div class="toolbar"><slot name="toolbar"></slot></div>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
class="after-toolbars node-type"
|
||||||
|
v-text="nodeShortType"
|
||||||
|
:title="config.nodeType ?? UNKNOWN_TYPE"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Teleport>
|
</Teleport>
|
||||||
@ -232,7 +233,6 @@ const resizeBottomRight = usePointer((pos, _, type) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.toolbars {
|
.toolbars {
|
||||||
width: 100%;
|
|
||||||
transition-duration: 100ms;
|
transition-duration: 100ms;
|
||||||
transition-property: padding-left;
|
transition-property: padding-left;
|
||||||
}
|
}
|
||||||
@ -250,6 +250,7 @@ const resizeBottomRight = usePointer((pos, _, type) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.toolbars {
|
.toolbars {
|
||||||
|
width: 100%;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -257,6 +258,15 @@ const resizeBottomRight = usePointer((pos, _, type) => {
|
|||||||
top: calc(var(--node-height) + 4px);
|
top: calc(var(--node-height) + 4px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.after-toolbars {
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-type {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
.VisualizationContainer.fullscreen .toolbars {
|
.VisualizationContainer.fullscreen .toolbars {
|
||||||
top: 4px;
|
top: 4px;
|
||||||
}
|
}
|
||||||
|
@ -16,6 +16,7 @@ export interface VisualizationConfig {
|
|||||||
readonly nodeSize: Vec2
|
readonly nodeSize: Vec2
|
||||||
readonly scale: number
|
readonly scale: number
|
||||||
readonly isFocused: boolean
|
readonly isFocused: boolean
|
||||||
|
readonly nodeType: string | undefined
|
||||||
isBelowToolbar: boolean
|
isBelowToolbar: boolean
|
||||||
width: number | null
|
width: number | null
|
||||||
height: number
|
height: number
|
||||||
|
Loading…
Reference in New Issue
Block a user