feat(editor): Distinguish official verified nodes from community built nodes (#15630)

This commit is contained in:
Elias Meire
2025-05-23 17:04:22 +02:00
committed by GitHub
parent dc0802bbd1
commit 7f0c6d62e6
21 changed files with 437 additions and 283 deletions

View File

@@ -4,7 +4,6 @@ import { ElTag } from 'element-plus';
import { useI18n } from '../../composables/useI18n';
import type { NodeCreatorTag } from '../../types/node-creator-node';
import N8nTooltip from '../N8nTooltip';
export interface Props {
active?: boolean;
@@ -14,6 +13,7 @@ export interface Props {
tag?: NodeCreatorTag;
title: string;
showActionArrow?: boolean;
isOfficial?: boolean;
}
defineProps<Props>();
@@ -22,6 +22,8 @@ defineEmits<{
tooltipClick: [e: MouseEvent];
}>();
defineSlots<{ icon: {}; extraDetails: {}; dragContent: {} }>();
const { t } = useI18n();
</script>
@@ -49,16 +51,8 @@ const { t } = useI18n();
:title="t('nodeCreator.nodeItem.triggerIconTitle')"
:class="$style.triggerIcon"
/>
<N8nTooltip
v-if="!!$slots.tooltip"
placement="top"
data-test-id="node-creator-item-tooltip"
>
<template #content>
<slot name="tooltip" />
</template>
<n8n-icon :class="$style.tooltipIcon" icon="cube" />
</N8nTooltip>
<slot name="extraDetails" />
</div>
<p
v-if="description"
@@ -121,7 +115,9 @@ const { t } = useI18n();
width: 12px;
}
.details {
display: flex;
align-items: center;
gap: var(--spacing-3xs);
}
.nodeIcon {
display: flex;
@@ -141,12 +137,10 @@ const { t } = useI18n();
}
.aiIcon {
margin-left: var(--spacing-3xs);
color: var(--color-secondary);
}
.triggerIcon {
margin-left: var(--spacing-3xs);
color: var(--color-primary);
}
</style>

View File

@@ -1,12 +1,13 @@
import type { INodeTranslationHeaders, IRestApiContext } from '@/Interface';
import { makeRestApiRequest } from '@/utils/apiUtils';
import type {
ActionResultRequestDto,
CommunityNodeType,
OptionsRequestDto,
ResourceLocatorRequestDto,
ResourceMapperFieldsRequestDto,
} from '@n8n/api-types';
import { makeRestApiRequest } from '@/utils/apiUtils';
import type { INodeTranslationHeaders, IRestApiContext } from '@/Interface';
import type { CommunityNodeAttributes } from '@n8n/api-types';
import axios from 'axios';
import {
type INodeListSearchResult,
type INodePropertyOptions,
@@ -16,7 +17,6 @@ import {
type ResourceMapperFields,
sleep,
} from 'n8n-workflow';
import axios from 'axios';
async function fetchNodeTypesJsonWithRetry(url: string, retries = 5, delay = 500) {
for (let attempt = 0; attempt < retries; attempt++) {
@@ -38,14 +38,14 @@ export async function getNodeTypes(baseUrl: string) {
export async function fetchCommunityNodeTypes(
context: IRestApiContext,
): Promise<INodeTypeDescription[]> {
): Promise<CommunityNodeType[]> {
return await makeRestApiRequest(context, 'GET', '/community-node-types');
}
export async function fetchCommunityNodeAttributes(
context: IRestApiContext,
type: string,
): Promise<CommunityNodeAttributes | null> {
): Promise<CommunityNodeType | null> {
return await makeRestApiRequest(
context,
'GET',

View File

@@ -1,5 +1,4 @@
<script setup lang="ts">
import { computed, ref } from 'vue';
import type { SimplifiedNodeType } from '@/Interface';
import {
COMMUNITY_NODES_INSTALLATION_DOCS_URL,
@@ -8,17 +7,21 @@ import {
DRAG_EVENT_DATA_KEY,
HITL_SUBCATEGORY,
} from '@/constants';
import { computed, ref } from 'vue';
import { isCommunityPackageName } from '@/utils/nodeTypesUtils';
import { useNodeCreatorStore } from '@/stores/nodeCreator.store';
import NodeIcon from '@/components/NodeIcon.vue';
import { useNodeCreatorStore } from '@/stores/nodeCreator.store';
import { isCommunityPackageName } from '@/utils/nodeTypesUtils';
import OfficialIcon from 'virtual:icons/mdi/verified';
import { useI18n } from '@/composables/useI18n';
import { useNodeType } from '@/composables/useNodeType';
import { useTelemetry } from '@/composables/useTelemetry';
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
import { N8nTooltip } from '@n8n/design-system';
import { useActions } from '../composables/useActions';
import { useViewStacks } from '../composables/useViewStacks';
import { useI18n } from '@/composables/useI18n';
import { useTelemetry } from '@/composables/useTelemetry';
import { useNodeType } from '@/composables/useNodeType';
import { isNodePreviewKey } from '../utils';
import { isNodePreviewKey, removePreviewToken } from '../utils';
export interface Props {
nodeType: SimplifiedNodeType;
@@ -40,6 +43,7 @@ const { activeViewStack } = useViewStacks();
const { isSubNodeType } = useNodeType({
nodeType: props.nodeType,
});
const nodeTypesStore = useNodeTypesStore();
const dragging = ref(false);
const draggablePosition = ref({ x: -100, y: -100 });
@@ -108,6 +112,18 @@ const isTrigger = computed<boolean>(() => {
return props.nodeType.group.includes('trigger') && !hasActions.value;
});
const communityNodeType = computed(() => {
return nodeTypesStore.communityNodeType(removePreviewToken(props.nodeType.name));
});
const isOfficial = computed(() => {
return communityNodeType.value?.isOfficialNode ?? false;
});
const author = computed(() => {
return communityNodeType.value?.displayName ?? displayName.value;
});
function onDragStart(event: DragEvent): void {
if (event.dataTransfer) {
event.dataTransfer.effectAllowed = 'copy';
@@ -145,6 +161,7 @@ function onCommunityNodeTooltipClick(event: MouseEvent) {
:title="displayName"
:show-action-arrow="showActionArrow"
:is-trigger="isTrigger"
:is-official="isOfficial"
:data-test-id="dataTestId"
:tag="nodeType.tag"
@dragstart="onDragStart"
@@ -155,22 +172,38 @@ function onCommunityNodeTooltipClick(event: MouseEvent) {
<NodeIcon :class="$style.nodeIcon" :node-type="nodeType" />
</template>
<template v-if="isOfficial" #extraDetails>
<N8nTooltip placement="top" :show-after="500">
<template #content>
{{ i18n.baseText('generic.officialNode.tooltip', { interpolate: { author: author } }) }}
</template>
<OfficialIcon :class="[$style.icon, $style.official]" />
</N8nTooltip>
</template>
<template
v-if="isCommunityNode && !isCommunityNodePreview && !activeViewStack?.communityNodeDetails"
#tooltip
v-else-if="
isCommunityNode && !isCommunityNodePreview && !activeViewStack?.communityNodeDetails
"
#extraDetails
>
<p
v-n8n-html="
i18n.baseText('generic.communityNode.tooltip', {
interpolate: {
packageName: nodeType.name.split('.')[0],
docURL: COMMUNITY_NODES_INSTALLATION_DOCS_URL,
},
})
"
:class="$style.communityNodeIcon"
@click="onCommunityNodeTooltipClick"
/>
<N8nTooltip placement="top" :show-after="500">
<template #content>
<p
v-n8n-html="
i18n.baseText('generic.communityNode.tooltip', {
interpolate: {
packageName: nodeType.name.split('.')[0],
docURL: COMMUNITY_NODES_INSTALLATION_DOCS_URL,
},
})
"
:class="$style.communityNodeIcon"
@click="onCommunityNodeTooltipClick"
/>
</template>
<n8n-icon size="small" :class="$style.icon" icon="cube" />
</N8nTooltip>
</template>
<template #dragContent>
<div
@@ -230,4 +263,14 @@ function onCommunityNodeTooltipClick(event: MouseEvent) {
width: 1px;
height: 1px;
}
.icon {
display: inline-flex;
color: var(--color-text-base);
width: 12px;
&.official {
width: 14px;
}
}
</style>

View File

@@ -2,7 +2,8 @@ import { createComponentRenderer } from '@/__tests__/render';
import { type TestingPinia, createTestingPinia } from '@pinia/testing';
import { setActivePinia } from 'pinia';
import CommunityNodeDetails from './CommunityNodeDetails.vue';
import { fireEvent, waitFor } from '@testing-library/vue';
import { waitFor } from '@testing-library/vue';
import { userEvent } from '@testing-library/user-event';
const fetchCredentialTypes = vi.fn();
const getCommunityNodeAttributes = vi.fn(() => ({ npmVersion: '1.0.0' }));
@@ -55,6 +56,7 @@ vi.mock('@/stores/nodeTypes.store', () => ({
useNodeTypesStore: vi.fn(() => ({
getCommunityNodeAttributes,
getNodeTypes,
communityNodeType: vi.fn(() => ({ isOfficialNode: true })),
})),
}));
@@ -108,7 +110,7 @@ vi.mock('../composables/useViewStacks', () => ({
mode: 'community-node',
rootView: undefined,
subcategory: 'Other Node',
title: 'Community node details',
title: 'Node details',
},
pushViewStack,
popViewStack,
@@ -135,9 +137,9 @@ describe('CommunityNodeDetails', () => {
const installButton = wrapper.getByTestId('install-community-node-button');
expect(wrapper.container.querySelector('.title span')?.textContent).toEqual('Other Node');
expect(installButton.querySelector('span')?.textContent).toEqual('Install Node');
expect(installButton.querySelector('span')?.textContent).toEqual('Install node');
await fireEvent.click(installButton);
await userEvent.click(installButton);
await waitFor(() => expect(removeNodeFromMergedNodes).toHaveBeenCalled());
@@ -155,6 +157,7 @@ describe('CommunityNodeDetails', () => {
nodeIcon: undefined,
packageName: 'n8n-nodes-test',
title: 'Other Node',
official: true,
},
hasSearch: false,
items: [
@@ -178,9 +181,10 @@ describe('CommunityNodeDetails', () => {
mode: 'community-node',
rootView: undefined,
subcategory: 'Other Node',
title: 'Community node details',
title: 'Node details',
},
{
resetStacks: true,
transitionDirection: 'none',
},
);
@@ -196,7 +200,7 @@ describe('CommunityNodeDetails', () => {
const installButton = wrapper.getByTestId('install-community-node-button');
await fireEvent.click(installButton);
await userEvent.click(installButton);
expect(showError).toHaveBeenCalledWith(expect.any(Error), 'Error installing new package');
expect(pushViewStack).not.toHaveBeenCalled();

View File

@@ -8,6 +8,7 @@ import { i18n } from '@/plugins/i18n';
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
import { useNodeCreatorStore } from '@/stores/nodeCreator.store';
import { useCredentialsStore } from '@/stores/credentials.store';
import OfficialIcon from 'virtual:icons/mdi/verified';
import { getNodeIconSource } from '@/utils/nodeIcon';
@@ -44,6 +45,7 @@ const updateViewStack = (key: string) => {
);
pushViewStack(viewStack, {
resetStacks: true,
transitionDirection: 'none',
});
} else {
@@ -95,33 +97,50 @@ const onInstall = async () => {
</script>
<template>
<div :class="$style.container">
<div v-if="communityNodeDetails" :class="$style.container">
<div :class="$style.header">
<div :class="$style.title">
<NodeIcon
v-if="communityNodeDetails?.nodeIcon"
v-if="communityNodeDetails.nodeIcon"
:class="$style.nodeIcon"
:icon-source="communityNodeDetails.nodeIcon"
:circle="false"
:show-tooltip="false"
/>
<span>{{ communityNodeDetails?.title }}</span>
<span>{{ communityNodeDetails.title }}</span>
<N8nTooltip v-if="communityNodeDetails.official" placement="bottom" :show-after="500">
<template #content>
{{
i18n.baseText('generic.officialNode.tooltip', {
interpolate: {
author: communityNodeDetails.companyName ?? communityNodeDetails.title,
},
})
}}
</template>
<OfficialIcon :class="$style.officialIcon" />
</N8nTooltip>
</div>
<div>
<div v-if="communityNodeDetails?.installed" :class="$style.installed">
<FontAwesomeIcon :class="$style.installedIcon" icon="cube" />
<div v-if="communityNodeDetails.installed" :class="$style.installed">
<FontAwesomeIcon
v-if="!communityNodeDetails.official"
:class="$style.installedIcon"
icon="cube"
/>
<N8nText color="text-light" size="small" bold>
{{ i18n.baseText('communityNodeDetails.installed') }}
</N8nText>
</div>
<N8nButton
v-else-if="isOwner"
v-if="isOwner && !communityNodeDetails.installed"
:loading="loading"
:disabled="loading"
label="Install Node"
:label="i18n.baseText('communityNodeDetails.install')"
size="small"
@click="onInstall"
data-test-id="install-community-node-button"
@click="onInstall"
/>
</div>
</div>
@@ -138,6 +157,7 @@ const onInstall = async () => {
}
.header {
display: flex;
gap: var(--spacing-2xs);
align-items: center;
justify-content: space-between;
}
@@ -159,6 +179,14 @@ const onInstall = async () => {
font-size: var(--font-size-2xs);
}
.officialIcon {
display: inline-flex;
flex-shrink: 0;
margin-left: var(--spacing-4xs);
color: var(--color-text-base);
width: 14px;
}
.installed {
display: flex;
align-items: center;

View File

@@ -59,7 +59,7 @@ vi.mock('../composables/useViewStacks', () => ({
mode: 'community-node',
rootView: undefined,
subcategory: 'Other Node',
title: 'Community node details',
title: 'Node details',
},
})),
}));

View File

@@ -7,6 +7,7 @@ import { useNodeTypesStore } from '@/stores/nodeTypes.store';
import { useCommunityNodesStore } from '@/stores/communityNodes.store';
import { captureException } from '@sentry/vue';
import { N8nText, N8nTooltip, N8nIcon } from '@n8n/design-system';
import ShieldIcon from 'virtual:icons/fa-solid/shield-alt';
const { activeViewStack } = useViewStacks();
@@ -19,6 +20,7 @@ interface DownloadData {
const publisherName = ref<string | undefined>(undefined);
const downloads = ref<string | null>(null);
const verified = ref(false);
const official = ref(false);
const communityNodesStore = useCommunityNodesStore();
const nodeTypesStore = useNodeTypesStore();
@@ -41,8 +43,9 @@ async function fetchPackageInfo(packageName: string) {
);
if (communityNodeAttributes) {
publisherName.value = communityNodeAttributes.authorName;
publisherName.value = communityNodeAttributes.companyName ?? communityNodeAttributes.authorName;
downloads.value = formatNumber(communityNodeAttributes.numberOfDownloads);
official.value = communityNodeAttributes.isOfficialNode;
const packageInfo = communityNodesStore.getInstalledPackages.find(
(p) => p.packageName === communityNodeAttributes.packageName,
);
@@ -106,17 +109,21 @@ onMounted(async () => {
</N8nText>
<div :class="$style.separator"></div>
<div :class="$style.info">
<N8nTooltip placement="top" v-if="verified">
<template #content>{{ i18n.baseText('communityNodeInfo.approved') }}</template>
<N8nTooltip v-if="verified" placement="top">
<template #content>{{
official
? i18n.baseText('communityNodeInfo.officialApproved')
: i18n.baseText('communityNodeInfo.approved')
}}</template>
<div>
<FontAwesomeIcon :class="$style.tooltipIcon" icon="check-circle" />
<ShieldIcon :class="$style.tooltipIcon" />
<N8nText color="text-light" size="xsmall" bold data-test-id="verified-tag">
{{ i18n.baseText('communityNodeInfo.approved.label') }}
</N8nText>
</div>
</N8nTooltip>
<N8nTooltip placement="top" v-else>
<N8nTooltip v-else placement="top">
<template #content>{{ i18n.baseText('communityNodeInfo.unverified') }}</template>
<div>
<FontAwesomeIcon :class="$style.tooltipIcon" icon="cube" />
@@ -146,7 +153,7 @@ onMounted(async () => {
<div style="padding-bottom: 8px">
{{ i18n.baseText('communityNodeInfo.contact.admin') }}
</div>
<N8nText bold v-if="ownerEmailList.length">
<N8nText v-if="ownerEmailList.length" bold>
{{ ownerEmailList.join(', ') }}
</N8nText>
</N8nText>
@@ -188,12 +195,13 @@ onMounted(async () => {
.info div {
display: flex;
align-items: center;
gap: var(--spacing-3xs);
gap: var(--spacing-4xs);
}
.tooltipIcon {
color: var(--color-text-light);
font-size: var(--font-size-2xs);
width: 12px;
}
.contactOwnerHint {

View File

@@ -120,11 +120,13 @@ registerKeyHook(`CategoryLeft_${props.category}`, {
</n8n-tooltip>
</span>
</CategoryItem>
<div v-if="expanded && actionCount > 0 && $slots.default" :class="$style.contentSlot">
<slot />
</div>
<CommunityNodeInstallHint
v-if="isPreview"
v-if="isPreview && expanded"
:hint="i18n.baseText('communityNodeItem.actions.hint')"
/>

View File

@@ -51,6 +51,8 @@ export type CommunityNodeDetails = {
description: string;
packageName: string;
installed: boolean;
official: boolean;
companyName?: string;
nodeIcon?: NodeIconSource;
};

View File

@@ -17,6 +17,8 @@ import {
mockNodeCreateElement,
mockSectionCreateElement,
} from './__tests__/utils';
import { setActivePinia } from 'pinia';
import { createTestingPinia } from '@pinia/testing';
vi.mock('@/stores/settings.store', () => ({
useSettingsStore: vi.fn(() => ({ settings: {}, isAskAiEnabled: true })),
@@ -134,6 +136,9 @@ describe('NodeCreator - utils', () => {
});
});
describe('prepareCommunityNodeDetailsViewStack', () => {
beforeEach(() => {
setActivePinia(createTestingPinia());
});
const nodeCreateElement: NodeCreateElement = {
key: 'n8n-nodes-preview-test.OtherNode',
properties: {
@@ -162,6 +167,7 @@ describe('NodeCreator - utils', () => {
nodeIcon: undefined,
packageName: 'n8n-nodes-test',
title: 'Other Node',
official: false,
},
hasSearch: false,
items: [
@@ -185,7 +191,7 @@ describe('NodeCreator - utils', () => {
mode: 'community-node',
rootView: undefined,
subcategory: 'Other Node',
title: 'Community node details',
title: 'Node details',
});
});
@@ -255,6 +261,7 @@ describe('NodeCreator - utils', () => {
nodeIcon: undefined,
packageName: 'n8n-nodes-test',
title: 'Other Node',
official: false,
},
hasSearch: false,
items: [
@@ -322,7 +329,7 @@ describe('NodeCreator - utils', () => {
mode: 'actions',
rootView: undefined,
subcategory: 'Other Node',
title: 'Community node details',
title: 'Node details',
});
});
});

View File

@@ -29,6 +29,7 @@ import { useSettingsStore } from '@/stores/settings.store';
import { SEND_AND_WAIT_OPERATION } from 'n8n-workflow';
import type { NodeIconSource } from '../../../utils/nodeIcon';
import type { CommunityNodeDetails, ViewStack } from './composables/useViewStacks';
import { useNodeTypesStore } from '../../../stores/nodeTypes.store';
const COMMUNITY_NODE_TYPE_PREVIEW_TOKEN = '-preview';
@@ -264,6 +265,8 @@ export function prepareCommunityNodeDetailsViewStack(
): ViewStack {
const installed = !isNodePreviewKey(item.key);
const packageName = removePreviewToken(item.key.split('.')[0]);
const nodeTypesStore = useNodeTypesStore();
const nodeType = nodeTypesStore.communityNodeType(removePreviewToken(item.key));
const communityNodeDetails: CommunityNodeDetails = {
title: item.properties.displayName,
@@ -271,7 +274,9 @@ export function prepareCommunityNodeDetailsViewStack(
key: item.key,
nodeIcon,
installed,
official: nodeType?.isOfficialNode ?? false,
packageName,
companyName: nodeType?.companyName,
};
if (nodeActions.length) {

View File

@@ -50,6 +50,7 @@
"generic.resetAllFilters": "Reset all filters",
"generic.communityNode": "Community Node",
"generic.communityNode.tooltip": "This is a node from our community. It's part of the {packageName} package. <a href=\"{docURL}\" target=\"_blank\" title=\"Read the n8n docs\">Learn more</a>",
"generic.officialNode.tooltip": "This is an official node maintained by {author}",
"generic.copy": "Copy",
"generic.delete": "Delete",
"generic.dontShowAgain": "Don't show again",
@@ -1409,7 +1410,7 @@
"nodeSettings.scopes.expandedNoticeWithScopes": "<a data-key=\"toggle-expand\">{count} scope</a> available for {activeCredential} credentials<br>{scopes}<br><a data-key=\"show-less\">Show less</a> | <a data-key=\"toggle-expand\">{count} scopes</a> available for {activeCredential} credentials<br>{scopes}<br><a data-key=\"show-less\">Show less</a>",
"nodeSettings.scopes.notice": "<a data-key=\"toggle-expand\">{count} scope</a> available for {activeCredential} credentials | <a data-key=\"toggle-expand\">{count} scopes</a> available for {activeCredential} credentials",
"nodeSettings.theNodeIsNotValidAsItsTypeIsUnknown": "The node is not valid as its type ({nodeType}) is unknown",
"nodeSettings.communityNodeDetails.title": "Community node details",
"nodeSettings.communityNodeDetails.title": "Node details",
"nodeSettings.communityNodeUnknown.title": "Install this node to use it",
"nodeSettings.communityNodeUnknown.description": "This node is not currently installed. It's part of the {action} community package.",
"nodeSettings.communityNodeUnknown.installLink.text": "How to install community nodes",
@@ -3170,7 +3171,9 @@
"communityNodeItem.actions.hint": "Install this node to start using actions",
"communityNodeItem.label": "Add to workflow",
"communityNodeDetails.installed": "Installed",
"communityNodeDetails.install": "Install node",
"communityNodeInfo.approved": "This community node has been reviewed and approved by n8n",
"communityNodeInfo.officialApproved": "This node has been reviewed and approved by n8n",
"communityNodeInfo.approved.label": "Verified",
"communityNodeInfo.unverified": "This community node was added via npm and has not been verified by n8n",
"communityNodeInfo.unverified.label": "Via npm",

View File

@@ -1,5 +1,6 @@
import type {
ActionResultRequestDto,
CommunityNodeType,
OptionsRequestDto,
ResourceLocatorRequestDto,
ResourceMapperFieldsRequestDto,
@@ -35,7 +36,7 @@ export type NodeTypesStore = ReturnType<typeof useNodeTypesStore>;
export const useNodeTypesStore = defineStore(STORES.NODE_TYPES, () => {
const nodeTypes = ref<NodeTypesByTypeNameAndVersion>({});
const communityPreviews = ref<INodeTypeDescription[]>([]);
const vettedCommunityNodeTypes = ref<Map<string, CommunityNodeType>>(new Map());
const rootStore = useRootStore();
@@ -47,34 +48,41 @@ export const useNodeTypesStore = defineStore(STORES.NODE_TYPES, () => {
// #region Computed
// ---------------------------------------------------------------------------
const communityNodeType = computed(() => {
return (nodeTypeName: string) => {
return vettedCommunityNodeTypes.value.get(nodeTypeName);
};
});
const officialCommunityNodeTypes = computed(() =>
Array.from(vettedCommunityNodeTypes.value.values())
.filter(({ isOfficialNode, isInstalled }) => isOfficialNode && !isInstalled)
.map(({ nodeDescription }) => nodeDescription),
);
const unofficialCommunityNodeTypes = computed(() =>
Array.from(vettedCommunityNodeTypes.value.values())
.filter(({ isOfficialNode, isInstalled }) => !isOfficialNode && !isInstalled)
.map(({ nodeDescription }) => nodeDescription),
);
const communityNodesAndActions = computed(() => {
return actionsGenerator.generateMergedNodesAndActions(communityPreviews.value, []);
return actionsGenerator.generateMergedNodesAndActions(unofficialCommunityNodeTypes.value, []);
});
const allNodeTypes = computed(() => {
return Object.values(nodeTypes.value).reduce<INodeTypeDescription[]>(
(allNodeTypes, nodeType) => {
const versionNumbers = Object.keys(nodeType).map(Number);
const allNodeVersions = versionNumbers.map((version) => nodeType[version]);
return [...allNodeTypes, ...allNodeVersions];
},
[],
return Object.values(nodeTypes.value).flatMap((nodeType) =>
Object.keys(nodeType).map((version) => nodeType[Number(version)]),
);
});
const allLatestNodeTypes = computed(() => {
return Object.values(nodeTypes.value).reduce<INodeTypeDescription[]>(
(allLatestNodeTypes, nodeVersions) => {
return Object.values(nodeTypes.value)
.map((nodeVersions) => {
const versionNumbers = Object.keys(nodeVersions).map(Number);
const latestNodeVersion = nodeVersions[Math.max(...versionNumbers)];
if (!latestNodeVersion) return allLatestNodeTypes;
return [...allLatestNodeTypes, latestNodeVersion];
},
[],
);
return nodeVersions[Math.max(...versionNumbers)];
})
.filter(Boolean);
});
const getNodeType = computed(() => {
@@ -159,7 +167,9 @@ export const useNodeTypesStore = defineStore(STORES.NODE_TYPES, () => {
});
const visibleNodeTypes = computed(() => {
return allLatestNodeTypes.value.filter((nodeType: INodeTypeDescription) => !nodeType.hidden);
return allLatestNodeTypes.value
.concat(officialCommunityNodeTypes.value)
.filter((nodeType) => !nodeType.hidden);
});
const nativelyNumberSuffixedDefaults = computed(() => {
@@ -360,11 +370,15 @@ export const useNodeTypesStore = defineStore(STORES.NODE_TYPES, () => {
return;
}
try {
communityPreviews.value = await nodeTypesApi.fetchCommunityNodeTypes(
const communityNodeTypes = await nodeTypesApi.fetchCommunityNodeTypes(
rootStore.restApiContext,
);
vettedCommunityNodeTypes.value = new Map(
communityNodeTypes.map((nodeType) => [nodeType.name, nodeType]),
);
} catch (error) {
communityPreviews.value = [];
vettedCommunityNodeTypes.value = new Map();
}
};
@@ -402,6 +416,7 @@ export const useNodeTypesStore = defineStore(STORES.NODE_TYPES, () => {
visibleNodeTypesByInputConnectionTypeNames,
isConfigurableNode,
communityNodesAndActions,
communityNodeType,
getResourceMapperFields,
getLocalResourceMapperFields,
getNodeParameterActionResult,