mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 01:56:46 +00:00
feat(editor): Add HTTP request nodes for credentials without a node (#7157)
Github issue / Community forum post (link here to close automatically): --------- Co-authored-by: Giulio Andreini <g.andreini@gmail.com> Co-authored-by: कारतोफ्फेलस्क्रिप्ट™ <aditya@netroy.in>
This commit is contained in:
@@ -809,6 +809,7 @@ export type SimplifiedNodeType = Pick<
|
||||
| 'group'
|
||||
| 'icon'
|
||||
| 'iconUrl'
|
||||
| 'badgeIconUrl'
|
||||
| 'codex'
|
||||
| 'defaults'
|
||||
| 'outputs'
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div :class="$style.container">
|
||||
<el-row>
|
||||
<el-row v-if="nodesWithAccess.length > 0">
|
||||
<el-col :span="8" :class="$style.accessLabel">
|
||||
<n8n-text :compact="true" :bold="true">
|
||||
{{ $locale.baseText('credentialEdit.credentialInfo.allowUseBy') }}
|
||||
|
||||
@@ -781,6 +781,7 @@ export default defineComponent({
|
||||
border: 2px solid var(--color-foreground-xdark);
|
||||
border-radius: var(--border-radius-large);
|
||||
background-color: var(--color-canvas-node-background);
|
||||
--color-background-node-icon-badge: var(--color-canvas-node-background);
|
||||
&.executing {
|
||||
background-color: $node-background-executing !important;
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
@dragstart="onDragStart"
|
||||
@dragend="onDragEnd"
|
||||
:class="$style.nodeItem"
|
||||
:description="subcategory !== DEFAULT_SUBCATEGORY ? description : ''"
|
||||
:description="description"
|
||||
:title="displayName"
|
||||
:show-action-arrow="showActionArrow"
|
||||
:is-trigger="isTrigger"
|
||||
@@ -44,6 +44,7 @@ import { computed, ref } from 'vue';
|
||||
import type { SimplifiedNodeType } from '@/Interface';
|
||||
import {
|
||||
COMMUNITY_NODES_INSTALLATION_DOCS_URL,
|
||||
CREDENTIAL_ONLY_NODE_PREFIX,
|
||||
DEFAULT_SUBCATEGORY,
|
||||
DRAG_EVENT_DATA_KEY,
|
||||
} from '@/constants';
|
||||
@@ -78,6 +79,13 @@ const draggablePosition = ref({ x: -100, y: -100 });
|
||||
const draggableDataTransfer = ref(null as Element | null);
|
||||
|
||||
const description = computed<string>(() => {
|
||||
if (
|
||||
props.subcategory === DEFAULT_SUBCATEGORY &&
|
||||
!props.nodeType.name.startsWith(CREDENTIAL_ONLY_NODE_PREFIX)
|
||||
) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return i18n.headerText({
|
||||
key: `headers.${shortNodeType.value}.description`,
|
||||
fallback: props.nodeType.description,
|
||||
|
||||
@@ -30,7 +30,7 @@ import { useViewStacks } from './composables/useViewStacks';
|
||||
import { useKeyboardNavigation } from './composables/useKeyboardNavigation';
|
||||
import { useActionsGenerator } from './composables/useActionsGeneration';
|
||||
import NodesListPanel from './Panel/NodesListPanel.vue';
|
||||
import { useUIStore } from '@/stores';
|
||||
import { useCredentialsStore, useUIStore } from '@/stores';
|
||||
import { DRAG_EVENT_DATA_KEY } from '@/constants';
|
||||
|
||||
export interface Props {
|
||||
@@ -135,9 +135,12 @@ registerKeyHook('NodeCreatorCloseTab', {
|
||||
});
|
||||
|
||||
watch(
|
||||
() => useNodeTypesStore().visibleNodeTypes,
|
||||
(nodeTypes) => {
|
||||
const { actions, mergedNodes } = generateMergedNodesAndActions(nodeTypes);
|
||||
() => ({
|
||||
httpOnlyCredentials: useCredentialsStore().httpOnlyCredentialTypes,
|
||||
nodeTypes: useNodeTypesStore().visibleNodeTypes,
|
||||
}),
|
||||
({ nodeTypes, httpOnlyCredentials }) => {
|
||||
const { actions, mergedNodes } = generateMergedNodesAndActions(nodeTypes, httpOnlyCredentials);
|
||||
|
||||
setActions(actions);
|
||||
setMergeNodes(mergedNodes);
|
||||
|
||||
@@ -235,6 +235,7 @@ function onBackButton() {
|
||||
background: var(--color-background-xlight);
|
||||
height: 100%;
|
||||
background-color: $node-creator-background-color;
|
||||
--color-background-node-icon-badge: var(--color-background-xlight);
|
||||
width: 385px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
import type { ActionTypeDescription, ActionsRecord, SimplifiedNodeType } from '@/Interface';
|
||||
import { CUSTOM_API_CALL_KEY, HTTP_REQUEST_NODE_TYPE } from '@/constants';
|
||||
import { memoize, startCase } from 'lodash-es';
|
||||
import type {
|
||||
ICredentialType,
|
||||
INodeProperties,
|
||||
INodePropertyCollection,
|
||||
INodePropertyOptions,
|
||||
INodeProperties,
|
||||
INodeTypeDescription,
|
||||
} from 'n8n-workflow';
|
||||
import { CUSTOM_API_CALL_KEY } from '@/constants';
|
||||
import type { ActionTypeDescription, SimplifiedNodeType, ActionsRecord } from '@/Interface';
|
||||
|
||||
import { i18n } from '@/plugins/i18n';
|
||||
|
||||
import { getCredentialOnlyNodeType } from '@/utils/credentialOnlyNodes';
|
||||
|
||||
const PLACEHOLDER_RECOMMENDED_ACTION_KEY = 'placeholder_recommended';
|
||||
|
||||
function translate(...args: Parameters<typeof i18n.baseText>) {
|
||||
@@ -241,7 +244,18 @@ export function useActionsGenerator() {
|
||||
}
|
||||
|
||||
function getSimplifiedNodeType(node: INodeTypeDescription): SimplifiedNodeType {
|
||||
const { displayName, defaults, description, name, group, icon, iconUrl, outputs, codex } = node;
|
||||
const {
|
||||
displayName,
|
||||
defaults,
|
||||
description,
|
||||
name,
|
||||
group,
|
||||
icon,
|
||||
iconUrl,
|
||||
badgeIconUrl,
|
||||
outputs,
|
||||
codex,
|
||||
} = node;
|
||||
|
||||
return {
|
||||
displayName,
|
||||
@@ -251,12 +265,16 @@ export function useActionsGenerator() {
|
||||
group,
|
||||
icon,
|
||||
iconUrl,
|
||||
badgeIconUrl,
|
||||
outputs,
|
||||
codex,
|
||||
};
|
||||
}
|
||||
|
||||
function generateMergedNodesAndActions(nodeTypes: INodeTypeDescription[]) {
|
||||
function generateMergedNodesAndActions(
|
||||
nodeTypes: INodeTypeDescription[],
|
||||
httpOnlyCredentials: ICredentialType[],
|
||||
) {
|
||||
const visibleNodeTypes = [...nodeTypes];
|
||||
const actions: ActionsRecord<typeof mergedNodes> = {};
|
||||
const mergedNodes: SimplifiedNodeType[] = [];
|
||||
@@ -267,6 +285,13 @@ export function useActionsGenerator() {
|
||||
const appActions = generateNodeActions(app);
|
||||
actions[app.name] = appActions;
|
||||
|
||||
if (app.name === HTTP_REQUEST_NODE_TYPE) {
|
||||
const credentialOnlyNodes = httpOnlyCredentials.map((credentialType) =>
|
||||
getSimplifiedNodeType(getCredentialOnlyNodeType(app, credentialType)),
|
||||
);
|
||||
mergedNodes.push(...credentialOnlyNodes);
|
||||
}
|
||||
|
||||
mergedNodes.push(getSimplifiedNodeType(app));
|
||||
});
|
||||
|
||||
|
||||
@@ -128,7 +128,7 @@ import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
||||
import { useCredentialsStore } from '@/stores/credentials.store';
|
||||
import { useNDVStore } from '@/stores/ndv.store';
|
||||
import { KEEP_AUTH_IN_NDV_FOR_NODES } from '@/constants';
|
||||
import { CREDENTIAL_ONLY_NODE_PREFIX, KEEP_AUTH_IN_NDV_FOR_NODES } from '@/constants';
|
||||
import {
|
||||
getAuthTypeForNodeCredential,
|
||||
getMainAuthField,
|
||||
@@ -248,7 +248,7 @@ export default defineComponent({
|
||||
// When active node parameters change, check if authentication type has been changed
|
||||
// and set `subscribedToCredentialType` to corresponding credential type
|
||||
const isActive = this.node.name === this.ndvStore.activeNode?.name;
|
||||
const nodeType = this.nodeTypesStore.getNodeType(this.node.type, this.node.typeVersion);
|
||||
const nodeType = this.nodeType;
|
||||
// Only do this for active node and if it's listening for auth change
|
||||
if (isActive && nodeType && this.listeningForAuthChange) {
|
||||
if (this.mainNodeAuthField && oldValue && newValue) {
|
||||
@@ -297,7 +297,7 @@ export default defineComponent({
|
||||
|
||||
if (credType) return [credType];
|
||||
|
||||
const activeNodeType = this.nodeTypesStore.getNodeType(node.type, node.typeVersion);
|
||||
const activeNodeType = this.nodeType;
|
||||
if (activeNodeType?.credentials) {
|
||||
return activeNodeType.credentials;
|
||||
}
|
||||
@@ -548,19 +548,24 @@ export default defineComponent({
|
||||
this.subscribedToCredentialType = credentialType;
|
||||
},
|
||||
showMixedCredentials(credentialType: INodeCredentialDescription): boolean {
|
||||
const nodeType = this.nodeTypesStore.getNodeType(this.node.type, this.node.typeVersion);
|
||||
const nodeType = this.nodeType;
|
||||
const isRequired = isRequiredCredential(nodeType, credentialType);
|
||||
|
||||
return !KEEP_AUTH_IN_NDV_FOR_NODES.includes(this.node.type || '') && isRequired;
|
||||
},
|
||||
getCredentialsFieldLabel(credentialType: INodeCredentialDescription): string {
|
||||
const credentialTypeName = this.credentialTypeNames[credentialType.name];
|
||||
const isCredentialOnlyNode = this.node.type.startsWith(CREDENTIAL_ONLY_NODE_PREFIX);
|
||||
|
||||
if (isCredentialOnlyNode) {
|
||||
return this.$locale.baseText('nodeCredentials.credentialFor', {
|
||||
interpolate: { credentialType: this.nodeType?.displayName ?? credentialTypeName },
|
||||
});
|
||||
}
|
||||
|
||||
if (!this.showMixedCredentials(credentialType)) {
|
||||
return this.$locale.baseText('nodeCredentials.credentialFor', {
|
||||
interpolate: {
|
||||
credentialType: credentialTypeName,
|
||||
},
|
||||
interpolate: { credentialType: credentialTypeName },
|
||||
});
|
||||
}
|
||||
return this.$locale.baseText('nodeCredentials.credentialsLabel');
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
:circle="circle"
|
||||
:nodeTypeName="nodeType ? nodeType.displayName : ''"
|
||||
:showTooltip="showTooltip"
|
||||
:badge="badge"
|
||||
@click="(e) => $emit('click')"
|
||||
></n8n-node-icon>
|
||||
</template>
|
||||
@@ -18,7 +19,7 @@ import type { IVersionNode } from '@/Interface';
|
||||
import { useRootStore } from '@/stores/n8nRoot.store';
|
||||
import type { INodeTypeDescription } from 'n8n-workflow';
|
||||
import { mapStores } from 'pinia';
|
||||
import { defineComponent } from 'vue';
|
||||
import { defineComponent, type PropType } from 'vue';
|
||||
|
||||
interface NodeIconSource {
|
||||
path?: string;
|
||||
@@ -29,7 +30,9 @@ interface NodeIconSource {
|
||||
export default defineComponent({
|
||||
name: 'NodeIcon',
|
||||
props: {
|
||||
nodeType: {},
|
||||
nodeType: {
|
||||
type: Object as PropType<INodeTypeDescription | IVersionNode | null>,
|
||||
},
|
||||
size: {
|
||||
type: Number,
|
||||
required: false,
|
||||
@@ -54,7 +57,7 @@ export default defineComponent({
|
||||
computed: {
|
||||
...mapStores(useRootStore),
|
||||
type(): string {
|
||||
const nodeType = this.nodeType as INodeTypeDescription | IVersionNode | null;
|
||||
const nodeType = this.nodeType;
|
||||
let iconType = 'unknown';
|
||||
if (nodeType) {
|
||||
if (nodeType.iconUrl) return 'file';
|
||||
@@ -67,8 +70,8 @@ export default defineComponent({
|
||||
return iconType;
|
||||
},
|
||||
color(): string {
|
||||
const nodeType = this.nodeType as INodeTypeDescription | IVersionNode | null;
|
||||
if (nodeType && nodeType.defaults && nodeType.defaults.color) {
|
||||
const nodeType = this.nodeType;
|
||||
if (nodeType?.defaults?.color) {
|
||||
return nodeType.defaults.color.toString();
|
||||
}
|
||||
if (this.colorDefault) {
|
||||
@@ -77,7 +80,7 @@ export default defineComponent({
|
||||
return '';
|
||||
},
|
||||
iconSource(): NodeIconSource {
|
||||
const nodeType = this.nodeType as INodeTypeDescription | IVersionNode | null;
|
||||
const nodeType = this.nodeType;
|
||||
const baseUrl = this.rootStore.getBaseUrl;
|
||||
const iconSource = {} as NodeIconSource;
|
||||
|
||||
@@ -104,6 +107,14 @@ export default defineComponent({
|
||||
}
|
||||
return iconSource;
|
||||
},
|
||||
badge(): { src: string; type: string } | undefined {
|
||||
const nodeType = this.nodeType as INodeTypeDescription;
|
||||
if (nodeType && 'badgeIconUrl' in nodeType && nodeType.badgeIconUrl) {
|
||||
return { type: 'file', src: this.rootStore.getBaseUrl + nodeType.badgeIconUrl };
|
||||
}
|
||||
|
||||
return undefined;
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -40,8 +40,8 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import NodeIcon from '@/components/NodeIcon.vue';
|
||||
import { defineComponent } from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'NodeTitle',
|
||||
|
||||
@@ -419,6 +419,7 @@ import type { EventBus } from 'n8n-design-system/utils';
|
||||
import { createEventBus } from 'n8n-design-system/utils';
|
||||
import { useI18n } from '@/composables';
|
||||
import type { N8nInput } from 'n8n-design-system';
|
||||
import { isCredentialOnlyNodeType } from '@/utils/credentialOnlyNodes';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'parameter-input',
|
||||
@@ -961,7 +962,7 @@ export default defineComponent({
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.node.type.startsWith('n8n-nodes-base')) {
|
||||
if (this.node.type.startsWith('n8n-nodes-base') || isCredentialOnlyNodeType(this.node.type)) {
|
||||
this.$telemetry.track('User opened Expression Editor', {
|
||||
node_type: this.node.type,
|
||||
parameter_name: this.parameter.displayName,
|
||||
|
||||
@@ -23,11 +23,7 @@
|
||||
</div>
|
||||
|
||||
<import-parameter
|
||||
v-else-if="
|
||||
parameter.type === 'curlImport' &&
|
||||
nodeTypeName === 'n8n-nodes-base.httpRequest' &&
|
||||
nodeTypeVersion >= 3
|
||||
"
|
||||
v-else-if="parameter.type === 'curlImport'"
|
||||
:isReadOnly="isReadOnly"
|
||||
@valueChanged="valueChanged"
|
||||
/>
|
||||
@@ -102,7 +98,10 @@
|
||||
labelSize="small"
|
||||
@valueChanged="valueChanged"
|
||||
/>
|
||||
<div v-else-if="displayNodeParameter(parameter)" class="parameter-item">
|
||||
<div
|
||||
v-else-if="displayNodeParameter(parameter) && credentialsParameterIndex !== index"
|
||||
class="parameter-item"
|
||||
>
|
||||
<div
|
||||
class="delete-option clickable"
|
||||
:title="$locale.baseText('parameterInputList.delete')"
|
||||
@@ -137,9 +136,6 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineAsyncComponent, defineComponent } from 'vue';
|
||||
import type { PropType } from 'vue';
|
||||
import { mapStores } from 'pinia';
|
||||
import type {
|
||||
INodeParameters,
|
||||
INodeProperties,
|
||||
@@ -147,19 +143,22 @@ import type {
|
||||
NodeParameterValue,
|
||||
} from 'n8n-workflow';
|
||||
import { deepCopy } from 'n8n-workflow';
|
||||
import { mapStores } from 'pinia';
|
||||
import type { PropType } from 'vue';
|
||||
import { defineAsyncComponent, defineComponent } from 'vue';
|
||||
|
||||
import type { INodeUi, IUpdateInformation } from '@/Interface';
|
||||
|
||||
import MultipleParameter from '@/components/MultipleParameter.vue';
|
||||
import { workflowHelpers } from '@/mixins/workflowHelpers';
|
||||
import ParameterInputFull from '@/components/ParameterInputFull.vue';
|
||||
import ImportParameter from '@/components/ImportParameter.vue';
|
||||
import MultipleParameter from '@/components/MultipleParameter.vue';
|
||||
import ParameterInputFull from '@/components/ParameterInputFull.vue';
|
||||
import ResourceMapper from '@/components/ResourceMapper/ResourceMapper.vue';
|
||||
import { get, set } from 'lodash-es';
|
||||
import { KEEP_AUTH_IN_NDV_FOR_NODES } from '@/constants';
|
||||
import { workflowHelpers } from '@/mixins/workflowHelpers';
|
||||
import { useNDVStore } from '@/stores/ndv.store';
|
||||
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
||||
import { isAuthRelatedParameter, getNodeAuthFields, getMainAuthField } from '@/utils';
|
||||
import { KEEP_AUTH_IN_NDV_FOR_NODES } from '@/constants';
|
||||
import { getMainAuthField, getNodeAuthFields, isAuthRelatedParameter } from '@/utils';
|
||||
import { get, set } from 'lodash-es';
|
||||
import { nodeViewEventBus } from '@/event-bus';
|
||||
|
||||
const FixedCollectionParameter = defineAsyncComponent(
|
||||
@@ -242,7 +241,16 @@ export default defineComponent({
|
||||
nodeAuthFields(): INodeProperties[] {
|
||||
return getNodeAuthFields(this.nodeType);
|
||||
},
|
||||
credentialsParameterIndex(): number {
|
||||
return this.filteredParameters.findIndex((parameter) => parameter.type === 'credentials');
|
||||
},
|
||||
indexToShowSlotAt(): number {
|
||||
const credentialsParameterIndex = this.credentialsParameterIndex;
|
||||
|
||||
if (credentialsParameterIndex !== -1) {
|
||||
return credentialsParameterIndex;
|
||||
}
|
||||
|
||||
let index = 0;
|
||||
// For nodes that use old credentials UI, keep credentials below authentication field in NDV
|
||||
// otherwise credentials will use auth filed position since the auth field is moved to credentials modal
|
||||
@@ -255,7 +263,7 @@ export default defineComponent({
|
||||
}
|
||||
});
|
||||
|
||||
return index < this.filteredParameters.length ? index : this.filteredParameters.length - 1;
|
||||
return Math.min(index, this.filteredParameters.length - 1);
|
||||
},
|
||||
mainNodeAuthField(): INodeProperties | null {
|
||||
return getMainAuthField(this.nodeType || null);
|
||||
|
||||
@@ -157,6 +157,9 @@ export const ZENDESK_NODE_TYPE = 'n8n-nodes-base.zendesk';
|
||||
export const ZENDESK_TRIGGER_NODE_TYPE = 'n8n-nodes-base.zendeskTrigger';
|
||||
export const DISCORD_NODE_TYPE = 'n8n-nodes-base.discord';
|
||||
|
||||
export const CREDENTIAL_ONLY_NODE_PREFIX = 'n8n-creds-base';
|
||||
export const CREDENTIAL_ONLY_HTTP_NODE_VERSION = 4.1;
|
||||
|
||||
export const EXECUTABLE_TRIGGER_NODE_TYPES = [
|
||||
START_NODE_TYPE,
|
||||
MANUAL_TRIGGER_NODE_TYPE,
|
||||
|
||||
@@ -66,6 +66,7 @@ import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||
import { getSourceItems } from '@/utils';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { useSettingsStore } from '@/stores/settings.store';
|
||||
import { getCredentialTypeName, isCredentialOnlyNodeType } from '@/utils/credentialOnlyNodes';
|
||||
|
||||
export function getParentMainInputNode(workflow: Workflow, node: INode): INode {
|
||||
const nodeType = useNodeTypesStore().getNodeType(node.type);
|
||||
@@ -683,11 +684,18 @@ export const workflowHelpers = defineComponent({
|
||||
const nodeType = this.nodeTypesStore.getNodeType(node.type, node.typeVersion);
|
||||
|
||||
if (nodeType !== null) {
|
||||
const isCredentialOnly = isCredentialOnlyNodeType(nodeType.name);
|
||||
|
||||
if (isCredentialOnly) {
|
||||
nodeData.type = HTTP_REQUEST_NODE_TYPE;
|
||||
nodeData.extendsCredential = getCredentialTypeName(nodeType.name);
|
||||
}
|
||||
|
||||
// Node-Type is known so we can save the parameters correctly
|
||||
const nodeParameters = NodeHelpers.getNodeParameters(
|
||||
nodeType.properties,
|
||||
node.parameters,
|
||||
false,
|
||||
isCredentialOnly,
|
||||
false,
|
||||
node,
|
||||
);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { defineComponent } from 'vue';
|
||||
import { mapStores } from 'pinia';
|
||||
import type { IExecutionPushResponse, IExecutionResponse, IStartRunData } from '@/Interface';
|
||||
import { mapStores } from 'pinia';
|
||||
import { defineComponent } from 'vue';
|
||||
|
||||
import type { IRunData, IRunExecutionData, ITaskData, IWorkflowBase } from 'n8n-workflow';
|
||||
import {
|
||||
@@ -10,15 +10,15 @@ import {
|
||||
FORM_TRIGGER_PATH_IDENTIFIER,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import { useToast } from '@/composables';
|
||||
import { externalHooks } from '@/mixins/externalHooks';
|
||||
import { workflowHelpers } from '@/mixins/workflowHelpers';
|
||||
import { useToast } from '@/composables';
|
||||
|
||||
import { useTitleChange } from '@/composables/useTitleChange';
|
||||
import { useRootStore } from '@/stores/n8nRoot.store';
|
||||
import { useUIStore } from '@/stores/ui.store';
|
||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||
import { useRootStore } from '@/stores/n8nRoot.store';
|
||||
import { FORM_TRIGGER_NODE_TYPE } from '../constants';
|
||||
import { FORM_TRIGGER_NODE_TYPE } from '@/constants';
|
||||
import { openPopUpWindow } from '@/utils/executionUtils';
|
||||
|
||||
export const workflowRun = defineComponent({
|
||||
|
||||
@@ -814,6 +814,7 @@
|
||||
"ndv.pinData.error.tooLarge.description": "Workflow has reached the maximum allowed pinned data size",
|
||||
"ndv.pinData.error.tooLargeWorkflow.title": "Pinned data too big",
|
||||
"ndv.pinData.error.tooLargeWorkflow.description": "Workflow has reached the maximum allowed size",
|
||||
"ndv.httpRequest.credentialOnly.docsNotice": "Use the <a target=\"_blank\" href=\"{docsUrl}\">{nodeName} docs</a> to construct your request. We'll take care of the authentication part if you add a {nodeName} credential below.",
|
||||
"noTagsView.readyToOrganizeYourWorkflows": "Ready to organize your workflows?",
|
||||
"noTagsView.withWorkflowTagsYouReFree": "With workflow tags, you're free to create the perfect tagging system for your flows",
|
||||
"node.thisIsATriggerNode": "This is a Trigger node. <a target=\"_blank\" href=\"https://docs.n8n.io/workflows/components/nodes/\">Learn more</a>",
|
||||
|
||||
@@ -191,6 +191,9 @@ export const useCredentialsStore = defineStore(STORES.CREDENTIALS, {
|
||||
return this.getCredentialOwnerName(credential);
|
||||
};
|
||||
},
|
||||
httpOnlyCredentialTypes(): ICredentialType[] {
|
||||
return this.allCredentialTypes.filter((credentialType) => credentialType.httpRequestNode);
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
setCredentialTypes(credentialTypes: ICredentialType[]): void {
|
||||
|
||||
@@ -6,7 +6,12 @@ import {
|
||||
getResourceLocatorResults,
|
||||
getResourceMapperFields,
|
||||
} from '@/api/nodeTypes';
|
||||
import { DEFAULT_NODETYPE_VERSION, STORES } from '@/constants';
|
||||
import {
|
||||
DEFAULT_NODETYPE_VERSION,
|
||||
HTTP_REQUEST_NODE_TYPE,
|
||||
STORES,
|
||||
CREDENTIAL_ONLY_HTTP_NODE_VERSION,
|
||||
} from '@/constants';
|
||||
import type {
|
||||
INodeTypesState,
|
||||
IResourceLocatorReqParams,
|
||||
@@ -15,6 +20,7 @@ import type {
|
||||
import { addHeaders, addNodeTranslation } from '@/plugins/i18n';
|
||||
import { omit } from '@/utils';
|
||||
import type {
|
||||
ConnectionTypes,
|
||||
ILoadOptions,
|
||||
INode,
|
||||
INodeCredentials,
|
||||
@@ -26,12 +32,16 @@ import type {
|
||||
INodeTypeNameVersion,
|
||||
ResourceMapperFields,
|
||||
Workflow,
|
||||
ConnectionTypes,
|
||||
} from 'n8n-workflow';
|
||||
import { NodeConnectionType, NodeHelpers } from 'n8n-workflow';
|
||||
import { defineStore } from 'pinia';
|
||||
import { useCredentialsStore } from './credentials.store';
|
||||
import { useRootStore } from './n8nRoot.store';
|
||||
import { NodeHelpers, NodeConnectionType } from 'n8n-workflow';
|
||||
import {
|
||||
getCredentialOnlyNodeType,
|
||||
getCredentialTypeName,
|
||||
isCredentialOnlyNodeType,
|
||||
} from '@/utils/credentialOnlyNodes';
|
||||
|
||||
function getNodeVersions(nodeType: INodeTypeDescription) {
|
||||
return Array.isArray(nodeType.version) ? nodeType.version : [nodeType.version];
|
||||
@@ -68,14 +78,28 @@ export const useNodeTypesStore = defineStore(STORES.NODE_TYPES, {
|
||||
},
|
||||
getNodeType() {
|
||||
return (nodeTypeName: string, version?: number): INodeTypeDescription | null => {
|
||||
if (isCredentialOnlyNodeType(nodeTypeName)) {
|
||||
return this.getCredentialOnlyNodeType(nodeTypeName, version);
|
||||
}
|
||||
|
||||
const nodeVersions = this.nodeTypes[nodeTypeName];
|
||||
|
||||
if (!nodeVersions) return null;
|
||||
|
||||
const versionNumbers = Object.keys(nodeVersions).map(Number);
|
||||
const nodeType = nodeVersions[version || Math.max(...versionNumbers)];
|
||||
|
||||
return nodeType || null;
|
||||
const nodeType = nodeVersions[version ?? Math.max(...versionNumbers)];
|
||||
return nodeType ?? null;
|
||||
};
|
||||
},
|
||||
getCredentialOnlyNodeType() {
|
||||
return (nodeTypeName: string, version?: number): INodeTypeDescription | null => {
|
||||
const credentialName = getCredentialTypeName(nodeTypeName);
|
||||
const httpNode = this.getNodeType(
|
||||
HTTP_REQUEST_NODE_TYPE,
|
||||
version ?? CREDENTIAL_ONLY_HTTP_NODE_VERSION,
|
||||
);
|
||||
const credential = useCredentialsStore().getCredentialTypeByName(credentialName);
|
||||
return getCredentialOnlyNodeType(httpNode, credential) ?? null;
|
||||
};
|
||||
},
|
||||
isConfigNode() {
|
||||
|
||||
@@ -86,6 +86,7 @@ import { useNDVStore } from '@/stores/ndv.store';
|
||||
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
||||
import { useUsersStore } from '@/stores/users.store';
|
||||
import { useSettingsStore } from '@/stores/settings.store';
|
||||
import { getCredentialOnlyNodeTypeName } from '@/utils/credentialOnlyNodes';
|
||||
|
||||
const defaults: Omit<IWorkflowDb, 'id'> & { settings: NonNullable<IWorkflowDb['settings']> } = {
|
||||
name: '',
|
||||
@@ -954,6 +955,10 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, {
|
||||
return;
|
||||
}
|
||||
|
||||
if (nodeData.extendsCredential) {
|
||||
nodeData.type = getCredentialOnlyNodeTypeName(nodeData.extendsCredential);
|
||||
}
|
||||
|
||||
this.workflow.nodes.push(nodeData);
|
||||
// Init node metadata
|
||||
if (!this.nodeMetadata[nodeData.name]) {
|
||||
|
||||
90
packages/editor-ui/src/utils/credentialOnlyNodes.ts
Normal file
90
packages/editor-ui/src/utils/credentialOnlyNodes.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import { deepCopy, type ICredentialType, type INodeTypeDescription } from 'n8n-workflow';
|
||||
import { CREDENTIAL_ONLY_NODE_PREFIX } from '../constants';
|
||||
import { i18n } from '@/plugins/i18n';
|
||||
|
||||
export function isCredentialOnlyNodeType(nodeTypeName: string): boolean {
|
||||
return nodeTypeName?.startsWith(CREDENTIAL_ONLY_NODE_PREFIX) ?? false;
|
||||
}
|
||||
|
||||
export function getCredentialTypeName(nodeTypeName: string): string {
|
||||
return nodeTypeName.split('.')[1];
|
||||
}
|
||||
|
||||
export function getCredentialOnlyNodeTypeName(credentialTypeName: string): string {
|
||||
return `${CREDENTIAL_ONLY_NODE_PREFIX}.${credentialTypeName}`;
|
||||
}
|
||||
|
||||
export function getCredentialOnlyNodeType(
|
||||
httpNode?: INodeTypeDescription | null,
|
||||
credentialType?: ICredentialType,
|
||||
): INodeTypeDescription | undefined {
|
||||
const { httpRequestNode } = credentialType ?? {};
|
||||
if (!httpNode || !credentialType || !httpRequestNode) return undefined;
|
||||
|
||||
const { docsUrl, name: displayName } = httpRequestNode;
|
||||
|
||||
const credentialOnlyNode = deepCopy(httpNode);
|
||||
|
||||
const httpIcon = httpNode.iconUrl;
|
||||
|
||||
credentialOnlyNode.name = getCredentialOnlyNodeTypeName(credentialType.name);
|
||||
credentialOnlyNode.extendsCredential = credentialType.name;
|
||||
credentialOnlyNode.displayName = displayName ?? credentialType.displayName;
|
||||
credentialOnlyNode.description = 'HTTP request';
|
||||
credentialOnlyNode.defaults.name = `${displayName} HTTP Request`;
|
||||
credentialOnlyNode.codex = {
|
||||
...credentialOnlyNode.codex,
|
||||
alias: [],
|
||||
categories: [],
|
||||
subcategories: {},
|
||||
};
|
||||
|
||||
credentialOnlyNode.credentials = [{ name: credentialType.name, required: true }];
|
||||
|
||||
if (credentialType.icon ?? credentialType.iconUrl) {
|
||||
credentialOnlyNode.icon = credentialType.icon;
|
||||
credentialOnlyNode.iconUrl = credentialType.iconUrl;
|
||||
credentialOnlyNode.badgeIconUrl = httpIcon;
|
||||
} else {
|
||||
credentialOnlyNode.iconUrl = httpIcon;
|
||||
}
|
||||
|
||||
credentialOnlyNode.properties = httpNode.properties.map((prop) => {
|
||||
switch (prop.name) {
|
||||
case 'authentication':
|
||||
return { ...prop, type: 'hidden', default: 'predefinedCredentialType' };
|
||||
case 'nodeCredentialType':
|
||||
return { ...prop, type: 'hidden', default: credentialType.name };
|
||||
case 'url':
|
||||
const properties = { ...prop };
|
||||
if ('apiBaseUrl' in httpRequestNode) {
|
||||
const { apiBaseUrl } = httpRequestNode;
|
||||
properties.default = apiBaseUrl;
|
||||
properties.placeholder = apiBaseUrl ? `e.g. ${apiBaseUrl}` : prop.placeholder;
|
||||
} else {
|
||||
properties.placeholder = httpRequestNode.apiBaseUrlPlaceholder;
|
||||
}
|
||||
return properties;
|
||||
default:
|
||||
return prop;
|
||||
}
|
||||
});
|
||||
|
||||
credentialOnlyNode.properties.splice(1, 0, {
|
||||
type: 'notice',
|
||||
displayName: i18n.baseText('ndv.httpRequest.credentialOnly.docsNotice', {
|
||||
interpolate: { nodeName: displayName, docsUrl },
|
||||
}),
|
||||
name: 'httpVariantWarning',
|
||||
default: '',
|
||||
});
|
||||
|
||||
credentialOnlyNode.properties.splice(4, 0, {
|
||||
type: 'credentials',
|
||||
displayName: '',
|
||||
name: '',
|
||||
default: '',
|
||||
});
|
||||
|
||||
return credentialOnlyNode;
|
||||
}
|
||||
Reference in New Issue
Block a user