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

View file

@ -70,6 +70,7 @@ import { completerExtension } from './completer';
import { codeNodeEditorTheme } from './theme';
import AskAI from './AskAI/AskAI.vue';
import { useMessage } from '@/composables/useMessage';
import { useSettingsStore } from '@/stores/settings.store';
export default defineComponent({
name: 'code-node-editor',
@ -156,7 +157,7 @@ export default defineComponent({
},
},
computed: {
...mapStores(useRootStore, usePostHog),
...mapStores(useRootStore, usePostHog, useSettingsStore),
aiEnabled(): boolean {
const isAiExperimentEnabled = [ASK_AI_EXPERIMENT.gpt3, ASK_AI_EXPERIMENT.gpt4].includes(
(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">
<n8n-button
v-if="parameter.options.length === 1"
v-if="(parameter.options ?? []).length === 1"
type="tertiary"
block
@click="optionSelected(parameter.options[0].name)"
@click="optionSelected((parameter.options ?? [])[0].name)"
:label="getPlaceholderText"
/>
<div v-else class="add-option">
@ -36,7 +36,11 @@
<n8n-option
v-for="item in parameterOptions"
:key="item.name"
:label="$locale.nodeText().collectionOptionDisplayName(parameter, item, path)"
:label="
isNodePropertyCollection(item)
? i18n.nodeText().collectionOptionDisplayName(parameter, item, path)
: item.name
"
:value="item.name"
>
</n8n-option>
@ -47,136 +51,136 @@
</div>
</template>
<script lang="ts">
import { defineAsyncComponent, defineComponent } from 'vue';
import { mapStores } from 'pinia';
import type { INodeUi, IUpdateInformation } from '@/Interface';
<script lang="ts" setup>
import { ref, computed, defineAsyncComponent } from 'vue';
import type { 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 { nodeHelpers } from '@/mixins/nodeHelpers';
import { get } from 'lodash-es';
import { useNDVStore } from '@/stores/ndv.store';
import { useNodeHelpers } from '@/composables/useNodeHelpers';
import { useI18n } from '@/composables/useI18n';
const ParameterInputList = defineAsyncComponent(async () => import('./ParameterInputList.vue'));
export default defineComponent({
name: 'CollectionParameter',
mixins: [nodeHelpers],
props: [
'hideDelete', // boolean
'nodeValues', // NodeParameters
'parameter', // INodeProperties
'path', // string
'values', // NodeParameters
'isReadOnly', // boolean
],
components: {
ParameterInputList,
},
data() {
return {
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);
},
const selectedOption = ref<string | undefined>(undefined);
export interface Props {
hideDelete?: boolean;
nodeValues: INodeParameters;
parameter: INodeProperties;
path: string;
values: INodeProperties;
isReadOnly?: boolean;
}
const emit = defineEmits<{
(event: 'valueChanged', value: IUpdateInformation): void;
}>();
const props = defineProps<Props>();
const ndvStore = useNDVStore();
const i18n = useI18n();
const nodeHelpers = useNodeHelpers();
const getPlaceholderText = computed(() => {
return (
i18n.nodeText().placeholder(props.parameter, props.path) ??
i18n.baseText('collectionParameter.choose')
);
},
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;
}
});
function isNodePropertyCollection(
object: INodePropertyOptions | INodeProperties | INodePropertyCollection,
): object is INodePropertyCollection {
return 'values' in object;
}
if (this.parameter.typeOptions[argumentName] === undefined) {
return undefined;
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);
}
return this.parameter.typeOptions[argumentName];
},
getOptionProperties(optionName: string): INodeProperties[] {
const properties: INodeProperties[] = [];
for (const option of this.parameter.options) {
function getOptionProperties(optionName: string) {
const properties = [];
for (const option of props.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;
}
const propertyNames = computed<string[]>(() => {
if (props.values) {
return Object.keys(props.values);
}
return this.displayParameter(this.nodeValues, parameter, this.path, this.node);
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);
},
optionSelected(optionName: string) {
const options = this.getOptionProperties(optionName);
);
});
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 = `${this.path}.${option.name}`;
const name = `${props.path}.${option.name}`;
let parameterData;
if (option.typeOptions !== undefined && option.typeOptions.multipleValues === true) {
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.
newValue = get(this.nodeValues, `${this.path}.${optionName}`, {});
const retrievedObjectValue = get(props.nodeValues, `${props.path}.${optionName}`, {});
newValue = retrievedObjectValue;
} else {
// Everything else saves them directly as an array.
newValue = get(this.nodeValues, `${this.path}.${optionName}`, []);
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,
@ -186,18 +190,16 @@ export default defineComponent({
// Add a new option
parameterData = {
name,
value: deepCopy(option.default),
value: 'default' in option ? deepCopy(option.default) : null,
};
}
this.$emit('valueChanged', parameterData);
this.selectedOption = undefined;
},
valueChanged(parameterData: IUpdateInformation) {
this.$emit('valueChanged', parameterData);
},
},
});
emit('valueChanged', parameterData);
selectedOption.value = undefined;
}
function valueChanged(parameterData: IUpdateInformation) {
emit('valueChanged', parameterData);
}
</script>
<style lang="scss">

View file

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

View file

@ -157,10 +157,8 @@ import {
WAIT_TIME_UNLIMITED,
} from '@/constants';
import { nodeBase } from '@/mixins/nodeBase';
import { nodeHelpers } from '@/mixins/nodeHelpers';
import { workflowHelpers } from '@/mixins/workflowHelpers';
import { pinData } from '@/mixins/pinData';
import type {
ConnectionTypes,
IExecutionsSummary,
@ -186,6 +184,7 @@ import { useNodeTypesStore } from '@/stores/nodeTypes.store';
import { EnableNodeToggleCommand } from '@/models/history';
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
import { type ContextMenuTarget, useContextMenu } from '@/composables/useContextMenu';
import { useNodeHelpers } from '@/composables/useNodeHelpers';
import { useExternalHooks } from '@/composables/useExternalHooks';
export default defineComponent({
@ -193,10 +192,11 @@ export default defineComponent({
setup() {
const contextMenu = useContextMenu();
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: {
TitledList,
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...
// so we only update it when necessary (when node is mounted and when it's opened and closed (isActive))
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;
} catch (e) {
@ -640,7 +642,7 @@ export default defineComponent({
},
disableNode() {
if (this.data !== null) {
this.disableNodes([this.data]);
this.nodeHelpers.disableNodes([this.data]);
this.historyStore.pushCommandToUndo(
new EnableNodeToggleCommand(
this.data.name,

View file

@ -118,7 +118,7 @@ import type {
} from 'n8n-workflow';
import { genericHelpers } from '@/mixins/genericHelpers';
import { nodeHelpers } from '@/mixins/nodeHelpers';
import { useNodeHelpers } from '@/composables/useNodeHelpers';
import { useToast } from '@/composables/useToast';
import TitledList from '@/components/TitledList.vue';
@ -144,7 +144,7 @@ interface CredentialDropdownOption extends ICredentialsResponse {
export default defineComponent({
name: 'NodeCredentials',
mixins: [genericHelpers, nodeHelpers],
mixins: [genericHelpers],
props: {
readonly: {
type: Boolean,
@ -170,8 +170,11 @@ export default defineComponent({
TitledList,
},
setup() {
const nodeHelpers = useNodeHelpers();
return {
...useToast(),
nodeHelpers,
};
},
data() {
@ -291,8 +294,6 @@ export default defineComponent({
});
},
credentialTypesNodeDescription(): INodeCredentialDescription[] {
const node = this.node;
const credType = this.credentialsStore.getCredentialTypeByName(this.overrideCredType);
if (credType) return [credType];
@ -433,7 +434,7 @@ export default defineComponent({
this.$telemetry.track('User selected credential from node modal', {
credential_type: credentialType,
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,
credential_id: credentialId,
});
@ -461,7 +462,7 @@ export default defineComponent({
invalid: oldCredentials,
type: selectedCredentialsType,
});
this.updateNodesCredentialsIssues();
this.nodeHelpers.updateNodesCredentialsIssues();
this.showMessage({
title: this.$locale.baseText('nodeCredentials.showMessage.title'),
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
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[] {

View file

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

View file

@ -119,7 +119,7 @@
</div>
<div
v-if="isCustomApiCallSelected(nodeValues)"
v-if="nodeHelpers.isCustomApiCallSelected(nodeValues)"
class="parameter-item parameter-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 { get, set, unset } from 'lodash-es';
import { nodeHelpers } from '@/mixins/nodeHelpers';
import NodeExecuteButton from './NodeExecuteButton.vue';
import { isCommunityPackageName } from '@/utils/nodeTypesUtils';
import { useUIStore } from '@/stores/ui.store';
@ -215,10 +213,10 @@ import useWorkflowsEEStore from '@/stores/workflows.ee.store';
import { useCredentialsStore } from '@/stores/credentials.store';
import type { EventBus } from 'n8n-design-system';
import { useExternalHooks } from '@/composables/useExternalHooks';
import { useNodeHelpers } from '@/composables/useNodeHelpers';
export default defineComponent({
name: 'NodeSettings',
mixins: [nodeHelpers],
components: {
NodeTitle,
NodeCredentials,
@ -228,9 +226,12 @@ export default defineComponent({
NodeExecuteButton,
},
setup() {
const nodeHelpers = useNodeHelpers();
const externalHooks = useExternalHooks();
return {
externalHooks,
nodeHelpers,
};
},
computed: {
@ -679,7 +680,7 @@ export default defineComponent({
if (node) {
// Update the issues
this.updateNodeCredentialIssues(node);
this.nodeHelpers.updateNodeCredentialIssues(node);
}
void this.externalHooks.run('nodeSettings.credentialSelected', { updateInformation });
@ -813,8 +814,8 @@ export default defineComponent({
this.workflowsStore.setNodeParameters(updateInformation);
this.updateNodeParameterIssuesByName(node.name);
this.updateNodeCredentialIssuesByName(node.name);
this.nodeHelpers.updateNodeParameterIssuesByName(node.name);
this.nodeHelpers.updateNodeCredentialIssuesByName(node.name);
}
} else if (parameterData.name.startsWith('parameters.')) {
// A node parameter changed
@ -896,8 +897,8 @@ export default defineComponent({
oldNodeParameters,
});
this.updateNodeParameterIssuesByName(node.name);
this.updateNodeCredentialIssuesByName(node.name);
this.nodeHelpers.updateNodeParameterIssuesByName(node.name);
this.nodeHelpers.updateNodeCredentialIssuesByName(node.name);
this.$telemetry.trackNodeParametersValuesChange(nodeType.name, parameterData);
} else {
// A property on the node itself changed
@ -1060,7 +1061,7 @@ export default defineComponent({
this.setNodeValues();
this.eventBus?.on('openSettings', this.openSettings);
this.updateNodeParameterIssues(this.node as INodeUi, this.nodeType);
this.nodeHelpers.updateNodeParameterIssues(this.node as INodeUi, this.nodeType);
},
beforeUnmount() {
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 HtmlEditor from '@/components/HtmlEditor/HtmlEditor.vue';
import SqlEditor from '@/components/SqlEditor/SqlEditor.vue';
import { nodeHelpers } from '@/mixins/nodeHelpers';
import { workflowHelpers } from '@/mixins/workflowHelpers';
import { hasExpressionMapping, isValueExpression } from '@/utils/nodeTypesUtils';
import { isResourceLocatorValue } from '@/utils/typeGuards';
@ -417,6 +417,7 @@ import { useSettingsStore } from '@/stores/settings.store';
import { htmlEditorEventBus } from '@/event-bus';
import type { EventBus } from 'n8n-design-system/utils';
import { createEventBus } from 'n8n-design-system/utils';
import { useNodeHelpers } from '@/composables/useNodeHelpers';
import { useI18n } from '@/composables/useI18n';
import type { N8nInput } from 'n8n-design-system';
import { isCredentialOnlyNodeType } from '@/utils/credentialOnlyNodes';
@ -426,7 +427,7 @@ type Picker = { $emit: (arg0: string, arg1: Date) => void };
export default defineComponent({
name: 'parameter-input',
mixins: [nodeHelpers, workflowHelpers, debounceHelper],
mixins: [workflowHelpers, debounceHelper],
components: {
CodeNodeEditor,
HtmlEditor,
@ -505,10 +506,12 @@ export default defineComponent({
setup() {
const externalHooks = useExternalHooks();
const i18n = useI18n();
const nodeHelpers = useNodeHelpers();
return {
externalHooks,
i18n,
nodeHelpers,
};
},
data() {
@ -881,7 +884,7 @@ export default defineComponent({
if (node) {
// Update the issues
this.updateNodeCredentialIssues(node);
this.nodeHelpers.updateNodeCredentialIssues(node);
}
void this.externalHooks.run('nodeSettings.credentialSelected', { updateInformation });

View file

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

View file

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

View file

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

View file

@ -43,14 +43,14 @@ import type { INodeUi } from '@/Interface';
import type { IDataObject } from 'n8n-workflow';
import { copyPaste } from '@/mixins/copyPaste';
import { pinData } from '@/mixins/pinData';
import { nodeHelpers } from '@/mixins/nodeHelpers';
import { genericHelpers } from '@/mixins/genericHelpers';
import { clearJsonKey, convertPath } from '@/utils/typesUtils';
import { executionDataToJson } from '@/utils/nodeTypesUtils';
import { useWorkflowsStore } from '@/stores/workflows.store';
import { useNDVStore } from '@/stores/ndv.store';
import { useI18n } from '@/composables/useI18n';
import { useNodeHelpers } from '@/composables/useNodeHelpers';
import { useToast } from '@/composables/useToast';
import { useI18n } from '@/composables/useI18n';
import { nonExistingJsonPath } from '@/constants';
type JsonPathData = {
@ -60,7 +60,7 @@ type JsonPathData = {
export default defineComponent({
name: 'run-data-json-actions',
mixins: [genericHelpers, nodeHelpers, pinData, copyPaste],
mixins: [genericHelpers, pinData, copyPaste],
props: {
node: {
@ -95,9 +95,10 @@ export default defineComponent({
},
setup() {
const i18n = useI18n();
const nodeHelpers = useNodeHelpers();
return {
i18n,
nodeHelpers,
...useToast(),
};
},
@ -121,7 +122,7 @@ export default defineComponent({
selectedValue = clearJsonKey(this.pinData as object);
} else {
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 { nodeBase } from '@/mixins/nodeBase';
import { nodeHelpers } from '@/mixins/nodeHelpers';
import { workflowHelpers } from '@/mixins/workflowHelpers';
import { isNumber, isString } from '@/utils/typeGuards';
import type {
@ -125,7 +124,7 @@ import { useContextMenu } from '@/composables/useContextMenu';
export default defineComponent({
name: 'Sticky',
mixins: [nodeBase, nodeHelpers, workflowHelpers],
mixins: [nodeBase, workflowHelpers],
setup() {
const colorPopoverTrigger = ref<HTMLDivElement>();
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,
} from '@/Interface';
import { nodeHelpers } from '@/mixins/nodeHelpers';
import { useNodeHelpers } from '@/composables/useNodeHelpers';
import { useTitleChange } from '@/composables/useTitleChange';
import { useToast } from '@/composables/useToast';
import { workflowHelpers } from '@/mixins/workflowHelpers';
@ -45,6 +45,7 @@ export const pushConnection = defineComponent({
return {
...useTitleChange(),
...useToast(),
nodeHelpers: useNodeHelpers(),
};
},
created() {
@ -52,7 +53,7 @@ export const pushConnection = defineComponent({
void this.pushMessageReceived(message);
});
},
mixins: [nodeHelpers, workflowHelpers],
mixins: [workflowHelpers],
data() {
return {
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
// it can be displayed in the node-view
this.updateNodesExecutionIssues();
this.nodeHelpers.updateNodesExecutionIssues();
const lastNodeExecuted: string | undefined =
runDataExecuted.data.resultData.lastNodeExecuted;

View file

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

View file

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

View file

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