feat(editor): Improve ai nodes, handles and connections design in new canvas (no-changelog) (#10969)

This commit is contained in:
Alex Grozav 2024-09-30 14:29:30 +03:00 committed by GitHub
parent a62a9db8fa
commit fe871d89a9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 135 additions and 26 deletions

View file

@ -125,6 +125,7 @@ export function createCanvasHandleProvide({
isConnected = false,
isConnecting = false,
isReadOnly = false,
isRequired = false,
}: {
label?: string;
mode?: CanvasConnectionMode;
@ -134,6 +135,7 @@ export function createCanvasHandleProvide({
isConnected?: boolean;
isConnecting?: boolean;
isReadOnly?: boolean;
isRequired?: boolean;
} = {}) {
return {
[String(CanvasNodeHandleKey)]: {
@ -143,8 +145,9 @@ export function createCanvasHandleProvide({
index: ref(index),
isConnected: ref(isConnected),
isConnecting: ref(isConnecting),
runData: ref(runData),
isReadOnly: ref(isReadOnly),
isRequired: ref(isRequired),
runData: ref(runData),
} satisfies CanvasNodeHandleInjectionData,
};
}

View file

@ -5,7 +5,7 @@ import { isValidNodeConnectionType } from '@/utils/typeGuards';
import type { Connection, EdgeProps } from '@vue-flow/core';
import { useVueFlow, BaseEdge, EdgeLabelRenderer } from '@vue-flow/core';
import { NodeConnectionType } from 'n8n-workflow';
import { computed, useCssModule, ref } from 'vue';
import { computed, useCssModule, ref, toRef } from 'vue';
import CanvasEdgeToolbar from './CanvasEdgeToolbar.vue';
import { getCustomPath } from './utils/edgePath';
@ -21,6 +21,8 @@ export type CanvasEdgeProps = EdgeProps<CanvasConnectionData> & {
const props = defineProps<CanvasEdgeProps>();
const data = toRef(props, 'data');
const { onEdgeMouseEnter, onEdgeMouseLeave } = useVueFlow();
const isHovered = ref(false);
@ -44,6 +46,8 @@ const connectionType = computed(() =>
const renderToolbar = computed(() => (props.selected || isHovered.value) && !props.readOnly);
const isMainConnection = computed(() => data.value.source.type === NodeConnectionType.Main);
const status = computed(() => props.data.status);
const statusColor = computed(() => {
if (props.selected) {
@ -54,6 +58,8 @@ const statusColor = computed(() => {
return 'var(--color-secondary)';
} else if (status.value === 'running') {
return 'var(--color-primary)';
} else if (!isMainConnection.value) {
return 'var(--node-type-supplemental-color)';
} else {
return 'var(--color-foreground-xdark)';
}
@ -61,6 +67,7 @@ const statusColor = computed(() => {
const edgeStyle = computed(() => ({
...props.style,
...(isMainConnection.value ? {} : { strokeDasharray: '8,8' }),
strokeWidth: 2,
stroke: isHovered.value ? 'var(--color-primary)' : statusColor.value,
}));

View file

@ -20,6 +20,7 @@ const props = defineProps<{
isConnecting?: boolean;
isReadOnly?: boolean;
label?: string;
required?: boolean;
type: CanvasConnectionPort['type'];
index: CanvasConnectionPort['index'];
position: CanvasElementPortWithRenderData['position'];
@ -116,6 +117,7 @@ const isReadOnly = toRef(props, 'isReadOnly');
const mode = toRef(props, 'mode');
const type = toRef(props, 'type');
const index = toRef(props, 'index');
const isRequired = toRef(props, 'required');
provide(CanvasNodeHandleKey, {
label,
@ -123,6 +125,7 @@ provide(CanvasNodeHandleKey, {
type,
index,
runData,
isRequired,
isConnected,
isConnecting,
isReadOnly,

View file

@ -1,12 +1,21 @@
<script lang="ts" setup>
import { useCanvasNodeHandle } from '@/composables/useCanvasNodeHandle';
import { computed, useCssModule } from 'vue';
const { label } = useCanvasNodeHandle();
const $style = useCssModule();
const { label, isRequired } = useCanvasNodeHandle();
const classes = computed(() => ({
'canvas-node-handle-main-input': true,
[$style.handle]: true,
[$style.required]: isRequired.value,
}));
const handleClasses = 'target';
</script>
<template>
<div :class="['canvas-node-handle-main-input', $style.handle]">
<div :class="classes">
<div :class="[$style.label]">{{ label }}</div>
<CanvasHandleRectangle :handle-classes="handleClasses" />
</div>
@ -32,4 +41,9 @@ const handleClasses = 'target';
text-align: center;
white-space: nowrap;
}
.required .label::after {
content: '*';
color: var(--color-danger);
}
</style>

View file

@ -1,7 +1,7 @@
<script lang="ts" setup>
import { useCanvasNodeHandle } from '@/composables/useCanvasNodeHandle';
import { useCanvasNode } from '@/composables/useCanvasNode';
import { computed, ref } from 'vue';
import { computed, ref, useCssModule } from 'vue';
import type { CanvasNodeDefaultRender } from '@/types';
import { useI18n } from '@/composables/useI18n';
@ -9,11 +9,20 @@ const emit = defineEmits<{
add: [];
}>();
const $style = useCssModule();
const i18n = useI18n();
const { render } = useCanvasNode();
const { label, isConnected, isConnecting, isReadOnly, runData } = useCanvasNodeHandle();
const { label, isConnected, isConnecting, isReadOnly, isRequired, runData } = useCanvasNodeHandle();
const handleClasses = 'source';
const classes = computed(() => ({
'canvas-node-handle-main-output': true,
[$style.handle]: true,
[$style.required]: isRequired.value,
}));
const isHovered = ref(false);
const renderOptions = computed(() => render.value.options as CanvasNodeDefaultRender['options']);
@ -29,7 +38,7 @@ const runDataLabel = computed(() =>
const isHandlePlusVisible = computed(() => !isConnecting.value || isHovered.value);
const plusState = computed(() => (runData.value ? 'success' : 'default'));
const plusStatus = computed(() => (runData.value ? 'success' : 'default'));
const plusLineSize = computed(
() =>
@ -53,7 +62,7 @@ function onClickAdd() {
}
</script>
<template>
<div :class="['canvas-node-handle-main-output', $style.handle]">
<div :class="classes">
<div v-if="label" :class="[$style.label, $style.outputLabel]">{{ label }}</div>
<div v-else-if="runData" :class="[$style.label, $style.runDataLabel]">{{ runDataLabel }}</div>
<CanvasHandleDot :handle-classes="handleClasses" />
@ -64,7 +73,7 @@ function onClickAdd() {
data-test-id="canvas-handle-plus"
:line-size="plusLineSize"
:handle-classes="handleClasses"
:state="plusState"
:status="plusStatus"
@mouseenter="onMouseEnter"
@mouseleave="onMouseLeave"
@click:plus="onClickAdd"
@ -91,6 +100,11 @@ function onClickAdd() {
overflow: hidden;
}
.required .label::after {
content: '*';
color: var(--color-danger);
}
.outputLabel {
top: 50%;
left: var(--spacing-m);

View file

@ -2,24 +2,36 @@
import CanvasHandlePlus from '@/components/canvas/elements/handles/render-types/parts/CanvasHandlePlus.vue';
import { useCanvasNodeHandle } from '@/composables/useCanvasNodeHandle';
import { NodeConnectionType } from 'n8n-workflow';
import { computed, ref } from 'vue';
import { computed, ref, useCssModule } from 'vue';
const emit = defineEmits<{
add: [];
}>();
const { label, isConnected, isConnecting, type } = useCanvasNodeHandle();
const $style = useCssModule();
const { label, isConnected, isConnecting, isRequired, type, runData } = useCanvasNodeHandle();
const handleClasses = 'target';
const classes = computed(() => ({
'canvas-node-handle-non-main-input': true,
[$style.handle]: true,
[$style.required]: isRequired.value,
}));
const supportsMultipleConnections = computed(() => type.value === NodeConnectionType.AiTool);
const isHandlePlusAvailable = computed(
() => !isConnected.value || supportsMultipleConnections.value,
);
const isHandlePlusVisible = computed(
() => !isConnecting.value || isHovered.value || supportsMultipleConnections.value,
);
const plusStatus = computed(() => (runData.value ? 'success' : 'ai'));
const isHovered = ref(false);
function onMouseEnter() {
@ -35,7 +47,7 @@ function onClickAdd() {
}
</script>
<template>
<div :class="['canvas-node-handle-non-main-input', $style.handle]">
<div :class="classes">
<div :class="[$style.label]">{{ label }}</div>
<CanvasHandleDiamond :handle-classes="handleClasses" />
<Transition name="canvas-node-handle-non-main-input">
@ -43,6 +55,7 @@ function onClickAdd() {
v-if="isHandlePlusAvailable"
v-show="isHandlePlusVisible"
:handle-classes="handleClasses"
:status="plusStatus"
position="bottom"
@mouseenter="onMouseEnter"
@mouseleave="onMouseLeave"
@ -66,12 +79,17 @@ function onClickAdd() {
left: 50%;
transform: translate(-50%, 0);
font-size: var(--font-size-2xs);
color: var(--color-foreground-xdark);
color: var(--node-type-supplemental-color);
background: var(--color-canvas-label-background);
z-index: 1;
text-align: center;
white-space: nowrap;
}
.required .label::after {
content: '*';
color: var(--color-danger);
}
</style>
<style lang="scss">

View file

@ -1,13 +1,21 @@
<script lang="ts" setup>
import { useCanvasNodeHandle } from '@/composables/useCanvasNodeHandle';
import { computed, useCssModule } from 'vue';
const { label } = useCanvasNodeHandle();
const $style = useCssModule();
const { label, isRequired } = useCanvasNodeHandle();
const handleClasses = 'source';
const classes = computed(() => ({
'canvas-node-handle-non-main-output': true,
[$style.handle]: true,
[$style.required]: isRequired.value,
}));
</script>
<template>
<div :class="['canvas-node-handle-non-main-output', $style.handle]">
<div :class="[$style.label]">{{ label }}</div>
<div :class="classes">
<div :class="$style.label">{{ label }}</div>
<CanvasHandleDiamond :handle-classes="handleClasses" />
</div>
</template>
@ -23,15 +31,20 @@ const handleClasses = 'source';
.label {
position: absolute;
top: -20px;
left: -50%;
transform: translate(0%, 0);
left: 50%;
transform: translate(-50%, 0);
font-size: var(--font-size-2xs);
color: var(--color-foreground-xdark);
color: var(--node-type-supplemental-color);
background: var(--color-canvas-label-background);
z-index: 0;
white-space: nowrap;
}
.required .label::after {
content: '*';
color: var(--color-danger);
}
:global(.vue-flow__handle:not(.connectionindicator)) .plus {
display: none;
position: absolute;

View file

@ -40,9 +40,9 @@ describe('CanvasHandlePlus', () => {
});
});
it('should apply correct classes based on state', () => {
it('should apply correct classes based on status', () => {
const { container } = renderComponent({
props: { state: 'success' },
props: { status: 'success' },
});
expect(container.firstChild).toHaveClass('success');

View file

@ -7,14 +7,14 @@ const props = withDefaults(
handleClasses?: string;
plusSize?: number;
lineSize?: number;
state?: 'success' | 'default';
status?: 'success' | 'ai' | 'default';
}>(),
{
position: 'right',
handleClasses: undefined,
plusSize: 24,
lineSize: 46,
state: 'default',
status: 'default',
},
);
@ -27,7 +27,7 @@ const style = useCssModule();
const classes = computed(() => [
style.wrapper,
style[props.position],
style[props.state],
style[props.status],
props.handleClasses,
]);
@ -135,8 +135,26 @@ function onClick(event: MouseEvent) {
.wrapper {
position: relative;
&.success .line {
stroke: var(--color-success);
&.ai {
.line {
stroke: var(--node-type-supplemental-color);
}
.plus {
path {
fill: var(--node-type-supplemental-color);
}
rect {
stroke: var(--node-type-supplemental-color);
}
}
}
&.success {
.line {
stroke: var(--color-success);
}
}
}

View file

@ -269,6 +269,7 @@ onBeforeUnmount(() => {
:index="source.index"
:position="source.position"
:offset="source.offset"
:required="source.required"
:is-connected="source.isConnected"
:is-connecting="source.isConnecting"
:is-read-only="readOnly"
@ -286,6 +287,7 @@ onBeforeUnmount(() => {
:index="target.index"
:position="target.position"
:offset="target.offset"
:required="target.required"
:is-connected="target.isConnected"
:is-connecting="target.isConnecting"
:is-read-only="readOnly"

View file

@ -190,6 +190,7 @@ function openContextMenu(event: MouseEvent) {
.description {
top: unset;
position: relative;
margin-top: 0;
margin-left: var(--spacing-s);
width: auto;
min-width: unset;
@ -199,6 +200,19 @@ function openContextMenu(event: MouseEvent) {
) - 2 * var(--spacing-s)
);
}
.label {
text-align: left;
}
&.configuration {
--canvas-node--height: 75px;
.statusIcons {
right: calc(-1 * var(--spacing-2xs));
bottom: 0;
}
}
}
/**

View file

@ -15,6 +15,7 @@ export function useCanvasNodeHandle() {
const isConnected = computed(() => handle?.isConnected.value ?? false);
const isConnecting = computed(() => handle?.isConnecting.value ?? false);
const isReadOnly = computed(() => handle?.isReadOnly.value);
const isRequired = computed(() => handle?.isRequired.value);
const type = computed(() => handle?.type.value ?? NodeConnectionType.Main);
const mode = computed(() => handle?.mode.value ?? CanvasConnectionMode.Input);
const index = computed(() => handle?.index.value ?? 0);
@ -25,6 +26,7 @@ export function useCanvasNodeHandle() {
isConnected,
isConnecting,
isReadOnly,
isRequired,
type,
mode,
index,

View file

@ -165,6 +165,7 @@ export interface CanvasNodeHandleInjectionData {
mode: Ref<CanvasConnectionMode>;
type: Ref<NodeConnectionType>;
index: Ref<number>;
isRequired: Ref<boolean | undefined>;
isConnected: Ref<boolean | undefined>;
isConnecting: Ref<boolean | undefined>;
isReadOnly: Ref<boolean | undefined>;