diff --git a/packages/cli/src/services/__tests__/community-packages.service.test.ts b/packages/cli/src/services/__tests__/community-packages.service.test.ts index d7f7bb88dd3..857c278bfcb 100644 --- a/packages/cli/src/services/__tests__/community-packages.service.test.ts +++ b/packages/cli/src/services/__tests__/community-packages.service.test.ts @@ -534,7 +534,7 @@ describe('CommunityPackagesService', () => { globalConfig.nodes.communityPackages.unverifiedEnabled = false; globalConfig.nodes.communityPackages.registry = 'https://registry.npmjs.org'; await expect(communityPackagesService.installPackage('package', '0.1.0')).rejects.toThrow( - 'Installation of non-vetted community packages is forbidden!', + 'Installation of unverified community packages is forbidden!', ); }); }); diff --git a/packages/cli/src/services/community-packages.service.ts b/packages/cli/src/services/community-packages.service.ts index fa7355fc925..b100417068b 100644 --- a/packages/cli/src/services/community-packages.service.ts +++ b/packages/cli/src/services/community-packages.service.ts @@ -352,7 +352,7 @@ export class CommunityPackagesService { if (isUpdate) return; if (!this.globalConfig.nodes.communityPackages.unverifiedEnabled && !checksumProvided) { - throw new UnexpectedError('Installation of non-vetted community packages is forbidden!'); + throw new UnexpectedError('Installation of unverified community packages is forbidden!'); } } diff --git a/packages/frontend/editor-ui/src/components/Node/NodeCreator/Modes/ActionsMode.vue b/packages/frontend/editor-ui/src/components/Node/NodeCreator/Modes/ActionsMode.vue index 99c705c3874..bb7a9f0bb8c 100644 --- a/packages/frontend/editor-ui/src/components/Node/NodeCreator/Modes/ActionsMode.vue +++ b/packages/frontend/editor-ui/src/components/Node/NodeCreator/Modes/ActionsMode.vue @@ -39,7 +39,7 @@ const emit = defineEmits<{ const telemetry = useTelemetry(); const i18n = useI18n(); -const { userActivated } = useUsersStore(); +const { userActivated, isInstanceOwner } = useUsersStore(); const { popViewStack, updateCurrentViewStack } = useViewStacks(); const { registerKeyHook } = useKeyboardNavigation(); const { @@ -337,6 +337,7 @@ onMounted(() => { :class="$style.communityNodeFooter" v-if="communityNodeDetails" :package-name="communityNodeDetails.packageName" + :show-manage="communityNodeDetails.installed && isInstanceOwner" /> diff --git a/packages/frontend/editor-ui/src/components/Node/NodeCreator/Panel/CommunityNodeFooter.test.ts b/packages/frontend/editor-ui/src/components/Node/NodeCreator/Panel/CommunityNodeFooter.test.ts new file mode 100644 index 00000000000..c31fca7e5e2 --- /dev/null +++ b/packages/frontend/editor-ui/src/components/Node/NodeCreator/Panel/CommunityNodeFooter.test.ts @@ -0,0 +1,58 @@ +import { fireEvent } from '@testing-library/vue'; +import { VIEWS } from '@/constants'; +import { createTestingPinia } from '@pinia/testing'; +import { setActivePinia } from 'pinia'; +import CommunityNodeFooter from './CommunityNodeFooter.vue'; +import { createComponentRenderer } from '@/__tests__/render'; +import { vi } from 'vitest'; + +const push = vi.fn(); + +vi.mock('vue-router', async (importOriginal) => { + const actual = await importOriginal(); + return { + ...(actual as object), + useRouter: vi.fn(() => ({ + push, + })), + }; +}); + +describe('CommunityNodeInfo - links & bugs URL', () => { + beforeEach(() => { + setActivePinia(createTestingPinia()); + const fetchMock = vi.fn().mockResolvedValue({ + ok: true, + json: async () => ({ + bugs: { + url: 'https://github.com/n8n-io/n8n/issues', + }, + }), + }); + + vi.stubGlobal('fetch', fetchMock); + }); + + afterEach(() => { + vi.resetAllMocks(); + }); + + it('calls router.push to open settings page when "Manage" is clicked', async () => { + const { getByText } = createComponentRenderer(CommunityNodeFooter)({ + props: { packageName: 'n8n-nodes-test', showManage: true }, + }); + + const manageLink = getByText('Manage'); + await fireEvent.click(manageLink); + + expect(push).toHaveBeenCalledWith({ name: VIEWS.COMMUNITY_NODES }); + }); + + it('Manage should not be in the footer', async () => { + const { queryByText } = createComponentRenderer(CommunityNodeFooter)({ + props: { packageName: 'n8n-nodes-test', showManage: false }, + }); + + expect(queryByText('Manage')).not.toBeInTheDocument(); + }); +}); diff --git a/packages/frontend/editor-ui/src/components/Node/NodeCreator/Panel/CommunityNodeFooter.vue b/packages/frontend/editor-ui/src/components/Node/NodeCreator/Panel/CommunityNodeFooter.vue index 8932218e60f..ee7928517f0 100644 --- a/packages/frontend/editor-ui/src/components/Node/NodeCreator/Panel/CommunityNodeFooter.vue +++ b/packages/frontend/editor-ui/src/components/Node/NodeCreator/Panel/CommunityNodeFooter.vue @@ -8,6 +8,7 @@ import { N8nText, N8nLink } from '@n8n/design-system'; export interface Props { packageName: string; + showManage: boolean; } const props = defineProps(); @@ -54,10 +55,12 @@ onMounted(async () => { diff --git a/packages/frontend/editor-ui/src/components/Node/NodeCreator/Panel/NodesListPanel.vue b/packages/frontend/editor-ui/src/components/Node/NodeCreator/Panel/NodesListPanel.vue index 285ec6166fc..8e5d70abf8f 100644 --- a/packages/frontend/editor-ui/src/components/Node/NodeCreator/Panel/NodesListPanel.vue +++ b/packages/frontend/editor-ui/src/components/Node/NodeCreator/Panel/NodesListPanel.vue @@ -25,6 +25,7 @@ import CommunityNodeDetails from './CommunityNodeDetails.vue'; import CommunityNodeInfo from './CommunityNodeInfo.vue'; import CommunityNodeDocsLink from './CommunityNodeDocsLink.vue'; import CommunityNodeFooter from './CommunityNodeFooter.vue'; +import { useUsersStore } from '@/stores/users.store'; const i18n = useI18n(); const { callDebounced } = useDebounce(); @@ -34,6 +35,8 @@ const { pushViewStack, popViewStack, updateCurrentViewStack } = useViewStacks(); const { setActiveItemIndex, attachKeydownEvent, detachKeydownEvent } = useKeyboardNavigation(); const nodeCreatorStore = useNodeCreatorStore(); +const { isInstanceOwner } = useUsersStore(); + const activeViewStack = computed(() => useViewStacks().activeViewStack); const communityNodeDetails = computed(() => activeViewStack.value.communityNodeDetails); @@ -233,6 +236,7 @@ function onBackButton() { diff --git a/packages/frontend/editor-ui/src/plugins/i18n/locales/en.json b/packages/frontend/editor-ui/src/plugins/i18n/locales/en.json index 37aeccf1845..229c7a3f002 100644 --- a/packages/frontend/editor-ui/src/plugins/i18n/locales/en.json +++ b/packages/frontend/editor-ui/src/plugins/i18n/locales/en.json @@ -1831,7 +1831,9 @@ "settings": "Settings", "settings.communityNodes": "Community nodes", "settings.communityNodes.empty.title": "Supercharge your workflows with community nodes", + "settings.communityNodes.empty.verified.only.title": "Supercharge your workflows with verified community nodes", "settings.communityNodes.empty.description": "Install over {count} node packages contributed by our community.", + "settings.communityNodes.empty.verified.only.description": "You can install community and partner built node packages that have been verified by n8n directly from the nodes panel. Installed packages will show up here.", "settings.communityNodes.empty.description.no-packages": "Install node packages contributed by our community.", "settings.communityNodes.empty.installPackageLabel": "Install a community node", "settings.communityNodes.npmUnavailable.warning": "To use this feature, please install npm and restart n8n.", diff --git a/packages/frontend/editor-ui/src/views/SettingsCommunityNodesView.vue b/packages/frontend/editor-ui/src/views/SettingsCommunityNodesView.vue index 97f6a27e6af..e1fbb0439ba 100644 --- a/packages/frontend/editor-ui/src/views/SettingsCommunityNodesView.vue +++ b/packages/frontend/editor-ui/src/views/SettingsCommunityNodesView.vue @@ -36,7 +36,19 @@ const communityNodesStore = useCommunityNodesStore(); const uiStore = useUIStore(); const settingsStore = useSettingsStore(); +const getEmptyStateTitle = computed(() => { + if (!settingsStore.isUnverifiedPackagesEnabled) { + return i18n.baseText('settings.communityNodes.empty.verified.only.title'); + } + + return i18n.baseText('settings.communityNodes.empty.title'); +}); + const getEmptyStateDescription = computed(() => { + if (!settingsStore.isUnverifiedPackagesEnabled) { + return i18n.baseText('settings.communityNodes.empty.verified.only.description'); + } + const packageCount = communityNodesStore.availablePackageCount; return packageCount < PACKAGE_COUNT_THRESHOLD @@ -53,9 +65,10 @@ const getEmptyStateDescription = computed(() => { }); }); -const getEmptyStateButtonText = computed(() => - i18n.baseText('settings.communityNodes.empty.installPackageLabel'), -); +const getEmptyStateButtonText = computed(() => { + if (!settingsStore.isUnverifiedPackagesEnabled) return ''; + return i18n.baseText('settings.communityNodes.empty.installPackageLabel'); +}); const actionBoxConfig = computed(() => { return { @@ -163,9 +176,10 @@ onBeforeUnmount(() => { :class="$style.actionBoxContainer" >