mirror of
https://github.com/n8n-io/n8n.git
synced 2024-12-25 04:34:06 -08:00
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:
parent
e8a493f718
commit
35fbc37c8e
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,157 +51,155 @@
|
|||
</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);
|
||||
},
|
||||
);
|
||||
},
|
||||
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;
|
||||
}
|
||||
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();
|
||||
|
||||
if (this.parameter.typeOptions[argumentName] === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
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);
|
||||
},
|
||||
},
|
||||
const getPlaceholderText = computed(() => {
|
||||
return (
|
||||
i18n.nodeText().placeholder(props.parameter, props.path) ??
|
||||
i18n.baseText('collectionParameter.choose')
|
||||
);
|
||||
});
|
||||
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>
|
||||
|
||||
<style lang="scss">
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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[] {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 });
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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];
|
||||
|
|
|
@ -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),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
723
packages/editor-ui/src/composables/useNodeHelpers.ts
Normal file
723
packages/editor-ui/src/composables/useNodeHelpers.ts
Normal 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,
|
||||
};
|
||||
}
|
|
@ -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;
|
||||
};
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
Loading…
Reference in a new issue