mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 10:02:05 +00:00
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:
@@ -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,
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
143
packages/frontend/editor-ui/src/components/NodeActionsList.vue
Normal file
143
packages/frontend/editor-ui/src/components/NodeActionsList.vue
Normal 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>
|
||||||
@@ -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"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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>
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
};
|
||||||
|
}
|
||||||
7
packages/frontend/editor-ui/src/types/nodeSettings.ts
Normal file
7
packages/frontend/editor-ui/src/types/nodeSettings.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
export type NodeSettingsTab =
|
||||||
|
| 'settings'
|
||||||
|
| 'params'
|
||||||
|
| 'communityNode'
|
||||||
|
| 'docs'
|
||||||
|
| 'action'
|
||||||
|
| 'credential';
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user