mirror of
https://github.com/n8n-io/n8n.git
synced 2024-12-31 15:37:26 -08:00
fix(editor): Add missing trigger waiting tooltip on new canvas (#11918)
This commit is contained in:
parent
f91fecfe0d
commit
a8df221bfb
|
@ -14,6 +14,7 @@ import { CanvasConnectionMode, CanvasNodeRenderType } from '@/types';
|
||||||
import { NodeConnectionType } from 'n8n-workflow';
|
import { NodeConnectionType } from 'n8n-workflow';
|
||||||
import type { EventBus } from 'n8n-design-system';
|
import type { EventBus } from 'n8n-design-system';
|
||||||
import { createEventBus } from 'n8n-design-system';
|
import { createEventBus } from 'n8n-design-system';
|
||||||
|
import type { ViewportTransform } from '@vue-flow/core';
|
||||||
|
|
||||||
export function createCanvasNodeData({
|
export function createCanvasNodeData({
|
||||||
id = 'node',
|
id = 'node',
|
||||||
|
@ -91,16 +92,22 @@ export function createCanvasNodeProps({
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createCanvasProvide({
|
export function createCanvasProvide({
|
||||||
|
initialized = true,
|
||||||
isExecuting = false,
|
isExecuting = false,
|
||||||
connectingHandle = undefined,
|
connectingHandle = undefined,
|
||||||
|
viewport = { x: 0, y: 0, zoom: 1 },
|
||||||
}: {
|
}: {
|
||||||
|
initialized?: boolean;
|
||||||
isExecuting?: boolean;
|
isExecuting?: boolean;
|
||||||
connectingHandle?: ConnectStartEvent;
|
connectingHandle?: ConnectStartEvent;
|
||||||
|
viewport?: ViewportTransform;
|
||||||
} = {}) {
|
} = {}) {
|
||||||
return {
|
return {
|
||||||
[String(CanvasKey)]: {
|
[String(CanvasKey)]: {
|
||||||
|
initialized: ref(initialized),
|
||||||
isExecuting: ref(isExecuting),
|
isExecuting: ref(isExecuting),
|
||||||
connectingHandle: ref(connectingHandle),
|
connectingHandle: ref(connectingHandle),
|
||||||
|
viewport: ref(viewport),
|
||||||
} satisfies CanvasInjectionData,
|
} satisfies CanvasInjectionData,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -80,6 +80,10 @@ export function createComponentRenderer(
|
||||||
global: {
|
global: {
|
||||||
...defaultOptions.global,
|
...defaultOptions.global,
|
||||||
...options.global,
|
...options.global,
|
||||||
|
provide: {
|
||||||
|
...defaultOptions.global?.provide,
|
||||||
|
...options.global?.provide,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
|
@ -6,13 +6,7 @@ import type {
|
||||||
CanvasEventBusEvents,
|
CanvasEventBusEvents,
|
||||||
ConnectStartEvent,
|
ConnectStartEvent,
|
||||||
} from '@/types';
|
} from '@/types';
|
||||||
import type {
|
import type { Connection, XYPosition, NodeDragEvent, GraphNode } from '@vue-flow/core';
|
||||||
Connection,
|
|
||||||
XYPosition,
|
|
||||||
ViewportTransform,
|
|
||||||
NodeDragEvent,
|
|
||||||
GraphNode,
|
|
||||||
} from '@vue-flow/core';
|
|
||||||
import { useVueFlow, VueFlow, PanelPosition, MarkerType } from '@vue-flow/core';
|
import { useVueFlow, VueFlow, PanelPosition, MarkerType } from '@vue-flow/core';
|
||||||
import { Background } from '@vue-flow/background';
|
import { Background } from '@vue-flow/background';
|
||||||
import { MiniMap } from '@vue-flow/minimap';
|
import { MiniMap } from '@vue-flow/minimap';
|
||||||
|
@ -114,6 +108,7 @@ const {
|
||||||
project,
|
project,
|
||||||
nodes: graphNodes,
|
nodes: graphNodes,
|
||||||
onPaneReady,
|
onPaneReady,
|
||||||
|
onNodesInitialized,
|
||||||
findNode,
|
findNode,
|
||||||
viewport,
|
viewport,
|
||||||
onEdgeMouseLeave,
|
onEdgeMouseLeave,
|
||||||
|
@ -431,7 +426,6 @@ function emitWithLastSelectedNode(emitFn: (id: string) => void) {
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const defaultZoom = 1;
|
const defaultZoom = 1;
|
||||||
const zoom = ref(defaultZoom);
|
|
||||||
const isPaneMoving = ref(false);
|
const isPaneMoving = ref(false);
|
||||||
|
|
||||||
function getProjectedPosition(event?: Pick<MouseEvent, 'clientX' | 'clientY'>) {
|
function getProjectedPosition(event?: Pick<MouseEvent, 'clientX' | 'clientY'>) {
|
||||||
|
@ -469,10 +463,6 @@ async function onResetZoom() {
|
||||||
await onZoomTo(defaultZoom);
|
await onZoomTo(defaultZoom);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onViewportChange(viewport: ViewportTransform) {
|
|
||||||
zoom.value = viewport.zoom;
|
|
||||||
}
|
|
||||||
|
|
||||||
function setReadonly(value: boolean) {
|
function setReadonly(value: boolean) {
|
||||||
setInteractive(!value);
|
setInteractive(!value);
|
||||||
elementsSelectable.value = true;
|
elementsSelectable.value = true;
|
||||||
|
@ -589,6 +579,8 @@ function onMinimapMouseLeave() {
|
||||||
* Lifecycle
|
* Lifecycle
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
const initialized = ref(false);
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
props.eventBus.on('fitView', onFitView);
|
props.eventBus.on('fitView', onFitView);
|
||||||
props.eventBus.on('nodes:select', onSelectNodes);
|
props.eventBus.on('nodes:select', onSelectNodes);
|
||||||
|
@ -604,6 +596,10 @@ onPaneReady(async () => {
|
||||||
isPaneReady.value = true;
|
isPaneReady.value = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
onNodesInitialized(() => {
|
||||||
|
initialized.value = true;
|
||||||
|
});
|
||||||
|
|
||||||
watch(() => props.readOnly, setReadonly, {
|
watch(() => props.readOnly, setReadonly, {
|
||||||
immediate: true,
|
immediate: true,
|
||||||
});
|
});
|
||||||
|
@ -617,6 +613,8 @@ const isExecuting = toRef(props, 'executing');
|
||||||
provide(CanvasKey, {
|
provide(CanvasKey, {
|
||||||
connectingHandle,
|
connectingHandle,
|
||||||
isExecuting,
|
isExecuting,
|
||||||
|
initialized,
|
||||||
|
viewport,
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -644,7 +642,6 @@ provide(CanvasKey, {
|
||||||
@connect-end="onConnectEnd"
|
@connect-end="onConnectEnd"
|
||||||
@pane-click="onClickPane"
|
@pane-click="onClickPane"
|
||||||
@contextmenu="onOpenContextMenu"
|
@contextmenu="onOpenContextMenu"
|
||||||
@viewport-change="onViewportChange"
|
|
||||||
@move-start="onPaneMoveStart"
|
@move-start="onPaneMoveStart"
|
||||||
@move-end="onPaneMoveEnd"
|
@move-end="onPaneMoveEnd"
|
||||||
@node-drag-stop="onNodeDragStop"
|
@node-drag-stop="onNodeDragStop"
|
||||||
|
@ -717,7 +714,7 @@ provide(CanvasKey, {
|
||||||
:position="controlsPosition"
|
:position="controlsPosition"
|
||||||
:show-interactive="false"
|
:show-interactive="false"
|
||||||
:show-bug-reporting-button="showBugReportingButton"
|
:show-bug-reporting-button="showBugReportingButton"
|
||||||
:zoom="zoom"
|
:zoom="viewport.zoom"
|
||||||
@zoom-to-fit="onFitView"
|
@zoom-to-fit="onFitView"
|
||||||
@zoom-in="onZoomIn"
|
@zoom-in="onZoomIn"
|
||||||
@zoom-out="onZoomOut"
|
@zoom-out="onZoomOut"
|
||||||
|
|
|
@ -4,6 +4,7 @@ import { createTestingPinia } from '@pinia/testing';
|
||||||
import { setActivePinia } from 'pinia';
|
import { setActivePinia } from 'pinia';
|
||||||
import type { ConnectionLineProps } from '@vue-flow/core';
|
import type { ConnectionLineProps } from '@vue-flow/core';
|
||||||
import { Position } from '@vue-flow/core';
|
import { Position } from '@vue-flow/core';
|
||||||
|
import { createCanvasProvide } from '@/__tests__/data';
|
||||||
|
|
||||||
const DEFAULT_PROPS = {
|
const DEFAULT_PROPS = {
|
||||||
sourceX: 0,
|
sourceX: 0,
|
||||||
|
@ -15,6 +16,11 @@ const DEFAULT_PROPS = {
|
||||||
} satisfies Partial<ConnectionLineProps>;
|
} satisfies Partial<ConnectionLineProps>;
|
||||||
const renderComponent = createComponentRenderer(CanvasConnectionLine, {
|
const renderComponent = createComponentRenderer(CanvasConnectionLine, {
|
||||||
props: DEFAULT_PROPS,
|
props: DEFAULT_PROPS,
|
||||||
|
global: {
|
||||||
|
provide: {
|
||||||
|
...createCanvasProvide(),
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { createComponentRenderer } from '@/__tests__/render';
|
||||||
import { createPinia, setActivePinia } from 'pinia';
|
import { createPinia, setActivePinia } from 'pinia';
|
||||||
import { NodeConnectionType } from 'n8n-workflow';
|
import { NodeConnectionType } from 'n8n-workflow';
|
||||||
import { fireEvent } from '@testing-library/vue';
|
import { fireEvent } from '@testing-library/vue';
|
||||||
import { createCanvasNodeProps } from '@/__tests__/data';
|
import { createCanvasNodeProps, createCanvasProvide } from '@/__tests__/data';
|
||||||
|
|
||||||
vi.mock('@/stores/nodeTypes.store', () => ({
|
vi.mock('@/stores/nodeTypes.store', () => ({
|
||||||
useNodeTypesStore: vi.fn(() => ({
|
useNodeTypesStore: vi.fn(() => ({
|
||||||
|
@ -19,7 +19,14 @@ beforeEach(() => {
|
||||||
const pinia = createPinia();
|
const pinia = createPinia();
|
||||||
setActivePinia(pinia);
|
setActivePinia(pinia);
|
||||||
|
|
||||||
renderComponent = createComponentRenderer(CanvasNode, { pinia });
|
renderComponent = createComponentRenderer(CanvasNode, {
|
||||||
|
pinia,
|
||||||
|
global: {
|
||||||
|
provide: {
|
||||||
|
...createCanvasProvide(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('CanvasNode', () => {
|
describe('CanvasNode', () => {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import CanvasNodeRenderer from '@/components/canvas/elements/nodes/CanvasNodeRenderer.vue';
|
import CanvasNodeRenderer from '@/components/canvas/elements/nodes/CanvasNodeRenderer.vue';
|
||||||
import { createComponentRenderer } from '@/__tests__/render';
|
import { createComponentRenderer } from '@/__tests__/render';
|
||||||
import { createCanvasNodeProvide } from '@/__tests__/data';
|
import { createCanvasNodeProvide, createCanvasProvide } from '@/__tests__/data';
|
||||||
import { createTestingPinia } from '@pinia/testing';
|
import { createTestingPinia } from '@pinia/testing';
|
||||||
import { setActivePinia } from 'pinia';
|
import { setActivePinia } from 'pinia';
|
||||||
import { CanvasNodeRenderType } from '@/types';
|
import { CanvasNodeRenderType } from '@/types';
|
||||||
|
@ -17,6 +17,7 @@ describe('CanvasNodeRenderer', () => {
|
||||||
const { getByTestId } = renderComponent({
|
const { getByTestId } = renderComponent({
|
||||||
global: {
|
global: {
|
||||||
provide: {
|
provide: {
|
||||||
|
...createCanvasProvide(),
|
||||||
...createCanvasNodeProvide(),
|
...createCanvasNodeProvide(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -29,6 +30,7 @@ describe('CanvasNodeRenderer', () => {
|
||||||
const { getByTestId } = renderComponent({
|
const { getByTestId } = renderComponent({
|
||||||
global: {
|
global: {
|
||||||
provide: {
|
provide: {
|
||||||
|
...createCanvasProvide(),
|
||||||
...createCanvasNodeProvide({
|
...createCanvasNodeProvide({
|
||||||
data: {
|
data: {
|
||||||
render: {
|
render: {
|
||||||
|
@ -48,6 +50,7 @@ describe('CanvasNodeRenderer', () => {
|
||||||
const { getByTestId } = renderComponent({
|
const { getByTestId } = renderComponent({
|
||||||
global: {
|
global: {
|
||||||
provide: {
|
provide: {
|
||||||
|
...createCanvasProvide(),
|
||||||
...createCanvasNodeProvide({
|
...createCanvasNodeProvide({
|
||||||
data: {
|
data: {
|
||||||
render: {
|
render: {
|
||||||
|
|
|
@ -1,12 +1,18 @@
|
||||||
import CanvasNodeDefault from '@/components/canvas/elements/nodes/render-types/CanvasNodeDefault.vue';
|
import CanvasNodeDefault from '@/components/canvas/elements/nodes/render-types/CanvasNodeDefault.vue';
|
||||||
import { createComponentRenderer } from '@/__tests__/render';
|
import { createComponentRenderer } from '@/__tests__/render';
|
||||||
import { NodeConnectionType } from 'n8n-workflow';
|
import { NodeConnectionType } from 'n8n-workflow';
|
||||||
import { createCanvasNodeProvide } from '@/__tests__/data';
|
import { createCanvasNodeProvide, createCanvasProvide } from '@/__tests__/data';
|
||||||
import { createTestingPinia } from '@pinia/testing';
|
import { createTestingPinia } from '@pinia/testing';
|
||||||
import { setActivePinia } from 'pinia';
|
import { setActivePinia } from 'pinia';
|
||||||
import { CanvasConnectionMode, CanvasNodeRenderType } from '@/types';
|
import { CanvasConnectionMode, CanvasNodeRenderType } from '@/types';
|
||||||
|
|
||||||
const renderComponent = createComponentRenderer(CanvasNodeDefault);
|
const renderComponent = createComponentRenderer(CanvasNodeDefault, {
|
||||||
|
global: {
|
||||||
|
provide: {
|
||||||
|
...createCanvasProvide(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
const pinia = createTestingPinia();
|
const pinia = createTestingPinia();
|
||||||
|
|
|
@ -1,13 +1,11 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, useCssModule } from 'vue';
|
import { computed, ref, useCssModule, watch } from 'vue';
|
||||||
import { useNodeConnections } from '@/composables/useNodeConnections';
|
import { useNodeConnections } from '@/composables/useNodeConnections';
|
||||||
import { useI18n } from '@/composables/useI18n';
|
import { useI18n } from '@/composables/useI18n';
|
||||||
import CanvasNodeDisabledStrikeThrough from './parts/CanvasNodeDisabledStrikeThrough.vue';
|
|
||||||
import CanvasNodeStatusIcons from '@/components/canvas/elements/nodes/render-types/parts/CanvasNodeStatusIcons.vue';
|
|
||||||
import { useCanvasNode } from '@/composables/useCanvasNode';
|
import { useCanvasNode } from '@/composables/useCanvasNode';
|
||||||
import { NODE_INSERT_SPACER_BETWEEN_INPUT_GROUPS } from '@/constants';
|
import { NODE_INSERT_SPACER_BETWEEN_INPUT_GROUPS } from '@/constants';
|
||||||
import { N8nTooltip } from 'n8n-design-system';
|
|
||||||
import type { CanvasNodeDefaultRender } from '@/types';
|
import type { CanvasNodeDefaultRender } from '@/types';
|
||||||
|
import { useCanvas } from '@/composables/useCanvas';
|
||||||
|
|
||||||
const $style = useCssModule();
|
const $style = useCssModule();
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
|
@ -16,6 +14,7 @@ const emit = defineEmits<{
|
||||||
'open:contextmenu': [event: MouseEvent];
|
'open:contextmenu': [event: MouseEvent];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
const { initialized, viewport } = useCanvas();
|
||||||
const {
|
const {
|
||||||
label,
|
label,
|
||||||
subtitle,
|
subtitle,
|
||||||
|
@ -105,6 +104,21 @@ const isStrikethroughVisible = computed(() => {
|
||||||
return isDisabled.value && isSingleMainInputNode && isSingleMainOutputNode;
|
return isDisabled.value && isSingleMainInputNode && isSingleMainOutputNode;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const showTooltip = ref(false);
|
||||||
|
|
||||||
|
watch(initialized, () => {
|
||||||
|
if (initialized.value) {
|
||||||
|
showTooltip.value = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(viewport, () => {
|
||||||
|
showTooltip.value = false;
|
||||||
|
setTimeout(() => {
|
||||||
|
showTooltip.value = true;
|
||||||
|
}, 0);
|
||||||
|
});
|
||||||
|
|
||||||
function openContextMenu(event: MouseEvent) {
|
function openContextMenu(event: MouseEvent) {
|
||||||
emit('open:contextmenu', event);
|
emit('open:contextmenu', event);
|
||||||
}
|
}
|
||||||
|
@ -112,15 +126,9 @@ function openContextMenu(event: MouseEvent) {
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div :class="classes" :style="styles" :data-test-id="dataTestId" @contextmenu="openContextMenu">
|
<div :class="classes" :style="styles" :data-test-id="dataTestId" @contextmenu="openContextMenu">
|
||||||
|
<CanvasNodeTooltip v-if="renderOptions.tooltip" :visible="showTooltip" />
|
||||||
<slot />
|
<slot />
|
||||||
<N8nTooltip v-if="renderOptions.trigger" placement="bottom">
|
<CanvasNodeTriggerIcon v-if="renderOptions.trigger" />
|
||||||
<template #content>
|
|
||||||
<span v-n8n-html="i18n.baseText('node.thisIsATriggerNode')" />
|
|
||||||
</template>
|
|
||||||
<div :class="$style.triggerIcon">
|
|
||||||
<FontAwesomeIcon icon="bolt" size="lg" />
|
|
||||||
</div>
|
|
||||||
</N8nTooltip>
|
|
||||||
<CanvasNodeStatusIcons v-if="!isDisabled" :class="$style.statusIcons" />
|
<CanvasNodeStatusIcons v-if="!isDisabled" :class="$style.statusIcons" />
|
||||||
<CanvasNodeDisabledStrikeThrough v-if="isStrikethroughVisible" />
|
<CanvasNodeDisabledStrikeThrough v-if="isStrikethroughVisible" />
|
||||||
<div :class="$style.description">
|
<div :class="$style.description">
|
||||||
|
@ -152,6 +160,7 @@ function openContextMenu(event: MouseEvent) {
|
||||||
--trigger-node--border-radius: 36px;
|
--trigger-node--border-radius: 36px;
|
||||||
--canvas-node--status-icons-offset: var(--spacing-2xs);
|
--canvas-node--status-icons-offset: var(--spacing-2xs);
|
||||||
|
|
||||||
|
position: relative;
|
||||||
height: var(--canvas-node--height);
|
height: var(--canvas-node--height);
|
||||||
width: var(--canvas-node--width);
|
width: var(--canvas-node--width);
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -298,12 +307,4 @@ function openContextMenu(event: MouseEvent) {
|
||||||
bottom: var(--canvas-node--status-icons-offset);
|
bottom: var(--canvas-node--status-icons-offset);
|
||||||
right: var(--canvas-node--status-icons-offset);
|
right: var(--canvas-node--status-icons-offset);
|
||||||
}
|
}
|
||||||
|
|
||||||
.triggerIcon {
|
|
||||||
position: absolute;
|
|
||||||
right: 100%;
|
|
||||||
margin: auto;
|
|
||||||
color: var(--color-primary);
|
|
||||||
padding: var(--spacing-2xs);
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -6,6 +6,7 @@ exports[`CanvasNodeDefault > configurable > should render configurable node corr
|
||||||
data-test-id="canvas-configurable-node"
|
data-test-id="canvas-configurable-node"
|
||||||
style="--configurable-node--input-count: 0; --canvas-node--main-input-count: 0; --canvas-node--main-output-count: 0;"
|
style="--configurable-node--input-count: 0; --canvas-node--main-input-count: 0; --canvas-node--main-output-count: 0;"
|
||||||
>
|
>
|
||||||
|
<!--v-if-->
|
||||||
|
|
||||||
|
|
||||||
<!--v-if-->
|
<!--v-if-->
|
||||||
|
@ -35,6 +36,7 @@ exports[`CanvasNodeDefault > configuration > should render configurable configur
|
||||||
data-test-id="canvas-configurable-node"
|
data-test-id="canvas-configurable-node"
|
||||||
style="--configurable-node--input-count: 0; --canvas-node--main-input-count: 0; --canvas-node--main-output-count: 0;"
|
style="--configurable-node--input-count: 0; --canvas-node--main-input-count: 0; --canvas-node--main-output-count: 0;"
|
||||||
>
|
>
|
||||||
|
<!--v-if-->
|
||||||
|
|
||||||
|
|
||||||
<!--v-if-->
|
<!--v-if-->
|
||||||
|
@ -64,6 +66,7 @@ exports[`CanvasNodeDefault > configuration > should render configuration node co
|
||||||
data-test-id="canvas-configuration-node"
|
data-test-id="canvas-configuration-node"
|
||||||
style="--canvas-node--main-input-count: 0; --canvas-node--main-output-count: 0;"
|
style="--canvas-node--main-input-count: 0; --canvas-node--main-output-count: 0;"
|
||||||
>
|
>
|
||||||
|
<!--v-if-->
|
||||||
|
|
||||||
|
|
||||||
<!--v-if-->
|
<!--v-if-->
|
||||||
|
@ -93,6 +96,7 @@ exports[`CanvasNodeDefault > should render node correctly 1`] = `
|
||||||
data-test-id="canvas-default-node"
|
data-test-id="canvas-default-node"
|
||||||
style="--canvas-node--main-input-count: 0; --canvas-node--main-output-count: 0;"
|
style="--canvas-node--main-input-count: 0; --canvas-node--main-output-count: 0;"
|
||||||
>
|
>
|
||||||
|
<!--v-if-->
|
||||||
|
|
||||||
|
|
||||||
<!--v-if-->
|
<!--v-if-->
|
||||||
|
@ -122,6 +126,7 @@ exports[`CanvasNodeDefault > trigger > should render trigger node correctly 1`]
|
||||||
data-test-id="canvas-trigger-node"
|
data-test-id="canvas-trigger-node"
|
||||||
style="--canvas-node--main-input-count: 0; --canvas-node--main-output-count: 0;"
|
style="--canvas-node--main-input-count: 0; --canvas-node--main-output-count: 0;"
|
||||||
>
|
>
|
||||||
|
<!--v-if-->
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,54 @@
|
||||||
|
import CanvasNodeTooltip from './CanvasNodeTooltip.vue';
|
||||||
|
import { createComponentRenderer } from '@/__tests__/render';
|
||||||
|
import type { CanvasNodeDefaultRender } from '@/types';
|
||||||
|
import { createCanvasNodeProvide } from '@/__tests__/data';
|
||||||
|
import { waitFor } from '@testing-library/vue';
|
||||||
|
|
||||||
|
const renderComponent = createComponentRenderer(CanvasNodeTooltip);
|
||||||
|
|
||||||
|
describe('CanvasNodeTooltip', () => {
|
||||||
|
describe('rendering', () => {
|
||||||
|
it('should render tooltip when tooltip option is provided', async () => {
|
||||||
|
const { container, getByText } = renderComponent({
|
||||||
|
props: {
|
||||||
|
visible: true,
|
||||||
|
},
|
||||||
|
global: {
|
||||||
|
provide: createCanvasNodeProvide({
|
||||||
|
data: {
|
||||||
|
render: {
|
||||||
|
options: {
|
||||||
|
tooltip: 'Test tooltip text',
|
||||||
|
},
|
||||||
|
} as CanvasNodeDefaultRender,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(getByText('Test tooltip text')).toBeInTheDocument();
|
||||||
|
await waitFor(() => expect(container.querySelector('.el-popper')).toBeVisible());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not render tooltip when tooltip option is not provided', () => {
|
||||||
|
const { container } = renderComponent({
|
||||||
|
props: {
|
||||||
|
visible: false,
|
||||||
|
},
|
||||||
|
global: {
|
||||||
|
provide: createCanvasNodeProvide({
|
||||||
|
data: {
|
||||||
|
render: {
|
||||||
|
options: {
|
||||||
|
tooltip: 'Test tooltip text',
|
||||||
|
},
|
||||||
|
} as CanvasNodeDefaultRender,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(container.querySelector('.el-popper')).not.toBeVisible();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,42 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { useCanvasNode } from '@/composables/useCanvasNode';
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import type { CanvasNodeDefaultRender } from '@/types';
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
visible: boolean;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const { render } = useCanvasNode();
|
||||||
|
|
||||||
|
const renderOptions = computed(() => render.value.options as CanvasNodeDefaultRender['options']);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<N8nTooltip
|
||||||
|
placement="top"
|
||||||
|
:show-after="500"
|
||||||
|
:visible="true"
|
||||||
|
:teleported="false"
|
||||||
|
:popper-class="$style.popper"
|
||||||
|
>
|
||||||
|
<template #content>
|
||||||
|
{{ renderOptions.tooltip }}
|
||||||
|
</template>
|
||||||
|
<div :class="$style.tooltipTrigger" />
|
||||||
|
</N8nTooltip>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" module>
|
||||||
|
.tooltipTrigger {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popper {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,35 @@
|
||||||
|
import CanvasNodeTriggerIcon from './CanvasNodeTriggerIcon.vue';
|
||||||
|
import { createComponentRenderer } from '@/__tests__/render';
|
||||||
|
|
||||||
|
vi.mock('@/composables/useI18n', () => ({
|
||||||
|
useI18n: vi.fn(() => ({
|
||||||
|
baseText: vi.fn().mockReturnValue('This is a trigger node'),
|
||||||
|
})),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const renderComponent = createComponentRenderer(CanvasNodeTriggerIcon, {
|
||||||
|
global: {
|
||||||
|
stubs: {
|
||||||
|
FontAwesomeIcon: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('CanvasNodeTriggerIcon', () => {
|
||||||
|
it('should render trigger icon with tooltip', () => {
|
||||||
|
const { container } = renderComponent();
|
||||||
|
|
||||||
|
expect(container.querySelector('.triggerIcon')).toBeInTheDocument();
|
||||||
|
|
||||||
|
const icon = container.querySelector('font-awesome-icon-stub');
|
||||||
|
expect(icon).toBeInTheDocument();
|
||||||
|
expect(icon?.getAttribute('icon')).toBe('bolt');
|
||||||
|
expect(icon?.getAttribute('size')).toBe('lg');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render tooltip with correct content', () => {
|
||||||
|
const { getByText } = renderComponent();
|
||||||
|
|
||||||
|
expect(getByText('This is a trigger node')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,26 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { useI18n } from '@/composables/useI18n';
|
||||||
|
|
||||||
|
const i18n = useI18n();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<N8nTooltip placement="bottom">
|
||||||
|
<template #content>
|
||||||
|
<span v-n8n-html="i18n.baseText('node.thisIsATriggerNode')" />
|
||||||
|
</template>
|
||||||
|
<div :class="$style.triggerIcon">
|
||||||
|
<FontAwesomeIcon icon="bolt" size="lg" />
|
||||||
|
</div>
|
||||||
|
</N8nTooltip>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" module>
|
||||||
|
.triggerIcon {
|
||||||
|
position: absolute;
|
||||||
|
right: 100%;
|
||||||
|
margin: auto;
|
||||||
|
color: var(--color-primary);
|
||||||
|
padding: var(--spacing-2xs);
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,14 +1,6 @@
|
||||||
import { computed, inject } from 'vue';
|
|
||||||
import { CanvasKey } from '@/constants';
|
import { CanvasKey } from '@/constants';
|
||||||
|
import { injectStrict } from '@/utils/injectStrict';
|
||||||
|
|
||||||
export function useCanvas() {
|
export function useCanvas() {
|
||||||
const canvas = inject(CanvasKey);
|
return injectStrict(CanvasKey);
|
||||||
|
|
||||||
const connectingHandle = computed(() => canvas?.connectingHandle.value);
|
|
||||||
const isExecuting = computed(() => canvas?.isExecuting.value);
|
|
||||||
|
|
||||||
return {
|
|
||||||
isExecuting,
|
|
||||||
connectingHandle,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,6 +33,7 @@ import type {
|
||||||
ExecutionSummary,
|
ExecutionSummary,
|
||||||
IConnections,
|
IConnections,
|
||||||
INodeExecutionData,
|
INodeExecutionData,
|
||||||
|
INodeTypeDescription,
|
||||||
ITaskData,
|
ITaskData,
|
||||||
Workflow,
|
Workflow,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
|
@ -48,6 +49,7 @@ import {
|
||||||
import { sanitizeHtml } from '@/utils/htmlUtils';
|
import { sanitizeHtml } from '@/utils/htmlUtils';
|
||||||
import { MarkerType } from '@vue-flow/core';
|
import { MarkerType } from '@vue-flow/core';
|
||||||
import { useNodeHelpers } from './useNodeHelpers';
|
import { useNodeHelpers } from './useNodeHelpers';
|
||||||
|
import { getTriggerNodeServiceName } from '@/utils/nodeTypesUtils';
|
||||||
|
|
||||||
export function useCanvasMapping({
|
export function useCanvasMapping({
|
||||||
nodes,
|
nodes,
|
||||||
|
@ -86,7 +88,7 @@ export function useCanvasMapping({
|
||||||
return {
|
return {
|
||||||
type: CanvasNodeRenderType.Default,
|
type: CanvasNodeRenderType.Default,
|
||||||
options: {
|
options: {
|
||||||
trigger: nodeTypesStore.isTriggerNode(node.type),
|
trigger: isTriggerNodeById.value[node.id],
|
||||||
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: {
|
inputs: {
|
||||||
|
@ -95,6 +97,7 @@ export function useCanvasMapping({
|
||||||
outputs: {
|
outputs: {
|
||||||
labelSize: nodeOutputLabelSizeById.value[node.id],
|
labelSize: nodeOutputLabelSizeById.value[node.id],
|
||||||
},
|
},
|
||||||
|
tooltip: nodeTooltipById.value[node.id],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -117,10 +120,34 @@ export function useCanvasMapping({
|
||||||
}, {}) ?? {},
|
}, {}) ?? {},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const nodeTypeDescriptionByNodeId = computed(() =>
|
||||||
|
nodes.value.reduce<Record<string, INodeTypeDescription | null>>((acc, node) => {
|
||||||
|
acc[node.id] = nodeTypesStore.getNodeType(node.type, node.typeVersion);
|
||||||
|
return acc;
|
||||||
|
}, {}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const isTriggerNodeById = computed(() =>
|
||||||
|
nodes.value.reduce<Record<string, boolean>>((acc, node) => {
|
||||||
|
acc[node.id] = nodeTypesStore.isTriggerNode(node.type);
|
||||||
|
return acc;
|
||||||
|
}, {}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const activeTriggerNodeCount = computed(
|
||||||
|
() =>
|
||||||
|
nodes.value.filter(
|
||||||
|
(node) =>
|
||||||
|
nodeTypeDescriptionByNodeId.value[node.id]?.eventTriggerDescription !== '' &&
|
||||||
|
isTriggerNodeById.value[node.id] &&
|
||||||
|
!node.disabled,
|
||||||
|
).length,
|
||||||
|
);
|
||||||
|
|
||||||
const nodeSubtitleById = computed(() => {
|
const nodeSubtitleById = computed(() => {
|
||||||
return nodes.value.reduce<Record<string, string>>((acc, node) => {
|
return nodes.value.reduce<Record<string, string>>((acc, node) => {
|
||||||
try {
|
try {
|
||||||
const nodeTypeDescription = nodeTypesStore.getNodeType(node.type, node.typeVersion);
|
const nodeTypeDescription = nodeTypeDescriptionByNodeId.value[node.id];
|
||||||
if (!nodeTypeDescription) {
|
if (!nodeTypeDescription) {
|
||||||
return acc;
|
return acc;
|
||||||
}
|
}
|
||||||
|
@ -140,7 +167,7 @@ export function useCanvasMapping({
|
||||||
|
|
||||||
const nodeInputsById = computed(() =>
|
const nodeInputsById = 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 = nodeTypeDescriptionByNodeId.value[node.id];
|
||||||
const workflowObjectNode = workflowObject.value.getNode(node.name);
|
const workflowObjectNode = workflowObject.value.getNode(node.name);
|
||||||
|
|
||||||
acc[node.id] =
|
acc[node.id] =
|
||||||
|
@ -203,7 +230,7 @@ export function useCanvasMapping({
|
||||||
|
|
||||||
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 = nodeTypeDescriptionByNodeId.value[node.id];
|
||||||
const workflowObjectNode = workflowObject.value.getNode(node.name);
|
const workflowObjectNode = workflowObject.value.getNode(node.name);
|
||||||
|
|
||||||
acc[node.id] =
|
acc[node.id] =
|
||||||
|
@ -229,6 +256,37 @@ export function useCanvasMapping({
|
||||||
}, {}),
|
}, {}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const nodeTooltipById = computed(() =>
|
||||||
|
nodes.value.reduce<Record<string, string | undefined>>((acc, node) => {
|
||||||
|
const nodeTypeDescription = nodeTypeDescriptionByNodeId.value[node.id];
|
||||||
|
if (nodeTypeDescription && isTriggerNodeById.value[node.id]) {
|
||||||
|
if (
|
||||||
|
activeTriggerNodeCount.value !== 1 ||
|
||||||
|
!workflowsStore.isWorkflowRunning ||
|
||||||
|
!['new', 'unknown', 'waiting'].includes(nodeExecutionStatusById.value[node.id])
|
||||||
|
) {
|
||||||
|
return acc;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('eventTriggerDescription' in nodeTypeDescription) {
|
||||||
|
const nodeName = i18n.shortNodeType(nodeTypeDescription.name);
|
||||||
|
const { eventTriggerDescription } = nodeTypeDescription;
|
||||||
|
acc[node.id] = i18n
|
||||||
|
.nodeText()
|
||||||
|
.eventTriggerDescription(nodeName, eventTriggerDescription ?? '');
|
||||||
|
} else {
|
||||||
|
acc[node.id] = i18n.baseText('node.waitingForYouToCreateAnEventIn', {
|
||||||
|
interpolate: {
|
||||||
|
nodeType: nodeTypeDescription ? getTriggerNodeServiceName(nodeTypeDescription) : '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
}, {}),
|
||||||
|
);
|
||||||
|
|
||||||
const nodeExecutionRunningById = computed(() =>
|
const nodeExecutionRunningById = computed(() =>
|
||||||
nodes.value.reduce<Record<string, boolean>>((acc, node) => {
|
nodes.value.reduce<Record<string, boolean>>((acc, node) => {
|
||||||
acc[node.id] = workflowsStore.isNodeExecuting(node.name);
|
acc[node.id] = workflowsStore.isNodeExecuting(node.name);
|
||||||
|
|
|
@ -5,7 +5,14 @@ import type {
|
||||||
IConnection,
|
IConnection,
|
||||||
NodeConnectionType,
|
NodeConnectionType,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
import type { DefaultEdge, Node, NodeProps, Position, OnConnectStartParams } from '@vue-flow/core';
|
import type {
|
||||||
|
DefaultEdge,
|
||||||
|
Node,
|
||||||
|
NodeProps,
|
||||||
|
Position,
|
||||||
|
OnConnectStartParams,
|
||||||
|
ViewportTransform,
|
||||||
|
} from '@vue-flow/core';
|
||||||
import type { IExecutionResponse, INodeUi } from '@/Interface';
|
import type { IExecutionResponse, INodeUi } from '@/Interface';
|
||||||
import type { ComputedRef, Ref } from 'vue';
|
import type { ComputedRef, Ref } from 'vue';
|
||||||
import type { PartialBy } from '@/utils/typeHelpers';
|
import type { PartialBy } from '@/utils/typeHelpers';
|
||||||
|
@ -59,6 +66,7 @@ export type CanvasNodeDefaultRender = {
|
||||||
outputs: {
|
outputs: {
|
||||||
labelSize: CanvasNodeDefaultRenderLabelSize;
|
labelSize: CanvasNodeDefaultRenderLabelSize;
|
||||||
};
|
};
|
||||||
|
tooltip?: string;
|
||||||
}>;
|
}>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -135,8 +143,10 @@ export type CanvasConnectionCreateData = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface CanvasInjectionData {
|
export interface CanvasInjectionData {
|
||||||
|
initialized: Ref<boolean>;
|
||||||
isExecuting: Ref<boolean | undefined>;
|
isExecuting: Ref<boolean | undefined>;
|
||||||
connectingHandle: Ref<ConnectStartEvent | undefined>;
|
connectingHandle: Ref<ConnectStartEvent | undefined>;
|
||||||
|
viewport: Ref<ViewportTransform>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CanvasNodeEventBusEvents = {
|
export type CanvasNodeEventBusEvents = {
|
||||||
|
|
35
packages/editor-ui/src/utils/injectStrict.test.ts
Normal file
35
packages/editor-ui/src/utils/injectStrict.test.ts
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
import { injectStrict } from '@/utils/injectStrict';
|
||||||
|
import type { InjectionKey } from 'vue';
|
||||||
|
import { inject } from 'vue';
|
||||||
|
|
||||||
|
vi.mock('vue', async () => {
|
||||||
|
const original = await vi.importActual('vue');
|
||||||
|
return {
|
||||||
|
...original,
|
||||||
|
inject: vi.fn(),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('injectStrict', () => {
|
||||||
|
it('should return the injected value when it exists', () => {
|
||||||
|
const key = Symbol('testKey') as InjectionKey<string>;
|
||||||
|
const value = 'testValue';
|
||||||
|
vi.mocked(inject).mockReturnValueOnce(value);
|
||||||
|
const result = injectStrict(key);
|
||||||
|
expect(result).toBe(value);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the fallback value when the injected value does not exist', () => {
|
||||||
|
const key = Symbol('testKey') as InjectionKey<string>;
|
||||||
|
const fallback = 'fallbackValue';
|
||||||
|
vi.mocked(inject).mockReturnValueOnce(fallback);
|
||||||
|
const result = injectStrict(key, fallback);
|
||||||
|
expect(result).toBe(fallback);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw an error when the injected value does not exist and no fallback is provided', () => {
|
||||||
|
const key = Symbol('testKey') as InjectionKey<string>;
|
||||||
|
vi.mocked(inject).mockReturnValueOnce(undefined);
|
||||||
|
expect(() => injectStrict(key)).toThrowError(`Could not resolve ${key.description}`);
|
||||||
|
});
|
||||||
|
});
|
10
packages/editor-ui/src/utils/injectStrict.ts
Normal file
10
packages/editor-ui/src/utils/injectStrict.ts
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
import type { InjectionKey } from 'vue';
|
||||||
|
import { inject } from 'vue';
|
||||||
|
|
||||||
|
export function injectStrict<T>(key: InjectionKey<T>, fallback?: T) {
|
||||||
|
const resolved = inject(key, fallback);
|
||||||
|
if (!resolved) {
|
||||||
|
throw new Error(`Could not resolve ${key.description}`);
|
||||||
|
}
|
||||||
|
return resolved;
|
||||||
|
}
|
Loading…
Reference in a new issue