mirror of
https://github.com/n8n-io/n8n.git
synced 2024-12-24 04:04:06 -08:00
feat(editor): Update new canvas node handle label rendering mechanism and design (no-changelog) (#10611)
This commit is contained in:
parent
402a8b40c0
commit
38eb00a643
|
@ -72,6 +72,12 @@
|
|||
--color-canvas-read-only-line: var(--prim-gray-800);
|
||||
--color-canvas-selected: var(--prim-gray-0-alpha-025);
|
||||
--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
|
||||
--color-node-background: var(--prim-gray-740);
|
||||
|
|
|
@ -80,6 +80,12 @@
|
|||
--color-canvas-read-only-line: var(--prim-gray-30);
|
||||
--color-canvas-selected: var(--prim-gray-70);
|
||||
--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
|
||||
--color-node-background: var(--color-background-xlight);
|
||||
|
|
|
@ -22,13 +22,14 @@ const handleClasses = 'target';
|
|||
|
||||
.label {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
left: 50%;
|
||||
transform: translate(-50%, 0);
|
||||
top: 50%;
|
||||
left: calc(var(--spacing-xs) * -1);
|
||||
transform: translate(-100%, -50%);
|
||||
font-size: var(--font-size-2xs);
|
||||
color: var(--color-foreground-xdark);
|
||||
background: var(--color-background-light);
|
||||
background: var(--color-canvas-label-background);
|
||||
z-index: 1;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,17 +1,31 @@
|
|||
<script lang="ts" setup>
|
||||
import { useCanvasNodeHandle } from '@/composables/useCanvasNodeHandle';
|
||||
import { useCanvasNode } from '@/composables/useCanvasNode';
|
||||
import { computed, ref } from 'vue';
|
||||
import type { CanvasNodeDefaultRender } from '@/types';
|
||||
|
||||
const emit = defineEmits<{
|
||||
add: [];
|
||||
}>();
|
||||
|
||||
const { render } = useCanvasNode();
|
||||
const { label, isConnected, isConnecting } = useCanvasNodeHandle();
|
||||
|
||||
const handleClasses = 'source';
|
||||
const isHovered = ref(false);
|
||||
|
||||
const renderOptions = computed(() => render.value.options as CanvasNodeDefaultRender['options']);
|
||||
|
||||
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() {
|
||||
isHovered.value = true;
|
||||
|
@ -33,6 +47,7 @@ function onClickAdd() {
|
|||
<CanvasHandlePlus
|
||||
v-if="!isConnected"
|
||||
v-show="isHandlePlusVisible"
|
||||
:line-size="plusLineSize"
|
||||
:handle-classes="handleClasses"
|
||||
@mouseenter="onMouseEnter"
|
||||
@mouseleave="onMouseLeave"
|
||||
|
@ -53,12 +68,16 @@ function onClickAdd() {
|
|||
.label {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: var(--spacing-s);
|
||||
left: var(--spacing-m);
|
||||
transform: translate(0, -50%);
|
||||
font-size: var(--font-size-2xs);
|
||||
color: var(--color-foreground-xdark);
|
||||
background: var(--color-background-light);
|
||||
background: var(--color-canvas-label-background);
|
||||
z-index: 1;
|
||||
max-width: calc(100% - var(--spacing-m) - 24px);
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
|
|
@ -67,9 +67,10 @@ function onClickAdd() {
|
|||
transform: translate(-50%, 0);
|
||||
font-size: var(--font-size-2xs);
|
||||
color: var(--color-foreground-xdark);
|
||||
background: var(--color-background-light);
|
||||
background: var(--color-canvas-label-background);
|
||||
z-index: 1;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
|
|
@ -27,8 +27,9 @@ const handleClasses = 'source';
|
|||
transform: translate(0%, 0);
|
||||
font-size: var(--font-size-2xs);
|
||||
color: var(--color-foreground-xdark);
|
||||
background: var(--color-background-light);
|
||||
background: var(--color-canvas-label-background);
|
||||
z-index: 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
:global(.vue-flow__handle:not(.connectionindicator)) .plus {
|
||||
|
|
|
@ -5,10 +5,14 @@ const props = withDefaults(
|
|||
defineProps<{
|
||||
position?: 'top' | 'right' | 'bottom' | 'left';
|
||||
handleClasses?: string;
|
||||
plusSize?: number;
|
||||
lineSize?: number;
|
||||
}>(),
|
||||
{
|
||||
position: 'right',
|
||||
handleClasses: undefined,
|
||||
plusSize: 24,
|
||||
lineSize: 46,
|
||||
},
|
||||
);
|
||||
|
||||
|
@ -20,46 +24,48 @@ const style = useCssModule();
|
|||
|
||||
const classes = computed(() => [style.wrapper, style[props.position], props.handleClasses]);
|
||||
|
||||
const plusSize = 24;
|
||||
const lineSize = 46;
|
||||
|
||||
const viewBox = computed(() => {
|
||||
switch (props.position) {
|
||||
case 'bottom':
|
||||
case 'top':
|
||||
return {
|
||||
width: plusSize,
|
||||
height: lineSize + plusSize,
|
||||
width: props.plusSize,
|
||||
height: props.lineSize + props.plusSize,
|
||||
};
|
||||
default:
|
||||
return {
|
||||
width: lineSize + plusSize,
|
||||
height: plusSize,
|
||||
width: props.lineSize + props.plusSize,
|
||||
height: props.plusSize,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
const styles = computed(() => ({
|
||||
width: `${viewBox.value.width}px`,
|
||||
height: `${viewBox.value.height}px`,
|
||||
}));
|
||||
|
||||
const linePosition = computed(() => {
|
||||
switch (props.position) {
|
||||
case 'top':
|
||||
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],
|
||||
];
|
||||
case 'bottom':
|
||||
return [
|
||||
[viewBox.value.width / 2, 0],
|
||||
[viewBox.value.width / 2, lineSize + 1],
|
||||
[viewBox.value.width / 2, props.lineSize + 1],
|
||||
];
|
||||
case 'left':
|
||||
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],
|
||||
];
|
||||
default:
|
||||
return [
|
||||
[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(() => {
|
||||
switch (props.position) {
|
||||
case 'bottom':
|
||||
return [0, viewBox.value.height - plusSize];
|
||||
return [0, viewBox.value.height - props.plusSize];
|
||||
case 'top':
|
||||
return [0, 0];
|
||||
case 'left':
|
||||
return [0, 0];
|
||||
default:
|
||||
return [viewBox.value.width - plusSize, 0];
|
||||
return [viewBox.value.width - props.plusSize, 0];
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -83,7 +89,7 @@ function onClick(event: MouseEvent) {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<svg :class="classes" :viewBox="`0 0 ${viewBox.width} ${viewBox.height}`">
|
||||
<svg :class="classes" :viewBox="`0 0 ${viewBox.width} ${viewBox.height}`" :style="styles">
|
||||
<line
|
||||
:class="handleClasses"
|
||||
:x1="linePosition[0][0]"
|
||||
|
@ -121,18 +127,6 @@ function onClick(event: MouseEvent) {
|
|||
<style lang="scss" module>
|
||||
.wrapper {
|
||||
position: relative;
|
||||
|
||||
&.top,
|
||||
&.bottom {
|
||||
width: 24px;
|
||||
height: 70px;
|
||||
}
|
||||
|
||||
&.left,
|
||||
&.right {
|
||||
width: 70px;
|
||||
height: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.plus {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
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>
|
||||
<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>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
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>
|
||||
<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>
|
||||
|
|
|
@ -133,6 +133,12 @@ describe('useCanvasMapping', () => {
|
|||
configurable: false,
|
||||
configuration: false,
|
||||
trigger: true,
|
||||
inputs: {
|
||||
labelSize: 'small',
|
||||
},
|
||||
outputs: {
|
||||
labelSize: 'small',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -264,6 +270,12 @@ describe('useCanvasMapping', () => {
|
|||
configurable: false,
|
||||
configuration: false,
|
||||
trigger: true,
|
||||
inputs: {
|
||||
labelSize: 'small',
|
||||
},
|
||||
outputs: {
|
||||
labelSize: 'small',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
|
|
@ -16,6 +16,7 @@ import type {
|
|||
CanvasNodeAddNodesRender,
|
||||
CanvasNodeData,
|
||||
CanvasNodeDefaultRender,
|
||||
CanvasNodeDefaultRenderLabelSize,
|
||||
CanvasNodeStickyNoteRender,
|
||||
} from '@/types';
|
||||
import { CanvasConnectionMode, CanvasNodeRenderType } from '@/types';
|
||||
|
@ -31,7 +32,7 @@ import type {
|
|||
ITaskData,
|
||||
Workflow,
|
||||
} from 'n8n-workflow';
|
||||
import { NodeHelpers } from 'n8n-workflow';
|
||||
import { NodeConnectionType, NodeHelpers } from 'n8n-workflow';
|
||||
import type { INodeUi } from '@/Interface';
|
||||
import { CUSTOM_API_CALL_KEY, STICKY_NODE_TYPE, WAIT_TIME_UNLIMITED } from '@/constants';
|
||||
import { sanitizeHtml } from '@/utils/htmlUtils';
|
||||
|
@ -78,6 +79,12 @@ export function useCanvasMapping({
|
|||
trigger: nodeTypesStore.isTriggerNode(node.type),
|
||||
configuration: nodeTypesStore.isConfigNode(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(() =>
|
||||
nodes.value.reduce<Record<string, CanvasConnectionPort[]>>((acc, node) => {
|
||||
const nodeTypeDescription = nodeTypesStore.getNodeType(node.type, node.typeVersion);
|
||||
|
|
|
@ -44,12 +44,20 @@ export const enum CanvasNodeRenderType {
|
|||
AddNodes = 'n8n-nodes-internal.addNodes',
|
||||
}
|
||||
|
||||
export type CanvasNodeDefaultRenderLabelSize = 'small' | 'medium' | 'large';
|
||||
|
||||
export type CanvasNodeDefaultRender = {
|
||||
type: CanvasNodeRenderType.Default;
|
||||
options: Partial<{
|
||||
configurable: boolean;
|
||||
configuration: boolean;
|
||||
trigger: boolean;
|
||||
inputs: {
|
||||
labelSize: CanvasNodeDefaultRenderLabelSize;
|
||||
};
|
||||
outputs: {
|
||||
labelSize: CanvasNodeDefaultRenderLabelSize;
|
||||
};
|
||||
}>;
|
||||
};
|
||||
|
||||
|
|
Loading…
Reference in a new issue