diff --git a/packages/frontend/@n8n/i18n/src/locales/en.json b/packages/frontend/@n8n/i18n/src/locales/en.json index 8d3601b36fe..766587e622f 100644 --- a/packages/frontend/@n8n/i18n/src/locales/en.json +++ b/packages/frontend/@n8n/i18n/src/locales/en.json @@ -3558,6 +3558,8 @@ "settings.communityNodes.uninstallAction.label": "Uninstall package", "settings.communityNodes.upToDate.tooltip": "You are up to date", "settings.communityNodes.failedToLoad.tooltip": "This package failed to load. It may be corrupted, or it may need a newer version of n8n. Try uninstalling and reinstalling it, or upgrade n8n.", + "settings.communityNodes.restricted.badge.label": "Restricted", + "settings.communityNodes.restricted.tooltip.heading": "Blocked by an instance policy", "settings.communityNodes.managedByEnv": "Community packages are managed via environment variables and cannot be modified from the UI.", "settings.communityNodes.fetchError.title": "Problem fetching installed packages", "settings.communityNodes.fetchError.message": "There may be a problem with your internet connection or your n8n instance", diff --git a/packages/frontend/editor-ui/src/features/settings/communityNodes/components/CommunityPackageCard.test.ts b/packages/frontend/editor-ui/src/features/settings/communityNodes/components/CommunityPackageCard.test.ts index 5c81e9c4ff1..4b2d47bb600 100644 --- a/packages/frontend/editor-ui/src/features/settings/communityNodes/components/CommunityPackageCard.test.ts +++ b/packages/frontend/editor-ui/src/features/settings/communityNodes/components/CommunityPackageCard.test.ts @@ -5,6 +5,9 @@ import { STORES } from '@n8n/stores'; import CommunityPackageCard from './CommunityPackageCard.vue'; import { createComponentRenderer } from '@/__tests__/render'; import type { PublicInstalledNode, PublicInstalledPackage } from 'n8n-workflow'; +import { useTypeAvailabilityPoliciesStore } from '@n8n/frontend-module-type-availability-policies'; +import type { NodeTypeAvailability } from '@n8n/api-types'; +import { waitFor } from '@testing-library/vue'; const communityPackage: PublicInstalledPackage = { packageName: 'n8n-nodes-test', @@ -113,4 +116,72 @@ describe('CommunityPackageCard', () => { expect(queryByText('Uninstall package')).not.toBeInTheDocument(); }); }); + + describe('restricted badge', () => { + const mockNodeAvailability = ( + unavailable: Record>, + ) => { + vi.spyOn(useTypeAvailabilityPoliciesStore(), 'getNodeTypeAvailability').mockImplementation( + (name: string) => + name in unavailable + ? { name, available: false, ...unavailable[name] } + : { name, available: true }, + ); + }; + + // Reka UI only mounts tooltip content once the trigger is hovered. + function hoverTrigger(element: Element) { + element.dispatchEvent( + new PointerEvent('pointermove', { + bubbles: true, + cancelable: true, + pointerType: 'mouse', + clientX: 100, + clientY: 100, + }), + ); + } + + it('shows the badge and lists instance-blocked node types in the tooltip', async () => { + mockNodeAvailability({ 'n8n-nodes-test.oldNode': { scope: 'instance' } }); + + const { getByText } = renderComponent({ props: { communityPackage } }); + + const badge = getByText('Restricted'); + expect(badge).toBeInTheDocument(); + + hoverTrigger(badge); + + await waitFor(() => { + expect(document.querySelector('[data-dismissable-layer]')).toHaveTextContent('OldNode'); + }); + expect(document.querySelector('[data-dismissable-layer]')).not.toHaveTextContent('TestNode'); + }); + + it('does not show the badge when no node type is blocked', () => { + mockNodeAvailability({}); + + const { queryByText } = renderComponent({ props: { communityPackage } }); + + expect(queryByText('Restricted')).not.toBeInTheDocument(); + }); + + it('ignores a project-scoped restriction', () => { + mockNodeAvailability({ 'n8n-nodes-test.oldNode': { scope: 'project' } }); + + const { queryByText } = renderComponent({ props: { communityPackage } }); + + expect(queryByText('Restricted')).not.toBeInTheDocument(); + }); + + it('ignores an instance restriction that projects can opt into', () => { + mockNodeAvailability({ + 'n8n-nodes-test.oldNode': { scope: 'instance', optInAvailable: true }, + }); + + const { queryByText } = renderComponent({ props: { communityPackage } }); + + expect(queryByText('Restricted')).not.toBeInTheDocument(); + }); + }); }); diff --git a/packages/frontend/editor-ui/src/features/settings/communityNodes/components/CommunityPackageCard.vue b/packages/frontend/editor-ui/src/features/settings/communityNodes/components/CommunityPackageCard.vue index 3db55978b42..3244b65268f 100644 --- a/packages/frontend/editor-ui/src/features/settings/communityNodes/components/CommunityPackageCard.vue +++ b/packages/frontend/editor-ui/src/features/settings/communityNodes/components/CommunityPackageCard.vue @@ -1,6 +1,6 @@