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
This commit is contained in:
Csaba Tuncsik 2022-10-24 20:17:25 +02:00 committed by GitHub
parent 1f4eaeb3ae
commit 408bd96815
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 369 additions and 219 deletions

View file

@ -1,3 +1,14 @@
import {
jsPlumbInstance,
DragOptions,
DropOptions,
ElementGroupRef,
Endpoint,
EndpointOptions,
EndpointRectangle,
EndpointRectangleOptions,
EndpointSpec,
} from "jsplumb";
import { import {
GenericValue, GenericValue,
IConnections, IConnections,
@ -104,24 +115,40 @@ declare module 'jsplumb' {
} }
// EndpointOptions from jsplumb seems incomplete and wrong so we define an own one // EndpointOptions from jsplumb seems incomplete and wrong so we define an own one
export interface IEndpointOptions { export type IEndpointOptions = Omit<EndpointOptions, 'endpoint' | 'dragProxy'> & {
anchor?: any; // tslint:disable-line:no-any endpointStyle: EndpointStyle
createEndpoint?: boolean; endpointHoverStyle: EndpointStyle
dragAllowedWhenFull?: boolean; endpoint?: EndpointSpec | string
dropOptions?: any; // tslint:disable-line:no-any dragAllowedWhenFull?: boolean
dragProxy?: any; // tslint:disable-line:no-any dropOptions?: DropOptions & {
endpoint?: string; tolerance: string
endpointStyle?: object; };
endpointHoverStyle?: object; dragProxy?: string | string[] | EndpointSpec | [ EndpointRectangle, EndpointRectangleOptions & { strokeWidth: number } ]
isSource?: boolean; };
isTarget?: boolean;
maxConnections?: number; export type EndpointStyle = {
overlays?: any; // tslint:disable-line:no-any width?: number
parameters?: any; // tslint:disable-line:no-any height?: number
uuid?: string; fill?: string
enabled?: boolean; stroke?: string
cssClass?: 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<jsPlumbInstance, 'addEndpoint' | 'draggable'> & {
clearDragSelection: () => void
addEndpoint(el: ElementGroupRef, params?: IEndpointOptions, referenceParams?: IEndpointOptions): Endpoint | Endpoint[]
draggable(el: {}, options?: IDragOptions): IJsPlumbInstance
};
export interface IUpdateInformation { export interface IUpdateInformation {
name: string; name: string;
@ -908,6 +935,7 @@ export interface ICredentialMap {
export interface ICredentialsState { export interface ICredentialsState {
credentialTypes: ICredentialTypeMap; credentialTypes: ICredentialTypeMap;
credentials: ICredentialMap; credentials: ICredentialMap;
foreignCredentials?: ICredentialMap;
} }
export interface ITagsState { export interface ITagsState {

View file

@ -51,3 +51,9 @@ export async function oAuth2CredentialAuthorize(context: IRestApiContext, data:
export async function testCredential(context: IRestApiContext, data: INodeCredentialTestRequest): Promise<INodeCredentialTestResult> { export async function testCredential(context: IRestApiContext, data: INodeCredentialTestRequest): Promise<INodeCredentialTestResult> {
return makeRestApiRequest(context, 'POST', '/credentials/test', data as unknown as IDataObject); return makeRestApiRequest(context, 'POST', '/credentials/test', data as unknown as IDataObject);
} }
export async function getForeignCredentials(context: IRestApiContext): Promise<ICredentialsResponse[]> {
// TODO: Get foreign credentials
//return await makeRestApiRequest(context, 'GET', '/foreign-credentials');
return [];
}

View file

@ -5,7 +5,7 @@
<n8n-text size="small">{{ $locale.baseText('collectionParameter.noProperties') }}</n8n-text> <n8n-text size="small">{{ $locale.baseText('collectionParameter.noProperties') }}</n8n-text>
</div> </div>
<parameter-input-list :parameters="getProperties" :nodeValues="nodeValues" :path="path" :hideDelete="hideDelete" :indent="true" @valueChanged="valueChanged" /> <parameter-input-list :parameters="getProperties" :nodeValues="nodeValues" :path="path" :hideDelete="hideDelete" :indent="true" :isReadOnly="isReadOnly" @valueChanged="valueChanged" />
<div v-if="parameterOptions.length > 0 && !isReadOnly" class="param-options"> <div v-if="parameterOptions.length > 0 && !isReadOnly" class="param-options">
<n8n-button <n8n-button
@ -43,7 +43,6 @@ import {
INodePropertyOptions, INodePropertyOptions,
} from 'n8n-workflow'; } from 'n8n-workflow';
import { genericHelpers } from '@/components/mixins/genericHelpers';
import { nodeHelpers } from '@/components/mixins/nodeHelpers'; import { nodeHelpers } from '@/components/mixins/nodeHelpers';
import { get } from 'lodash'; import { get } from 'lodash';
@ -52,7 +51,6 @@ import mixins from 'vue-typed-mixins';
import {Component} from "vue"; import {Component} from "vue";
export default mixins( export default mixins(
genericHelpers,
nodeHelpers, nodeHelpers,
) )
.extend({ .extend({
@ -63,6 +61,7 @@ export default mixins(
'parameter', // INodeProperties 'parameter', // INodeProperties
'path', // string 'path', // string
'values', // NodeParameters 'values', // NodeParameters
'isReadOnly', // boolean
], ],
components: { components: {
ParameterInputList: () => import('./ParameterInputList.vue') as Promise<Component>, ParameterInputList: () => import('./ParameterInputList.vue') as Promise<Component>,

View file

@ -7,6 +7,7 @@
:value="displayValue" :value="displayValue"
:placeholder="parameter.placeholder ? getPlaceholder() : $locale.baseText('parameterInput.select')" :placeholder="parameter.placeholder ? getPlaceholder() : $locale.baseText('parameterInput.select')"
:title="displayTitle" :title="displayTitle"
:disabled="isReadOnly"
ref="innerSelect" ref="innerSelect"
@change="(value) => $emit('valueChanged', value)" @change="(value) => $emit('valueChanged', value)"
@keydown.stop @keydown.stop

View file

@ -16,9 +16,9 @@
size="small" size="small"
color="text-dark" color="text-dark"
/> />
<div v-if="multipleValues === true"> <div v-if="multipleValues">
<div <div
v-for="(value, index) in values[property.name]" v-for="(value, index) in mutableValues[property.name]"
:key="property.name + index" :key="property.name + index"
class="parameter-item" class="parameter-item"
> >
@ -39,7 +39,7 @@
@click="moveOptionUp(property.name, index)" @click="moveOptionUp(property.name, index)"
/> />
<font-awesome-icon <font-awesome-icon
v-if="index !== (values[property.name].length - 1)" v-if="index !== (mutableValues[property.name].length - 1)"
icon="angle-down" icon="angle-down"
class="clickable" class="clickable"
:title="$locale.baseText('fixedCollectionParameter.moveDown')" :title="$locale.baseText('fixedCollectionParameter.moveDown')"
@ -52,6 +52,7 @@
:nodeValues="nodeValues" :nodeValues="nodeValues"
:path="getPropertyPath(property.name, index)" :path="getPropertyPath(property.name, index)"
:hideDelete="true" :hideDelete="true"
:isReadOnly="isReadOnly"
@valueChanged="valueChanged" @valueChanged="valueChanged"
/> />
</div> </div>
@ -71,6 +72,7 @@
:parameters="property.values" :parameters="property.values"
:nodeValues="nodeValues" :nodeValues="nodeValues"
:path="getPropertyPath(property.name)" :path="getPropertyPath(property.name)"
:isReadOnly="isReadOnly"
class="parameter-item" class="parameter-item"
@valueChanged="valueChanged" @valueChanged="valueChanged"
:hideDelete="true" :hideDelete="true"
@ -108,42 +110,66 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import Vue, { Component, PropType } from "vue";
import { import {
IUpdateInformation, IUpdateInformation,
} from '@/Interface'; } from '@/Interface';
import { import {
deepCopy,
INodeParameters, INodeParameters,
INodeProperties,
INodePropertyCollection, INodePropertyCollection,
NodeParameterValue, NodeParameterValue,
deepCopy,
isINodePropertyCollectionList,
} from 'n8n-workflow'; } from 'n8n-workflow';
import { get } from 'lodash'; import { get } from 'lodash';
import { genericHelpers } from '@/components/mixins/genericHelpers'; export default Vue.extend({
import mixins from 'vue-typed-mixins';
import {Component} from "vue";
export default mixins(genericHelpers)
.extend({
name: 'FixedCollectionParameter', name: 'FixedCollectionParameter',
props: [ props: {
'nodeValues', // INodeParameters nodeValues: {
'parameter', // INodeProperties type: Object as PropType<Record<string, INodeParameters[]>>,
'path', // string required: true,
'values', // INodeParameters },
], parameter: {
type: Object as PropType<INodeProperties>,
required: true,
},
path: {
type: String,
required: true,
},
values: {
type: Object as PropType<Record<string, INodeParameters[]>>,
default: () => ({}),
},
isReadOnly: {
type: Boolean,
default: false,
},
},
components: { components: {
ParameterInputList: () => import('./ParameterInputList.vue') as Promise<Component>, ParameterInputList: () => import('./ParameterInputList.vue') as Promise<Component>,
}, },
data() { data() {
return { return {
selectedOption: undefined, selectedOption: undefined,
mutableValues: {} as Record<string, INodeParameters[]>,
}; };
}, },
watch: {
values: {
handler(newValues: Record<string, INodeParameters[]>) {
this.mutableValues = deepCopy(newValues);
},
deep: true,
},
},
created(){
this.mutableValues = deepCopy(this.values);
},
computed: { computed: {
getPlaceholderText(): string { getPlaceholderText(): string {
const placeholder = this.$locale.nodeText().placeholder(this.parameter, this.path); const placeholder = this.$locale.nodeText().placeholder(this.parameter, this.path);
@ -161,14 +187,11 @@ export default mixins(genericHelpers)
return returnProperties; return returnProperties;
}, },
multipleValues(): boolean { multipleValues(): boolean {
if (this.parameter.typeOptions !== undefined && this.parameter.typeOptions.multipleValues === true) { return !!this.parameter.typeOptions?.multipleValues;
return true;
}
return false;
}, },
parameterOptions(): INodePropertyCollection[] { parameterOptions(): INodePropertyCollection[] {
if (this.multipleValues === true) { if (this.multipleValues && isINodePropertyCollectionList(this.parameter.options)) {
return this.parameter.options; return this.parameter.options;
} }
@ -177,18 +200,15 @@ export default mixins(genericHelpers)
}); });
}, },
propertyNames(): string[] { propertyNames(): string[] {
if (this.values) { return Object.keys(this.mutableValues || {});
return Object.keys(this.values);
}
return [];
}, },
sortable(): string { sortable(): boolean {
return this.parameter.typeOptions && this.parameter.typeOptions.sortable; return !!this.parameter.typeOptions?.sortable;
}, },
}, },
methods: { methods: {
deleteOption(optionName: string, index?: number) { deleteOption(optionName: string, index?: number) {
const currentOptionsOfSameType = this.values[optionName]; const currentOptionsOfSameType = this.mutableValues[optionName];
if (!currentOptionsOfSameType || currentOptionsOfSameType.length > 1) { if (!currentOptionsOfSameType || currentOptionsOfSameType.length > 1) {
// it's not the only option of this type, so just remove it. // it's not the only option of this type, so just remove it.
this.$emit('valueChanged', { this.$emit('valueChanged', {
@ -207,30 +227,35 @@ export default mixins(genericHelpers)
return `${this.path}.${name}` + (index !== undefined ? `[${index}]` : ''); return `${this.path}.${name}` + (index !== undefined ? `[${index}]` : '');
}, },
getOptionProperties(optionName: string): INodePropertyCollection | undefined { getOptionProperties(optionName: string): INodePropertyCollection | undefined {
if(isINodePropertyCollectionList(this.parameter.options)){
for (const option of this.parameter.options) { for (const option of this.parameter.options) {
if (option.name === optionName) { if (option.name === optionName) {
return option; return option;
} }
} }
}
return undefined; return undefined;
}, },
moveOptionDown(optionName: string, index: number) { moveOptionDown(optionName: string, index: number) {
this.values[optionName].splice(index + 1, 0, this.values[optionName].splice(index, 1)[0]); if(Array.isArray(this.mutableValues[optionName])){
this.mutableValues[optionName].splice(index + 1, 0, this.mutableValues[optionName].splice(index, 1)[0]);
}
const parameterData = { const parameterData = {
name: this.getPropertyPath(optionName), name: this.getPropertyPath(optionName),
value: this.values[optionName], value: this.mutableValues[optionName],
}; };
this.$emit('valueChanged', parameterData); this.$emit('valueChanged', parameterData);
}, },
moveOptionUp(optionName: string, index: number) { moveOptionUp(optionName: string, index: number) {
this.values[optionName].splice(index - 1, 0, this.values[optionName].splice(index, 1)[0]); if(Array.isArray(this.mutableValues[optionName])) {
this.mutableValues?.[optionName].splice(index - 1, 0, this.mutableValues[optionName].splice(index, 1)[0]);
}
const parameterData = { const parameterData = {
name: this.getPropertyPath(optionName), name: this.getPropertyPath(optionName),
value: this.values[optionName], value: this.mutableValues[optionName],
}; };
this.$emit('valueChanged', parameterData); this.$emit('valueChanged', parameterData);
@ -262,8 +287,8 @@ export default mixins(genericHelpers)
} }
let newValue; let newValue;
if (this.multipleValues === true) { if (this.multipleValues) {
newValue = get(this.nodeValues, name, []); newValue = get(this.nodeValues, name, [] as INodeParameters[]);
newValue.push(newParameterValue); newValue.push(newParameterValue);
} else { } else {

View file

@ -3,6 +3,7 @@
<n8n-button <n8n-button
type="secondary" type="secondary"
:label="$locale.baseText('importParameter.label')" :label="$locale.baseText('importParameter.label')"
:disabled="isReadOnly"
size="mini" size="mini"
@click="onImportCurlClicked" @click="onImportCurlClicked"
/> />
@ -16,6 +17,12 @@ import { showMessage } from './mixins/showMessage';
export default mixins(showMessage).extend({ export default mixins(showMessage).extend({
name: 'import-parameter', name: 'import-parameter',
props: {
isReadOnly: {
type: Boolean,
default: false,
},
},
methods: { methods: {
onImportCurlClicked() { onImportCurlClicked() {
this.$store.dispatch('ui/openModal', IMPORT_CURL_MODAL_KEY); this.$store.dispatch('ui/openModal', IMPORT_CURL_MODAL_KEY);

View file

@ -8,16 +8,16 @@
color="text-dark" color="text-dark"
/> />
<div v-for="(value, index) in values" :key="index" class="duplicate-parameter-item" :class="parameter.type"> <div v-for="(value, index) in mutableValues" :key="index" class="duplicate-parameter-item" :class="parameter.type">
<div class="delete-item clickable" v-if="!isReadOnly"> <div class="delete-item clickable" v-if="!isReadOnly">
<font-awesome-icon icon="trash" :title="$locale.baseText('multipleParameter.deleteItem')" @click="deleteItem(index)" /> <font-awesome-icon icon="trash" :title="$locale.baseText('multipleParameter.deleteItem')" @click="deleteItem(index)" />
<div v-if="sortable"> <div v-if="sortable">
<font-awesome-icon v-if="index !== 0" icon="angle-up" class="clickable" :title="$locale.baseText('multipleParameter.moveUp')" @click="moveOptionUp(index)" /> <font-awesome-icon v-if="index !== 0" icon="angle-up" class="clickable" :title="$locale.baseText('multipleParameter.moveUp')" @click="moveOptionUp(index)" />
<font-awesome-icon v-if="index !== (values.length -1)" icon="angle-down" class="clickable" :title="$locale.baseText('multipleParameter.moveDown')" @click="moveOptionDown(index)" /> <font-awesome-icon v-if="index !== (mutableValues.length - 1)" icon="angle-down" class="clickable" :title="$locale.baseText('multipleParameter.moveDown')" @click="moveOptionDown(index)" />
</div> </div>
</div> </div>
<div v-if="parameter.type === 'collection'"> <div v-if="parameter.type === 'collection'">
<collection-parameter :parameter="parameter" :values="value" :nodeValues="nodeValues" :path="getPath(index)" :hideDelete="hideDelete" @valueChanged="valueChanged" /> <collection-parameter :parameter="parameter" :values="value" :nodeValues="nodeValues" :path="getPath(index)" :hideDelete="hideDelete" :isReadOnly="isReadOnly" @valueChanged="valueChanged" />
</div> </div>
<div v-else> <div v-else>
<parameter-input-full class="duplicate-parameter-input-item" :parameter="parameter" :value="value" :displayOptions="true" :hideLabel="true" :path="getPath(index)" @valueChanged="valueChanged" inputSize="small" :isReadOnly="isReadOnly" /> <parameter-input-full class="duplicate-parameter-input-item" :parameter="parameter" :value="value" :displayOptions="true" :hideLabel="true" :path="getPath(index)" @valueChanged="valueChanged" inputSize="small" :isReadOnly="isReadOnly" />
@ -25,7 +25,7 @@
</div> </div>
<div class="add-item-wrapper"> <div class="add-item-wrapper">
<div v-if="values && Object.keys(values).length === 0 || isReadOnly" class="no-items-exist"> <div v-if="mutableValues && mutableValues.length === 0 || isReadOnly" class="no-items-exist">
<n8n-text size="small">{{ $locale.baseText('multipleParameter.currentlyNoItemsExist') }}</n8n-text> <n8n-text size="small">{{ $locale.baseText('multipleParameter.currentlyNoItemsExist') }}</n8n-text>
</div> </div>
<n8n-button v-if="!isReadOnly" type="tertiary" block @click="addItem()" :label="addButtonText" /> <n8n-button v-if="!isReadOnly" type="tertiary" block @click="addItem()" :label="addButtonText" />
@ -34,33 +34,60 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import Vue, { PropType } from "vue";
import { import {
IUpdateInformation, IUpdateInformation,
} from '@/Interface'; } from '@/Interface';
import { deepCopy, INodeParameters, INodeProperties } from "n8n-workflow";
import CollectionParameter from '@/components/CollectionParameter.vue'; import CollectionParameter from '@/components/CollectionParameter.vue';
import ParameterInputFull from '@/components/ParameterInputFull.vue'; import ParameterInputFull from '@/components/ParameterInputFull.vue';
import { get } from 'lodash'; import { get } from 'lodash';
import { genericHelpers } from '@/components/mixins/genericHelpers'; export default Vue.extend({
import mixins from 'vue-typed-mixins';
import { deepCopy } from "n8n-workflow";
export default mixins(genericHelpers)
.extend({
name: 'MultipleParameter', name: 'MultipleParameter',
components: { components: {
CollectionParameter, CollectionParameter,
ParameterInputFull, ParameterInputFull,
}, },
props: [ props: {
'nodeValues', // NodeParameters nodeValues: {
'parameter', // NodeProperties type: Object as PropType<Record<string, INodeParameters[]>>,
'path', // string required: true,
'values', // NodeParameters[] },
], parameter: {
type: Object as PropType<INodeProperties>,
required: true,
},
path: {
type: String,
required: true,
},
values: {
type: Array as PropType<INodeParameters[]>,
default: () => [],
},
isReadOnly: {
type: Boolean,
default: false,
},
},
data() {
return {
mutableValues: [] as INodeParameters[],
};
},
watch: {
values: {
handler(newValues: INodeParameters[]) {
this.mutableValues = deepCopy(newValues);
},
deep: true,
},
},
created(){
this.mutableValues = deepCopy(this.values);
},
computed: { computed: {
addButtonText (): string { addButtonText (): string {
if ( if (
@ -73,22 +100,18 @@ export default mixins(genericHelpers)
return this.$locale.nodeText().multipleValueButtonText(this.parameter); return this.$locale.nodeText().multipleValueButtonText(this.parameter);
}, },
hideDelete (): boolean { hideDelete (): boolean {
return this.parameter.options.length === 1; return this.parameter.options?.length === 1;
}, },
sortable (): string { sortable (): boolean {
return this.parameter.typeOptions && this.parameter.typeOptions.sortable; return !!this.parameter.typeOptions?.sortable;
}, },
}, },
methods: { methods: {
addItem () { addItem () {
const name = this.getPath(); const name = this.getPath();
let currentValue = get(this.nodeValues, name); const currentValue = get(this.nodeValues, name, [] as INodeParameters[]);
if (currentValue === undefined) { currentValue.push(deepCopy(this.parameter.default as INodeParameters));
currentValue = [];
}
currentValue.push(deepCopy(this.parameter.default));
const parameterData = { const parameterData = {
name, name,
@ -109,21 +132,21 @@ export default mixins(genericHelpers)
return this.path + (index !== undefined ? `[${index}]` : ''); return this.path + (index !== undefined ? `[${index}]` : '');
}, },
moveOptionDown (index: number) { moveOptionDown (index: number) {
this.values.splice(index + 1, 0, this.values.splice(index, 1)[0]); this.mutableValues.splice(index + 1, 0, this.mutableValues.splice(index, 1)[0]);
const parameterData = { const parameterData = {
name: this.path, name: this.path,
value: this.values, value: this.mutableValues,
}; };
this.$emit('valueChanged', parameterData); this.$emit('valueChanged', parameterData);
}, },
moveOptionUp (index: number) { moveOptionUp (index: number) {
this.values.splice(index - 1, 0, this.values.splice(index, 1)[0]); this.mutableValues.splice(index - 1, 0, this.mutableValues.splice(index, 1)[0]);
const parameterData = { const parameterData = {
name: this.path, name: this.path,
value: this.values, value: this.mutableValues,
}; };
this.$emit('valueChanged', parameterData); this.$emit('valueChanged', parameterData);

View file

@ -63,10 +63,10 @@
<div v-touch:tap="duplicateNode" class="option" :title="$locale.baseText('node.duplicateNode')" v-if="isDuplicatable"> <div v-touch:tap="duplicateNode" class="option" :title="$locale.baseText('node.duplicateNode')" v-if="isDuplicatable">
<font-awesome-icon icon="clone" /> <font-awesome-icon icon="clone" />
</div> </div>
<div v-touch:tap="setNodeActive" class="option touch" :title="$locale.baseText('node.editNode')" v-if="!isReadOnly"> <div v-touch:tap="setNodeActive" class="option touch" :title="$locale.baseText('node.editNode')">
<font-awesome-icon class="execute-icon" icon="cog" /> <font-awesome-icon class="execute-icon" icon="cog" />
</div> </div>
<div v-touch:tap="executeNode" class="option" :title="$locale.baseText('node.executeNode')" v-if="!isReadOnly && !workflowRunning"> <div v-touch:tap="executeNode" class="option" :title="$locale.baseText('node.executeNode')" v-if="!workflowRunning">
<font-awesome-icon class="execute-icon" icon="play-circle" /> <font-awesome-icon class="execute-icon" icon="play-circle" />
</div> </div>
</div> </div>

View file

@ -54,7 +54,7 @@
:linkedRuns="linked" :linkedRuns="linked"
:currentNodeName="inputNodeName" :currentNodeName="inputNodeName"
:sessionId="sessionId" :sessionId="sessionId"
:readOnly="readOnly" :readOnly="readOnly || hasForeignCredential"
@linkRun="onLinkRunToInput" @linkRun="onLinkRunToInput"
@unlinkRun="() => onUnlinkRun('input')" @unlinkRun="() => onUnlinkRun('input')"
@runChange="onRunInputIndexChange" @runChange="onRunInputIndexChange"
@ -71,7 +71,7 @@
:runIndex="outputRun" :runIndex="outputRun"
:linkedRuns="linked" :linkedRuns="linked"
:sessionId="sessionId" :sessionId="sessionId"
:isReadOnly="readOnly" :isReadOnly="readOnly || hasForeignCredential"
@linkRun="onLinkRunToOutput" @linkRun="onLinkRunToOutput"
@unlinkRun="() => onUnlinkRun('output')" @unlinkRun="() => onUnlinkRun('output')"
@runChange="onRunOutputIndexChange" @runChange="onRunOutputIndexChange"
@ -86,6 +86,7 @@
:dragging="isDragging" :dragging="isDragging"
:sessionId="sessionId" :sessionId="sessionId"
:nodeType="activeNodeType" :nodeType="activeNodeType"
:isReadOnly="readOnly || hasForeignCredential"
@valueChanged="valueChanged" @valueChanged="valueChanged"
@execute="onNodeExecute" @execute="onNodeExecute"
@activate="onWorkflowActivate" @activate="onWorkflowActivate"
@ -114,7 +115,7 @@ import {
Workflow, Workflow,
jsonParse, jsonParse,
} from 'n8n-workflow'; } from 'n8n-workflow';
import { IExecutionResponse, INodeUi, IUpdateInformation, TargetItem } from '../Interface'; import { IExecutionResponse, INodeUi, IUpdateInformation, TargetItem } from '@/Interface';
import { externalHooks } from '@/components/mixins/externalHooks'; import { externalHooks } from '@/components/mixins/externalHooks';
import { nodeHelpers } from '@/components/mixins/nodeHelpers'; import { nodeHelpers } from '@/components/mixins/nodeHelpers';
@ -136,7 +137,7 @@ import {
} from '@/constants'; } from '@/constants';
import { workflowActivate } from './mixins/workflowActivate'; import { workflowActivate } from './mixins/workflowActivate';
import { pinData } from "@/components/mixins/pinData"; import { pinData } from "@/components/mixins/pinData";
import { dataPinningEventBus } from '../event-bus/data-pinning-event-bus'; import { dataPinningEventBus } from '@/event-bus/data-pinning-event-bus';
export default mixins( export default mixins(
externalHooks, externalHooks,
@ -174,6 +175,7 @@ export default mixins(
pinDataDiscoveryTooltipVisible: false, pinDataDiscoveryTooltipVisible: false,
avgInputRowHeight: 0, avgInputRowHeight: 0,
avgOutputRowHeight: 0, avgOutputRowHeight: 0,
hasForeignCredential: false,
}; };
}, },
mounted() { mounted() {
@ -360,6 +362,8 @@ export default mixins(
nodeSubtitle: this.getNodeSubtitle(node, this.activeNodeType, this.getCurrentWorkflow()), nodeSubtitle: this.getNodeSubtitle(node, this.activeNodeType, this.getCurrentWorkflow()),
}); });
this.checkForeignCredentials();
setTimeout(() => { setTimeout(() => {
if (this.activeNode) { if (this.activeNode) {
const outogingConnections = this.$store.getters.outgoingConnectionsByNodeName( const outogingConnections = this.$store.getters.outgoingConnectionsByNodeName(
@ -603,6 +607,10 @@ export default mixins(
input_node_type: this.inputNode ? this.inputNode.type : '', input_node_type: this.inputNode ? this.inputNode.type : '',
}); });
}, },
checkForeignCredentials() {
const issues = this.getNodeCredentialIssues(this.activeNode);
this.hasForeignCredential = !!issues?.credentials?.foreign;
},
}, },
}); });
</script> </script>

View file

@ -7,8 +7,8 @@
class="node-name" class="node-name"
:value="node && node.name" :value="node && node.name"
:nodeType="nodeType" :nodeType="nodeType"
:isReadOnly="isReadOnly"
@input="nameChanged" @input="nameChanged"
:readOnly="isReadOnly"
></NodeTitle> ></NodeTitle>
<div v-if="!isReadOnly"> <div v-if="!isReadOnly">
<NodeExecuteButton <NodeExecuteButton
@ -72,11 +72,11 @@
:parameters="parametersNoneSetting" :parameters="parametersNoneSetting"
:hideDelete="true" :hideDelete="true"
:nodeValues="nodeValues" :nodeValues="nodeValues"
:isReadOnly="isReadOnly"
path="parameters" path="parameters"
@valueChanged="valueChanged" @valueChanged="valueChanged"
@activate="onWorkflowActivate" @activate="onWorkflowActivate"
> >
<node-credentials :node="node" @credentialSelected="credentialSelected" /> <node-credentials :node="node" @credentialSelected="credentialSelected" />
</parameter-input-list> </parameter-input-list>
<div v-if="parametersNoneSetting.length === 0" class="no-parameters"> <div v-if="parametersNoneSetting.length === 0" class="no-parameters">
@ -99,6 +99,7 @@
<parameter-input-list <parameter-input-list
:parameters="parametersSetting" :parameters="parametersSetting"
:nodeValues="nodeValues" :nodeValues="nodeValues"
:isReadOnly="isReadOnly"
path="parameters" path="parameters"
@valueChanged="valueChanged" @valueChanged="valueChanged"
/> />
@ -106,6 +107,7 @@
:parameters="nodeSettings" :parameters="nodeSettings"
:hideDelete="true" :hideDelete="true"
:nodeValues="nodeValues" :nodeValues="nodeValues"
:isReadOnly="isReadOnly"
path="" path=""
@valueChanged="valueChanged" @valueChanged="valueChanged"
/> />
@ -142,14 +144,13 @@ import NodeWebhooks from '@/components/NodeWebhooks.vue';
import { get, set, unset } from 'lodash'; import { get, set, unset } from 'lodash';
import { externalHooks } from '@/components/mixins/externalHooks'; import { externalHooks } from '@/components/mixins/externalHooks';
import { genericHelpers } from '@/components/mixins/genericHelpers';
import { nodeHelpers } from '@/components/mixins/nodeHelpers'; import { nodeHelpers } from '@/components/mixins/nodeHelpers';
import mixins from 'vue-typed-mixins'; import mixins from 'vue-typed-mixins';
import NodeExecuteButton from './NodeExecuteButton.vue'; import NodeExecuteButton from './NodeExecuteButton.vue';
import { isCommunityPackageName } from './helpers'; import { isCommunityPackageName } from './helpers';
export default mixins(externalHooks, genericHelpers, nodeHelpers).extend({ export default mixins(externalHooks, nodeHelpers).extend({
name: 'NodeSettings', name: 'NodeSettings',
components: { components: {
NodeTitle, NodeTitle,
@ -235,6 +236,9 @@ export default mixins(externalHooks, genericHelpers, nodeHelpers).extend({
nodeType: { nodeType: {
type: Object as PropType<INodeTypeDescription>, type: Object as PropType<INodeTypeDescription>,
}, },
isReadOnly: {
type: Boolean,
},
}, },
data() { data() {
return { return {

View file

@ -40,6 +40,7 @@
:rows="getArgument('rows')" :rows="getArgument('rows')"
:value="expressionDisplayValue" :value="expressionDisplayValue"
:title="displayTitle" :title="displayTitle"
:readOnly="isReadOnly"
@keydown.stop @keydown.stop
/> />
<div <div
@ -64,6 +65,7 @@
:value="value" :value="value"
:parameter="parameter" :parameter="parameter"
:path="path" :path="path"
:isReadOnly="isReadOnly"
@closeDialog="closeTextEditDialog" @closeDialog="closeTextEditDialog"
@valueChanged="expressionUpdated" @valueChanged="expressionUpdated"
></text-edit> ></text-edit>

View file

@ -12,12 +12,14 @@
:values="getParameterValue(nodeValues, parameter.name, path)" :values="getParameterValue(nodeValues, parameter.name, path)"
:nodeValues="nodeValues" :nodeValues="nodeValues"
:path="getPath(parameter.name)" :path="getPath(parameter.name)"
:isReadOnly="isReadOnly"
@valueChanged="valueChanged" @valueChanged="valueChanged"
/> />
</div> </div>
<import-parameter <import-parameter
v-else-if="parameter.type === 'curlImport' && nodeTypeName === 'n8n-nodes-base.httpRequest' && nodeTypeVersion >= 3" v-else-if="parameter.type === 'curlImport' && nodeTypeName === 'n8n-nodes-base.httpRequest' && nodeTypeVersion >= 3"
:isReadOnly="isReadOnly"
@valueChanged="valueChanged" @valueChanged="valueChanged"
/> />
@ -53,6 +55,7 @@
:values="getParameterValue(nodeValues, parameter.name, path)" :values="getParameterValue(nodeValues, parameter.name, path)"
:nodeValues="nodeValues" :nodeValues="nodeValues"
:path="getPath(parameter.name)" :path="getPath(parameter.name)"
:isReadOnly="isReadOnly"
@valueChanged="valueChanged" @valueChanged="valueChanged"
/> />
<fixed-collection-parameter <fixed-collection-parameter
@ -61,6 +64,7 @@
:values="getParameterValue(nodeValues, parameter.name, path)" :values="getParameterValue(nodeValues, parameter.name, path)"
:nodeValues="nodeValues" :nodeValues="nodeValues"
:path="getPath(parameter.name)" :path="getPath(parameter.name)"
:isReadOnly="isReadOnly"
@valueChanged="valueChanged" @valueChanged="valueChanged"
/> />
</div> </div>
@ -107,7 +111,6 @@ import {
import { INodeUi, IUpdateInformation } from '@/Interface'; import { INodeUi, IUpdateInformation } from '@/Interface';
import MultipleParameter from '@/components/MultipleParameter.vue'; import MultipleParameter from '@/components/MultipleParameter.vue';
import { genericHelpers } from '@/components/mixins/genericHelpers';
import { workflowHelpers } from '@/components/mixins/workflowHelpers'; import { workflowHelpers } from '@/components/mixins/workflowHelpers';
import ParameterInputFull from '@/components/ParameterInputFull.vue'; import ParameterInputFull from '@/components/ParameterInputFull.vue';
import ImportParameter from '@/components/ImportParameter.vue'; import ImportParameter from '@/components/ImportParameter.vue';
@ -118,7 +121,6 @@ import mixins from 'vue-typed-mixins';
import {Component} from "vue"; import {Component} from "vue";
export default mixins( export default mixins(
genericHelpers,
workflowHelpers, workflowHelpers,
) )
.extend({ .extend({
@ -136,6 +138,7 @@ export default mixins(
'path', // string 'path', // string
'hideDelete', // boolean 'hideDelete', // boolean
'indent', 'indent',
'isReadOnly',
], ],
computed: { computed: {
nodeTypeVersion(): number | null { nodeTypeVersion(): number | null {

View file

@ -84,6 +84,7 @@
:size="inputSize" :size="inputSize"
:value="expressionDisplayValue" :value="expressionDisplayValue"
:title="displayTitle" :title="displayTitle"
:disabled="isReadOnly"
@keydown.stop @keydown.stop
ref="input" ref="input"
/> />

View file

@ -5,7 +5,7 @@
<div class="ignore-key-press"> <div class="ignore-key-press">
<n8n-input-label :label="$locale.nodeText().inputLabelDisplayName(parameter, path)"> <n8n-input-label :label="$locale.nodeText().inputLabelDisplayName(parameter, path)">
<div @keydown.stop @keydown.esc="onKeyDownEsc()"> <div @keydown.stop @keydown.esc="onKeyDownEsc()">
<n8n-input v-model="tempValue" type="textarea" ref="inputField" :value="value" :placeholder="$locale.nodeText().placeholder(parameter, path)" @change="valueChanged" @keydown.stop="noOp" :rows="15" /> <n8n-input v-model="tempValue" type="textarea" ref="inputField" :value="value" :placeholder="$locale.nodeText().placeholder(parameter, path)" :readOnly="isReadOnly" @change="valueChanged" :rows="15" />
</div> </div>
</n8n-input-label> </n8n-input-label>
</div> </div>
@ -24,6 +24,7 @@ export default Vue.extend({
'parameter', 'parameter',
'path', 'path',
'value', 'value',
'isReadOnly',
], ],
data () { data () {
return { return {

View file

@ -17,7 +17,7 @@ export const genericHelpers = mixins(showMessage).extend({
methods: { methods: {
displayTimer (msPassed: number, showMs = false): string { displayTimer (msPassed: number, showMs = false): string {
if (msPassed < 60000) { if (msPassed < 60000) {
if (showMs === false) { if (!showMs) {
return `${Math.floor(msPassed / 1000)} ${this.$locale.baseText('genericHelpers.sec')}`; return `${Math.floor(msPassed / 1000)} ${this.$locale.baseText('genericHelpers.sec')}`;
} }

View file

@ -1,11 +1,9 @@
import { IEndpointOptions, INodeUi, XYPosition } from '@/Interface'; import { PropType } from "vue";
import mixins from 'vue-typed-mixins'; import mixins from 'vue-typed-mixins';
import { IJsPlumbInstance, IEndpointOptions, INodeUi, XYPosition } from '@/Interface';
import { deviceSupportHelpers } from '@/components/mixins/deviceSupportHelpers'; import { deviceSupportHelpers } from '@/components/mixins/deviceSupportHelpers';
import { NO_OP_NODE_TYPE, STICKY_NODE_TYPE } from '@/constants'; import { NO_OP_NODE_TYPE, STICKY_NODE_TYPE } from '@/constants';
import * as CanvasHelpers from '@/views/canvasHelpers'; import * as CanvasHelpers from '@/views/canvasHelpers';
import { Endpoint } from 'jsplumb';
import { import {
INodeTypeDescription, INodeTypeDescription,
@ -34,9 +32,7 @@ export const nodeBase = mixins(
type: String, type: String,
}, },
instance: { instance: {
// We can't use PropType<jsPlumbInstance> here because the version of jsplumb doesn't type: Object as PropType<IJsPlumbInstance>,
// include correct typing for draggable instance(`clearDragSelection`, `destroyDraggable`, etc.)
type: Object,
}, },
isReadOnly: { isReadOnly: {
type: Boolean, type: Boolean,
@ -104,13 +100,15 @@ export const nodeBase = mixins(
]; ];
} }
const endpoint: Endpoint = this.instance.addEndpoint(this.nodeId, newEndpointData); const endpoint = this.instance.addEndpoint(this.nodeId, newEndpointData);
if(!Array.isArray(endpoint)) {
endpoint.__meta = { endpoint.__meta = {
nodeName: node.name, nodeName: node.name,
nodeId: this.nodeId, nodeId: this.nodeId,
index: i, index: i,
totalEndpoints: nodeTypeData.inputs.length, totalEndpoints: nodeTypeData.inputs.length,
}; };
}
// TODO: Activate again if it makes sense. Currently makes problems when removing // 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 // 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', cssClass: 'dot-output-endpoint',
dragAllowedWhenFull: false, dragAllowedWhenFull: false,
dragProxy: ['Rectangle', { width: 1, height: 1, strokeWidth: 0 }], dragProxy: ['Rectangle', {width: 1, height: 1, strokeWidth: 0}],
}; };
if (nodeTypeData.outputNames) { if (nodeTypeData.outputNames) {
@ -169,13 +167,15 @@ export const nodeBase = mixins(
]; ];
} }
const endpoint: Endpoint = this.instance.addEndpoint(this.nodeId, {...newEndpointData}); const endpoint = this.instance.addEndpoint(this.nodeId, {...newEndpointData});
if(!Array.isArray(endpoint)) {
endpoint.__meta = { endpoint.__meta = {
nodeName: node.name, nodeName: node.name,
nodeId: this.nodeId, nodeId: this.nodeId,
index: i, index: i,
totalEndpoints: nodeTypeData.outputs.length, totalEndpoints: nodeTypeData.outputs.length,
}; };
}
if (!this.isReadOnly) { if (!this.isReadOnly) {
const plusEndpointData: IEndpointOptions = { const plusEndpointData: IEndpointOptions = {
@ -206,10 +206,11 @@ export const nodeBase = mixins(
}, },
cssClass: 'plus-draggable-endpoint', cssClass: 'plus-draggable-endpoint',
dragAllowedWhenFull: false, 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); const plusEndpoint = this.instance.addEndpoint(this.nodeId, plusEndpointData);
if(!Array.isArray(plusEndpoint)) {
plusEndpoint.__meta = { plusEndpoint.__meta = {
nodeName: node.name, nodeName: node.name,
nodeId: this.nodeId, nodeId: this.nodeId,
@ -217,6 +218,7 @@ export const nodeBase = mixins(
totalEndpoints: nodeTypeData.outputs.length, totalEndpoints: nodeTypeData.outputs.length,
}; };
} }
}
}); });
}, },
__makeInstanceDraggable(node: INodeUi) { __makeInstanceDraggable(node: INodeUi) {

View file

@ -27,7 +27,7 @@ import {
import { import {
ICredentialsResponse, ICredentialsResponse,
INodeUi, INodeUi,
} from '../../Interface'; } from '@/Interface';
import { restApi } from '@/components/mixins/restApi'; import { restApi } from '@/components/mixins/restApi';
@ -214,31 +214,36 @@ export const nodeHelpers = mixins(
// Returns all the credential-issues of the node // Returns all the credential-issues of the node
getNodeCredentialIssues (node: INodeUi, nodeType?: INodeTypeDescription): INodeIssues | null { getNodeCredentialIssues (node: INodeUi, nodeType?: INodeTypeDescription): INodeIssues | null {
if (node.disabled === true) { if (node.disabled) {
// Node is disabled // Node is disabled
return null; return null;
} }
if (nodeType === undefined) { if (!nodeType) {
nodeType = this.$store.getters['nodeTypes/getNodeType'](node.type, node.typeVersion); 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 // Node does not need any credentials or nodeType could not be found
return null; return null;
} }
if (nodeType!.credentials === undefined) {
// No credentials defined for node type
return null;
}
const foundIssues: INodeIssueObjectProperty = {}; const foundIssues: INodeIssueObjectProperty = {};
let userCredentials: ICredentialsResponse[] | null; let userCredentials: ICredentialsResponse[] | null;
let credentialType: ICredentialType | null; let credentialType: ICredentialType | null;
let credentialDisplayName: string; let credentialDisplayName: string;
let selectedCredentials: INodeCredentialsDetails; 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 { const {
authentication, authentication,
@ -279,9 +284,9 @@ export const nodeHelpers = mixins(
return this.reportUnsetCredential(credential); return this.reportUnsetCredential(credential);
} }
for (const credentialTypeDescription of nodeType!.credentials!) { for (const credentialTypeDescription of nodeType.credentials) {
// Check if credentials should be displayed else ignore // Check if credentials should be displayed else ignore
if (this.displayParameter(node.parameters, credentialTypeDescription, '', node) !== true) { if (!this.displayParameter(node.parameters, credentialTypeDescription, '', node)) {
continue; continue;
} }
@ -293,9 +298,9 @@ export const nodeHelpers = mixins(
credentialDisplayName = credentialType.displayName; credentialDisplayName = credentialType.displayName;
} }
if (node.credentials === undefined || node.credentials[credentialTypeDescription.name] === undefined) { if (!node.credentials || !node.credentials?.[credentialTypeDescription.name]) {
// Credentials are not set // Credentials are not set
if (credentialTypeDescription.required === true) { if (credentialTypeDescription.required) {
foundIssues[credentialTypeDescription.name] = [this.$locale.baseText('nodeIssues.credentials.notSet', { interpolate: { type: credentialDisplayName } })]; foundIssues[credentialTypeDescription.name] = [this.$locale.baseText('nodeIssues.credentials.notSet', { interpolate: { type: credentialDisplayName } })];
} }
} else { } else {
@ -493,7 +498,7 @@ export const nodeHelpers = mixins(
* selected credentials are of the specified type. * selected credentials are of the specified type.
*/ */
function selectedCredsAreUnusable(node: INodeUi, credentialType: string) { 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);
} }
/** /**

View file

@ -1,4 +1,5 @@
import { getCredentialTypes, import {
getCredentialTypes,
getCredentialsNewName, getCredentialsNewName,
getAllCredentials, getAllCredentials,
deleteCredential, deleteCredential,
@ -8,6 +9,7 @@ import { getCredentialTypes,
oAuth2CredentialAuthorize, oAuth2CredentialAuthorize,
oAuth1CredentialAuthorize, oAuth1CredentialAuthorize,
testCredential, testCredential,
getForeignCredentials,
} from '@/api/credentials'; } from '@/api/credentials';
import Vue from 'vue'; import Vue from 'vue';
import { ActionContext, Module } from 'vuex'; import { ActionContext, Module } from 'vuex';
@ -17,7 +19,7 @@ import {
ICredentialsState, ICredentialsState,
ICredentialTypeMap, ICredentialTypeMap,
IRootState, IRootState,
} from '../Interface'; } from '@/Interface';
import { import {
ICredentialType, ICredentialType,
ICredentialsDecrypted, ICredentialsDecrypted,
@ -58,6 +60,15 @@ const module: Module<ICredentialsState, IRootState> = {
return accu; 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) { upsertCredential(state: ICredentialsState, credential: ICredentialsResponse) {
if (credential.id) { if (credential.id) {
Vue.set(state.credentials, credential.id, { ...state.credentials[credential.id], ...credential }); Vue.set(state.credentials, credential.id, { ...state.credentials[credential.id], ...credential });
@ -83,6 +94,10 @@ const module: Module<ICredentialsState, IRootState> = {
return Object.values(state.credentials) return Object.values(state.credentials)
.sort((a, b) => a.name.localeCompare(b.name)); .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 allCredentialsByType(state: ICredentialsState, getters: any): {[type: string]: ICredentialsResponse[]} { // tslint:disable-line:no-any
const credentials = getters.allCredentials as ICredentialsResponse[]; const credentials = getters.allCredentials as ICredentialsResponse[];
const types = getters.allCredentialTypes as ICredentialType[]; const types = getters.allCredentialTypes as ICredentialType[];
@ -181,6 +196,12 @@ const module: Module<ICredentialsState, IRootState> = {
return credentials; return credentials;
}, },
fetchForeignCredentials: async (context: ActionContext<ICredentialsState, IRootState>): Promise<ICredentialsResponse[]> => {
const credentials = await getForeignCredentials(context.rootGetters.getRestApiContext);
context.commit('setForeignCredentials', credentials);
return credentials;
},
getCredentialData: async (context: ActionContext<ICredentialsState, IRootState>, { id }: {id: string}) => { getCredentialData: async (context: ActionContext<ICredentialsState, IRootState>, { id }: {id: string}) => {
return await getCredentialData(context.rootGetters.getRestApiContext, id); return await getCredentialData(context.rootGetters.getRestApiContext, id);
}, },

View file

@ -15,6 +15,7 @@ import {
} from 'n8n-design-system'; } from 'n8n-design-system';
import englishBaseText from './locales/en.json'; import englishBaseText from './locales/en.json';
import { INodeProperties } from "n8n-workflow";
Vue.use(VueI18n); Vue.use(VueI18n);
locale.use('en'); locale.use('en');
@ -335,12 +336,11 @@ export class I18nClass {
* `fixedCollection` param having `multipleValues: true`. * `fixedCollection` param having `multipleValues: true`.
*/ */
multipleValueButtonText( multipleValueButtonText(
{ name: parameterName, typeOptions: { multipleValueButtonText } }: { name: parameterName, typeOptions}: INodeProperties,
{ name: string; typeOptions: { multipleValueButtonText: string; } },
) { ) {
return context.dynamicRender({ return context.dynamicRender({
key: `${initialKey}.${parameterName}.multipleValueButtonText`, key: `${initialKey}.${parameterName}.multipleValueButtonText`,
fallback: multipleValueButtonText, fallback: typeOptions!.multipleValueButtonText!,
}); });
}, },

View file

@ -254,26 +254,6 @@ export default mixins(
// When a node gets set as active deactivate the create-menu // When a node gets set as active deactivate the create-menu
this.createNodeActive = false; 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) { containsTrigger(containsTrigger) {
// Re-center CanvasAddButton if there's no triggers // Re-center CanvasAddButton if there's no triggers
if (containsTrigger === false) this.setRecenteredCanvasAddButtonPosition(this.getNodeViewOffsetPosition); if (containsTrigger === false) this.setRecenteredCanvasAddButtonPosition(this.getNodeViewOffsetPosition);
@ -348,11 +328,11 @@ export default mixins(
return this.$store.getters.allNodes; return this.$store.getters.allNodes;
}, },
runButtonText(): string { runButtonText(): string {
if (this.workflowRunning === false) { if (!this.workflowRunning) {
return this.$locale.baseText('nodeView.runButtonText.executeWorkflow'); return this.$locale.baseText('nodeView.runButtonText.executeWorkflow');
} }
if (this.executionWaitingForWebhook === true) { if (this.executionWaitingForWebhook) {
return this.$locale.baseText('nodeView.runButtonText.waitingForTriggerEvent'); return this.$locale.baseText('nodeView.runButtonText.waitingForTriggerEvent');
} }
@ -375,14 +355,14 @@ export default mixins(
}, },
workflowClasses() { workflowClasses() {
const returnClasses = []; const returnClasses = [];
if (this.ctrlKeyPressed === true) { if (this.ctrlKeyPressed) {
if (this.$store.getters.isNodeViewMoveInProgress === true) { if (this.$store.getters.isNodeViewMoveInProgress === true) {
returnClasses.push('move-in-process'); returnClasses.push('move-in-process');
} else { } else {
returnClasses.push('move-active'); 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 // Makes sure that nothing gets selected while select or move is active
returnClasses.push('do-not-select'); 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.$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 }); 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 // Check if any node contains an error
let nodeErrorFound = false; let nodeErrorFound = false;
if (data.data.resultData.runData) { if (data.data.resultData.runData) {
@ -628,7 +608,7 @@ export default mixins(
} }
} }
if (nodeErrorFound === false) { if (!nodeErrorFound) {
const resultError = data.data.resultData.error; const resultError = data.data.resultData.error;
const errorMessage = this.$getExecutionError(data.data); const errorMessage = this.$getExecutionError(data.data);
const shouldTrack = resultError && 'node' in resultError && resultError.node!.type.startsWith('n8n-nodes-base'); const shouldTrack = resultError && 'node' in resultError && resultError.node!.type.startsWith('n8n-nodes-base');
@ -724,7 +704,7 @@ export default mixins(
return; return;
} }
if (data === undefined) { if (!data) {
throw new Error( throw new Error(
this.$locale.baseText( this.$locale.baseText(
'nodeView.workflowWithIdCouldNotBeFound', 'nodeView.workflowWithIdCouldNotBeFound',
@ -800,9 +780,9 @@ export default mixins(
// Check if the keys got emitted from a message box or from something // Check if the keys got emitted from a message box or from something
// else which should ignore the default keybindings // else which should ignore the default keybindings
for (let index = 0; index < path.length; index++) { for (const element of path) {
if (path[index].className && typeof path[index].className === 'string' && ( if (element.className && typeof element.className === 'string' && (
path[index].className.includes('ignore-key-press') element.className.includes('ignore-key-press')
)) { )) {
return; return;
} }
@ -854,21 +834,21 @@ export default mixins(
this.resetZoom(); this.resetZoom();
} else if ((e.key === '1') && !this.isCtrlKeyPressed(e)) { } else if ((e.key === '1') && !this.isCtrlKeyPressed(e)) {
this.zoomToFit(); this.zoomToFit();
} else if ((e.key === 'a') && (this.isCtrlKeyPressed(e) === true)) { } else if ((e.key === 'a') && this.isCtrlKeyPressed(e)) {
// Select all nodes // Select all nodes
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
this.callDebounced('selectAllNodes', { debounceTime: 1000 }); 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 }); this.callDebounced('copySelectedNodes', { debounceTime: 1000 });
} else if ((e.key === 'x') && (this.isCtrlKeyPressed(e) === true)) { } else if ((e.key === 'x') && this.isCtrlKeyPressed(e)) {
// Cut nodes // Cut nodes
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
this.callDebounced('cutSelectedNodes', { debounceTime: 1000 }); 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 // Create a new workflow
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
@ -886,7 +866,7 @@ export default mixins(
title: this.$locale.baseText('nodeView.showMessage.keyDown.title'), title: this.$locale.baseText('nodeView.showMessage.keyDown.title'),
type: 'success', type: 'success',
}); });
} else if ((e.key === 's') && (this.isCtrlKeyPressed(e) === true)) { } else if ((e.key === 's') && this.isCtrlKeyPressed(e)) {
// Save workflow // Save workflow
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
@ -906,7 +886,7 @@ export default mixins(
} }
this.$store.commit('ndv/setActiveNodeName', lastSelectedNode.name); 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 // Select all downstream nodes
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
@ -926,7 +906,7 @@ export default mixins(
} }
this.callDebounced('nodeSelectedByName', { debounceTime: 100 }, connections.main[0][0].node, false, true); 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 // Select all downstream nodes
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
@ -969,14 +949,14 @@ export default mixins(
const connections = workflow.connectionsByDestinationNode[lastSelectedNode.name]; const connections = workflow.connectionsByDestinationNode[lastSelectedNode.name];
if (connections.main === undefined || connections.main.length === 0) { if (!Array.isArray(connections.main) || !connections.main.length) {
return; return;
} }
const parentNode = connections.main[0][0].node; const parentNode = connections.main[0][0].node;
const connectionsParent = this.$store.getters.outgoingConnectionsByNodeName(parentNode); const connectionsParent = this.$store.getters.outgoingConnectionsByNodeName(parentNode);
if (connectionsParent.main === undefined || connectionsParent.main.length === 0) { if (!Array.isArray(connectionsParent.main) || !connectionsParent.main.length) {
return; return;
} }
@ -1015,7 +995,7 @@ export default mixins(
}, },
deactivateSelectedNode() { deactivateSelectedNode() {
if (this.editAllowedCheck() === false) { if (!this.editAllowedCheck()) {
return; return;
} }
this.disableNodes(this.$store.getters.getSelectedNodes); this.disableNodes(this.$store.getters.getSelectedNodes);
@ -1160,13 +1140,12 @@ export default mixins(
} }
// https://docs.jsplumbtoolkit.com/community/current/articles/zooming.html // https://docs.jsplumbtoolkit.com/community/current/articles/zooming.html
const prependProperties = ['webkit', 'moz', 'ms', 'o'];
const scaleString = 'scale(' + zoomLevel + ')'; const scaleString = 'scale(' + zoomLevel + ')';
for (let i = 0; i < prependProperties.length; i++) { ['webkit', 'moz', 'ms', 'o'].forEach((prefix) => {
// @ts-ignore // @ts-ignore
element.style[prependProperties[i] + 'Transform'] = scaleString; element.style[prefix + 'Transform'] = scaleString;
} });
element.style['transform'] = scaleString; element.style['transform'] = scaleString;
// @ts-ignore // @ts-ignore
@ -1295,7 +1274,7 @@ export default mixins(
if (plainTextData.match(/^http[s]?:\/\/.*\.json$/i)) { if (plainTextData.match(/^http[s]?:\/\/.*\.json$/i)) {
// Pasted data points to a possible workflow JSON file // Pasted data points to a possible workflow JSON file
if (this.editAllowedCheck() === false) { if (!this.editAllowedCheck()) {
return; return;
} }
@ -1310,7 +1289,7 @@ export default mixins(
this.$locale.baseText('nodeView.confirmMessage.receivedCopyPasteData.cancelButtonText'), this.$locale.baseText('nodeView.confirmMessage.receivedCopyPasteData.cancelButtonText'),
); );
if (importConfirm === false) { if (!importConfirm) {
return; return;
} }
@ -1324,7 +1303,7 @@ export default mixins(
// Check first if it is valid JSON // Check first if it is valid JSON
workflowData = JSON.parse(plainTextData); workflowData = JSON.parse(plainTextData);
if (this.editAllowedCheck() === false) { if (!this.editAllowedCheck()) {
return; return;
} }
} catch (e) { } catch (e) {
@ -1509,7 +1488,7 @@ export default mixins(
this.lastSelectedConnection = null; this.lastSelectedConnection = null;
this.newNodeInsertPosition = null; this.newNodeInsertPosition = null;
if (setActive === true) { if (setActive) {
this.$store.commit('ndv/setActiveNodeName', node.name); this.$store.commit('ndv/setActiveNodeName', node.name);
} }
}, },
@ -1744,7 +1723,7 @@ export default mixins(
this.__addConnection(connectionData, true); this.__addConnection(connectionData, true);
}, },
async addNode(nodeTypeName: string, options: AddNodeOptions = {}) { async addNode(nodeTypeName: string, options: AddNodeOptions = {}) {
if (this.editAllowedCheck() === false) { if (!this.editAllowedCheck()) {
return; return;
} }
@ -1876,7 +1855,7 @@ export default mixins(
CanvasHelpers.resetConnection(info.connection); CanvasHelpers.resetConnection(info.connection);
if (this.isReadOnly === false) { if (!this.isReadOnly) {
let exitTimer: NodeJS.Timeout | undefined; let exitTimer: NodeJS.Timeout | undefined;
let enterTimer: NodeJS.Timeout | undefined; let enterTimer: NodeJS.Timeout | undefined;
info.connection.bind('mouseover', (connection: Connection) => { info.connection.bind('mouseover', (connection: Connection) => {
@ -2119,8 +2098,7 @@ export default mixins(
}, },
tryToAddWelcomeSticky: once(async function(this: any) { tryToAddWelcomeSticky: once(async function(this: any) {
const newWorkflow = this.workflowData; const newWorkflow = this.workflowData;
const flagAvailable = window.posthog !== undefined && window.posthog.getFeatureFlag !== undefined; if (window.posthog?.getFeatureFlag?.('welcome-note') === 'test') {
if (flagAvailable && window.posthog.getFeatureFlag('welcome-note') === 'test') {
// For novice users (onboardingFlowEnabled == true) // For novice users (onboardingFlowEnabled == true)
// Inject welcome sticky note and zoom to fit // Inject welcome sticky note and zoom to fit
@ -2250,7 +2228,7 @@ export default mixins(
return CanvasHelpers.getInputEndpointUUID(node.id, index); return CanvasHelpers.getInputEndpointUUID(node.id, index);
}, },
__addConnection(connection: [IConnection, IConnection], addVisualConnection = false) { __addConnection(connection: [IConnection, IConnection], addVisualConnection = false) {
if (addVisualConnection === true) { if (addVisualConnection) {
const outputUuid = this.getOutputEndpointUUID(connection[0].node, connection[0].index); const outputUuid = this.getOutputEndpointUUID(connection[0].node, connection[0].index);
const inputUuid = this.getInputEndpointUUID(connection[1].node, connection[1].index); const inputUuid = this.getInputEndpointUUID(connection[1].node, connection[1].index);
if (!outputUuid || !inputUuid) { if (!outputUuid || !inputUuid) {
@ -2280,7 +2258,7 @@ export default mixins(
}); });
}, },
__removeConnection(connection: [IConnection, IConnection], removeVisualConnection = false) { __removeConnection(connection: [IConnection, IConnection], removeVisualConnection = false) {
if (removeVisualConnection === true) { if (removeVisualConnection) {
const sourceId = this.$store.getters.getNodeByName(connection[0].node); const sourceId = this.$store.getters.getNodeByName(connection[0].node);
const targetId = this.$store.getters.getNodeByName(connection[1].node); const targetId = this.$store.getters.getNodeByName(connection[1].node);
// @ts-ignore // @ts-ignore
@ -2340,7 +2318,7 @@ export default mixins(
} }
}, },
async duplicateNode(nodeName: string) { async duplicateNode(nodeName: string) {
if (this.editAllowedCheck() === false) { if (!this.editAllowedCheck()) {
return; return;
} }
@ -2518,7 +2496,7 @@ export default mixins(
}); });
}, },
removeNode(nodeName: string) { removeNode(nodeName: string) {
if (this.editAllowedCheck() === false) { if (!this.editAllowedCheck()) {
return; return;
} }
@ -2545,7 +2523,7 @@ export default mixins(
} }
} }
if (deleteAllowed === false) { if (!deleteAllowed) {
return; return;
} }
} }
@ -3042,7 +3020,7 @@ export default mixins(
// Reset nodes // Reset nodes
this.deleteEveryEndpoint(); this.deleteEveryEndpoint();
if (this.executionWaitingForWebhook === true) { if (this.executionWaitingForWebhook) {
// Make sure that if there is a waiting test-webhook that // Make sure that if there is a waiting test-webhook that
// it gets removed // it gets removed
this.restApi().removeTestWebhook(this.$store.getters.workflowId) this.restApi().removeTestWebhook(this.$store.getters.workflowId)
@ -3088,6 +3066,7 @@ export default mixins(
}, },
async loadCredentials(): Promise<void> { async loadCredentials(): Promise<void> {
await this.$store.dispatch('credentials/fetchAllCredentials'); await this.$store.dispatch('credentials/fetchAllCredentials');
await this.$store.dispatch('credentials/fetchForeignCredentials');
}, },
async loadNodesProperties(nodeInfos: INodeTypeNameVersion[]): Promise<void> { async loadNodesProperties(nodeInfos: INodeTypeNameVersion[]): Promise<void> {
const allNodes: INodeTypeDescription[] = this.$store.getters['nodeTypes/allNodeTypes']; const allNodes: INodeTypeDescription[] = this.$store.getters['nodeTypes/allNodeTypes'];

View file

@ -1,7 +1,7 @@
import { getStyleTokenValue, isNumber } from "@/components/helpers"; import { getStyleTokenValue, isNumber } from "@/components/helpers";
import { NODE_OUTPUT_DEFAULT_KEY, START_NODE_TYPE, STICKY_NODE_TYPE, QUICKSTART_NOTE_NAME } from "@/constants"; import { NODE_OUTPUT_DEFAULT_KEY, START_NODE_TYPE, STICKY_NODE_TYPE, QUICKSTART_NOTE_NAME } from "@/constants";
import { IBounds, INodeUi, IZoomConfig, XYPosition } from "@/Interface"; import { EndpointStyle, IBounds, INodeUi, IZoomConfig, XYPosition } from "@/Interface";
import { Connection, Endpoint, Overlay, OverlaySpec, PaintStyle } from "jsplumb"; import { AnchorArraySpec, Connection, Endpoint, Overlay, OverlaySpec, PaintStyle } from "jsplumb";
import { import {
IConnection, IConnection,
INode, INode,
@ -146,7 +146,7 @@ export const CONNECTOR_ARROW_OVERLAYS: OverlaySpec[] = [
export const ANCHOR_POSITIONS: { export const ANCHOR_POSITIONS: {
[key: string]: { [key: string]: {
[key: number]: number[][]; [key: number]: AnchorArraySpec[];
} }
} = { } = {
input: { 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, width: 8,
height: nodeTypeData && nodeTypeData.outputs.length > 2 ? 18 : 20, height: nodeTypeData && nodeTypeData.outputs.length > 2 ? 18 : 20,
fill: getStyleTokenValue(color), fill: getStyleTokenValue(color),
@ -200,7 +200,7 @@ export const getInputEndpointStyle = (nodeTypeData: INodeTypeDescription, color:
lineWidth: 0, lineWidth: 0,
}); });
export const getInputNameOverlay = (label: string) => ([ export const getInputNameOverlay = (label: string): OverlaySpec => ([
'Label', 'Label',
{ {
id: OVERLAY_INPUT_NAME_LABEL, id: OVERLAY_INPUT_NAME_LABEL,
@ -217,7 +217,7 @@ export const getOutputEndpointStyle = (nodeTypeData: INodeTypeDescription, color
outlineStroke: 'none', outlineStroke: 'none',
}); });
export const getOutputNameOverlay = (label: string) => ([ export const getOutputNameOverlay = (label: string): OverlaySpec => ([
'Label', 'Label',
{ {
id: OVERLAY_OUTPUT_NAME_LABEL, id: OVERLAY_OUTPUT_NAME_LABEL,

View file

@ -17,3 +17,11 @@ export * from './WorkflowErrors';
export * from './WorkflowHooks'; export * from './WorkflowHooks';
export { LoggerProxy, NodeHelpers, ObservableObject, TelemetryHelpers }; export { LoggerProxy, NodeHelpers, ObservableObject, TelemetryHelpers };
export { deepCopy, jsonParse } from './utils'; export { deepCopy, jsonParse } from './utils';
export {
isINodeProperties,
isINodePropertyOptions,
isINodePropertyCollection,
isINodePropertiesList,
isINodePropertyCollectionList,
isINodePropertyOptionsList,
} from './type-guards';

View file

@ -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);
};