mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 18:12:04 +00:00
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:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user