mirror of
https://github.com/n8n-io/n8n.git
synced 2024-12-28 22:19:41 -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 type { EventBus } from 'n8n-design-system';
|
||||
import { createEventBus } from 'n8n-design-system';
|
||||
import type { ViewportTransform } from '@vue-flow/core';
|
||||
|
||||
export function createCanvasNodeData({
|
||||
id = 'node',
|
||||
|
@ -91,16 +92,22 @@ export function createCanvasNodeProps({
|
|||
}
|
||||
|
||||
export function createCanvasProvide({
|
||||
initialized = true,
|
||||
isExecuting = false,
|
||||
connectingHandle = undefined,
|
||||
viewport = { x: 0, y: 0, zoom: 1 },
|
||||
}: {
|
||||
initialized?: boolean;
|
||||
isExecuting?: boolean;
|
||||
connectingHandle?: ConnectStartEvent;
|
||||
viewport?: ViewportTransform;
|
||||
} = {}) {
|
||||
return {
|
||||
[String(CanvasKey)]: {
|
||||
initialized: ref(initialized),
|
||||
isExecuting: ref(isExecuting),
|
||||
connectingHandle: ref(connectingHandle),
|
||||
viewport: ref(viewport),
|
||||
} satisfies CanvasInjectionData,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -80,6 +80,10 @@ export function createComponentRenderer(
|
|||
global: {
|
||||
...defaultOptions.global,
|
||||
...options.global,
|
||||
provide: {
|
||||
...defaultOptions.global?.provide,
|
||||
...options.global?.provide,
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
|
|
@ -6,13 +6,7 @@ import type {
|
|||
CanvasEventBusEvents,
|
||||
ConnectStartEvent,
|
||||
} from '@/types';
|
||||
import type {
|
||||
Connection,
|
||||
XYPosition,
|
||||
ViewportTransform,
|
||||
NodeDragEvent,
|
||||
GraphNode,
|
||||
} from '@vue-flow/core';
|
||||
import type { Connection, XYPosition, NodeDragEvent, GraphNode } from '@vue-flow/core';
|
||||
import { useVueFlow, VueFlow, PanelPosition, MarkerType } from '@vue-flow/core';
|
||||
import { Background } from '@vue-flow/background';
|
||||
import { MiniMap } from '@vue-flow/minimap';
|
||||
|
@ -114,6 +108,7 @@ const {
|
|||
project,
|
||||
nodes: graphNodes,
|
||||
onPaneReady,
|
||||
onNodesInitialized,
|
||||
findNode,
|
||||
viewport,
|
||||
onEdgeMouseLeave,
|
||||
|
@ -431,7 +426,6 @@ function emitWithLastSelectedNode(emitFn: (id: string) => void) {
|
|||
*/
|
||||
|
||||
const defaultZoom = 1;
|
||||
const zoom = ref(defaultZoom);
|
||||
const isPaneMoving = ref(false);
|
||||
|
||||
function getProjectedPosition(event?: Pick<MouseEvent, 'clientX' | 'clientY'>) {
|
||||
|
@ -469,10 +463,6 @@ async function onResetZoom() {
|
|||
await onZoomTo(defaultZoom);
|
||||
}
|
||||
|
||||
function onViewportChange(viewport: ViewportTransform) {
|
||||
zoom.value = viewport.zoom;
|
||||
}
|
||||
|
||||
function setReadonly(value: boolean) {
|
||||
setInteractive(!value);
|
||||
elementsSelectable.value = true;
|
||||
|
@ -589,6 +579,8 @@ function onMinimapMouseLeave() {
|
|||
* Lifecycle
|
||||
*/
|
||||
|
||||
const initialized = ref(false);
|
||||
|
||||
onMounted(() => {
|
||||
props.eventBus.on('fitView', onFitView);
|
||||
props.eventBus.on('nodes:select', onSelectNodes);
|
||||
|
@ -604,6 +596,10 @@ onPaneReady(async () => {
|
|||
isPaneReady.value = true;
|
||||
});
|
||||
|
||||
onNodesInitialized(() => {
|
||||
initialized.value = true;
|
||||
});
|
||||
|
||||
watch(() => props.readOnly, setReadonly, {
|
||||
immediate: true,
|
||||
});
|
||||
|
@ -617,6 +613,8 @@ const isExecuting = toRef(props, 'executing');
|
|||
provide(CanvasKey, {
|
||||
connectingHandle,
|
||||
isExecuting,
|
||||
initialized,
|
||||
viewport,
|
||||
});
|
||||
</script>
|
||||
|
||||
|
@ -644,7 +642,6 @@ provide(CanvasKey, {
|
|||
@connect-end="onConnectEnd"
|
||||
@pane-click="onClickPane"
|
||||
@contextmenu="onOpenContextMenu"
|
||||
@viewport-change="onViewportChange"
|
||||
@move-start="onPaneMoveStart"
|
||||
@move-end="onPaneMoveEnd"
|
||||
@node-drag-stop="onNodeDragStop"
|
||||
|
@ -717,7 +714,7 @@ provide(CanvasKey, {
|
|||
:position="controlsPosition"
|
||||
:show-interactive="false"
|
||||
:show-bug-reporting-button="showBugReportingButton"
|
||||
:zoom="zoom"
|
||||
:zoom="viewport.zoom"
|
||||
@zoom-to-fit="onFitView"
|
||||
@zoom-in="onZoomIn"
|
||||
@zoom-out="onZoomOut"
|
||||
|
|
|
@ -4,6 +4,7 @@ import { createTestingPinia } from '@pinia/testing';
|
|||
import { setActivePinia } from 'pinia';
|
||||
import type { ConnectionLineProps } from '@vue-flow/core';
|
||||
import { Position } from '@vue-flow/core';
|
||||
import { createCanvasProvide } from '@/__tests__/data';
|
||||
|
||||
const DEFAULT_PROPS = {
|
||||
sourceX: 0,
|
||||
|
@ -15,6 +16,11 @@ const DEFAULT_PROPS = {
|
|||
} satisfies Partial<ConnectionLineProps>;
|
||||
const renderComponent = createComponentRenderer(CanvasConnectionLine, {
|
||||
props: DEFAULT_PROPS,
|
||||
global: {
|
||||
provide: {
|
||||
...createCanvasProvide(),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
|
|
|
@ -3,7 +3,7 @@ import { createComponentRenderer } from '@/__tests__/render';
|
|||
import { createPinia, setActivePinia } from 'pinia';
|
||||
import { NodeConnectionType } from 'n8n-workflow';
|
||||
import { fireEvent } from '@testing-library/vue';
|
||||
import { createCanvasNodeProps } from '@/__tests__/data';
|
||||
import { createCanvasNodeProps, createCanvasProvide } from '@/__tests__/data';
|
||||
|
||||
vi.mock('@/stores/nodeTypes.store', () => ({
|
||||
useNodeTypesStore: vi.fn(() => ({
|
||||
|
@ -19,7 +19,14 @@ beforeEach(() => {
|
|||
const pinia = createPinia();
|
||||
setActivePinia(pinia);
|
||||
|
||||
renderComponent = createComponentRenderer(CanvasNode, { pinia });
|
||||
renderComponent = createComponentRenderer(CanvasNode, {
|
||||
pinia,
|
||||
global: {
|
||||
provide: {
|
||||
...createCanvasProvide(),
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
describe('CanvasNode', () => {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import CanvasNodeRenderer from '@/components/canvas/elements/nodes/CanvasNodeRenderer.vue';
|
||||
import { createComponentRenderer } from '@/__tests__/render';
|
||||
import { createCanvasNodeProvide } from '@/__tests__/data';
|
||||
import { createCanvasNodeProvide, createCanvasProvide } from '@/__tests__/data';
|
||||
import { createTestingPinia } from '@pinia/testing';
|
||||
import { setActivePinia } from 'pinia';
|
||||
import { CanvasNodeRenderType } from '@/types';
|
||||
|
@ -17,6 +17,7 @@ describe('CanvasNodeRenderer', () => {
|
|||
const { getByTestId } = renderComponent({
|
||||
global: {
|
||||
provide: {
|
||||
...createCanvasProvide(),
|
||||
...createCanvasNodeProvide(),
|
||||
},
|
||||
},
|
||||
|
@ -29,6 +30,7 @@ describe('CanvasNodeRenderer', () => {
|
|||
const { getByTestId } = renderComponent({
|
||||
global: {
|
||||
provide: {
|
||||
...createCanvasProvide(),
|
||||
...createCanvasNodeProvide({
|
||||
data: {
|
||||
render: {
|
||||
|
@ -48,6 +50,7 @@ describe('CanvasNodeRenderer', () => {
|
|||
const { getByTestId } = renderComponent({
|
||||
global: {
|
||||
provide: {
|
||||
...createCanvasProvide(),
|
||||
...createCanvasNodeProvide({
|
||||
data: {
|
||||
render: {
|
||||
|
|
|
@ -1,12 +1,18 @@
|
|||
import CanvasNodeDefault from '@/components/canvas/elements/nodes/render-types/CanvasNodeDefault.vue';
|
||||
import { createComponentRenderer } from '@/__tests__/render';
|
||||
import { NodeConnectionType } from 'n8n-workflow';
|
||||
import { createCanvasNodeProvide } from '@/__tests__/data';
|
||||
import { createCanvasNodeProvide, createCanvasProvide } from '@/__tests__/data';
|
||||
import { createTestingPinia } from '@pinia/testing';
|
||||
import { setActivePinia } from 'pinia';
|
||||
import { CanvasConnectionMode, CanvasNodeRenderType } from '@/types';
|
||||
|
||||
const renderComponent = createComponentRenderer(CanvasNodeDefault);
|
||||
const renderComponent = createComponentRenderer(CanvasNodeDefault, {
|
||||
global: {
|
||||
provide: {
|
||||
...createCanvasProvide(),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
const pinia = createTestingPinia();
|
||||
|
|
|
@ -1,13 +1,11 @@
|
|||
<script lang="ts" setup>
|
||||
import { computed, useCssModule } from 'vue';
|
||||
import { computed, ref, useCssModule, watch } from 'vue';
|
||||
import { useNodeConnections } from '@/composables/useNodeConnections';
|
||||
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 { NODE_INSERT_SPACER_BETWEEN_INPUT_GROUPS } from '@/constants';
|
||||
import { N8nTooltip } from 'n8n-design-system';
|
||||
import type { CanvasNodeDefaultRender } from '@/types';
|
||||
import { useCanvas } from '@/composables/useCanvas';
|
||||
|
||||
const $style = useCssModule();
|
||||
const i18n = useI18n();
|
||||
|
@ -16,6 +14,7 @@ const emit = defineEmits<{
|
|||
'open:contextmenu': [event: MouseEvent];
|
||||
}>();
|
||||
|
||||
const { initialized, viewport } = useCanvas();
|
||||
const {
|
||||
label,
|
||||
subtitle,
|
||||
|
@ -105,6 +104,21 @@ const isStrikethroughVisible = computed(() => {
|
|||
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) {
|
||||
emit('open:contextmenu', event);
|
||||
}
|
||||
|
@ -112,15 +126,9 @@ function openContextMenu(event: MouseEvent) {
|
|||
|
||||
<template>
|
||||
<div :class="classes" :style="styles" :data-test-id="dataTestId" @contextmenu="openContextMenu">
|
||||
<CanvasNodeTooltip v-if="renderOptions.tooltip" :visible="showTooltip" />
|
||||
<slot />
|
||||
<N8nTooltip v-if="renderOptions.trigger" placement="bottom">
|
||||
<template #content>
|
||||
<span v-n8n-html="i18n.baseText('node.thisIsATriggerNode')" />
|
||||
</template>
|
||||
<div :class="$style.triggerIcon">
|
||||
<FontAwesomeIcon icon="bolt" size="lg" />
|
||||
</div>
|
||||
</N8nTooltip>
|
||||
<CanvasNodeTriggerIcon v-if="renderOptions.trigger" />
|
||||
<CanvasNodeStatusIcons v-if="!isDisabled" :class="$style.statusIcons" />
|
||||
<CanvasNodeDisabledStrikeThrough v-if="isStrikethroughVisible" />
|
||||
<div :class="$style.description">
|
||||
|
@ -152,6 +160,7 @@ function openContextMenu(event: MouseEvent) {
|
|||
--trigger-node--border-radius: 36px;
|
||||
--canvas-node--status-icons-offset: var(--spacing-2xs);
|
||||
|
||||
position: relative;
|
||||
height: var(--canvas-node--height);
|
||||
width: var(--canvas-node--width);
|
||||
display: flex;
|
||||
|
@ -298,12 +307,4 @@ function openContextMenu(event: MouseEvent) {
|
|||
bottom: 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>
|
||||
|
|
|
@ -6,6 +6,7 @@ exports[`CanvasNodeDefault > configurable > should render configurable node corr
|
|||
data-test-id="canvas-configurable-node"
|
||||
style="--configurable-node--input-count: 0; --canvas-node--main-input-count: 0; --canvas-node--main-output-count: 0;"
|
||||
>
|
||||
<!--v-if-->
|
||||
|
||||
|
||||
<!--v-if-->
|
||||
|
@ -35,6 +36,7 @@ exports[`CanvasNodeDefault > configuration > should render configurable configur
|
|||
data-test-id="canvas-configurable-node"
|
||||
style="--configurable-node--input-count: 0; --canvas-node--main-input-count: 0; --canvas-node--main-output-count: 0;"
|
||||
>
|
||||
<!--v-if-->
|
||||
|
||||
|
||||
<!--v-if-->
|
||||
|
@ -64,6 +66,7 @@ exports[`CanvasNodeDefault > configuration > should render configuration node co
|
|||
data-test-id="canvas-configuration-node"
|
||||
style="--canvas-node--main-input-count: 0; --canvas-node--main-output-count: 0;"
|
||||
>
|
||||
<!--v-if-->
|
||||
|
||||
|
||||
<!--v-if-->
|
||||
|
@ -93,6 +96,7 @@ exports[`CanvasNodeDefault > should render node correctly 1`] = `
|
|||
data-test-id="canvas-default-node"
|
||||
style="--canvas-node--main-input-count: 0; --canvas-node--main-output-count: 0;"
|
||||
>
|
||||
<!--v-if-->
|
||||
|
||||
|
||||
<!--v-if-->
|
||||
|
@ -122,6 +126,7 @@ exports[`CanvasNodeDefault > trigger > should render trigger node correctly 1`]
|
|||
data-test-id="canvas-trigger-node"
|
||||
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 { injectStrict } from '@/utils/injectStrict';
|
||||
|
||||
export function useCanvas() {
|
||||
const canvas = inject(CanvasKey);
|
||||
|
||||
const connectingHandle = computed(() => canvas?.connectingHandle.value);
|
||||
const isExecuting = computed(() => canvas?.isExecuting.value);
|
||||
|
||||
return {
|
||||
isExecuting,
|
||||
connectingHandle,
|
||||
};
|
||||
return injectStrict(CanvasKey);
|
||||
}
|
||||
|
|
|
@ -33,6 +33,7 @@ import type {
|
|||
ExecutionSummary,
|
||||
IConnections,
|
||||
INodeExecutionData,
|
||||
INodeTypeDescription,
|
||||
ITaskData,
|
||||
Workflow,
|
||||
} from 'n8n-workflow';
|
||||
|
@ -48,6 +49,7 @@ import {
|
|||
import { sanitizeHtml } from '@/utils/htmlUtils';
|
||||
import { MarkerType } from '@vue-flow/core';
|
||||
import { useNodeHelpers } from './useNodeHelpers';
|
||||
import { getTriggerNodeServiceName } from '@/utils/nodeTypesUtils';
|
||||
|
||||
export function useCanvasMapping({
|
||||
nodes,
|
||||
|
@ -86,7 +88,7 @@ export function useCanvasMapping({
|
|||
return {
|
||||
type: CanvasNodeRenderType.Default,
|
||||
options: {
|
||||
trigger: nodeTypesStore.isTriggerNode(node.type),
|
||||
trigger: isTriggerNodeById.value[node.id],
|
||||
configuration: nodeTypesStore.isConfigNode(workflowObject.value, node, node.type),
|
||||
configurable: nodeTypesStore.isConfigurableNode(workflowObject.value, node, node.type),
|
||||
inputs: {
|
||||
|
@ -95,6 +97,7 @@ export function useCanvasMapping({
|
|||
outputs: {
|
||||
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(() => {
|
||||
return nodes.value.reduce<Record<string, string>>((acc, node) => {
|
||||
try {
|
||||
const nodeTypeDescription = nodeTypesStore.getNodeType(node.type, node.typeVersion);
|
||||
const nodeTypeDescription = nodeTypeDescriptionByNodeId.value[node.id];
|
||||
if (!nodeTypeDescription) {
|
||||
return acc;
|
||||
}
|
||||
|
@ -140,7 +167,7 @@ export function useCanvasMapping({
|
|||
|
||||
const nodeInputsById = computed(() =>
|
||||
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);
|
||||
|
||||
acc[node.id] =
|
||||
|
@ -203,7 +230,7 @@ export function useCanvasMapping({
|
|||
|
||||
const nodeOutputsById = computed(() =>
|
||||
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);
|
||||
|
||||
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(() =>
|
||||
nodes.value.reduce<Record<string, boolean>>((acc, node) => {
|
||||
acc[node.id] = workflowsStore.isNodeExecuting(node.name);
|
||||
|
|
|
@ -5,7 +5,14 @@ import type {
|
|||
IConnection,
|
||||
NodeConnectionType,
|
||||
} 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 { ComputedRef, Ref } from 'vue';
|
||||
import type { PartialBy } from '@/utils/typeHelpers';
|
||||
|
@ -59,6 +66,7 @@ export type CanvasNodeDefaultRender = {
|
|||
outputs: {
|
||||
labelSize: CanvasNodeDefaultRenderLabelSize;
|
||||
};
|
||||
tooltip?: string;
|
||||
}>;
|
||||
};
|
||||
|
||||
|
@ -135,8 +143,10 @@ export type CanvasConnectionCreateData = {
|
|||
};
|
||||
|
||||
export interface CanvasInjectionData {
|
||||
initialized: Ref<boolean>;
|
||||
isExecuting: Ref<boolean | undefined>;
|
||||
connectingHandle: Ref<ConnectStartEvent | undefined>;
|
||||
viewport: Ref<ViewportTransform>;
|
||||
}
|
||||
|
||||
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