mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 18:12:04 +00:00
feat(editor): Distinguish official verified nodes from community built nodes (#15630)
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -59,7 +59,7 @@ vi.mock('../composables/useViewStacks', () => ({
|
||||
mode: 'community-node',
|
||||
rootView: undefined,
|
||||
subcategory: 'Other Node',
|
||||
title: 'Community node details',
|
||||
title: 'Node details',
|
||||
},
|
||||
})),
|
||||
}));
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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')"
|
||||
/>
|
||||
|
||||
|
||||
@@ -51,6 +51,8 @@ export type CommunityNodeDetails = {
|
||||
description: string;
|
||||
packageName: string;
|
||||
installed: boolean;
|
||||
official: boolean;
|
||||
companyName?: string;
|
||||
nodeIcon?: NodeIconSource;
|
||||
};
|
||||
|
||||
|
||||
@@ -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',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user