mirror of
https://github.com/n8n-io/n8n.git
synced 2026-08-30 18:01:23 +08:00
fix(editor): Render node:* icons in credentials (#33887)
This commit is contained in:
@@ -2,7 +2,7 @@ import { render, waitFor } from '@testing-library/vue';
|
||||
import { vi } from 'vitest';
|
||||
|
||||
import Icon from './Icon.vue';
|
||||
import { deprecatedIconSet, type IconName } from './icons';
|
||||
import { deprecatedIconSet, isSupportedIconName, type IconName } from './icons';
|
||||
import { IconBodyLoaderKey } from '../../composables/useIconBodyLoader';
|
||||
|
||||
const loaderStub = vi.fn(async (iconName: string) =>
|
||||
@@ -85,3 +85,17 @@ describe('Icon', () => {
|
||||
expect(loaderStub).toHaveBeenCalledWith('app-window-mac');
|
||||
});
|
||||
});
|
||||
|
||||
describe('isSupportedIconName', () => {
|
||||
it('returns true for supported icon names', () => {
|
||||
expect(isSupportedIconName('check')).toBe(true);
|
||||
expect(isSupportedIconName(Object.keys(deprecatedIconSet)[0])).toBe(true);
|
||||
expect(isSupportedIconName('node:ftp')).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false for unsupported icon names', () => {
|
||||
expect(isSupportedIconName()).toBe(false);
|
||||
expect(isSupportedIconName('not-a-real-icon')).toBe(false);
|
||||
expect(isSupportedIconName('node:not-a-real-node-icon')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -39,7 +39,7 @@ import Toolbox from './custom/toolbox.svg';
|
||||
import Triangle from './custom/triangle.svg';
|
||||
import VectorSquare from './custom/vector-square.svg';
|
||||
import Webhook from './custom/webhook.svg';
|
||||
import type { NodeIconName } from './node-icons';
|
||||
import { nodeIconNames, type NodeIconName } from './node-icon-names';
|
||||
|
||||
import IconLucideAlignRight from '~icons/lucide/align-right';
|
||||
import IconLucideArchive from '~icons/lucide/archive';
|
||||
@@ -780,19 +780,18 @@ export const updatedIconSet = {
|
||||
|
||||
export type IconName = keyof typeof updatedIconSet; // only new icon names should be used moving forward
|
||||
|
||||
export { type NodeIconName } from './node-icons';
|
||||
export { type NodeIconName } from './node-icon-names';
|
||||
|
||||
const NODE_ICON_PREFIX = 'node:';
|
||||
const nodeIconNameSet = new Set<string>(nodeIconNames);
|
||||
|
||||
export function isNodeIcon(iconName?: string): iconName is NodeIconName {
|
||||
return typeof iconName === 'string' && iconName.startsWith(NODE_ICON_PREFIX);
|
||||
}
|
||||
|
||||
export function isSupportedIconName(iconName?: string): iconName is IconName {
|
||||
export function isSupportedIconName(iconName?: string): iconName is IconName | NodeIconName {
|
||||
return (
|
||||
typeof iconName === 'string' &&
|
||||
(iconName in updatedIconSet ||
|
||||
iconName in deprecatedIconSet ||
|
||||
iconName.startsWith(NODE_ICON_PREFIX))
|
||||
(iconName in updatedIconSet || iconName in deprecatedIconSet || nodeIconNameSet.has(iconName))
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
import N8nIcon from './Icon.vue';
|
||||
|
||||
export default N8nIcon;
|
||||
export { type IconName, type NodeIconName, isNodeIcon, updatedIconSet } from './icons';
|
||||
export {
|
||||
type IconName,
|
||||
type NodeIconName,
|
||||
isNodeIcon,
|
||||
isSupportedIconName,
|
||||
updatedIconSet,
|
||||
} from './icons';
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
export const nodeIconNames = [
|
||||
'node:aggregate',
|
||||
'node:ai-agent',
|
||||
'node:ai-transform',
|
||||
'node:basic-llm-chain',
|
||||
'node:calculator',
|
||||
'node:call-n8n-sub-workflow-tool',
|
||||
'node:character-text-splitter',
|
||||
'node:chat-memory-manager',
|
||||
'node:chat-trigger',
|
||||
'node:code',
|
||||
'node:compare-datasets',
|
||||
'node:compression',
|
||||
'node:contextual-compression-retriever',
|
||||
'node:convert-to-file',
|
||||
'node:crypto',
|
||||
'node:data-table',
|
||||
'node:date-and-time',
|
||||
'node:default-data-loader',
|
||||
'node:edit-fields',
|
||||
'node:edit-image',
|
||||
'node:email-trigger',
|
||||
'node:error-trigger',
|
||||
'node:execute-command',
|
||||
'node:execute-sub-workflow',
|
||||
'node:execution-data',
|
||||
'node:extract-from-file',
|
||||
'node:filter',
|
||||
'node:form-trigger',
|
||||
'node:ftp',
|
||||
'node:guardrails',
|
||||
'node:html',
|
||||
'node:http-request',
|
||||
'node:if',
|
||||
'node:information-extractor',
|
||||
'node:item-list-output-parser',
|
||||
'node:limit',
|
||||
'node:local-file-trigger',
|
||||
'node:loop-over-items',
|
||||
'node:manual-trigger',
|
||||
'node:markdown',
|
||||
'node:merge',
|
||||
'node:model-selector',
|
||||
'node:multiquery-retriever',
|
||||
'node:n8n',
|
||||
'node:n8n-trigger',
|
||||
'node:no-operation',
|
||||
'node:question-and-answer-chain',
|
||||
'node:read-write-files-from-disk',
|
||||
'node:recursive-character-text-splitter',
|
||||
'node:remove-duplicates',
|
||||
'node:rename-keys',
|
||||
'node:respond-to-webhook',
|
||||
'node:rss-feed-trigger',
|
||||
'node:rss-read',
|
||||
'node:schedule-trigger',
|
||||
'node:send-mail',
|
||||
'node:sentiment-analysis',
|
||||
'node:simple-memory',
|
||||
'node:simple-vector-store',
|
||||
'node:sort',
|
||||
'node:split-out',
|
||||
'node:sse-trigger',
|
||||
'node:ssh',
|
||||
'node:stop-and-error',
|
||||
'node:structured-output-parser',
|
||||
'node:sub-workflow-trigger',
|
||||
'node:summarization-chain',
|
||||
'node:summarize',
|
||||
'node:switch',
|
||||
'node:text-classifier',
|
||||
'node:think-tool',
|
||||
'node:token-splitter',
|
||||
'node:totp',
|
||||
'node:track-time-saved',
|
||||
'node:vector-store-question-answer-tool',
|
||||
'node:vector-store-retriever',
|
||||
'node:wait',
|
||||
'node:webhook',
|
||||
'node:workflow-retriever',
|
||||
'node:xml',
|
||||
] as const;
|
||||
|
||||
export type NodeIconName = (typeof nodeIconNames)[number];
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { NodeIconName } from './node-icon-names';
|
||||
import NodeAggregate from './nodes/aggregate.svg';
|
||||
import NodeAiAgent from './nodes/ai-agent.svg';
|
||||
import NodeAiTransform from './nodes/ai-transform.svg';
|
||||
@@ -160,6 +161,4 @@ export const nodeIconSet = {
|
||||
'node:webhook': NodeWebhook,
|
||||
'node:workflow-retriever': NodeWorkflowRetriever,
|
||||
'node:xml': NodeXml,
|
||||
} as const;
|
||||
|
||||
export type NodeIconName = keyof typeof nodeIconSet;
|
||||
} as const satisfies Record<NodeIconName, unknown>;
|
||||
|
||||
@@ -3,7 +3,7 @@ import { computed } from 'vue';
|
||||
|
||||
import N8nNodeIcon from '.';
|
||||
import N8nIcon from '../N8nIcon';
|
||||
import type { IconName } from '../N8nIcon/icons';
|
||||
import type { IconName, NodeIconName } from '../N8nIcon/icons';
|
||||
import { isSupportedIconName } from '../N8nIcon/icons';
|
||||
import N8nTooltip from '../N8nTooltip';
|
||||
|
||||
@@ -51,7 +51,7 @@ const badgeStyleData = computed((): Record<string, string> => {
|
||||
};
|
||||
});
|
||||
|
||||
const supportedIconName = computed((): IconName | undefined => {
|
||||
const supportedIconName = computed((): IconName | NodeIconName | undefined => {
|
||||
return isSupportedIconName(props.name) ? props.name : undefined;
|
||||
});
|
||||
</script>
|
||||
|
||||
+18
@@ -132,4 +132,22 @@ describe('CredentialIcon', () => {
|
||||
|
||||
expect(baseElement.querySelector('.nodeIconPlaceholder')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows named icon when credential uses a node icon token directly', () => {
|
||||
useCredentialsStore().setCredentialTypes([
|
||||
mock<ICredentialType>({
|
||||
name: 'ftp',
|
||||
icon: 'node:ftp',
|
||||
}),
|
||||
]);
|
||||
|
||||
const { container } = renderComponent({
|
||||
pinia,
|
||||
props: {
|
||||
credentialTypeName: 'ftp',
|
||||
},
|
||||
});
|
||||
|
||||
expect(container.querySelector('.nodeIconPlaceholder')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useNodeTypesStore } from '@/app/stores/nodeTypes.store';
|
||||
import { useRootStore } from '@n8n/stores/useRootStore';
|
||||
import { useUIStore } from '@/app/stores/ui.store';
|
||||
import { getThemedValue } from '@/app/utils/nodeTypesUtils';
|
||||
import { getNodeIconSource } from '@/app/utils/nodeIcon';
|
||||
import { getNodeIconSource, type NodeIconSource } from '@/app/utils/nodeIcon';
|
||||
import type { ICredentialType } from 'n8n-workflow';
|
||||
import { computed } from 'vue';
|
||||
import { N8nNodeIcon } from '@n8n/design-system';
|
||||
@@ -31,8 +31,12 @@ const referencedNodeIconSource = computed(() => {
|
||||
const icon = getThemedValue(credentialWithIcon.value?.icon, theme.value);
|
||||
if (!icon?.startsWith('node:')) return undefined;
|
||||
const nodeType = nodeTypesStore.getNodeType(icon.replace('node:', ''));
|
||||
if (!nodeType) return undefined;
|
||||
return getNodeIconSource(nodeType, null, null);
|
||||
if (nodeType) return getNodeIconSource(nodeType, null, null);
|
||||
|
||||
return {
|
||||
type: 'icon',
|
||||
name: icon,
|
||||
} satisfies NodeIconSource;
|
||||
});
|
||||
|
||||
const iconSource = computed(() => {
|
||||
@@ -78,7 +82,6 @@ function getCredentialWithIcon(name: string | null): ICredentialType | null {
|
||||
}
|
||||
|
||||
const type = credentialsStore.getCredentialTypeByName(name);
|
||||
|
||||
if (!type) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user