fix: Community nodes - setting page empty state (#15305)

This commit is contained in:
Michael Kret
2025-05-12 14:40:11 +03:00
committed by GitHub
parent 81caedb319
commit e7c095d047
9 changed files with 95 additions and 13 deletions

View File

@@ -534,7 +534,7 @@ describe('CommunityPackagesService', () => {
globalConfig.nodes.communityPackages.unverifiedEnabled = false; globalConfig.nodes.communityPackages.unverifiedEnabled = false;
globalConfig.nodes.communityPackages.registry = 'https://registry.npmjs.org'; globalConfig.nodes.communityPackages.registry = 'https://registry.npmjs.org';
await expect(communityPackagesService.installPackage('package', '0.1.0')).rejects.toThrow( 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!',
); );
}); });
}); });

View File

@@ -352,7 +352,7 @@ export class CommunityPackagesService {
if (isUpdate) return; if (isUpdate) return;
if (!this.globalConfig.nodes.communityPackages.unverifiedEnabled && !checksumProvided) { 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!');
} }
} }

View File

@@ -39,7 +39,7 @@ const emit = defineEmits<{
const telemetry = useTelemetry(); const telemetry = useTelemetry();
const i18n = useI18n(); const i18n = useI18n();
const { userActivated } = useUsersStore(); const { userActivated, isInstanceOwner } = useUsersStore();
const { popViewStack, updateCurrentViewStack } = useViewStacks(); const { popViewStack, updateCurrentViewStack } = useViewStacks();
const { registerKeyHook } = useKeyboardNavigation(); const { registerKeyHook } = useKeyboardNavigation();
const { const {
@@ -337,6 +337,7 @@ onMounted(() => {
:class="$style.communityNodeFooter" :class="$style.communityNodeFooter"
v-if="communityNodeDetails" v-if="communityNodeDetails"
:package-name="communityNodeDetails.packageName" :package-name="communityNodeDetails.packageName"
:show-manage="communityNodeDetails.installed && isInstanceOwner"
/> />
</div> </div>
</template> </template>

View File

@@ -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();
});
});

View File

@@ -8,6 +8,7 @@ import { N8nText, N8nLink } from '@n8n/design-system';
export interface Props { export interface Props {
packageName: string; packageName: string;
showManage: boolean;
} }
const props = defineProps<Props>(); const props = defineProps<Props>();
@@ -54,10 +55,12 @@ onMounted(async () => {
<template> <template>
<div :class="$style.container"> <div :class="$style.container">
<N8nLink theme="text" @click="openSettingsPage"> <template v-if="props.showManage">
<N8nText size="small" color="primary" bold> Manage </N8nText> <N8nLink theme="text" @click="openSettingsPage">
</N8nLink> <N8nText size="small" color="primary" bold> Manage </N8nText>
<N8nText size="small" color="primary" bold>|</N8nText> </N8nLink>
<N8nText size="small" color="primary" bold>|</N8nText>
</template>
<N8nLink theme="text" @click="openIssuesPage"> <N8nLink theme="text" @click="openIssuesPage">
<N8nText size="small" color="primary" bold> Report issue </N8nText> <N8nText size="small" color="primary" bold> Report issue </N8nText>
</N8nLink> </N8nLink>

View File

@@ -142,14 +142,14 @@ onMounted(async () => {
</div> </div>
<div v-if="!isOwner && !communityNodeDetails?.installed" :class="$style.contactOwnerHint"> <div v-if="!isOwner && !communityNodeDetails?.installed" :class="$style.contactOwnerHint">
<N8nIcon color="text-light" icon="info-circle" size="large" /> <N8nIcon color="text-light" icon="info-circle" size="large" />
<nN8nText color="text-base" size="medium"> <N8nText color="text-base" size="medium">
<div style="padding-bottom: 8px"> <div style="padding-bottom: 8px">
{{ i18n.baseText('communityNodeInfo.contact.admin') }} {{ i18n.baseText('communityNodeInfo.contact.admin') }}
</div> </div>
<N8nText bold v-if="ownerEmailList.length"> <N8nText bold v-if="ownerEmailList.length">
{{ ownerEmailList.join(', ') }} {{ ownerEmailList.join(', ') }}
</N8nText> </N8nText>
</nN8nText> </N8nText>
</div> </div>
</div> </div>
</template> </template>

View File

@@ -25,6 +25,7 @@ import CommunityNodeDetails from './CommunityNodeDetails.vue';
import CommunityNodeInfo from './CommunityNodeInfo.vue'; import CommunityNodeInfo from './CommunityNodeInfo.vue';
import CommunityNodeDocsLink from './CommunityNodeDocsLink.vue'; import CommunityNodeDocsLink from './CommunityNodeDocsLink.vue';
import CommunityNodeFooter from './CommunityNodeFooter.vue'; import CommunityNodeFooter from './CommunityNodeFooter.vue';
import { useUsersStore } from '@/stores/users.store';
const i18n = useI18n(); const i18n = useI18n();
const { callDebounced } = useDebounce(); const { callDebounced } = useDebounce();
@@ -34,6 +35,8 @@ const { pushViewStack, popViewStack, updateCurrentViewStack } = useViewStacks();
const { setActiveItemIndex, attachKeydownEvent, detachKeydownEvent } = useKeyboardNavigation(); const { setActiveItemIndex, attachKeydownEvent, detachKeydownEvent } = useKeyboardNavigation();
const nodeCreatorStore = useNodeCreatorStore(); const nodeCreatorStore = useNodeCreatorStore();
const { isInstanceOwner } = useUsersStore();
const activeViewStack = computed(() => useViewStacks().activeViewStack); const activeViewStack = computed(() => useViewStacks().activeViewStack);
const communityNodeDetails = computed(() => activeViewStack.value.communityNodeDetails); const communityNodeDetails = computed(() => activeViewStack.value.communityNodeDetails);
@@ -233,6 +236,7 @@ function onBackButton() {
<CommunityNodeFooter <CommunityNodeFooter
v-if="communityNodeDetails && !isCommunityNodeActionsMode" v-if="communityNodeDetails && !isCommunityNodeActionsMode"
:package-name="communityNodeDetails.packageName" :package-name="communityNodeDetails.packageName"
:show-manage="communityNodeDetails.installed && isInstanceOwner"
/> />
</aside> </aside>
</transition> </transition>

View File

@@ -1831,7 +1831,9 @@
"settings": "Settings", "settings": "Settings",
"settings.communityNodes": "Community nodes", "settings.communityNodes": "Community nodes",
"settings.communityNodes.empty.title": "Supercharge your workflows with 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.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.description.no-packages": "Install node packages contributed by our community.",
"settings.communityNodes.empty.installPackageLabel": "Install a community node", "settings.communityNodes.empty.installPackageLabel": "Install a community node",
"settings.communityNodes.npmUnavailable.warning": "To use this feature, please <a href=\"{npmUrl}\" target=\"_blank\" title=\"How to install npm\">install npm</a> and restart n8n.", "settings.communityNodes.npmUnavailable.warning": "To use this feature, please <a href=\"{npmUrl}\" target=\"_blank\" title=\"How to install npm\">install npm</a> and restart n8n.",

View File

@@ -36,7 +36,19 @@ const communityNodesStore = useCommunityNodesStore();
const uiStore = useUIStore(); const uiStore = useUIStore();
const settingsStore = useSettingsStore(); 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(() => { const getEmptyStateDescription = computed(() => {
if (!settingsStore.isUnverifiedPackagesEnabled) {
return i18n.baseText('settings.communityNodes.empty.verified.only.description');
}
const packageCount = communityNodesStore.availablePackageCount; const packageCount = communityNodesStore.availablePackageCount;
return packageCount < PACKAGE_COUNT_THRESHOLD return packageCount < PACKAGE_COUNT_THRESHOLD
@@ -53,9 +65,10 @@ const getEmptyStateDescription = computed(() => {
}); });
}); });
const getEmptyStateButtonText = computed(() => const getEmptyStateButtonText = computed(() => {
i18n.baseText('settings.communityNodes.empty.installPackageLabel'), if (!settingsStore.isUnverifiedPackagesEnabled) return '';
); return i18n.baseText('settings.communityNodes.empty.installPackageLabel');
});
const actionBoxConfig = computed(() => { const actionBoxConfig = computed(() => {
return { return {
@@ -163,9 +176,10 @@ onBeforeUnmount(() => {
:class="$style.actionBoxContainer" :class="$style.actionBoxContainer"
> >
<n8n-action-box <n8n-action-box
:heading="i18n.baseText('settings.communityNodes.empty.title')" :heading="getEmptyStateTitle"
:description="getEmptyStateDescription" :description="getEmptyStateDescription"
:button-text="getEmptyStateButtonText" :button-text="getEmptyStateButtonText"
:button-disabled="!settingsStore.isUnverifiedPackagesEnabled"
:callout-text="actionBoxConfig.calloutText" :callout-text="actionBoxConfig.calloutText"
:callout-theme="actionBoxConfig.calloutTheme" :callout-theme="actionBoxConfig.calloutTheme"
@click:button="onClickEmptyStateButton" @click:button="onClickEmptyStateButton"