refactor(editor): Refactor nodeHelpers mixin to composable (#7810)

- Convert `nodeHelpers` mixin into composable and fix types
- Replace usage of the mixin with the new composable
- Add missing store imports in components that were dependent on opaque
imports from nodeHelpers mixin
- Refactor the `CollectionParameter` component to the modern script
setup syntax
Github issue / Community forum post (link here to close automatically):

---------

Signed-off-by: Oleg Ivaniv <me@olegivaniv.com>
This commit is contained in:
oleg 2023-12-08 16:59:03 +01:00 committed by GitHub
parent e8a493f718
commit 35fbc37c8e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 1017 additions and 977 deletions

View file

@ -25,16 +25,22 @@ import type { IBinaryData, IRunData } from 'n8n-workflow';
import BinaryDataDisplayEmbed from '@/components/BinaryDataDisplayEmbed.vue'; import BinaryDataDisplayEmbed from '@/components/BinaryDataDisplayEmbed.vue';
import { nodeHelpers } from '@/mixins/nodeHelpers';
import { useWorkflowsStore } from '@/stores/workflows.store'; import { useWorkflowsStore } from '@/stores/workflows.store';
import { useNodeHelpers } from '@/composables/useNodeHelpers';
export default defineComponent({ export default defineComponent({
name: 'BinaryDataDisplay', name: 'BinaryDataDisplay',
mixins: [nodeHelpers],
components: { components: {
BinaryDataDisplayEmbed, BinaryDataDisplayEmbed,
}, },
setup() {
const nodeHelpers = useNodeHelpers();
return {
nodeHelpers,
};
},
props: [ props: [
'displayData', // IBinaryData 'displayData', // IBinaryData
'windowVisible', // boolean 'windowVisible', // boolean
@ -42,7 +48,7 @@ export default defineComponent({
computed: { computed: {
...mapStores(useWorkflowsStore), ...mapStores(useWorkflowsStore),
binaryData(): IBinaryData | null { binaryData(): IBinaryData | null {
const binaryData = this.getBinaryData( const binaryData = this.nodeHelpers.getBinaryData(
this.workflowRunData, this.workflowRunData,
this.displayData.node, this.displayData.node,
this.displayData.runIndex, this.displayData.runIndex,

View file

@ -70,6 +70,7 @@ import { completerExtension } from './completer';
import { codeNodeEditorTheme } from './theme'; import { codeNodeEditorTheme } from './theme';
import AskAI from './AskAI/AskAI.vue'; import AskAI from './AskAI/AskAI.vue';
import { useMessage } from '@/composables/useMessage'; import { useMessage } from '@/composables/useMessage';
import { useSettingsStore } from '@/stores/settings.store';
export default defineComponent({ export default defineComponent({
name: 'code-node-editor', name: 'code-node-editor',
@ -156,7 +157,7 @@ export default defineComponent({
}, },
}, },
computed: { computed: {
...mapStores(useRootStore, usePostHog), ...mapStores(useRootStore, usePostHog, useSettingsStore),
aiEnabled(): boolean { aiEnabled(): boolean {
const isAiExperimentEnabled = [ASK_AI_EXPERIMENT.gpt3, ASK_AI_EXPERIMENT.gpt4].includes( const isAiExperimentEnabled = [ASK_AI_EXPERIMENT.gpt3, ASK_AI_EXPERIMENT.gpt4].includes(
(this.posthogStore.getVariant(ASK_AI_EXPERIMENT.name) ?? '') as string, (this.posthogStore.getVariant(ASK_AI_EXPERIMENT.name) ?? '') as string,

View file

@ -19,10 +19,10 @@
<div v-if="parameterOptions.length > 0 && !isReadOnly" class="param-options"> <div v-if="parameterOptions.length > 0 && !isReadOnly" class="param-options">
<n8n-button <n8n-button
v-if="parameter.options.length === 1" v-if="(parameter.options ?? []).length === 1"
type="tertiary" type="tertiary"
block block
@click="optionSelected(parameter.options[0].name)" @click="optionSelected((parameter.options ?? [])[0].name)"
:label="getPlaceholderText" :label="getPlaceholderText"
/> />
<div v-else class="add-option"> <div v-else class="add-option">
@ -36,7 +36,11 @@
<n8n-option <n8n-option
v-for="item in parameterOptions" v-for="item in parameterOptions"
:key="item.name" :key="item.name"
:label="$locale.nodeText().collectionOptionDisplayName(parameter, item, path)" :label="
isNodePropertyCollection(item)
? i18n.nodeText().collectionOptionDisplayName(parameter, item, path)
: item.name
"
:value="item.name" :value="item.name"
> >
</n8n-option> </n8n-option>
@ -47,157 +51,155 @@
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts" setup>
import { defineAsyncComponent, defineComponent } from 'vue'; import { ref, computed, defineAsyncComponent } from 'vue';
import { mapStores } from 'pinia'; import type { IUpdateInformation } from '@/Interface';
import type { INodeUi, IUpdateInformation } from '@/Interface';
import type { INodeProperties, INodePropertyOptions } from 'n8n-workflow'; import type {
INodeParameters,
INodeProperties,
INodePropertyCollection,
INodePropertyOptions,
} from 'n8n-workflow';
import { deepCopy } from 'n8n-workflow'; import { deepCopy } from 'n8n-workflow';
import { nodeHelpers } from '@/mixins/nodeHelpers';
import { get } from 'lodash-es'; import { get } from 'lodash-es';
import { useNDVStore } from '@/stores/ndv.store'; import { useNDVStore } from '@/stores/ndv.store';
import { useNodeHelpers } from '@/composables/useNodeHelpers';
import { useI18n } from '@/composables/useI18n';
const ParameterInputList = defineAsyncComponent(async () => import('./ParameterInputList.vue')); const ParameterInputList = defineAsyncComponent(async () => import('./ParameterInputList.vue'));
export default defineComponent({ const selectedOption = ref<string | undefined>(undefined);
name: 'CollectionParameter', export interface Props {
mixins: [nodeHelpers], hideDelete?: boolean;
props: [ nodeValues: INodeParameters;
'hideDelete', // boolean parameter: INodeProperties;
'nodeValues', // NodeParameters path: string;
'parameter', // INodeProperties values: INodeProperties;
'path', // string isReadOnly?: boolean;
'values', // NodeParameters }
'isReadOnly', // boolean const emit = defineEmits<{
], (event: 'valueChanged', value: IUpdateInformation): void;
components: { }>();
ParameterInputList, const props = defineProps<Props>();
}, const ndvStore = useNDVStore();
data() { const i18n = useI18n();
return { const nodeHelpers = useNodeHelpers();
selectedOption: undefined,
};
},
computed: {
...mapStores(useNDVStore),
getPlaceholderText(): string {
const placeholder = this.$locale.nodeText().placeholder(this.parameter, this.path);
return placeholder ? placeholder : this.$locale.baseText('collectionParameter.choose');
},
getProperties(): INodeProperties[] {
const returnProperties = [];
let tempProperties;
for (const name of this.propertyNames) {
tempProperties = this.getOptionProperties(name);
if (tempProperties !== undefined) {
returnProperties.push(...tempProperties);
}
}
return returnProperties;
},
// Returns all the options which should be displayed
filteredOptions(): Array<INodePropertyOptions | INodeProperties> {
return (this.parameter.options as Array<INodePropertyOptions | INodeProperties>).filter(
(option) => {
return this.displayNodeParameter(option as INodeProperties);
},
);
},
node(): INodeUi | null {
return this.ndvStore.activeNode;
},
// Returns all the options which did not get added already
parameterOptions(): Array<INodePropertyOptions | INodeProperties> {
return this.filteredOptions.filter((option) => {
return !this.propertyNames.includes(option.name);
});
},
propertyNames(): string[] {
if (this.values) {
return Object.keys(this.values);
}
return [];
},
},
methods: {
getArgument(argumentName: string): string | number | boolean | undefined {
if (this.parameter.typeOptions === undefined) {
return undefined;
}
if (this.parameter.typeOptions[argumentName] === undefined) { const getPlaceholderText = computed(() => {
return undefined; return (
} i18n.nodeText().placeholder(props.parameter, props.path) ??
i18n.baseText('collectionParameter.choose')
return this.parameter.typeOptions[argumentName]; );
},
getOptionProperties(optionName: string): INodeProperties[] {
const properties: INodeProperties[] = [];
for (const option of this.parameter.options) {
if (option.name === optionName) {
properties.push(option);
}
}
return properties;
},
displayNodeParameter(parameter: INodeProperties) {
if (parameter.displayOptions === undefined) {
// If it is not defined no need to do a proper check
return true;
}
return this.displayParameter(this.nodeValues, parameter, this.path, this.node);
},
optionSelected(optionName: string) {
const options = this.getOptionProperties(optionName);
if (options.length === 0) {
return;
}
const option = options[0];
const name = `${this.path}.${option.name}`;
let parameterData;
if (option.typeOptions !== undefined && option.typeOptions.multipleValues === true) {
// Multiple values are allowed
let newValue;
if (option.type === 'fixedCollection') {
// The "fixedCollection" entries are different as they save values
// in an object and then underneath there is an array. So initialize
// them differently.
newValue = get(this.nodeValues, `${this.path}.${optionName}`, {});
} else {
// Everything else saves them directly as an array.
newValue = get(this.nodeValues, `${this.path}.${optionName}`, []);
newValue.push(deepCopy(option.default));
}
parameterData = {
name,
value: newValue,
};
} else {
// Add a new option
parameterData = {
name,
value: deepCopy(option.default),
};
}
this.$emit('valueChanged', parameterData);
this.selectedOption = undefined;
},
valueChanged(parameterData: IUpdateInformation) {
this.$emit('valueChanged', parameterData);
},
},
}); });
function isNodePropertyCollection(
object: INodePropertyOptions | INodeProperties | INodePropertyCollection,
): object is INodePropertyCollection {
return 'values' in object;
}
function displayNodeParameter(parameter: INodeProperties) {
if (parameter.displayOptions === undefined) {
// If it is not defined no need to do a proper check
return true;
}
return nodeHelpers.displayParameter(props.nodeValues, parameter, props.path, ndvStore.activeNode);
}
function getOptionProperties(optionName: string) {
const properties = [];
for (const option of props.parameter.options ?? []) {
if (option.name === optionName) {
properties.push(option);
}
}
return properties;
}
const propertyNames = computed<string[]>(() => {
if (props.values) {
return Object.keys(props.values);
}
return [];
});
const getProperties = computed(() => {
const returnProperties = [];
let tempProperties;
for (const name of propertyNames.value) {
tempProperties = getOptionProperties(name);
if (tempProperties !== undefined) {
returnProperties.push(...tempProperties);
}
}
return returnProperties;
});
const filteredOptions = computed<Array<INodePropertyOptions | INodeProperties>>(() => {
return (props.parameter.options as Array<INodePropertyOptions | INodeProperties>).filter(
(option) => {
return displayNodeParameter(option as INodeProperties);
},
);
});
const parameterOptions = computed(() => {
return filteredOptions.value.filter((option) => {
return !propertyNames.value.includes(option.name);
});
});
function optionSelected(optionName: string) {
const options = getOptionProperties(optionName);
if (options.length === 0) {
return;
}
const option = options[0];
const name = `${props.path}.${option.name}`;
let parameterData;
if (
'typeOptions' in option &&
option.typeOptions !== undefined &&
option.typeOptions.multipleValues === true
) {
// Multiple values are allowed
let newValue;
if (option.type === 'fixedCollection') {
// The "fixedCollection" entries are different as they save values
// in an object and then underneath there is an array. So initialize
// them differently.
const retrievedObjectValue = get(props.nodeValues, `${props.path}.${optionName}`, {});
newValue = retrievedObjectValue;
} else {
// Everything else saves them directly as an array.
const retrievedArrayValue = get(props.nodeValues, `${props.path}.${optionName}`, []) as Array<
typeof option.default
>;
if (Array.isArray(retrievedArrayValue)) {
newValue = retrievedArrayValue;
newValue.push(deepCopy(option.default));
}
}
parameterData = {
name,
value: newValue,
};
} else {
// Add a new option
parameterData = {
name,
value: 'default' in option ? deepCopy(option.default) : null,
};
}
emit('valueChanged', parameterData);
selectedOption.value = undefined;
}
function valueChanged(parameterData: IUpdateInformation) {
emit('valueChanged', parameterData);
}
</script> </script>
<style lang="scss"> <style lang="scss">

View file

@ -135,10 +135,9 @@ import type {
import { NodeHelpers } from 'n8n-workflow'; import { NodeHelpers } from 'n8n-workflow';
import CredentialIcon from '@/components/CredentialIcon.vue'; import CredentialIcon from '@/components/CredentialIcon.vue';
import { nodeHelpers } from '@/mixins/nodeHelpers';
import { useMessage } from '@/composables/useMessage';
import { useToast } from '@/composables/useToast'; import { useToast } from '@/composables/useToast';
import { useNodeHelpers } from '@/composables/useNodeHelpers';
import { useMessage } from '@/composables/useMessage';
import CredentialConfig from '@/components/CredentialEdit/CredentialConfig.vue'; import CredentialConfig from '@/components/CredentialEdit/CredentialConfig.vue';
import CredentialInfo from '@/components/CredentialEdit/CredentialInfo.vue'; import CredentialInfo from '@/components/CredentialEdit/CredentialInfo.vue';
import CredentialSharing from '@/components/CredentialEdit/CredentialSharing.ee.vue'; import CredentialSharing from '@/components/CredentialEdit/CredentialSharing.ee.vue';
@ -157,6 +156,8 @@ import { useUsersStore } from '@/stores/users.store';
import { useWorkflowsStore } from '@/stores/workflows.store'; import { useWorkflowsStore } from '@/stores/workflows.store';
import { useNDVStore } from '@/stores/ndv.store'; import { useNDVStore } from '@/stores/ndv.store';
import { useCredentialsStore } from '@/stores/credentials.store'; import { useCredentialsStore } from '@/stores/credentials.store';
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
import { import {
getNodeAuthOptions, getNodeAuthOptions,
getNodeCredentialForSelectedAuthType, getNodeCredentialForSelectedAuthType,
@ -172,7 +173,6 @@ interface NodeAccessMap {
export default defineComponent({ export default defineComponent({
name: 'CredentialEdit', name: 'CredentialEdit',
mixins: [nodeHelpers],
components: { components: {
CredentialSharing, CredentialSharing,
CredentialConfig, CredentialConfig,
@ -197,10 +197,13 @@ export default defineComponent({
}, },
}, },
setup() { setup() {
const nodeHelpers = useNodeHelpers();
return { return {
externalHooks: useExternalHooks(), externalHooks: useExternalHooks(),
...useToast(), ...useToast(),
...useMessage(), ...useMessage(),
nodeHelpers,
}; };
}, },
data() { data() {
@ -296,6 +299,7 @@ export default defineComponent({
useUIStore, useUIStore,
useUsersStore, useUsersStore,
useWorkflowsStore, useWorkflowsStore,
useNodeTypesStore,
), ),
activeNodeType(): INodeTypeDescription | null { activeNodeType(): INodeTypeDescription | null {
const activeNode = this.ndvStore.activeNode; const activeNode = this.ndvStore.activeNode;
@ -577,7 +581,12 @@ export default defineComponent({
return true; return true;
} }
return this.displayParameter(this.credentialData as INodeParameters, parameter, '', null); return this.nodeHelpers.displayParameter(
this.credentialData as INodeParameters,
parameter,
'',
null,
);
}, },
getCredentialProperties(name: string): INodeProperties[] { getCredentialProperties(name: string): INodeProperties[] {
const credentialTypeData = this.credentialsStore.getCredentialTypeByName(name); const credentialTypeData = this.credentialsStore.getCredentialTypeByName(name);
@ -957,7 +966,7 @@ export default defineComponent({
// Now that the credentials changed check if any nodes use credentials // Now that the credentials changed check if any nodes use credentials
// which have now a different name // which have now a different name
this.updateNodesCredentialsIssues(); this.nodeHelpers.updateNodesCredentialsIssues();
return credential; return credential;
}, },
@ -1004,7 +1013,7 @@ export default defineComponent({
this.isDeleting = false; this.isDeleting = false;
// Now that the credentials were removed check if any nodes used them // Now that the credentials were removed check if any nodes used them
this.updateNodesCredentialsIssues(); this.nodeHelpers.updateNodesCredentialsIssues();
this.credentialData = {}; this.credentialData = {};
this.showMessage({ this.showMessage({

View file

@ -157,10 +157,8 @@ import {
WAIT_TIME_UNLIMITED, WAIT_TIME_UNLIMITED,
} from '@/constants'; } from '@/constants';
import { nodeBase } from '@/mixins/nodeBase'; import { nodeBase } from '@/mixins/nodeBase';
import { nodeHelpers } from '@/mixins/nodeHelpers';
import { workflowHelpers } from '@/mixins/workflowHelpers'; import { workflowHelpers } from '@/mixins/workflowHelpers';
import { pinData } from '@/mixins/pinData'; import { pinData } from '@/mixins/pinData';
import type { import type {
ConnectionTypes, ConnectionTypes,
IExecutionsSummary, IExecutionsSummary,
@ -186,6 +184,7 @@ import { useNodeTypesStore } from '@/stores/nodeTypes.store';
import { EnableNodeToggleCommand } from '@/models/history'; import { EnableNodeToggleCommand } from '@/models/history';
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
import { type ContextMenuTarget, useContextMenu } from '@/composables/useContextMenu'; import { type ContextMenuTarget, useContextMenu } from '@/composables/useContextMenu';
import { useNodeHelpers } from '@/composables/useNodeHelpers';
import { useExternalHooks } from '@/composables/useExternalHooks'; import { useExternalHooks } from '@/composables/useExternalHooks';
export default defineComponent({ export default defineComponent({
@ -193,10 +192,11 @@ export default defineComponent({
setup() { setup() {
const contextMenu = useContextMenu(); const contextMenu = useContextMenu();
const externalHooks = useExternalHooks(); const externalHooks = useExternalHooks();
const nodeHelpers = useNodeHelpers();
return { contextMenu, externalHooks }; return { contextMenu, externalHooks, nodeHelpers };
}, },
mixins: [nodeBase, nodeHelpers, workflowHelpers, pinData, debounceHelper], mixins: [nodeBase, workflowHelpers, pinData, debounceHelper],
components: { components: {
TitledList, TitledList,
FontAwesomeIcon, FontAwesomeIcon,
@ -631,7 +631,9 @@ export default defineComponent({
// and ends up bogging down the UI with big workflows, for example when pasting a workflow or even opening a node... // and ends up bogging down the UI with big workflows, for example when pasting a workflow or even opening a node...
// so we only update it when necessary (when node is mounted and when it's opened and closed (isActive)) // so we only update it when necessary (when node is mounted and when it's opened and closed (isActive))
try { try {
const nodeSubtitle = this.getNodeSubtitle(this.data, this.nodeType, this.workflow) || ''; const nodeSubtitle =
this.nodeHelpers.getNodeSubtitle(this.data, this.nodeType, this.getCurrentWorkflow()) ||
'';
this.nodeSubtitle = nodeSubtitle.includes(CUSTOM_API_CALL_KEY) ? '' : nodeSubtitle; this.nodeSubtitle = nodeSubtitle.includes(CUSTOM_API_CALL_KEY) ? '' : nodeSubtitle;
} catch (e) { } catch (e) {
@ -640,7 +642,7 @@ export default defineComponent({
}, },
disableNode() { disableNode() {
if (this.data !== null) { if (this.data !== null) {
this.disableNodes([this.data]); this.nodeHelpers.disableNodes([this.data]);
this.historyStore.pushCommandToUndo( this.historyStore.pushCommandToUndo(
new EnableNodeToggleCommand( new EnableNodeToggleCommand(
this.data.name, this.data.name,

View file

@ -118,7 +118,7 @@ import type {
} from 'n8n-workflow'; } from 'n8n-workflow';
import { genericHelpers } from '@/mixins/genericHelpers'; import { genericHelpers } from '@/mixins/genericHelpers';
import { nodeHelpers } from '@/mixins/nodeHelpers'; import { useNodeHelpers } from '@/composables/useNodeHelpers';
import { useToast } from '@/composables/useToast'; import { useToast } from '@/composables/useToast';
import TitledList from '@/components/TitledList.vue'; import TitledList from '@/components/TitledList.vue';
@ -144,7 +144,7 @@ interface CredentialDropdownOption extends ICredentialsResponse {
export default defineComponent({ export default defineComponent({
name: 'NodeCredentials', name: 'NodeCredentials',
mixins: [genericHelpers, nodeHelpers], mixins: [genericHelpers],
props: { props: {
readonly: { readonly: {
type: Boolean, type: Boolean,
@ -170,8 +170,11 @@ export default defineComponent({
TitledList, TitledList,
}, },
setup() { setup() {
const nodeHelpers = useNodeHelpers();
return { return {
...useToast(), ...useToast(),
nodeHelpers,
}; };
}, },
data() { data() {
@ -291,8 +294,6 @@ export default defineComponent({
}); });
}, },
credentialTypesNodeDescription(): INodeCredentialDescription[] { credentialTypesNodeDescription(): INodeCredentialDescription[] {
const node = this.node;
const credType = this.credentialsStore.getCredentialTypeByName(this.overrideCredType); const credType = this.credentialsStore.getCredentialTypeByName(this.overrideCredType);
if (credType) return [credType]; if (credType) return [credType];
@ -433,7 +434,7 @@ export default defineComponent({
this.$telemetry.track('User selected credential from node modal', { this.$telemetry.track('User selected credential from node modal', {
credential_type: credentialType, credential_type: credentialType,
node_type: this.node.type, node_type: this.node.type,
...(this.hasProxyAuth(this.node) ? { is_service_specific: true } : {}), ...(this.nodeHelpers.hasProxyAuth(this.node) ? { is_service_specific: true } : {}),
workflow_id: this.workflowsStore.workflowId, workflow_id: this.workflowsStore.workflowId,
credential_id: credentialId, credential_id: credentialId,
}); });
@ -461,7 +462,7 @@ export default defineComponent({
invalid: oldCredentials, invalid: oldCredentials,
type: selectedCredentialsType, type: selectedCredentialsType,
}); });
this.updateNodesCredentialsIssues(); this.nodeHelpers.updateNodesCredentialsIssues();
this.showMessage({ this.showMessage({
title: this.$locale.baseText('nodeCredentials.showMessage.title'), title: this.$locale.baseText('nodeCredentials.showMessage.title'),
message: this.$locale.baseText('nodeCredentials.showMessage.message', { message: this.$locale.baseText('nodeCredentials.showMessage.message', {
@ -512,7 +513,12 @@ export default defineComponent({
// If it is not defined no need to do a proper check // If it is not defined no need to do a proper check
return true; return true;
} }
return this.displayParameter(this.node.parameters, credentialTypeDescription, '', this.node); return this.nodeHelpers.displayParameter(
this.node.parameters,
credentialTypeDescription,
'',
this.node,
);
}, },
getIssues(credentialTypeName: string): string[] { getIssues(credentialTypeName: string): string[] {

View file

@ -146,8 +146,6 @@ import type {
} from 'n8n-workflow'; } from 'n8n-workflow';
import { jsonParse, NodeHelpers, NodeConnectionType } from 'n8n-workflow'; import { jsonParse, NodeHelpers, NodeConnectionType } from 'n8n-workflow';
import type { IExecutionResponse, INodeUi, IUpdateInformation, TargetItem } from '@/Interface'; import type { IExecutionResponse, INodeUi, IUpdateInformation, TargetItem } from '@/Interface';
import { nodeHelpers } from '@/mixins/nodeHelpers';
import { workflowHelpers } from '@/mixins/workflowHelpers'; import { workflowHelpers } from '@/mixins/workflowHelpers';
import NodeSettings from '@/components/NodeSettings.vue'; import NodeSettings from '@/components/NodeSettings.vue';
@ -173,12 +171,13 @@ import { useNodeTypesStore } from '@/stores/nodeTypes.store';
import { useUIStore } from '@/stores/ui.store'; import { useUIStore } from '@/stores/ui.store';
import { useSettingsStore } from '@/stores/settings.store'; import { useSettingsStore } from '@/stores/settings.store';
import { useDeviceSupport } from 'n8n-design-system/composables/useDeviceSupport'; import { useDeviceSupport } from 'n8n-design-system/composables/useDeviceSupport';
import { useNodeHelpers } from '@/composables/useNodeHelpers';
import { useMessage } from '@/composables/useMessage'; import { useMessage } from '@/composables/useMessage';
import { useExternalHooks } from '@/composables/useExternalHooks'; import { useExternalHooks } from '@/composables/useExternalHooks';
export default defineComponent({ export default defineComponent({
name: 'NodeDetailsView', name: 'NodeDetailsView',
mixins: [nodeHelpers, workflowHelpers, workflowActivate, pinData], mixins: [workflowHelpers, workflowActivate, pinData],
components: { components: {
NodeSettings, NodeSettings,
InputPanel, InputPanel,
@ -200,9 +199,11 @@ export default defineComponent({
}, },
setup(props, ctx) { setup(props, ctx) {
const externalHooks = useExternalHooks(); const externalHooks = useExternalHooks();
const nodeHelpers = useNodeHelpers();
return { return {
externalHooks, externalHooks,
nodeHelpers,
...useDeviceSupport(), ...useDeviceSupport(),
...useMessage(), ...useMessage(),
// eslint-disable-next-line @typescript-eslint/no-misused-promises // eslint-disable-next-line @typescript-eslint/no-misused-promises
@ -471,14 +472,18 @@ export default defineComponent({
setTimeout(() => this.ndvStore.setNDVSessionId(), 0); setTimeout(() => this.ndvStore.setNDVSessionId(), 0);
void this.externalHooks.run('dataDisplay.nodeTypeChanged', { void this.externalHooks.run('dataDisplay.nodeTypeChanged', {
nodeSubtitle: this.getNodeSubtitle(node, this.activeNodeType, this.getCurrentWorkflow()), nodeSubtitle: this.nodeHelpers.getNodeSubtitle(
node,
this.activeNodeType,
this.getCurrentWorkflow(),
),
}); });
setTimeout(() => { setTimeout(() => {
if (this.activeNode) { if (this.activeNode) {
const outgoingConnections = this.workflowsStore.outgoingConnectionsByNodeName( const outgoingConnections = this.workflowsStore.outgoingConnectionsByNodeName(
this.activeNode.name, this.activeNode.name,
) as INodeConnections; );
this.$telemetry.track('User opened node modal', { this.$telemetry.track('User opened node modal', {
node_type: this.activeNodeType ? this.activeNodeType.name : '', node_type: this.activeNodeType ? this.activeNodeType.name : '',
@ -493,8 +498,7 @@ export default defineComponent({
: this.ndvStore.inputPanelDisplayMode, : this.ndvStore.inputPanelDisplayMode,
selected_view_outputs: this.ndvStore.outputPanelDisplayMode, selected_view_outputs: this.ndvStore.outputPanelDisplayMode,
input_connectors: this.parentNodes.length, input_connectors: this.parentNodes.length,
output_connectors: output_connectors: outgoingConnections?.main?.length,
outgoingConnections && outgoingConnections.main && outgoingConnections.main.length,
input_displayed_run_index: this.inputRun, input_displayed_run_index: this.inputRun,
output_displayed_run_index: this.outputRun, output_displayed_run_index: this.outputRun,
data_pinning_tooltip_presented: this.pinDataDiscoveryTooltipVisible, data_pinning_tooltip_presented: this.pinDataDiscoveryTooltipVisible,

View file

@ -119,7 +119,7 @@
</div> </div>
<div <div
v-if="isCustomApiCallSelected(nodeValues)" v-if="nodeHelpers.isCustomApiCallSelected(nodeValues)"
class="parameter-item parameter-notice" class="parameter-item parameter-notice"
data-test-id="node-parameters-http-notice" data-test-id="node-parameters-http-notice"
> >
@ -201,8 +201,6 @@ import NodeSettingsTabs from '@/components/NodeSettingsTabs.vue';
import NodeWebhooks from '@/components/NodeWebhooks.vue'; import NodeWebhooks from '@/components/NodeWebhooks.vue';
import { get, set, unset } from 'lodash-es'; import { get, set, unset } from 'lodash-es';
import { nodeHelpers } from '@/mixins/nodeHelpers';
import NodeExecuteButton from './NodeExecuteButton.vue'; import NodeExecuteButton from './NodeExecuteButton.vue';
import { isCommunityPackageName } from '@/utils/nodeTypesUtils'; import { isCommunityPackageName } from '@/utils/nodeTypesUtils';
import { useUIStore } from '@/stores/ui.store'; import { useUIStore } from '@/stores/ui.store';
@ -215,10 +213,10 @@ import useWorkflowsEEStore from '@/stores/workflows.ee.store';
import { useCredentialsStore } from '@/stores/credentials.store'; import { useCredentialsStore } from '@/stores/credentials.store';
import type { EventBus } from 'n8n-design-system'; import type { EventBus } from 'n8n-design-system';
import { useExternalHooks } from '@/composables/useExternalHooks'; import { useExternalHooks } from '@/composables/useExternalHooks';
import { useNodeHelpers } from '@/composables/useNodeHelpers';
export default defineComponent({ export default defineComponent({
name: 'NodeSettings', name: 'NodeSettings',
mixins: [nodeHelpers],
components: { components: {
NodeTitle, NodeTitle,
NodeCredentials, NodeCredentials,
@ -228,9 +226,12 @@ export default defineComponent({
NodeExecuteButton, NodeExecuteButton,
}, },
setup() { setup() {
const nodeHelpers = useNodeHelpers();
const externalHooks = useExternalHooks(); const externalHooks = useExternalHooks();
return { return {
externalHooks, externalHooks,
nodeHelpers,
}; };
}, },
computed: { computed: {
@ -679,7 +680,7 @@ export default defineComponent({
if (node) { if (node) {
// Update the issues // Update the issues
this.updateNodeCredentialIssues(node); this.nodeHelpers.updateNodeCredentialIssues(node);
} }
void this.externalHooks.run('nodeSettings.credentialSelected', { updateInformation }); void this.externalHooks.run('nodeSettings.credentialSelected', { updateInformation });
@ -813,8 +814,8 @@ export default defineComponent({
this.workflowsStore.setNodeParameters(updateInformation); this.workflowsStore.setNodeParameters(updateInformation);
this.updateNodeParameterIssuesByName(node.name); this.nodeHelpers.updateNodeParameterIssuesByName(node.name);
this.updateNodeCredentialIssuesByName(node.name); this.nodeHelpers.updateNodeCredentialIssuesByName(node.name);
} }
} else if (parameterData.name.startsWith('parameters.')) { } else if (parameterData.name.startsWith('parameters.')) {
// A node parameter changed // A node parameter changed
@ -896,8 +897,8 @@ export default defineComponent({
oldNodeParameters, oldNodeParameters,
}); });
this.updateNodeParameterIssuesByName(node.name); this.nodeHelpers.updateNodeParameterIssuesByName(node.name);
this.updateNodeCredentialIssuesByName(node.name); this.nodeHelpers.updateNodeCredentialIssuesByName(node.name);
this.$telemetry.trackNodeParametersValuesChange(nodeType.name, parameterData); this.$telemetry.trackNodeParametersValuesChange(nodeType.name, parameterData);
} else { } else {
// A property on the node itself changed // A property on the node itself changed
@ -1060,7 +1061,7 @@ export default defineComponent({
this.setNodeValues(); this.setNodeValues();
this.eventBus?.on('openSettings', this.openSettings); this.eventBus?.on('openSettings', this.openSettings);
this.updateNodeParameterIssues(this.node as INodeUi, this.nodeType); this.nodeHelpers.updateNodeParameterIssues(this.node as INodeUi, this.nodeType);
}, },
beforeUnmount() { beforeUnmount() {
this.eventBus?.off('openSettings', this.openSettings); this.eventBus?.off('openSettings', this.openSettings);

View file

@ -395,7 +395,7 @@ import TextEdit from '@/components/TextEdit.vue';
import CodeNodeEditor from '@/components/CodeNodeEditor/CodeNodeEditor.vue'; import CodeNodeEditor from '@/components/CodeNodeEditor/CodeNodeEditor.vue';
import HtmlEditor from '@/components/HtmlEditor/HtmlEditor.vue'; import HtmlEditor from '@/components/HtmlEditor/HtmlEditor.vue';
import SqlEditor from '@/components/SqlEditor/SqlEditor.vue'; import SqlEditor from '@/components/SqlEditor/SqlEditor.vue';
import { nodeHelpers } from '@/mixins/nodeHelpers';
import { workflowHelpers } from '@/mixins/workflowHelpers'; import { workflowHelpers } from '@/mixins/workflowHelpers';
import { hasExpressionMapping, isValueExpression } from '@/utils/nodeTypesUtils'; import { hasExpressionMapping, isValueExpression } from '@/utils/nodeTypesUtils';
import { isResourceLocatorValue } from '@/utils/typeGuards'; import { isResourceLocatorValue } from '@/utils/typeGuards';
@ -417,6 +417,7 @@ import { useSettingsStore } from '@/stores/settings.store';
import { htmlEditorEventBus } from '@/event-bus'; import { htmlEditorEventBus } from '@/event-bus';
import type { EventBus } from 'n8n-design-system/utils'; import type { EventBus } from 'n8n-design-system/utils';
import { createEventBus } from 'n8n-design-system/utils'; import { createEventBus } from 'n8n-design-system/utils';
import { useNodeHelpers } from '@/composables/useNodeHelpers';
import { useI18n } from '@/composables/useI18n'; import { useI18n } from '@/composables/useI18n';
import type { N8nInput } from 'n8n-design-system'; import type { N8nInput } from 'n8n-design-system';
import { isCredentialOnlyNodeType } from '@/utils/credentialOnlyNodes'; import { isCredentialOnlyNodeType } from '@/utils/credentialOnlyNodes';
@ -426,7 +427,7 @@ type Picker = { $emit: (arg0: string, arg1: Date) => void };
export default defineComponent({ export default defineComponent({
name: 'parameter-input', name: 'parameter-input',
mixins: [nodeHelpers, workflowHelpers, debounceHelper], mixins: [workflowHelpers, debounceHelper],
components: { components: {
CodeNodeEditor, CodeNodeEditor,
HtmlEditor, HtmlEditor,
@ -505,10 +506,12 @@ export default defineComponent({
setup() { setup() {
const externalHooks = useExternalHooks(); const externalHooks = useExternalHooks();
const i18n = useI18n(); const i18n = useI18n();
const nodeHelpers = useNodeHelpers();
return { return {
externalHooks, externalHooks,
i18n, i18n,
nodeHelpers,
}; };
}, },
data() { data() {
@ -881,7 +884,7 @@ export default defineComponent({
if (node) { if (node) {
// Update the issues // Update the issues
this.updateNodeCredentialIssues(node); this.nodeHelpers.updateNodeCredentialIssues(node);
} }
void this.externalHooks.run('nodeSettings.credentialSelected', { updateInformation }); void this.externalHooks.run('nodeSettings.credentialSelected', { updateInformation });

View file

@ -14,7 +14,7 @@
> >
<multiple-parameter <multiple-parameter
:parameter="parameter" :parameter="parameter"
:values="getParameterValue(nodeValues, parameter.name, path)" :values="nodeHelpers.getParameterValue(nodeValues, parameter.name, path)"
:nodeValues="nodeValues" :nodeValues="nodeValues"
:path="getPath(parameter.name)" :path="getPath(parameter.name)"
:isReadOnly="isReadOnly" :isReadOnly="isReadOnly"
@ -71,7 +71,7 @@
<collection-parameter <collection-parameter
v-if="parameter.type === 'collection'" v-if="parameter.type === 'collection'"
:parameter="parameter" :parameter="parameter"
:values="getParameterValue(nodeValues, parameter.name, path)" :values="nodeHelpers.getParameterValue(nodeValues, parameter.name, path)"
:nodeValues="nodeValues" :nodeValues="nodeValues"
:path="getPath(parameter.name)" :path="getPath(parameter.name)"
:isReadOnly="isReadOnly" :isReadOnly="isReadOnly"
@ -80,7 +80,7 @@
<fixed-collection-parameter <fixed-collection-parameter
v-else-if="parameter.type === 'fixedCollection'" v-else-if="parameter.type === 'fixedCollection'"
:parameter="parameter" :parameter="parameter"
:values="getParameterValue(nodeValues, parameter.name, path)" :values="nodeHelpers.getParameterValue(nodeValues, parameter.name, path)"
:nodeValues="nodeValues" :nodeValues="nodeValues"
:path="getPath(parameter.name)" :path="getPath(parameter.name)"
:isReadOnly="isReadOnly" :isReadOnly="isReadOnly"
@ -118,7 +118,7 @@
<parameter-input-full <parameter-input-full
:parameter="parameter" :parameter="parameter"
:hide-issues="hiddenIssuesInputs.includes(parameter.name)" :hide-issues="hiddenIssuesInputs.includes(parameter.name)"
:value="getParameterValue(nodeValues, parameter.name, path)" :value="nodeHelpers.getParameterValue(nodeValues, parameter.name, path)"
:displayOptions="shouldShowOptions(parameter)" :displayOptions="shouldShowOptions(parameter)"
:path="getPath(parameter.name)" :path="getPath(parameter.name)"
:isReadOnly="isReadOnly" :isReadOnly="isReadOnly"
@ -164,6 +164,7 @@ import {
} from '@/utils/nodeTypesUtils'; } from '@/utils/nodeTypesUtils';
import { get, set } from 'lodash-es'; import { get, set } from 'lodash-es';
import { nodeViewEventBus } from '@/event-bus'; import { nodeViewEventBus } from '@/event-bus';
import { useNodeHelpers } from '@/composables/useNodeHelpers';
const FixedCollectionParameter = defineAsyncComponent( const FixedCollectionParameter = defineAsyncComponent(
async () => import('./FixedCollectionParameter.vue'), async () => import('./FixedCollectionParameter.vue'),
@ -181,6 +182,13 @@ export default defineComponent({
ImportParameter, ImportParameter,
ResourceMapper, ResourceMapper,
}, },
setup() {
const nodeHelpers = useNodeHelpers();
return {
nodeHelpers,
};
},
props: { props: {
nodeValues: { nodeValues: {
type: Object as PropType<INodeParameters>, type: Object as PropType<INodeParameters>,
@ -348,7 +356,7 @@ export default defineComponent({
} }
if ( if (
this.isCustomApiCallSelected(this.nodeValues) && this.nodeHelpers.isCustomApiCallSelected(this.nodeValues) &&
this.mustHideDuringCustomApiCall(parameter, this.nodeValues) this.mustHideDuringCustomApiCall(parameter, this.nodeValues)
) { ) {
return false; return false;
@ -422,13 +430,13 @@ export default defineComponent({
if (this.path) { if (this.path) {
rawValues = deepCopy(this.nodeValues); rawValues = deepCopy(this.nodeValues);
set(rawValues, this.path, nodeValues); set(rawValues, this.path, nodeValues);
return this.displayParameter(rawValues, parameter, this.path, this.node); return this.nodeHelpers.displayParameter(rawValues, parameter, this.path, this.node);
} else { } else {
return this.displayParameter(nodeValues, parameter, '', this.node); return this.nodeHelpers.displayParameter(nodeValues, parameter, '', this.node);
} }
} }
return this.displayParameter(this.nodeValues, parameter, this.path, this.node); return this.nodeHelpers.displayParameter(this.nodeValues, parameter, this.path, this.node);
}, },
valueChanged(parameterData: IUpdateInformation): void { valueChanged(parameterData: IUpdateInformation): void {
this.$emit('valueChanged', parameterData); this.$emit('valueChanged', parameterData);

View file

@ -149,7 +149,6 @@ import DraggableTarget from '@/components/DraggableTarget.vue';
import ExpressionParameterInput from '@/components/ExpressionParameterInput.vue'; import ExpressionParameterInput from '@/components/ExpressionParameterInput.vue';
import ParameterIssues from '@/components/ParameterIssues.vue'; import ParameterIssues from '@/components/ParameterIssues.vue';
import { debounceHelper } from '@/mixins/debounce'; import { debounceHelper } from '@/mixins/debounce';
import { nodeHelpers } from '@/mixins/nodeHelpers';
import { workflowHelpers } from '@/mixins/workflowHelpers'; import { workflowHelpers } from '@/mixins/workflowHelpers';
import { useRootStore } from '@/stores/n8nRoot.store'; import { useRootStore } from '@/stores/n8nRoot.store';
import { useNDVStore } from '@/stores/ndv.store'; import { useNDVStore } from '@/stores/ndv.store';
@ -185,7 +184,7 @@ interface IResourceLocatorQuery {
export default defineComponent({ export default defineComponent({
name: 'resource-locator', name: 'resource-locator',
mixins: [debounceHelper, workflowHelpers, nodeHelpers], mixins: [debounceHelper, workflowHelpers],
components: { components: {
DraggableTarget, DraggableTarget,
ExpressionParameterInput, ExpressionParameterInput,

View file

@ -606,7 +606,6 @@ import BinaryDataDisplay from '@/components/BinaryDataDisplay.vue';
import NodeErrorView from '@/components/Error/NodeErrorView.vue'; import NodeErrorView from '@/components/Error/NodeErrorView.vue';
import { genericHelpers } from '@/mixins/genericHelpers'; import { genericHelpers } from '@/mixins/genericHelpers';
import { nodeHelpers } from '@/mixins/nodeHelpers';
import { pinData } from '@/mixins/pinData'; import { pinData } from '@/mixins/pinData';
import type { PinDataSource } from '@/mixins/pinData'; import type { PinDataSource } from '@/mixins/pinData';
import CodeNodeEditor from '@/components/CodeNodeEditor/CodeNodeEditor.vue'; import CodeNodeEditor from '@/components/CodeNodeEditor/CodeNodeEditor.vue';
@ -617,6 +616,7 @@ import { searchInObject } from '@/utils/objectUtils';
import { useWorkflowsStore } from '@/stores/workflows.store'; import { useWorkflowsStore } from '@/stores/workflows.store';
import { useNDVStore } from '@/stores/ndv.store'; import { useNDVStore } from '@/stores/ndv.store';
import { useNodeTypesStore } from '@/stores/nodeTypes.store'; import { useNodeTypesStore } from '@/stores/nodeTypes.store';
import { useNodeHelpers } from '@/composables/useNodeHelpers';
import { useToast } from '@/composables/useToast'; import { useToast } from '@/composables/useToast';
import { isObject } from 'lodash-es'; import { isObject } from 'lodash-es';
import { useExternalHooks } from '@/composables/useExternalHooks'; import { useExternalHooks } from '@/composables/useExternalHooks';
@ -633,7 +633,7 @@ export type EnterEditModeArgs = {
export default defineComponent({ export default defineComponent({
name: 'RunData', name: 'RunData',
mixins: [genericHelpers, nodeHelpers, pinData], mixins: [genericHelpers, pinData],
components: { components: {
BinaryDataDisplay, BinaryDataDisplay,
NodeErrorView, NodeErrorView,
@ -699,11 +699,13 @@ export default defineComponent({
}, },
}, },
setup() { setup() {
const nodeHelpers = useNodeHelpers();
const externalHooks = useExternalHooks(); const externalHooks = useExternalHooks();
return { return {
externalHooks,
...useToast(), ...useToast(),
externalHooks,
nodeHelpers,
}; };
}, },
data() { data() {
@ -931,7 +933,7 @@ export default defineComponent({
return []; return [];
} }
const binaryData = this.getBinaryData( const binaryData = this.nodeHelpers.getBinaryData(
this.workflowRunData, this.workflowRunData,
this.node.name, this.node.name,
this.runIndex, this.runIndex,
@ -1141,7 +1143,7 @@ export default defineComponent({
this.$telemetry.track('User clicked pin data icon', telemetryPayload); this.$telemetry.track('User clicked pin data icon', telemetryPayload);
} }
this.updateNodeParameterIssues(this.node); this.nodeHelpers.updateNodeParameterIssues(this.node);
if (this.hasPinData) { if (this.hasPinData) {
this.unsetPinData(this.node, source); this.unsetPinData(this.node, source);
@ -1282,7 +1284,7 @@ export default defineComponent({
let inputData: INodeExecutionData[] = []; let inputData: INodeExecutionData[] = [];
if (this.node) { if (this.node) {
inputData = this.getNodeInputData( inputData = this.nodeHelpers.getNodeInputData(
this.node, this.node,
runIndex, runIndex,
outputIndex, outputIndex,
@ -1365,7 +1367,7 @@ export default defineComponent({
}, },
clearExecutionData() { clearExecutionData() {
this.workflowsStore.setWorkflowExecutionData(null); this.workflowsStore.setWorkflowExecutionData(null);
this.updateNodesExecutionIssues(); this.nodeHelpers.updateNodesExecutionIssues();
}, },
isViewable(index: number, key: string): boolean { isViewable(index: number, key: string): boolean {
const { fileType } = this.binaryData[index][key]; const { fileType } = this.binaryData[index][key];

View file

@ -43,14 +43,14 @@ import type { INodeUi } from '@/Interface';
import type { IDataObject } from 'n8n-workflow'; import type { IDataObject } from 'n8n-workflow';
import { copyPaste } from '@/mixins/copyPaste'; import { copyPaste } from '@/mixins/copyPaste';
import { pinData } from '@/mixins/pinData'; import { pinData } from '@/mixins/pinData';
import { nodeHelpers } from '@/mixins/nodeHelpers';
import { genericHelpers } from '@/mixins/genericHelpers'; import { genericHelpers } from '@/mixins/genericHelpers';
import { clearJsonKey, convertPath } from '@/utils/typesUtils'; import { clearJsonKey, convertPath } from '@/utils/typesUtils';
import { executionDataToJson } from '@/utils/nodeTypesUtils'; import { executionDataToJson } from '@/utils/nodeTypesUtils';
import { useWorkflowsStore } from '@/stores/workflows.store'; import { useWorkflowsStore } from '@/stores/workflows.store';
import { useNDVStore } from '@/stores/ndv.store'; import { useNDVStore } from '@/stores/ndv.store';
import { useI18n } from '@/composables/useI18n'; import { useNodeHelpers } from '@/composables/useNodeHelpers';
import { useToast } from '@/composables/useToast'; import { useToast } from '@/composables/useToast';
import { useI18n } from '@/composables/useI18n';
import { nonExistingJsonPath } from '@/constants'; import { nonExistingJsonPath } from '@/constants';
type JsonPathData = { type JsonPathData = {
@ -60,7 +60,7 @@ type JsonPathData = {
export default defineComponent({ export default defineComponent({
name: 'run-data-json-actions', name: 'run-data-json-actions',
mixins: [genericHelpers, nodeHelpers, pinData, copyPaste], mixins: [genericHelpers, pinData, copyPaste],
props: { props: {
node: { node: {
@ -95,9 +95,10 @@ export default defineComponent({
}, },
setup() { setup() {
const i18n = useI18n(); const i18n = useI18n();
const nodeHelpers = useNodeHelpers();
return { return {
i18n, i18n,
nodeHelpers,
...useToast(), ...useToast(),
}; };
}, },
@ -121,7 +122,7 @@ export default defineComponent({
selectedValue = clearJsonKey(this.pinData as object); selectedValue = clearJsonKey(this.pinData as object);
} else { } else {
selectedValue = executionDataToJson( selectedValue = executionDataToJson(
this.getNodeInputData(this.node, this.runIndex, this.currentOutputIndex), this.nodeHelpers.getNodeInputData(this.node, this.runIndex, this.currentOutputIndex),
); );
} }
} }

View file

@ -105,7 +105,6 @@ import { defineComponent, ref } from 'vue';
import { mapStores } from 'pinia'; import { mapStores } from 'pinia';
import { nodeBase } from '@/mixins/nodeBase'; import { nodeBase } from '@/mixins/nodeBase';
import { nodeHelpers } from '@/mixins/nodeHelpers';
import { workflowHelpers } from '@/mixins/workflowHelpers'; import { workflowHelpers } from '@/mixins/workflowHelpers';
import { isNumber, isString } from '@/utils/typeGuards'; import { isNumber, isString } from '@/utils/typeGuards';
import type { import type {
@ -125,7 +124,7 @@ import { useContextMenu } from '@/composables/useContextMenu';
export default defineComponent({ export default defineComponent({
name: 'Sticky', name: 'Sticky',
mixins: [nodeBase, nodeHelpers, workflowHelpers], mixins: [nodeBase, workflowHelpers],
setup() { setup() {
const colorPopoverTrigger = ref<HTMLDivElement>(); const colorPopoverTrigger = ref<HTMLDivElement>();
const forceActions = ref(false); const forceActions = ref(false);

View file

@ -0,0 +1,723 @@
import { useHistoryStore } from '@/stores/history.store';
import { CUSTOM_API_CALL_KEY, PLACEHOLDER_FILLED_AT_EXECUTION_TIME } from '@/constants';
import { NodeHelpers, NodeConnectionType, ExpressionEvaluatorProxy } from 'n8n-workflow';
import type {
INodeProperties,
INodeCredentialDescription,
INodeTypeDescription,
INodeIssues,
ICredentialType,
INodeIssueObjectProperty,
ConnectionTypes,
INodeInputConfiguration,
Workflow,
INodeExecutionData,
ITaskDataConnections,
IRunData,
IBinaryKeyData,
IDataObject,
INode,
INodePropertyOptions,
INodeCredentialsDetails,
INodeParameters,
} from 'n8n-workflow';
import type {
ICredentialsResponse,
INodeUi,
INodeUpdatePropertiesInformation,
NodePanelType,
} from '@/Interface';
import { isString } from '@/utils/typeGuards';
import { isObject } from '@/utils/objectUtils';
import { useSettingsStore } from '@/stores/settings.store';
import { useUsersStore } from '@/stores/users.store';
import { useWorkflowsStore } from '@/stores/workflows.store';
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
import { useCredentialsStore } from '@/stores/credentials.store';
import { get } from 'lodash-es';
import { useI18n } from './useI18n';
import { EnableNodeToggleCommand } from '@/models/history';
import { useTelemetry } from './useTelemetry';
import { getCredentialPermissions } from '@/permissions';
import { hasPermission } from '@/rbac/permissions';
declare namespace HttpRequestNode {
namespace V2 {
type AuthParams = {
authentication: 'none' | 'genericCredentialType' | 'predefinedCredentialType';
genericAuthType: string;
nodeCredentialType: string;
};
}
}
export function useNodeHelpers() {
const credentialsStore = useCredentialsStore();
const historyStore = useHistoryStore();
const nodeTypesStore = useNodeTypesStore();
const workflowsStore = useWorkflowsStore();
const i18n = useI18n();
function hasProxyAuth(node: INodeUi): boolean {
return Object.keys(node.parameters).includes('nodeCredentialType');
}
function isCustomApiCallSelected(nodeValues: INodeParameters): boolean {
const { parameters } = nodeValues;
if (!isObject(parameters)) return false;
const { resource, operation } = parameters;
if (!isString(resource) || !isString(operation)) return false;
return resource.includes(CUSTOM_API_CALL_KEY) || operation.includes(CUSTOM_API_CALL_KEY);
}
function getParameterValue(nodeValues: INodeParameters, parameterName: string, path: string) {
return get(nodeValues, path ? path + '.' + parameterName : parameterName);
}
// Returns if the given parameter should be displayed or not
function displayParameter(
nodeValues: INodeParameters,
parameter: INodeProperties | INodeCredentialDescription,
path: string,
node: INodeUi | null,
) {
return NodeHelpers.displayParameterPath(nodeValues, parameter, path, node);
}
function refreshNodeIssues(): void {
const nodes = workflowsStore.allNodes;
const workflow = workflowsStore.getCurrentWorkflow();
let nodeType: INodeTypeDescription | null;
let foundNodeIssues: INodeIssues | null;
nodes.forEach((node) => {
if (node.disabled === true) return;
nodeType = nodeTypesStore.getNodeType(node.type, node.typeVersion);
foundNodeIssues = getNodeIssues(nodeType, node, workflow);
if (foundNodeIssues !== null) {
node.issues = foundNodeIssues;
}
});
}
function getNodeIssues(
nodeType: INodeTypeDescription | null,
node: INodeUi,
workflow: Workflow,
ignoreIssues?: string[],
): INodeIssues | null {
const pinDataNodeNames = Object.keys(workflowsStore.getPinData ?? {});
let nodeIssues: INodeIssues | null = null;
ignoreIssues = ignoreIssues ?? [];
if (node.disabled === true || pinDataNodeNames.includes(node.name)) {
// Ignore issues on disabled and pindata nodes
return null;
}
if (nodeType === null) {
// Node type is not known
if (!ignoreIssues.includes('typeUnknown')) {
nodeIssues = {
typeUnknown: true,
};
}
} else {
// Node type is known
// Add potential parameter issues
if (!ignoreIssues.includes('parameters')) {
nodeIssues = NodeHelpers.getNodeParametersIssues(nodeType.properties, node);
}
if (!ignoreIssues.includes('credentials')) {
// Add potential credential issues
const nodeCredentialIssues = getNodeCredentialIssues(node, nodeType);
if (nodeIssues === null) {
nodeIssues = nodeCredentialIssues;
} else {
NodeHelpers.mergeIssues(nodeIssues, nodeCredentialIssues);
}
}
const nodeInputIssues = getNodeInputIssues(workflow, node, nodeType);
if (nodeIssues === null) {
nodeIssues = nodeInputIssues;
} else {
NodeHelpers.mergeIssues(nodeIssues, nodeInputIssues);
}
}
if (hasNodeExecutionIssues(node) && !ignoreIssues.includes('execution')) {
if (nodeIssues === null) {
nodeIssues = {};
}
nodeIssues.execution = true;
}
return nodeIssues;
}
// Set the status on all the nodes which produced an error so that it can be
// displayed in the node-view
function hasNodeExecutionIssues(node: INodeUi): boolean {
const workflowResultData = workflowsStore.getWorkflowRunData;
if (workflowResultData === null || !workflowResultData.hasOwnProperty(node.name)) {
return false;
}
for (const taskData of workflowResultData[node.name]) {
if (taskData.error !== undefined) {
return true;
}
}
return false;
}
function reportUnsetCredential(credentialType: ICredentialType) {
return {
credentials: {
[credentialType.name]: [
i18n.baseText('nodeHelpers.credentialsUnset', {
interpolate: {
credentialType: credentialType.displayName,
},
}),
],
},
};
}
function updateNodesInputIssues() {
const nodes = workflowsStore.allNodes;
const workflow = workflowsStore.getCurrentWorkflow();
for (const node of nodes) {
const nodeType = nodeTypesStore.getNodeType(node.type, node.typeVersion);
if (!nodeType) {
return;
}
const nodeInputIssues = getNodeInputIssues(workflow, node, nodeType);
workflowsStore.setNodeIssue({
node: node.name,
type: 'input',
value: nodeInputIssues?.input ? nodeInputIssues.input : null,
});
}
}
function updateNodesExecutionIssues() {
const nodes = workflowsStore.allNodes;
for (const node of nodes) {
workflowsStore.setNodeIssue({
node: node.name,
type: 'execution',
value: hasNodeExecutionIssues(node) ? true : null,
});
}
}
function updateNodeCredentialIssuesByName(name: string): void {
const node = workflowsStore.getNodeByName(name);
if (node) {
updateNodeCredentialIssues(node);
}
}
function updateNodeCredentialIssues(node: INodeUi): void {
const fullNodeIssues: INodeIssues | null = getNodeCredentialIssues(node);
let newIssues: INodeIssueObjectProperty | null = null;
if (fullNodeIssues !== null) {
newIssues = fullNodeIssues.credentials!;
}
workflowsStore.setNodeIssue({
node: node.name,
type: 'credentials',
value: newIssues,
});
}
function updateNodeParameterIssuesByName(name: string): void {
const node = workflowsStore.getNodeByName(name);
if (node) {
updateNodeParameterIssues(node);
}
}
function updateNodeParameterIssues(node: INodeUi, nodeType?: INodeTypeDescription): void {
const localNodeType = nodeType ?? nodeTypesStore.getNodeType(node.type, node.typeVersion);
if (localNodeType === null) {
// Could not find localNodeType so can not update issues
return;
}
// All data got updated everywhere so update now the issues
const fullNodeIssues: INodeIssues | null = NodeHelpers.getNodeParametersIssues(
localNodeType.properties,
node,
);
let newIssues: INodeIssueObjectProperty | null = null;
if (fullNodeIssues !== null) {
newIssues = fullNodeIssues.parameters!;
}
workflowsStore.setNodeIssue({
node: node.name,
type: 'parameters',
value: newIssues,
});
}
function getNodeInputIssues(
workflow: Workflow,
node: INodeUi,
nodeType?: INodeTypeDescription,
): INodeIssues | null {
const foundIssues: INodeIssueObjectProperty = {};
const workflowNode = workflow.getNode(node.name);
let inputs: Array<ConnectionTypes | INodeInputConfiguration> = [];
if (nodeType && workflowNode) {
inputs = NodeHelpers.getNodeInputs(workflow, workflowNode, nodeType);
}
inputs.forEach((input) => {
if (typeof input === 'string' || input.required !== true) {
return;
}
const parentNodes = workflow.getParentNodes(node.name, input.type, 1);
if (parentNodes.length === 0) {
foundIssues[input.type] = [
i18n.baseText('nodeIssues.input.missing', {
interpolate: { inputName: input.displayName || input.type },
}),
];
}
});
if (Object.keys(foundIssues).length) {
return {
input: foundIssues,
};
}
return null;
}
function getNodeCredentialIssues(
node: INodeUi,
nodeType?: INodeTypeDescription,
): INodeIssues | null {
const localNodeType = nodeType ?? nodeTypesStore.getNodeType(node.type, node.typeVersion);
if (node.disabled) {
// Node is disabled
return null;
}
if (!localNodeType?.credentials) {
// Node does not need any credentials or nodeType could not be found
return null;
}
const foundIssues: INodeIssueObjectProperty = {};
let userCredentials: ICredentialsResponse[] | null;
let credentialType: ICredentialType | undefined;
let credentialDisplayName: string;
let selectedCredentials: INodeCredentialsDetails;
const { authentication, genericAuthType, nodeCredentialType } =
node.parameters as HttpRequestNode.V2.AuthParams;
if (
authentication === 'genericCredentialType' &&
genericAuthType !== '' &&
selectedCredsAreUnusable(node, genericAuthType)
) {
const credential = credentialsStore.getCredentialTypeByName(genericAuthType);
return credential ? reportUnsetCredential(credential) : null;
}
if (
hasProxyAuth(node) &&
authentication === 'predefinedCredentialType' &&
nodeCredentialType !== '' &&
node.credentials !== undefined
) {
const stored = credentialsStore.getCredentialsByType(nodeCredentialType);
if (selectedCredsDoNotExist(node, nodeCredentialType, stored)) {
const credential = credentialsStore.getCredentialTypeByName(nodeCredentialType);
return credential ? reportUnsetCredential(credential) : null;
}
}
if (
hasProxyAuth(node) &&
authentication === 'predefinedCredentialType' &&
nodeCredentialType !== '' &&
selectedCredsAreUnusable(node, nodeCredentialType)
) {
const credential = credentialsStore.getCredentialTypeByName(nodeCredentialType);
return credential ? reportUnsetCredential(credential) : null;
}
for (const credentialTypeDescription of localNodeType.credentials) {
// Check if credentials should be displayed else ignore
if (!displayParameter(node.parameters, credentialTypeDescription, '', node)) {
continue;
}
// Get the display name of the credential type
credentialType = credentialsStore.getCredentialTypeByName(credentialTypeDescription.name);
if (!credentialType) {
credentialDisplayName = credentialTypeDescription.name;
} else {
credentialDisplayName = credentialType.displayName;
}
if (!node.credentials?.[credentialTypeDescription.name]) {
// Credentials are not set
if (credentialTypeDescription.required) {
foundIssues[credentialTypeDescription.name] = [
i18n.baseText('nodeIssues.credentials.notSet', {
interpolate: { type: localNodeType.displayName },
}),
];
}
} else {
// If they are set check if the value is valid
selectedCredentials = node.credentials[credentialTypeDescription.name];
if (typeof selectedCredentials === 'string') {
selectedCredentials = {
id: null,
name: selectedCredentials,
};
}
const usersStore = useUsersStore();
const currentUser = usersStore.currentUser;
userCredentials = credentialsStore
.getCredentialsByType(credentialTypeDescription.name)
.filter((credential: ICredentialsResponse) => {
const permissions = getCredentialPermissions(currentUser, credential);
return permissions.read;
});
if (userCredentials === null) {
userCredentials = [];
}
if (selectedCredentials.id) {
const idMatch = userCredentials.find(
(credentialData) => credentialData.id === selectedCredentials.id,
);
if (idMatch) {
continue;
}
}
const nameMatches = userCredentials.filter(
(credentialData) => credentialData.name === selectedCredentials.name,
);
if (nameMatches.length > 1) {
foundIssues[credentialTypeDescription.name] = [
i18n.baseText('nodeIssues.credentials.notIdentified', {
interpolate: { name: selectedCredentials.name, type: credentialDisplayName },
}),
i18n.baseText('nodeIssues.credentials.notIdentified.hint'),
];
continue;
}
if (nameMatches.length === 0) {
const isCredentialUsedInWorkflow =
workflowsStore.usedCredentials?.[selectedCredentials.id as string];
if (
!isCredentialUsedInWorkflow &&
!hasPermission(['rbac'], { rbac: { scope: 'credential:read' } })
) {
foundIssues[credentialTypeDescription.name] = [
i18n.baseText('nodeIssues.credentials.doNotExist', {
interpolate: { name: selectedCredentials.name, type: credentialDisplayName },
}),
i18n.baseText('nodeIssues.credentials.doNotExist.hint'),
];
}
}
}
}
// TODO: Could later check also if the node has access to the credentials
if (Object.keys(foundIssues).length === 0) {
return null;
}
return {
credentials: foundIssues,
};
}
/**
* Whether the node has no selected credentials, or none of the node's
* selected credentials are of the specified type.
*/
function selectedCredsAreUnusable(node: INodeUi, credentialType: string) {
return !node.credentials || !Object.keys(node.credentials).includes(credentialType);
}
/**
* Whether the node's selected credentials of the specified type
* can no longer be found in the database.
*/
function selectedCredsDoNotExist(
node: INodeUi,
nodeCredentialType: string,
storedCredsByType: ICredentialsResponse[] | null,
) {
if (!node.credentials || !storedCredsByType) return false;
const selectedCredsByType = node.credentials[nodeCredentialType];
if (!selectedCredsByType) return false;
return !storedCredsByType.find((c) => c.id === selectedCredsByType.id);
}
function updateNodesCredentialsIssues() {
const nodes = workflowsStore.allNodes;
let issues: INodeIssues | null;
for (const node of nodes) {
issues = getNodeCredentialIssues(node);
workflowsStore.setNodeIssue({
node: node.name,
type: 'credentials',
value: issues === null ? null : issues.credentials,
});
}
}
function getNodeInputData(
node: INodeUi | null,
runIndex = 0,
outputIndex = 0,
paneType: NodePanelType = 'output',
connectionType: ConnectionTypes = NodeConnectionType.Main,
): INodeExecutionData[] {
if (node === null) {
return [];
}
if (workflowsStore.getWorkflowExecution === null) {
return [];
}
const executionData = workflowsStore.getWorkflowExecution.data;
if (!executionData?.resultData) {
// unknown status
return [];
}
const runData = executionData.resultData.runData;
const taskData = get(runData, `[${node.name}][${runIndex}]`);
if (!taskData) {
return [];
}
// TODO: Is this problematic?
let data: ITaskDataConnections | undefined = taskData.data!;
if (paneType === 'input' && taskData.inputOverride) {
data = taskData.inputOverride!;
}
if (!data) {
return [];
}
return getInputData(data, outputIndex, connectionType);
}
function getInputData(
connectionsData: ITaskDataConnections,
outputIndex: number,
connectionType: ConnectionTypes = NodeConnectionType.Main,
): INodeExecutionData[] {
if (
!connectionsData ||
!connectionsData.hasOwnProperty(connectionType) ||
connectionsData[connectionType] === undefined ||
connectionsData[connectionType].length < outputIndex ||
connectionsData[connectionType][outputIndex] === null
) {
return [];
}
return connectionsData[connectionType][outputIndex] as INodeExecutionData[];
}
function getBinaryData(
workflowRunData: IRunData | null,
node: string | null,
runIndex: number,
outputIndex: number,
connectionType: ConnectionTypes = NodeConnectionType.Main,
): IBinaryKeyData[] {
if (node === null) {
return [];
}
const runData: IRunData | null = workflowRunData;
if (!runData?.[node]?.[runIndex]?.data) {
return [];
}
const inputData = getInputData(runData[node][runIndex].data!, outputIndex, connectionType);
const returnData: IBinaryKeyData[] = [];
for (let i = 0; i < inputData.length; i++) {
if (inputData[i].hasOwnProperty('binary') && inputData[i].binary !== undefined) {
returnData.push(inputData[i].binary!);
}
}
return returnData;
}
function disableNodes(nodes: INodeUi[], trackHistory = false) {
const telemetry = useTelemetry();
if (trackHistory) {
historyStore.startRecordingUndo();
}
for (const node of nodes) {
const oldState = node.disabled;
// Toggle disabled flag
const updateInformation = {
name: node.name,
properties: {
disabled: !oldState,
} as IDataObject,
} as INodeUpdatePropertiesInformation;
telemetry.track('User set node enabled status', {
node_type: node.type,
is_enabled: node.disabled,
workflow_id: workflowsStore.workflowId,
});
workflowsStore.updateNodeProperties(updateInformation);
workflowsStore.clearNodeExecutionData(node.name);
updateNodeParameterIssues(node);
updateNodeCredentialIssues(node);
updateNodesInputIssues();
if (trackHistory) {
historyStore.pushCommandToUndo(
new EnableNodeToggleCommand(node.name, oldState === true, node.disabled === true),
);
}
}
if (trackHistory) {
historyStore.stopRecordingUndo();
}
}
function getNodeSubtitle(
data: INode,
nodeType: INodeTypeDescription,
workflow: Workflow,
): string | undefined {
if (!data) {
return undefined;
}
if (data.notesInFlow) {
return data.notes;
}
if (nodeType?.subtitle !== undefined) {
try {
ExpressionEvaluatorProxy.setEvaluator(
useSettingsStore().settings.expressions?.evaluator ?? 'tmpl',
);
return workflow.expression.getSimpleParameterValue(
data,
nodeType.subtitle,
'internal',
{},
undefined,
PLACEHOLDER_FILLED_AT_EXECUTION_TIME,
) as string | undefined;
} catch (e) {
return undefined;
}
}
if (data.parameters.operation !== undefined) {
const operation = data.parameters.operation as string;
if (nodeType === null) {
return operation;
}
const operationData = nodeType.properties.find((property: INodeProperties) => {
return property.name === 'operation';
});
if (operationData === undefined) {
return operation;
}
if (operationData.options === undefined) {
return operation;
}
const optionData = operationData.options.find((option) => {
return (option as INodePropertyOptions).value === data.parameters.operation;
});
if (optionData === undefined) {
return operation;
}
return optionData.name;
}
return undefined;
}
return {
hasProxyAuth,
isCustomApiCallSelected,
getParameterValue,
displayParameter,
getNodeIssues,
refreshNodeIssues,
updateNodesInputIssues,
updateNodesExecutionIssues,
updateNodeCredentialIssuesByName,
updateNodeCredentialIssues,
updateNodeParameterIssuesByName,
updateNodeParameterIssues,
getBinaryData,
disableNodes,
getNodeSubtitle,
updateNodesCredentialsIssues,
getNodeInputData,
};
}

View file

@ -1,743 +0,0 @@
import { EnableNodeToggleCommand } from './../models/history';
import { useHistoryStore } from '@/stores/history.store';
import { PLACEHOLDER_FILLED_AT_EXECUTION_TIME, CUSTOM_API_CALL_KEY } from '@/constants';
import type {
ConnectionTypes,
IBinaryKeyData,
ICredentialType,
INodeCredentialDescription,
INodeCredentialsDetails,
INodeExecutionData,
INodeIssues,
INodeIssueObjectProperty,
INodeParameters,
INodeProperties,
INodeTypeDescription,
IRunData,
ITaskDataConnections,
INode,
INodePropertyOptions,
IDataObject,
Workflow,
INodeInputConfiguration,
} from 'n8n-workflow';
import { NodeHelpers, ExpressionEvaluatorProxy, NodeConnectionType } from 'n8n-workflow';
import type {
ICredentialsResponse,
INodeUi,
INodeUpdatePropertiesInformation,
IUser,
NodePanelType,
} from '@/Interface';
import { get } from 'lodash-es';
import { isObject } from '@/utils/objectUtils';
import { getCredentialPermissions } from '@/permissions';
import { mapStores } from 'pinia';
import { useSettingsStore } from '@/stores/settings.store';
import { hasPermission } from '@/rbac/permissions';
import { useWorkflowsStore } from '@/stores/workflows.store';
import { useRootStore } from '@/stores/n8nRoot.store';
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
import { useCredentialsStore } from '@/stores/credentials.store';
import { defineComponent } from 'vue';
import { useUsersStore } from '@/stores/users.store';
export const nodeHelpers = defineComponent({
computed: {
...mapStores(
useCredentialsStore,
useHistoryStore,
useNodeTypesStore,
useSettingsStore,
useWorkflowsStore,
useRootStore,
),
},
methods: {
hasProxyAuth(node: INodeUi): boolean {
return Object.keys(node.parameters).includes('nodeCredentialType');
},
isCustomApiCallSelected(nodeValues: INodeParameters): boolean {
const { parameters } = nodeValues;
if (!isObject(parameters)) return false;
return (
(parameters.resource !== undefined && parameters.resource.includes(CUSTOM_API_CALL_KEY)) ||
(parameters.operation !== undefined && parameters.operation.includes(CUSTOM_API_CALL_KEY))
);
},
// Returns the parameter value
getParameterValue(nodeValues: INodeParameters, parameterName: string, path: string) {
return get(nodeValues, path ? path + '.' + parameterName : parameterName);
},
// Returns if the given parameter should be displayed or not
displayParameter(
nodeValues: INodeParameters,
parameter: INodeProperties | INodeCredentialDescription,
path: string,
node: INodeUi | null,
) {
return NodeHelpers.displayParameterPath(nodeValues, parameter, path, node);
},
// Updates all the issues on all the nodes
refreshNodeIssues(): void {
const nodes = this.workflowsStore.allNodes;
const workflow = this.workflowsStore.getCurrentWorkflow();
let nodeType: INodeTypeDescription | null;
let foundNodeIssues: INodeIssues | null;
nodes.forEach((node) => {
if (node.disabled === true) {
return;
}
nodeType = this.nodeTypesStore.getNodeType(node.type, node.typeVersion);
foundNodeIssues = this.getNodeIssues(nodeType, node, workflow);
if (foundNodeIssues !== null) {
node.issues = foundNodeIssues;
}
});
},
// Returns all the issues of the node
getNodeIssues(
nodeType: INodeTypeDescription | null,
node: INodeUi,
workflow: Workflow,
ignoreIssues?: string[],
): INodeIssues | null {
const pinDataNodeNames = Object.keys(this.workflowsStore.getPinData || {});
let nodeIssues: INodeIssues | null = null;
ignoreIssues = ignoreIssues || [];
if (node.disabled === true || pinDataNodeNames.includes(node.name)) {
// Ignore issues on disabled and pindata nodes
return null;
}
if (nodeType === null) {
// Node type is not known
if (!ignoreIssues.includes('typeUnknown')) {
nodeIssues = {
typeUnknown: true,
};
}
} else {
// Node type is known
// Add potential parameter issues
if (!ignoreIssues.includes('parameters')) {
nodeIssues = NodeHelpers.getNodeParametersIssues(nodeType.properties, node);
}
if (!ignoreIssues.includes('credentials')) {
// Add potential credential issues
const nodeCredentialIssues = this.getNodeCredentialIssues(node, nodeType);
if (nodeIssues === null) {
nodeIssues = nodeCredentialIssues;
} else {
NodeHelpers.mergeIssues(nodeIssues, nodeCredentialIssues);
}
}
const nodeInputIssues = this.getNodeInputIssues(workflow, node, nodeType);
if (nodeIssues === null) {
nodeIssues = nodeInputIssues;
} else {
NodeHelpers.mergeIssues(nodeIssues, nodeInputIssues);
}
}
if (this.hasNodeExecutionIssues(node) && !ignoreIssues.includes('execution')) {
if (nodeIssues === null) {
nodeIssues = {};
}
nodeIssues.execution = true;
}
return nodeIssues;
},
// Set the status on all the nodes which produced an error so that it can be
// displayed in the node-view
hasNodeExecutionIssues(node: INodeUi): boolean {
const workflowResultData = this.workflowsStore.getWorkflowRunData;
if (workflowResultData === null || !workflowResultData.hasOwnProperty(node.name)) {
return false;
}
for (const taskData of workflowResultData[node.name]) {
if (!taskData) return false;
if (taskData.error !== undefined) {
return true;
}
}
return false;
},
reportUnsetCredential(credentialType: ICredentialType) {
return {
credentials: {
[credentialType.name]: [
this.$locale.baseText('nodeHelpers.credentialsUnset', {
interpolate: {
credentialType: credentialType.displayName,
},
}),
],
},
};
},
updateNodesInputIssues() {
const nodes = this.workflowsStore.allNodes;
const workflow = this.workflowsStore.getCurrentWorkflow();
for (const node of nodes) {
const nodeType = this.nodeTypesStore.getNodeType(node.type, node.typeVersion);
if (!nodeType) {
return;
}
const nodeInputIssues = this.getNodeInputIssues(workflow, node, nodeType);
this.workflowsStore.setNodeIssue({
node: node.name,
type: 'input',
value: nodeInputIssues?.input ? nodeInputIssues.input : null,
});
}
},
// Updates the execution issues.
updateNodesExecutionIssues() {
const nodes = this.workflowsStore.allNodes;
for (const node of nodes) {
this.workflowsStore.setNodeIssue({
node: node.name,
type: 'execution',
value: this.hasNodeExecutionIssues(node) ? true : null,
});
}
},
updateNodeCredentialIssuesByName(name: string): void {
const node = this.workflowsStore.getNodeByName(name);
if (node) {
this.updateNodeCredentialIssues(node);
}
},
// Updates the credential-issues of the node
updateNodeCredentialIssues(node: INodeUi): void {
const fullNodeIssues: INodeIssues | null = this.getNodeCredentialIssues(node);
let newIssues: INodeIssueObjectProperty | null = null;
if (fullNodeIssues !== null) {
newIssues = fullNodeIssues.credentials!;
}
this.workflowsStore.setNodeIssue({
node: node.name,
type: 'credentials',
value: newIssues,
});
},
updateNodeParameterIssuesByName(name: string): void {
const node = this.workflowsStore.getNodeByName(name);
if (node) {
this.updateNodeParameterIssues(node);
}
},
// Updates the parameter-issues of the node
updateNodeParameterIssues(node: INodeUi, nodeType?: INodeTypeDescription): void {
if (nodeType === undefined) {
nodeType = this.nodeTypesStore.getNodeType(node.type, node.typeVersion);
}
if (nodeType === null) {
// Could not find nodeType so can not update issues
return;
}
// All data got updated everywhere so update now the issues
const fullNodeIssues: INodeIssues | null = NodeHelpers.getNodeParametersIssues(
nodeType!.properties,
node,
);
let newIssues: INodeIssueObjectProperty | null = null;
if (fullNodeIssues !== null) {
newIssues = fullNodeIssues.parameters!;
}
this.workflowsStore.setNodeIssue({
node: node.name,
type: 'parameters',
value: newIssues,
});
},
// Returns all the input-issues of the node
getNodeInputIssues(
workflow: Workflow,
node: INodeUi,
nodeType?: INodeTypeDescription,
): INodeIssues | null {
const foundIssues: INodeIssueObjectProperty = {};
const workflowNode = workflow.getNode(node.name);
let inputs: Array<ConnectionTypes | INodeInputConfiguration> = [];
if (nodeType && workflowNode) {
inputs = NodeHelpers.getNodeInputs(workflow, workflowNode, nodeType);
}
inputs.forEach((input) => {
if (typeof input === 'string' || input.required !== true) {
return;
}
const parentNodes = workflow.getParentNodes(node.name, input.type, 1);
if (parentNodes.length === 0) {
// We want to show different error for missing AI subnodes
if (input.type.startsWith('ai_')) {
foundIssues[input.type] = [
this.$locale.baseText('nodeIssues.input.missingSubNode', {
interpolate: {
inputName: input.displayName?.toLocaleLowerCase() ?? input.type,
inputType: input.type,
node: node.name,
},
}),
];
} else {
foundIssues[input.type] = [
this.$locale.baseText('nodeIssues.input.missing', {
interpolate: { inputName: input.displayName ?? input.type },
}),
];
}
}
});
if (Object.keys(foundIssues).length) {
return {
input: foundIssues,
};
}
return null;
},
// Returns all the credential-issues of the node
getNodeCredentialIssues(node: INodeUi, nodeType?: INodeTypeDescription): INodeIssues | null {
if (node.disabled) {
// Node is disabled
return null;
}
if (!nodeType) {
nodeType = this.nodeTypesStore.getNodeType(node.type, node.typeVersion);
}
if (!nodeType?.credentials) {
// Node does not need any credentials or nodeType could not be found
return null;
}
const foundIssues: INodeIssueObjectProperty = {};
let userCredentials: ICredentialsResponse[] | null;
let credentialType: ICredentialType | undefined;
let credentialDisplayName: string;
let selectedCredentials: INodeCredentialsDetails;
const { authentication, genericAuthType, nodeCredentialType } =
node.parameters as HttpRequestNode.V2.AuthParams;
if (
authentication === 'genericCredentialType' &&
genericAuthType !== '' &&
selectedCredsAreUnusable(node, genericAuthType)
) {
const credential = this.credentialsStore.getCredentialTypeByName(genericAuthType);
return credential ? this.reportUnsetCredential(credential) : null;
}
if (
this.hasProxyAuth(node) &&
authentication === 'predefinedCredentialType' &&
nodeCredentialType !== '' &&
node.credentials !== undefined
) {
const stored = this.credentialsStore.getCredentialsByType(nodeCredentialType);
if (selectedCredsDoNotExist(node, nodeCredentialType, stored)) {
const credential = this.credentialsStore.getCredentialTypeByName(nodeCredentialType);
return credential ? this.reportUnsetCredential(credential) : null;
}
}
if (
this.hasProxyAuth(node) &&
authentication === 'predefinedCredentialType' &&
nodeCredentialType !== '' &&
selectedCredsAreUnusable(node, nodeCredentialType)
) {
const credential = this.credentialsStore.getCredentialTypeByName(nodeCredentialType);
return credential ? this.reportUnsetCredential(credential) : null;
}
for (const credentialTypeDescription of nodeType.credentials) {
// Check if credentials should be displayed else ignore
if (!this.displayParameter(node.parameters, credentialTypeDescription, '', node)) {
continue;
}
// Get the display name of the credential type
credentialType = this.credentialsStore.getCredentialTypeByName(
credentialTypeDescription.name,
);
if (credentialType === null) {
credentialDisplayName = credentialTypeDescription.name;
} else {
credentialDisplayName = credentialType.displayName;
}
if (!node.credentials?.[credentialTypeDescription.name]) {
// Credentials are not set
if (credentialTypeDescription.required) {
foundIssues[credentialTypeDescription.name] = [
this.$locale.baseText('nodeIssues.credentials.notSet', {
interpolate: { type: nodeType.displayName },
}),
];
}
} else {
// If they are set check if the value is valid
selectedCredentials = node.credentials[credentialTypeDescription.name];
if (typeof selectedCredentials === 'string') {
selectedCredentials = {
id: null,
name: selectedCredentials,
};
}
const usersStore = useUsersStore();
const currentUser = usersStore.currentUser || ({} as IUser);
userCredentials = this.credentialsStore
.getCredentialsByType(credentialTypeDescription.name)
.filter((credential: ICredentialsResponse) => {
const permissions = getCredentialPermissions(currentUser, credential);
return permissions.read;
});
if (userCredentials === null) {
userCredentials = [];
}
if (selectedCredentials.id) {
const idMatch = userCredentials.find(
(credentialData) => credentialData.id === selectedCredentials.id,
);
if (idMatch) {
continue;
}
}
const nameMatches = userCredentials.filter(
(credentialData) => credentialData.name === selectedCredentials.name,
);
if (nameMatches.length > 1) {
foundIssues[credentialTypeDescription.name] = [
this.$locale.baseText('nodeIssues.credentials.notIdentified', {
interpolate: { name: selectedCredentials.name, type: credentialDisplayName },
}),
this.$locale.baseText('nodeIssues.credentials.notIdentified.hint'),
];
continue;
}
if (nameMatches.length === 0) {
const isCredentialUsedInWorkflow =
this.workflowsStore.usedCredentials?.[selectedCredentials.id as string];
if (
!isCredentialUsedInWorkflow &&
!hasPermission(['rbac'], { rbac: { scope: 'credential:read' } })
) {
foundIssues[credentialTypeDescription.name] = [
this.$locale.baseText('nodeIssues.credentials.doNotExist', {
interpolate: { name: selectedCredentials.name, type: credentialDisplayName },
}),
this.$locale.baseText('nodeIssues.credentials.doNotExist.hint'),
];
}
}
}
}
// TODO: Could later check also if the node has access to the credentials
if (Object.keys(foundIssues).length === 0) {
return null;
}
return {
credentials: foundIssues,
};
},
// Updates the node credential issues
updateNodesCredentialsIssues() {
const nodes = this.workflowsStore.allNodes;
let issues: INodeIssues | null;
for (const node of nodes) {
issues = this.getNodeCredentialIssues(node);
this.workflowsStore.setNodeIssue({
node: node.name,
type: 'credentials',
value: issues === null ? null : issues.credentials,
});
}
},
getNodeInputData(
node: INodeUi | null,
runIndex = 0,
outputIndex = 0,
paneType: NodePanelType = 'output',
connectionType: ConnectionTypes = NodeConnectionType.Main,
): INodeExecutionData[] {
if (node === null) {
return [];
}
if (this.workflowsStore.getWorkflowExecution === null) {
return [];
}
const executionData = this.workflowsStore.getWorkflowExecution.data;
if (!executionData?.resultData) {
// unknown status
return [];
}
const runData = executionData.resultData.runData;
const taskData = get(runData, `[${node.name}][${runIndex}]`);
if (!taskData) {
return [];
}
let data: ITaskDataConnections | undefined = taskData.data!;
if (paneType === 'input' && taskData.inputOverride) {
data = taskData.inputOverride!;
}
if (!data) {
return [];
}
return this.getInputData(data, outputIndex, connectionType);
},
// Returns the data of the main input
getInputData(
connectionsData: ITaskDataConnections,
outputIndex: number,
connectionType: ConnectionTypes = NodeConnectionType.Main,
): INodeExecutionData[] {
if (
!connectionsData ||
!connectionsData.hasOwnProperty(connectionType) ||
connectionsData[connectionType] === undefined ||
connectionsData[connectionType].length < outputIndex ||
connectionsData[connectionType][outputIndex] === null
) {
return [];
}
return connectionsData[connectionType][outputIndex] as INodeExecutionData[];
},
// Returns all the binary data of all the entries
getBinaryData(
workflowRunData: IRunData | null,
node: string | null,
runIndex: number,
outputIndex: number,
connectionType: ConnectionTypes = NodeConnectionType.Main,
): IBinaryKeyData[] {
if (node === null) {
return [];
}
const runData: IRunData | null = workflowRunData;
if (!runData?.[node]?.[runIndex]?.data) {
return [];
}
const inputData = this.getInputData(
runData[node][runIndex].data!,
outputIndex,
connectionType,
);
const returnData: IBinaryKeyData[] = [];
for (let i = 0; i < inputData.length; i++) {
if (inputData[i].hasOwnProperty('binary') && inputData[i].binary !== undefined) {
returnData.push(inputData[i].binary!);
}
}
return returnData;
},
disableNodes(nodes: INodeUi[], trackHistory = false) {
if (trackHistory) {
this.historyStore.startRecordingUndo();
}
for (const node of nodes) {
const oldState = node.disabled;
// Toggle disabled flag
const updateInformation = {
name: node.name,
properties: {
disabled: !oldState,
} as IDataObject,
} as INodeUpdatePropertiesInformation;
this.$telemetry.track('User set node enabled status', {
node_type: node.type,
is_enabled: node.disabled,
workflow_id: this.workflowsStore.workflowId,
});
this.workflowsStore.updateNodeProperties(updateInformation);
this.workflowsStore.clearNodeExecutionData(node.name);
this.updateNodeParameterIssues(node);
this.updateNodeCredentialIssues(node);
this.updateNodesInputIssues();
if (trackHistory) {
this.historyStore.pushCommandToUndo(
new EnableNodeToggleCommand(node.name, oldState === true, node.disabled === true),
);
}
}
if (trackHistory) {
this.historyStore.stopRecordingUndo();
}
},
// @ts-ignore
getNodeSubtitle(data, nodeType, workflow): string | undefined {
if (!data) {
return undefined;
}
if (data.notesInFlow) {
return data.notes;
}
if (nodeType !== null && nodeType.subtitle !== undefined) {
try {
ExpressionEvaluatorProxy.setEvaluator(
useSettingsStore().settings.expressions?.evaluator ?? 'tmpl',
);
return workflow.expression.getSimpleParameterValue(
data as INode,
nodeType.subtitle,
'internal',
{},
undefined,
PLACEHOLDER_FILLED_AT_EXECUTION_TIME,
) as string | undefined;
} catch (e) {
return undefined;
}
}
if (data.parameters.operation !== undefined) {
const operation = data.parameters.operation as string;
if (nodeType === null) {
return operation;
}
const operationData: INodeProperties = nodeType.properties.find(
(property: INodeProperties) => {
return property.name === 'operation';
},
);
if (operationData === undefined) {
return operation;
}
if (operationData.options === undefined) {
return operation;
}
const optionData = operationData.options.find((option) => {
return (option as INodePropertyOptions).value === data.parameters.operation;
});
if (optionData === undefined) {
return operation;
}
return optionData.name;
}
return undefined;
},
},
});
/**
* Whether the node has no selected credentials, or none of the node's
* selected credentials are of the specified type.
*/
function selectedCredsAreUnusable(node: INodeUi, credentialType: string) {
return !node.credentials || !Object.keys(node.credentials).includes(credentialType);
}
/**
* Whether the node's selected credentials of the specified type
* can no longer be found in the database.
*/
function selectedCredsDoNotExist(
node: INodeUi,
nodeCredentialType: string,
storedCredsByType: ICredentialsResponse[] | null,
) {
if (!node.credentials || !storedCredsByType) return false;
const selectedCredsByType = node.credentials[nodeCredentialType];
if (!selectedCredsByType) return false;
return !storedCredsByType.find((c) => c.id === selectedCredsByType.id);
}
declare namespace HttpRequestNode {
namespace V2 {
type AuthParams = {
authentication: 'none' | 'genericCredentialType' | 'predefinedCredentialType';
genericAuthType: string;
nodeCredentialType: string;
};
}
}

View file

@ -5,7 +5,7 @@ import type {
IPushDataExecutionFinished, IPushDataExecutionFinished,
} from '@/Interface'; } from '@/Interface';
import { nodeHelpers } from '@/mixins/nodeHelpers'; import { useNodeHelpers } from '@/composables/useNodeHelpers';
import { useTitleChange } from '@/composables/useTitleChange'; import { useTitleChange } from '@/composables/useTitleChange';
import { useToast } from '@/composables/useToast'; import { useToast } from '@/composables/useToast';
import { workflowHelpers } from '@/mixins/workflowHelpers'; import { workflowHelpers } from '@/mixins/workflowHelpers';
@ -45,6 +45,7 @@ export const pushConnection = defineComponent({
return { return {
...useTitleChange(), ...useTitleChange(),
...useToast(), ...useToast(),
nodeHelpers: useNodeHelpers(),
}; };
}, },
created() { created() {
@ -52,7 +53,7 @@ export const pushConnection = defineComponent({
void this.pushMessageReceived(message); void this.pushMessageReceived(message);
}); });
}, },
mixins: [nodeHelpers, workflowHelpers], mixins: [workflowHelpers],
data() { data() {
return { return {
retryTimeout: null as NodeJS.Timeout | null, retryTimeout: null as NodeJS.Timeout | null,
@ -504,7 +505,7 @@ export const pushConnection = defineComponent({
// Set the node execution issues on all the nodes which produced an error so that // Set the node execution issues on all the nodes which produced an error so that
// it can be displayed in the node-view // it can be displayed in the node-view
this.updateNodesExecutionIssues(); this.nodeHelpers.updateNodesExecutionIssues();
const lastNodeExecuted: string | undefined = const lastNodeExecuted: string | undefined =
runDataExecuted.data.resultData.lastNodeExecuted; runDataExecuted.data.resultData.lastNodeExecuted;

View file

@ -47,8 +47,8 @@ import type {
import { useMessage } from '@/composables/useMessage'; import { useMessage } from '@/composables/useMessage';
import { useToast } from '@/composables/useToast'; import { useToast } from '@/composables/useToast';
import { useNodeHelpers } from '@/composables/useNodeHelpers';
import { genericHelpers } from '@/mixins/genericHelpers'; import { genericHelpers } from '@/mixins/genericHelpers';
import { nodeHelpers } from '@/mixins/nodeHelpers';
import { get, isEqual } from 'lodash-es'; import { get, isEqual } from 'lodash-es';
@ -474,11 +474,13 @@ export function executeData(
} }
export const workflowHelpers = defineComponent({ export const workflowHelpers = defineComponent({
mixins: [nodeHelpers, genericHelpers], mixins: [genericHelpers],
setup() { setup() {
const nodeHelpers = useNodeHelpers();
return { return {
...useToast(), ...useToast(),
...useMessage(), ...useMessage(),
nodeHelpers,
}; };
}, },
computed: { computed: {
@ -612,7 +614,9 @@ export const workflowHelpers = defineComponent({
typeUnknown: true, typeUnknown: true,
}; };
} else { } else {
nodeIssues = this.getNodeIssues(nodeType.description, node, workflow, ['execution']); nodeIssues = useNodeHelpers().getNodeIssues(nodeType.description, node, workflow, [
'execution',
]);
} }
if (nodeIssues !== null) { if (nodeIssues !== null) {
@ -714,7 +718,7 @@ export const workflowHelpers = defineComponent({
const saveCredentials: INodeCredentials = {}; const saveCredentials: INodeCredentials = {};
for (const nodeCredentialTypeName of Object.keys(node.credentials)) { for (const nodeCredentialTypeName of Object.keys(node.credentials)) {
if ( if (
this.hasProxyAuth(node) || useNodeHelpers().hasProxyAuth(node) ||
Object.keys(node.parameters).includes('genericAuthType') Object.keys(node.parameters).includes('genericAuthType')
) { ) {
saveCredentials[nodeCredentialTypeName] = node.credentials[nodeCredentialTypeName]; saveCredentials[nodeCredentialTypeName] = node.credentials[nodeCredentialTypeName];
@ -723,7 +727,7 @@ export const workflowHelpers = defineComponent({
const credentialTypeDescription = nodeType.credentials const credentialTypeDescription = nodeType.credentials
// filter out credentials with same name in different node versions // filter out credentials with same name in different node versions
.filter((c) => this.displayParameter(node.parameters, c, '', node)) .filter((c) => useNodeHelpers().displayParameter(node.parameters, c, '', node))
.find((c) => c.name === nodeCredentialTypeName); .find((c) => c.name === nodeCredentialTypeName);
if (credentialTypeDescription === undefined) { if (credentialTypeDescription === undefined) {
@ -731,7 +735,14 @@ export const workflowHelpers = defineComponent({
continue; continue;
} }
if (!this.displayParameter(node.parameters, credentialTypeDescription, '', node)) { if (
!useNodeHelpers().displayParameter(
node.parameters,
credentialTypeDescription,
'',
node,
)
) {
// Credential should not be displayed so do also not save // Credential should not be displayed so do also not save
continue; continue;
} }
@ -1026,9 +1037,8 @@ export const workflowHelpers = defineComponent({
const workflowData = await this.workflowsStore.createNewWorkflow(workflowDataRequest); const workflowData = await this.workflowsStore.createNewWorkflow(workflowDataRequest);
this.workflowsStore.addWorkflow(workflowData); this.workflowsStore.addWorkflow(workflowData);
if ( if (
this.settingsStore.isEnterpriseFeatureEnabled(EnterpriseEditionFeature.Sharing) && useSettingsStore().isEnterpriseFeatureEnabled(EnterpriseEditionFeature.Sharing) &&
this.usersStore.currentUser this.usersStore.currentUser
) { ) {
this.workflowsEEStore.setWorkflowOwnedBy({ this.workflowsEEStore.setWorkflowOwnedBy({

View file

@ -11,6 +11,7 @@ import {
} from 'n8n-workflow'; } from 'n8n-workflow';
import { useToast } from '@/composables/useToast'; import { useToast } from '@/composables/useToast';
import { useNodeHelpers } from '@/composables/useNodeHelpers';
import { workflowHelpers } from '@/mixins/workflowHelpers'; import { workflowHelpers } from '@/mixins/workflowHelpers';
import { useTitleChange } from '@/composables/useTitleChange'; import { useTitleChange } from '@/composables/useTitleChange';
@ -24,9 +25,12 @@ import { useExternalHooks } from '@/composables/useExternalHooks';
export const workflowRun = defineComponent({ export const workflowRun = defineComponent({
mixins: [workflowHelpers], mixins: [workflowHelpers],
setup() { setup() {
const nodeHelpers = useNodeHelpers();
return { return {
...useTitleChange(), ...useTitleChange(),
...useToast(), ...useToast(),
nodeHelpers,
}; };
}, },
computed: { computed: {
@ -83,7 +87,7 @@ export const workflowRun = defineComponent({
try { try {
// Check first if the workflow has any issues before execute it // Check first if the workflow has any issues before execute it
this.refreshNodeIssues(); this.nodeHelpers.refreshNodeIssues();
const issuesExist = this.workflowsStore.nodesIssuesExist; const issuesExist = this.workflowsStore.nodesIssuesExist;
if (issuesExist) { if (issuesExist) {
// If issues exist get all of the issues of all nodes // If issues exist get all of the issues of all nodes
@ -265,7 +269,7 @@ export const workflowRun = defineComponent({
}, },
}; };
this.workflowsStore.setWorkflowExecutionData(executionData); this.workflowsStore.setWorkflowExecutionData(executionData);
this.updateNodesExecutionIssues(); this.nodeHelpers.updateNodesExecutionIssues();
const runWorkflowApiResponse = await this.runWorkflowApi(startRunData); const runWorkflowApiResponse = await this.runWorkflowApi(startRunData);

View file

@ -238,8 +238,9 @@ import {
import { copyPaste } from '@/mixins/copyPaste'; import { copyPaste } from '@/mixins/copyPaste';
import { genericHelpers } from '@/mixins/genericHelpers'; import { genericHelpers } from '@/mixins/genericHelpers';
import { moveNodeWorkflow } from '@/mixins/moveNodeWorkflow'; import { moveNodeWorkflow } from '@/mixins/moveNodeWorkflow';
import { nodeHelpers } from '@/mixins/nodeHelpers';
import useGlobalLinkActions from '@/composables/useGlobalLinkActions'; import useGlobalLinkActions from '@/composables/useGlobalLinkActions';
import { useNodeHelpers } from '@/composables/useNodeHelpers';
import useCanvasMouseSelect from '@/composables/useCanvasMouseSelect'; import useCanvasMouseSelect from '@/composables/useCanvasMouseSelect';
import { useExecutionDebugging } from '@/composables/useExecutionDebugging'; import { useExecutionDebugging } from '@/composables/useExecutionDebugging';
import { useTitleChange } from '@/composables/useTitleChange'; import { useTitleChange } from '@/composables/useTitleChange';
@ -386,7 +387,6 @@ export default defineComponent({
workflowHelpers, workflowHelpers,
workflowRun, workflowRun,
debounceHelper, debounceHelper,
nodeHelpers,
pinData, pinData,
], ],
components: { components: {
@ -404,11 +404,13 @@ export default defineComponent({
const locale = useI18n(); const locale = useI18n();
const contextMenu = useContextMenu(); const contextMenu = useContextMenu();
const dataSchema = useDataSchema(); const dataSchema = useDataSchema();
const nodeHelpers = useNodeHelpers();
return { return {
locale, locale,
contextMenu, contextMenu,
dataSchema, dataSchema,
nodeHelpers,
externalHooks, externalHooks,
...useCanvasMouseSelect(), ...useCanvasMouseSelect(),
...useGlobalLinkActions(), ...useGlobalLinkActions(),
@ -871,7 +873,7 @@ export default defineComponent({
}, },
clearExecutionData() { clearExecutionData() {
this.workflowsStore.workflowExecutionData = null; this.workflowsStore.workflowExecutionData = null;
this.updateNodesExecutionIssues(); this.nodeHelpers.updateNodesExecutionIssues();
}, },
async onSaveKeyboardShortcut(e: KeyboardEvent) { async onSaveKeyboardShortcut(e: KeyboardEvent) {
let saved = await this.saveCurrentWorkflow(); let saved = await this.saveCurrentWorkflow();
@ -1442,7 +1444,7 @@ export default defineComponent({
return; return;
} }
this.disableNodes(nodes, true); this.nodeHelpers.disableNodes(nodes, true);
}, },
togglePinNodes(nodes: INode[], source: PinDataSource) { togglePinNodes(nodes: INode[], source: PinDataSource) {
@ -2758,7 +2760,7 @@ export default defineComponent({
if (!this.suspendRecordingDetachedConnections) { if (!this.suspendRecordingDetachedConnections) {
this.historyStore.pushCommandToUndo(new AddConnectionCommand(connectionData)); this.historyStore.pushCommandToUndo(new AddConnectionCommand(connectionData));
} }
this.updateNodesInputIssues(); this.nodeHelpers.updateNodesInputIssues();
this.resetEndpointsErrors(); this.resetEndpointsErrors();
} }
} catch (e) { } catch (e) {
@ -2932,7 +2934,7 @@ export default defineComponent({
this.historyStore.pushCommandToUndo(removeCommand); this.historyStore.pushCommandToUndo(removeCommand);
} }
void this.updateNodesInputIssues(); void this.nodeHelpers.updateNodesInputIssues();
} catch (e) { } catch (e) {
console.error(e); console.error(e);
} }
@ -3998,7 +4000,7 @@ export default defineComponent({
} }
// Add the node issues at the end as the node-connections are required // Add the node issues at the end as the node-connections are required
void this.refreshNodeIssues(); void this.nodeHelpers.refreshNodeIssues();
// Now it can draw again // Now it can draw again
this.instance?.setSuspendDrawing(false, true); this.instance?.setSuspendDrawing(false, true);
@ -4551,7 +4553,7 @@ export default defineComponent({
onRevertEnableToggle({ nodeName, isDisabled }: { nodeName: string; isDisabled: boolean }) { onRevertEnableToggle({ nodeName, isDisabled }: { nodeName: string; isDisabled: boolean }) {
const node = this.workflowsStore.getNodeByName(nodeName); const node = this.workflowsStore.getNodeByName(nodeName);
if (node) { if (node) {
this.disableNodes([node]); this.nodeHelpers.disableNodes([node]);
} }
}, },
onPageShow(e: PageTransitionEvent) { onPageShow(e: PageTransitionEvent) {