diff --git a/packages/frontend/@n8n/design-system/src/components/N8nTabs/Tabs.vue b/packages/frontend/@n8n/design-system/src/components/N8nTabs/Tabs.vue
index f0dda8872b..0104b138a8 100644
--- a/packages/frontend/@n8n/design-system/src/components/N8nTabs/Tabs.vue
+++ b/packages/frontend/@n8n/design-system/src/components/N8nTabs/Tabs.vue
@@ -116,7 +116,10 @@ const scrollRight = () => scroll(50);
@click="() => handleTabClick(option.value)"
>
- {{ option.label }}
+ {{ option.label }}
+
@@ -211,6 +214,26 @@ const scrollRight = () => scroll(50);
font-weight: var(--font-weight-bold);
}
+.notificationContainer {
+ display: flex;
+ position: relative;
+}
+
+.notification {
+ display: flex;
+ position: absolute;
+ right: -0.5em;
+ align-items: center;
+ justify-content: center;
+
+ div {
+ height: 0.3em;
+ width: 0.3em;
+ background-color: var(--color-primary);
+ border-radius: 50%;
+ }
+}
+
.back {
composes: tab;
composes: button;
diff --git a/packages/frontend/@n8n/design-system/src/types/tabs.ts b/packages/frontend/@n8n/design-system/src/types/tabs.ts
index 9739882f56..5d85d06af3 100644
--- a/packages/frontend/@n8n/design-system/src/types/tabs.ts
+++ b/packages/frontend/@n8n/design-system/src/types/tabs.ts
@@ -10,4 +10,5 @@ export interface TabOptions {
tooltip?: string;
align?: 'left' | 'right';
to?: RouteLocationRaw;
+ notification?: boolean;
}
diff --git a/packages/frontend/editor-ui/src/Interface.ts b/packages/frontend/editor-ui/src/Interface.ts
index 4cb8e280a0..8ee8df79b6 100644
--- a/packages/frontend/editor-ui/src/Interface.ts
+++ b/packages/frontend/editor-ui/src/Interface.ts
@@ -1076,6 +1076,7 @@ export interface ITab {
icon?: IconName;
align?: 'right';
tooltip?: string;
+ notification?: boolean;
}
export interface ITabBarItem {
diff --git a/packages/frontend/editor-ui/src/components/Node/NodeCreator/Panel/CommunityNodeUpdateInfo.vue b/packages/frontend/editor-ui/src/components/Node/NodeCreator/Panel/CommunityNodeUpdateInfo.vue
index cb9309eab5..08a9aa6006 100644
--- a/packages/frontend/editor-ui/src/components/Node/NodeCreator/Panel/CommunityNodeUpdateInfo.vue
+++ b/packages/frontend/editor-ui/src/components/Node/NodeCreator/Panel/CommunityNodeUpdateInfo.vue
@@ -3,7 +3,6 @@ import { N8nNotice } from '@n8n/design-system';
import { i18n } from '@n8n/i18n';
import { useUIStore } from '@/stores/ui.store';
import { computed } from 'vue';
-
const noticeStyles = computed(() => {
const isDark = useUIStore().appliedTheme === 'dark';
if (isDark) {
diff --git a/packages/frontend/editor-ui/src/components/NodeSettings.vue b/packages/frontend/editor-ui/src/components/NodeSettings.vue
index 4788bb1a92..53d6cd0b43 100644
--- a/packages/frontend/editor-ui/src/components/NodeSettings.vue
+++ b/packages/frontend/editor-ui/src/components/NodeSettings.vue
@@ -6,6 +6,7 @@ import type {
NodeConnectionType,
NodeParameterValue,
INodeCredentialDescription,
+ PublicInstalledPackage,
} from 'n8n-workflow';
import { NodeConnectionTypes, NodeHelpers, deepCopy } from 'n8n-workflow';
import type {
@@ -39,6 +40,8 @@ import { useNodeTypesStore } from '@/stores/nodeTypes.store';
import { useHistoryStore } from '@/stores/history.store';
import { RenameNodeCommand } from '@/models/history';
import { useCredentialsStore } from '@/stores/credentials.store';
+import { useCommunityNodesStore } from '@/stores/communityNodes.store';
+import { useUsersStore } from '@/stores/users.store';
import type { EventBus } from '@n8n/utils/event-bus';
import { useExternalHooks } from '@/composables/useExternalHooks';
import { useNodeHelpers } from '@/composables/useNodeHelpers';
@@ -142,6 +145,8 @@ const hiddenIssuesInputs = ref([]);
const nodeSettings = ref([]);
const subConnections = ref | null>(null);
+const installedPackage = ref(undefined);
+
const currentWorkflowInstance = computed(() => workflowsStore.getCurrentWorkflow());
const currentWorkflow = computed(() =>
workflowsStore.getWorkflowById(currentWorkflowInstance.value.id),
@@ -240,6 +245,7 @@ const showNoParametersNotice = computed(
const outputPanelEditMode = computed(() => ndvStore.outputPanelEditMode);
const isCommunityNode = computed(() => !!node.value && isCommunityPackageName(node.value.type));
+const packageName = computed(() => node.value?.type.split('.')[0] ?? '');
const usedCredentials = computed(() =>
Object.values(workflowsStore.usedCredentials).filter((credential) =>
@@ -771,7 +777,7 @@ watch(node, () => {
setNodeValues();
});
-onMounted(() => {
+onMounted(async () => {
populateHiddenIssuesSet();
populateSettings();
setNodeValues();
@@ -781,6 +787,10 @@ onMounted(() => {
}
importCurlEventBus.on('setHttpNodeParameters', setHttpNodeParameters);
ndvEventBus.on('updateParameterValue', valueChanged);
+
+ if (isCommunityNode.value && useUsersStore().isInstanceOwner) {
+ installedPackage.value = await useCommunityNodesStore().getInstalledPackage(packageName.value);
+ }
});
onBeforeUnmount(() => {
@@ -978,6 +988,10 @@ function handleWheelEvent(event: WheelEvent) {
+
({
+ useCommunityNodesStore: vi.fn(() => ({
+ getInstalledPackage: vi.fn(),
+ })),
+}));
+
+describe('NodeSettingsTabs', () => {
+ beforeEach(() => {
+ createTestingPinia({ stubActions: false });
+ });
+
+ afterEach(() => {
+ vi.clearAllMocks();
+ });
+
+ it('renders the component', () => {
+ const { getByText } = renderComponent({
+ props: {},
+ });
+ expect(getByText('Parameters')).toBeInTheDocument();
+ });
+
+ it('displays notification when updateAvailable', async () => {
+ const communityNodesStore = useCommunityNodesStore();
+ vi.spyOn(communityNodesStore, 'getInstalledPackage').mockResolvedValue({
+ updateAvailable: '1.0.1',
+ } as PublicInstalledPackage);
+
+ const { findByTestId } = renderComponent({
+ props: {},
+ });
+
+ const notification = await findByTestId('tab-settings');
+ expect(notification).toBeDefined();
+ });
+
+ it('does not display notification when not updateAvailable', async () => {
+ const communityNodesStore = useCommunityNodesStore();
+ vi.spyOn(communityNodesStore, 'getInstalledPackage').mockResolvedValue(
+ {} as PublicInstalledPackage,
+ );
+
+ const { queryByTestId } = renderComponent({
+ props: {},
+ });
+
+ const tab = queryByTestId('tab-settings');
+ const notification = tab?.querySelector('.notification');
+ expect(notification).toBeNull();
+ });
+});
diff --git a/packages/frontend/editor-ui/src/components/NodeSettingsTabs.vue b/packages/frontend/editor-ui/src/components/NodeSettingsTabs.vue
index e522116b05..abdf6bb43c 100644
--- a/packages/frontend/editor-ui/src/components/NodeSettingsTabs.vue
+++ b/packages/frontend/editor-ui/src/components/NodeSettingsTabs.vue
@@ -3,9 +3,9 @@ import type { ITab } from '@/Interface';
import { COMMUNITY_NODES_INSTALLATION_DOCS_URL } from '@/constants';
import { useNDVStore } from '@/stores/ndv.store';
import { useWorkflowsStore } from '@/stores/workflows.store';
-import type { INodeTypeDescription } from 'n8n-workflow';
+import type { INodeTypeDescription, PublicInstalledPackage } from 'n8n-workflow';
import { NodeConnectionTypes } from 'n8n-workflow';
-import { computed } from 'vue';
+import { computed, onMounted, ref } from 'vue';
import { useExternalHooks } from '@/composables/useExternalHooks';
import { useI18n } from '@n8n/i18n';
@@ -13,6 +13,8 @@ import { useTelemetry } from '@/composables/useTelemetry';
import { isCommunityPackageName } from '@/utils/nodeTypesUtils';
import { N8nTabs } from '@n8n/design-system';
import { useNodeDocsUrl } from '@/composables/useNodeDocsUrl';
+import { useCommunityNodesStore } from '@/stores/communityNodes.store';
+import { useUsersStore } from '@/stores/users.store';
export type Tab = 'settings' | 'params' | 'communityNode' | 'docs';
type Props = {
@@ -37,9 +39,12 @@ const workflowsStore = useWorkflowsStore();
const i18n = useI18n();
const telemetry = useTelemetry();
const { docsUrl } = useNodeDocsUrl({ nodeType: () => props.nodeType });
+const communityNodesStore = useCommunityNodesStore();
const activeNode = computed(() => ndvStore.activeNode);
+const installedPackage = ref(undefined);
+
const isCommunityNode = computed(() => {
const nodeType = props.nodeType;
if (nodeType) {
@@ -67,6 +72,7 @@ const options = computed(() => {
{
label: i18n.baseText('nodeSettings.settings'),
value: 'settings',
+ notification: installedPackage.value?.updateAvailable ? true : undefined,
},
];
@@ -129,6 +135,12 @@ function onTooltipClick(tab: string | number, event: MouseEvent) {
telemetry.track('user clicked cnr docs link', { source: 'node details view' });
}
}
+
+onMounted(async () => {
+ if (isCommunityNode.value && useUsersStore().isInstanceOwner) {
+ installedPackage.value = await communityNodesStore.getInstalledPackage(packageName.value);
+ }
+});