refactor(editor): Extract communityNodes code into features (no-changelog) (#20747)

This commit is contained in:
Alex Grozav 2025-10-15 12:17:32 +03:00 committed by GitHub
parent 0fab5ea3d3
commit eb805d2ba7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
43 changed files with 286 additions and 94 deletions

View File

@ -4,8 +4,6 @@ import {
ANNOTATION_TAGS_MANAGER_MODAL_KEY,
CHANGE_PASSWORD_MODAL_KEY,
CHAT_EMBED_MODAL_KEY,
COMMUNITY_PACKAGE_CONFIRM_MODAL_KEY,
COMMUNITY_PACKAGE_INSTALL_MODAL_KEY,
CONTACT_PROMPT_MODAL_KEY,
CREDENTIAL_EDIT_MODAL_KEY,
CREDENTIAL_SELECT_MODAL_KEY,
@ -52,6 +50,10 @@ import {
SOURCE_CONTROL_PULL_MODAL_KEY,
SOURCE_CONTROL_PUSH_MODAL_KEY,
} from '@/features/sourceControl.ee/sourceControl.constants';
import {
COMMUNITY_PACKAGE_CONFIRM_MODAL_KEY,
COMMUNITY_PACKAGE_INSTALL_MODAL_KEY,
} from '@/features/communityNodes/communityNodes.constants';
import { API_KEY_CREATE_OR_EDIT_MODAL_KEY } from '@/features/apiKeys/apiKeys.constants';
import AboutModal from '@/components/AboutModal.vue';
import ActivationModal from '@/components/ActivationModal.vue';
@ -60,8 +62,8 @@ import NewAssistantSessionModal from '@/features/assistant/components/Chat/NewAs
import ChangePasswordModal from '@/features/auth/components/ChangePasswordModal.vue';
import ConfirmPasswordModal from '@/features/auth/components/ConfirmPasswordModal.vue';
import ChatEmbedModal from '@/components/ChatEmbedModal.vue';
import CommunityPackageInstallModal from '@/components/CommunityPackageInstallModal.vue';
import CommunityPackageManageConfirmModal from '@/components/CommunityPackageManageConfirmModal.vue';
import CommunityPackageInstallModal from '@/features/communityNodes/components/CommunityPackageInstallModal.vue';
import CommunityPackageManageConfirmModal from '@/features/communityNodes/components/CommunityPackageManageConfirmModal.vue';
import CommunityPlusEnrollmentModal from '@/features/communityPlus/components/CommunityPlusEnrollmentModal.vue';
import ContactPromptModal from '@/components/ContactPromptModal.vue';
import CredentialEdit from '@/components/CredentialEdit/CredentialEdit.vue';

View File

@ -1,6 +1,6 @@
<script setup lang="ts">
import { useI18n } from '@n8n/i18n';
import CommunityNodeInstallHint from '../Panel/CommunityNodeInstallHint.vue';
import CommunityNodeInstallHint from '@/features/communityNodes/components/nodeCreator/CommunityNodeInstallHint.vue';
import { N8nButton } from '@n8n/design-system';
export interface Props {
isPreview: boolean;

View File

@ -1,12 +1,12 @@
<script setup lang="ts">
import type { SimplifiedNodeType } from '@/Interface';
import {
COMMUNITY_NODES_INSTALLATION_DOCS_URL,
CREDENTIAL_ONLY_NODE_PREFIX,
DEFAULT_SUBCATEGORY,
DRAG_EVENT_DATA_KEY,
HITL_SUBCATEGORY,
} from '@/constants';
import { COMMUNITY_NODES_INSTALLATION_DOCS_URL } from '@/features/communityNodes/communityNodes.constants';
import { computed, ref } from 'vue';
import NodeIcon from '@/components/NodeIcon.vue';

View File

@ -31,8 +31,8 @@ import { useNodeCreatorStore } from '@/stores/nodeCreator.store';
import OrderSwitcher from './../OrderSwitcher.vue';
import { getActiveViewCallouts, isNodePreviewKey } from '../utils';
import CommunityNodeInfo from '../Panel/CommunityNodeInfo.vue';
import CommunityNodeFooter from '../Panel/CommunityNodeFooter.vue';
import CommunityNodeInfo from '@/features/communityNodes/components/nodeCreator/CommunityNodeInfo.vue';
import CommunityNodeFooter from '@/features/communityNodes/components/nodeCreator/CommunityNodeFooter.vue';
import { useCalloutHelpers } from '@/composables/useCalloutHelpers';
import { N8nCallout, N8nInfoTip } from '@n8n/design-system';

View File

@ -1,5 +1,5 @@
<script setup lang="ts">
import { useInstallNode } from '@/composables/useInstallNode';
import { useInstallNode } from '@/features/communityNodes/composables/useInstallNode';
import { useNodeCreatorStore } from '@/stores/nodeCreator.store';
import { useUsersStore } from '@/features/users/users.store';
import { getNodeIconSource } from '@/utils/nodeIcon';

View File

@ -22,10 +22,10 @@ import { useI18n } from '@n8n/i18n';
import { useDebounce } from '@/composables/useDebounce';
import NodeIcon from '@/components/NodeIcon.vue';
import CommunityNodeDetails from './CommunityNodeDetails.vue';
import CommunityNodeInfo from './CommunityNodeInfo.vue';
import CommunityNodeDocsLink from './CommunityNodeDocsLink.vue';
import CommunityNodeFooter from './CommunityNodeFooter.vue';
import CommunityNodeDetails from '@/features/communityNodes/components/nodeCreator/CommunityNodeDetails.vue';
import CommunityNodeInfo from '@/features/communityNodes/components/nodeCreator/CommunityNodeInfo.vue';
import CommunityNodeDocsLink from '@/features/communityNodes/components/nodeCreator/CommunityNodeDocsLink.vue';
import CommunityNodeFooter from '@/features/communityNodes/components/nodeCreator/CommunityNodeFooter.vue';
import { useUsersStore } from '@/features/users/users.store';
import { N8nIcon, N8nNotice } from '@n8n/design-system';

View File

@ -10,7 +10,7 @@ import ItemsRenderer from './ItemsRenderer.vue';
import CategoryItem from '../ItemTypes/CategoryItem.vue';
import { useNodeCreatorStore } from '@/stores/nodeCreator.store';
import CommunityNodeInstallHint from '../Panel/CommunityNodeInstallHint.vue';
import CommunityNodeInstallHint from '@/features/communityNodes/components/nodeCreator/CommunityNodeInstallHint.vue';
export interface Props {
elements: INodeCreateElement[];

View File

@ -29,7 +29,7 @@ import FreeAiCreditsCallout from '@/components/FreeAiCreditsCallout.vue';
import NodeActionsList from '@/components/NodeActionsList.vue';
import NodeSettingsInvalidNodeWarning from '@/components/NodeSettingsInvalidNodeWarning.vue';
import { useExternalHooks } from '@/composables/useExternalHooks';
import { useInstalledCommunityPackage } from '@/composables/useInstalledCommunityPackage';
import { useInstalledCommunityPackage } from '@/features/communityNodes/composables/useInstalledCommunityPackage';
import { useNodeCredentialOptions } from '@/composables/useNodeCredentialOptions';
import { useNodeHelpers } from '@/composables/useNodeHelpers';
import { useNodeSettingsParameters } from '@/composables/useNodeSettingsParameters';
@ -56,8 +56,8 @@ import {
import { useI18n } from '@n8n/i18n';
import type { EventBus } from '@n8n/utils/event-bus';
import { useResizeObserver } from '@vueuse/core';
import CommunityNodeFooter from '@/components/Node/NodeCreator/Panel/CommunityNodeFooter.vue';
import CommunityNodeUpdateInfo from '@/components/Node/NodeCreator/Panel/CommunityNodeUpdateInfo.vue';
import CommunityNodeFooter from '@/features/communityNodes/components/nodeCreator/CommunityNodeFooter.vue';
import CommunityNodeUpdateInfo from '@/features/communityNodes/components/nodeCreator/CommunityNodeUpdateInfo.vue';
import NodeExecuteButton from './NodeExecuteButton.vue';
import { N8nBlockUi, N8nIcon, N8nNotice, N8nText } from '@n8n/design-system';

View File

@ -1,7 +1,7 @@
import { mockNode } from '@/__tests__/mocks';
import { renderComponent } from '@/__tests__/render';
import { mockedStore, type MockedStore } from '@/__tests__/utils';
import { useInstallNode } from '@/composables/useInstallNode';
import { useInstallNode } from '@/features/communityNodes/composables/useInstallNode';
import { type NodeTypesByTypeNameAndVersion } from '@/Interface';
import { useNDVStore } from '@/stores/ndv.store';
import { useNodeCreatorStore } from '@/stores/nodeCreator.store';
@ -16,7 +16,7 @@ import { vi, type MockedFunction } from 'vitest';
import { ref } from 'vue';
import NodeSettingsInvalidNodeWarning from './NodeSettingsInvalidNodeWarning.vue';
vi.mock('@/composables/useInstallNode');
vi.mock('@/features/communityNodes/composables/useInstallNode');
vi.mock('@/composables/useTelemetry', () => ({
useTelemetry: () => ({
track: vi.fn(),

View File

@ -1,7 +1,8 @@
<script setup lang="ts">
import { useInstallNode } from '@/composables/useInstallNode';
import { useInstallNode } from '@/features/communityNodes/composables/useInstallNode';
import { useTelemetry } from '@/composables/useTelemetry';
import { COMMUNITY_PACKAGE_INSTALL_MODAL_KEY, CUSTOM_NODES_DOCS_URL } from '@/constants';
import { CUSTOM_NODES_DOCS_URL } from '@/constants';
import { COMMUNITY_PACKAGE_INSTALL_MODAL_KEY } from '@/features/communityNodes/communityNodes.constants';
import type { INodeUi } from '@/Interface';
import { useNDVStore } from '@/stores/ndv.store';
import { useNodeCreatorStore } from '@/stores/nodeCreator.store';
@ -13,7 +14,7 @@ import { useI18n } from '@n8n/i18n';
import { isCommunityPackageName } from 'n8n-workflow';
import { computed, watch } from 'vue';
import { I18nT } from 'vue-i18n';
import ContactAdministratorToInstall from './ContactAdministratorToInstall.vue';
import ContactAdministratorToInstall from '@/features/communityNodes/components/ContactAdministratorToInstall.vue';
import { removePreviewToken } from './Node/NodeCreator/utils';
const { node, previewMode = false } = defineProps<{ node: INodeUi; previewMode?: boolean }>();

View File

@ -4,12 +4,12 @@ import { createComponentRenderer } from '@/__tests__/render';
import { createTestingPinia } from '@pinia/testing';
import NodeSettingsTabs from '@/components/NodeSettingsTabs.vue';
import { ref } from 'vue';
import type { ExtendedPublicInstalledPackage } from '@/utils/communityNodeUtils';
import { useInstalledCommunityPackage } from '@/composables/useInstalledCommunityPackage';
import type { ExtendedPublicInstalledPackage } from '@/features/communityNodes/communityNodes.utils';
import { useInstalledCommunityPackage } from '@/features/communityNodes/composables/useInstalledCommunityPackage';
const renderComponent = createComponentRenderer(NodeSettingsTabs);
vi.mock('@/composables/useInstalledCommunityPackage', () => ({
vi.mock('@/features/communityNodes/composables/useInstalledCommunityPackage', () => ({
useInstalledCommunityPackage: vi.fn(() => ({
installedPackage: ref<ExtendedPublicInstalledPackage | undefined>(undefined),
isCommunityNode: ref(false),

View File

@ -1,6 +1,6 @@
<script setup lang="ts">
import type { ITab } from '@/Interface';
import { COMMUNITY_NODES_INSTALLATION_DOCS_URL } from '@/constants';
import { COMMUNITY_NODES_INSTALLATION_DOCS_URL } from '@/features/communityNodes/communityNodes.constants';
import { useNDVStore } from '@/stores/ndv.store';
import { useWorkflowsStore } from '@/stores/workflows.store';
import type { INodeTypeDescription } from 'n8n-workflow';
@ -8,7 +8,7 @@ import { NodeConnectionTypes } from 'n8n-workflow';
import { computed } from 'vue';
import { useExternalHooks } from '@/composables/useExternalHooks';
import { useInstalledCommunityPackage } from '@/composables/useInstalledCommunityPackage';
import { useInstalledCommunityPackage } from '@/features/communityNodes/composables/useInstalledCommunityPackage';
import { useNodeDocsUrl } from '@/composables/useNodeDocsUrl';
import { useTelemetry } from '@/composables/useTelemetry';
import type { NodeSettingsTab } from '@/types/nodeSettings';

View File

@ -64,8 +64,6 @@ export const CONTACT_PROMPT_MODAL_KEY = 'contactPrompt';
export const NODE_PINNING_MODAL_KEY = 'nodePinning';
export const NPS_SURVEY_MODAL_KEY = 'npsSurvey';
export const WORKFLOW_ACTIVE_MODAL_KEY = 'activation';
export const COMMUNITY_PACKAGE_INSTALL_MODAL_KEY = 'communityPackageInstall';
export const COMMUNITY_PACKAGE_CONFIRM_MODAL_KEY = 'communityPackageManageConfirm';
export const IMPORT_CURL_MODAL_KEY = 'importCurl';
export const LOG_STREAM_MODAL_KEY = 'settingsLogStream';
export const DEBUG_PAYWALL_MODAL_KEY = 'debugPaywall';
@ -88,12 +86,6 @@ export const CHAT_HUB_SIDE_MENU_DRAWER_MODAL_KEY = 'chatHubSideMenuDrawer';
export const EXPERIMENT_TEMPLATE_RECO_V2_KEY = 'templateRecoV2';
export const EXPERIMENT_TEMPLATE_RECO_V3_KEY = 'templateRecoV3';
export const COMMUNITY_PACKAGE_MANAGE_ACTIONS = {
UNINSTALL: 'uninstall',
UPDATE: 'update',
VIEW_DOCS: 'view-documentation',
};
// breakpoints
export const BREAKPOINT_SM = 768;
export const BREAKPOINT_MD = 992;
@ -108,12 +100,7 @@ export const DATA_EDITING_DOCS_URL = `https://${DOCS_DOMAIN}/data/data-editing/`
export const SCHEMA_PREVIEW_DOCS_URL = `https://${DOCS_DOMAIN}/data/schema-preview/`;
export const MFA_DOCS_URL = `https://${DOCS_DOMAIN}/user-management/two-factor-auth/`;
export const NPM_PACKAGE_DOCS_BASE_URL = 'https://www.npmjs.com/package/';
export const NPM_KEYWORD_SEARCH_URL =
'https://www.npmjs.com/search?q=keywords%3An8n-community-node-package';
export const N8N_QUEUE_MODE_DOCS_URL = `https://${DOCS_DOMAIN}/hosting/scaling/queue-mode/`;
export const COMMUNITY_NODES_INSTALLATION_DOCS_URL = `https://${DOCS_DOMAIN}/integrations/community-nodes/installation/gui-install/`;
export const COMMUNITY_NODES_RISKS_DOCS_URL = `https://${DOCS_DOMAIN}/integrations/community-nodes/risks/`;
export const COMMUNITY_NODES_BLOCKLIST_DOCS_URL = `https://${DOCS_DOMAIN}/integrations/community-nodes/blocklist/`;
export const CUSTOM_NODES_DOCS_URL = `https://${DOCS_DOMAIN}/integrations/creating-nodes/code/create-n8n-nodes-module/`;
export const EXPRESSIONS_DOCS_URL = `https://${DOCS_DOMAIN}/code-examples/expressions/`;
export const EVALUATIONS_DOCS_URL = `https://${DOCS_DOMAIN}/advanced-ai/evaluations/overview/`;

View File

@ -0,0 +1,16 @@
import { DOCS_DOMAIN } from '@/constants';
export const COMMUNITY_PACKAGE_INSTALL_MODAL_KEY = 'communityPackageInstall';
export const COMMUNITY_PACKAGE_CONFIRM_MODAL_KEY = 'communityPackageManageConfirm';
export const COMMUNITY_NODES_INSTALLATION_DOCS_URL = `https://${DOCS_DOMAIN}/integrations/community-nodes/installation/gui-install/`;
export const COMMUNITY_NODES_RISKS_DOCS_URL = `https://${DOCS_DOMAIN}/integrations/community-nodes/risks/`;
export const COMMUNITY_NODES_BLOCKLIST_DOCS_URL = `https://${DOCS_DOMAIN}/integrations/community-nodes/blocklist/`;
export const NPM_KEYWORD_SEARCH_URL =
'https://www.npmjs.com/search?q=keywords%3An8n-community-node-package';
export const COMMUNITY_PACKAGE_MANAGE_ACTIONS = {
UNINSTALL: 'uninstall',
UPDATE: 'update',
VIEW_DOCS: 'view-documentation',
};

View File

@ -5,7 +5,7 @@ import type { PublicInstalledPackage } from 'n8n-workflow';
import { STORES } from '@n8n/stores';
import { computed, ref } from 'vue';
import { isAuthenticated } from '@/utils/rbac/checks';
import type { CommunityPackageMap } from '@/Interface';
import type { CommunityPackageMap } from './communityNodes.types';
const LOADER_DELAY = 300;

View File

@ -0,0 +1,5 @@
import type { PublicInstalledPackage } from 'n8n-workflow';
export interface CommunityPackageMap {
[name: string]: PublicInstalledPackage;
}

View File

@ -1,12 +1,12 @@
import { describe, it, expect, vi } from 'vitest';
import { fetchInstalledPackageInfo } from './communityNodeUtils';
import { useCommunityNodesStore } from '@/stores/communityNodes.store';
import { fetchInstalledPackageInfo } from './communityNodes.utils';
import { useCommunityNodesStore } from './communityNodes.store';
import { type NodeTypesStore, useNodeTypesStore } from '@/stores/nodeTypes.store';
import type { PublicInstalledPackage } from 'n8n-workflow';
import type { CommunityNodeType } from '@n8n/api-types';
import { createTestingPinia } from '@pinia/testing';
vi.mock('@/stores/communityNodes.store', () => ({
vi.mock('./communityNodes.store', () => ({
useCommunityNodesStore: vi.fn(() => ({
getInstalledPackage: vi.fn(),
})),

View File

@ -1,4 +1,4 @@
import { useCommunityNodesStore } from '@/stores/communityNodes.store';
import { useCommunityNodesStore } from './communityNodes.store';
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
import { type PublicInstalledPackage } from 'n8n-workflow';
import semver from 'semver';

View File

@ -1,7 +1,8 @@
<script lang="ts" setup>
import { useUIStore } from '@/stores/ui.store';
import type { IUser, PublicInstalledPackage } from 'n8n-workflow';
import { NPM_PACKAGE_DOCS_BASE_URL, COMMUNITY_PACKAGE_MANAGE_ACTIONS } from '@/constants';
import { COMMUNITY_PACKAGE_MANAGE_ACTIONS } from '../communityNodes.constants';
import { NPM_PACKAGE_DOCS_BASE_URL } from '@/constants';
import { useI18n } from '@n8n/i18n';
import { useTelemetry } from '@/composables/useTelemetry';
import { useSettingsStore } from '@/stores/settings.store';

View File

@ -1,7 +1,7 @@
import { createComponentRenderer } from '@/__tests__/render';
import { retry } from '@/__tests__/utils';
import { useInstallNode } from '@/composables/useInstallNode';
import { COMMUNITY_PACKAGE_INSTALL_MODAL_KEY } from '@/constants';
import { useInstallNode } from '../composables/useInstallNode';
import { COMMUNITY_PACKAGE_INSTALL_MODAL_KEY } from '../communityNodes.constants';
import { STORES } from '@n8n/stores';
import { createTestingPinia } from '@pinia/testing';
import userEvent from '@testing-library/user-event';
@ -9,7 +9,7 @@ import { vi, type MockedFunction } from 'vitest';
import { ref } from 'vue';
import CommunityPackageInstallModal from './CommunityPackageInstallModal.vue';
vi.mock('@/composables/useInstallNode');
vi.mock('../composables/useInstallNode');
vi.mock('@/composables/useTelemetry', () => ({
useTelemetry: () => ({
track: vi.fn(),

View File

@ -1,13 +1,13 @@
<script setup lang="ts">
import Modal from '@/components/Modal.vue';
import { useInstallNode } from '@/composables/useInstallNode';
import { useInstallNode } from '../composables/useInstallNode';
import { useTelemetry } from '@/composables/useTelemetry';
import {
COMMUNITY_PACKAGE_INSTALL_MODAL_KEY,
COMMUNITY_NODES_INSTALLATION_DOCS_URL,
COMMUNITY_NODES_RISKS_DOCS_URL,
COMMUNITY_PACKAGE_INSTALL_MODAL_KEY,
NPM_KEYWORD_SEARCH_URL,
} from '@/constants';
} from '../communityNodes.constants';
import { useUIStore } from '@/stores/ui.store';
import { useI18n } from '@n8n/i18n';
import { createEventBus } from '@n8n/utils/event-bus';

View File

@ -7,7 +7,7 @@ import { defaultSettings } from '@/__tests__/defaults';
import { mockNodeTypeDescription } from '@/__tests__/mocks';
import { createTestingPinia } from '@pinia/testing';
import { STORES } from '@n8n/stores';
import { COMMUNITY_PACKAGE_CONFIRM_MODAL_KEY } from '@/constants';
import { COMMUNITY_PACKAGE_CONFIRM_MODAL_KEY } from '../communityNodes.constants';
const fetchWorkflowsWithNodesIncluded = vi.fn();
vi.mock('@/stores/workflows.store', () => ({

View File

@ -1,8 +1,11 @@
<script setup lang="ts">
import Modal from '@/components/Modal.vue';
import { COMMUNITY_PACKAGE_CONFIRM_MODAL_KEY, COMMUNITY_PACKAGE_MANAGE_ACTIONS } from '@/constants';
import {
COMMUNITY_PACKAGE_CONFIRM_MODAL_KEY,
COMMUNITY_PACKAGE_MANAGE_ACTIONS,
} from '../communityNodes.constants';
import { useToast } from '@/composables/useToast';
import { useCommunityNodesStore } from '@/stores/communityNodes.store';
import { useCommunityNodesStore } from '../communityNodes.store';
import { createEventBus } from '@n8n/utils/event-bus';
import { useI18n } from '@n8n/i18n';
import { useTelemetry } from '@/composables/useTelemetry';
@ -16,7 +19,7 @@ import { useWorkflowsStore } from '@/stores/workflows.store';
import type { WorkflowResource } from '@/Interface';
import { N8nButton, N8nNotice, N8nText } from '@n8n/design-system';
import NodesInWorkflowTable from '@/components/NodesInWorkflowTable.vue';
import NodesInWorkflowTable from './NodesInWorkflowTable.vue';
export type CommunityPackageManageMode = 'uninstall' | 'update' | 'view-documentation';

View File

@ -1,5 +1,5 @@
import { createComponentRenderer } from '@/__tests__/render';
import NodesInWorkflowTable from '@/components/NodesInWorkflowTable.vue';
import NodesInWorkflowTable from './NodesInWorkflowTable.vue';
import { describe, it, expect } from 'vitest';
import { screen } from '@testing-library/vue';
import { createTestingPinia } from '@pinia/testing';

View File

@ -61,7 +61,7 @@ vi.mock('@/stores/nodeTypes.store', () => ({
})),
}));
vi.mock('@/stores/communityNodes.store', () => ({
vi.mock('../../communityNodes.store', () => ({
useCommunityNodesStore: vi.fn(() => ({
installPackage,
})),
@ -78,7 +78,7 @@ vi.mock('@/composables/useToast', () => ({
})),
}));
vi.mock('../composables/useViewStacks', () => ({
vi.mock('@/components/Node/NodeCreator/composables/useViewStacks', () => ({
useViewStacks: vi.fn(() => ({
activeViewStack: {
communityNodeDetails: {

View File

@ -0,0 +1,169 @@
<script setup lang="ts">
import { useInstallNode } from '@/features/communityNodes/composables/useInstallNode';
import { useNodeCreatorStore } from '@/stores/nodeCreator.store';
import { useUsersStore } from '@/features/users/users.store';
import { getNodeIconSource } from '@/utils/nodeIcon';
import { N8nButton, N8nIcon, N8nText, N8nTooltip } from '@n8n/design-system';
import { i18n } from '@n8n/i18n';
import OfficialIcon from 'virtual:icons/mdi/verified';
import { computed } from 'vue';
import { useViewStacks } from '@/components/Node/NodeCreator/composables/useViewStacks';
import {
prepareCommunityNodeDetailsViewStack,
removePreviewToken,
} from '@/components/Node/NodeCreator/utils';
import NodeIcon from '@/components/NodeIcon.vue';
const {
activeViewStack,
pushViewStack,
popViewStack,
getAllNodeCreateElements,
updateCurrentViewStack,
} = useViewStacks();
const { communityNodeDetails } = activeViewStack;
const nodeCreatorStore = useNodeCreatorStore();
const { installNode, loading } = useInstallNode();
const isOwner = computed(() => useUsersStore().isInstanceOwner);
const updateViewStack = (key: string) => {
const installedNodeKey = removePreviewToken(key);
const installedNode = getAllNodeCreateElements().find((node) => node.key === installedNodeKey);
if (installedNode) {
const nodeActions = nodeCreatorStore.actions?.[installedNode.key] || [];
popViewStack();
updateCurrentViewStack({ searchItems: nodeCreatorStore.mergedNodes });
const viewStack = prepareCommunityNodeDetailsViewStack(
installedNode,
getNodeIconSource(installedNode.properties),
activeViewStack.rootView,
nodeActions,
);
pushViewStack(viewStack, {
transitionDirection: 'none',
});
} else {
const viewStack = { ...activeViewStack };
viewStack.communityNodeDetails!.installed = true;
pushViewStack(activeViewStack, { resetStacks: true });
}
};
const updateStoresAndViewStack = (key: string) => {
updateViewStack(key);
nodeCreatorStore.removeNodeFromMergedNodes(key);
};
const onInstall = async () => {
if (isOwner.value && activeViewStack.communityNodeDetails && !communityNodeDetails?.installed) {
const { key, packageName } = activeViewStack.communityNodeDetails;
const result = await installNode({ type: 'verified', packageName, nodeType: key });
if (result.success) {
updateStoresAndViewStack(key);
}
}
};
</script>
<template>
<div v-if="communityNodeDetails" :class="$style.container">
<div :class="$style.header">
<div :class="$style.title">
<NodeIcon
v-if="communityNodeDetails.nodeIcon"
:class="$style.nodeIcon"
:icon-source="communityNodeDetails.nodeIcon"
:circle="false"
:show-tooltip="false"
/>
<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">
<N8nIcon v-if="!communityNodeDetails.official" :class="$style.installedIcon" icon="box" />
<N8nText color="text-light" size="small" bold>
{{ i18n.baseText('communityNodeDetails.installed') }}
</N8nText>
</div>
<N8nButton
v-if="isOwner && !communityNodeDetails.installed"
:loading="loading"
:disabled="loading"
:label="i18n.baseText('communityNodeDetails.install')"
size="small"
data-test-id="install-community-node-button"
@click="onInstall"
/>
</div>
</div>
</div>
</template>
<style lang="scss" module>
.container {
width: 100%;
padding: var(--spacing--sm);
display: flex;
flex-direction: column;
padding-bottom: var(--spacing--xs);
}
.header {
display: flex;
gap: var(--spacing--2xs);
align-items: center;
justify-content: space-between;
}
.title {
display: flex;
align-items: center;
color: var(--color-text);
font-size: var(--font-size--xl);
font-weight: var(--font-weight--bold);
}
.nodeIcon {
--node--icon--size: 36px;
margin-right: var(--spacing--sm);
}
.installedIcon {
margin-right: var(--spacing--3xs);
color: var(--color--text);
font-size: var(--font-size--2xs);
}
.officialIcon {
display: inline-flex;
flex-shrink: 0;
margin-left: var(--spacing--4xs);
color: var(--color--text);
width: 14px;
}
.installed {
display: flex;
align-items: center;
margin-right: var(--spacing--xs);
}
</style>

View File

@ -6,12 +6,12 @@ import CommunityNodeFooter from './CommunityNodeFooter.vue';
import { createComponentRenderer } from '@/__tests__/render';
import { vi } from 'vitest';
import { ref } from 'vue';
import type { ExtendedPublicInstalledPackage } from '@/utils/communityNodeUtils';
import type { ExtendedPublicInstalledPackage } from '../../communityNodes.utils';
// Mock the useInstalledCommunityPackage composable
const mockInstalledPackage = ref<ExtendedPublicInstalledPackage | undefined>(undefined);
vi.mock('@/composables/useInstalledCommunityPackage', () => ({
vi.mock('../../composables/useInstalledCommunityPackage', () => ({
useInstalledCommunityPackage: vi.fn(() => ({
installedPackage: mockInstalledPackage,
isUpdateCheckAvailable: ref(false),

View File

@ -4,7 +4,7 @@ import { captureException } from '@sentry/vue';
import { onMounted, ref } from 'vue';
import { useRouter } from 'vue-router';
import { useInstalledCommunityPackage } from '@/composables/useInstalledCommunityPackage';
import { useInstalledCommunityPackage } from '../../composables/useInstalledCommunityPackage';
import { i18n } from '@n8n/i18n';
import { N8nLink, N8nText } from '@n8n/design-system';

View File

@ -1,11 +1,11 @@
import { createComponentRenderer } from '@/__tests__/render';
import { useInstalledCommunityPackage } from '@/composables/useInstalledCommunityPackage';
import type { ExtendedPublicInstalledPackage } from '@/utils/communityNodeUtils';
import { useInstalledCommunityPackage } from '../../composables/useInstalledCommunityPackage';
import type { ExtendedPublicInstalledPackage } from '../../communityNodes.utils';
import { type TestingPinia, createTestingPinia } from '@pinia/testing';
import { waitFor } from '@testing-library/vue';
import { setActivePinia } from 'pinia';
import { type ComputedRef, ref } from 'vue';
import type { CommunityNodeDetails } from '../composables/useViewStacks';
import type { CommunityNodeDetails } from '@/components/Node/NodeCreator/composables/useViewStacks';
import CommunityNodeInfo from './CommunityNodeInfo.vue';
vi.mock('./utils', () => ({
@ -26,7 +26,7 @@ const defaultUseInstalledCommunityPackage = {
initInstalledPackage: vi.fn(),
} as unknown as ReturnType<typeof useInstalledCommunityPackage>;
vi.mock('@/composables/useInstalledCommunityPackage', () => ({
vi.mock('../../composables/useInstalledCommunityPackage', () => ({
useInstalledCommunityPackage: vi.fn(() => defaultUseInstalledCommunityPackage),
}));
@ -44,7 +44,7 @@ vi.mock('@/features/users/users.store', () => ({
})),
}));
vi.mock('../composables/useViewStacks', () => ({
vi.mock('@/components/Node/NodeCreator/composables/useViewStacks', () => ({
useViewStacks: vi.fn(),
}));
@ -94,7 +94,9 @@ describe('CommunityNodeInfo', () => {
originalFetch = global.fetch;
global.fetch = vi.fn();
const { useViewStacks } = await import('../composables/useViewStacks');
const { useViewStacks } = await import(
'@/components/Node/NodeCreator/composables/useViewStacks'
);
vi.mocked(useViewStacks).mockReturnValue({
activeViewStack: defaultViewStack,
} as ReturnType<typeof useViewStacks>);
@ -135,7 +137,9 @@ describe('CommunityNodeInfo', () => {
});
it('should display update notice, should show verified badge for older versions', async () => {
const { useViewStacks } = await import('../composables/useViewStacks');
const { useViewStacks } = await import(
'@/components/Node/NodeCreator/composables/useViewStacks'
);
vi.mocked(useViewStacks).mockReturnValue({
activeViewStack: {
...defaultViewStack,
@ -179,7 +183,9 @@ describe('CommunityNodeInfo', () => {
});
it('should NOT display update notice for unverified update', async () => {
const { useViewStacks } = await import('../composables/useViewStacks');
const { useViewStacks } = await import(
'@/components/Node/NodeCreator/composables/useViewStacks'
);
vi.mocked(useViewStacks).mockReturnValue({
activeViewStack: {
...defaultViewStack,

View File

@ -1,16 +1,16 @@
<script setup lang="ts">
import { computed, onMounted, ref } from 'vue';
import { useViewStacks } from '../composables/useViewStacks';
import { useViewStacks } from '@/components/Node/NodeCreator/composables/useViewStacks';
import { useUsersStore } from '@/features/users/users.store';
import { i18n } from '@n8n/i18n';
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
import { captureException } from '@sentry/vue';
import ShieldIcon from 'virtual:icons/fa-solid/shield-alt';
import ContactAdministratorToInstall from '@/components/ContactAdministratorToInstall.vue';
import { useInstalledCommunityPackage } from '@/composables/useInstalledCommunityPackage';
import ContactAdministratorToInstall from '../ContactAdministratorToInstall.vue';
import { useInstalledCommunityPackage } from '../../composables/useInstalledCommunityPackage';
import { N8nIcon, N8nText, N8nTooltip } from '@n8n/design-system';
import CommunityNodeUpdateInfo from '@/components/Node/NodeCreator/Panel/CommunityNodeUpdateInfo.vue';
import CommunityNodeUpdateInfo from './CommunityNodeUpdateInfo.vue';
const { activeViewStack } = useViewStacks();

View File

@ -1,6 +1,6 @@
import { removePreviewToken } from '@/components/Node/NodeCreator/utils';
import type { IWorkflowDb } from '@/Interface';
import { useCommunityNodesStore } from '@/stores/communityNodes.store';
import { useCommunityNodesStore } from '../communityNodes.store';
import { useCredentialsStore } from '@/stores/credentials.store';
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
import { useUsersStore } from '@/features/users/users.store';
@ -9,9 +9,9 @@ import type { CommunityNodeType } from '@n8n/api-types';
import { createTestingPinia } from '@pinia/testing';
import type { INode } from 'n8n-workflow';
import { setActivePinia } from 'pinia';
import { useCanvasOperations } from './useCanvasOperations';
import { useCanvasOperations } from '@/composables/useCanvasOperations';
import { useInstallNode } from './useInstallNode';
import { useToast } from './useToast';
import { useToast } from '@/composables/useToast';
vi.mock('@/composables/useCanvasOperations', () => ({
useCanvasOperations: vi.fn().mockReturnValue({
@ -19,7 +19,7 @@ vi.mock('@/composables/useCanvasOperations', () => ({
}),
}));
vi.mock('./useToast', () => ({
vi.mock('@/composables/useToast', () => ({
useToast: vi.fn().mockReturnValue({
showError: vi.fn(),
showMessage: vi.fn(),

View File

@ -1,12 +1,12 @@
import { useCommunityNodesStore } from '@/stores/communityNodes.store';
import { useCommunityNodesStore } from '../communityNodes.store';
import { useCredentialsStore } from '@/stores/credentials.store';
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
import { useUsersStore } from '@/features/users/users.store';
import { computed, nextTick, ref } from 'vue';
import { i18n } from '@n8n/i18n';
import { useToast } from './useToast';
import { useToast } from '@/composables/useToast';
import { useWorkflowsStore } from '@/stores/workflows.store';
import { useCanvasOperations } from './useCanvasOperations';
import { useCanvasOperations } from '@/composables/useCanvasOperations';
import { removePreviewToken } from '@/components/Node/NodeCreator/utils';
type InstallNodeProps = {

View File

@ -5,9 +5,9 @@ import { nextTick } from 'vue';
import { mockedStore } from '@/__tests__/utils';
import { useInstalledCommunityPackage } from './useInstalledCommunityPackage';
import { useCommunityNodesStore } from '@/stores/communityNodes.store';
import { useCommunityNodesStore } from '../communityNodes.store';
import { useUsersStore } from '@/features/users/users.store';
import type { ExtendedPublicInstalledPackage } from '@/utils/communityNodeUtils';
import type { ExtendedPublicInstalledPackage } from '../communityNodes.utils';
import type * as n8nWorkflow from 'n8n-workflow';
vi.mock('n8n-workflow', async (importOriginal) => {
@ -18,13 +18,13 @@ vi.mock('n8n-workflow', async (importOriginal) => {
};
});
vi.mock('@/utils/communityNodeUtils', () => ({
vi.mock('../communityNodes.utils', () => ({
fetchInstalledPackageInfo: vi.fn(),
}));
// Import mocked functions
import { isCommunityPackageName } from 'n8n-workflow';
import { fetchInstalledPackageInfo } from '@/utils/communityNodeUtils';
import { fetchInstalledPackageInfo } from '../communityNodes.utils';
const mockIsCommunityPackageName = vi.mocked(isCommunityPackageName);
const mockFetchInstalledPackageInfo = vi.mocked(fetchInstalledPackageInfo);

View File

@ -1,10 +1,10 @@
import { useCommunityNodesStore } from '@/stores/communityNodes.store';
import { useCommunityNodesStore } from '../communityNodes.store';
import { useUsersStore } from '@/features/users/users.store';
import { isCommunityPackageName } from 'n8n-workflow';
import {
type ExtendedPublicInstalledPackage,
fetchInstalledPackageInfo,
} from '@/utils/communityNodeUtils';
} from '../communityNodes.utils';
import { computed, type MaybeRefOrGetter, onMounted, ref, watch, toValue } from 'vue';
export function useInstalledCommunityPackage(nodeTypeName?: MaybeRefOrGetter<string | undefined>) {

View File

@ -2,13 +2,13 @@
import {
COMMUNITY_PACKAGE_INSTALL_MODAL_KEY,
COMMUNITY_NODES_INSTALLATION_DOCS_URL,
} from '@/constants';
import CommunityPackageCard from '@/components/CommunityPackageCard.vue';
} from '../communityNodes.constants';
import CommunityPackageCard from '../components/CommunityPackageCard.vue';
import { useToast } from '@/composables/useToast';
import { useDocumentTitle } from '@/composables/useDocumentTitle';
import type { PublicInstalledPackage } from 'n8n-workflow';
import { useCommunityNodesStore } from '@/stores/communityNodes.store';
import { useCommunityNodesStore } from '../communityNodes.store';
import { useUIStore } from '@/stores/ui.store';
import { onBeforeUnmount, ref, computed, onBeforeMount, onMounted } from 'vue';
import { useExternalHooks } from '@/composables/useExternalHooks';

View File

@ -44,7 +44,7 @@ const SettingsPersonalView = async () =>
await import('@/features/auth/views/SettingsPersonalView.vue');
const SettingsUsersView = async () => await import('@/features/users/views/SettingsUsersView.vue');
const SettingsCommunityNodesView = async () =>
await import('./views/SettingsCommunityNodesView.vue');
await import('@/features/communityNodes/views/SettingsCommunityNodesView.vue');
const SettingsApiView = async () => await import('@/features/apiKeys/views/SettingsApiView.vue');
const SettingsLogStreamingView = async () =>
await import('@/features/logStreaming.ee/views/SettingsLogStreamingView.vue');

View File

@ -2,9 +2,6 @@ import {
ABOUT_MODAL_KEY,
CHAT_EMBED_MODAL_KEY,
CHANGE_PASSWORD_MODAL_KEY,
COMMUNITY_PACKAGE_CONFIRM_MODAL_KEY,
COMMUNITY_PACKAGE_INSTALL_MODAL_KEY,
COMMUNITY_PACKAGE_MANAGE_ACTIONS,
CONTACT_PROMPT_MODAL_KEY,
CREDENTIAL_EDIT_MODAL_KEY,
CREDENTIAL_SELECT_MODAL_KEY,
@ -55,6 +52,11 @@ import {
SOURCE_CONTROL_PUSH_MODAL_KEY,
SOURCE_CONTROL_PULL_MODAL_KEY,
} from '@/features/sourceControl.ee/sourceControl.constants';
import {
COMMUNITY_PACKAGE_CONFIRM_MODAL_KEY,
COMMUNITY_PACKAGE_INSTALL_MODAL_KEY,
COMMUNITY_PACKAGE_MANAGE_ACTIONS,
} from '@/features/communityNodes/communityNodes.constants';
import { API_KEY_CREATE_OR_EDIT_MODAL_KEY } from '@/features/apiKeys/apiKeys.constants';
import { STORES } from '@n8n/stores';
import type {