refactor(editor): Apply Prettier (no-changelog) (#4920)

*  Adjust `format` script

* 🔥 Remove exemption for `editor-ui`

* 🎨 Prettify

* 👕 Fix lint
This commit is contained in:
Iván Ovejero
2022-12-14 10:04:10 +01:00
committed by GitHub
parent bcde07e032
commit 5ca2148c7e
284 changed files with 19247 additions and 15540 deletions

View File

@@ -6,7 +6,7 @@ import Vue from 'vue';
import { debounce } from 'lodash';
export const copyPaste = Vue.extend({
data () {
data() {
return {
copyPasteElementsGotCreated: false,
hiddenInput: null as null | Element,
@@ -14,7 +14,7 @@ export const copyPaste = Vue.extend({
onBeforePaste: null as null | Function,
};
},
mounted () {
mounted() {
if (this.copyPasteElementsGotCreated === true) {
return;
}
@@ -49,8 +49,14 @@ export const copyPaste = Vue.extend({
// Code is mainly from
// https://www.lucidchart.com/techblog/2014/12/02/definitive-guide-copying-pasting-javascript/
const isSafari = navigator.appVersion.search('Safari') !== -1 && navigator.appVersion.search('Chrome') === -1 && navigator.appVersion.search('CrMo') === -1 && navigator.appVersion.search('CriOS') === -1;
const isIe = (navigator.userAgent.toLowerCase().indexOf('msie') !== -1 || navigator.userAgent.toLowerCase().indexOf('trident') !== -1);
const isSafari =
navigator.appVersion.search('Safari') !== -1 &&
navigator.appVersion.search('Chrome') === -1 &&
navigator.appVersion.search('CrMo') === -1 &&
navigator.appVersion.search('CriOS') === -1;
const isIe =
navigator.userAgent.toLowerCase().indexOf('msie') !== -1 ||
navigator.userAgent.toLowerCase().indexOf('trident') !== -1;
const hiddenInput = document.createElement('input');
hiddenInput.setAttribute('type', 'text');
@@ -69,7 +75,7 @@ export const copyPaste = Vue.extend({
ieClipboardDiv.setAttribute('contenteditable', 'true');
document.body.append(ieClipboardDiv);
this.onBeforePaste = () => {
this.onBeforePaste = () => {
// @ts-ignore
if (hiddenInput.is(':focus')) {
this.focusIeClipboardDiv(ieClipboardDiv as HTMLDivElement);
@@ -80,7 +86,7 @@ export const copyPaste = Vue.extend({
}
let userInput = '';
const hiddenInputListener = (text: string) => { };
const hiddenInputListener = (text: string) => {};
hiddenInput.addEventListener('input', (e) => {
const value = hiddenInput.value;
@@ -91,52 +97,65 @@ export const copyPaste = Vue.extend({
// the input event, so we update the input area after the event is done being processed
if (isSafari) {
hiddenInput.focus();
setTimeout(() => { this.focusHiddenArea(hiddenInput); }, 0);
setTimeout(() => {
this.focusHiddenArea(hiddenInput);
}, 0);
} else {
this.focusHiddenArea(hiddenInput);
}
});
this.onPaste = debounce((e) => {
const event = 'paste';
// Check if the event got emitted from a message box or from something
// else which should ignore the copy/paste
// @ts-ignore
const path = e.path || (e.composedPath && e.composedPath());
for (let index = 0; index < path.length; index++) {
if (path[index].className && typeof path[index].className === 'string' && (
path[index].className.includes('el-message-box') || path[index].className.includes('ignore-key-press')
)) {
return;
}
}
if (ieClipboardDiv !== null) {
this.ieClipboardEvent(event, ieClipboardDiv);
} else {
this.standardClipboardEvent(event, e as ClipboardEvent);
this.onPaste = debounce(
(e) => {
const event = 'paste';
// Check if the event got emitted from a message box or from something
// else which should ignore the copy/paste
// @ts-ignore
if (!document.activeElement || (document.activeElement && ['textarea', 'text', 'email', 'password'].indexOf(document.activeElement.type) === -1)) {
// That it still allows to paste into text, email, password & textarea-fields we
// check if we can identify the active element and if so only
// run it if something else is selected.
this.focusHiddenArea(hiddenInput);
e.preventDefault();
const path = e.path || (e.composedPath && e.composedPath());
for (let index = 0; index < path.length; index++) {
if (
path[index].className &&
typeof path[index].className === 'string' &&
(path[index].className.includes('el-message-box') ||
path[index].className.includes('ignore-key-press'))
) {
return;
}
}
}
}, 1000, { leading: true });
if (ieClipboardDiv !== null) {
this.ieClipboardEvent(event, ieClipboardDiv);
} else {
this.standardClipboardEvent(event, e as ClipboardEvent);
// @ts-ignore
if (
!document.activeElement ||
(document.activeElement &&
['textarea', 'text', 'email', 'password'].indexOf(document.activeElement.type) === -1)
) {
// That it still allows to paste into text, email, password & textarea-fields we
// check if we can identify the active element and if so only
// run it if something else is selected.
this.focusHiddenArea(hiddenInput);
e.preventDefault();
}
}
},
1000,
{ leading: true },
);
// Set clipboard event listeners on the document.
// @ts-ignore
document.addEventListener('paste', this.onPaste);
},
methods: {
receivedCopyPasteData (plainTextData: string, event?: ClipboardEvent): void {
receivedCopyPasteData(plainTextData: string, event?: ClipboardEvent): void {
// THIS HAS TO BE DEFINED IN COMPONENT!
},
// For every browser except IE, we can easily get and set data on the clipboard
standardClipboardEvent (clipboardEventName: string, event: ClipboardEvent) {
standardClipboardEvent(clipboardEventName: string, event: ClipboardEvent) {
const clipboardData = event.clipboardData;
if (clipboardData !== null && clipboardEventName === 'paste') {
const clipboardText = clipboardData.getData('text/plain');
@@ -145,7 +164,7 @@ export const copyPaste = Vue.extend({
},
// For IE, we can get/set Text or URL just as we normally would
ieClipboardEvent (clipboardEventName: string, ieClipboardDiv: HTMLDivElement) {
ieClipboardEvent(clipboardEventName: string, ieClipboardDiv: HTMLDivElement) {
// @ts-ignore
const clipboardData = window.clipboardData;
if (clipboardEventName === 'paste') {
@@ -157,11 +176,11 @@ export const copyPaste = Vue.extend({
},
// Focuses an element to be ready for copy/paste (used exclusively for IE)
focusIeClipboardDiv (ieClipboardDiv: HTMLDivElement) {
focusIeClipboardDiv(ieClipboardDiv: HTMLDivElement) {
ieClipboardDiv.focus();
const range = document.createRange();
// @ts-ignore
range.selectNodeContents((ieClipboardDiv.get(0)));
range.selectNodeContents(ieClipboardDiv.get(0));
const selection = window.getSelection();
if (selection !== null) {
selection.removeAllRanges();
@@ -169,7 +188,7 @@ export const copyPaste = Vue.extend({
}
},
focusHiddenArea (hiddenInput: HTMLInputElement) {
focusHiddenArea(hiddenInput: HTMLInputElement) {
// In order to ensure that the browser will fire clipboard events, we always need to have something selected
hiddenInput.value = ' ';
hiddenInput.focus();
@@ -179,7 +198,7 @@ export const copyPaste = Vue.extend({
/**
* Copies given data to clipboard
*/
copyToClipboard (value: string): void {
copyToClipboard(value: string): void {
// FROM: https://hackernoon.com/copying-text-to-clipboard-with-javascript-df4d4988697f
const element = document.createElement('textarea'); // Create a <textarea> element
element.value = value; // Set its value to the string that you want copied
@@ -193,9 +212,10 @@ export const copyPaste = Vue.extend({
return;
}
const selected = selection.rangeCount > 0 // Check if there is any content selected previously
? selection.getRangeAt(0) // Store selection if found
: false; // Mark as false to know no selection existed before
const selected =
selection.rangeCount > 0 // Check if there is any content selected previously
? selection.getRangeAt(0) // Store selection if found
: false; // Mark as false to know no selection existed before
element.select(); // Select the <textarea> content
document.execCommand('copy'); // Copy - only works as a result of a user action (e.g. click events)
document.body.removeChild(element); // Remove the <textarea> element
@@ -205,7 +225,6 @@ export const copyPaste = Vue.extend({
selection.addRange(selected); // Restore the original selection
}
},
},
beforeDestroy() {
if (this.hiddenInput) {

View File

@@ -2,20 +2,25 @@ import { debounce } from 'lodash';
import Vue from 'vue';
export const debounceHelper = Vue.extend({
data () {
data() {
return {
debouncedFunctions: [] as any[], // tslint:disable-line:no-any
};
},
methods: {
async callDebounced (...inputParameters: any[]): Promise<void> { // tslint:disable-line:no-any
// tslint:disable-next-line:no-any
async callDebounced(...inputParameters: any[]): Promise<void> {
const functionName = inputParameters.shift() as string;
const { trailing, debounceTime } = inputParameters.shift();
const { trailing, debounceTime } = inputParameters.shift();
// @ts-ignore
if (this.debouncedFunctions[functionName] === undefined) {
// @ts-ignore
this.debouncedFunctions[functionName] = debounce(this[functionName], debounceTime, trailing ? { trailing } : { leading: true } );
this.debouncedFunctions[functionName] = debounce(
this[functionName],
debounceTime,
trailing ? { trailing } : { leading: true },
);
}
// @ts-ignore
await this.debouncedFunctions[functionName].apply(this, inputParameters);

View File

@@ -1,8 +1,9 @@
import Vue from 'vue';
function broadcast(componentName: string, eventName: string, params: any) { // tslint:disable-line:no-any
// tslint:disable-next-line:no-any
function broadcast(componentName: string, eventName: string, params: any) {
// @ts-ignore
(this as Vue).$children.forEach(child => {
(this as Vue).$children.forEach((child) => {
const name = child.$options.name;
if (name === componentName) {
@@ -18,7 +19,8 @@ function broadcast(componentName: string, eventName: string, params: any) { // t
export default Vue.extend({
methods: {
$dispatch(componentName: string, eventName: string, params: any) { // tslint:disable-line:no-any
// tslint:disable-next-line:no-any
$dispatch(componentName: string, eventName: string, params: any) {
let parent = this.$parent || this.$root;
let name = parent.$options.name;
@@ -35,7 +37,8 @@ export default Vue.extend({
parent.$emit.apply(parent, [eventName].concat(params));
}
},
$broadcast(componentName: string, eventName: string, params: any) { // tslint:disable-line:no-any
// tslint:disable-next-line:no-any
$broadcast(componentName: string, eventName: string, params: any) {
broadcast.call(this, componentName, eventName, params);
},
},

View File

@@ -1,9 +1,9 @@
import { IExecutionsSummary } from "@/Interface";
import { useWorkflowsStore } from "@/stores/workflows";
import dateFormat from "dateformat";
import { mapStores } from "pinia";
import mixins from "vue-typed-mixins";
import { genericHelpers } from "./genericHelpers";
import { IExecutionsSummary } from '@/Interface';
import { useWorkflowsStore } from '@/stores/workflows';
import dateFormat from 'dateformat';
import { mapStores } from 'pinia';
import mixins from 'vue-typed-mixins';
import { genericHelpers } from './genericHelpers';
export interface IExecutionUIData {
name: string;
@@ -14,16 +14,14 @@ export interface IExecutionUIData {
export const executionHelpers = mixins(genericHelpers).extend({
computed: {
...mapStores(
useWorkflowsStore,
),
...mapStores(useWorkflowsStore),
executionId(): string {
return this.$route.params.executionId;
},
workflowName (): string {
workflowName(): string {
return this.workflowsStore.workflowName;
},
currentWorkflow (): string {
currentWorkflow(): string {
return this.$route.params.name || this.workflowsStore.workflowId;
},
executions(): IExecutionsSummary[] {
@@ -48,18 +46,27 @@ export const executionHelpers = mixins(genericHelpers).extend({
} else if (execution.stoppedAt === undefined) {
status.name = 'running';
status.label = this.$locale.baseText('executionsList.running');
status.runningTime = this.displayTimer(new Date().getTime() - new Date(execution.startedAt).getTime(), true);
status.runningTime = this.displayTimer(
new Date().getTime() - new Date(execution.startedAt).getTime(),
true,
);
} else if (execution.finished) {
status.name = 'success';
status.label = this.$locale.baseText('executionsList.succeeded');
if (execution.stoppedAt) {
status.runningTime = this.displayTimer(new Date(execution.stoppedAt).getTime() - new Date(execution.startedAt).getTime(), true);
status.runningTime = this.displayTimer(
new Date(execution.stoppedAt).getTime() - new Date(execution.startedAt).getTime(),
true,
);
}
} else if (execution.stoppedAt !== null) {
status.name = 'error';
status.label = this.$locale.baseText('executionsList.error');
if (execution.stoppedAt) {
status.runningTime = this.displayTimer(new Date(execution.stoppedAt).getTime() - new Date(execution.startedAt).getTime(), true);
status.runningTime = this.displayTimer(
new Date(execution.stoppedAt).getTime() - new Date(execution.startedAt).getTime(),
true,
);
}
}

View File

@@ -6,15 +6,14 @@ import Vue from 'vue';
declare global {
interface Window {
n8nExternalHooks?: Record<string, Record<string, Array<(store: Store, metadata?: IDataObject) => Promise<void>>>>;
n8nExternalHooks?: Record<
string,
Record<string, Array<(store: Store, metadata?: IDataObject) => Promise<void>>>
>;
}
}
export async function runExternalHook(
eventName: string,
store: Store,
metadata?: IDataObject,
) {
export async function runExternalHook(eventName: string, store: Store, metadata?: IDataObject) {
if (!window.n8nExternalHooks) {
return;
}

View File

@@ -4,18 +4,18 @@ import { VIEWS } from '@/constants';
import mixins from 'vue-typed-mixins';
export const genericHelpers = mixins(showMessage).extend({
data () {
data() {
return {
loadingService: null as any | null, // tslint:disable-line:no-any
};
},
computed: {
isReadOnly (): boolean {
isReadOnly(): boolean {
return ![VIEWS.WORKFLOW, VIEWS.NEW_WORKFLOW].includes(this.$route.name as VIEWS);
},
},
methods: {
displayTimer (msPassed: number, showMs = false): string {
displayTimer(msPassed: number, showMs = false): string {
if (msPassed < 60000) {
if (!showMs) {
return `${Math.floor(msPassed / 1000)} ${this.$locale.baseText('genericHelpers.sec')}`;
@@ -26,11 +26,11 @@ export const genericHelpers = mixins(showMessage).extend({
const secondsPassed = Math.floor(msPassed / 1000);
const minutesPassed = Math.floor(secondsPassed / 60);
const secondsLeft = (secondsPassed - (minutesPassed * 60)).toString().padStart(2, '0');
const secondsLeft = (secondsPassed - minutesPassed * 60).toString().padStart(2, '0');
return `${minutesPassed}:${secondsLeft} ${this.$locale.baseText('genericHelpers.min')}`;
},
editAllowedCheck (): boolean {
editAllowedCheck(): boolean {
if (this.isReadOnly) {
this.$showMessage({
// title: 'Workflow can not be changed!',
@@ -45,25 +45,23 @@ export const genericHelpers = mixins(showMessage).extend({
return true;
},
startLoading (text?: string) {
startLoading(text?: string) {
if (this.loadingService !== null) {
return;
}
// @ts-ignore
this.loadingService = this.$loading(
{
lock: true,
text: text || this.$locale.baseText('genericHelpers.loading'),
spinner: 'el-icon-loading',
background: 'rgba(255, 255, 255, 0.8)',
},
);
this.loadingService = this.$loading({
lock: true,
text: text || this.$locale.baseText('genericHelpers.loading'),
spinner: 'el-icon-loading',
background: 'rgba(255, 255, 255, 0.8)',
});
},
setLoadingText (text: string) {
setLoadingText(text: string) {
this.loadingService.text = text;
},
stopLoading () {
stopLoading() {
if (this.loadingService !== null) {
this.loadingService.close();
this.loadingService = null;

View File

@@ -16,12 +16,7 @@ const UNDO_REDO_DEBOUNCE_INTERVAL = 100;
export const historyHelper = mixins(debounceHelper, deviceSupportHelpers).extend({
computed: {
...mapStores(
useNDVStore,
useHistoryStore,
useUIStore,
useWorkflowsStore,
),
...mapStores(useNDVStore, useHistoryStore, useUIStore, useWorkflowsStore),
isNDVOpen(): boolean {
return this.ndvStore.activeNodeName !== null;
},
@@ -41,9 +36,15 @@ export const historyHelper = mixins(debounceHelper, deviceSupportHelpers).extend
event.preventDefault();
if (!this.isNDVOpen) {
if (event.shiftKey) {
this.callDebounced('redo', { debounceTime: UNDO_REDO_DEBOUNCE_INTERVAL, trailing: true });
this.callDebounced('redo', {
debounceTime: UNDO_REDO_DEBOUNCE_INTERVAL,
trailing: true,
});
} else {
this.callDebounced('undo', { debounceTime: UNDO_REDO_DEBOUNCE_INTERVAL, trailing: true });
this.callDebounced('undo', {
debounceTime: UNDO_REDO_DEBOUNCE_INTERVAL,
trailing: true,
});
}
} else if (!event.shiftKey) {
this.trackUndoAttempt(event);
@@ -98,11 +99,14 @@ export const historyHelper = mixins(debounceHelper, deviceSupportHelpers).extend
}
this.trackCommand(command, 'redo');
},
trackCommand(command: Undoable, type: 'undo'|'redo'): void {
trackCommand(command: Undoable, type: 'undo' | 'redo'): void {
if (command instanceof Command) {
this.$telemetry.track(`User hit ${type}`, { commands_length: 1, commands: [ command.name ] });
this.$telemetry.track(`User hit ${type}`, { commands_length: 1, commands: [command.name] });
} else if (command instanceof BulkCommand) {
this.$telemetry.track(`User hit ${type}`, { commands_length: command.commands.length, commands: command.commands.map(c => c.name) });
this.$telemetry.track(`User hit ${type}`, {
commands_length: command.commands.length,
commands: command.commands.map((c) => c.name),
});
}
},
trackUndoAttempt(event: KeyboardEvent) {

View File

@@ -7,31 +7,32 @@ import { VIEWS } from '@/constants';
import { mapStores } from 'pinia';
import { useUIStore } from '@/stores/ui';
import { useWorkflowsStore } from '@/stores/workflows';
import { getMousePosition, getRelativePosition, HEADER_HEIGHT, SIDEBAR_WIDTH, SIDEBAR_WIDTH_EXPANDED } from '@/utils/nodeViewUtils';
import {
getMousePosition,
getRelativePosition,
HEADER_HEIGHT,
SIDEBAR_WIDTH,
SIDEBAR_WIDTH_EXPANDED,
} from '@/utils/nodeViewUtils';
export const mouseSelect = mixins(
deviceSupportHelpers,
).extend({
data () {
export const mouseSelect = mixins(deviceSupportHelpers).extend({
data() {
return {
selectActive: false,
selectBox: document.createElement('span'),
};
},
mounted () {
mounted() {
this.createSelectBox();
},
computed: {
...mapStores(
useUIStore,
useWorkflowsStore,
),
isDemo (): boolean {
...mapStores(useUIStore, useWorkflowsStore),
isDemo(): boolean {
return this.$route.name === VIEWS.DEMO;
},
},
methods: {
createSelectBox () {
createSelectBox() {
this.selectBox.id = 'select-box';
this.selectBox.style.margin = '0px auto';
this.selectBox.style.border = '2px dotted #FF0000';
@@ -45,7 +46,7 @@ export const mouseSelect = mixins(
const nodeViewEl = this.$el.querySelector('#node-view') as HTMLDivElement;
nodeViewEl.appendChild(this.selectBox);
},
isCtrlKeyPressed (e: MouseEvent | KeyboardEvent): boolean {
isCtrlKeyPressed(e: MouseEvent | KeyboardEvent): boolean {
if (this.isTouchDevice === true) {
return true;
}
@@ -54,16 +55,25 @@ export const mouseSelect = mixins(
}
return e.ctrlKey;
},
getMousePositionWithinNodeView (event: MouseEvent | TouchEvent): XYPosition {
getMousePositionWithinNodeView(event: MouseEvent | TouchEvent): XYPosition {
const [x, y] = getMousePosition(event);
const sidebarOffset = this.isDemo ? 0 : this.uiStore.sidebarMenuCollapsed ? SIDEBAR_WIDTH : SIDEBAR_WIDTH_EXPANDED;
const sidebarOffset = this.isDemo
? 0
: this.uiStore.sidebarMenuCollapsed
? SIDEBAR_WIDTH
: SIDEBAR_WIDTH_EXPANDED;
const headerOffset = this.isDemo ? 0 : HEADER_HEIGHT;
// @ts-ignore
return getRelativePosition(x - sidebarOffset, y - headerOffset, this.nodeViewScale, this.uiStore.nodeViewOffsetPosition);
return getRelativePosition(
x - sidebarOffset,
y - headerOffset,
this.nodeViewScale,
this.uiStore.nodeViewOffsetPosition,
);
},
showSelectBox (event: MouseEvent) {
showSelectBox(event: MouseEvent) {
const [x, y] = this.getMousePositionWithinNodeView(event);
this.selectBox = Object.assign(this.selectBox, {x, y});
this.selectBox = Object.assign(this.selectBox, { x, y });
// @ts-ignore
this.selectBox.style.left = this.selectBox.x + 'px';
@@ -73,7 +83,7 @@ export const mouseSelect = mixins(
this.selectActive = true;
},
updateSelectBox (event: MouseEvent) {
updateSelectBox(event: MouseEvent) {
const selectionBox = this.getSelectionBox(event);
this.selectBox.style.left = selectionBox.x + 'px';
this.selectBox.style.top = selectionBox.y + 'px';
@@ -81,7 +91,7 @@ export const mouseSelect = mixins(
this.selectBox.style.width = selectionBox.width + 'px';
this.selectBox.style.height = selectionBox.height + 'px';
},
hideSelectBox () {
hideSelectBox() {
this.selectBox.style.visibility = 'hidden';
// @ts-ignore
this.selectBox.x = 0;
@@ -94,7 +104,7 @@ export const mouseSelect = mixins(
this.selectActive = false;
},
getSelectionBox (event: MouseEvent) {
getSelectionBox(event: MouseEvent) {
const [x, y] = this.getMousePositionWithinNodeView(event);
return {
// @ts-ignore
@@ -107,17 +117,23 @@ export const mouseSelect = mixins(
height: Math.abs(y - this.selectBox.y),
};
},
getNodesInSelection (event: MouseEvent): INodeUi[] {
getNodesInSelection(event: MouseEvent): INodeUi[] {
const returnNodes: INodeUi[] = [];
const selectionBox = this.getSelectionBox(event);
// Go through all nodes and check if they are selected
this.workflowsStore.allNodes.forEach((node: INodeUi) => {
// TODO: Currently always uses the top left corner for checking. Should probably use the center instead
if (node.position[0] < selectionBox.x || node.position[0] > (selectionBox.x + selectionBox.width)) {
if (
node.position[0] < selectionBox.x ||
node.position[0] > selectionBox.x + selectionBox.width
) {
return;
}
if (node.position[1] < selectionBox.y || node.position[1] > (selectionBox.y + selectionBox.height)) {
if (
node.position[1] < selectionBox.y ||
node.position[1] > selectionBox.y + selectionBox.height
) {
return;
}
returnNodes.push(node);
@@ -125,7 +141,7 @@ export const mouseSelect = mixins(
return returnNodes;
},
mouseDownMouseSelect (e: MouseEvent) {
mouseDownMouseSelect(e: MouseEvent) {
if (this.isCtrlKeyPressed(e) === true) {
// We only care about it when the ctrl key is not pressed at the same time.
// So we exit when it is pressed.
@@ -141,7 +157,7 @@ export const mouseSelect = mixins(
// @ts-ignore // Leave like this. Do not add a anonymous function because then remove would not work anymore
this.$el.addEventListener('mousemove', this.mouseMoveSelect);
},
mouseUpMouseSelect (e: MouseEvent) {
mouseUpMouseSelect(e: MouseEvent) {
if (this.selectActive === false) {
if (this.isTouchDevice === true) {
// @ts-ignore
@@ -173,7 +189,7 @@ export const mouseSelect = mixins(
this.hideSelectBox();
},
mouseMoveSelect (e: MouseEvent) {
mouseMoveSelect(e: MouseEvent) {
if (e.buttons === 0) {
// Mouse button is not pressed anymore so stop selection mode
// Happens normally when mouse leave the view pressed and then
@@ -184,17 +200,17 @@ export const mouseSelect = mixins(
this.updateSelectBox(e);
},
nodeDeselected (node: INodeUi) {
nodeDeselected(node: INodeUi) {
this.uiStore.removeNodeFromSelection(node);
// @ts-ignore
this.instance.removeFromDragSelection(node.id);
},
nodeSelected (node: INodeUi) {
nodeSelected(node: INodeUi) {
this.uiStore.addSelectedNode(node);
// @ts-ignore
this.instance.addToDragSelection(node.id);
},
deselectAllNodes () {
deselectAllNodes() {
// @ts-ignore
this.instance.clearDragSelection();
this.uiStore.resetSelectedNodes();

View File

@@ -4,10 +4,8 @@ import { getMousePosition } from '@/utils/nodeViewUtils';
import { mapStores } from 'pinia';
import { useUIStore } from '@/stores/ui';
export const moveNodeWorkflow = mixins(
deviceSupportHelpers,
).extend({
data () {
export const moveNodeWorkflow = mixins(deviceSupportHelpers).extend({
data() {
return {
moveLastPosition: [0, 0],
};
@@ -16,7 +14,7 @@ export const moveNodeWorkflow = mixins(
...mapStores(useUIStore),
},
methods: {
moveWorkflow (e: MouseEvent) {
moveWorkflow(e: MouseEvent) {
const offsetPosition = this.uiStore.nodeViewOffsetPosition;
const [x, y] = getMousePosition(e);
@@ -29,7 +27,7 @@ export const moveNodeWorkflow = mixins(
this.moveLastPosition[0] = x;
this.moveLastPosition[1] = y;
},
mouseDownMoveWorkflow (e: MouseEvent) {
mouseDownMoveWorkflow(e: MouseEvent) {
if (this.isCtrlKeyPressed(e) === false) {
// We only care about it when the ctrl key is pressed at the same time.
// So we exit when it is not pressed.
@@ -51,7 +49,7 @@ export const moveNodeWorkflow = mixins(
// @ts-ignore
this.$el.addEventListener('mousemove', this.mouseMoveNodeWorkflow);
},
mouseUpMoveWorkflow (e: MouseEvent) {
mouseUpMoveWorkflow(e: MouseEvent) {
if (this.uiStore.nodeViewMoveInProgress === false) {
// If it is not active return directly.
// Else normal node dragging will not work.
@@ -65,7 +63,7 @@ export const moveNodeWorkflow = mixins(
// Nothing else to do. Simply leave the node view at the current offset
},
mouseMoveNodeWorkflow (e: MouseEvent) {
mouseMoveNodeWorkflow(e: MouseEvent) {
// @ts-ignore
if (e.target && !e.target.id.includes('node-view')) {
return;

View File

@@ -5,14 +5,9 @@ import { mapStores } from 'pinia';
import { useUIStore } from '@/stores/ui';
import { useVersionsStore } from '@/stores/versions';
export const newVersions = mixins(
showMessage,
).extend({
export const newVersions = mixins(showMessage).extend({
computed: {
...mapStores(
useUIStore,
useVersionsStore,
),
...mapStores(useUIStore, useVersionsStore),
},
methods: {
async checkForNewVersions() {

View File

@@ -1,47 +1,38 @@
import { PropType } from "vue";
import { PropType } from 'vue';
import mixins from 'vue-typed-mixins';
import { IJsPlumbInstance, IEndpointOptions, INodeUi, XYPosition } from '@/Interface';
import { deviceSupportHelpers } from '@/mixins/deviceSupportHelpers';
import { NO_OP_NODE_TYPE, STICKY_NODE_TYPE } from '@/constants';
import {
INodeTypeDescription,
} from 'n8n-workflow';
import { INodeTypeDescription } from 'n8n-workflow';
import { mapStores } from 'pinia';
import { useUIStore } from '@/stores/ui';
import { useWorkflowsStore } from "@/stores/workflows";
import { useNodeTypesStore } from "@/stores/nodeTypes";
import { useWorkflowsStore } from '@/stores/workflows';
import { useNodeTypesStore } from '@/stores/nodeTypes';
import * as NodeViewUtils from '@/utils/nodeViewUtils';
import { getStyleTokenValue } from "@/utils";
import { useHistoryStore } from "@/stores/history";
import { MoveNodeCommand } from "@/models/history";
import { getStyleTokenValue } from '@/utils';
import { useHistoryStore } from '@/stores/history';
import { MoveNodeCommand } from '@/models/history';
export const nodeBase = mixins(
deviceSupportHelpers,
).extend({
mounted () {
export const nodeBase = mixins(deviceSupportHelpers).extend({
mounted() {
// Initialize the node
if (this.data !== null) {
try {
this.__addNode(this.data);
} catch(error) {
} catch (error) {
// This breaks when new nodes are loaded into store but workflow tab is not currently active
// Shouldn't affect anything
}
}
},
computed: {
...mapStores(
useNodeTypesStore,
useUIStore,
useWorkflowsStore,
useHistoryStore,
),
data (): INodeUi | null {
...mapStores(useNodeTypesStore, useUIStore, useWorkflowsStore, useHistoryStore),
data(): INodeUi | null {
return this.workflowsStore.getNodeByName(this.name);
},
nodeId (): string {
return this.data?.id || '';
nodeId(): string {
return this.data?.id || '';
},
},
props: {
@@ -68,7 +59,7 @@ export const nodeBase = mixins(
},
},
methods: {
__addInputEndpoints (node: INodeUi, nodeTypeData: INodeTypeDescription) {
__addInputEndpoints(node: INodeUi, nodeTypeData: INodeTypeDescription) {
// Add Inputs
let index;
const indexData: {
@@ -85,14 +76,18 @@ export const nodeBase = mixins(
index = indexData[inputName];
// Get the position of the anchor depending on how many it has
const anchorPosition = NodeViewUtils.ANCHOR_POSITIONS.input[nodeTypeData.inputs.length][index];
const anchorPosition =
NodeViewUtils.ANCHOR_POSITIONS.input[nodeTypeData.inputs.length][index];
const newEndpointData: IEndpointOptions = {
uuid:NodeViewUtils. getInputEndpointUUID(this.nodeId, index),
uuid: NodeViewUtils.getInputEndpointUUID(this.nodeId, index),
anchor: anchorPosition,
maxConnections: -1,
endpoint: 'Rectangle',
endpointStyle: NodeViewUtils.getInputEndpointStyle(nodeTypeData, '--color-foreground-xdark'),
endpointStyle: NodeViewUtils.getInputEndpointStyle(
nodeTypeData,
'--color-foreground-xdark',
),
endpointHoverStyle: NodeViewUtils.getInputEndpointStyle(nodeTypeData, '--color-primary'),
isSource: false,
isTarget: !this.isReadOnly && nodeTypeData.inputs.length > 1, // only enabled for nodes with multiple inputs.. otherwise attachment handled by connectionDrag event in NodeView,
@@ -118,7 +113,7 @@ export const nodeBase = mixins(
}
const endpoint = this.instance.addEndpoint(this.nodeId, newEndpointData);
if(!Array.isArray(endpoint)) {
if (!Array.isArray(endpoint)) {
endpoint.__meta = {
nodeName: node.name,
nodeId: this.nodeId,
@@ -155,14 +150,18 @@ export const nodeBase = mixins(
index = indexData[inputName];
// Get the position of the anchor depending on how many it has
const anchorPosition = NodeViewUtils.ANCHOR_POSITIONS.output[nodeTypeData.outputs.length][index];
const anchorPosition =
NodeViewUtils.ANCHOR_POSITIONS.output[nodeTypeData.outputs.length][index];
const newEndpointData: IEndpointOptions = {
uuid: NodeViewUtils.getOutputEndpointUUID(this.nodeId, index),
anchor: anchorPosition,
maxConnections: -1,
endpoint: 'Dot',
endpointStyle: NodeViewUtils.getOutputEndpointStyle(nodeTypeData, '--color-foreground-xdark'),
endpointStyle: NodeViewUtils.getOutputEndpointStyle(
nodeTypeData,
'--color-foreground-xdark',
),
endpointHoverStyle: NodeViewUtils.getOutputEndpointStyle(nodeTypeData, '--color-primary'),
isSource: true,
isTarget: false,
@@ -174,7 +173,7 @@ export const nodeBase = mixins(
},
cssClass: 'dot-output-endpoint',
dragAllowedWhenFull: false,
dragProxy: ['Rectangle', {width: 1, height: 1, strokeWidth: 0}],
dragProxy: ['Rectangle', { width: 1, height: 1, strokeWidth: 0 }],
};
if (nodeTypeData.outputNames) {
@@ -184,15 +183,15 @@ export const nodeBase = mixins(
];
}
const endpoint = this.instance.addEndpoint(this.nodeId, {...newEndpointData});
if(!Array.isArray(endpoint)) {
endpoint.__meta = {
nodeName: node.name,
nodeId: this.nodeId,
index: i,
totalEndpoints: nodeTypeData.outputs.length,
};
}
const endpoint = this.instance.addEndpoint(this.nodeId, { ...newEndpointData });
if (!Array.isArray(endpoint)) {
endpoint.__meta = {
nodeName: node.name,
nodeId: this.nodeId,
index: i,
totalEndpoints: nodeTypeData.outputs.length,
};
}
if (!this.isReadOnly) {
const plusEndpointData: IEndpointOptions = {
@@ -223,11 +222,11 @@ export const nodeBase = mixins(
},
cssClass: 'plus-draggable-endpoint',
dragAllowedWhenFull: false,
dragProxy: ['Rectangle', {width: 1, height: 1, strokeWidth: 0}],
dragProxy: ['Rectangle', { width: 1, height: 1, strokeWidth: 0 }],
};
const plusEndpoint = this.instance.addEndpoint(this.nodeId, plusEndpointData);
if(!Array.isArray(plusEndpoint)) {
if (!Array.isArray(plusEndpoint)) {
plusEndpoint.__meta = {
nodeName: node.name,
nodeId: this.nodeId,
@@ -284,7 +283,7 @@ export const nodeBase = mixins(
moveNodes.push(this.data);
}
if(moveNodes.length > 1) {
if (moveNodes.length > 1) {
this.historyStore.startRecordingUndo();
}
// This does for some reason just get called once for the node that got clicked
@@ -312,12 +311,14 @@ export const nodeBase = mixins(
};
const oldPosition = node.position;
if (oldPosition[0] !== newNodePosition[0] || oldPosition[1] !== newNodePosition[1]) {
this.historyStore.pushCommandToUndo(new MoveNodeCommand(node.name, oldPosition, newNodePosition, this));
this.historyStore.pushCommandToUndo(
new MoveNodeCommand(node.name, oldPosition, newNodePosition, this),
);
this.workflowsStore.updateNodeProperties(updateInformation);
this.$emit('moved', node);
}
});
if(moveNodes.length > 1) {
if (moveNodes.length > 1) {
this.historyStore.stopRecordingUndo();
}
}
@@ -325,7 +326,7 @@ export const nodeBase = mixins(
filter: '.node-description, .node-description .node-name, .node-description .node-subtitle',
});
},
__addNode (node: INodeUi) {
__addNode(node: INodeUi) {
let nodeTypeData = this.nodeTypesStore.getNodeType(node.type, node.typeVersion);
if (!nodeTypeData) {
// If node type is not know use by default the base.noOp data to display it
@@ -343,11 +344,15 @@ export const nodeBase = mixins(
}
}
},
mouseLeftClick (e: MouseEvent) {
mouseLeftClick(e: MouseEvent) {
// @ts-ignore
const path = e.path || (e.composedPath && e.composedPath());
for (let index = 0; index < path.length; index++) {
if (path[index].className && typeof path[index].className === 'string' && path[index].className.includes('no-select-on-click')) {
if (
path[index].className &&
typeof path[index].className === 'string' &&
path[index].className.includes('no-select-on-click')
) {
return;
}
}

View File

@@ -38,7 +38,7 @@ import { get } from 'lodash';
import mixins from 'vue-typed-mixins';
import { isObjectLiteral } from '@/utils';
import {getCredentialPermissions} from "@/permissions";
import { getCredentialPermissions } from '@/permissions';
import { mapStores } from 'pinia';
import { useSettingsStore } from '@/stores/settings';
import { useUsersStore } from '@/stores/users';
@@ -46,467 +46,524 @@ import { useWorkflowsStore } from '@/stores/workflows';
import { useNodeTypesStore } from '@/stores/nodeTypes';
import { useCredentialsStore } from '@/stores/credentials';
export const nodeHelpers = mixins(
restApi,
)
.extend({
computed: {
...mapStores(
useCredentialsStore,
useHistoryStore,
useNodeTypesStore,
useSettingsStore,
useWorkflowsStore,
),
export const nodeHelpers = mixins(restApi).extend({
computed: {
...mapStores(
useCredentialsStore,
useHistoryStore,
useNodeTypesStore,
useSettingsStore,
useWorkflowsStore,
),
},
methods: {
hasProxyAuth(node: INodeUi): boolean {
return Object.keys(node.parameters).includes('nodeCredentialType');
},
methods: {
hasProxyAuth (node: INodeUi): boolean {
return Object.keys(node.parameters).includes('nodeCredentialType');
},
isCustomApiCallSelected (nodeValues: INodeParameters): boolean {
const { parameters } = nodeValues;
isCustomApiCallSelected(nodeValues: INodeParameters): boolean {
const { parameters } = nodeValues;
if (!isObjectLiteral(parameters)) return false;
if (!isObjectLiteral(parameters)) return false;
return (
parameters.resource !== undefined && parameters.resource.includes(CUSTOM_API_CALL_KEY) ||
parameters.operation !== undefined && parameters.operation.includes(CUSTOM_API_CALL_KEY)
);
},
return (
(parameters.resource !== undefined && parameters.resource.includes(CUSTOM_API_CALL_KEY)) ||
(parameters.operation !== undefined && parameters.operation.includes(CUSTOM_API_CALL_KEY))
);
},
// Returns the parameter value
getParameterValue (nodeValues: INodeParameters, parameterName: string, path: string) {
return get(
nodeValues,
path ? path + '.' + parameterName : parameterName,
);
},
// Returns the parameter value
getParameterValue(nodeValues: INodeParameters, parameterName: string, path: string) {
return get(nodeValues, path ? path + '.' + parameterName : parameterName);
},
// Returns if the given parameter should be displayed or not
displayParameter (nodeValues: INodeParameters, parameter: INodeProperties | INodeCredentialDescription, path: string, node: INodeUi | null) {
return NodeHelpers.displayParameterPath(nodeValues, parameter, path, node);
},
// Returns if the given parameter should be displayed or not
displayParameter(
nodeValues: INodeParameters,
parameter: INodeProperties | INodeCredentialDescription,
path: string,
node: INodeUi | null,
) {
return NodeHelpers.displayParameterPath(nodeValues, parameter, path, node);
},
// Returns all the issues of the node
getNodeIssues (nodeType: INodeTypeDescription | null, node: INodeUi, ignoreIssues?: string[]): INodeIssues | null {
const pinDataNodeNames = Object.keys(this.workflowsStore.getPinData || {});
// Returns all the issues of the node
getNodeIssues(
nodeType: INodeTypeDescription | null,
node: INodeUi,
ignoreIssues?: string[],
): INodeIssues | null {
const pinDataNodeNames = Object.keys(this.workflowsStore.getPinData || {});
let nodeIssues: INodeIssues | null = null;
ignoreIssues = ignoreIssues || [];
let nodeIssues: INodeIssues | null = null;
ignoreIssues = ignoreIssues || [];
if (node.disabled === true || pinDataNodeNames.includes(node.name)) {
// Ignore issues on disabled and pindata nodes
return null;
if (node.disabled === true || pinDataNodeNames.includes(node.name)) {
// Ignore issues on disabled and pindata nodes
return null;
}
if (nodeType === null) {
// Node type is not known
if (!ignoreIssues.includes('typeUnknown')) {
nodeIssues = {
typeUnknown: true,
};
}
} else {
// Node type is known
// Add potential parameter issues
if (!ignoreIssues.includes('parameters')) {
nodeIssues = NodeHelpers.getNodeParametersIssues(nodeType.properties, node);
}
if (nodeType === null) {
// Node type is not known
if (!ignoreIssues.includes('typeUnknown')) {
nodeIssues = {
typeUnknown: true,
};
if (!ignoreIssues.includes('credentials')) {
// Add potential credential issues
const nodeCredentialIssues = this.getNodeCredentialIssues(node, nodeType);
if (nodeIssues === null) {
nodeIssues = nodeCredentialIssues;
} else {
NodeHelpers.mergeIssues(nodeIssues, nodeCredentialIssues);
}
}
}
if (this.hasNodeExecutionIssues(node) === true && !ignoreIssues.includes('execution')) {
if (nodeIssues === null) {
nodeIssues = {};
}
nodeIssues.execution = true;
}
return nodeIssues;
},
// Set the status on all the nodes which produced an error so that it can be
// displayed in the node-view
hasNodeExecutionIssues(node: INodeUi): boolean {
const workflowResultData = this.workflowsStore.getWorkflowRunData;
if (workflowResultData === null || !workflowResultData.hasOwnProperty(node.name)) {
return false;
}
for (const taskData of workflowResultData[node.name]) {
if (taskData.error !== undefined) {
return true;
}
}
return false;
},
reportUnsetCredential(credentialType: ICredentialType) {
return {
credentials: {
[credentialType.name]: [
this.$locale.baseText('nodeHelpers.credentialsUnset', {
interpolate: {
credentialType: credentialType.displayName,
},
}),
],
},
};
},
// Updates the execution issues.
updateNodesExecutionIssues() {
const nodes = this.workflowsStore.allNodes;
for (const node of nodes) {
this.workflowsStore.setNodeIssue({
node: node.name,
type: 'execution',
value: this.hasNodeExecutionIssues(node) ? true : null,
});
}
},
// Updates the credential-issues of the node
updateNodeCredentialIssues(node: INodeUi): void {
const fullNodeIssues: INodeIssues | null = this.getNodeCredentialIssues(node);
let newIssues: INodeIssueObjectProperty | null = null;
if (fullNodeIssues !== null) {
newIssues = fullNodeIssues.credentials!;
}
this.workflowsStore.setNodeIssue({
node: node.name,
type: 'credentials',
value: newIssues,
});
},
// Updates the parameter-issues of the node
updateNodeParameterIssues(node: INodeUi, nodeType?: INodeTypeDescription): void {
if (nodeType === undefined) {
nodeType = this.nodeTypesStore.getNodeType(node.type, node.typeVersion);
}
if (nodeType === null) {
// Could not find nodeType so can not update issues
return;
}
// All data got updated everywhere so update now the issues
const fullNodeIssues: INodeIssues | null = NodeHelpers.getNodeParametersIssues(
nodeType!.properties,
node,
);
let newIssues: INodeIssueObjectProperty | null = null;
if (fullNodeIssues !== null) {
newIssues = fullNodeIssues.parameters!;
}
this.workflowsStore.setNodeIssue({
node: node.name,
type: 'parameters',
value: newIssues,
});
},
// Returns all the credential-issues of the node
getNodeCredentialIssues(node: INodeUi, nodeType?: INodeTypeDescription): INodeIssues | null {
if (node.disabled) {
// Node is disabled
return null;
}
if (!nodeType) {
nodeType = this.nodeTypesStore.getNodeType(node.type, node.typeVersion);
}
if (!nodeType?.credentials) {
// Node does not need any credentials or nodeType could not be found
return null;
}
const foundIssues: INodeIssueObjectProperty = {};
let userCredentials: ICredentialsResponse[] | null;
let credentialType: ICredentialType | null;
let credentialDisplayName: string;
let selectedCredentials: INodeCredentialsDetails;
const { authentication, genericAuthType, nodeCredentialType } =
node.parameters as HttpRequestNode.V2.AuthParams;
if (
authentication === 'genericCredentialType' &&
genericAuthType !== '' &&
selectedCredsAreUnusable(node, genericAuthType)
) {
const credential = this.credentialsStore.getCredentialTypeByName(genericAuthType);
return this.reportUnsetCredential(credential);
}
if (
this.hasProxyAuth(node) &&
authentication === 'predefinedCredentialType' &&
nodeCredentialType !== '' &&
node.credentials !== undefined
) {
const stored = this.credentialsStore.getCredentialsByType(nodeCredentialType);
if (selectedCredsDoNotExist(node, nodeCredentialType, stored)) {
const credential = this.credentialsStore.getCredentialTypeByName(nodeCredentialType);
return this.reportUnsetCredential(credential);
}
}
if (
this.hasProxyAuth(node) &&
authentication === 'predefinedCredentialType' &&
nodeCredentialType !== '' &&
selectedCredsAreUnusable(node, nodeCredentialType)
) {
const credential = this.credentialsStore.getCredentialTypeByName(nodeCredentialType);
return this.reportUnsetCredential(credential);
}
for (const credentialTypeDescription of nodeType.credentials) {
// Check if credentials should be displayed else ignore
if (!this.displayParameter(node.parameters, credentialTypeDescription, '', node)) {
continue;
}
// Get the display name of the credential type
credentialType = this.credentialsStore.getCredentialTypeByName(
credentialTypeDescription.name,
);
if (credentialType === null) {
credentialDisplayName = credentialTypeDescription.name;
} else {
credentialDisplayName = credentialType.displayName;
}
if (!node.credentials || !node.credentials?.[credentialTypeDescription.name]) {
// Credentials are not set
if (credentialTypeDescription.required) {
foundIssues[credentialTypeDescription.name] = [
this.$locale.baseText('nodeIssues.credentials.notSet', {
interpolate: { type: credentialDisplayName },
}),
];
}
} else {
// Node type is known
// Add potential parameter issues
if (!ignoreIssues.includes('parameters')) {
nodeIssues = NodeHelpers.getNodeParametersIssues(nodeType.properties, node);
// If they are set check if the value is valid
selectedCredentials = node.credentials[
credentialTypeDescription.name
] as INodeCredentialsDetails;
if (typeof selectedCredentials === 'string') {
selectedCredentials = {
id: null,
name: selectedCredentials,
};
}
if (!ignoreIssues.includes('credentials')) {
// Add potential credential issues
const nodeCredentialIssues = this.getNodeCredentialIssues(node, nodeType);
if (nodeIssues === null) {
nodeIssues = nodeCredentialIssues;
} else {
NodeHelpers.mergeIssues(nodeIssues, nodeCredentialIssues);
const usersStore = useUsersStore();
const currentUser = usersStore.currentUser || ({} as IUser);
userCredentials = this.credentialsStore
.getCredentialsByType(credentialTypeDescription.name)
.filter((credential: ICredentialsResponse) => {
const permissions = getCredentialPermissions(currentUser, credential);
return permissions.use;
});
if (userCredentials === null) {
userCredentials = [];
}
if (selectedCredentials.id) {
const idMatch = userCredentials.find(
(credentialData) => credentialData.id === selectedCredentials.id,
);
if (idMatch) {
continue;
}
}
const nameMatches = userCredentials.filter(
(credentialData) => credentialData.name === selectedCredentials.name,
);
if (nameMatches.length > 1) {
foundIssues[credentialTypeDescription.name] = [
this.$locale.baseText('nodeIssues.credentials.notIdentified', {
interpolate: { name: selectedCredentials.name, type: credentialDisplayName },
}),
this.$locale.baseText('nodeIssues.credentials.notIdentified.hint'),
];
continue;
}
if (nameMatches.length === 0) {
const isCredentialUsedInWorkflow =
this.workflowsStore.usedCredentials?.[selectedCredentials.id as string];
if (!isCredentialUsedInWorkflow) {
foundIssues[credentialTypeDescription.name] = [
this.$locale.baseText('nodeIssues.credentials.doNotExist', {
interpolate: { name: selectedCredentials.name, type: credentialDisplayName },
}),
this.$locale.baseText('nodeIssues.credentials.doNotExist.hint'),
];
}
}
}
}
if (this.hasNodeExecutionIssues(node) === true && !ignoreIssues.includes('execution')) {
if (nodeIssues === null) {
nodeIssues = {};
}
nodeIssues.execution = true;
}
// TODO: Could later check also if the node has access to the credentials
if (Object.keys(foundIssues).length === 0) {
return null;
}
return nodeIssues;
},
return {
credentials: foundIssues,
};
},
// Set the status on all the nodes which produced an error so that it can be
// displayed in the node-view
hasNodeExecutionIssues (node: INodeUi): boolean {
const workflowResultData = this.workflowsStore.getWorkflowRunData;
// Updates the node credential issues
updateNodesCredentialsIssues() {
const nodes = this.workflowsStore.allNodes;
let issues: INodeIssues | null;
if (workflowResultData === null || !workflowResultData.hasOwnProperty(node.name)) {
return false;
}
for (const taskData of workflowResultData[node.name]) {
if (taskData.error !== undefined) {
return true;
}
}
return false;
},
reportUnsetCredential(credentialType: ICredentialType) {
return {
credentials: {
[credentialType.name]: [
this.$locale.baseText(
'nodeHelpers.credentialsUnset',
{
interpolate: {
credentialType: credentialType.displayName,
},
},
),
],
},
};
},
// Updates the execution issues.
updateNodesExecutionIssues () {
const nodes = this.workflowsStore.allNodes;
for (const node of nodes) {
this.workflowsStore.setNodeIssue({
node: node.name,
type: 'execution',
value: this.hasNodeExecutionIssues(node) ? true : null,
});
}
},
// Updates the credential-issues of the node
updateNodeCredentialIssues(node: INodeUi): void {
const fullNodeIssues: INodeIssues | null = this.getNodeCredentialIssues(node);
let newIssues: INodeIssueObjectProperty | null = null;
if (fullNodeIssues !== null) {
newIssues = fullNodeIssues.credentials!;
}
for (const node of nodes) {
issues = this.getNodeCredentialIssues(node);
this.workflowsStore.setNodeIssue({
node: node.name,
type: 'credentials',
value: newIssues,
value: issues === null ? null : issues.credentials,
});
},
// Updates the parameter-issues of the node
updateNodeParameterIssues(node: INodeUi, nodeType?: INodeTypeDescription): void {
if (nodeType === undefined) {
nodeType = this.nodeTypesStore.getNodeType(node.type, node.typeVersion);
}
if (nodeType === null) {
// Could not find nodeType so can not update issues
return;
}
// All data got updated everywhere so update now the issues
const fullNodeIssues: INodeIssues | null = NodeHelpers.getNodeParametersIssues(nodeType!.properties, node);
let newIssues: INodeIssueObjectProperty | null = null;
if (fullNodeIssues !== null) {
newIssues = fullNodeIssues.parameters!;
}
this.workflowsStore.setNodeIssue({
node: node.name,
type: 'parameters',
value: newIssues,
});
},
// Returns all the credential-issues of the node
getNodeCredentialIssues (node: INodeUi, nodeType?: INodeTypeDescription): INodeIssues | null {
if (node.disabled) {
// Node is disabled
return null;
}
if (!nodeType) {
nodeType = this.nodeTypesStore.getNodeType(node.type, node.typeVersion);
}
if (!nodeType?.credentials) {
// Node does not need any credentials or nodeType could not be found
return null;
}
const foundIssues: INodeIssueObjectProperty = {};
let userCredentials: ICredentialsResponse[] | null;
let credentialType: ICredentialType | null;
let credentialDisplayName: string;
let selectedCredentials: INodeCredentialsDetails;
const {
authentication,
genericAuthType,
nodeCredentialType,
} = node.parameters as HttpRequestNode.V2.AuthParams;
if (
authentication === 'genericCredentialType' &&
genericAuthType !== '' &&
selectedCredsAreUnusable(node, genericAuthType)
) {
const credential = this.credentialsStore.getCredentialTypeByName(genericAuthType);
return this.reportUnsetCredential(credential);
}
if (
this.hasProxyAuth(node) &&
authentication === 'predefinedCredentialType' &&
nodeCredentialType !== '' &&
node.credentials !== undefined
) {
const stored = this.credentialsStore.getCredentialsByType(nodeCredentialType);
if (selectedCredsDoNotExist(node, nodeCredentialType, stored)) {
const credential = this.credentialsStore.getCredentialTypeByName(nodeCredentialType);
return this.reportUnsetCredential(credential);
}
}
if (
this.hasProxyAuth(node) &&
authentication === 'predefinedCredentialType' &&
nodeCredentialType !== '' &&
selectedCredsAreUnusable(node, nodeCredentialType)
) {
const credential = this.credentialsStore.getCredentialTypeByName(nodeCredentialType);
return this.reportUnsetCredential(credential);
}
for (const credentialTypeDescription of nodeType.credentials) {
// Check if credentials should be displayed else ignore
if (!this.displayParameter(node.parameters, credentialTypeDescription, '', node)) {
continue;
}
// Get the display name of the credential type
credentialType = this.credentialsStore.getCredentialTypeByName(credentialTypeDescription.name);
if (credentialType === null) {
credentialDisplayName = credentialTypeDescription.name;
} else {
credentialDisplayName = credentialType.displayName;
}
if (!node.credentials || !node.credentials?.[credentialTypeDescription.name]) {
// Credentials are not set
if (credentialTypeDescription.required) {
foundIssues[credentialTypeDescription.name] = [this.$locale.baseText('nodeIssues.credentials.notSet', { interpolate: { type: credentialDisplayName } })];
}
} else {
// If they are set check if the value is valid
selectedCredentials = node.credentials[credentialTypeDescription.name] as INodeCredentialsDetails;
if (typeof selectedCredentials === 'string') {
selectedCredentials = {
id: null,
name: selectedCredentials,
};
}
const usersStore = useUsersStore();
const currentUser = usersStore.currentUser || {} as IUser;
userCredentials = this.credentialsStore.getCredentialsByType(credentialTypeDescription.name)
.filter((credential: ICredentialsResponse) => {
const permissions = getCredentialPermissions(currentUser, credential);
return permissions.use;
});
if (userCredentials === null) {
userCredentials = [];
}
if (selectedCredentials.id) {
const idMatch = userCredentials.find((credentialData) => credentialData.id === selectedCredentials.id);
if (idMatch) {
continue;
}
}
const nameMatches = userCredentials.filter((credentialData) => credentialData.name === selectedCredentials.name);
if (nameMatches.length > 1) {
foundIssues[credentialTypeDescription.name] = [this.$locale.baseText('nodeIssues.credentials.notIdentified', { interpolate: { name: selectedCredentials.name, type: credentialDisplayName } }), this.$locale.baseText('nodeIssues.credentials.notIdentified.hint')];
continue;
}
if (nameMatches.length === 0) {
const isCredentialUsedInWorkflow = this.workflowsStore.usedCredentials?.[selectedCredentials.id as string];
if (!isCredentialUsedInWorkflow) {
foundIssues[credentialTypeDescription.name] = [this.$locale.baseText('nodeIssues.credentials.doNotExist', { interpolate: { name: selectedCredentials.name, type: credentialDisplayName } }), this.$locale.baseText('nodeIssues.credentials.doNotExist.hint')];
}
}
}
}
// TODO: Could later check also if the node has access to the credentials
if (Object.keys(foundIssues).length === 0) {
return null;
}
return {
credentials: foundIssues,
};
},
// Updates the node credential issues
updateNodesCredentialsIssues () {
const nodes = this.workflowsStore.allNodes;
let issues: INodeIssues | null;
for (const node of nodes) {
issues = this.getNodeCredentialIssues(node);
this.workflowsStore.setNodeIssue({
node: node.name,
type: 'credentials',
value: issues === null ? null : issues.credentials,
});
}
},
getNodeInputData (node: INodeUi | null, runIndex = 0, outputIndex = 0): INodeExecutionData[] {
if (node === null) {
return [];
}
if (this.workflowsStore.getWorkflowExecution === null) {
return [];
}
const executionData = this.workflowsStore.getWorkflowExecution.data;
if (!executionData || !executionData.resultData) { // unknown status
return [];
}
const runData = executionData.resultData.runData;
if (runData === null || runData[node.name] === undefined ||
!runData[node.name][runIndex].data ||
runData[node.name][runIndex].data === undefined
) {
return [];
}
return this.getMainInputData(runData[node.name][runIndex].data!, outputIndex);
},
// Returns the data of the main input
getMainInputData (connectionsData: ITaskDataConnections, outputIndex: number): INodeExecutionData[] {
if (!connectionsData || !connectionsData.hasOwnProperty('main') || connectionsData.main === undefined || connectionsData.main.length < outputIndex || connectionsData.main[outputIndex] === null) {
return [];
}
return connectionsData.main[outputIndex] as INodeExecutionData[];
},
// Returns all the binary data of all the entries
getBinaryData (workflowRunData: IRunData | null, node: string | null, runIndex: number, outputIndex: number): IBinaryKeyData[] {
if (node === null) {
return [];
}
const runData: IRunData | null = workflowRunData;
if (runData === null || !runData[node] || !runData[node][runIndex] ||
!runData[node][runIndex].data
) {
return [];
}
const inputData = this.getMainInputData(runData[node][runIndex].data!, outputIndex);
const returnData: IBinaryKeyData[] = [];
for (let i = 0; i < inputData.length; i++) {
if (inputData[i].hasOwnProperty('binary') && inputData[i].binary !== undefined) {
returnData.push(inputData[i].binary!);
}
}
return returnData;
},
disableNodes(nodes: INodeUi[], trackHistory = false) {
if (trackHistory) {
this.historyStore.startRecordingUndo();
}
for (const node of nodes) {
const oldState = node.disabled;
// Toggle disabled flag
const updateInformation = {
name: node.name,
properties: {
disabled: !oldState,
} as IDataObject,
} as INodeUpdatePropertiesInformation;
this.$telemetry.track('User set node enabled status', { node_type: node.type, is_enabled: node.disabled, workflow_id: this.workflowsStore.workflowId });
this.workflowsStore.updateNodeProperties(updateInformation);
this.workflowsStore.clearNodeExecutionData(node.name);
this.updateNodeParameterIssues(node);
this.updateNodeCredentialIssues(node);
if (trackHistory) {
this.historyStore.pushCommandToUndo(new EnableNodeToggleCommand(node.name, oldState === true, node.disabled === true, this));
}
}
if (trackHistory) {
this.historyStore.stopRecordingUndo();
}
},
// @ts-ignore
getNodeSubtitle (data, nodeType, workflow): string | undefined {
if (!data) {
return undefined;
}
if (data.notesInFlow) {
return data.notes;
}
if (nodeType !== null && nodeType.subtitle !== undefined) {
return workflow.expression.getSimpleParameterValue(data as INode, nodeType.subtitle, 'internal', PLACEHOLDER_FILLED_AT_EXECUTION_TIME) as string | undefined;
}
if (data.parameters.operation !== undefined) {
const operation = data.parameters.operation as string;
if (nodeType === null) {
return operation;
}
const operationData:INodeProperties = nodeType.properties.find((property: INodeProperties) => {
return property.name === 'operation';
});
if (operationData === undefined) {
return operation;
}
if (operationData.options === undefined) {
return operation;
}
const optionData = operationData.options.find((option) => {
return (option as INodePropertyOptions).value === data.parameters.operation;
});
if (optionData === undefined) {
return operation;
}
return optionData.name;
}
return undefined;
},
}
},
});
getNodeInputData(node: INodeUi | null, runIndex = 0, outputIndex = 0): INodeExecutionData[] {
if (node === null) {
return [];
}
if (this.workflowsStore.getWorkflowExecution === null) {
return [];
}
const executionData = this.workflowsStore.getWorkflowExecution.data;
if (!executionData || !executionData.resultData) {
// unknown status
return [];
}
const runData = executionData.resultData.runData;
if (
runData === null ||
runData[node.name] === undefined ||
!runData[node.name][runIndex].data ||
runData[node.name][runIndex].data === undefined
) {
return [];
}
return this.getMainInputData(runData[node.name][runIndex].data!, outputIndex);
},
// Returns the data of the main input
getMainInputData(
connectionsData: ITaskDataConnections,
outputIndex: number,
): INodeExecutionData[] {
if (
!connectionsData ||
!connectionsData.hasOwnProperty('main') ||
connectionsData.main === undefined ||
connectionsData.main.length < outputIndex ||
connectionsData.main[outputIndex] === null
) {
return [];
}
return connectionsData.main[outputIndex] as INodeExecutionData[];
},
// Returns all the binary data of all the entries
getBinaryData(
workflowRunData: IRunData | null,
node: string | null,
runIndex: number,
outputIndex: number,
): IBinaryKeyData[] {
if (node === null) {
return [];
}
const runData: IRunData | null = workflowRunData;
if (
runData === null ||
!runData[node] ||
!runData[node][runIndex] ||
!runData[node][runIndex].data
) {
return [];
}
const inputData = this.getMainInputData(runData[node][runIndex].data!, outputIndex);
const returnData: IBinaryKeyData[] = [];
for (let i = 0; i < inputData.length; i++) {
if (inputData[i].hasOwnProperty('binary') && inputData[i].binary !== undefined) {
returnData.push(inputData[i].binary!);
}
}
return returnData;
},
disableNodes(nodes: INodeUi[], trackHistory = false) {
if (trackHistory) {
this.historyStore.startRecordingUndo();
}
for (const node of nodes) {
const oldState = node.disabled;
// Toggle disabled flag
const updateInformation = {
name: node.name,
properties: {
disabled: !oldState,
} as IDataObject,
} as INodeUpdatePropertiesInformation;
this.$telemetry.track('User set node enabled status', {
node_type: node.type,
is_enabled: node.disabled,
workflow_id: this.workflowsStore.workflowId,
});
this.workflowsStore.updateNodeProperties(updateInformation);
this.workflowsStore.clearNodeExecutionData(node.name);
this.updateNodeParameterIssues(node);
this.updateNodeCredentialIssues(node);
if (trackHistory) {
this.historyStore.pushCommandToUndo(
new EnableNodeToggleCommand(node.name, oldState === true, node.disabled === true, this),
);
}
}
if (trackHistory) {
this.historyStore.stopRecordingUndo();
}
},
// @ts-ignore
getNodeSubtitle(data, nodeType, workflow): string | undefined {
if (!data) {
return undefined;
}
if (data.notesInFlow) {
return data.notes;
}
if (nodeType !== null && nodeType.subtitle !== undefined) {
return workflow.expression.getSimpleParameterValue(
data as INode,
nodeType.subtitle,
'internal',
PLACEHOLDER_FILLED_AT_EXECUTION_TIME,
) as string | undefined;
}
if (data.parameters.operation !== undefined) {
const operation = data.parameters.operation as string;
if (nodeType === null) {
return operation;
}
const operationData: INodeProperties = nodeType.properties.find(
(property: INodeProperties) => {
return property.name === 'operation';
},
);
if (operationData === undefined) {
return operation;
}
if (operationData.options === undefined) {
return operation;
}
const optionData = operationData.options.find((option) => {
return (option as INodePropertyOptions).value === data.parameters.operation;
});
if (optionData === undefined) {
return operation;
}
return optionData.name;
}
return undefined;
},
},
});
/**
* Whether the node has no selected credentials, or none of the node's

View File

@@ -14,10 +14,10 @@ export interface IPinDataContext {
export const pinData = (Vue as Vue.VueConstructor<Vue & IPinDataContext>).extend({
computed: {
...mapStores(useWorkflowsStore),
pinData (): IPinData[string] | undefined {
pinData(): IPinData[string] | undefined {
return this.node ? this.workflowsStore.pinDataByNodeName(this.node!.name) : undefined;
},
hasPinData (): boolean {
hasPinData(): boolean {
return !!this.node && typeof this.pinData !== 'undefined';
},
isPinDataNodeType(): boolean {
@@ -40,7 +40,7 @@ export const pinData = (Vue as Vue.VueConstructor<Vue & IPinDataContext>).extend
error.message = message.charAt(0).toUpperCase() + message.slice(1);
error.message = error.message.replace(
'Unexpected token \' in JSON',
"Unexpected token ' in JSON",
this.$locale.baseText('runData.editOutputInvalid.singleQuote'),
);
@@ -48,7 +48,8 @@ export const pinData = (Vue as Vue.VueConstructor<Vue & IPinDataContext>).extend
const position = parseInt(positionMatch[1], 10);
const lineBreaksUpToPosition = (data.slice(0, position).match(/\n/g) || []).length;
error.message = error.message.replace(positionMatchRegEx,
error.message = error.message.replace(
positionMatchRegEx,
this.$locale.baseText('runData.editOutputInvalid.atPosition', {
interpolate: {
position: `${position}`,
@@ -56,13 +57,11 @@ export const pinData = (Vue as Vue.VueConstructor<Vue & IPinDataContext>).extend
}),
);
error.message = `${
this.$locale.baseText('runData.editOutputInvalid.onLine', {
interpolate: {
line: `${lineBreaksUpToPosition + 1}`,
},
})
} ${error.message}`;
error.message = `${this.$locale.baseText('runData.editOutputInvalid.onLine', {
interpolate: {
line: `${lineBreaksUpToPosition + 1}`,
},
})} ${error.message}`;
}
this.$showError(error, title);
@@ -73,7 +72,10 @@ export const pinData = (Vue as Vue.VueConstructor<Vue & IPinDataContext>).extend
isValidPinDataSize(data: string | object): boolean {
if (typeof data === 'object') data = JSON.stringify(data);
if (this.workflowsStore.pinDataSize + stringSizeInBytes(data) > MAX_WORKFLOW_PINNED_DATA_SIZE) {
if (
this.workflowsStore.pinDataSize + stringSizeInBytes(data) >
MAX_WORKFLOW_PINNED_DATA_SIZE
) {
this.$showError(
new Error(this.$locale.baseText('ndv.pinData.error.tooLarge.description')),
this.$locale.baseText('ndv.pinData.error.tooLarge.title'),

View File

@@ -1,8 +1,4 @@
import {
IExecutionResponse,
IExecutionsCurrentSummaryExtended,
IPushData,
} from '../../Interface';
import { IExecutionResponse, IExecutionsCurrentSummaryExtended, IPushData } from '../../Interface';
import { externalHooks } from '@/mixins/externalHooks';
import { nodeHelpers } from '@/mixins/nodeHelpers';
@@ -35,60 +31,60 @@ export const pushConnection = mixins(
showMessage,
titleChange,
workflowHelpers,
)
.extend({
data () {
return {
eventSource: null as EventSource | null,
reconnectTimeout: null as NodeJS.Timeout | null,
retryTimeout: null as NodeJS.Timeout | null,
pushMessageQueue: [] as Array<{ event: Event, retriesLeft: number }>,
};
).extend({
data() {
return {
eventSource: null as EventSource | null,
reconnectTimeout: null as NodeJS.Timeout | null,
retryTimeout: null as NodeJS.Timeout | null,
pushMessageQueue: [] as Array<{ event: Event; retriesLeft: number }>,
};
},
computed: {
...mapStores(useCredentialsStore, useNodeTypesStore, useUIStore, useWorkflowsStore),
sessionId(): string {
return this.rootStore.sessionId;
},
computed: {
...mapStores(
useCredentialsStore,
useNodeTypesStore,
useUIStore,
useWorkflowsStore,
),
sessionId (): string {
return this.rootStore.sessionId;
},
},
methods: {
pushAutomaticReconnect(): void {
if (this.reconnectTimeout !== null) {
return;
}
this.reconnectTimeout = setTimeout(() => {
this.pushConnect();
}, 3000);
},
methods: {
pushAutomaticReconnect (): void {
if (this.reconnectTimeout !== null) {
return;
}
this.reconnectTimeout = setTimeout(() => {
this.pushConnect();
}, 3000);
},
/**
* Connect to server to receive data via EventSource
*/
pushConnect(): void {
// Make sure existing event-source instances get
// always removed that we do not end up with multiple ones
this.pushDisconnect();
/**
* Connect to server to receive data via EventSource
*/
pushConnect (): void {
// Make sure existing event-source instances get
// always removed that we do not end up with multiple ones
this.pushDisconnect();
const connectionUrl = `${this.rootStore.getRestUrl}/push?sessionId=${this.sessionId}`;
const connectionUrl = `${this.rootStore.getRestUrl}/push?sessionId=${this.sessionId}`;
this.eventSource = new EventSource(connectionUrl, { withCredentials: true });
this.eventSource.addEventListener('message', this.pushMessageReceived, false);
this.eventSource = new EventSource(connectionUrl, { withCredentials: true });
this.eventSource.addEventListener('message', this.pushMessageReceived, false);
this.eventSource.addEventListener('open', () => {
this.rootStore.pushConnectionActive = true;
this.eventSource.addEventListener(
'open',
() => {
this.rootStore.pushConnectionActive = true;
if (this.reconnectTimeout !== null) {
clearTimeout(this.reconnectTimeout);
this.reconnectTimeout = null;
}
}, false);
},
false,
);
this.eventSource.addEventListener('error', () => {
this.eventSource.addEventListener(
'error',
() => {
this.pushDisconnect();
if (this.reconnectTimeout !== null) {
@@ -96,362 +92,386 @@ export const pushConnection = mixins(
this.reconnectTimeout = null;
}
this.rootStore.pushConnectionActive = false;
this.rootStore.pushConnectionActive = false;
this.pushAutomaticReconnect();
}, false);
},
},
false,
);
},
/**
* Close connection to server
*/
pushDisconnect (): void {
if (this.eventSource !== null) {
this.eventSource.close();
this.eventSource = null;
/**
* Close connection to server
*/
pushDisconnect(): void {
if (this.eventSource !== null) {
this.eventSource.close();
this.eventSource = null;
this.rootStore.pushConnectionActive = false;
}
},
this.rootStore.pushConnectionActive = false;
}
},
/**
* Sometimes the push message is faster as the result from
* the REST API so we do not know yet what execution ID
* is currently active. So internally resend the message
* a few more times
*
*/
queuePushMessage (event: Event, retryAttempts: number) {
this.pushMessageQueue.push({ event, retriesLeft: retryAttempts });
/**
* Sometimes the push message is faster as the result from
* the REST API so we do not know yet what execution ID
* is currently active. So internally resend the message
* a few more times
*
*/
queuePushMessage(event: Event, retryAttempts: number) {
this.pushMessageQueue.push({ event, retriesLeft: retryAttempts });
if (this.retryTimeout === null) {
this.retryTimeout = setTimeout(this.processWaitingPushMessages, 20);
}
},
if (this.retryTimeout === null) {
this.retryTimeout = setTimeout(this.processWaitingPushMessages, 20);
}
},
/**
* Process the push messages which are waiting in the queue
*/
processWaitingPushMessages() {
if (this.retryTimeout !== null) {
clearTimeout(this.retryTimeout);
this.retryTimeout = null;
}
/**
* Process the push messages which are waiting in the queue
*/
processWaitingPushMessages () {
if (this.retryTimeout !== null) {
clearTimeout(this.retryTimeout);
this.retryTimeout = null;
}
const queueLength = this.pushMessageQueue.length;
for (let i = 0; i < queueLength; i++) {
const messageData = this.pushMessageQueue.shift();
const queueLength = this.pushMessageQueue.length;
for (let i = 0; i < queueLength; i++) {
const messageData = this.pushMessageQueue.shift();
if (this.pushMessageReceived(messageData!.event, true) === false) {
// Was not successful
messageData!.retriesLeft -= 1;
if (this.pushMessageReceived(messageData!.event, true) === false) {
// Was not successful
messageData!.retriesLeft -= 1;
if (messageData!.retriesLeft > 0) {
// If still retries are left add it back and stop execution
this.pushMessageQueue.unshift(messageData!);
}
break;
if (messageData!.retriesLeft > 0) {
// If still retries are left add it back and stop execution
this.pushMessageQueue.unshift(messageData!);
}
break;
}
}
if (this.pushMessageQueue.length !== 0 && this.retryTimeout === null) {
this.retryTimeout = setTimeout(this.processWaitingPushMessages, 25);
if (this.pushMessageQueue.length !== 0 && this.retryTimeout === null) {
this.retryTimeout = setTimeout(this.processWaitingPushMessages, 25);
}
},
/**
* Process a newly received message
*
* @param {Event} event The event data with the message data
* @param {boolean} [isRetry] If it is a retry
*/
pushMessageReceived(event: Event, isRetry?: boolean): boolean {
const retryAttempts = 5;
let receivedData: IPushData;
try {
// @ts-ignore
receivedData = JSON.parse(event.data);
} catch (error) {
return false;
}
if (receivedData.type === 'sendConsoleMessage') {
const pushData = receivedData.data;
console.log(pushData.source, ...pushData.messages); // eslint-disable-line no-console
return true;
}
if (
!['testWebhookReceived'].includes(receivedData.type) &&
isRetry !== true &&
this.pushMessageQueue.length
) {
// If there are already messages in the queue add the new one that all of them
// get executed in order
this.queuePushMessage(event, retryAttempts);
return false;
}
if (receivedData.type === 'nodeExecuteAfter' || receivedData.type === 'nodeExecuteBefore') {
if (!this.uiStore.isActionActive('workflowRunning')) {
// No workflow is running so ignore the messages
return false;
}
},
const pushData = receivedData.data;
if (this.workflowsStore.activeExecutionId !== pushData.executionId) {
// The data is not for the currently active execution or
// we do not have the execution id yet.
if (isRetry !== true) {
this.queuePushMessage(event, retryAttempts);
}
return false;
}
}
if (receivedData.type === 'executionFinished') {
// The workflow finished executing
const pushData = receivedData.data;
/**
* Process a newly received message
*
* @param {Event} event The event data with the message data
* @param {boolean} [isRetry] If it is a retry
*/
pushMessageReceived (event: Event, isRetry?: boolean): boolean {
const retryAttempts = 5;
let receivedData: IPushData;
try {
// @ts-ignore
receivedData = JSON.parse(event.data);
} catch (error) {
this.workflowsStore.finishActiveExecution(pushData);
if (!this.uiStore.isActionActive('workflowRunning')) {
// No workflow is running so ignore the messages
return false;
}
if (receivedData.type === 'sendConsoleMessage') {
const pushData = receivedData.data;
console.log(pushData.source, ...pushData.messages); // eslint-disable-line no-console
return true;
}
if (!['testWebhookReceived'].includes(receivedData.type) && isRetry !== true && this.pushMessageQueue.length) {
// If there are already messages in the queue add the new one that all of them
// get executed in order
this.queuePushMessage(event, retryAttempts);
if (this.workflowsStore.activeExecutionId !== pushData.executionId) {
// The workflow which did finish execution did either not get started
// by this session or we do not have the execution id yet.
if (isRetry !== true) {
this.queuePushMessage(event, retryAttempts);
}
return false;
}
if (receivedData.type === 'nodeExecuteAfter' || receivedData.type === 'nodeExecuteBefore') {
if (!this.uiStore.isActionActive('workflowRunning')) {
// No workflow is running so ignore the messages
return false;
}
const pushData = receivedData.data;
if (this.workflowsStore.activeExecutionId !== pushData.executionId) {
// The data is not for the currently active execution or
// we do not have the execution id yet.
if (isRetry !== true) {
this.queuePushMessage(event, retryAttempts);
}
return false;
}
}
const runDataExecuted = pushData.data;
if (receivedData.type === 'executionFinished') {
// The workflow finished executing
const pushData = receivedData.data;
const runDataExecutedErrorMessage = this.$getExecutionError(runDataExecuted.data);
this.workflowsStore.finishActiveExecution(pushData);
const lineNumber =
runDataExecuted &&
runDataExecuted.data &&
runDataExecuted.data.resultData &&
runDataExecuted.data.resultData.error &&
runDataExecuted.data.resultData.error.lineNumber;
if (!this.uiStore.isActionActive('workflowRunning')) {
// No workflow is running so ignore the messages
return false;
codeNodeEditorEventBus.$emit('error-line-number', lineNumber || 'final');
const workflow = this.getCurrentWorkflow();
if (runDataExecuted.waitTill !== undefined) {
const activeExecutionId = this.workflowsStore.activeExecutionId;
const workflowSettings = this.workflowsStore.workflowSettings;
const saveManualExecutions = this.rootStore.saveManualExecutions;
const isSavingExecutions =
workflowSettings.saveManualExecutions === undefined
? saveManualExecutions
: workflowSettings.saveManualExecutions;
let action;
if (!isSavingExecutions) {
this.$root.$emit('registerGlobalLinkAction', 'open-settings', async () => {
if (this.workflowsStore.isNewWorkflow) await this.saveAsNewWorkflow();
this.uiStore.openModal(WORKFLOW_SETTINGS_MODAL_KEY);
});
action =
'<a data-action="open-settings">Turn on saving manual executions</a> and run again to see what happened after this node.';
} else {
action = `<a href="/workflow/${workflow.id}/executions/${activeExecutionId}">View the execution</a> to see what happened after this node.`;
}
if (this.workflowsStore.activeExecutionId !== pushData.executionId) {
// The workflow which did finish execution did either not get started
// by this session or we do not have the execution id yet.
if (isRetry !== true) {
this.queuePushMessage(event, retryAttempts);
}
return false;
// Workflow did start but had been put to wait
this.$titleSet(workflow.name as string, 'IDLE');
this.$showToast({
title: 'Workflow started waiting',
message: `${action} <a href="https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.wait/" target="_blank">More info</a>`,
type: 'success',
duration: 0,
});
} else if (runDataExecuted.finished !== true) {
this.$titleSet(workflow.name as string, 'ERROR');
if (
runDataExecuted.data.resultData.error?.name === 'ExpressionError' &&
(runDataExecuted.data.resultData.error as ExpressionError).context.functionality ===
'pairedItem'
) {
const error = runDataExecuted.data.resultData.error as ExpressionError;
this.getWorkflowDataToSave().then((workflowData) => {
const eventData: IDataObject = {
caused_by_credential: false,
error_message: error.description,
error_title: error.message,
error_type: error.context.type,
node_graph_string: JSON.stringify(
TelemetryHelpers.generateNodesGraph(
workflowData as IWorkflowBase,
this.getNodeTypes(),
).nodeGraph,
),
workflow_id: this.workflowsStore.workflowId,
};
if (
error.context.nodeCause &&
['no pairing info', 'invalid pairing info'].includes(error.context.type as string)
) {
const node = workflow.getNode(error.context.nodeCause as string);
if (node) {
eventData.is_pinned = !!workflow.getPinDataOfNode(node.name);
eventData.mode = node.parameters.mode;
eventData.node_type = node.type;
eventData.operation = node.parameters.operation;
eventData.resource = node.parameters.resource;
}
}
this.$telemetry.track('Instance FE emitted paired item error', eventData);
});
}
const runDataExecuted = pushData.data;
if (runDataExecuted.data.resultData.error?.name === 'SubworkflowOperationError') {
const error = runDataExecuted.data.resultData.error as SubworkflowOperationError;
const runDataExecutedErrorMessage = this.$getExecutionError(runDataExecuted.data);
this.workflowsStore.subWorkflowExecutionError = error;
const lineNumber = runDataExecuted &&
runDataExecuted.data &&
runDataExecuted.data.resultData &&
runDataExecuted.data.resultData.error &&
runDataExecuted.data.resultData.error.lineNumber;
codeNodeEditorEventBus.$emit('error-line-number', lineNumber || 'final');
const workflow = this.getCurrentWorkflow();
if (runDataExecuted.waitTill !== undefined) {
const activeExecutionId = this.workflowsStore.activeExecutionId;
const workflowSettings = this.workflowsStore.workflowSettings;
const saveManualExecutions = this.rootStore.saveManualExecutions;
const isSavingExecutions= workflowSettings.saveManualExecutions === undefined ? saveManualExecutions : workflowSettings.saveManualExecutions;
let action;
if (!isSavingExecutions) {
this.$root.$emit('registerGlobalLinkAction', 'open-settings', async () => {
if (this.workflowsStore.isNewWorkflow) await this.saveAsNewWorkflow();
this.uiStore.openModal(WORKFLOW_SETTINGS_MODAL_KEY);
});
action = '<a data-action="open-settings">Turn on saving manual executions</a> and run again to see what happened after this node.';
}
else {
action = `<a href="/workflow/${workflow.id}/executions/${activeExecutionId}">View the execution</a> to see what happened after this node.`;
}
// Workflow did start but had been put to wait
this.$titleSet(workflow.name as string, 'IDLE');
this.$showToast({
title: 'Workflow started waiting',
message: `${action} <a href="https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.wait/" target="_blank">More info</a>`,
type: 'success',
this.$showMessage({
title: error.message,
message: error.description,
type: 'error',
duration: 0,
});
} else if (runDataExecuted.finished !== true) {
this.$titleSet(workflow.name as string, 'ERROR');
if (
runDataExecuted.data.resultData.error?.name === 'ExpressionError' &&
(runDataExecuted.data.resultData.error as ExpressionError).context.functionality === 'pairedItem'
) {
const error = runDataExecuted.data.resultData.error as ExpressionError;
this.getWorkflowDataToSave().then((workflowData) => {
const eventData: IDataObject = {
caused_by_credential: false,
error_message: error.description,
error_title: error.message,
error_type: error.context.type,
node_graph_string: JSON.stringify(TelemetryHelpers.generateNodesGraph(workflowData as IWorkflowBase, this.getNodeTypes()).nodeGraph),
workflow_id: this.workflowsStore.workflowId,
};
if (error.context.nodeCause && ['no pairing info', 'invalid pairing info'].includes(error.context.type as string)) {
const node = workflow.getNode(error.context.nodeCause as string);
if (node) {
eventData.is_pinned = !!workflow.getPinDataOfNode(node.name);
eventData.mode = node.parameters.mode;
eventData.node_type = node.type;
eventData.operation = node.parameters.operation;
eventData.resource = node.parameters.resource;
}
}
this.$telemetry.track('Instance FE emitted paired item error', eventData);
});
}
if (runDataExecuted.data.resultData.error?.name === 'SubworkflowOperationError') {
const error = runDataExecuted.data.resultData.error as SubworkflowOperationError;
this.workflowsStore.subWorkflowExecutionError = error;
this.$showMessage({
title: error.message,
message: error.description,
type: 'error',
duration: 0,
});
} else {
let title: string;
if (runDataExecuted.data.resultData.lastNodeExecuted) {
title = `Problem in node ${runDataExecuted.data.resultData.lastNodeExecuted}`;
} else {
title = 'Problem executing workflow';
}
this.$showMessage({
title,
message: runDataExecutedErrorMessage,
type: 'error',
duration: 0,
});
}
} else {
// Workflow did execute without a problem
this.$titleSet(workflow.name as string, 'IDLE');
const execution = this.workflowsStore.getWorkflowExecution;
if (execution && execution.executedNode) {
const node = this.workflowsStore.getNodeByName(execution.executedNode);
const nodeType = node && this.nodeTypesStore.getNodeType(node.type, node.typeVersion);
const nodeOutput = execution && execution.executedNode && execution.data && execution.data.resultData && execution.data.resultData.runData && execution.data.resultData.runData[execution.executedNode];
if (node && nodeType && !nodeOutput) {
this.$showMessage({
title: this.$locale.baseText('pushConnection.pollingNode.dataNotFound', {
interpolate: {
service: getTriggerNodeServiceName(nodeType),
},
}),
message: this.$locale.baseText('pushConnection.pollingNode.dataNotFound.message', {
interpolate: {
service: getTriggerNodeServiceName(nodeType),
},
}),
type: 'success',
});
} else {
this.$showMessage({
title: this.$locale.baseText('pushConnection.nodeExecutedSuccessfully'),
type: 'success',
});
}
let title: string;
if (runDataExecuted.data.resultData.lastNodeExecuted) {
title = `Problem in node ${runDataExecuted.data.resultData.lastNodeExecuted}`;
} else {
title = 'Problem executing workflow';
}
else {
this.$showMessage({
title,
message: runDataExecutedErrorMessage,
type: 'error',
duration: 0,
});
}
} else {
// Workflow did execute without a problem
this.$titleSet(workflow.name as string, 'IDLE');
const execution = this.workflowsStore.getWorkflowExecution;
if (execution && execution.executedNode) {
const node = this.workflowsStore.getNodeByName(execution.executedNode);
const nodeType = node && this.nodeTypesStore.getNodeType(node.type, node.typeVersion);
const nodeOutput =
execution &&
execution.executedNode &&
execution.data &&
execution.data.resultData &&
execution.data.resultData.runData &&
execution.data.resultData.runData[execution.executedNode];
if (node && nodeType && !nodeOutput) {
this.$showMessage({
title: this.$locale.baseText('pushConnection.workflowExecutedSuccessfully'),
title: this.$locale.baseText('pushConnection.pollingNode.dataNotFound', {
interpolate: {
service: getTriggerNodeServiceName(nodeType),
},
}),
message: this.$locale.baseText('pushConnection.pollingNode.dataNotFound.message', {
interpolate: {
service: getTriggerNodeServiceName(nodeType),
},
}),
type: 'success',
});
} else {
this.$showMessage({
title: this.$locale.baseText('pushConnection.nodeExecutedSuccessfully'),
type: 'success',
});
}
}
// It does not push the runData as it got already pushed with each
// node that did finish. For that reason copy in here the data
// which we already have.
if (this.workflowsStore.getWorkflowRunData) {
runDataExecuted.data.resultData.runData = this.workflowsStore.getWorkflowRunData;
}
this.workflowsStore.executingNode = null;
this.workflowsStore.setWorkflowExecutionData(runDataExecuted as IExecutionResponse);
this.uiStore.removeActiveAction('workflowRunning');
// Set the node execution issues on all the nodes which produced an error so that
// it can be displayed in the node-view
this.updateNodesExecutionIssues();
const lastNodeExecuted: string | undefined = runDataExecuted.data.resultData.lastNodeExecuted;
let itemsCount = 0;
if(lastNodeExecuted && runDataExecuted.data.resultData.runData[lastNodeExecuted as string] && !runDataExecutedErrorMessage) {
itemsCount = runDataExecuted.data.resultData.runData[lastNodeExecuted as string][0].data!.main[0]!.length;
}
this.$externalHooks().run('pushConnection.executionFinished', {
itemsCount,
nodeName: runDataExecuted.data.resultData.lastNodeExecuted,
errorMessage: runDataExecutedErrorMessage,
runDataExecutedStartData: runDataExecuted.data.startData,
resultDataError: runDataExecuted.data.resultData.error,
});
} else if (receivedData.type === 'executionStarted') {
const pushData = receivedData.data;
const executionData: IExecutionsCurrentSummaryExtended = {
id: pushData.executionId,
finished: false,
mode: pushData.mode,
startedAt: pushData.startedAt,
retryOf: pushData.retryOf,
workflowId: pushData.workflowId,
workflowName: pushData.workflowName,
};
this.workflowsStore.addActiveExecution(executionData);
} else if (receivedData.type === 'nodeExecuteAfter') {
// A node finished to execute. Add its data
const pushData = receivedData.data;
this.workflowsStore.addNodeExecutionData(pushData);
} else if (receivedData.type === 'nodeExecuteBefore') {
// A node started to be executed. Set it as executing.
const pushData = receivedData.data;
this.workflowsStore.executingNode = pushData.nodeName;
} else if (receivedData.type === 'testWebhookDeleted') {
// A test-webhook was deleted
const pushData = receivedData.data;
if (pushData.workflowId === this.workflowsStore.workflowId) {
this.workflowsStore.executionWaitingForWebhook = false;
this.uiStore.removeActiveAction('workflowRunning');
}
} else if (receivedData.type === 'testWebhookReceived') {
// A test-webhook did get called
const pushData = receivedData.data;
if (pushData.workflowId === this.workflowsStore.workflowId) {
this.workflowsStore.executionWaitingForWebhook = false;
this.workflowsStore.activeExecutionId = pushData.executionId;
}
this.processWaitingPushMessages();
} else if (receivedData.type === 'reloadNodeType') {
this.nodeTypesStore.getNodeTypes();
this.nodeTypesStore.getFullNodesProperties([receivedData.data]);
} else if (receivedData.type === 'removeNodeType') {
const pushData = receivedData.data;
const nodesToBeRemoved: INodeTypeNameVersion[] = [pushData];
// Force reload of all credential types
this.credentialsStore.fetchCredentialTypes(false)
.then(() => {
this.nodeTypesStore.removeNodeTypes(nodesToBeRemoved);
} else {
this.$showMessage({
title: this.$locale.baseText('pushConnection.workflowExecutedSuccessfully'),
type: 'success',
});
}
}
return true;
},
// It does not push the runData as it got already pushed with each
// node that did finish. For that reason copy in here the data
// which we already have.
if (this.workflowsStore.getWorkflowRunData) {
runDataExecuted.data.resultData.runData = this.workflowsStore.getWorkflowRunData;
}
this.workflowsStore.executingNode = null;
this.workflowsStore.setWorkflowExecutionData(runDataExecuted as IExecutionResponse);
this.uiStore.removeActiveAction('workflowRunning');
// Set the node execution issues on all the nodes which produced an error so that
// it can be displayed in the node-view
this.updateNodesExecutionIssues();
const lastNodeExecuted: string | undefined =
runDataExecuted.data.resultData.lastNodeExecuted;
let itemsCount = 0;
if (
lastNodeExecuted &&
runDataExecuted.data.resultData.runData[lastNodeExecuted as string] &&
!runDataExecutedErrorMessage
) {
itemsCount =
runDataExecuted.data.resultData.runData[lastNodeExecuted as string][0].data!.main[0]!
.length;
}
this.$externalHooks().run('pushConnection.executionFinished', {
itemsCount,
nodeName: runDataExecuted.data.resultData.lastNodeExecuted,
errorMessage: runDataExecutedErrorMessage,
runDataExecutedStartData: runDataExecuted.data.startData,
resultDataError: runDataExecuted.data.resultData.error,
});
} else if (receivedData.type === 'executionStarted') {
const pushData = receivedData.data;
const executionData: IExecutionsCurrentSummaryExtended = {
id: pushData.executionId,
finished: false,
mode: pushData.mode,
startedAt: pushData.startedAt,
retryOf: pushData.retryOf,
workflowId: pushData.workflowId,
workflowName: pushData.workflowName,
};
this.workflowsStore.addActiveExecution(executionData);
} else if (receivedData.type === 'nodeExecuteAfter') {
// A node finished to execute. Add its data
const pushData = receivedData.data;
this.workflowsStore.addNodeExecutionData(pushData);
} else if (receivedData.type === 'nodeExecuteBefore') {
// A node started to be executed. Set it as executing.
const pushData = receivedData.data;
this.workflowsStore.executingNode = pushData.nodeName;
} else if (receivedData.type === 'testWebhookDeleted') {
// A test-webhook was deleted
const pushData = receivedData.data;
if (pushData.workflowId === this.workflowsStore.workflowId) {
this.workflowsStore.executionWaitingForWebhook = false;
this.uiStore.removeActiveAction('workflowRunning');
}
} else if (receivedData.type === 'testWebhookReceived') {
// A test-webhook did get called
const pushData = receivedData.data;
if (pushData.workflowId === this.workflowsStore.workflowId) {
this.workflowsStore.executionWaitingForWebhook = false;
this.workflowsStore.activeExecutionId = pushData.executionId;
}
this.processWaitingPushMessages();
} else if (receivedData.type === 'reloadNodeType') {
this.nodeTypesStore.getNodeTypes();
this.nodeTypesStore.getFullNodesProperties([receivedData.data]);
} else if (receivedData.type === 'removeNodeType') {
const pushData = receivedData.data;
const nodesToBeRemoved: INodeTypeNameVersion[] = [pushData];
// Force reload of all credential types
this.credentialsStore.fetchCredentialTypes(false).then(() => {
this.nodeTypesStore.removeNodeTypes(nodesToBeRemoved);
});
}
return true;
},
});
},
});

View File

@@ -36,7 +36,7 @@ import { useRootStore } from '@/stores/n8nRootStore';
*
* @param {IExecutionFlattedResponse} fullExecutionData The data to unflatten
*/
function unflattenExecutionData (fullExecutionData: IExecutionFlattedResponse): IExecutionResponse {
function unflattenExecutionData(fullExecutionData: IExecutionFlattedResponse): IExecutionResponse {
// Unflatten the data
const returnData: IExecutionResponse = {
...fullExecutionData,
@@ -55,15 +55,18 @@ function unflattenExecutionData (fullExecutionData: IExecutionFlattedResponse):
export const restApi = Vue.extend({
computed: {
...mapStores(
useRootStore,
),
...mapStores(useRootStore),
},
methods: {
restApi (): IRestApi {
restApi(): IRestApi {
const self = this;
return {
async makeRestApiRequest (method: Method, endpoint: string, data?: IDataObject): Promise<any> { // tslint:disable-line:no-any
async makeRestApiRequest(
method: Method,
endpoint: string,
data?: IDataObject,
// tslint:disable-next-line:no-any
): Promise<any> {
return makeRestApiRequest(self.rootStore.getRestApiContext, method, endpoint, data);
},
getActiveWorkflows: (): Promise<string[]> => {
@@ -82,11 +85,15 @@ export const restApi = Vue.extend({
return self.restApi().makeRestApiRequest('GET', `/executions-current`, sendData);
},
stopCurrentExecution: (executionId: string): Promise<IExecutionsStopData> => {
return self.restApi().makeRestApiRequest('POST', `/executions-current/${executionId}/stop`);
return self
.restApi()
.makeRestApiRequest('POST', `/executions-current/${executionId}/stop`);
},
getCredentialTranslation: (credentialType): Promise<object> => {
return self.restApi().makeRestApiRequest('GET', '/credential-translation', { credentialType });
return self
.restApi()
.makeRestApiRequest('GET', '/credential-translation', { credentialType });
},
// Removes a test webhook
@@ -105,8 +112,18 @@ export const restApi = Vue.extend({
},
// Updates an existing workflow
updateWorkflow: (id: string, data: IWorkflowDataUpdate, forceSave = false): Promise<IWorkflowDb> => {
return self.restApi().makeRestApiRequest('PATCH', `/workflows/${id}${forceSave ? '?forceSave=true' : ''}`, data);
updateWorkflow: (
id: string,
data: IWorkflowDataUpdate,
forceSave = false,
): Promise<IWorkflowDb> => {
return self
.restApi()
.makeRestApiRequest(
'PATCH',
`/workflows/${id}${forceSave ? '?forceSave=true' : ''}`,
data,
);
},
// Deletes a workflow
@@ -159,7 +176,12 @@ export const restApi = Vue.extend({
// Returns all saved executions
// TODO: For sure needs some kind of default filter like last day, with max 10 results, ...
getPastExecutions: (filter: object, limit: number, lastId?: string | number, firstId?: string | number): Promise<IExecutionsListResponse> => {
getPastExecutions: (
filter: object,
limit: number,
lastId?: string | number,
firstId?: string | number,
): Promise<IExecutionsListResponse> => {
let sendData = {};
if (filter) {
sendData = {

View File

@@ -3,7 +3,7 @@ import { ElNotificationComponent, ElNotificationOptions } from 'element-ui/types
import mixins from 'vue-typed-mixins';
import { externalHooks } from '@/mixins/externalHooks';
import {IExecuteContextData, IRunExecutionData} from 'n8n-workflow';
import { IExecuteContextData, IRunExecutionData } from 'n8n-workflow';
import type { ElMessageBoxOptions } from 'element-ui/types/message-box';
import type { ElMessageComponent, ElMessageOptions, MessageType } from 'element-ui/types/message';
import { sanitizeHtml } from '@/utils';
@@ -14,9 +14,7 @@ let stickyNotificationQueue: ElNotificationComponent[] = [];
export const showMessage = mixins(externalHooks).extend({
computed: {
...mapStores(
useWorkflowsStore,
),
...mapStores(useWorkflowsStore),
},
methods: {
$showMessage(
@@ -24,7 +22,9 @@ export const showMessage = mixins(externalHooks).extend({
track = true,
) {
messageData.dangerouslyUseHTMLString = true;
messageData.message = messageData.message ? sanitizeHtml(messageData.message) : messageData.message;
messageData.message = messageData.message
? sanitizeHtml(messageData.message)
: messageData.message;
if (messageData.position === undefined) {
messageData.position = 'bottom-right';
@@ -49,15 +49,15 @@ export const showMessage = mixins(externalHooks).extend({
},
$showToast(config: {
title: string,
message: string,
onClick?: () => void,
onClose?: () => void,
duration?: number,
customClass?: string,
closeOnClick?: boolean,
type?: MessageType,
}) {
title: string;
message: string;
onClick?: () => void;
onClose?: () => void;
duration?: number;
customClass?: string;
closeOnClick?: boolean;
type?: MessageType;
}) {
// eslint-disable-next-line prefer-const
let notification: ElNotificationComponent;
if (config.closeOnClick) {
@@ -102,14 +102,10 @@ export const showMessage = mixins(externalHooks).extend({
if (error && error.message) {
let nodeName: string | undefined;
if ('node' in error) {
nodeName = typeof error.node === 'string'
? error.node
: error.node!.name;
nodeName = typeof error.node === 'string' ? error.node : error.node!.name;
}
const receivedError = nodeName
? `${nodeName}: ${error.message}`
: error.message;
const receivedError = nodeName ? `${nodeName}: ${error.message}` : error.message;
errorMessage = `There was a problem executing the workflow:<br /><strong>"${receivedError}"</strong>`;
}
}
@@ -120,15 +116,18 @@ export const showMessage = mixins(externalHooks).extend({
$showError(e: Error | unknown, title: string, message?: string) {
const error = e as Error;
const messageLine = message ? `${message}<br/>` : '';
this.$showMessage({
title,
message: `
this.$showMessage(
{
title,
message: `
${messageLine}
<i>${error.message}</i>
${this.collapsableDetails(error)}`,
type: 'error',
duration: 0,
}, false);
type: 'error',
duration: 0,
},
false,
);
this.$externalHooks().run('showMessage.showError', {
title,
@@ -145,9 +144,15 @@ export const showMessage = mixins(externalHooks).extend({
});
},
async confirmMessage (message: string, headline: string, type: MessageType | null = 'warning', confirmButtonText?: string, cancelButtonText?: string): Promise<boolean> {
async confirmMessage(
message: string,
headline: string,
type: MessageType | null = 'warning',
confirmButtonText?: string,
cancelButtonText?: string,
): Promise<boolean> {
try {
const options: ElMessageBoxOptions = {
const options: ElMessageBoxOptions = {
confirmButtonText: confirmButtonText || this.$locale.baseText('showMessage.ok'),
cancelButtonText: cancelButtonText || this.$locale.baseText('showMessage.cancel'),
dangerouslyUseHTMLString: true,
@@ -162,9 +167,16 @@ export const showMessage = mixins(externalHooks).extend({
}
},
async confirmModal (message: string, headline: string, type: MessageType | null = 'warning', confirmButtonText?: string, cancelButtonText?: string, showClose = false): Promise<string> {
async confirmModal(
message: string,
headline: string,
type: MessageType | null = 'warning',
confirmButtonText?: string,
cancelButtonText?: string,
showClose = false,
): Promise<string> {
try {
const options: ElMessageBoxOptions = {
const options: ElMessageBoxOptions = {
confirmButtonText: confirmButtonText || this.$locale.baseText('showMessage.ok'),
cancelButtonText: cancelButtonText || this.$locale.baseText('showMessage.cancel'),
dangerouslyUseHTMLString: true,
@@ -195,9 +207,7 @@ export const showMessage = mixins(externalHooks).extend({
if (!description) return '';
const errorDescription =
description.length > 500
? `${description.slice(0, 500)}...`
: description;
description.length > 500 ? `${description.slice(0, 500)}...` : description;
return `
<br>

View File

@@ -1,8 +1,6 @@
import Vue from 'vue';
import {
WorkflowTitleStatus,
} from '../../Interface';
import { WorkflowTitleStatus } from '../../Interface';
export const titleChange = Vue.extend({
methods: {
@@ -26,6 +24,5 @@ export const titleChange = Vue.extend({
$titleReset() {
document.title = `n8n - Workflow Automation`;
},
},
});

View File

@@ -7,7 +7,7 @@ import { Route } from 'vue-router';
export const userHelpers = Vue.extend({
methods: {
canUserAccessRouteByName(name: string): boolean {
const {route} = this.$router.resolve({name});
const { route } = this.$router.resolve({ name });
return this.canUserAccessRoute(route);
},

View File

@@ -2,114 +2,122 @@ import { externalHooks } from '@/mixins/externalHooks';
import { workflowHelpers } from '@/mixins/workflowHelpers';
import { showMessage } from '@/mixins/showMessage';
import mixins from 'vue-typed-mixins';
import { LOCAL_STORAGE_ACTIVATION_FLAG, PLACEHOLDER_EMPTY_WORKFLOW_ID, WORKFLOW_ACTIVE_MODAL_KEY } from '@/constants';
import {
LOCAL_STORAGE_ACTIVATION_FLAG,
PLACEHOLDER_EMPTY_WORKFLOW_ID,
WORKFLOW_ACTIVE_MODAL_KEY,
} from '@/constants';
import { mapStores } from 'pinia';
import { useUIStore } from '@/stores/ui';
import { useSettingsStore } from '@/stores/settings';
import { useWorkflowsStore } from '@/stores/workflows';
export const workflowActivate = mixins(
externalHooks,
workflowHelpers,
showMessage,
)
.extend({
data() {
return {
updatingWorkflowActivation: false,
};
export const workflowActivate = mixins(externalHooks, workflowHelpers, showMessage).extend({
data() {
return {
updatingWorkflowActivation: false,
};
},
computed: {
...mapStores(useSettingsStore, useUIStore, useWorkflowsStore),
},
methods: {
async activateCurrentWorkflow(telemetrySource?: string) {
const workflowId = this.workflowsStore.workflowId;
return this.updateWorkflowActivation(workflowId, true, telemetrySource);
},
computed: {
...mapStores(
useSettingsStore,
useUIStore,
useWorkflowsStore,
),
},
methods: {
async activateCurrentWorkflow(telemetrySource?: string) {
const workflowId = this.workflowsStore.workflowId;
return this.updateWorkflowActivation(workflowId, true, telemetrySource);
},
async updateWorkflowActivation(workflowId: string | undefined, newActiveState: boolean, telemetrySource?: string) {
this.updatingWorkflowActivation = true;
const nodesIssuesExist = this.workflowsStore.nodesIssuesExist as boolean;
async updateWorkflowActivation(
workflowId: string | undefined,
newActiveState: boolean,
telemetrySource?: string,
) {
this.updatingWorkflowActivation = true;
const nodesIssuesExist = this.workflowsStore.nodesIssuesExist as boolean;
let currWorkflowId: string | undefined = workflowId;
if (!currWorkflowId || currWorkflowId === PLACEHOLDER_EMPTY_WORKFLOW_ID) {
const saved = await this.saveCurrentWorkflow();
if (!saved) {
this.updatingWorkflowActivation = false;
return;
}
currWorkflowId = this.workflowsStore.workflowId as string;
let currWorkflowId: string | undefined = workflowId;
if (!currWorkflowId || currWorkflowId === PLACEHOLDER_EMPTY_WORKFLOW_ID) {
const saved = await this.saveCurrentWorkflow();
if (!saved) {
this.updatingWorkflowActivation = false;
return;
}
const isCurrentWorkflow = currWorkflowId === this.workflowsStore.workflowId;
currWorkflowId = this.workflowsStore.workflowId as string;
}
const isCurrentWorkflow = currWorkflowId === this.workflowsStore.workflowId;
const activeWorkflows = this.workflowsStore.activeWorkflows;
const isWorkflowActive = activeWorkflows.includes(currWorkflowId);
const activeWorkflows = this.workflowsStore.activeWorkflows;
const isWorkflowActive = activeWorkflows.includes(currWorkflowId);
const telemetryPayload = {
workflow_id: currWorkflowId,
is_active: newActiveState,
previous_status: isWorkflowActive,
ndv_input: telemetrySource === 'ndv',
};
this.$telemetry.track('User set workflow active status', telemetryPayload);
this.$externalHooks().run('workflowActivate.updateWorkflowActivation', telemetryPayload);
const telemetryPayload = {
workflow_id: currWorkflowId,
is_active: newActiveState,
previous_status: isWorkflowActive,
ndv_input: telemetrySource === 'ndv',
};
this.$telemetry.track('User set workflow active status', telemetryPayload);
this.$externalHooks().run('workflowActivate.updateWorkflowActivation', telemetryPayload);
try {
if (isWorkflowActive && newActiveState) {
this.$showMessage({
title: this.$locale.baseText('workflowActivator.workflowIsActive'),
type: 'success',
});
this.updatingWorkflowActivation = false;
try {
if (isWorkflowActive && newActiveState) {
this.$showMessage({
title: this.$locale.baseText('workflowActivator.workflowIsActive'),
type: 'success',
});
this.updatingWorkflowActivation = false;
return;
}
return;
}
if (isCurrentWorkflow && nodesIssuesExist) {
this.$showMessage({
title: this.$locale.baseText('workflowActivator.showMessage.activeChangedNodesIssuesExistTrue.title'),
message: this.$locale.baseText('workflowActivator.showMessage.activeChangedNodesIssuesExistTrue.message'),
type: 'error',
});
if (isCurrentWorkflow && nodesIssuesExist) {
this.$showMessage({
title: this.$locale.baseText(
'workflowActivator.showMessage.activeChangedNodesIssuesExistTrue.title',
),
message: this.$locale.baseText(
'workflowActivator.showMessage.activeChangedNodesIssuesExistTrue.message',
),
type: 'error',
});
this.updatingWorkflowActivation = false;
return;
}
await this.updateWorkflow({workflowId: currWorkflowId, active: newActiveState});
} catch (error) {
const newStateName = newActiveState === true ? 'activated' : 'deactivated';
this.$showError(
error,
this.$locale.baseText(
'workflowActivator.showError.title',
{ interpolate: { newStateName } },
) + ':',
);
this.updatingWorkflowActivation = false;
return;
}
const activationEventName = isCurrentWorkflow ? 'workflow.activeChangeCurrent' : 'workflow.activeChange';
this.$externalHooks().run(activationEventName, { workflowId: currWorkflowId, active: newActiveState });
this.$emit('workflowActiveChanged', { id: currWorkflowId, active: newActiveState });
await this.updateWorkflow({ workflowId: currWorkflowId, active: newActiveState });
} catch (error) {
const newStateName = newActiveState === true ? 'activated' : 'deactivated';
this.$showError(
error,
this.$locale.baseText('workflowActivator.showError.title', {
interpolate: { newStateName },
}) + ':',
);
this.updatingWorkflowActivation = false;
return;
}
if (isCurrentWorkflow) {
if (newActiveState && window.localStorage.getItem(LOCAL_STORAGE_ACTIVATION_FLAG) !== 'true') {
this.uiStore.openModal(WORKFLOW_ACTIVE_MODAL_KEY);
}
else {
this.settingsStore.fetchPromptsData();
}
const activationEventName = isCurrentWorkflow
? 'workflow.activeChangeCurrent'
: 'workflow.activeChange';
this.$externalHooks().run(activationEventName, {
workflowId: currWorkflowId,
active: newActiveState,
});
this.$emit('workflowActiveChanged', { id: currWorkflowId, active: newActiveState });
this.updatingWorkflowActivation = false;
if (isCurrentWorkflow) {
if (
newActiveState &&
window.localStorage.getItem(LOCAL_STORAGE_ACTIVATION_FLAG) !== 'true'
) {
this.uiStore.openModal(WORKFLOW_ACTIVE_MODAL_KEY);
} else {
this.settingsStore.fetchPromptsData();
}
},
}
},
});
},
});

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +1,4 @@
import {
IExecutionPushResponse,
IExecutionResponse,
IStartRunData,
} from '@/Interface';
import { IExecutionPushResponse, IExecutionResponse, IStartRunData } from '@/Interface';
import {
IRunData,
@@ -32,21 +28,15 @@ export const workflowRun = mixins(
titleChange,
).extend({
computed: {
...mapStores(
useRootStore,
useUIStore,
useWorkflowsStore,
),
...mapStores(useRootStore, useUIStore, useWorkflowsStore),
},
methods: {
// Starts to executes a workflow on server.
async runWorkflowApi (runData: IStartRunData): Promise<IExecutionPushResponse> {
async runWorkflowApi(runData: IStartRunData): Promise<IExecutionPushResponse> {
if (this.rootStore.pushConnectionActive === false) {
// Do not start if the connection to server is not active
// because then it can not receive the data as it executes.
throw new Error(
this.$locale.baseText('workflowRun.noActiveConnectionToTheServer'),
);
throw new Error(this.$locale.baseText('workflowRun.noActiveConnectionToTheServer'));
}
this.workflowsStore.subWorkflowExecutionError = null;
@@ -72,7 +62,10 @@ export const workflowRun = mixins(
return response;
},
async runWorkflow (nodeName?: string, source?: string): Promise<IExecutionPushResponse | undefined> {
async runWorkflow(
nodeName?: string,
source?: string,
): Promise<IExecutionPushResponse | undefined> {
const workflow = this.getCurrentWorkflow();
if (this.uiStore.isActionActive('workflowRunning')) {
@@ -134,7 +127,12 @@ export const workflowRun = mixins(
workflow_id: workflow.id,
workflow_name: workflow.name,
execution_type: nodeName ? 'node' : 'workflow',
node_graph_string: JSON.stringify(TelemetryHelpers.generateNodesGraph(workflowData as IWorkflowBase, this.getNodeTypes()).nodeGraph),
node_graph_string: JSON.stringify(
TelemetryHelpers.generateNodesGraph(
workflowData as IWorkflowBase,
this.getNodeTypes(),
).nodeGraph,
),
error_node_types: JSON.stringify(trackErrorNodeTypes),
errors: JSON.stringify(trackNodeIssues),
});
@@ -245,13 +243,10 @@ export const workflowRun = mixins(
this.$externalHooks().run('workflowRun.runWorkflow', { nodeName, source });
return runWorkflowApiResponse;
return runWorkflowApiResponse;
} catch (error) {
this.$titleSet(workflow.name as string, 'ERROR');
this.$showError(
error,
this.$locale.baseText('workflowRun.showError.title'),
);
this.$showError(error, this.$locale.baseText('workflowRun.showError.title'));
return undefined;
}
},