feat(editor): Action/credentials tab in node settings in zoomed view (no-changelog) (#17730)

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Suguru Inoue
2025-08-01 13:36:34 +02:00
committed by GitHub
parent b99b93a637
commit 4deb0b2ddb
17 changed files with 508 additions and 171 deletions

View File

@@ -41,6 +41,7 @@ import IconLucideAtSign from '~icons/lucide/at-sign';
import IconLucideBan from '~icons/lucide/ban'; import IconLucideBan from '~icons/lucide/ban';
import IconLucideBell from '~icons/lucide/bell'; import IconLucideBell from '~icons/lucide/bell';
import IconLucideBook from '~icons/lucide/book'; import IconLucideBook from '~icons/lucide/book';
import IconLucideBookOpen from '~icons/lucide/book-open';
import IconLucideBot from '~icons/lucide/bot'; import IconLucideBot from '~icons/lucide/bot';
import IconLucideBox from '~icons/lucide/box'; import IconLucideBox from '~icons/lucide/box';
import IconLucideBraces from '~icons/lucide/braces'; import IconLucideBraces from '~icons/lucide/braces';
@@ -163,6 +164,7 @@ import IconLucideScissors from '~icons/lucide/scissors';
import IconLucideSearch from '~icons/lucide/search'; import IconLucideSearch from '~icons/lucide/search';
import IconLucideSend from '~icons/lucide/send'; import IconLucideSend from '~icons/lucide/send';
import IconLucideServer from '~icons/lucide/server'; import IconLucideServer from '~icons/lucide/server';
import IconLucideSettings from '~icons/lucide/settings';
import IconLucideShare from '~icons/lucide/share'; import IconLucideShare from '~icons/lucide/share';
import IconLucideSlidersHorizontal from '~icons/lucide/sliders-horizontal'; import IconLucideSlidersHorizontal from '~icons/lucide/sliders-horizontal';
import IconLucideSmile from '~icons/lucide/smile'; import IconLucideSmile from '~icons/lucide/smile';
@@ -451,6 +453,7 @@ export const updatedIconSet = {
ban: IconLucideBan, ban: IconLucideBan,
bell: IconLucideBell, bell: IconLucideBell,
book: IconLucideBook, book: IconLucideBook,
'book-open': IconLucideBookOpen,
bot: IconLucideBot, bot: IconLucideBot,
box: IconLucideBox, box: IconLucideBox,
brain: IconLucideBrain, brain: IconLucideBrain,
@@ -570,6 +573,7 @@ export const updatedIconSet = {
scale: IconLucideScale, scale: IconLucideScale,
scissors: IconLucideScissors, scissors: IconLucideScissors,
search: IconLucideSearch, search: IconLucideSearch,
settings: IconLucideSettings,
send: IconLucideSend, send: IconLucideSend,
server: IconLucideServer, server: IconLucideServer,
share: IconLucideShare, share: IconLucideShare,

View File

@@ -35,6 +35,7 @@ const Template: StoryFn = (args, { argTypes }) => ({
export const Example = Template.bind({}); export const Example = Template.bind({});
Example.args = { Example.args = {
modelValue: 'first',
options: [ options: [
{ {
label: 'First', label: 'First',
@@ -104,23 +105,27 @@ const options: Array<TabOptions<string>> = [
export const TabVariants = Template.bind({}); export const TabVariants = Template.bind({});
TabVariants.args = { TabVariants.args = {
modelValue: 'first',
options, options,
}; };
export const WithSmallSize = Template.bind({}); export const WithSmallSize = Template.bind({});
WithSmallSize.args = { WithSmallSize.args = {
modelValue: 'first',
options, options,
size: 'small', size: 'small',
}; };
export const WithModernVariant = Template.bind({}); export const WithModernVariant = Template.bind({});
WithModernVariant.args = { WithModernVariant.args = {
modelValue: 'first',
variant: 'modern', variant: 'modern',
options, options,
}; };
export const WithSmallAndModern = Template.bind({}); export const WithSmallAndModern = Template.bind({});
WithSmallAndModern.args = { WithSmallAndModern.args = {
modelValue: 'first',
variant: 'modern', variant: 'modern',
options, options,
size: 'small', size: 'small',

View File

@@ -188,6 +188,10 @@ const scrollRight = () => scroll(50);
/* Hide scrollbar for IE, Edge and Firefox */ /* Hide scrollbar for IE, Edge and Firefox */
-ms-overflow-style: none; /* IE and Edge */ -ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */ scrollbar-width: none; /* Firefox */
.small.modern & {
gap: var(--spacing-xs);
}
} }
.tab { .tab {
@@ -219,6 +223,10 @@ const scrollRight = () => scroll(50);
.small & { .small & {
font-size: var(--font-size-2xs); font-size: var(--font-size-2xs);
} }
.small.modern & {
padding-inline: 0;
}
} }
.activeTab { .activeTab {

View File

@@ -1470,7 +1470,10 @@
"nodeSettings.notesInFlow.description": "If active, the note above will display in the flow as a subtitle", "nodeSettings.notesInFlow.description": "If active, the note above will display in the flow as a subtitle",
"nodeSettings.notesInFlow.displayName": "Display Note in Flow?", "nodeSettings.notesInFlow.displayName": "Display Note in Flow?",
"nodeSettings.parameters": "Parameters", "nodeSettings.parameters": "Parameters",
"nodeSettings.parametersShort": "Params",
"nodeSettings.settings": "Settings", "nodeSettings.settings": "Settings",
"nodeSettings.action": "Action",
"nodeSettings.credential": "Auth",
"nodeSettings.communityNodeTooltip": "This is a <a href=\"{docUrl}\" target=\"_blank\"/>community node</a>", "nodeSettings.communityNodeTooltip": "This is a <a href=\"{docUrl}\" target=\"_blank\"/>community node</a>",
"nodeSettings.retryOnFail.description": "If active, the node tries to execute again when it fails", "nodeSettings.retryOnFail.description": "If active, the node tries to execute again when it fails",
"nodeSettings.retryOnFail.displayName": "Retry On Fail", "nodeSettings.retryOnFail.displayName": "Retry On Fail",

View File

@@ -0,0 +1,143 @@
<script setup lang="ts">
import { useActions } from '@/components/Node/NodeCreator/composables/useActions';
import { useActionsGenerator } from '@/components/Node/NodeCreator/composables/useActionsGeneration';
import { CUSTOM_API_CALL_KEY } from '@/constants';
import type { ActionCreateElement, INodeCreateElement, INodeUi } from '@/Interface';
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
import { N8nIcon, N8nText } from '@n8n/design-system';
import { useI18n } from '@n8n/i18n';
import { type INodeParameters } from 'n8n-workflow';
import { computed, ref, watch } from 'vue';
const { node } = defineProps<{
node: INodeUi;
}>();
const emit = defineEmits<{
actionSelected: [INodeParameters];
}>();
const nodeTypesStore = useNodeTypesStore();
const { generateMergedNodesAndActions } = useActionsGenerator();
const { parseCategoryActions, getActionData } = useActions();
const i18n = useI18n();
const selectedActionRef = ref<HTMLElement>();
const nodeType = computed(() => nodeTypesStore.getNodeType(node.type, node.typeVersion));
const options = computed(() => {
const { actions } = generateMergedNodesAndActions(nodeType.value ? [nodeType.value] : [], []);
return parseCategoryActions(
Object.values(actions).flatMap((typeDescriptions) =>
typeDescriptions
.filter(({ actionKey }) => actionKey !== CUSTOM_API_CALL_KEY)
.map<ActionCreateElement>((typeDescription) => ({
type: 'action',
subcategory: typeDescription.actionKey,
key: typeDescription.actionKey,
properties: typeDescription,
})),
),
i18n.baseText('nodeCreator.actionsCategory.actions'),
true,
).map((action) => {
if (action.type !== 'action') {
return { action, isSelected: false };
}
const data = getActionData(action.properties).value;
let isSelected = true;
for (const [key, value] of Object.entries(data)) {
isSelected = isSelected && node.parameters[key] === value;
}
return { action, isSelected };
});
});
function handleClickOption(option: INodeCreateElement) {
if (option.type !== 'action') {
return;
}
emit('actionSelected', getActionData(option.properties).value);
}
function handleSelectedItemRef(el: unknown) {
if (el instanceof HTMLDivElement) {
selectedActionRef.value = el;
}
}
watch(
selectedActionRef,
(selected) => {
selected?.scrollIntoView();
},
{ flush: 'post' },
);
</script>
<template>
<div :class="$style.component">
<template v-for="option in options" :key="option.action.key">
<N8nText
v-if="option.action.type === 'label'"
tag="div"
:class="$style.label"
size="xsmall"
color="text-base"
bold
>
{{ option.action.key }}
</N8nText>
<div
v-else-if="option.action.type === 'action'"
:ref="option.isSelected ? handleSelectedItemRef : undefined"
:class="{
[$style.option]: true,
[$style.selected]: option.isSelected,
}"
role="button"
@click="handleClickOption(option.action)"
>
<NodeIcon :size="20" :node-type="nodeType" />
<N8nText size="small" bold :class="$style.optionText">{{
option.action.properties.displayName
}}</N8nText>
<N8nIcon v-if="option.isSelected" icon="check" color="primary" />
</div>
</template>
</div>
</template>
<style lang="scss" module>
.component {
padding-block: var(--spacing-2xs);
}
.label {
padding: var(--spacing-3xs) var(--spacing-s);
text-transform: uppercase;
}
.option {
display: flex;
align-items: center;
padding: var(--spacing-3xs) var(--spacing-s);
gap: var(--spacing-2xs);
cursor: pointer;
&.selected,
&:hover {
background-color: var(--color-background-base);
}
}
.optionText {
flex-grow: 1;
flex-shrink: 1;
}
</style>

View File

@@ -1,7 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import type { ICredentialsResponse, INodeUi, INodeUpdatePropertiesInformation } from '@/Interface'; import type { ICredentialsResponse, INodeUi, INodeUpdatePropertiesInformation } from '@/Interface';
import { import {
HTTP_REQUEST_NODE_TYPE,
type ICredentialType, type ICredentialType,
type INodeCredentialDescription, type INodeCredentialDescription,
type INodeCredentialsDetails, type INodeCredentialsDetails,
@@ -15,7 +14,7 @@ import { useToast } from '@/composables/useToast';
import TitledList from '@/components/TitledList.vue'; import TitledList from '@/components/TitledList.vue';
import { useI18n } from '@n8n/i18n'; import { useI18n } from '@n8n/i18n';
import { useTelemetry } from '@/composables/useTelemetry'; import { useTelemetry } from '@/composables/useTelemetry';
import { CREDENTIAL_ONLY_NODE_PREFIX, KEEP_AUTH_IN_NDV_FOR_NODES } from '@/constants'; import { CREDENTIAL_ONLY_NODE_PREFIX } from '@/constants';
import { ndvEventBus } from '@/event-bus'; import { ndvEventBus } from '@/event-bus';
import { useCredentialsStore } from '@/stores/credentials.store'; import { useCredentialsStore } from '@/stores/credentials.store';
import { useNDVStore } from '@/stores/ndv.store'; import { useNDVStore } from '@/stores/ndv.store';
@@ -24,11 +23,8 @@ import { useUIStore } from '@/stores/ui.store';
import { useWorkflowsStore } from '@/stores/workflows.store'; import { useWorkflowsStore } from '@/stores/workflows.store';
import { assert } from '@n8n/utils/assert'; import { assert } from '@n8n/utils/assert';
import { import {
getAllNodeCredentialForAuthType,
getAuthTypeForNodeCredential, getAuthTypeForNodeCredential,
getMainAuthField,
getNodeCredentialForSelectedAuthType, getNodeCredentialForSelectedAuthType,
isRequiredCredential,
updateNodeAuthType, updateNodeAuthType,
} from '@/utils/nodeTypesUtils'; } from '@/utils/nodeTypesUtils';
import { import {
@@ -41,10 +37,7 @@ import {
N8nTooltip, N8nTooltip,
} from '@n8n/design-system'; } from '@n8n/design-system';
import { isEmpty } from '@/utils/typesUtils'; import { isEmpty } from '@/utils/typesUtils';
import { useNodeCredentialOptions } from '@/composables/useNodeCredentialOptions';
interface CredentialDropdownOption extends ICredentialsResponse {
typeDisplayName: string;
}
type Props = { type Props = {
node: INodeUi; node: INodeUi;
@@ -85,40 +78,39 @@ const filter = ref('');
const listeningForAuthChange = ref(false); const listeningForAuthChange = ref(false);
const selectRefs = ref<Array<InstanceType<typeof N8nSelect>>>([]); const selectRefs = ref<Array<InstanceType<typeof N8nSelect>>>([]);
const credentialTypesNodeDescriptions = computed(() => const node = computed(() => props.node);
credentialsStore.getCredentialTypesNodeDescriptions(props.overrideCredType, nodeType.value),
const nodeType = computed(() =>
nodeTypesStore.getNodeType(props.node.type, props.node.typeVersion),
); );
const credentialTypesNode = computed(() => const {
credentialTypesNodeDescriptions.value.map( mainNodeAuthField,
(credentialTypeDescription) => credentialTypeDescription.name, credentialTypesNodeDescriptionDisplayed,
), credentialTypesNodeDescriptions,
); isCredentialExisting,
showMixedCredentials,
const credentialTypesNodeDescriptionDisplayed = computed(() => } = useNodeCredentialOptions(
credentialTypesNodeDescriptions.value node,
.filter((credentialTypeDescription) => displayCredentials(credentialTypeDescription)) nodeType,
.map((type) => ({ type, options: getCredentialOptions(getAllRelatedCredentialTypes(type)) })), computed(() => props.overrideCredType),
); );
const credentialTypeNames = computed(() => { const credentialTypeNames = computed(() => {
const returnData: Record<string, string> = {}; const returnData: Record<string, string> = {};
for (const credentialTypeName of credentialTypesNode.value) {
const credentialType = credentialsStore.getCredentialTypeByName(credentialTypeName); for (const { name } of credentialTypesNodeDescriptions.value) {
returnData[credentialTypeName] = credentialType const credentialType = credentialsStore.getCredentialTypeByName(name);
? credentialType.displayName returnData[name] = credentialType ? credentialType.displayName : name;
: credentialTypeName;
} }
return returnData; return returnData;
}); });
const selected = computed<Record<string, INodeCredentialsDetails>>( const selected = computed<Record<string, INodeCredentialsDetails>>(
() => props.node.credentials ?? {}, () => props.node.credentials ?? {},
); );
const nodeType = computed(() =>
nodeTypesStore.getNodeType(props.node.type, props.node.typeVersion),
);
const mainNodeAuthField = computed(() => getMainAuthField(nodeType.value));
watch( watch(
() => props.node.parameters, () => props.node.parameters,
(newValue, oldValue) => { (newValue, oldValue) => {
@@ -228,44 +220,9 @@ onBeforeUnmount(() => {
ndvEventBus.off('credential.createNew', onCreateAndAssignNewCredential); ndvEventBus.off('credential.createNew', onCreateAndAssignNewCredential);
}); });
function getAllRelatedCredentialTypes(credentialType: INodeCredentialDescription): string[] { function getSelectedId(type: INodeCredentialDescription) {
const credentialIsRequired = showMixedCredentials(credentialType);
if (credentialIsRequired) {
if (mainNodeAuthField.value) {
const credentials = getAllNodeCredentialForAuthType(
nodeType.value,
mainNodeAuthField.value.name,
);
return credentials.map((cred) => cred.name);
}
}
return [credentialType.name];
}
function getCredentialOptions(types: string[]): CredentialDropdownOption[] {
let options: CredentialDropdownOption[] = [];
types.forEach((type) => {
options = options.concat(
credentialsStore.allUsableCredentialsByType[type].map(
(option: ICredentialsResponse) =>
({
...option,
typeDisplayName: credentialsStore.getCredentialTypeByName(type)?.displayName,
}) as CredentialDropdownOption,
),
);
});
if (ndvStore.activeNode?.type === HTTP_REQUEST_NODE_TYPE) {
options = options.filter((option) => !option.isManaged);
}
return options;
}
function getSelectedId(type: string) {
if (isCredentialExisting(type)) { if (isCredentialExisting(type)) {
return selected.value[type].id; return selected.value[type.name].id;
} }
return undefined; return undefined;
} }
@@ -417,19 +374,6 @@ function onCredentialSelected(
emit('credentialSelected', updateInformation); emit('credentialSelected', updateInformation);
} }
function displayCredentials(credentialTypeDescription: INodeCredentialDescription): boolean {
if (credentialTypeDescription.displayOptions === undefined) {
// If it is not defined no need to do a proper check
return true;
}
return nodeHelpers.displayParameter(
props.node.parameters,
credentialTypeDescription,
'',
props.node,
);
}
function getIssues(credentialTypeName: string): string[] { function getIssues(credentialTypeName: string): string[] {
const node = props.node; const node = props.node;
@@ -443,16 +387,6 @@ function getIssues(credentialTypeName: string): string[] {
return node.issues.credentials[credentialTypeName]; return node.issues.credentials[credentialTypeName];
} }
function isCredentialExisting(credentialType: string): boolean {
if (!props.node.credentials?.[credentialType]?.id) {
return false;
}
const { id } = props.node.credentials[credentialType];
const options = getCredentialOptions([credentialType]);
return !!options.find((option: ICredentialsResponse) => option.id === id);
}
function editCredential(credentialType: string): void { function editCredential(credentialType: string): void {
const credential = props.node.credentials?.[credentialType]; const credential = props.node.credentials?.[credentialType];
assert(credential?.id); assert(credential?.id);
@@ -468,12 +402,6 @@ function editCredential(credentialType: string): void {
subscribedToCredentialType.value = credentialType; subscribedToCredentialType.value = credentialType;
} }
function showMixedCredentials(credentialType: INodeCredentialDescription): boolean {
const isRequired = isRequiredCredential(nodeType.value, credentialType);
return !KEEP_AUTH_IN_NDV_FOR_NODES.includes(props.node.type ?? '') && isRequired;
}
function getCredentialsFieldLabel(credentialType: INodeCredentialDescription): string { function getCredentialsFieldLabel(credentialType: INodeCredentialDescription): string {
if (credentialType.displayName) return credentialType.displayName; if (credentialType.displayName) return credentialType.displayName;
const credentialTypeName = credentialTypeNames.value[credentialType.name]; const credentialTypeName = credentialTypeNames.value[credentialType.name];
@@ -536,7 +464,7 @@ async function onClickCreateCredential(type: ICredentialType | INodeCredentialDe
> >
<N8nSelect <N8nSelect
ref="selectRefs" ref="selectRefs"
:model-value="getSelectedId(type.name)" :model-value="getSelectedId(type)"
:placeholder="getSelectPlaceholder(type.name, getIssues(type.name))" :placeholder="getSelectPlaceholder(type.name, getIssues(type.name))"
size="small" size="small"
filterable filterable
@@ -585,7 +513,7 @@ async function onClickCreateCredential(type: ICredentialType | INodeCredentialDe
</div> </div>
<div <div
v-if="selected[type.name] && isCredentialExisting(type.name)" v-if="selected[type.name] && isCredentialExisting(type)"
:class="$style.edit" :class="$style.edit"
data-test-id="credential-edit-button" data-test-id="credential-edit-button"
> >

View File

@@ -205,14 +205,6 @@ const showTriggerPanel = computed(() => {
); );
}); });
const hasOutputConnection = computed(() => {
if (!activeNode.value) return false;
const outgoingConnections = workflowsStore.outgoingConnectionsByNodeName(activeNode.value.name);
// Check if there's at-least one output connection
return (Object.values(outgoingConnections)?.[0]?.[0] ?? []).length > 0;
});
const isExecutableTriggerNode = computed(() => { const isExecutableTriggerNode = computed(() => {
if (!activeNodeType.value) return false; if (!activeNodeType.value) return false;
@@ -712,7 +704,6 @@ onBeforeUnmount(() => {
:append-to="`#${APP_MODALS_ELEMENT_ID}`" :append-to="`#${APP_MODALS_ELEMENT_ID}`"
data-test-id="ndv" data-test-id="ndv"
:z-index="APP_Z_INDEXES.NDV" :z-index="APP_Z_INDEXES.NDV"
:data-has-output-connection="hasOutputConnection"
> >
<n8n-tooltip <n8n-tooltip
placement="bottom-start" placement="bottom-start"
@@ -848,10 +839,6 @@ onBeforeUnmount(() => {
</template> </template>
<style lang="scss"> <style lang="scss">
// Hide notice(.ndv-connection-hint-notice) warning when node has output connection
[data-has-output-connection='true'] .ndv-connection-hint-notice {
display: none;
}
.ndv-wrapper { .ndv-wrapper {
overflow: visible; overflow: visible;
margin-top: 0; margin-top: 0;

View File

@@ -939,10 +939,3 @@ onBeforeUnmount(() => {
height: var(--draggable-height); height: var(--draggable-height);
} }
</style> </style>
<style lang="scss">
// Hide notice(.ndv-connection-hint-notice) warning when node has output connection
[data-has-output-connection='true'] .ndv-connection-hint-notice {
display: none;
}
</style>

View File

@@ -19,7 +19,7 @@ import { BASE_NODE_SURVEY_URL, NDV_UI_OVERHAUL_EXPERIMENT } from '@/constants';
import ParameterInputList from '@/components/ParameterInputList.vue'; import ParameterInputList from '@/components/ParameterInputList.vue';
import NodeCredentials from '@/components/NodeCredentials.vue'; import NodeCredentials from '@/components/NodeCredentials.vue';
import NodeSettingsTabs, { type Tab } from '@/components/NodeSettingsTabs.vue'; import NodeSettingsTabs from '@/components/NodeSettingsTabs.vue';
import NodeWebhooks from '@/components/NodeWebhooks.vue'; import NodeWebhooks from '@/components/NodeWebhooks.vue';
import NDVSubConnections from '@/components/NDVSubConnections.vue'; import NDVSubConnections from '@/components/NDVSubConnections.vue';
import NodeSettingsHeader from '@/components/NodeSettingsHeader.vue'; import NodeSettingsHeader from '@/components/NodeSettingsHeader.vue';
@@ -31,6 +31,7 @@ import {
createCommonNodeSettings, createCommonNodeSettings,
nameIsParameter, nameIsParameter,
getNodeSettingsInitialValues, getNodeSettingsInitialValues,
collectParametersByTab,
} from '@/utils/nodeSettingsUtils'; } from '@/utils/nodeSettingsUtils';
import { isCommunityPackageName } from '@/utils/nodeTypesUtils'; import { isCommunityPackageName } from '@/utils/nodeTypesUtils';
import { useWorkflowsStore } from '@/stores/workflows.store'; import { useWorkflowsStore } from '@/stores/workflows.store';
@@ -50,12 +51,14 @@ import { importCurlEventBus, ndvEventBus } from '@/event-bus';
import { ProjectTypes } from '@/types/projects.types'; import { ProjectTypes } from '@/types/projects.types';
import FreeAiCreditsCallout from '@/components/FreeAiCreditsCallout.vue'; import FreeAiCreditsCallout from '@/components/FreeAiCreditsCallout.vue';
import { usePostHog } from '@/stores/posthog.store'; import { usePostHog } from '@/stores/posthog.store';
import { shouldShowParameter } from './canvas/experimental/experimentalNdv.utils';
import { useResizeObserver } from '@vueuse/core'; import { useResizeObserver } from '@vueuse/core';
import { useNodeSettingsParameters } from '@/composables/useNodeSettingsParameters'; import { useNodeSettingsParameters } from '@/composables/useNodeSettingsParameters';
import { N8nBlockUi, N8nIcon, N8nNotice, N8nText } from '@n8n/design-system'; import { N8nBlockUi, N8nIcon, N8nNotice, N8nText } from '@n8n/design-system';
import ExperimentalEmbeddedNdvHeader from '@/components/canvas/experimental/components/ExperimentalEmbeddedNdvHeader.vue'; import ExperimentalEmbeddedNdvHeader from '@/components/canvas/experimental/components/ExperimentalEmbeddedNdvHeader.vue';
import NodeSettingsInvalidNodeWarning from '@/components/NodeSettingsInvalidNodeWarning.vue'; import NodeSettingsInvalidNodeWarning from '@/components/NodeSettingsInvalidNodeWarning.vue';
import type { NodeSettingsTab } from '@/types/nodeSettings';
import NodeActionsList from '@/components/NodeActionsList.vue';
import { useNodeCredentialOptions } from '@/composables/useNodeCredentialOptions';
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
@@ -122,7 +125,7 @@ if (props.isEmbeddedInCanvas) {
} }
const nodeValid = ref(true); const nodeValid = ref(true);
const openPanel = ref<Tab>('params'); const openPanel = ref<NodeSettingsTab>('params');
// Used to prevent nodeValues from being overwritten by defaults on reopening ndv // Used to prevent nodeValues from being overwritten by defaults on reopening ndv
const nodeValuesInitialized = ref(false); const nodeValuesInitialized = ref(false);
@@ -149,6 +152,8 @@ const nodeType = computed(() =>
node.value ? nodeTypesStore.getNodeType(node.value.type, node.value.typeVersion) : null, node.value ? nodeTypesStore.getNodeType(node.value.type, node.value.typeVersion) : null,
); );
const { areAllCredentialsSet } = useNodeCredentialOptions(node, nodeType, '');
const isTriggerNode = computed(() => !!node.value && nodeTypesStore.isTriggerNode(node.value.type)); const isTriggerNode = computed(() => !!node.value && nodeTypesStore.isTriggerNode(node.value.type));
const isToolNode = computed(() => !!node.value && nodeTypesStore.isToolNode(node.value.type)); const isToolNode = computed(() => !!node.value && nodeTypesStore.isToolNode(node.value.type));
@@ -204,14 +209,9 @@ const parameters = computed(() => {
return nodeType.value?.properties ?? []; return nodeType.value?.properties ?? [];
}); });
const parametersSetting = computed(() => parameters.value.filter((item) => item.isNodeSetting)); const parametersByTab = computed(() =>
collectParametersByTab(parameters.value, props.isEmbeddedInCanvas),
const parametersNoneSetting = computed(() => { );
// The connection hint notice is visually hidden via CSS in NodeDetails.vue when the node has output connections
const paramsToShow = parameters.value.filter((item) => !item.isNodeSetting);
return props.isEmbeddedInCanvas ? parameters.value.filter(shouldShowParameter) : paramsToShow;
});
const isDisplayingCredentials = computed( const isDisplayingCredentials = computed(
() => () =>
@@ -224,7 +224,7 @@ const isDisplayingCredentials = computed(
const showNoParametersNotice = computed( const showNoParametersNotice = computed(
() => () =>
!isDisplayingCredentials.value && !isDisplayingCredentials.value &&
parametersNoneSetting.value.filter((item) => item.type !== 'notice').length === 0, (parametersByTab.value.params ?? []).filter((item) => item.type !== 'notice').length === 0,
); );
const outputPanelEditMode = computed(() => ndvStore.outputPanelEditMode); const outputPanelEditMode = computed(() => ndvStore.outputPanelEditMode);
@@ -264,6 +264,14 @@ const featureRequestUrl = computed(() => {
return `${BASE_NODE_SURVEY_URL}${nodeType.value.name}`; return `${BASE_NODE_SURVEY_URL}${nodeType.value.name}`;
}); });
const hasOutputConnection = computed(() => {
if (!node.value) return false;
const outgoingConnections = workflowsStore.outgoingConnectionsByNodeName(node.value.name);
// Check if there's at-least one output connection
return (Object.values(outgoingConnections)?.[0]?.[0] ?? []).length > 0;
});
const valueChanged = (parameterData: IUpdateInformation) => { const valueChanged = (parameterData: IUpdateInformation) => {
let newValue: NodeParameterValue; let newValue: NodeParameterValue;
@@ -427,7 +435,7 @@ const onOpenConnectionNodeCreator = (
const populateHiddenIssuesSet = () => { const populateHiddenIssuesSet = () => {
if (!node.value || !workflowsStore.isNodePristine(node.value.name)) return; if (!node.value || !workflowsStore.isNodePristine(node.value.name)) return;
hiddenIssuesInputs.value.push('credentials'); hiddenIssuesInputs.value.push('credentials');
parametersNoneSetting.value.forEach((parameter) => { parametersByTab.value.params.forEach((parameter) => {
hiddenIssuesInputs.value.push(parameter.name); hiddenIssuesInputs.value.push(parameter.name);
}); });
workflowsStore.setNodePristine(node.value.name, false); workflowsStore.setNodePristine(node.value.name, false);
@@ -501,7 +509,7 @@ const openSettings = () => {
openPanel.value = 'settings'; openPanel.value = 'settings';
}; };
const onTabSelect = (tab: Tab) => { const onTabSelect = (tab: NodeSettingsTab) => {
openPanel.value = tab; openPanel.value = tab;
}; };
@@ -554,6 +562,21 @@ function displayCredentials(credentialTypeDescription: INodeCredentialDescriptio
nodeHelpers.displayParameter(node.value.parameters, credentialTypeDescription, '', node.value) nodeHelpers.displayParameter(node.value.parameters, credentialTypeDescription, '', node.value)
); );
} }
function handleSelectAction(params: INodeParameters) {
for (const [key, value] of Object.entries(params)) {
valueChanged({ name: `parameters.${key}`, value });
}
if (isDisplayingCredentials.value && !areAllCredentialsSet.value) {
onTabSelect('credential');
return;
}
if (parametersByTab.value.params.length > 0) {
onTabSelect('params');
}
}
</script> </script>
<template> <template>
@@ -563,6 +586,7 @@ function displayCredentials(credentialTypeDescription: INodeCredentialDescriptio
dragging: dragging, dragging: dragging,
embedded: props.isEmbeddedInCanvas, embedded: props.isEmbeddedInCanvas,
}" }"
:data-has-output-connection="hasOutputConnection"
@keydown.stop @keydown.stop
> >
<ExperimentalEmbeddedNdvHeader <ExperimentalEmbeddedNdvHeader
@@ -573,6 +597,9 @@ function displayCredentials(credentialTypeDescription: INodeCredentialDescriptio
:node-type="nodeType" :node-type="nodeType"
:push-ref="pushRef" :push-ref="pushRef"
:sub-title="subTitle" :sub-title="subTitle"
:include-action="parametersByTab.action.length > 0"
:include-credential="isDisplayingCredentials"
:has-credential-issue="!areAllCredentialsSet"
@name-changed="nameChanged" @name-changed="nameChanged"
@tab-changed="onTabSelect" @tab-changed="onTabSelect"
> >
@@ -649,12 +676,28 @@ function displayCredentials(credentialTypeDescription: INodeCredentialDescriptio
" "
/> />
<FreeAiCreditsCallout /> <FreeAiCreditsCallout />
<NodeActionsList
v-if="openPanel === 'action'"
class="action-tab"
:node="node"
@action-selected="handleSelectAction"
/>
<NodeCredentials
v-if="openPanel === 'credential'"
:node="node"
:readonly="isReadOnly"
:show-all="true"
:hide-issues="hiddenIssuesInputs.includes('credentials')"
@credential-selected="credentialSelected"
@value-changed="valueChanged"
@blur="onParameterBlur"
/>
<div v-show="openPanel === 'params'"> <div v-show="openPanel === 'params'">
<NodeWebhooks :node="node" :node-type-description="nodeType" /> <NodeWebhooks :node="node" :node-type-description="nodeType" />
<ParameterInputList <ParameterInputList
v-if="nodeValuesInitialized" v-if="nodeValuesInitialized"
:parameters="parametersNoneSetting" :parameters="parametersByTab.params"
:hide-delete="true" :hide-delete="true"
:node-values="nodeValues" :node-values="nodeValues"
:is-read-only="isReadOnly" :is-read-only="isReadOnly"
@@ -702,7 +745,7 @@ function displayCredentials(credentialTypeDescription: INodeCredentialDescriptio
data-test-id="update-available" data-test-id="update-available"
/> />
<ParameterInputList <ParameterInputList
:parameters="parametersSetting" :parameters="parametersByTab.settings"
:node-values="nodeValues" :node-values="nodeValues"
:is-read-only="isReadOnly" :is-read-only="isReadOnly"
:hide-delete="true" :hide-delete="true"
@@ -733,7 +776,10 @@ function displayCredentials(credentialTypeDescription: INodeCredentialDescriptio
<span>({{ nodeVersionTag }})</span> <span>({{ nodeVersionTag }})</span>
</div> </div>
</div> </div>
<div v-if="isNDVV2 && featureRequestUrl" :class="$style.featureRequest"> <div
v-if="isNDVV2 && featureRequestUrl && !isEmbeddedInCanvas"
:class="$style.featureRequest"
>
<a target="_blank" @click="onFeatureRequestClick"> <a target="_blank" @click="onFeatureRequestClick">
<N8nIcon icon="lightbulb" /> <N8nIcon icon="lightbulb" />
{{ i18n.baseText('ndv.featureRequest') }} {{ i18n.baseText('ndv.featureRequest') }}
@@ -817,11 +863,19 @@ function displayCredentials(credentialTypeDescription: INodeCredentialDescriptio
&.embedded .node-parameters-wrapper { &.embedded .node-parameters-wrapper {
padding: 0 var(--spacing-xs) var(--spacing-xs) var(--spacing-xs); padding: 0 var(--spacing-xs) var(--spacing-xs) var(--spacing-xs);
&:has(.action-tab) {
padding: 0 0 var(--spacing-xs) 0;
}
} }
&.embedded .node-parameters-wrapper.with-static-scrollbar { &.embedded .node-parameters-wrapper.with-static-scrollbar {
padding: 0 var(--spacing-4xs) var(--spacing-xs) var(--spacing-xs); padding: 0 var(--spacing-4xs) var(--spacing-xs) var(--spacing-xs);
&:has(.action-tab) {
padding: 0 0 var(--spacing-xs) 0;
}
@supports not (selector(::-webkit-scrollbar)) { @supports not (selector(::-webkit-scrollbar)) {
scrollbar-width: thin; scrollbar-width: thin;
} }
@@ -898,3 +952,10 @@ function displayCredentials(credentialTypeDescription: INodeCredentialDescriptio
} }
} }
</style> </style>
<style lang="scss">
// Hide notice(.ndv-connection-hint-notice) warning when node has output connection
[data-has-output-connection='true'] .ndv-connection-hint-notice {
display: none;
}
</style>

View File

@@ -1,8 +1,9 @@
<script setup lang="ts"> <script setup lang="ts">
import type { IUpdateInformation } from '@/Interface'; import type { IUpdateInformation } from '@/Interface';
import type { INodeTypeDescription } from 'n8n-workflow'; import type { INodeTypeDescription } from 'n8n-workflow';
import { type Tab, default as NodeSettingsTabs } from './NodeSettingsTabs.vue'; import NodeSettingsTabs from './NodeSettingsTabs.vue';
import NodeExecuteButton from './NodeExecuteButton.vue'; import NodeExecuteButton from './NodeExecuteButton.vue';
import type { NodeSettingsTab } from '@/types/nodeSettings';
type Props = { type Props = {
nodeName: string; nodeName: string;
@@ -10,7 +11,7 @@ type Props = {
hideTabs: boolean; hideTabs: boolean;
disableExecute: boolean; disableExecute: boolean;
executeButtonTooltip: string; executeButtonTooltip: string;
selectedTab: Tab; selectedTab: NodeSettingsTab;
nodeType?: INodeTypeDescription | null; nodeType?: INodeTypeDescription | null;
pushRef: string; pushRef: string;
}; };
@@ -21,7 +22,7 @@ const emit = defineEmits<{
execute: []; execute: [];
'stop-execution': []; 'stop-execution': [];
'value-changed': [update: IUpdateInformation]; 'value-changed': [update: IUpdateInformation];
'tab-changed': [tab: Tab]; 'tab-changed': [tab: NodeSettingsTab];
}>(); }>();
</script> </script>

View File

@@ -15,14 +15,18 @@ import { N8nTabs } from '@n8n/design-system';
import { useNodeDocsUrl } from '@/composables/useNodeDocsUrl'; import { useNodeDocsUrl } from '@/composables/useNodeDocsUrl';
import { useCommunityNodesStore } from '@/stores/communityNodes.store'; import { useCommunityNodesStore } from '@/stores/communityNodes.store';
import { useUsersStore } from '@/stores/users.store'; import { useUsersStore } from '@/stores/users.store';
import type { NodeSettingsTab } from '@/types/nodeSettings';
export type Tab = 'settings' | 'params' | 'communityNode' | 'docs';
type Props = { type Props = {
modelValue?: Tab; modelValue?: NodeSettingsTab;
nodeType?: INodeTypeDescription | null; nodeType?: INodeTypeDescription | null;
pushRef?: string; pushRef?: string;
hideDocs?: boolean; hideDocs?: boolean;
tabsVariant?: 'modern' | 'legacy'; tabsVariant?: 'modern' | 'legacy';
includeAction?: boolean;
includeCredential?: boolean;
hasCredentialIssue?: boolean;
compact?: boolean;
}; };
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
@@ -30,9 +34,10 @@ const props = withDefaults(defineProps<Props>(), {
nodeType: undefined, nodeType: undefined,
pushRef: '', pushRef: '',
tabsVariant: undefined, tabsVariant: undefined,
hasCredentialIssue: false,
}); });
const emit = defineEmits<{ const emit = defineEmits<{
'update:model-value': [tab: Tab]; 'update:model-value': [tab: NodeSettingsTab];
}>(); }>();
const externalHooks = useExternalHooks(); const externalHooks = useExternalHooks();
@@ -66,20 +71,45 @@ const documentationUrl = computed(() => {
}); });
const options = computed(() => { const options = computed(() => {
const options: Array<ITab<Tab>> = [ const ret: Array<ITab<NodeSettingsTab>> = [];
if (props.includeAction) {
ret.push({
label: i18n.baseText('nodeSettings.action'),
value: 'action',
});
}
if (props.includeCredential) {
ret.push({
label: i18n.baseText('nodeSettings.credential'),
value: 'credential',
...(props.hasCredentialIssue && {
icon: 'triangle-alert',
iconPosition: 'right',
variant: 'danger',
}),
});
}
ret.push(
{ {
label: i18n.baseText('nodeSettings.parameters'), label: i18n.baseText(
props.compact ? 'nodeSettings.parametersShort' : 'nodeSettings.parameters',
),
value: 'params', value: 'params',
}, },
{ {
label: i18n.baseText('nodeSettings.settings'),
value: 'settings', value: 'settings',
notification: installedPackage.value?.updateAvailable ? true : undefined, notification: installedPackage.value?.updateAvailable ? true : undefined,
...(props.compact
? { icon: 'settings', align: 'right', tooltip: i18n.baseText('nodeSettings.settings') }
: { label: i18n.baseText('nodeSettings.settings') }),
}, },
]; );
if (isCommunityNode.value) { if (isCommunityNode.value) {
options.push({ ret.push({
icon: 'box', icon: 'box',
value: 'communityNode', value: 'communityNode',
align: 'right', align: 'right',
@@ -93,18 +123,20 @@ const options = computed(() => {
} }
if (documentationUrl.value) { if (documentationUrl.value) {
options.push({ ret.push({
label: i18n.baseText('nodeSettings.docs'),
value: 'docs', value: 'docs',
href: documentationUrl.value, href: documentationUrl.value,
align: 'right', align: 'right',
...(props.compact
? { icon: 'book-open', tooltip: i18n.baseText('nodeSettings.docs') }
: { label: i18n.baseText('nodeSettings.docs') }),
}); });
} }
return options; return ret;
}); });
function onTabSelect(tab: string | number) { function onTabSelect(tab: NodeSettingsTab) {
if (tab === 'docs' && props.nodeType) { if (tab === 'docs' && props.nodeType) {
void externalHooks.run('dataDisplay.onDocumentationUrlClick', { void externalHooks.run('dataDisplay.onDocumentationUrlClick', {
nodeType: props.nodeType, nodeType: props.nodeType,
@@ -127,12 +159,12 @@ function onTabSelect(tab: string | number) {
}); });
} }
if (tab === 'settings' || tab === 'params') { if (tab === 'settings' || tab === 'params' || tab === 'action' || tab === 'credential') {
emit('update:model-value', tab); emit('update:model-value', tab);
} }
} }
function onTooltipClick(tab: string | number, event: MouseEvent) { function onTooltipClick(tab: NodeSettingsTab, event: MouseEvent) {
if (tab === 'communityNode' && (event.target as Element).localName === 'a') { if (tab === 'communityNode' && (event.target as Element).localName === 'a') {
telemetry.track('user clicked cnr docs link', { source: 'node details view' }); telemetry.track('user clicked cnr docs link', { source: 'node details view' });
} }
@@ -150,6 +182,7 @@ onMounted(async () => {
:options="options" :options="options"
:model-value="modelValue" :model-value="modelValue"
:variant="tabsVariant" :variant="tabsVariant"
:size="compact ? 'small' : 'medium'"
@update:model-value="onTabSelect" @update:model-value="onTabSelect"
@tooltip-click="onTooltipClick" @tooltip-click="onTooltipClick"
/> />

View File

@@ -1,8 +1,9 @@
<script setup lang="ts"> <script setup lang="ts">
import NodeIcon from '@/components/NodeIcon.vue'; import NodeIcon from '@/components/NodeIcon.vue';
import NodeSettingsTabs, { type Tab } from '@/components/NodeSettingsTabs.vue'; import NodeSettingsTabs from '@/components/NodeSettingsTabs.vue';
import { N8nText } from '@n8n/design-system'; import { N8nText } from '@n8n/design-system';
import type { INode, INodeTypeDescription } from 'n8n-workflow'; import type { INode, INodeTypeDescription } from 'n8n-workflow';
import type { NodeSettingsTab } from '@/types/nodeSettings';
defineProps<{ defineProps<{
node: INode; node: INode;
@@ -10,12 +11,15 @@ defineProps<{
nodeType?: INodeTypeDescription | null; nodeType?: INodeTypeDescription | null;
pushRef: string; pushRef: string;
subTitle?: string; subTitle?: string;
selectedTab: Tab; selectedTab: NodeSettingsTab;
includeAction: boolean;
includeCredential: boolean;
hasCredentialIssue?: boolean;
}>(); }>();
const emit = defineEmits<{ const emit = defineEmits<{
'name-changed': [value: string]; 'name-changed': [value: string];
'tab-changed': [tab: Tab]; 'tab-changed': [tab: NodeSettingsTab];
}>(); }>();
defineSlots<{ actions?: {} }>(); defineSlots<{ actions?: {} }>();
@@ -38,13 +42,19 @@ defineSlots<{ actions?: {} }>();
</N8nText> </N8nText>
<slot name="actions" /> <slot name="actions" />
</div> </div>
<NodeSettingsTabs <div :class="$style.tabsContainer">
:model-value="selectedTab" <NodeSettingsTabs
:node-type="nodeType" :model-value="selectedTab"
:push-ref="pushRef" :node-type="nodeType"
tabs-variant="modern" :push-ref="pushRef"
@update:model-value="emit('tab-changed', $event)" tabs-variant="modern"
/> compact
:include-action="includeAction"
:include-credential="includeCredential"
:has-credential-issue="hasCredentialIssue"
@update:model-value="emit('tab-changed', $event)"
/>
</div>
</div> </div>
</template> </template>
@@ -58,7 +68,7 @@ defineSlots<{ actions?: {} }>();
align-items: center; align-items: center;
padding: var(--spacing-2xs) var(--spacing-3xs) var(--spacing-2xs) var(--spacing-xs); padding: var(--spacing-2xs) var(--spacing-3xs) var(--spacing-2xs) var(--spacing-xs);
border-bottom: var(--border-base); border-bottom: var(--border-base);
margin-bottom: var(--spacing-xs); margin-bottom: 14px; // to match bottom padding of tabs
gap: var(--spacing-4xs); gap: var(--spacing-4xs);
.disabled & { .disabled & {
@@ -87,4 +97,8 @@ defineSlots<{ actions?: {} }>();
text-overflow: ellipsis; text-overflow: ellipsis;
padding-top: var(--spacing-5xs); padding-top: var(--spacing-5xs);
} }
.tabsContainer {
padding-inline: var(--spacing-xs);
}
</style> </style>

View File

@@ -44,8 +44,8 @@ const isVisible = computed(() =>
{ {
x: -vf.viewport.value.x / vf.viewport.value.zoom, x: -vf.viewport.value.x / vf.viewport.value.zoom,
y: -vf.viewport.value.y / vf.viewport.value.zoom, y: -vf.viewport.value.y / vf.viewport.value.zoom,
width: vf.dimensions.value.width, width: vf.dimensions.value.width / vf.viewport.value.zoom,
height: vf.dimensions.value.height, height: vf.dimensions.value.height / vf.viewport.value.zoom,
}, },
), ),
); );
@@ -184,7 +184,7 @@ watchOnce(isVisible, (visible) => {
<style lang="scss" module> <style lang="scss" module>
.component { .component {
align-items: flex-start; align-items: flex-start !important;
justify-content: stretch; justify-content: stretch;
border-width: 1px !important; border-width: 1px !important;
border-radius: var(--border-radius-base) !important; border-radius: var(--border-radius-base) !important;

View File

@@ -1,10 +1,6 @@
import type { INodeUi } from '@/Interface'; import type { INodeUi } from '@/Interface';
import type { I18nClass } from '@n8n/i18n'; import type { I18nClass } from '@n8n/i18n';
import type { INodeProperties, INodeTypeDescription } from 'n8n-workflow'; import type { INodeTypeDescription } from 'n8n-workflow';
export function shouldShowParameter(item: INodeProperties): boolean {
return item.name.match(/resource|authentication|operation/i) === null;
}
export function getNodeSubTitleText( export function getNodeSubTitleText(
node: INodeUi, node: INodeUi,

View File

@@ -0,0 +1,125 @@
import { useNodeHelpers } from '@/composables/useNodeHelpers';
import { KEEP_AUTH_IN_NDV_FOR_NODES } from '@/constants';
import type { ICredentialsResponse, INodeUi } from '@/Interface';
import { useCredentialsStore } from '@/stores/credentials.store';
import {
getAllNodeCredentialForAuthType,
getMainAuthField,
isRequiredCredential,
} from '@/utils/nodeTypesUtils';
import {
HTTP_REQUEST_NODE_TYPE,
type INodeCredentialDescription,
type INodeTypeDescription,
type NodeParameterValueType,
} from 'n8n-workflow';
import { computed, unref, type ComputedRef, type MaybeRef } from 'vue';
export interface CredentialDropdownOption extends ICredentialsResponse {
typeDisplayName: string;
}
export function useNodeCredentialOptions(
node: ComputedRef<INodeUi | null>,
nodeType: ComputedRef<INodeTypeDescription | null>,
overrideCredType: MaybeRef<NodeParameterValueType | undefined>,
) {
const nodeHelpers = useNodeHelpers();
const credentialsStore = useCredentialsStore();
const mainNodeAuthField = computed(() => getMainAuthField(nodeType.value));
const credentialTypesNodeDescriptions = computed(() =>
credentialsStore.getCredentialTypesNodeDescriptions(unref(overrideCredType), nodeType.value),
);
const credentialTypesNodeDescriptionDisplayed = computed(() =>
credentialTypesNodeDescriptions.value.filter(displayCredentials).map((type) => ({
type,
options: getCredentialOptions(getAllRelatedCredentialTypes(type)),
})),
);
const areAllCredentialsSet = computed(() =>
credentialTypesNodeDescriptionDisplayed.value.every(({ type }) => isCredentialExisting(type)),
);
function getCredentialOptions(types: string[]): CredentialDropdownOption[] {
let options: CredentialDropdownOption[] = [];
types.forEach((type) => {
options = options.concat(
credentialsStore.allUsableCredentialsByType[type].map<CredentialDropdownOption>(
(option: ICredentialsResponse) => ({
...option,
typeDisplayName: credentialsStore.getCredentialTypeByName(type)?.displayName ?? '',
}),
),
);
});
if (node.value?.type === HTTP_REQUEST_NODE_TYPE) {
options = options.filter((option) => !option.isManaged);
}
return options;
}
function displayCredentials(credentialTypeDescription: INodeCredentialDescription): boolean {
if (!node.value) {
return false;
}
if (credentialTypeDescription.displayOptions === undefined) {
// If it is not defined no need to do a proper check
return true;
}
return nodeHelpers.displayParameter(
node.value.parameters,
credentialTypeDescription,
'',
node.value,
);
}
function showMixedCredentials(credentialType: INodeCredentialDescription): boolean {
if (!node.value) {
return false;
}
const isRequired = isRequiredCredential(nodeType.value, credentialType);
return !KEEP_AUTH_IN_NDV_FOR_NODES.includes(node.value.type) && isRequired;
}
function getAllRelatedCredentialTypes(credentialType: INodeCredentialDescription): string[] {
const credentialIsRequired = showMixedCredentials(credentialType);
if (credentialIsRequired) {
if (mainNodeAuthField.value) {
const credentials = getAllNodeCredentialForAuthType(
nodeType.value,
mainNodeAuthField.value.name,
);
return credentials.map((cred) => cred.name);
}
}
return [credentialType.name];
}
function isCredentialExisting(credentialType: INodeCredentialDescription): boolean {
if (!node.value?.credentials?.[credentialType.name]?.id) {
return false;
}
const { id } = node.value.credentials[credentialType.name];
const options = getCredentialOptions([credentialType.name]);
return !!options.find((option: ICredentialsResponse) => option.id === id);
}
return {
credentialTypesNodeDescriptions,
credentialTypesNodeDescriptionDisplayed,
mainNodeAuthField,
areAllCredentialsSet,
showMixedCredentials,
isCredentialExisting,
};
}

View File

@@ -0,0 +1,7 @@
export type NodeSettingsTab =
| 'settings'
| 'params'
| 'communityNode'
| 'docs'
| 'action'
| 'credential';

View File

@@ -696,3 +696,32 @@ export function collectSettings(node: INodeUi, nodeSettings: INodeProperties[]):
return ret; return ret;
} }
export function collectParametersByTab(parameters: INodeProperties[], isEmbeddedInCanvas: boolean) {
const ret: Record<'settings' | 'action' | 'params', INodeProperties[]> = {
settings: [],
action: [],
params: [],
};
for (const item of parameters) {
if (item.isNodeSetting) {
ret.settings.push(item);
continue;
}
if (!isEmbeddedInCanvas) {
ret.params.push(item);
continue;
}
if (item.name === 'resource' || item.name === 'operation') {
ret.action.push(item);
continue;
}
ret.params.push(item);
}
return ret;
}