diff --git a/packages/@n8n/eslint-config/src/configs/frontend.ts b/packages/@n8n/eslint-config/src/configs/frontend.ts index ac2bdf60b15..715589dacb3 100644 --- a/packages/@n8n/eslint-config/src/configs/frontend.ts +++ b/packages/@n8n/eslint-config/src/configs/frontend.ts @@ -64,7 +64,6 @@ export const frontendConfig = tseslint.config( 'error', { ignorePatterns: [ - 'FontAwesomeIcon', // Globally registered in plugins/icons/index.ts 'RouterLink', // Vue Router global component 'RouterView', // Vue Router global component 'Teleport', // Vue 3 built-in diff --git a/packages/frontend/@n8n/design-system/.storybook/preview.ts b/packages/frontend/@n8n/design-system/.storybook/preview.ts index 577e51127a1..4326f01f6a5 100644 --- a/packages/frontend/@n8n/design-system/.storybook/preview.ts +++ b/packages/frontend/@n8n/design-system/.storybook/preview.ts @@ -1,5 +1,3 @@ -import { library } from '@fortawesome/fontawesome-svg-core'; -import { fas } from '@fortawesome/free-solid-svg-icons'; import { sharedTags } from '@n8n/storybook/main'; import { withThemeByDataAttribute } from '@storybook/addon-themes'; import { setup } from '@storybook/vue3'; @@ -13,8 +11,6 @@ import './storybook.scss'; // import '../src/css/tailwind/index.css'; setup((app) => { - library.add(fas); - app.use(ElementPlus, { locale: lang, }); diff --git a/packages/frontend/@n8n/design-system/package.json b/packages/frontend/@n8n/design-system/package.json index 99b5f8f2f8a..0ee3a972a1b 100644 --- a/packages/frontend/@n8n/design-system/package.json +++ b/packages/frontend/@n8n/design-system/package.json @@ -50,9 +50,6 @@ "@vue/test-utils": "catalog:frontend" }, "dependencies": { - "@fortawesome/fontawesome-svg-core": "^1.2.36", - "@fortawesome/free-solid-svg-icons": "^5.15.4", - "@fortawesome/vue-fontawesome": "^3.0.3", "@n8n/composables": "workspace:*", "@n8n/utils": "workspace:*", "@tanstack/vue-table": "^8.21.2", diff --git a/packages/frontend/@n8n/design-system/src/components/N8nIconPicker/IconPicker.test.ts b/packages/frontend/@n8n/design-system/src/components/N8nIconPicker/IconPicker.test.ts index 099f6f92be8..321d072afc7 100644 --- a/packages/frontend/@n8n/design-system/src/components/N8nIconPicker/IconPicker.test.ts +++ b/packages/frontend/@n8n/design-system/src/components/N8nIconPicker/IconPicker.test.ts @@ -5,21 +5,6 @@ import { createRouter, createWebHistory } from 'vue-router'; import IconPicker from '.'; import { ALL_ICON_PICKER_ICONS } from './constants'; -// Create a proxy handler that returns a mock icon object for any icon name -// and mock the entire icon library with the proxy -vi.mock( - '@fortawesome/free-solid-svg-icons', - () => - new Proxy( - {}, - { - get: (_target, prop) => { - return { prefix: 'fas', iconName: prop.toString().replace('fa', '').toLowerCase() }; - }, - }, - ), -); - const router = createRouter({ history: createWebHistory(), routes: [ diff --git a/packages/frontend/@n8n/design-system/src/components/N8nNodeIcon/IconContent.vue b/packages/frontend/@n8n/design-system/src/components/N8nNodeIcon/IconContent.vue new file mode 100644 index 00000000000..7ff7a937aef --- /dev/null +++ b/packages/frontend/@n8n/design-system/src/components/N8nNodeIcon/IconContent.vue @@ -0,0 +1,112 @@ + + + + + diff --git a/packages/frontend/@n8n/design-system/src/components/N8nNodeIcon/NodeIcon.test.ts b/packages/frontend/@n8n/design-system/src/components/N8nNodeIcon/NodeIcon.test.ts new file mode 100644 index 00000000000..2643291b6d1 --- /dev/null +++ b/packages/frontend/@n8n/design-system/src/components/N8nNodeIcon/NodeIcon.test.ts @@ -0,0 +1,204 @@ +import { render } from '@testing-library/vue'; +import { describe, expect, it, vi } from 'vitest'; + +import NodeIcon from './NodeIcon.vue'; + +// Mock N8nTooltip component +vi.mock('../N8nTooltip', () => ({ + default: { + name: 'N8nTooltip', + template: '
', + props: ['placement', 'disabled'], + }, +})); + +// Mock N8nIcon component +vi.mock('../N8nIcon', () => ({ + default: { + name: 'N8nIcon', + template: '', + props: ['icon'], + }, +})); + +describe('NodeIcon', () => { + describe('icon rendering', () => { + it('renders file type icon with image', () => { + const { container } = render(NodeIcon, { + props: { + type: 'file', + src: 'https://example.com/icon.png', + }, + }); + + const img = container.querySelector('img'); + expect(img).toBeTruthy(); + expect(img?.getAttribute('src')).toBe('https://example.com/icon.png'); + }); + + it('renders icon type with supported icon name', () => { + const { container } = render(NodeIcon, { + props: { + type: 'icon', + name: 'check', + }, + }); + + // N8nIcon should be rendered (via IconContent) + const icon = container.querySelector('[data-testid="n8n-icon"]'); + expect(icon).toBeTruthy(); + expect(icon?.getAttribute('data-icon')).toBe('check'); + }); + + it('renders unknown type with placeholder', () => { + const { getByText } = render(NodeIcon, { + props: { + type: 'unknown', + nodeTypeName: 'TestNode', + }, + }); + + expect(getByText('T')).toBeTruthy(); + }); + + it('renders unknown type with question mark when no nodeTypeName', () => { + const { getByText } = render(NodeIcon, { + props: { + type: 'unknown', + }, + }); + + expect(getByText('?')).toBeTruthy(); + }); + }); + + describe('styling', () => { + it('applies custom size to wrapper', () => { + const { container } = render(NodeIcon, { + props: { + type: 'file', + src: 'test.png', + size: 40, + }, + }); + + const wrapper = container.querySelector('[class*="nodeIconWrapper"]'); + expect(wrapper).toBeTruthy(); + const style = (wrapper as HTMLElement).style; + expect(style.width).toBe('40px'); + expect(style.height).toBe('40px'); + }); + + it('applies custom color', () => { + const { container } = render(NodeIcon, { + props: { + type: 'file', + src: 'test.png', + color: '#ff0000', + }, + }); + + const wrapper = container.querySelector('[class*="nodeIconWrapper"]'); + const style = (wrapper as HTMLElement).style; + expect(style.color).toBe('rgb(255, 0, 0)'); + }); + + it('applies circle class when circle prop is true', () => { + const { container } = render(NodeIcon, { + props: { + type: 'file', + src: 'test.png', + circle: true, + }, + }); + + const wrapper = container.querySelector('[class*="nodeIconWrapper"]'); + expect(wrapper?.className).toContain('circle'); + }); + + it('applies disabled class when disabled prop is true', () => { + const { container } = render(NodeIcon, { + props: { + type: 'file', + src: 'test.png', + disabled: true, + }, + }); + + const wrapper = container.querySelector('[class*="nodeIconWrapper"]'); + expect(wrapper?.className).toContain('disabled'); + }); + }); + + describe('tooltip', () => { + it('renders tooltip when showTooltip is true', () => { + const { container } = render(NodeIcon, { + props: { + type: 'file', + src: 'test.png', + showTooltip: true, + nodeTypeName: 'Test Node', + }, + }); + + const tooltip = container.querySelector('[data-testid="tooltip"]'); + expect(tooltip).toBeTruthy(); + }); + + it('does not render tooltip when showTooltip is false', () => { + const { container } = render(NodeIcon, { + props: { + type: 'file', + src: 'test.png', + showTooltip: false, + }, + }); + + const tooltip = container.querySelector('[data-testid="tooltip"]'); + expect(tooltip).toBeNull(); + }); + }); + + describe('badge', () => { + it('renders badge when badge prop is provided', () => { + const { container } = render(NodeIcon, { + props: { + type: 'file', + src: 'test.png', + size: 40, + badge: { + type: 'file', + src: 'badge.png', + }, + }, + }); + + const badge = container.querySelector('[class*="badge"]'); + expect(badge).toBeTruthy(); + }); + + it('does not render badge when badge prop is not provided', () => { + const { container } = render(NodeIcon, { + props: { + type: 'file', + src: 'test.png', + }, + }); + + const badge = container.querySelector('[class*="badge"]'); + expect(badge).toBeNull(); + }); + + it('renders placeholder when icon name is not supported', () => { + const { getByText } = render(NodeIcon, { + props: { + type: 'icon', + name: 'unsupported-icon-name', + nodeTypeName: 'CustomNode', + }, + }); + + expect(getByText('C')).toBeTruthy(); + }); + }); +}); diff --git a/packages/frontend/@n8n/design-system/src/components/N8nNodeIcon/NodeIcon.vue b/packages/frontend/@n8n/design-system/src/components/N8nNodeIcon/NodeIcon.vue index 46ab08999d7..1655301129a 100644 --- a/packages/frontend/@n8n/design-system/src/components/N8nNodeIcon/NodeIcon.vue +++ b/packages/frontend/@n8n/design-system/src/components/N8nNodeIcon/NodeIcon.vue @@ -1,12 +1,9 @@ @@ -143,40 +92,6 @@ const N8nNodeIcon = getCurrentInstance()?.type; font-size: 20px; } -.icon { - height: 100%; - width: 100%; - display: flex; - justify-content: center; - align-items: center; - position: relative; - - svg { - max-width: 100%; - max-height: 100%; - } - - img, - svg { - pointer-events: none; - } -} -.nodeIconPlaceholder { - text-align: center; -} -.nodeIconImage { - max-width: 100%; - max-height: 100%; - width: auto; - height: auto; -} - -.badge { - position: absolute; - background: var(--color-background-node-icon-badge, var(--color--background)); - border-radius: 50%; -} - .circle { border-radius: 50%; } diff --git a/packages/frontend/editor-ui/package.json b/packages/frontend/editor-ui/package.json index 538acd4ce0f..009dc6d0e50 100644 --- a/packages/frontend/editor-ui/package.json +++ b/packages/frontend/editor-ui/package.json @@ -143,11 +143,5 @@ "vitest-mock-extended": "catalog:", "vue-tsc": "catalog:frontend", "z-vue-scan": "^0.0.35" - }, - "peerDependencies": { - "@fortawesome/fontawesome-svg-core": "*", - "@fortawesome/free-regular-svg-icons": "*", - "@fortawesome/free-solid-svg-icons": "*", - "@fortawesome/vue-fontawesome": "*" } } diff --git a/packages/frontend/editor-ui/src/__tests__/render.ts b/packages/frontend/editor-ui/src/__tests__/render.ts index 5f7652f5e33..0cd0da7f7e7 100644 --- a/packages/frontend/editor-ui/src/__tests__/render.ts +++ b/packages/frontend/editor-ui/src/__tests__/render.ts @@ -2,7 +2,6 @@ import type { Plugin } from 'vue'; import { render, type RenderOptions as TestingLibraryRenderOptions } from '@testing-library/vue'; import { i18nInstance } from '@n8n/i18n'; import { GlobalDirectivesPlugin } from '@/plugins/directives'; -import { FontAwesomePlugin } from '@/plugins/icons'; import { N8nPlugin } from '@n8n/design-system'; import type { Pinia } from 'pinia'; import { PiniaVuePlugin } from 'pinia'; @@ -33,14 +32,7 @@ const defaultOptions = { }, VueJsonPretty: vueJsonPretty, }, - plugins: [ - i18nInstance, - PiniaVuePlugin, - FontAwesomePlugin, - N8nPlugin, - GlobalDirectivesPlugin, - TelemetryPlugin, - ], + plugins: [i18nInstance, PiniaVuePlugin, N8nPlugin, GlobalDirectivesPlugin, TelemetryPlugin], }, }; diff --git a/packages/frontend/editor-ui/src/components/CredentialIcon.test.ts b/packages/frontend/editor-ui/src/components/CredentialIcon.test.ts index 13aae7438b6..0c5e9c6fcc0 100644 --- a/packages/frontend/editor-ui/src/components/CredentialIcon.test.ts +++ b/packages/frontend/editor-ui/src/components/CredentialIcon.test.ts @@ -13,7 +13,15 @@ describe('CredentialIcon', () => { const renderComponent = createComponentRenderer(CredentialIcon, { pinia: createTestingPinia(), global: { - stubs: ['N8nTooltip'], + stubs: { + N8nTooltip: true, + N8nNodeIcon: { + template: ` + + `, + props: ['type', 'src', 'name', 'color', 'size'], + }, + }, }, }); let pinia: TestingPinia; @@ -50,16 +58,16 @@ describe('CredentialIcon', () => { }), ]); - const { getByRole } = renderComponent({ + const { container } = renderComponent({ pinia, props: { credentialTypeName: 'test', }, }); - const icon = getByRole('img', { hidden: true }); - expect(icon.tagName).toBe('svg'); - expect(icon).toHaveClass('fa-clock'); + const icon = container.querySelector('.n8n-icon'); + expect(icon).toBeInTheDocument(); + expect(icon?.getAttribute('data-icon')).toBe('clock'); }); it('shows correct icon when credential has an icon with node: prefix', () => { diff --git a/packages/frontend/editor-ui/src/components/Node/NodeCreator/ItemTypes/LinkItem.vue b/packages/frontend/editor-ui/src/components/Node/NodeCreator/ItemTypes/LinkItem.vue index 8b7a2261a4a..73ba2a89cf9 100644 --- a/packages/frontend/editor-ui/src/components/Node/NodeCreator/ItemTypes/LinkItem.vue +++ b/packages/frontend/editor-ui/src/components/Node/NodeCreator/ItemTypes/LinkItem.vue @@ -19,13 +19,7 @@ defineProps(); :show-action-arrow="true" > diff --git a/packages/frontend/editor-ui/src/components/Node/NodeCreator/ItemTypes/OpenTemplateItem.vue b/packages/frontend/editor-ui/src/components/Node/NodeCreator/ItemTypes/OpenTemplateItem.vue index b643c5b13a3..5c63f4aef0d 100644 --- a/packages/frontend/editor-ui/src/components/Node/NodeCreator/ItemTypes/OpenTemplateItem.vue +++ b/packages/frontend/editor-ui/src/components/Node/NodeCreator/ItemTypes/OpenTemplateItem.vue @@ -20,13 +20,7 @@ defineProps(); :is-trigger="false" > diff --git a/packages/frontend/editor-ui/src/components/Node/NodeCreator/ItemTypes/ViewItem.vue b/packages/frontend/editor-ui/src/components/Node/NodeCreator/ItemTypes/ViewItem.vue index ec52a6d091a..302973980ca 100644 --- a/packages/frontend/editor-ui/src/components/Node/NodeCreator/ItemTypes/ViewItem.vue +++ b/packages/frontend/editor-ui/src/components/Node/NodeCreator/ItemTypes/ViewItem.vue @@ -19,13 +19,7 @@ defineProps(); :show-action-arrow="true" > 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 8253f4eacfe..ae3fbaceb57 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 @@ -194,7 +194,6 @@ function onBackButton() { :circle="false" :show-tooltip="false" :size="20" - :use-updated-icons="true" />

@@ -286,6 +285,7 @@ function onBackButton() { } .nodeIcon { --node-icon-size: 20px; + --node-icon-color: var(--color--text); margin-right: var(--spacing--sm); } .renderedItems { diff --git a/packages/frontend/editor-ui/src/components/NodeSettingsHint.vue b/packages/frontend/editor-ui/src/components/NodeSettingsHint.vue index f6a3eb9a264..d81b0931ff7 100644 --- a/packages/frontend/editor-ui/src/components/NodeSettingsHint.vue +++ b/packages/frontend/editor-ui/src/components/NodeSettingsHint.vue @@ -71,8 +71,7 @@ const activeSettings = computed(() => {

- - +
{{ setting.message }} diff --git a/packages/frontend/editor-ui/src/components/PanelDragButtonV2.vue b/packages/frontend/editor-ui/src/components/PanelDragButtonV2.vue index 4e45f6be2d9..025fc709b95 100644 --- a/packages/frontend/editor-ui/src/components/PanelDragButtonV2.vue +++ b/packages/frontend/editor-ui/src/components/PanelDragButtonV2.vue @@ -1,4 +1,5 @@