From 408bd968152ad8bbafda7037f6eb8f5550d04c77 Mon Sep 17 00:00:00 2001 From: Csaba Tuncsik Date: Mon, 24 Oct 2022 20:17:25 +0200 Subject: [PATCH] feat(editor): add readonly state for nodes (#4299) * fix(editor): add types to Node.vue component props * fix(editor): some cleanup in NodeView * fix(editor): fix some boolean usage * feat(editor): check foreign credentials * fix(editor): passing readOnly to inputs * fix(editor): add types to component and solve property mutation * fix(editor): add types to component and solve property mutation * fix(editor): component property type * fix(editor): component property type * fix(editor): default prop values * fix(editor): fix FixedCollectionParameter.vue --- packages/editor-ui/src/Interface.ts | 64 ++++++++--- packages/editor-ui/src/api/credentials.ts | 6 + .../src/components/CollectionParameter.vue | 5 +- .../src/components/CredentialsSelect.vue | 1 + .../components/FixedCollectionParameter.vue | 105 +++++++++++------- .../src/components/ImportParameter.vue | 7 ++ .../src/components/MultipleParameter.vue | 85 ++++++++------ packages/editor-ui/src/components/Node.vue | 4 +- .../src/components/NodeDetailsView.vue | 16 ++- .../editor-ui/src/components/NodeSettings.vue | 12 +- .../src/components/ParameterInput.vue | 2 + .../src/components/ParameterInputList.vue | 7 +- .../ResourceLocator/ResourceLocator.vue | 1 + .../editor-ui/src/components/TextEdit.vue | 3 +- .../src/components/mixins/genericHelpers.ts | 2 +- .../src/components/mixins/nodeBase.ts | 62 ++++++----- .../src/components/mixins/nodeHelpers.ts | 33 +++--- packages/editor-ui/src/modules/credentials.ts | 25 ++++- packages/editor-ui/src/plugins/i18n/index.ts | 6 +- packages/editor-ui/src/views/NodeView.vue | 95 ++++++---------- packages/editor-ui/src/views/canvasHelpers.ts | 12 +- packages/workflow/src/index.ts | 8 ++ packages/workflow/src/type-guards.ts | 27 +++++ 23 files changed, 369 insertions(+), 219 deletions(-) create mode 100644 packages/workflow/src/type-guards.ts diff --git a/packages/editor-ui/src/Interface.ts b/packages/editor-ui/src/Interface.ts index f9baa2be45..4613303b1c 100644 --- a/packages/editor-ui/src/Interface.ts +++ b/packages/editor-ui/src/Interface.ts @@ -1,3 +1,14 @@ +import { + jsPlumbInstance, + DragOptions, + DropOptions, + ElementGroupRef, + Endpoint, + EndpointOptions, + EndpointRectangle, + EndpointRectangleOptions, + EndpointSpec, +} from "jsplumb"; import { GenericValue, IConnections, @@ -104,24 +115,40 @@ declare module 'jsplumb' { } // EndpointOptions from jsplumb seems incomplete and wrong so we define an own one -export interface IEndpointOptions { - anchor?: any; // tslint:disable-line:no-any - createEndpoint?: boolean; - dragAllowedWhenFull?: boolean; - dropOptions?: any; // tslint:disable-line:no-any - dragProxy?: any; // tslint:disable-line:no-any - endpoint?: string; - endpointStyle?: object; - endpointHoverStyle?: object; - isSource?: boolean; - isTarget?: boolean; - maxConnections?: number; - overlays?: any; // tslint:disable-line:no-any - parameters?: any; // tslint:disable-line:no-any - uuid?: string; - enabled?: boolean; - cssClass?: string; -} +export type IEndpointOptions = Omit & { + endpointStyle: EndpointStyle + endpointHoverStyle: EndpointStyle + endpoint?: EndpointSpec | string + dragAllowedWhenFull?: boolean + dropOptions?: DropOptions & { + tolerance: string + }; + dragProxy?: string | string[] | EndpointSpec | [ EndpointRectangle, EndpointRectangleOptions & { strokeWidth: number } ] +}; + +export type EndpointStyle = { + width?: number + height?: number + fill?: string + stroke?: string + outlineStroke?:string + lineWidth?: number + hover?: boolean + showOutputLabel?: boolean + size?: string + hoverMessage?: string +}; + +export type IDragOptions = DragOptions & { + grid: [number, number] + filter: string +}; + +export type IJsPlumbInstance = Omit & { + clearDragSelection: () => void + addEndpoint(el: ElementGroupRef, params?: IEndpointOptions, referenceParams?: IEndpointOptions): Endpoint | Endpoint[] + draggable(el: {}, options?: IDragOptions): IJsPlumbInstance +}; export interface IUpdateInformation { name: string; @@ -908,6 +935,7 @@ export interface ICredentialMap { export interface ICredentialsState { credentialTypes: ICredentialTypeMap; credentials: ICredentialMap; + foreignCredentials?: ICredentialMap; } export interface ITagsState { diff --git a/packages/editor-ui/src/api/credentials.ts b/packages/editor-ui/src/api/credentials.ts index 60a81e061e..9ff6df1c64 100644 --- a/packages/editor-ui/src/api/credentials.ts +++ b/packages/editor-ui/src/api/credentials.ts @@ -51,3 +51,9 @@ export async function oAuth2CredentialAuthorize(context: IRestApiContext, data: export async function testCredential(context: IRestApiContext, data: INodeCredentialTestRequest): Promise { return makeRestApiRequest(context, 'POST', '/credentials/test', data as unknown as IDataObject); } + +export async function getForeignCredentials(context: IRestApiContext): Promise { + // TODO: Get foreign credentials + //return await makeRestApiRequest(context, 'GET', '/foreign-credentials'); + return []; +} diff --git a/packages/editor-ui/src/components/CollectionParameter.vue b/packages/editor-ui/src/components/CollectionParameter.vue index 69c943a724..1832e2f0f0 100644 --- a/packages/editor-ui/src/components/CollectionParameter.vue +++ b/packages/editor-ui/src/components/CollectionParameter.vue @@ -5,7 +5,7 @@ {{ $locale.baseText('collectionParameter.noProperties') }} - +
import('./ParameterInputList.vue') as Promise, diff --git a/packages/editor-ui/src/components/CredentialsSelect.vue b/packages/editor-ui/src/components/CredentialsSelect.vue index a03b8c4c44..f1999c190a 100644 --- a/packages/editor-ui/src/components/CredentialsSelect.vue +++ b/packages/editor-ui/src/components/CredentialsSelect.vue @@ -7,6 +7,7 @@ :value="displayValue" :placeholder="parameter.placeholder ? getPlaceholder() : $locale.baseText('parameterInput.select')" :title="displayTitle" + :disabled="isReadOnly" ref="innerSelect" @change="(value) => $emit('valueChanged', value)" @keydown.stop diff --git a/packages/editor-ui/src/components/FixedCollectionParameter.vue b/packages/editor-ui/src/components/FixedCollectionParameter.vue index 63eca4c079..e67f59dda3 100644 --- a/packages/editor-ui/src/components/FixedCollectionParameter.vue +++ b/packages/editor-ui/src/components/FixedCollectionParameter.vue @@ -16,9 +16,9 @@ size="small" color="text-dark" /> -
+
@@ -39,7 +39,7 @@ @click="moveOptionUp(property.name, index)" />
@@ -71,6 +72,7 @@ :parameters="property.values" :nodeValues="nodeValues" :path="getPropertyPath(property.name)" + :isReadOnly="isReadOnly" class="parameter-item" @valueChanged="valueChanged" :hideDelete="true" @@ -108,42 +110,66 @@ diff --git a/packages/editor-ui/src/components/NodeSettings.vue b/packages/editor-ui/src/components/NodeSettings.vue index b5c242869d..449c66a663 100644 --- a/packages/editor-ui/src/components/NodeSettings.vue +++ b/packages/editor-ui/src/components/NodeSettings.vue @@ -7,8 +7,8 @@ class="node-name" :value="node && node.name" :nodeType="nodeType" + :isReadOnly="isReadOnly" @input="nameChanged" - :readOnly="isReadOnly" >
-
@@ -99,6 +99,7 @@ @@ -106,6 +107,7 @@ :parameters="nodeSettings" :hideDelete="true" :nodeValues="nodeValues" + :isReadOnly="isReadOnly" path="" @valueChanged="valueChanged" /> @@ -142,14 +144,13 @@ import NodeWebhooks from '@/components/NodeWebhooks.vue'; import { get, set, unset } from 'lodash'; import { externalHooks } from '@/components/mixins/externalHooks'; -import { genericHelpers } from '@/components/mixins/genericHelpers'; import { nodeHelpers } from '@/components/mixins/nodeHelpers'; import mixins from 'vue-typed-mixins'; import NodeExecuteButton from './NodeExecuteButton.vue'; import { isCommunityPackageName } from './helpers'; -export default mixins(externalHooks, genericHelpers, nodeHelpers).extend({ +export default mixins(externalHooks, nodeHelpers).extend({ name: 'NodeSettings', components: { NodeTitle, @@ -235,6 +236,9 @@ export default mixins(externalHooks, genericHelpers, nodeHelpers).extend({ nodeType: { type: Object as PropType, }, + isReadOnly: { + type: Boolean, + }, }, data() { return { diff --git a/packages/editor-ui/src/components/ParameterInput.vue b/packages/editor-ui/src/components/ParameterInput.vue index e6362d648d..27f9a92b6d 100644 --- a/packages/editor-ui/src/components/ParameterInput.vue +++ b/packages/editor-ui/src/components/ParameterInput.vue @@ -40,6 +40,7 @@ :rows="getArgument('rows')" :value="expressionDisplayValue" :title="displayTitle" + :readOnly="isReadOnly" @keydown.stop />
diff --git a/packages/editor-ui/src/components/ParameterInputList.vue b/packages/editor-ui/src/components/ParameterInputList.vue index 49c8173e70..7d91ed798f 100644 --- a/packages/editor-ui/src/components/ParameterInputList.vue +++ b/packages/editor-ui/src/components/ParameterInputList.vue @@ -12,12 +12,14 @@ :values="getParameterValue(nodeValues, parameter.name, path)" :nodeValues="nodeValues" :path="getPath(parameter.name)" + :isReadOnly="isReadOnly" @valueChanged="valueChanged" />
@@ -53,6 +55,7 @@ :values="getParameterValue(nodeValues, parameter.name, path)" :nodeValues="nodeValues" :path="getPath(parameter.name)" + :isReadOnly="isReadOnly" @valueChanged="valueChanged" />
@@ -107,7 +111,6 @@ import { import { INodeUi, IUpdateInformation } from '@/Interface'; import MultipleParameter from '@/components/MultipleParameter.vue'; -import { genericHelpers } from '@/components/mixins/genericHelpers'; import { workflowHelpers } from '@/components/mixins/workflowHelpers'; import ParameterInputFull from '@/components/ParameterInputFull.vue'; import ImportParameter from '@/components/ImportParameter.vue'; @@ -118,7 +121,6 @@ import mixins from 'vue-typed-mixins'; import {Component} from "vue"; export default mixins( - genericHelpers, workflowHelpers, ) .extend({ @@ -136,6 +138,7 @@ export default mixins( 'path', // string 'hideDelete', // boolean 'indent', + 'isReadOnly', ], computed: { nodeTypeVersion(): number | null { diff --git a/packages/editor-ui/src/components/ResourceLocator/ResourceLocator.vue b/packages/editor-ui/src/components/ResourceLocator/ResourceLocator.vue index e86429c09d..d0535690e9 100644 --- a/packages/editor-ui/src/components/ResourceLocator/ResourceLocator.vue +++ b/packages/editor-ui/src/components/ResourceLocator/ResourceLocator.vue @@ -84,6 +84,7 @@ :size="inputSize" :value="expressionDisplayValue" :title="displayTitle" + :disabled="isReadOnly" @keydown.stop ref="input" /> diff --git a/packages/editor-ui/src/components/TextEdit.vue b/packages/editor-ui/src/components/TextEdit.vue index 7ee367556d..7b4b49a052 100644 --- a/packages/editor-ui/src/components/TextEdit.vue +++ b/packages/editor-ui/src/components/TextEdit.vue @@ -5,7 +5,7 @@
- +
@@ -24,6 +24,7 @@ export default Vue.extend({ 'parameter', 'path', 'value', + 'isReadOnly', ], data () { return { diff --git a/packages/editor-ui/src/components/mixins/genericHelpers.ts b/packages/editor-ui/src/components/mixins/genericHelpers.ts index 1306532152..2414881a15 100644 --- a/packages/editor-ui/src/components/mixins/genericHelpers.ts +++ b/packages/editor-ui/src/components/mixins/genericHelpers.ts @@ -17,7 +17,7 @@ export const genericHelpers = mixins(showMessage).extend({ methods: { displayTimer (msPassed: number, showMs = false): string { if (msPassed < 60000) { - if (showMs === false) { + if (!showMs) { return `${Math.floor(msPassed / 1000)} ${this.$locale.baseText('genericHelpers.sec')}`; } diff --git a/packages/editor-ui/src/components/mixins/nodeBase.ts b/packages/editor-ui/src/components/mixins/nodeBase.ts index 22ffb78511..62e8e781eb 100644 --- a/packages/editor-ui/src/components/mixins/nodeBase.ts +++ b/packages/editor-ui/src/components/mixins/nodeBase.ts @@ -1,11 +1,9 @@ -import { IEndpointOptions, INodeUi, XYPosition } from '@/Interface'; - +import { PropType } from "vue"; import mixins from 'vue-typed-mixins'; - +import { IJsPlumbInstance, IEndpointOptions, INodeUi, XYPosition } from '@/Interface'; import { deviceSupportHelpers } from '@/components/mixins/deviceSupportHelpers'; import { NO_OP_NODE_TYPE, STICKY_NODE_TYPE } from '@/constants'; import * as CanvasHelpers from '@/views/canvasHelpers'; -import { Endpoint } from 'jsplumb'; import { INodeTypeDescription, @@ -34,9 +32,7 @@ export const nodeBase = mixins( type: String, }, instance: { - // We can't use PropType here because the version of jsplumb doesn't - // include correct typing for draggable instance(`clearDragSelection`, `destroyDraggable`, etc.) - type: Object, + type: Object as PropType, }, isReadOnly: { type: Boolean, @@ -104,13 +100,15 @@ export const nodeBase = mixins( ]; } - const endpoint: Endpoint = this.instance.addEndpoint(this.nodeId, newEndpointData); - endpoint.__meta = { - nodeName: node.name, - nodeId: this.nodeId, - index: i, - totalEndpoints: nodeTypeData.inputs.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.inputs.length, + }; + } // TODO: Activate again if it makes sense. Currently makes problems when removing // connection on which the input has a name. It does not get hidden because @@ -159,7 +157,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) { @@ -169,13 +167,15 @@ export const nodeBase = mixins( ]; } - const endpoint: Endpoint = this.instance.addEndpoint(this.nodeId, {...newEndpointData}); - 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 = { @@ -206,16 +206,18 @@ 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: Endpoint = this.instance.addEndpoint(this.nodeId, plusEndpointData); - plusEndpoint.__meta = { - nodeName: node.name, - nodeId: this.nodeId, - index: i, - totalEndpoints: nodeTypeData.outputs.length, - }; + const plusEndpoint = this.instance.addEndpoint(this.nodeId, plusEndpointData); + if(!Array.isArray(plusEndpoint)) { + plusEndpoint.__meta = { + nodeName: node.name, + nodeId: this.nodeId, + index: i, + totalEndpoints: nodeTypeData.outputs.length, + }; + } } }); }, diff --git a/packages/editor-ui/src/components/mixins/nodeHelpers.ts b/packages/editor-ui/src/components/mixins/nodeHelpers.ts index 538129281a..110691c994 100644 --- a/packages/editor-ui/src/components/mixins/nodeHelpers.ts +++ b/packages/editor-ui/src/components/mixins/nodeHelpers.ts @@ -27,7 +27,7 @@ import { import { ICredentialsResponse, INodeUi, -} from '../../Interface'; +} from '@/Interface'; import { restApi } from '@/components/mixins/restApi'; @@ -214,31 +214,36 @@ export const nodeHelpers = mixins( // Returns all the credential-issues of the node getNodeCredentialIssues (node: INodeUi, nodeType?: INodeTypeDescription): INodeIssues | null { - if (node.disabled === true) { + if (node.disabled) { // Node is disabled return null; } - if (nodeType === undefined) { + if (!nodeType) { nodeType = this.$store.getters['nodeTypes/getNodeType'](node.type, node.typeVersion); } - if (nodeType === null || nodeType!.credentials === undefined) { + if (!nodeType?.credentials) { // Node does not need any credentials or nodeType could not be found return null; } - if (nodeType!.credentials === undefined) { - // No credentials defined for node type - return null; - } - const foundIssues: INodeIssueObjectProperty = {}; let userCredentials: ICredentialsResponse[] | null; let credentialType: ICredentialType | null; let credentialDisplayName: string; let selectedCredentials: INodeCredentialsDetails; + const foreignCredentials = this.$store.getters['credentials/allForeignCredentials']; + + // TODO: Check if any of the node credentials is found in foreign credentials + if(foreignCredentials?.some(() => true)){ + return { + credentials: { + foreign: [], + }, + }; + } const { authentication, @@ -279,9 +284,9 @@ export const nodeHelpers = mixins( return this.reportUnsetCredential(credential); } - for (const credentialTypeDescription of nodeType!.credentials!) { + for (const credentialTypeDescription of nodeType.credentials) { // Check if credentials should be displayed else ignore - if (this.displayParameter(node.parameters, credentialTypeDescription, '', node) !== true) { + if (!this.displayParameter(node.parameters, credentialTypeDescription, '', node)) { continue; } @@ -293,9 +298,9 @@ export const nodeHelpers = mixins( credentialDisplayName = credentialType.displayName; } - if (node.credentials === undefined || node.credentials[credentialTypeDescription.name] === undefined) { + if (!node.credentials || !node.credentials?.[credentialTypeDescription.name]) { // Credentials are not set - if (credentialTypeDescription.required === true) { + if (credentialTypeDescription.required) { foundIssues[credentialTypeDescription.name] = [this.$locale.baseText('nodeIssues.credentials.notSet', { interpolate: { type: credentialDisplayName } })]; } } else { @@ -493,7 +498,7 @@ export const nodeHelpers = mixins( * selected credentials are of the specified type. */ function selectedCredsAreUnusable(node: INodeUi, credentialType: string) { - return node.credentials === undefined || Object.keys(node.credentials).includes(credentialType) === false; + return !node.credentials || !Object.keys(node.credentials).includes(credentialType); } /** diff --git a/packages/editor-ui/src/modules/credentials.ts b/packages/editor-ui/src/modules/credentials.ts index 2258c04702..9922ae0186 100644 --- a/packages/editor-ui/src/modules/credentials.ts +++ b/packages/editor-ui/src/modules/credentials.ts @@ -1,4 +1,5 @@ -import { getCredentialTypes, +import { + getCredentialTypes, getCredentialsNewName, getAllCredentials, deleteCredential, @@ -8,6 +9,7 @@ import { getCredentialTypes, oAuth2CredentialAuthorize, oAuth1CredentialAuthorize, testCredential, + getForeignCredentials, } from '@/api/credentials'; import Vue from 'vue'; import { ActionContext, Module } from 'vuex'; @@ -17,7 +19,7 @@ import { ICredentialsState, ICredentialTypeMap, IRootState, -} from '../Interface'; +} from '@/Interface'; import { ICredentialType, ICredentialsDecrypted, @@ -58,6 +60,15 @@ const module: Module = { return accu; }, {}); }, + setForeignCredentials: (state: ICredentialsState, credentials: ICredentialsResponse[]) => { + state.foreignCredentials = credentials.reduce((accu: ICredentialMap, cred: ICredentialsResponse) => { + if (cred.id) { + accu[cred.id] = cred; + } + + return accu; + }, {}); + }, upsertCredential(state: ICredentialsState, credential: ICredentialsResponse) { if (credential.id) { Vue.set(state.credentials, credential.id, { ...state.credentials[credential.id], ...credential }); @@ -83,6 +94,10 @@ const module: Module = { return Object.values(state.credentials) .sort((a, b) => a.name.localeCompare(b.name)); }, + allForeignCredentials(state: ICredentialsState): ICredentialsResponse[] { + return Object.values(state.foreignCredentials || {}) + .sort((a, b) => a.name.localeCompare(b.name)); + }, allCredentialsByType(state: ICredentialsState, getters: any): {[type: string]: ICredentialsResponse[]} { // tslint:disable-line:no-any const credentials = getters.allCredentials as ICredentialsResponse[]; const types = getters.allCredentialTypes as ICredentialType[]; @@ -181,6 +196,12 @@ const module: Module = { return credentials; }, + fetchForeignCredentials: async (context: ActionContext): Promise => { + const credentials = await getForeignCredentials(context.rootGetters.getRestApiContext); + context.commit('setForeignCredentials', credentials); + + return credentials; + }, getCredentialData: async (context: ActionContext, { id }: {id: string}) => { return await getCredentialData(context.rootGetters.getRestApiContext, id); }, diff --git a/packages/editor-ui/src/plugins/i18n/index.ts b/packages/editor-ui/src/plugins/i18n/index.ts index 6f3507baf8..82bfa2b7d2 100644 --- a/packages/editor-ui/src/plugins/i18n/index.ts +++ b/packages/editor-ui/src/plugins/i18n/index.ts @@ -15,6 +15,7 @@ import { } from 'n8n-design-system'; import englishBaseText from './locales/en.json'; +import { INodeProperties } from "n8n-workflow"; Vue.use(VueI18n); locale.use('en'); @@ -335,12 +336,11 @@ export class I18nClass { * `fixedCollection` param having `multipleValues: true`. */ multipleValueButtonText( - { name: parameterName, typeOptions: { multipleValueButtonText } }: - { name: string; typeOptions: { multipleValueButtonText: string; } }, + { name: parameterName, typeOptions}: INodeProperties, ) { return context.dynamicRender({ key: `${initialKey}.${parameterName}.multipleValueButtonText`, - fallback: multipleValueButtonText, + fallback: typeOptions!.multipleValueButtonText!, }); }, diff --git a/packages/editor-ui/src/views/NodeView.vue b/packages/editor-ui/src/views/NodeView.vue index a3646c0028..8cdbde4c8a 100644 --- a/packages/editor-ui/src/views/NodeView.vue +++ b/packages/editor-ui/src/views/NodeView.vue @@ -254,26 +254,6 @@ export default mixins( // When a node gets set as active deactivate the create-menu this.createNodeActive = false; }, - nodes: { - async handler () { - // Load a workflow - let workflowId = null as string | null; - if (this.$route && this.$route.params.name) { - workflowId = this.$route.params.name; - } - }, - deep: true, - }, - connections: { - async handler(value, oldValue) { - // Load a workflow - let workflowId = null as string | null; - if (this.$route && this.$route.params.name) { - workflowId = this.$route.params.name; - } - }, - deep: true, - }, containsTrigger(containsTrigger) { // Re-center CanvasAddButton if there's no triggers if (containsTrigger === false) this.setRecenteredCanvasAddButtonPosition(this.getNodeViewOffsetPosition); @@ -348,11 +328,11 @@ export default mixins( return this.$store.getters.allNodes; }, runButtonText(): string { - if (this.workflowRunning === false) { + if (!this.workflowRunning) { return this.$locale.baseText('nodeView.runButtonText.executeWorkflow'); } - if (this.executionWaitingForWebhook === true) { + if (this.executionWaitingForWebhook) { return this.$locale.baseText('nodeView.runButtonText.waitingForTriggerEvent'); } @@ -375,14 +355,14 @@ export default mixins( }, workflowClasses() { const returnClasses = []; - if (this.ctrlKeyPressed === true) { + if (this.ctrlKeyPressed) { if (this.$store.getters.isNodeViewMoveInProgress === true) { returnClasses.push('move-in-process'); } else { returnClasses.push('move-active'); } } - if (this.selectActive || this.ctrlKeyPressed === true) { + if (this.selectActive || this.ctrlKeyPressed) { // Makes sure that nothing gets selected while select or move is active returnClasses.push('do-not-select'); } @@ -612,7 +592,7 @@ export default mixins( this.$externalHooks().run('execution.open', { workflowId: data.workflowData.id, workflowName: data.workflowData.name, executionId }); this.$telemetry.track('User opened read-only execution', { workflow_id: data.workflowData.id, execution_mode: data.mode, execution_finished: data.finished }); - if (data.finished !== true && data && data.data && data.data.resultData && data.data.resultData.error) { + if (!data.finished && data.data?.resultData?.error) { // Check if any node contains an error let nodeErrorFound = false; if (data.data.resultData.runData) { @@ -628,7 +608,7 @@ export default mixins( } } - if (nodeErrorFound === false) { + if (!nodeErrorFound) { const resultError = data.data.resultData.error; const errorMessage = this.$getExecutionError(data.data); const shouldTrack = resultError && 'node' in resultError && resultError.node!.type.startsWith('n8n-nodes-base'); @@ -724,7 +704,7 @@ export default mixins( return; } - if (data === undefined) { + if (!data) { throw new Error( this.$locale.baseText( 'nodeView.workflowWithIdCouldNotBeFound', @@ -800,9 +780,9 @@ export default mixins( // Check if the keys got emitted from a message box or from something // else which should ignore the default keybindings - for (let index = 0; index < path.length; index++) { - if (path[index].className && typeof path[index].className === 'string' && ( - path[index].className.includes('ignore-key-press') + for (const element of path) { + if (element.className && typeof element.className === 'string' && ( + element.className.includes('ignore-key-press') )) { return; } @@ -854,21 +834,21 @@ export default mixins( this.resetZoom(); } else if ((e.key === '1') && !this.isCtrlKeyPressed(e)) { this.zoomToFit(); - } else if ((e.key === 'a') && (this.isCtrlKeyPressed(e) === true)) { + } else if ((e.key === 'a') && this.isCtrlKeyPressed(e)) { // Select all nodes e.stopPropagation(); e.preventDefault(); this.callDebounced('selectAllNodes', { debounceTime: 1000 }); - } else if ((e.key === 'c') && (this.isCtrlKeyPressed(e) === true)) { + } else if ((e.key === 'c') && this.isCtrlKeyPressed(e)) { this.callDebounced('copySelectedNodes', { debounceTime: 1000 }); - } else if ((e.key === 'x') && (this.isCtrlKeyPressed(e) === true)) { + } else if ((e.key === 'x') && this.isCtrlKeyPressed(e)) { // Cut nodes e.stopPropagation(); e.preventDefault(); this.callDebounced('cutSelectedNodes', { debounceTime: 1000 }); - } else if (e.key === 'n' && this.isCtrlKeyPressed(e) === true && e.altKey === true) { + } else if (e.key === 'n' && this.isCtrlKeyPressed(e) && e.altKey) { // Create a new workflow e.stopPropagation(); e.preventDefault(); @@ -886,7 +866,7 @@ export default mixins( title: this.$locale.baseText('nodeView.showMessage.keyDown.title'), type: 'success', }); - } else if ((e.key === 's') && (this.isCtrlKeyPressed(e) === true)) { + } else if ((e.key === 's') && this.isCtrlKeyPressed(e)) { // Save workflow e.stopPropagation(); e.preventDefault(); @@ -906,7 +886,7 @@ export default mixins( } this.$store.commit('ndv/setActiveNodeName', lastSelectedNode.name); } - } else if (e.key === 'ArrowRight' && e.shiftKey === true) { + } else if (e.key === 'ArrowRight' && e.shiftKey) { // Select all downstream nodes e.stopPropagation(); e.preventDefault(); @@ -926,7 +906,7 @@ export default mixins( } this.callDebounced('nodeSelectedByName', { debounceTime: 100 }, connections.main[0][0].node, false, true); - } else if (e.key === 'ArrowLeft' && e.shiftKey === true) { + } else if (e.key === 'ArrowLeft' && e.shiftKey) { // Select all downstream nodes e.stopPropagation(); e.preventDefault(); @@ -969,14 +949,14 @@ export default mixins( const connections = workflow.connectionsByDestinationNode[lastSelectedNode.name]; - if (connections.main === undefined || connections.main.length === 0) { + if (!Array.isArray(connections.main) || !connections.main.length) { return; } const parentNode = connections.main[0][0].node; const connectionsParent = this.$store.getters.outgoingConnectionsByNodeName(parentNode); - if (connectionsParent.main === undefined || connectionsParent.main.length === 0) { + if (!Array.isArray(connectionsParent.main) || !connectionsParent.main.length) { return; } @@ -1015,7 +995,7 @@ export default mixins( }, deactivateSelectedNode() { - if (this.editAllowedCheck() === false) { + if (!this.editAllowedCheck()) { return; } this.disableNodes(this.$store.getters.getSelectedNodes); @@ -1160,13 +1140,12 @@ export default mixins( } // https://docs.jsplumbtoolkit.com/community/current/articles/zooming.html - const prependProperties = ['webkit', 'moz', 'ms', 'o']; const scaleString = 'scale(' + zoomLevel + ')'; - for (let i = 0; i < prependProperties.length; i++) { + ['webkit', 'moz', 'ms', 'o'].forEach((prefix) => { // @ts-ignore - element.style[prependProperties[i] + 'Transform'] = scaleString; - } + element.style[prefix + 'Transform'] = scaleString; + }); element.style['transform'] = scaleString; // @ts-ignore @@ -1295,7 +1274,7 @@ export default mixins( if (plainTextData.match(/^http[s]?:\/\/.*\.json$/i)) { // Pasted data points to a possible workflow JSON file - if (this.editAllowedCheck() === false) { + if (!this.editAllowedCheck()) { return; } @@ -1310,7 +1289,7 @@ export default mixins( this.$locale.baseText('nodeView.confirmMessage.receivedCopyPasteData.cancelButtonText'), ); - if (importConfirm === false) { + if (!importConfirm) { return; } @@ -1324,7 +1303,7 @@ export default mixins( // Check first if it is valid JSON workflowData = JSON.parse(plainTextData); - if (this.editAllowedCheck() === false) { + if (!this.editAllowedCheck()) { return; } } catch (e) { @@ -1509,7 +1488,7 @@ export default mixins( this.lastSelectedConnection = null; this.newNodeInsertPosition = null; - if (setActive === true) { + if (setActive) { this.$store.commit('ndv/setActiveNodeName', node.name); } }, @@ -1744,7 +1723,7 @@ export default mixins( this.__addConnection(connectionData, true); }, async addNode(nodeTypeName: string, options: AddNodeOptions = {}) { - if (this.editAllowedCheck() === false) { + if (!this.editAllowedCheck()) { return; } @@ -1876,7 +1855,7 @@ export default mixins( CanvasHelpers.resetConnection(info.connection); - if (this.isReadOnly === false) { + if (!this.isReadOnly) { let exitTimer: NodeJS.Timeout | undefined; let enterTimer: NodeJS.Timeout | undefined; info.connection.bind('mouseover', (connection: Connection) => { @@ -2119,8 +2098,7 @@ export default mixins( }, tryToAddWelcomeSticky: once(async function(this: any) { const newWorkflow = this.workflowData; - const flagAvailable = window.posthog !== undefined && window.posthog.getFeatureFlag !== undefined; - if (flagAvailable && window.posthog.getFeatureFlag('welcome-note') === 'test') { + if (window.posthog?.getFeatureFlag?.('welcome-note') === 'test') { // For novice users (onboardingFlowEnabled == true) // Inject welcome sticky note and zoom to fit @@ -2250,7 +2228,7 @@ export default mixins( return CanvasHelpers.getInputEndpointUUID(node.id, index); }, __addConnection(connection: [IConnection, IConnection], addVisualConnection = false) { - if (addVisualConnection === true) { + if (addVisualConnection) { const outputUuid = this.getOutputEndpointUUID(connection[0].node, connection[0].index); const inputUuid = this.getInputEndpointUUID(connection[1].node, connection[1].index); if (!outputUuid || !inputUuid) { @@ -2280,7 +2258,7 @@ export default mixins( }); }, __removeConnection(connection: [IConnection, IConnection], removeVisualConnection = false) { - if (removeVisualConnection === true) { + if (removeVisualConnection) { const sourceId = this.$store.getters.getNodeByName(connection[0].node); const targetId = this.$store.getters.getNodeByName(connection[1].node); // @ts-ignore @@ -2340,7 +2318,7 @@ export default mixins( } }, async duplicateNode(nodeName: string) { - if (this.editAllowedCheck() === false) { + if (!this.editAllowedCheck()) { return; } @@ -2518,7 +2496,7 @@ export default mixins( }); }, removeNode(nodeName: string) { - if (this.editAllowedCheck() === false) { + if (!this.editAllowedCheck()) { return; } @@ -2545,7 +2523,7 @@ export default mixins( } } - if (deleteAllowed === false) { + if (!deleteAllowed) { return; } } @@ -3042,7 +3020,7 @@ export default mixins( // Reset nodes this.deleteEveryEndpoint(); - if (this.executionWaitingForWebhook === true) { + if (this.executionWaitingForWebhook) { // Make sure that if there is a waiting test-webhook that // it gets removed this.restApi().removeTestWebhook(this.$store.getters.workflowId) @@ -3088,6 +3066,7 @@ export default mixins( }, async loadCredentials(): Promise { await this.$store.dispatch('credentials/fetchAllCredentials'); + await this.$store.dispatch('credentials/fetchForeignCredentials'); }, async loadNodesProperties(nodeInfos: INodeTypeNameVersion[]): Promise { const allNodes: INodeTypeDescription[] = this.$store.getters['nodeTypes/allNodeTypes']; diff --git a/packages/editor-ui/src/views/canvasHelpers.ts b/packages/editor-ui/src/views/canvasHelpers.ts index c127a45451..494f7c36c7 100644 --- a/packages/editor-ui/src/views/canvasHelpers.ts +++ b/packages/editor-ui/src/views/canvasHelpers.ts @@ -1,7 +1,7 @@ import { getStyleTokenValue, isNumber } from "@/components/helpers"; import { NODE_OUTPUT_DEFAULT_KEY, START_NODE_TYPE, STICKY_NODE_TYPE, QUICKSTART_NOTE_NAME } from "@/constants"; -import { IBounds, INodeUi, IZoomConfig, XYPosition } from "@/Interface"; -import { Connection, Endpoint, Overlay, OverlaySpec, PaintStyle } from "jsplumb"; +import { EndpointStyle, IBounds, INodeUi, IZoomConfig, XYPosition } from "@/Interface"; +import { AnchorArraySpec, Connection, Endpoint, Overlay, OverlaySpec, PaintStyle } from "jsplumb"; import { IConnection, INode, @@ -146,7 +146,7 @@ export const CONNECTOR_ARROW_OVERLAYS: OverlaySpec[] = [ export const ANCHOR_POSITIONS: { [key: string]: { - [key: number]: number[][]; + [key: number]: AnchorArraySpec[]; } } = { input: { @@ -192,7 +192,7 @@ export const ANCHOR_POSITIONS: { }; -export const getInputEndpointStyle = (nodeTypeData: INodeTypeDescription, color: string) => ({ +export const getInputEndpointStyle = (nodeTypeData: INodeTypeDescription, color: string): EndpointStyle => ({ width: 8, height: nodeTypeData && nodeTypeData.outputs.length > 2 ? 18 : 20, fill: getStyleTokenValue(color), @@ -200,7 +200,7 @@ export const getInputEndpointStyle = (nodeTypeData: INodeTypeDescription, color: lineWidth: 0, }); -export const getInputNameOverlay = (label: string) => ([ +export const getInputNameOverlay = (label: string): OverlaySpec => ([ 'Label', { id: OVERLAY_INPUT_NAME_LABEL, @@ -217,7 +217,7 @@ export const getOutputEndpointStyle = (nodeTypeData: INodeTypeDescription, color outlineStroke: 'none', }); -export const getOutputNameOverlay = (label: string) => ([ +export const getOutputNameOverlay = (label: string): OverlaySpec => ([ 'Label', { id: OVERLAY_OUTPUT_NAME_LABEL, diff --git a/packages/workflow/src/index.ts b/packages/workflow/src/index.ts index 02a858a2f8..2be086f7c1 100644 --- a/packages/workflow/src/index.ts +++ b/packages/workflow/src/index.ts @@ -17,3 +17,11 @@ export * from './WorkflowErrors'; export * from './WorkflowHooks'; export { LoggerProxy, NodeHelpers, ObservableObject, TelemetryHelpers }; export { deepCopy, jsonParse } from './utils'; +export { + isINodeProperties, + isINodePropertyOptions, + isINodePropertyCollection, + isINodePropertiesList, + isINodePropertyCollectionList, + isINodePropertyOptionsList, +} from './type-guards'; diff --git a/packages/workflow/src/type-guards.ts b/packages/workflow/src/type-guards.ts new file mode 100644 index 0000000000..1cc53aa620 --- /dev/null +++ b/packages/workflow/src/type-guards.ts @@ -0,0 +1,27 @@ +import { INodeProperties, INodePropertyOptions, INodePropertyCollection } from './Interfaces'; + +export const isINodeProperties = ( + item: INodePropertyOptions | INodeProperties | INodePropertyCollection, +): item is INodeProperties => 'name' in item && 'type' in item && !('value' in item); + +export const isINodePropertyOptions = ( + item: INodePropertyOptions | INodeProperties | INodePropertyCollection, +): item is INodePropertyOptions => 'value' in item && 'name' in item && !('displayName' in item); + +export const isINodePropertyCollection = ( + item: INodePropertyOptions | INodeProperties | INodePropertyCollection, +): item is INodePropertyCollection => 'values' in item && 'name' in item && 'displayName' in item; + +export const isINodePropertiesList = ( + items: INodeProperties['options'], +): items is INodeProperties[] => Array.isArray(items) && items.every(isINodeProperties); + +export const isINodePropertyOptionsList = ( + items: INodeProperties['options'], +): items is INodePropertyOptions[] => Array.isArray(items) && items.every(isINodePropertyOptions); + +export const isINodePropertyCollectionList = ( + items: INodeProperties['options'], +): items is INodePropertyCollection[] => { + return Array.isArray(items) && items.every(isINodePropertyCollection); +};