mirror of
https://github.com/n8n-io/n8n.git
synced 2025-03-05 20:50:17 -08:00
feat(editor): Add readonly mode to new canvas (no-changelog) (#10133)
This commit is contained in:
parent
a96db344e5
commit
6b8ad6fc3e
|
@ -64,12 +64,20 @@ export function createCanvasNodeProps({
|
||||||
id = 'node',
|
id = 'node',
|
||||||
label = 'Test Node',
|
label = 'Test Node',
|
||||||
selected = false,
|
selected = false,
|
||||||
|
readOnly = false,
|
||||||
data = {},
|
data = {},
|
||||||
}: { id?: string; label?: string; selected?: boolean; data?: Partial<CanvasNodeData> } = {}) {
|
}: {
|
||||||
|
id?: string;
|
||||||
|
label?: string;
|
||||||
|
selected?: boolean;
|
||||||
|
readOnly?: boolean;
|
||||||
|
data?: Partial<CanvasNodeData>;
|
||||||
|
} = {}) {
|
||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
label,
|
label,
|
||||||
selected,
|
selected,
|
||||||
|
readOnly,
|
||||||
data: createCanvasNodeData(data),
|
data: createCanvasNodeData(data),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,7 @@ import { Controls } from '@vue-flow/controls';
|
||||||
import { MiniMap } from '@vue-flow/minimap';
|
import { MiniMap } from '@vue-flow/minimap';
|
||||||
import Node from './elements/nodes/CanvasNode.vue';
|
import Node from './elements/nodes/CanvasNode.vue';
|
||||||
import Edge from './elements/edges/CanvasEdge.vue';
|
import Edge from './elements/edges/CanvasEdge.vue';
|
||||||
import { computed, onMounted, onUnmounted, ref, useCssModule } from 'vue';
|
import { computed, onMounted, onUnmounted, ref, useCssModule, watch } from 'vue';
|
||||||
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 { useContextMenu, type ContextMenuAction } from '@/composables/useContextMenu';
|
import { useContextMenu, type ContextMenuAction } from '@/composables/useContextMenu';
|
||||||
|
@ -59,6 +59,7 @@ const props = withDefaults(
|
||||||
connections: CanvasConnection[];
|
connections: CanvasConnection[];
|
||||||
controlsPosition?: PanelPosition;
|
controlsPosition?: PanelPosition;
|
||||||
eventBus?: EventBus;
|
eventBus?: EventBus;
|
||||||
|
readOnly?: boolean;
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
id: 'canvas',
|
id: 'canvas',
|
||||||
|
@ -66,6 +67,7 @@ const props = withDefaults(
|
||||||
connections: () => [],
|
connections: () => [],
|
||||||
controlsPosition: PanelPosition.BottomLeft,
|
controlsPosition: PanelPosition.BottomLeft,
|
||||||
eventBus: () => createEventBus(),
|
eventBus: () => createEventBus(),
|
||||||
|
readOnly: false,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -75,6 +77,8 @@ const {
|
||||||
removeSelectedNodes,
|
removeSelectedNodes,
|
||||||
viewportRef,
|
viewportRef,
|
||||||
fitView,
|
fitView,
|
||||||
|
setInteractive,
|
||||||
|
elementsSelectable,
|
||||||
project,
|
project,
|
||||||
nodes: graphNodes,
|
nodes: graphNodes,
|
||||||
onPaneReady,
|
onPaneReady,
|
||||||
|
@ -267,6 +271,11 @@ async function onFitView() {
|
||||||
await fitView({ maxZoom: 1.2, padding: 0.1 });
|
await fitView({ maxZoom: 1.2, padding: 0.1 });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setReadonly(value: boolean) {
|
||||||
|
setInteractive(!value);
|
||||||
|
elementsSelectable.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Context menu
|
* Context menu
|
||||||
*/
|
*/
|
||||||
|
@ -337,6 +346,10 @@ onPaneReady(async () => {
|
||||||
await onFitView();
|
await onFitView();
|
||||||
paneReady.value = true;
|
paneReady.value = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
watch(() => props.readOnly, setReadonly, {
|
||||||
|
immediate: true,
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
@ -365,6 +378,7 @@ onPaneReady(async () => {
|
||||||
<template #node-canvas-node="canvasNodeProps">
|
<template #node-canvas-node="canvasNodeProps">
|
||||||
<Node
|
<Node
|
||||||
v-bind="canvasNodeProps"
|
v-bind="canvasNodeProps"
|
||||||
|
:read-only="readOnly"
|
||||||
@delete="onDeleteNode"
|
@delete="onDeleteNode"
|
||||||
@run="onRunNode"
|
@run="onRunNode"
|
||||||
@select="onSelectNode"
|
@select="onSelectNode"
|
||||||
|
@ -380,6 +394,7 @@ onPaneReady(async () => {
|
||||||
<template #edge-canvas-edge="canvasEdgeProps">
|
<template #edge-canvas-edge="canvasEdgeProps">
|
||||||
<Edge
|
<Edge
|
||||||
v-bind="canvasEdgeProps"
|
v-bind="canvasEdgeProps"
|
||||||
|
:read-only="readOnly"
|
||||||
:hovered="hoveredEdges[canvasEdgeProps.id]"
|
:hovered="hoveredEdges[canvasEdgeProps.id]"
|
||||||
@add="onClickConnectionAdd"
|
@add="onClickConnectionAdd"
|
||||||
@delete="onDeleteConnection"
|
@delete="onDeleteConnection"
|
||||||
|
@ -394,6 +409,7 @@ onPaneReady(async () => {
|
||||||
data-test-id="canvas-controls"
|
data-test-id="canvas-controls"
|
||||||
:class="$style.canvasControls"
|
:class="$style.canvasControls"
|
||||||
:position="controlsPosition"
|
:position="controlsPosition"
|
||||||
|
:show-interactive="!readOnly"
|
||||||
@fit-view="onFitView"
|
@fit-view="onFitView"
|
||||||
></Controls>
|
></Controls>
|
||||||
|
|
||||||
|
|
|
@ -17,6 +17,7 @@ const props = withDefaults(
|
||||||
workflowObject: Workflow;
|
workflowObject: Workflow;
|
||||||
fallbackNodes?: IWorkflowDb['nodes'];
|
fallbackNodes?: IWorkflowDb['nodes'];
|
||||||
eventBus?: EventBus;
|
eventBus?: EventBus;
|
||||||
|
readOnly?: boolean;
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
id: 'canvas',
|
id: 'canvas',
|
||||||
|
@ -49,6 +50,7 @@ const { nodes: mappedNodes, connections: mappedConnections } = useCanvasMapping(
|
||||||
:nodes="mappedNodes"
|
:nodes="mappedNodes"
|
||||||
:connections="mappedConnections"
|
:connections="mappedConnections"
|
||||||
:event-bus="eventBus"
|
:event-bus="eventBus"
|
||||||
|
:read-only="readOnly"
|
||||||
v-bind="$attrs"
|
v-bind="$attrs"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -38,6 +38,26 @@ describe('CanvasEdge', () => {
|
||||||
expect(emitted()).toHaveProperty('delete');
|
expect(emitted()).toHaveProperty('delete');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should emit add event when toolbar add is clicked', async () => {
|
||||||
|
const { emitted, getByTestId } = renderComponent();
|
||||||
|
const addButton = getByTestId('add-connection-button');
|
||||||
|
|
||||||
|
await fireEvent.click(addButton);
|
||||||
|
|
||||||
|
expect(emitted()).toHaveProperty('add');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not render toolbar actions when readOnly', async () => {
|
||||||
|
const { getByTestId } = renderComponent({
|
||||||
|
props: {
|
||||||
|
readOnly: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(() => getByTestId('add-connection-button')).toThrow();
|
||||||
|
expect(() => getByTestId('delete-connection-button')).toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
it('should compute edgeStyle correctly', () => {
|
it('should compute edgeStyle correctly', () => {
|
||||||
const { container } = renderComponent();
|
const { container } = renderComponent();
|
||||||
|
|
||||||
|
|
|
@ -14,6 +14,7 @@ const emit = defineEmits<{
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
export type CanvasEdgeProps = EdgeProps<CanvasConnectionData> & {
|
export type CanvasEdgeProps = EdgeProps<CanvasConnectionData> & {
|
||||||
|
readOnly?: boolean;
|
||||||
hovered?: boolean;
|
hovered?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -51,7 +52,7 @@ const edgeStyle = computed(() => ({
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const edgeLabel = computed(() => {
|
const edgeLabel = computed(() => {
|
||||||
if (isFocused.value) {
|
if (isFocused.value && !props.readOnly) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -117,7 +118,7 @@ function onDelete() {
|
||||||
:label-style="edgeLabelStyle"
|
:label-style="edgeLabelStyle"
|
||||||
:label-show-bg="false"
|
:label-show-bg="false"
|
||||||
/>
|
/>
|
||||||
<EdgeLabelRenderer>
|
<EdgeLabelRenderer v-if="!readOnly">
|
||||||
<CanvasEdgeToolbar
|
<CanvasEdgeToolbar
|
||||||
:type="connectionType"
|
:type="connectionType"
|
||||||
:class="edgeToolbarClasses"
|
:class="edgeToolbarClasses"
|
||||||
|
|
|
@ -91,6 +91,29 @@ describe('CanvasNode', () => {
|
||||||
await fireEvent.mouseOver(node);
|
await fireEvent.mouseOver(node);
|
||||||
|
|
||||||
expect(getByTestId('canvas-node-toolbar')).toBeInTheDocument();
|
expect(getByTestId('canvas-node-toolbar')).toBeInTheDocument();
|
||||||
|
expect(getByTestId('execute-node-button')).toBeInTheDocument();
|
||||||
|
expect(getByTestId('disable-node-button')).toBeInTheDocument();
|
||||||
|
expect(getByTestId('delete-node-button')).toBeInTheDocument();
|
||||||
|
expect(getByTestId('overflow-node-button')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should contain only context menu when node is disabled', async () => {
|
||||||
|
const { getByTestId } = renderComponent({
|
||||||
|
props: {
|
||||||
|
...createCanvasNodeProps({
|
||||||
|
readOnly: true,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const node = getByTestId('canvas-node');
|
||||||
|
await fireEvent.mouseOver(node);
|
||||||
|
|
||||||
|
expect(getByTestId('canvas-node-toolbar')).toBeInTheDocument();
|
||||||
|
expect(() => getByTestId('execute-node-button')).toThrow();
|
||||||
|
expect(() => getByTestId('disable-node-button')).toThrow();
|
||||||
|
expect(() => getByTestId('delete-node-button')).toThrow();
|
||||||
|
expect(getByTestId('overflow-node-button')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -17,6 +17,10 @@ import { useContextMenu } from '@/composables/useContextMenu';
|
||||||
import { Position } from '@vue-flow/core';
|
import { Position } from '@vue-flow/core';
|
||||||
import type { XYPosition, NodeProps } from '@vue-flow/core';
|
import type { XYPosition, NodeProps } from '@vue-flow/core';
|
||||||
|
|
||||||
|
type Props = NodeProps<CanvasNodeData> & {
|
||||||
|
readOnly?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
add: [id: string, handle: string];
|
add: [id: string, handle: string];
|
||||||
delete: [id: string];
|
delete: [id: string];
|
||||||
|
@ -28,7 +32,8 @@ const emit = defineEmits<{
|
||||||
update: [id: string, parameters: Record<string, unknown>];
|
update: [id: string, parameters: Record<string, unknown>];
|
||||||
move: [id: string, position: XYPosition];
|
move: [id: string, position: XYPosition];
|
||||||
}>();
|
}>();
|
||||||
const props = defineProps<NodeProps<CanvasNodeData>>();
|
|
||||||
|
const props = defineProps<Props>();
|
||||||
|
|
||||||
const nodeTypesStore = useNodeTypesStore();
|
const nodeTypesStore = useNodeTypesStore();
|
||||||
const contextMenu = useContextMenu();
|
const contextMenu = useContextMenu();
|
||||||
|
@ -248,6 +253,7 @@ watch(
|
||||||
<CanvasNodeToolbar
|
<CanvasNodeToolbar
|
||||||
v-if="nodeTypeDescription"
|
v-if="nodeTypeDescription"
|
||||||
data-test-id="canvas-node-toolbar"
|
data-test-id="canvas-node-toolbar"
|
||||||
|
:read-only="readOnly"
|
||||||
:class="$style.canvasNodeToolbar"
|
:class="$style.canvasNodeToolbar"
|
||||||
@delete="onDelete"
|
@delete="onDelete"
|
||||||
@toggle="onDisabledToggle"
|
@toggle="onDisabledToggle"
|
||||||
|
@ -256,7 +262,7 @@ watch(
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<CanvasNodeRenderer
|
<CanvasNodeRenderer
|
||||||
@dblclick="onActivate"
|
@dblclick.stop="onActivate"
|
||||||
@move="onMove"
|
@move="onMove"
|
||||||
@update="onUpdate"
|
@update="onUpdate"
|
||||||
@open:contextmenu="onOpenContextMenuFromNode"
|
@open:contextmenu="onOpenContextMenuFromNode"
|
||||||
|
|
|
@ -11,6 +11,10 @@ const emit = defineEmits<{
|
||||||
'open:contextmenu': [event: MouseEvent];
|
'open:contextmenu': [event: MouseEvent];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
readOnly?: boolean;
|
||||||
|
}>();
|
||||||
|
|
||||||
const $style = useCssModule();
|
const $style = useCssModule();
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
|
|
||||||
|
@ -22,8 +26,14 @@ const workflowRunning = false;
|
||||||
// @TODO
|
// @TODO
|
||||||
const nodeDisabledTitle = 'Test';
|
const nodeDisabledTitle = 'Test';
|
||||||
|
|
||||||
|
const classes = computed(() => ({
|
||||||
|
[$style.canvasNodeToolbar]: true,
|
||||||
|
[$style.readOnly]: props.readOnly,
|
||||||
|
}));
|
||||||
|
|
||||||
const isExecuteNodeVisible = computed(() => {
|
const isExecuteNodeVisible = computed(() => {
|
||||||
return (
|
return (
|
||||||
|
!props.readOnly &&
|
||||||
render.value.type === CanvasNodeRenderType.Default &&
|
render.value.type === CanvasNodeRenderType.Default &&
|
||||||
'configuration' in render.value.options &&
|
'configuration' in render.value.options &&
|
||||||
!render.value.options.configuration
|
!render.value.options.configuration
|
||||||
|
@ -31,9 +41,11 @@ const isExecuteNodeVisible = computed(() => {
|
||||||
});
|
});
|
||||||
|
|
||||||
const isDisableNodeVisible = computed(() => {
|
const isDisableNodeVisible = computed(() => {
|
||||||
return render.value.type === CanvasNodeRenderType.Default;
|
return !props.readOnly && render.value.type === CanvasNodeRenderType.Default;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const isDeleteNodeVisible = computed(() => !props.readOnly);
|
||||||
|
|
||||||
function executeNode() {
|
function executeNode() {
|
||||||
emit('run');
|
emit('run');
|
||||||
}
|
}
|
||||||
|
@ -52,7 +64,7 @@ function onOpenContextMenu(event: MouseEvent) {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div :class="$style.canvasNodeToolbar">
|
<div :class="classes">
|
||||||
<div :class="$style.canvasNodeToolbarItems">
|
<div :class="$style.canvasNodeToolbarItems">
|
||||||
<N8nIconButton
|
<N8nIconButton
|
||||||
v-if="isExecuteNodeVisible"
|
v-if="isExecuteNodeVisible"
|
||||||
|
@ -76,6 +88,7 @@ function onOpenContextMenu(event: MouseEvent) {
|
||||||
@click="onToggleNode"
|
@click="onToggleNode"
|
||||||
/>
|
/>
|
||||||
<N8nIconButton
|
<N8nIconButton
|
||||||
|
v-if="isDeleteNodeVisible"
|
||||||
data-test-id="delete-node-button"
|
data-test-id="delete-node-button"
|
||||||
type="tertiary"
|
type="tertiary"
|
||||||
size="small"
|
size="small"
|
||||||
|
@ -99,6 +112,9 @@ function onOpenContextMenu(event: MouseEvent) {
|
||||||
<style lang="scss" module>
|
<style lang="scss" module>
|
||||||
.canvasNodeToolbar {
|
.canvasNodeToolbar {
|
||||||
padding-bottom: var(--spacing-2xs);
|
padding-bottom: var(--spacing-2xs);
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.canvasNodeToolbarItems {
|
.canvasNodeToolbarItems {
|
||||||
|
|
|
@ -213,8 +213,11 @@ export function useCanvasMapping({
|
||||||
const lastNodeExecuted = workflowExecution?.data?.resultData?.lastNodeExecuted;
|
const lastNodeExecuted = workflowExecution?.data?.resultData?.lastNodeExecuted;
|
||||||
|
|
||||||
if (workflowExecution && lastNodeExecuted && isExecutionSummary(workflowExecution)) {
|
if (workflowExecution && lastNodeExecuted && isExecutionSummary(workflowExecution)) {
|
||||||
if (node.name === workflowExecution.data?.resultData?.lastNodeExecuted) {
|
if (
|
||||||
const waitDate = new Date(workflowExecution.waitTill as Date);
|
node.name === workflowExecution.data?.resultData?.lastNodeExecuted &&
|
||||||
|
workflowExecution.waitTill
|
||||||
|
) {
|
||||||
|
const waitDate = new Date(workflowExecution.waitTill);
|
||||||
|
|
||||||
if (waitDate.toISOString() === WAIT_TIME_UNLIMITED) {
|
if (waitDate.toISOString() === WAIT_TIME_UNLIMITED) {
|
||||||
acc[node.id] = i18n.baseText(
|
acc[node.id] = i18n.baseText(
|
||||||
|
|
|
@ -19,6 +19,7 @@ import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||||
import { useRunWorkflow } from '@/composables/useRunWorkflow';
|
import { useRunWorkflow } from '@/composables/useRunWorkflow';
|
||||||
import type {
|
import type {
|
||||||
AddedNodesAndConnections,
|
AddedNodesAndConnections,
|
||||||
|
IExecutionResponse,
|
||||||
INodeUi,
|
INodeUi,
|
||||||
IUpdateInformation,
|
IUpdateInformation,
|
||||||
IWorkflowDataUpdate,
|
IWorkflowDataUpdate,
|
||||||
|
@ -194,11 +195,11 @@ const isReadOnlyEnvironment = computed(() => {
|
||||||
});
|
});
|
||||||
|
|
||||||
const isCanvasReadOnly = computed(() => {
|
const isCanvasReadOnly = computed(() => {
|
||||||
return isLoading.value || isDemoRoute.value || isReadOnlyEnvironment.value;
|
return isDemoRoute.value || isReadOnlyEnvironment.value;
|
||||||
});
|
});
|
||||||
|
|
||||||
const fallbackNodes = computed<INodeUi[]>(() =>
|
const fallbackNodes = computed<INodeUi[]>(() =>
|
||||||
isCanvasReadOnly.value
|
isLoading.value || isCanvasReadOnly.value
|
||||||
? []
|
? []
|
||||||
: [
|
: [
|
||||||
{
|
{
|
||||||
|
@ -940,8 +941,101 @@ function trackRunWorkflowToNode(node: INodeUi) {
|
||||||
void externalHooks.run('nodeView.onRunNode', telemetryPayload);
|
void externalHooks.run('nodeView.onRunNode', telemetryPayload);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function openExecution(_executionId: string) {
|
async function openExecution(executionId: string) {
|
||||||
// @TODO
|
canvasStore.startLoading();
|
||||||
|
resetWorkspace();
|
||||||
|
|
||||||
|
let data: IExecutionResponse | undefined;
|
||||||
|
try {
|
||||||
|
data = await workflowsStore.getExecution(executionId);
|
||||||
|
} catch (error) {
|
||||||
|
toast.showError(error, i18n.baseText('nodeView.showError.openExecution.title'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (data === undefined) {
|
||||||
|
throw new Error(`Execution with id "${executionId}" could not be found!`);
|
||||||
|
}
|
||||||
|
|
||||||
|
await initializeData();
|
||||||
|
await initializeWorkspace(data.workflowData);
|
||||||
|
workflowsStore.setWorkflowExecutionData(data);
|
||||||
|
|
||||||
|
uiStore.stateIsDirty = false;
|
||||||
|
canvasStore.stopLoading();
|
||||||
|
|
||||||
|
fitView();
|
||||||
|
|
||||||
|
canvasEventBus.emit('open:execution', data);
|
||||||
|
|
||||||
|
void externalHooks.run('execution.open', {
|
||||||
|
workflowId: data.workflowData.id,
|
||||||
|
workflowName: data.workflowData.name,
|
||||||
|
executionId,
|
||||||
|
});
|
||||||
|
|
||||||
|
telemetry.track('User opened read-only execution', {
|
||||||
|
workflow_id: data.workflowData.id,
|
||||||
|
execution_mode: data.mode,
|
||||||
|
execution_finished: data.finished,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function onExecutionOpenedWithError(data: IExecutionResponse) {
|
||||||
|
if (!data.finished && data.data?.resultData?.error) {
|
||||||
|
// Check if any node contains an error
|
||||||
|
let nodeErrorFound = false;
|
||||||
|
if (data.data.resultData.runData) {
|
||||||
|
const runData = data.data.resultData.runData;
|
||||||
|
errorCheck: for (const nodeName of Object.keys(runData)) {
|
||||||
|
for (const taskData of runData[nodeName]) {
|
||||||
|
if (taskData.error) {
|
||||||
|
nodeErrorFound = true;
|
||||||
|
break errorCheck;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!nodeErrorFound &&
|
||||||
|
(data.data.resultData.error.stack ?? data.data.resultData.error.message)
|
||||||
|
) {
|
||||||
|
// Display some more information for now in console to make debugging easier
|
||||||
|
console.error(`Execution ${data.id} error:`);
|
||||||
|
console.error(data.data.resultData.error.stack);
|
||||||
|
toast.showMessage({
|
||||||
|
title: i18n.baseText('nodeView.showError.workflowError'),
|
||||||
|
message: data.data.resultData.error.message,
|
||||||
|
type: 'error',
|
||||||
|
duration: 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onExecutionOpenedWithWaitTill(data: IExecutionResponse) {
|
||||||
|
if ((data as ExecutionSummary).waitTill) {
|
||||||
|
toast.showMessage({
|
||||||
|
title: i18n.baseText('nodeView.thisExecutionHasntFinishedYet'),
|
||||||
|
message: `<a data-action="reload">${i18n.baseText('nodeView.refresh')}</a> ${i18n.baseText(
|
||||||
|
'nodeView.toSeeTheLatestStatus',
|
||||||
|
)}.<br/> <a href="https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.wait/" target="_blank">${i18n.baseText(
|
||||||
|
'nodeView.moreInfo',
|
||||||
|
)}</a>`,
|
||||||
|
type: 'warning',
|
||||||
|
duration: 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function addExecutionOpenedEventBindings() {
|
||||||
|
canvasEventBus.on('open:execution', onExecutionOpenedWithError);
|
||||||
|
canvasEventBus.on('open:execution', onExecutionOpenedWithWaitTill);
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeExecutionOpenedEventBindings() {
|
||||||
|
canvasEventBus.off('open:execution', onExecutionOpenedWithError);
|
||||||
|
canvasEventBus.off('open:execution', onExecutionOpenedWithWaitTill);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onStopExecution() {
|
async function onStopExecution() {
|
||||||
|
@ -1353,6 +1447,7 @@ onMounted(async () => {
|
||||||
addPostMessageEventBindings();
|
addPostMessageEventBindings();
|
||||||
addSourceControlEventBindings();
|
addSourceControlEventBindings();
|
||||||
addImportEventBindings();
|
addImportEventBindings();
|
||||||
|
addExecutionOpenedEventBindings();
|
||||||
addWorkflowSavedEventBindings();
|
addWorkflowSavedEventBindings();
|
||||||
|
|
||||||
registerCustomActions();
|
registerCustomActions();
|
||||||
|
@ -1366,6 +1461,7 @@ onBeforeUnmount(() => {
|
||||||
removePostMessageEventBindings();
|
removePostMessageEventBindings();
|
||||||
removeSourceControlEventBindings();
|
removeSourceControlEventBindings();
|
||||||
removeImportEventBindings();
|
removeImportEventBindings();
|
||||||
|
removeExecutionOpenedEventBindings();
|
||||||
removeWorkflowSavedEventBindings();
|
removeWorkflowSavedEventBindings();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
@ -1377,6 +1473,7 @@ onBeforeUnmount(() => {
|
||||||
:workflow-object="editableWorkflowObject"
|
:workflow-object="editableWorkflowObject"
|
||||||
:fallback-nodes="fallbackNodes"
|
:fallback-nodes="fallbackNodes"
|
||||||
:event-bus="canvasEventBus"
|
:event-bus="canvasEventBus"
|
||||||
|
:read-only="isCanvasReadOnly"
|
||||||
@update:nodes:position="onUpdateNodesPosition"
|
@update:nodes:position="onUpdateNodesPosition"
|
||||||
@update:node:position="onUpdateNodePosition"
|
@update:node:position="onUpdateNodePosition"
|
||||||
@update:node:active="onSetNodeActive"
|
@update:node:active="onSetNodeActive"
|
||||||
|
@ -1429,7 +1526,7 @@ onBeforeUnmount(() => {
|
||||||
</div>
|
</div>
|
||||||
<Suspense>
|
<Suspense>
|
||||||
<NodeCreation
|
<NodeCreation
|
||||||
v-if="!isReadOnlyRoute && !isReadOnlyEnvironment"
|
v-if="!isCanvasReadOnly"
|
||||||
:create-node-active="uiStore.isCreateNodeActive"
|
:create-node-active="uiStore.isCreateNodeActive"
|
||||||
:node-view-scale="1"
|
:node-view-scale="1"
|
||||||
@toggle-node-creator="onOpenNodeCreator"
|
@toggle-node-creator="onOpenNodeCreator"
|
||||||
|
@ -1439,7 +1536,7 @@ onBeforeUnmount(() => {
|
||||||
<Suspense>
|
<Suspense>
|
||||||
<NodeDetailsView
|
<NodeDetailsView
|
||||||
:workflow-object="editableWorkflowObject"
|
:workflow-object="editableWorkflowObject"
|
||||||
:read-only="isReadOnlyRoute || isReadOnlyEnvironment"
|
:read-only="isCanvasReadOnly"
|
||||||
:is-production-execution-preview="isProductionExecutionPreview"
|
:is-production-execution-preview="isProductionExecutionPreview"
|
||||||
:renaming="false"
|
:renaming="false"
|
||||||
@value-changed="onRenameNode"
|
@value-changed="onRenameNode"
|
||||||
|
|
Loading…
Reference in a new issue