feat(editor, core, cli): implement new workflow experience (#4358)

* feat(ExecuteWorkflowTrigger node): Implement ExecuteWorkflowTrigger node (#4108)

* feat(ExecuteWorkflowTrigger node): Implement ExecuteWorkflowTrigger node

* feat(editor): Do not show duplicate button if canvas contains `maxNodes` amount of nodes

* feat(ManualTrigger node): Implement ManualTrigger node (#4110)

* feat(ManualTrigger node): Implement ManualTrigger node

* 📝 Remove generics doc items from ManualTrigger node

* feat(editor-ui): Trigger tab redesign (#4150)

* 🚧 Begin with TriggerPanel implementation, add Other Trigger Nodes subcategory

* 🚧 Extracted categorized categories/subcategory/nodes rendering into its own component — CategorizedItems, removed SubcategoryPanel, added translations

*  Implement MainPanel background scrim

* ♻️ Move `categoriesWithNodes`, 'visibleNodeTypes` and 'categorizedItems` to store, implemented dynamic categories count based on `selectedType`

* 🐛 Fix SlideTransition for all the NodeCreato panels

* 💄 Fix cursos for CategoryItem and NodeItem

* 🐛 Make sure ALL_NODE_FILTER is always set when MainPanel is mounted

* 🎨 Address PR comments

* label: Use Array type for CategorizedItems props

* 🏷️ Add proper types for Vue props

* 🎨 Use standard component registration for CategorizedItems inside TriggerHelperPanel

* 🎨 Use kebab case for main-panel and icon component

* 🏷️ Improve types

* feat(editor-ui): Redesign search input inside node creator panel (#4204)

* 🚧 Begin with TriggerPanel implementation, add Other Trigger Nodes subcategory

* 🚧 Extracted categorized categories/subcategory/nodes rendering into its own component — CategorizedItems, removed SubcategoryPanel, added translations

*  Implement MainPanel background scrim

* ♻️ Move `categoriesWithNodes`, 'visibleNodeTypes` and 'categorizedItems` to store, implemented dynamic categories count based on `selectedType`

* 🐛 Fix SlideTransition for all the NodeCreato panels

* 💄 Fix cursos for CategoryItem and NodeItem

* 🐛 Make sure ALL_NODE_FILTER is always set when MainPanel is mounted

* 🎨 Address PR comments

* label: Use Array type for CategorizedItems props

* 🏷️ Add proper types for Vue props

* 🎨 Use standard component registration for CategorizedItems inside TriggerHelperPanel

*  Redesign search input and unify usage of categorized items

* 🏷️ Use lowercase "Boolean" as `isSearchVisible` computed return type

* 🔥 Remove useless emit

*  Implement no result view based on subcategory, minor fixes

* 🎨 Remove unused properties

* feat(node-email): Change EmailReadImap display name and name (#4239)

* feat(editor-ui):  Implement "Choose a Triger" action and related behaviour (#4226)

*  Implement "Choose a Triger" action and related behaviour

* 🔇 Lint fix

* ♻️ Remove PlaceholderTrigger node, add a button instead

* 🎨 Merge onMouseEnter and onMouseLeave to a single function

* 💡 Add comment

* 🔥 Remove PlaceholderNode registration

* 🎨 Rename TriggerPlaceholderButton to CanvasAddButton

*  Add method to unregister custom action and rework CanvasAddButton centering logic

* 🎨 Run `setRecenteredCanvasAddButtonPosition` on `CanvasAddButton` mount

* fix(editor): Fix selecting of node from node-creator panel by clicking

* 🔀 Merge fixes

* fix(editor): Show execute workflow trigger instead of workflow trigger in the trigger helper panel

* feat(editor): Fix node creator panel slide transition (#4261)

* fix(editor): Fix node creator panel slide-in/slide-out transitions

* 🎨 Fix naming

* 🎨 Use kebab-case for transition component name

* feat(editor): Disable execution and show notice when user tries to run workflow without enabled triggers

* fix(editor): Address first batch of new WF experience review (#4279)

* fix(editor): Fix first batch of review items

* bug(editor): Fix nodeview canvas add button centering

* 🔇 Fix linter errors

* bug(ManualTrigger Node): Fix manual trigger node execution

* fix(editor): Do not show canvas add button in execution or demo mode and prevent clicking if creator is open

* fix(editor): do not show pin data tooltip for manual trigger node

* fix(editor): do not use nodeViewOffset on zoomToFit

* 💄 Add margin for last node creator item and set font-weight to 700 for category title

*  Position welcome note next to the added trigger node

* 🐛 Remve always true welcome note

* feat(editor): Minor UI and UX tweaks (#4328)

* 💄 Make top viewport buttons less prominent

*  Allow user to switch to all tabs if it contains filter results, move nodecreator state props to its own module

* 🔇 Fix linting errors

* 🔇 Fix linting errors

* 🔇 Fix linting errors

* chore(build): Ping Turbo version to 1.5.5

* 💄 Minor traigger panel and node view style changes

* 💬 Update display name of execute workflow trigger

* feat(core, editor): Update subworkflow execution logic (#4269)

*  Implement `findWorkflowStart`

*  Extend `WorkflowOperationError`

*  Add `WorkflowOperationError` to toast

* 📘 Extend interface

*  Add `subworkflowExecutionError` to store

*  Create `SubworkflowOperationError`

*  Render subworkflow error as node error

* 🚚 Move subworkflow start validation to `cli`

*  Reset subworkflow execution error state

* 🔥 Remove unused import

*  Adjust CLI commands

* 🔥 Remove unneeded check

* 🔥 Remove stray log

*  Simplify syntax

*  Sort in case both Start and EWT present

* ♻️ Address Omar's feedback

* 🔥 Remove unneeded lint exception

* ✏️ Fix copy

* 👕 Fix lint

* fix: moved find start node function to catchable place

Co-authored-by: Omar Ajoue <krynble@gmail.com>

* 💄 Change ExecuteWorkflow node to primary

*  Allow user to navigate to all tab if it contains search results

* 🐛 Fixed canvas control button while in demo, disable workflow activation for non-activavle nodes and revert zoomToFit bottom offset

* :fix: Do not chow request text if there's results

* 💬 Update noResults text

Co-authored-by: Iván Ovejero <ivov.src@gmail.com>
Co-authored-by: Omar Ajoue <krynble@gmail.com>
This commit is contained in:
OlegIvaniv
2022-10-18 14:23:22 +02:00
committed by GitHub
parent 128c3b83df
commit dae01f3abe
65 changed files with 2195 additions and 969 deletions

View File

@@ -4,22 +4,73 @@
@touchmove="mouseMoveNodeWorkflow" @mousedown="mouseDown" v-touch:tap="touchTap" @mouseup="mouseUp"
@wheel="wheelScroll">
<div id="node-view-background" class="node-view-background" :style="backgroundStyle" />
<div id="node-view" class="node-view" :style="workflowStyle">
<div
id="node-view"
class="node-view"
:style="workflowStyle"
ref="nodeView"
>
<canvas-add-button
:style="canvasAddButtonStyle"
@click="showTriggerCreator('tirger_placeholder_button')"
v-show="showCanvasAddButton"
:showTooltip="!containsTrigger && showTriggerMissingTooltip"
:position="canvasAddButtonPosition"
@hook:mounted="setRecenteredCanvasAddButtonPosition"
/>
<div v-for="nodeData in nodes" :key="nodeData.id">
<node v-if="nodeData.type !== STICKY_NODE_TYPE" @duplicateNode="duplicateNode"
@deselectAllNodes="deselectAllNodes" @deselectNode="nodeDeselectedByName" @nodeSelected="nodeSelectedByName"
@removeNode="removeNode" @runWorkflow="onRunNode" @moved="onNodeMoved" @run="onNodeRun" :key="nodeData.id"
:name="nodeData.name" :isReadOnly="isReadOnly" :instance="instance"
:isActive="!!activeNode && activeNode.name === nodeData.name" :hideActions="pullConnActive" />
<Sticky v-else @deselectAllNodes="deselectAllNodes" @deselectNode="nodeDeselectedByName"
@nodeSelected="nodeSelectedByName" @removeNode="removeNode" :key="nodeData.id" :name="nodeData.name"
:isReadOnly="isReadOnly" :instance="instance" :isActive="!!activeNode && activeNode.name === nodeData.name"
:nodeViewScale="nodeViewScale" :gridSize="GRID_SIZE" :hideActions="pullConnActive" />
<node
v-if="nodeData.type !== STICKY_NODE_TYPE"
@duplicateNode="duplicateNode"
@deselectAllNodes="deselectAllNodes"
@deselectNode="nodeDeselectedByName"
@nodeSelected="nodeSelectedByName"
@removeNode="removeNode"
@runWorkflow="onRunNode"
@moved="onNodeMoved"
@run="onNodeRun"
:key="`${nodeData.id}_node`"
:name="nodeData.name"
:isReadOnly="isReadOnly"
:instance="instance"
:isActive="!!activeNode && activeNode.name === nodeData.name"
:hideActions="pullConnActive"
>
<span
slot="custom-tooltip"
v-text="$locale.baseText('nodeView.placeholderNode.addTriggerNodeBeforeExecuting')"
/>
</node>
<sticky
v-else
@deselectAllNodes="deselectAllNodes"
@deselectNode="nodeDeselectedByName"
@nodeSelected="nodeSelectedByName"
@removeNode="removeNode"
:key="`${nodeData.id}_sticky`"
:name="nodeData.name"
:isReadOnly="isReadOnly"
:instance="instance"
:isActive="!!activeNode && activeNode.name === nodeData.name"
:nodeViewScale="nodeViewScale"
:gridSize="GRID_SIZE"
:hideActions="pullConnActive"
/>
</div>
</div>
</div>
<NodeDetailsView :readOnly="isReadOnly" :renaming="renamingActive" @valueChanged="valueChanged" />
<node-creation v-if="!isReadOnly" :create-node-active="createNodeActive" :node-view-scale="nodeViewScale" @toggleNodeCreator="onToggleNodeCreator" @addNode="onAddNode"/>
<node-details-view
:readOnly="isReadOnly"
:renaming="renamingActive"
@valueChanged="valueChanged"
/>
<node-creation
v-if="!isReadOnly"
:create-node-active="createNodeActive"
:node-view-scale="nodeViewScale"
@toggleNodeCreator="onToggleNodeCreator"
@addNode="onAddNode"
/>
<div
:class="{ 'zoom-menu': true, 'regular-zoom-menu': !isDemo, 'demo-zoom-menu': isDemo, expanded: !sidebarMenuCollapsed }">
<n8n-icon-button @click="zoomToFit" type="tertiary" size="large" :title="$locale.baseText('nodeView.zoomToFit')"
@@ -31,10 +82,25 @@
<n8n-icon-button v-if="nodeViewScale !== 1 && !isDemo" @click="resetZoom" type="tertiary" size="large"
:title="$locale.baseText('nodeView.resetZoom')" icon="undo" />
</div>
<div class="workflow-execute-wrapper" v-if="!isReadOnly">
<n8n-button @click.stop="onRunWorkflow" :loading="workflowRunning" :label="runButtonText"
:title="$locale.baseText('nodeView.executesTheWorkflowFromTheStartOrWebhookNode')" size="large"
icon="play-circle" type="primary" />
<div
class="workflow-execute-wrapper" v-if="!isReadOnly"
>
<span
@mouseenter="showTriggerMissingToltip(true)"
@mouseleave="showTriggerMissingToltip(false)"
@click="onRunContainerClick"
>
<n8n-button
@click.stop="onRunWorkflow"
:loading="workflowRunning"
:label="runButtonText"
:title="$locale.baseText('nodeView.executesTheWorkflowFromATriggerNode')"
size="large"
icon="play-circle"
type="primary"
:disabled="isExecutionDisabled"
/>
</span>
<n8n-icon-button v-if="workflowRunning === true && !executionWaitingForWebhook" icon="stop" size="large"
class="stop-execution" type="secondary" :title="stopExecutionInProgress
@@ -46,7 +112,7 @@
icon="stop" size="large" :title="$locale.baseText('nodeView.stopWaitingForWebhookCall')" type="secondary"
@click.stop="stopWaitingForWebhook()" />
<n8n-icon-button v-if="!isReadOnly && workflowExecution && !workflowRunning"
<n8n-icon-button v-if="!isReadOnly && workflowExecution && !workflowRunning && !allTriggersDisabled"
:title="$locale.baseText('nodeView.deletesTheCurrentExecutionData')" icon="trash" size="large"
@click.stop="clearExecutionData()" />
</div>
@@ -60,6 +126,8 @@ import {
} from 'jsplumb';
import type { MessageBoxInputData } from 'element-ui/types/message-box';
import { jsPlumb, OnConnectionBindInfo } from 'jsplumb';
import once from 'lodash/once';
import {
FIRST_ONBOARDING_PROMPT_TIMEOUT,
MODAL_CANCEL,
@@ -75,6 +143,7 @@ import {
VIEWS,
WEBHOOK_NODE_TYPE,
WORKFLOW_OPEN_MODAL_KEY,
TRIGGER_NODE_FILTER,
} from '@/constants';
import { copyPaste } from '@/components/mixins/copyPaste';
import { externalHooks } from '@/components/mixins/externalHooks';
@@ -82,6 +151,7 @@ import { genericHelpers } from '@/components/mixins/genericHelpers';
import { mouseSelect } from '@/components/mixins/mouseSelect';
import { moveNodeWorkflow } from '@/components/mixins/moveNodeWorkflow';
import { restApi } from '@/components/mixins/restApi';
import { globalLinkActions } from '@/components/mixins/globalLinkActions';
import { showMessage } from '@/components/mixins/showMessage';
import { titleChange } from '@/components/mixins/titleChange';
import { newVersions } from '@/components/mixins/newVersions';
@@ -93,6 +163,7 @@ import NodeDetailsView from '@/components/NodeDetailsView.vue';
import Node from '@/components/Node.vue';
import NodeSettings from '@/components/NodeSettings.vue';
import Sticky from '@/components/Sticky.vue';
import CanvasAddButton from './CanvasAddButton.vue';
import * as CanvasHelpers from './canvasHelpers';
@@ -129,6 +200,7 @@ import {
XYPosition,
IPushDataExecutionFinished,
ITag,
INewWorkflowData,
IWorkflowTemplate,
IExecutionsSummary,
IWorkflowToShare,
@@ -158,6 +230,7 @@ export default mixins(
workflowHelpers,
workflowRun,
newVersions,
globalLinkActions,
debounceHelper,
)
.extend({
@@ -168,6 +241,7 @@ export default mixins(
NodeCreator: () => import('@/components/Node/NodeCreator/NodeCreator.vue'),
NodeSettings,
Sticky,
CanvasAddButton,
NodeCreation: () => import('@/components/Node/NodeCreation.vue'),
},
errorCaptured: (err, vm, info) => {
@@ -182,7 +256,7 @@ export default mixins(
this.createNodeActive = false;
},
nodes: {
async handler(value, oldValue) {
async handler () {
// Load a workflow
let workflowId = null as string | null;
if (this.$route && this.$route.params.name) {
@@ -201,9 +275,14 @@ export default mixins(
},
deep: true,
},
containsTrigger(containsTrigger) {
// Re-center CanvasAddButton if there's no triggers
if (containsTrigger === false) this.setRecenteredCanvasAddButtonPosition(this.getNodeViewOffsetPosition);
else this.tryToAddWelcomeSticky();
},
},
async beforeRouteLeave(to, from, next) {
this.$store.commit('setSubworkflowExecutionError', null);
const result = this.$store.getters.getStateIsDirty;
if (result) {
const confirmModal = await this.confirmModal(
@@ -257,6 +336,12 @@ export default mixins(
isDemo(): boolean {
return this.$route.name === VIEWS.DEMO;
},
isExecutionView(): boolean {
return this.$route.name === VIEWS.EXECUTION;
},
showCanvasAddButton(): boolean {
return this.loadingService === null && !this.containsTrigger && !this.isDemo && !this.isExecutionView;
},
lastSelectedNode(): INodeUi | null {
return this.$store.getters.lastSelectedNode;
},
@@ -275,14 +360,19 @@ export default mixins(
return this.$locale.baseText('nodeView.runButtonText.executingWorkflow');
},
workflowStyle(): object {
const offsetPosition = this.$store.getters.getNodeViewOffsetPosition;
const offsetPosition = this.getNodeViewOffsetPosition;
return {
left: offsetPosition[0] + 'px',
top: offsetPosition[1] + 'px',
};
},
canvasAddButtonStyle(): object {
return {
'pointer-events': this.createNodeActive ? 'none' : 'all',
};
},
backgroundStyle(): object {
return CanvasHelpers.getBackgroundStyles(this.nodeViewScale, this.$store.getters.getNodeViewOffsetPosition);
return CanvasHelpers.getBackgroundStyles(this.nodeViewScale, this.getNodeViewOffsetPosition);
},
workflowClasses() {
const returnClasses = [];
@@ -305,6 +395,26 @@ export default mixins(
workflowRunning(): boolean {
return this.$store.getters.isActionActive('workflowRunning');
},
allTriggersDisabled(): boolean {
const disabledTriggerNodes = this.triggerNodes.filter(node => node.disabled);
return disabledTriggerNodes.length === this.triggerNodes.length;
},
triggerNodes(): INodeUi[] {
return this.nodes.filter(node =>
node.type === START_NODE_TYPE ||
this.$store.getters['nodeTypes/isTriggerNode'](node.type),
);
},
containsTrigger(): boolean {
return this.triggerNodes.length > 0;
},
isExecutionDisabled(): boolean {
return !this.containsTrigger || this.allTriggersDisabled;
},
getNodeViewOffsetPosition(): XYPosition {
return this.$store.getters.getNodeViewOffsetPosition;
},
},
data() {
return {
@@ -325,6 +435,9 @@ export default mixins(
dropPrevented: false,
renamingActive: false,
showStickyButton: false,
showTriggerMissingTooltip: false,
canvasAddButtonPosition: [1, 1] as XYPosition,
workflowData: null as INewWorkflowData | null,
};
},
beforeDestroy() {
@@ -333,8 +446,12 @@ export default mixins(
// could add up with them registred multiple times
document.removeEventListener('keydown', this.keyDown);
document.removeEventListener('keyup', this.keyUp);
this.unregisterCustomAction('showNodeCreator');
},
methods: {
showTriggerMissingToltip(isVisible: boolean) {
this.showTriggerMissingTooltip = isVisible;
},
onRunNode(nodeName: string, source: string) {
const node = this.$store.getters.getNodeByName(nodeName);
const telemetryPayload = {
@@ -358,6 +475,25 @@ export default mixins(
this.runWorkflow();
},
onRunContainerClick() {
if (this.containsTrigger && !this.allTriggersDisabled) return;
const message = this.containsTrigger && this.allTriggersDisabled
? this.$locale.baseText('nodeView.addOrEnableTriggerNode')
: this.$locale.baseText('nodeView.addATriggerNodeFirst');
this.registerCustomAction('showNodeCreator', () => this.showTriggerCreator('no_trigger_execution_tooltip'));
const notice = this.$showMessage({
type: 'info',
title: this.$locale.baseText('nodeView.cantExecuteNoTrigger'),
message,
duration: 3000,
onClick: () => setTimeout(() => {
// Close the creator panel if user clicked on the link
if(this.createNodeActive) notice.close();
}, 0),
});
},
clearExecutionData() {
this.$store.commit('setWorkflowExecutionData', null);
this.updateNodesExecutionIssues();
@@ -436,6 +572,13 @@ export default mixins(
const saved = await this.saveCurrentWorkflow();
if (saved) this.$store.dispatch('settings/fetchPromptsData');
},
showTriggerCreator(source: string) {
if(this.createNodeActive) return;
this.$store.commit('nodeCreator/setSelectedType', TRIGGER_NODE_FILTER);
this.$store.commit('nodeCreator/setShowScrim', true);
this.onToggleNodeCreator({ source, createNodeActive: true });
this.$nextTick(() => this.$store.commit('nodeCreator/setShowTabs', false));
},
async openExecution(executionId: string) {
this.resetWorkspace();
@@ -560,7 +703,7 @@ export default mixins(
this.$router.replace({ name: VIEWS.NEW_WORKFLOW, query: { templateId } });
await this.addNodes(data.workflow.nodes, data.workflow.connections);
await this.$store.dispatch('workflows/getNewWorkflowData', data.name);
this.workflowData = await this.$store.dispatch('workflows/getNewWorkflowData', data.name);
this.$nextTick(() => {
this.zoomToFit();
this.$store.commit('setStateDirty', true);
@@ -999,21 +1142,21 @@ export default mixins(
},
resetZoom() {
const { scale, offset } = CanvasHelpers.scaleReset({ scale: this.nodeViewScale, offset: this.$store.getters.getNodeViewOffsetPosition });
const { scale, offset } = CanvasHelpers.scaleReset({ scale: this.nodeViewScale, offset: this.getNodeViewOffsetPosition });
this.setZoomLevel(scale);
this.$store.commit('setNodeViewOffsetPosition', { newOffset: offset });
},
zoomIn() {
const { scale, offset: [xOffset, yOffset] } = CanvasHelpers.scaleBigger({ scale: this.nodeViewScale, offset: this.$store.getters.getNodeViewOffsetPosition });
const { scale, offset: [xOffset, yOffset] } = CanvasHelpers.scaleBigger({ scale: this.nodeViewScale, offset: this.getNodeViewOffsetPosition });
this.setZoomLevel(scale);
this.$store.commit('setNodeViewOffsetPosition', { newOffset: [xOffset, yOffset] });
},
zoomOut() {
const { scale, offset: [xOffset, yOffset] } = CanvasHelpers.scaleSmaller({ scale: this.nodeViewScale, offset: this.$store.getters.getNodeViewOffsetPosition });
const { scale, offset: [xOffset, yOffset] } = CanvasHelpers.scaleSmaller({ scale: this.nodeViewScale, offset: this.getNodeViewOffsetPosition });
this.setZoomLevel(scale);
this.$store.commit('setNodeViewOffsetPosition', { newOffset: [xOffset, yOffset] });
@@ -1021,7 +1164,7 @@ export default mixins(
setZoomLevel(zoomLevel: number) {
this.nodeViewScale = zoomLevel; // important for background
const element = this.instance.getContainer() as HTMLElement;
const element = this.$refs.nodeView as HTMLElement;
if (!element) {
return;
}
@@ -1039,15 +1182,44 @@ export default mixins(
// @ts-ignore
this.instance.setZoom(zoomLevel);
},
setRecenteredCanvasAddButtonPosition (offset?: XYPosition) {
zoomToFit() {
const position = CanvasHelpers.getMidCanvasPosition(this.nodeViewScale, offset || [0, 0]);
position[0] -= CanvasHelpers.PLACEHOLDER_TRIGGER_NODE_SIZE / 2;
position[1] -= CanvasHelpers.PLACEHOLDER_TRIGGER_NODE_SIZE / 2;
this.canvasAddButtonPosition = CanvasHelpers.getNewNodePosition(this.nodes, position);
},
getPlaceholderTriggerNodeUI (): INodeUi {
this.setRecenteredCanvasAddButtonPosition();
return {
id: uuid(),
...CanvasHelpers.DEFAULT_PLACEHOLDER_TRIGGER_BUTTON,
position: this.canvasAddButtonPosition,
};
},
// Extend nodes with placeholder trigger button as NodeUI object
// with the centered position if canvas doesn't contains trigger node
getNodesWithPlaceholderNode(): INodeUi[] {
const nodes = this.$store.getters.allNodes as INodeUi[];
const extendedNodes = this.containsTrigger
? nodes
: [this.getPlaceholderTriggerNodeUI(), ...nodes];
return extendedNodes;
},
zoomToFit() {
const nodes = this.getNodesWithPlaceholderNode() as INodeUi[];
if (nodes.length === 0) { // some unknown workflow executions
return;
}
const { zoomLevel, offset } = CanvasHelpers.getZoomToFit(nodes, !this.isDemo);
const { zoomLevel, offset } = CanvasHelpers.getZoomToFit(nodes);
this.setZoomLevel(zoomLevel);
this.$store.commit('setNodeViewOffsetPosition', { newOffset: offset });
@@ -1313,7 +1485,6 @@ export default mixins(
const nodeTypeName = event.dataTransfer.getData('nodeTypeName');
if (nodeTypeName) {
const mousePosition = this.getMousePositionWithinNodeView(event);
const sidebarOffset = this.sidebarMenuCollapsed ? CanvasHelpers.SIDEBAR_WIDTH : CanvasHelpers.SIDEBAR_WIDTH_EXPANDED;
this.addNode(nodeTypeName, {
position: [
@@ -1460,7 +1631,7 @@ export default mixins(
const lastSelectedNode = this.lastSelectedNode;
if (options.position) {
newNodeData.position = CanvasHelpers.getNewNodePosition(this.nodes, options.position);
newNodeData.position = CanvasHelpers.getNewNodePosition(this.getNodesWithPlaceholderNode(), options.position);
} else if (lastSelectedNode) {
const lastSelectedConnection = this.lastSelectedConnection;
if (lastSelectedConnection) { // set when injecting into a connection
@@ -1499,8 +1670,14 @@ export default mixins(
);
}
} else {
// If no node is active find a free spot
newNodeData.position = CanvasHelpers.getNewNodePosition(this.nodes, this.lastClickPosition);
// If added node is a trigger and it's the first one added to the canvas
// we place it at canvasAddButtonPosition to replace the canvas add button
const position = this.$store.getters['nodeTypes/isTriggerNode'](nodeTypeName) && !this.containsTrigger
? this.canvasAddButtonPosition
// If no node is active find a free spot
: this.lastClickPosition as XYPosition;
newNodeData.position = CanvasHelpers.getNewNodePosition(this.nodes, position);
}
@@ -1944,48 +2121,41 @@ export default mixins(
},
async newWorkflow(): Promise<void> {
await this.resetWorkspace();
const newWorkflow = await this.$store.dispatch('workflows/getNewWorkflowData');
this.workflowData = await this.$store.dispatch('workflows/getNewWorkflowData');
this.$store.commit('setStateDirty', false);
await this.addNodes([{
id: uuid(),
...CanvasHelpers.DEFAULT_START_NODE,
}]);
this.nodeSelectedByName(CanvasHelpers.DEFAULT_START_NODE.name, false);
this.$store.commit('setStateDirty', false);
this.setZoomLevel(1);
const flagAvailable = window.posthog !== undefined && window.posthog.getFeatureFlag !== undefined;
if (flagAvailable && window.posthog.getFeatureFlag('welcome-note') === 'test') {
setTimeout(() => {
this.$store.commit('setNodeViewOffsetPosition', { newOffset: [0, 0] });
// For novice users (onboardingFlowEnabled == true)
// Inject welcome sticky note and zoom to fit
if (newWorkflow.onboardingFlowEnabled && !this.isReadOnly) {
this.$nextTick(async () => {
await this.addNodes([
{
id: uuid(),
...CanvasHelpers.WELCOME_STICKY_NODE,
parameters: {
// Use parameters from the template but add translated content
...CanvasHelpers.WELCOME_STICKY_NODE.parameters,
content: this.$locale.baseText('onboardingWorkflow.stickyContent'),
},
},
]);
this.zoomToFit();
this.$telemetry.track('welcome note inserted');
});
}
}, 0);
}
this.zoomToFit();
},
tryToAddWelcomeSticky: once(async function(this: any) {
const newWorkflow = this.workflowData;
const flagAvailable = window.posthog !== undefined && window.posthog.getFeatureFlag !== undefined;
if (flagAvailable && window.posthog.getFeatureFlag('welcome-note') === 'test') {
// For novice users (onboardingFlowEnabled == true)
// Inject welcome sticky note and zoom to fit
if (newWorkflow?.onboardingFlowEnabled && !this.isReadOnly) {
const collisionPadding = CanvasHelpers.GRID_SIZE + CanvasHelpers.NODE_SIZE;
// Position the welcome sticky left to the added trigger node
let position: XYPosition = [...(this.triggerNodes[0].position as XYPosition)];
position[0] -= CanvasHelpers.WELCOME_STICKY_NODE.parameters.width + (CanvasHelpers.GRID_SIZE * 4);
position = CanvasHelpers.getNewNodePosition(this.nodes, position, [collisionPadding, collisionPadding]);
await this.addNodes([{
id: uuid(),
...CanvasHelpers.WELCOME_STICKY_NODE,
parameters: {
// Use parameters from the template but add translated content
...CanvasHelpers.WELCOME_STICKY_NODE.parameters,
content: this.$locale.baseText('onboardingWorkflow.stickyContent'),
},
position,
}]);
this.$telemetry.track('welcome note inserted');
}
}
}),
async initView(): Promise<void> {
if (this.$route.params.action === 'workflowSave') {
// In case the workflow got saved we do not have to run init
@@ -2001,7 +2171,7 @@ export default mixins(
const templateId = this.$route.params.id;
await this.openWorkflowTemplate(templateId);
}
else if (this.$route.name === VIEWS.EXECUTION) {
else if (this.isExecutionView) {
// Load an execution
const executionId = this.$route.params.id;
await this.openExecution(executionId);
@@ -2057,7 +2227,7 @@ export default mixins(
document.addEventListener('keyup', this.keyUp);
window.addEventListener("beforeunload", (e) => {
if (this.isDemo) {
if (this.isDemo){
return;
}
else if (this.$store.getters.getStateIsDirty === true) {
@@ -2368,7 +2538,7 @@ export default mixins(
}
// "requiredNodeTypes" are also defined in cli/commands/run.ts
const requiredNodeTypes = [START_NODE_TYPE];
const requiredNodeTypes: string[] = [];
if (requiredNodeTypes.includes(node.type)) {
// The node is of the required type so check first
@@ -2921,7 +3091,7 @@ export default mixins(
this.$store.commit('setActiveWorkflows', activeWorkflows);
},
async loadNodeTypes(): Promise<void> {
this.$store.dispatch('nodeTypes/getNodeTypes');
await this.$store.dispatch('nodeTypes/getNodeTypes');
},
async loadCredentialTypes(): Promise<void> {
await this.$store.dispatch('credentials/fetchCredentialTypes', true);
@@ -3017,6 +3187,11 @@ export default mixins(
});
},
onToggleNodeCreator({ source, createNodeActive }: { source?: string; createNodeActive: boolean }) {
if (createNodeActive === this.createNodeActive) return;
// Default to the trigger tab in node creator if there's no trigger node yet
if (!this.containsTrigger) this.$store.commit('nodeCreator/setSelectedType', TRIGGER_NODE_FILTER);
this.createNodeActive = createNodeActive;
this.$externalHooks().run('nodeView.createNodeActiveChanged', { source, createNodeActive });
this.$telemetry.trackNodesPanel('nodeView.createNodeActiveChanged', { source, createNodeActive, workflow_id: this.$store.getters.workflowId });
@@ -3140,7 +3315,7 @@ export default mixins(
color: #444;
padding-right: 5px;
&.expanded {
&:not(.demo-zoom-menu).expanded {
left: $sidebar-expanded-width + $--zoom-menu-margin;
}
@@ -3175,6 +3350,7 @@ export default mixins(
background-color: var(--color-canvas-background);
width: 100%;
height: 100%;
position: relative;
}
.node-view-wrapper {
@@ -3212,14 +3388,15 @@ export default mixins(
}
.workflow-execute-wrapper {
position: fixed;
line-height: 65px;
left: calc(50% - 150px);
bottom: 30px;
width: 300px;
text-align: center;
position: absolute;
display: flex;
justify-content: center;
left: 50%;
transform: translateX(-50%);
bottom: 110px;
width: auto;
>* {
> * {
margin-inline-end: 0.625rem;
}
}