feat(editor): Update new canvas node handle label rendering mechanism and design (no-changelog) (#10611)

This commit is contained in:
Alex Grozav 2024-08-29 17:56:50 +03:00 committed by GitHub
parent 402a8b40c0
commit 38eb00a643
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 135 additions and 38 deletions

View file

@ -72,6 +72,12 @@
--color-canvas-read-only-line: var(--prim-gray-800); --color-canvas-read-only-line: var(--prim-gray-800);
--color-canvas-selected: var(--prim-gray-0-alpha-025); --color-canvas-selected: var(--prim-gray-0-alpha-025);
--color-canvas-selected-transparent: var(--color-canvas-selected); --color-canvas-selected-transparent: var(--color-canvas-selected);
--color-canvas-label-background: hsla(
var(--color-canvas-background-h),
var(--color-canvas-background-s),
var(--color-canvas-background-l),
0.85
);
// Nodes // Nodes
--color-node-background: var(--prim-gray-740); --color-node-background: var(--prim-gray-740);

View file

@ -80,6 +80,12 @@
--color-canvas-read-only-line: var(--prim-gray-30); --color-canvas-read-only-line: var(--prim-gray-30);
--color-canvas-selected: var(--prim-gray-70); --color-canvas-selected: var(--prim-gray-70);
--color-canvas-selected-transparent: hsla(var(--prim-gray-h), 47%, 30%, 0.1); --color-canvas-selected-transparent: hsla(var(--prim-gray-h), 47%, 30%, 0.1);
--color-canvas-label-background: hsla(
var(--color-canvas-background-h),
var(--color-canvas-background-s),
var(--color-canvas-background-l),
0.85
);
// Nodes // Nodes
--color-node-background: var(--color-background-xlight); --color-node-background: var(--color-background-xlight);

View file

@ -22,13 +22,14 @@ const handleClasses = 'target';
.label { .label {
position: absolute; position: absolute;
top: 20px; top: 50%;
left: 50%; left: calc(var(--spacing-xs) * -1);
transform: translate(-50%, 0); transform: translate(-100%, -50%);
font-size: var(--font-size-2xs); font-size: var(--font-size-2xs);
color: var(--color-foreground-xdark); color: var(--color-foreground-xdark);
background: var(--color-background-light); background: var(--color-canvas-label-background);
z-index: 1; z-index: 1;
text-align: center; text-align: center;
white-space: nowrap;
} }
</style> </style>

View file

@ -1,17 +1,31 @@
<script lang="ts" setup> <script lang="ts" setup>
import { useCanvasNodeHandle } from '@/composables/useCanvasNodeHandle'; import { useCanvasNodeHandle } from '@/composables/useCanvasNodeHandle';
import { useCanvasNode } from '@/composables/useCanvasNode';
import { computed, ref } from 'vue'; import { computed, ref } from 'vue';
import type { CanvasNodeDefaultRender } from '@/types';
const emit = defineEmits<{ const emit = defineEmits<{
add: []; add: [];
}>(); }>();
const { render } = useCanvasNode();
const { label, isConnected, isConnecting } = useCanvasNodeHandle(); const { label, isConnected, isConnecting } = useCanvasNodeHandle();
const handleClasses = 'source'; const handleClasses = 'source';
const isHovered = ref(false);
const renderOptions = computed(() => render.value.options as CanvasNodeDefaultRender['options']);
const isHandlePlusVisible = computed(() => !isConnecting.value || isHovered.value); const isHandlePlusVisible = computed(() => !isConnecting.value || isHovered.value);
const isHovered = ref(false);
const plusLineSize = computed(
() =>
({
small: 46,
medium: 66,
large: 80,
})[renderOptions.value.outputs?.labelSize ?? 'small'],
);
function onMouseEnter() { function onMouseEnter() {
isHovered.value = true; isHovered.value = true;
@ -33,6 +47,7 @@ function onClickAdd() {
<CanvasHandlePlus <CanvasHandlePlus
v-if="!isConnected" v-if="!isConnected"
v-show="isHandlePlusVisible" v-show="isHandlePlusVisible"
:line-size="plusLineSize"
:handle-classes="handleClasses" :handle-classes="handleClasses"
@mouseenter="onMouseEnter" @mouseenter="onMouseEnter"
@mouseleave="onMouseLeave" @mouseleave="onMouseLeave"
@ -53,12 +68,16 @@ function onClickAdd() {
.label { .label {
position: absolute; position: absolute;
top: 50%; top: 50%;
left: var(--spacing-s); left: var(--spacing-m);
transform: translate(0, -50%); transform: translate(0, -50%);
font-size: var(--font-size-2xs); font-size: var(--font-size-2xs);
color: var(--color-foreground-xdark); color: var(--color-foreground-xdark);
background: var(--color-background-light); background: var(--color-canvas-label-background);
z-index: 1; z-index: 1;
max-width: calc(100% - var(--spacing-m) - 24px);
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
} }
</style> </style>

View file

@ -67,9 +67,10 @@ function onClickAdd() {
transform: translate(-50%, 0); transform: translate(-50%, 0);
font-size: var(--font-size-2xs); font-size: var(--font-size-2xs);
color: var(--color-foreground-xdark); color: var(--color-foreground-xdark);
background: var(--color-background-light); background: var(--color-canvas-label-background);
z-index: 1; z-index: 1;
text-align: center; text-align: center;
white-space: nowrap;
} }
</style> </style>

View file

@ -27,8 +27,9 @@ const handleClasses = 'source';
transform: translate(0%, 0); transform: translate(0%, 0);
font-size: var(--font-size-2xs); font-size: var(--font-size-2xs);
color: var(--color-foreground-xdark); color: var(--color-foreground-xdark);
background: var(--color-background-light); background: var(--color-canvas-label-background);
z-index: 0; z-index: 0;
white-space: nowrap;
} }
:global(.vue-flow__handle:not(.connectionindicator)) .plus { :global(.vue-flow__handle:not(.connectionindicator)) .plus {

View file

@ -5,10 +5,14 @@ const props = withDefaults(
defineProps<{ defineProps<{
position?: 'top' | 'right' | 'bottom' | 'left'; position?: 'top' | 'right' | 'bottom' | 'left';
handleClasses?: string; handleClasses?: string;
plusSize?: number;
lineSize?: number;
}>(), }>(),
{ {
position: 'right', position: 'right',
handleClasses: undefined, handleClasses: undefined,
plusSize: 24,
lineSize: 46,
}, },
); );
@ -20,46 +24,48 @@ const style = useCssModule();
const classes = computed(() => [style.wrapper, style[props.position], props.handleClasses]); const classes = computed(() => [style.wrapper, style[props.position], props.handleClasses]);
const plusSize = 24;
const lineSize = 46;
const viewBox = computed(() => { const viewBox = computed(() => {
switch (props.position) { switch (props.position) {
case 'bottom': case 'bottom':
case 'top': case 'top':
return { return {
width: plusSize, width: props.plusSize,
height: lineSize + plusSize, height: props.lineSize + props.plusSize,
}; };
default: default:
return { return {
width: lineSize + plusSize, width: props.lineSize + props.plusSize,
height: plusSize, height: props.plusSize,
}; };
} }
}); });
const styles = computed(() => ({
width: `${viewBox.value.width}px`,
height: `${viewBox.value.height}px`,
}));
const linePosition = computed(() => { const linePosition = computed(() => {
switch (props.position) { switch (props.position) {
case 'top': case 'top':
return [ return [
[viewBox.value.width / 2, viewBox.value.height - lineSize + 1], [viewBox.value.width / 2, viewBox.value.height - props.lineSize + 1],
[viewBox.value.width / 2, viewBox.value.height], [viewBox.value.width / 2, viewBox.value.height],
]; ];
case 'bottom': case 'bottom':
return [ return [
[viewBox.value.width / 2, 0], [viewBox.value.width / 2, 0],
[viewBox.value.width / 2, lineSize + 1], [viewBox.value.width / 2, props.lineSize + 1],
]; ];
case 'left': case 'left':
return [ return [
[viewBox.value.width - lineSize - 1, viewBox.value.height / 2], [viewBox.value.width - props.lineSize - 1, viewBox.value.height / 2],
[viewBox.value.width, viewBox.value.height / 2], [viewBox.value.width, viewBox.value.height / 2],
]; ];
default: default:
return [ return [
[0, viewBox.value.height / 2], [0, viewBox.value.height / 2],
[lineSize + 1, viewBox.value.height / 2], [props.lineSize + 1, viewBox.value.height / 2],
]; ];
} }
}); });
@ -67,13 +73,13 @@ const linePosition = computed(() => {
const plusPosition = computed(() => { const plusPosition = computed(() => {
switch (props.position) { switch (props.position) {
case 'bottom': case 'bottom':
return [0, viewBox.value.height - plusSize]; return [0, viewBox.value.height - props.plusSize];
case 'top': case 'top':
return [0, 0]; return [0, 0];
case 'left': case 'left':
return [0, 0]; return [0, 0];
default: default:
return [viewBox.value.width - plusSize, 0]; return [viewBox.value.width - props.plusSize, 0];
} }
}); });
@ -83,7 +89,7 @@ function onClick(event: MouseEvent) {
</script> </script>
<template> <template>
<svg :class="classes" :viewBox="`0 0 ${viewBox.width} ${viewBox.height}`"> <svg :class="classes" :viewBox="`0 0 ${viewBox.width} ${viewBox.height}`" :style="styles">
<line <line
:class="handleClasses" :class="handleClasses"
:x1="linePosition[0][0]" :x1="linePosition[0][0]"
@ -121,18 +127,6 @@ function onClick(event: MouseEvent) {
<style lang="scss" module> <style lang="scss" module>
.wrapper { .wrapper {
position: relative; position: relative;
&.top,
&.bottom {
width: 24px;
height: 70px;
}
&.left,
&.right {
width: 70px;
height: 24px;
}
} }
.plus { .plus {

View file

@ -1,7 +1,7 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`CanvasHandleDiamond > should render with default props 1`] = ` exports[`CanvasHandleDiamond > should render with default props 1`] = `
"<svg class="wrapper right" viewBox="0 0 70 24"> "<svg class="wrapper right" viewBox="0 0 70 24" style="width: 70px; height: 24px;">
<line class="" x1="0" y1="12" x2="47" y2="12" stroke="var(--color-foreground-xdark)" stroke-width="2"></line> <line class="" x1="0" y1="12" x2="47" y2="12" stroke="var(--color-foreground-xdark)" stroke-width="2"></line>
<g class="plus clickable" transform="translate(46, 0)"> <g class="plus clickable" transform="translate(46, 0)">
<rect class="clickable" x="2" y="2" width="20" height="20" stroke="var(--color-foreground-xdark)" stroke-width="2" rx="4" fill="#ffffff"></rect> <rect class="clickable" x="2" y="2" width="20" height="20" stroke="var(--color-foreground-xdark)" stroke-width="2" rx="4" fill="#ffffff"></rect>

View file

@ -1,7 +1,7 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`CanvasHandlePlus > should render with default props 1`] = ` exports[`CanvasHandlePlus > should render with default props 1`] = `
"<svg class="wrapper right" viewBox="0 0 70 24"> "<svg class="wrapper right" viewBox="0 0 70 24" style="width: 70px; height: 24px;">
<line class="" x1="0" y1="12" x2="47" y2="12" stroke="var(--color-foreground-xdark)" stroke-width="2"></line> <line class="" x1="0" y1="12" x2="47" y2="12" stroke="var(--color-foreground-xdark)" stroke-width="2"></line>
<g class="plus clickable" transform="translate(46, 0)"> <g class="plus clickable" transform="translate(46, 0)">
<rect class="clickable" x="2" y="2" width="20" height="20" stroke="var(--color-foreground-xdark)" stroke-width="2" rx="4" fill="#ffffff"></rect> <rect class="clickable" x="2" y="2" width="20" height="20" stroke="var(--color-foreground-xdark)" stroke-width="2" rx="4" fill="#ffffff"></rect>

View file

@ -133,6 +133,12 @@ describe('useCanvasMapping', () => {
configurable: false, configurable: false,
configuration: false, configuration: false,
trigger: true, trigger: true,
inputs: {
labelSize: 'small',
},
outputs: {
labelSize: 'small',
},
}, },
}, },
}, },
@ -264,6 +270,12 @@ describe('useCanvasMapping', () => {
configurable: false, configurable: false,
configuration: false, configuration: false,
trigger: true, trigger: true,
inputs: {
labelSize: 'small',
},
outputs: {
labelSize: 'small',
},
}, },
}); });
}); });

View file

@ -16,6 +16,7 @@ import type {
CanvasNodeAddNodesRender, CanvasNodeAddNodesRender,
CanvasNodeData, CanvasNodeData,
CanvasNodeDefaultRender, CanvasNodeDefaultRender,
CanvasNodeDefaultRenderLabelSize,
CanvasNodeStickyNoteRender, CanvasNodeStickyNoteRender,
} from '@/types'; } from '@/types';
import { CanvasConnectionMode, CanvasNodeRenderType } from '@/types'; import { CanvasConnectionMode, CanvasNodeRenderType } from '@/types';
@ -31,7 +32,7 @@ import type {
ITaskData, ITaskData,
Workflow, Workflow,
} from 'n8n-workflow'; } from 'n8n-workflow';
import { NodeHelpers } from 'n8n-workflow'; import { NodeConnectionType, NodeHelpers } from 'n8n-workflow';
import type { INodeUi } from '@/Interface'; import type { INodeUi } from '@/Interface';
import { CUSTOM_API_CALL_KEY, STICKY_NODE_TYPE, WAIT_TIME_UNLIMITED } from '@/constants'; import { CUSTOM_API_CALL_KEY, STICKY_NODE_TYPE, WAIT_TIME_UNLIMITED } from '@/constants';
import { sanitizeHtml } from '@/utils/htmlUtils'; import { sanitizeHtml } from '@/utils/htmlUtils';
@ -78,6 +79,12 @@ export function useCanvasMapping({
trigger: nodeTypesStore.isTriggerNode(node.type), trigger: nodeTypesStore.isTriggerNode(node.type),
configuration: nodeTypesStore.isConfigNode(workflowObject.value, node, node.type), configuration: nodeTypesStore.isConfigNode(workflowObject.value, node, node.type),
configurable: nodeTypesStore.isConfigurableNode(workflowObject.value, node, node.type), configurable: nodeTypesStore.isConfigurableNode(workflowObject.value, node, node.type),
inputs: {
labelSize: nodeInputLabelSizeById.value[node.id],
},
outputs: {
labelSize: nodeOutputLabelSizeById.value[node.id],
},
}, },
}; };
} }
@ -142,6 +149,48 @@ export function useCanvasMapping({
}, {}), }, {}),
); );
function getLabelSize(label: string = ''): number {
if (label.length <= 2) {
return 0;
} else if (label.length <= 6) {
return 1;
} else {
return 2;
}
}
function getMaxNodePortsLabelSize(
ports: CanvasConnectionPort[],
): CanvasNodeDefaultRenderLabelSize {
const labelSizes: CanvasNodeDefaultRenderLabelSize[] = ['small', 'medium', 'large'];
const labelSizeIndexes = ports.reduce<number[]>(
(sizeAcc, input) => {
if (input.type === NodeConnectionType.Main) {
sizeAcc.push(getLabelSize(input.label ?? ''));
}
return sizeAcc;
},
[0],
);
return labelSizes[Math.max(...labelSizeIndexes)];
}
const nodeInputLabelSizeById = computed(() =>
nodes.value.reduce<Record<string, CanvasNodeDefaultRenderLabelSize>>((acc, node) => {
acc[node.id] = getMaxNodePortsLabelSize(nodeInputsById.value[node.id]);
return acc;
}, {}),
);
const nodeOutputLabelSizeById = computed(() =>
nodes.value.reduce<Record<string, CanvasNodeDefaultRenderLabelSize>>((acc, node) => {
acc[node.id] = getMaxNodePortsLabelSize(nodeOutputsById.value[node.id]);
return acc;
}, {}),
);
const nodeOutputsById = computed(() => const nodeOutputsById = computed(() =>
nodes.value.reduce<Record<string, CanvasConnectionPort[]>>((acc, node) => { nodes.value.reduce<Record<string, CanvasConnectionPort[]>>((acc, node) => {
const nodeTypeDescription = nodeTypesStore.getNodeType(node.type, node.typeVersion); const nodeTypeDescription = nodeTypesStore.getNodeType(node.type, node.typeVersion);

View file

@ -44,12 +44,20 @@ export const enum CanvasNodeRenderType {
AddNodes = 'n8n-nodes-internal.addNodes', AddNodes = 'n8n-nodes-internal.addNodes',
} }
export type CanvasNodeDefaultRenderLabelSize = 'small' | 'medium' | 'large';
export type CanvasNodeDefaultRender = { export type CanvasNodeDefaultRender = {
type: CanvasNodeRenderType.Default; type: CanvasNodeRenderType.Default;
options: Partial<{ options: Partial<{
configurable: boolean; configurable: boolean;
configuration: boolean; configuration: boolean;
trigger: boolean; trigger: boolean;
inputs: {
labelSize: CanvasNodeDefaultRenderLabelSize;
};
outputs: {
labelSize: CanvasNodeDefaultRenderLabelSize;
};
}>; }>;
}; };