From 5ebcce0a51abf1f692e29f3425e9f6dd9956507b Mon Sep 17 00:00:00 2001
From: Etienne <45695613+etiennejouan@users.noreply.github.com>
Date: Wed, 29 Jul 2026 11:39:55 +0200
Subject: [PATCH] feat(ai-tool): resolve and default icons in AI metadata tools
(#23480)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## Context
Objects and fields created through the AI chat / MCP metadata tools
almost never get an icon, so they all render with the meaningless `123`
fallback icon. Two causes:
- The `icon` tool input was described only as `"Icon name"`, so the
model had no idea what the value space is and mostly skipped an optional
field it couldn't fill confidently.
- Any invalid name is silently swapped for `Icon123` by
`useIcons.getIcon` on the frontend, so near-misses were
indistinguishable from unset.
## What this PR does
**Guide the model** (icon names are Tabler names, which LLMs know well):
- `icon` / `targetFieldIcon` schema descriptions now state the
convention with examples (`IconBuildingSkyscraper`, `IconPaw`, …) and
ask for one to always be set
- The `metadata-building` skill gains an "Icons" section; the MCP server
instructions gain a one-line reminder
**Normalize server-side** (new `resolveIconName` util, used by all
create/update/batch metadata tool executes incl.
`relationCreationPayload.targetFieldIcon`):
- Fixes shape mistakes: raw tabler slugs (`"building-skyscraper"`),
separators, missing or lowercased `Icon` prefix
- Deliberately does NOT validate existence against the full ~4.2k icon
registry — an unknown name is harmless since the frontend falls back to
its default icon, exactly as for icons stored via the API today
- Unusable input (empty/garbage) resolves to nothing: creates fall back
to a default, updates keep the existing icon
**Fall back sensibly for fields**:
- New `FIELD_TYPE_DEFAULT_ICONS` in `twenty-shared/constants` maps every
`FieldMetadataType` to a sensible icon (mirroring the settings UI type
illustrations), applied when the model provides no usable icon — an
AI-created field always gets a meaningful icon
- Lives in twenty-shared so the frontend can reuse it later (e.g. as
`getIcon`'s custom default for fields)
The REST/GraphQL metadata APIs are untouched — this only affects the AI
tool layer.
## Test plan
- `resolve-icon-name.util.spec.ts` — canonical pass-through,
slug/prefix/separator fixes, unknown-name pass-through (FE fallback
contract), unusable inputs, icon-key dropping on updates
- `FieldTypeDefaultIcons.test.ts` — every field type mapped, all values
canonically shaped (values hand-checked against the twenty-ui
`ALL_ICONS` registry)
- `nx typecheck` twenty-server + twenty-shared, oxlint/oxfmt clean on
changed files
---
.../normalize-icon-name.util.spec.ts | 46 ++++++++
.../utils/normalize-icon-name.util.ts | 37 +++++++
.../tools/field-metadata-tools.factory.ts | 104 ++++++++++++++++--
.../tools/object-metadata-tools.factory.ts | 54 +++++++--
...reate-standard-flat-skill-metadata.util.ts | 6 +
.../src/constants/FieldTypeDefaultIcons.ts | 29 +++++
.../__tests__/FieldTypeDefaultIcons.test.ts | 19 ++++
packages/twenty-shared/src/constants/index.ts | 1 +
8 files changed, 275 insertions(+), 21 deletions(-)
create mode 100644 packages/twenty-server/src/engine/core-modules/tool-provider/utils/__tests__/normalize-icon-name.util.spec.ts
create mode 100644 packages/twenty-server/src/engine/core-modules/tool-provider/utils/normalize-icon-name.util.ts
create mode 100644 packages/twenty-shared/src/constants/FieldTypeDefaultIcons.ts
create mode 100644 packages/twenty-shared/src/constants/__tests__/FieldTypeDefaultIcons.test.ts
diff --git a/packages/twenty-server/src/engine/core-modules/tool-provider/utils/__tests__/normalize-icon-name.util.spec.ts b/packages/twenty-server/src/engine/core-modules/tool-provider/utils/__tests__/normalize-icon-name.util.spec.ts
new file mode 100644
index 0000000000..34beb213bd
--- /dev/null
+++ b/packages/twenty-server/src/engine/core-modules/tool-provider/utils/__tests__/normalize-icon-name.util.spec.ts
@@ -0,0 +1,46 @@
+import { normalizeIconName } from 'src/engine/core-modules/tool-provider/utils/normalize-icon-name.util';
+
+describe('normalizeIconName', () => {
+ it('should return a canonical icon name unchanged', () => {
+ expect(normalizeIconName('IconBuildingSkyscraper')).toBe(
+ 'IconBuildingSkyscraper',
+ );
+ expect(normalizeIconName('Icon123')).toBe('Icon123');
+ });
+
+ it('should fix a lowercased or separated Icon prefix', () => {
+ expect(normalizeIconName('iconPaw')).toBe('IconPaw');
+ expect(normalizeIconName('icon user')).toBe('IconUser');
+ });
+
+ it('should normalize raw tabler slugs without the Icon prefix', () => {
+ expect(normalizeIconName('building-skyscraper')).toBe(
+ 'IconBuildingSkyscraper',
+ );
+ expect(normalizeIconName('paw')).toBe('IconPaw');
+ expect(normalizeIconName('currency_dollar')).toBe('IconCurrencyDollar');
+ });
+
+ it('should normalize uppercase slugs without breaking camelCase words', () => {
+ expect(normalizeIconName('BUILDING_SKYSCRAPER')).toBe(
+ 'IconBuildingSkyscraper',
+ );
+ expect(normalizeIconName('ICONUSER')).toBe('IconUser');
+ expect(normalizeIconName('buildingSkyscraper')).toBe(
+ 'IconBuildingSkyscraper',
+ );
+ });
+
+ it('should normalize unknown names to a renderable shape for the frontend fallback', () => {
+ expect(normalizeIconName('IconDoesNotExist')).toBe('IconDoesNotExist');
+ expect(normalizeIconName('not a real icon')).toBe('IconNotARealIcon');
+ });
+
+ it('should return undefined for missing or unusable input', () => {
+ expect(normalizeIconName(undefined)).toBeUndefined();
+ expect(normalizeIconName('')).toBeUndefined();
+ expect(normalizeIconName(' ')).toBeUndefined();
+ expect(normalizeIconName('!!!')).toBeUndefined();
+ expect(normalizeIconName('icon')).toBeUndefined();
+ });
+});
diff --git a/packages/twenty-server/src/engine/core-modules/tool-provider/utils/normalize-icon-name.util.ts b/packages/twenty-server/src/engine/core-modules/tool-provider/utils/normalize-icon-name.util.ts
new file mode 100644
index 0000000000..ac0349cab6
--- /dev/null
+++ b/packages/twenty-server/src/engine/core-modules/tool-provider/utils/normalize-icon-name.util.ts
@@ -0,0 +1,37 @@
+import { isNonEmptyString } from '@sniptt/guards';
+
+const ICON_NAME_PATTERN = /^Icon[A-Za-z0-9]+$/;
+
+export const normalizeIconName = (
+ requestedIconName: string | undefined,
+): string | undefined => {
+ if (!isNonEmptyString(requestedIconName)) {
+ return undefined;
+ }
+
+ const trimmedIconName = requestedIconName.trim();
+
+ if (ICON_NAME_PATTERN.test(trimmedIconName)) {
+ return trimmedIconName;
+ }
+
+ const words = trimmedIconName
+ .replace(/^icon[\s_-]*/i, '')
+ .split(/[^A-Za-z0-9]+/)
+ .filter((word) => word.length > 0)
+ .map((word) =>
+ word === word.toUpperCase()
+ ? word.charAt(0) + word.slice(1).toLowerCase()
+ : word.charAt(0).toUpperCase() + word.slice(1),
+ );
+
+ if (words.length === 0) {
+ return undefined;
+ }
+
+ const normalizedIconName = `Icon${words.join('')}`;
+
+ return ICON_NAME_PATTERN.test(normalizedIconName)
+ ? normalizedIconName
+ : undefined;
+};
diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/tools/field-metadata-tools.factory.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/tools/field-metadata-tools.factory.ts
index 2dc4643e43..dcda6502e6 100644
--- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/tools/field-metadata-tools.factory.ts
+++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/tools/field-metadata-tools.factory.ts
@@ -1,12 +1,14 @@
import { Injectable } from '@nestjs/common';
import { type ToolSet } from 'ai';
+import { FIELD_TYPE_DEFAULT_ICONS } from 'twenty-shared/constants';
import { FieldMetadataType, RelationType } from 'twenty-shared/types';
import { z } from 'zod';
import { METADATA_TOOL_EXCLUDED_FIELD_NAMES } from 'src/engine/core-modules/tool-provider/constants/metadata-tool-excluded-field-names.constant';
import { compactMetadataOutput } from 'src/engine/core-modules/tool-provider/utils/compact-metadata-output.util';
import { formatValidationErrors } from 'src/engine/core-modules/tool-provider/utils/format-validation-errors.util';
+import { normalizeIconName } from 'src/engine/core-modules/tool-provider/utils/normalize-icon-name.util';
import { FieldMetadataService } from 'src/engine/metadata-modules/field-metadata/services/field-metadata.service';
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
import { getObjectMetadataIdByName } from 'src/engine/metadata-modules/flat-object-metadata/utils/get-object-metadata-id-by-name.util';
@@ -57,7 +59,12 @@ const CreateFieldMetadataInputSchema = z.object({
name: z.string().describe('Field name (camelCase)'),
label: z.string().describe('Display label'),
description: z.string().optional().describe('Description'),
- icon: z.string().optional().describe('Icon name'),
+ icon: z
+ .string()
+ .optional()
+ .describe(
+ 'Tabler icon name, PascalCase with "Icon" prefix (e.g. IconCurrencyDollar, IconCalendarTime, IconPaw). Set one matching the field meaning; falls back to a type-based default.',
+ ),
isNullable: z.boolean().optional().describe('Nullable'),
isUnique: z.boolean().optional().describe('Unique constraint'),
defaultValue: z.unknown().optional().describe('Default value'),
@@ -79,7 +86,10 @@ const UpdateFieldMetadataInputSchema = z.object({
name: z.string().optional().describe('Field name'),
label: z.string().optional().describe('Display label'),
description: z.string().optional().describe('Description'),
- icon: z.string().optional().describe('Icon name'),
+ icon: z
+ .string()
+ .optional()
+ .describe('Tabler icon name (e.g. IconCurrencyDollar)'),
isActive: z.boolean().optional().describe('Active state'),
isNullable: z.boolean().optional().describe('Nullable'),
isUnique: z.boolean().optional().describe('Unique constraint'),
@@ -120,11 +130,18 @@ const CreateManyRelationFieldsInputSchema = z.object({
name: z.string().describe('Field name (camelCase)'),
label: z.string().describe('Display label'),
description: z.string().optional().describe('Description'),
- icon: z.string().optional().describe('Icon name'),
+ icon: z
+ .string()
+ .optional()
+ .describe('Tabler icon name for the relation field (e.g. IconUsers)'),
type: z.nativeEnum(RelationType).describe('MANY_TO_ONE or ONE_TO_MANY'),
targetObjectMetadataId: z.string().uuid().describe('Target object ID'),
targetFieldLabel: z.string().describe('Inverse field label'),
- targetFieldIcon: z.string().describe('Inverse field icon'),
+ targetFieldIcon: z
+ .string()
+ .describe(
+ 'Inverse field Tabler icon name (e.g. IconBuildingSkyscraper)',
+ ),
}),
)
.min(1)
@@ -132,6 +149,29 @@ const CreateManyRelationFieldsInputSchema = z.object({
.describe('Relations to create (max 20).'),
});
+const normalizeRelationCreationPayloadIcon = (
+ relationCreationPayload: unknown,
+): unknown => {
+ if (
+ !isDefined(relationCreationPayload) ||
+ typeof relationCreationPayload !== 'object' ||
+ Array.isArray(relationCreationPayload)
+ ) {
+ return relationCreationPayload;
+ }
+
+ const { targetFieldIcon } = relationCreationPayload as {
+ targetFieldIcon?: string;
+ };
+
+ return {
+ ...relationCreationPayload,
+ targetFieldIcon:
+ normalizeIconName(targetFieldIcon) ??
+ FIELD_TYPE_DEFAULT_ICONS[FieldMetadataType.RELATION],
+ };
+};
+
@Injectable()
export class FieldMetadataToolsFactory {
constructor(
@@ -249,9 +289,20 @@ export class FieldMetadataToolsFactory {
relationCreationPayload?: unknown;
}) => {
try {
+ const { icon, relationCreationPayload, ...createFieldInput } =
+ parameters;
+
const flatFieldMetadata =
await this.fieldMetadataService.createOneField({
- createFieldInput: parameters as Parameters<
+ createFieldInput: {
+ ...createFieldInput,
+ icon:
+ normalizeIconName(icon) ??
+ FIELD_TYPE_DEFAULT_ICONS[parameters.type],
+ relationCreationPayload: normalizeRelationCreationPayloadIcon(
+ relationCreationPayload,
+ ),
+ } as Parameters<
typeof this.fieldMetadataService.createOneField
>[0]['createFieldInput'],
workspaceId,
@@ -291,11 +342,18 @@ export class FieldMetadataToolsFactory {
isLabelSyncedWithName?: boolean;
}) => {
try {
- const { id, ...update } = parameters;
+ const { id, icon, ...update } = parameters;
+ const normalizedIcon = normalizeIconName(icon);
const flatFieldMetadata =
await this.fieldMetadataService.updateOneField({
- updateFieldInput: { id, ...update } as Parameters<
+ updateFieldInput: {
+ id,
+ ...update,
+ ...(isDefined(normalizedIcon)
+ ? { icon: normalizedIcon }
+ : {}),
+ } as Parameters<
typeof this.fieldMetadataService.updateOneField
>[0]['updateFieldInput'],
workspaceId,
@@ -359,7 +417,17 @@ export class FieldMetadataToolsFactory {
}) => {
try {
await this.fieldMetadataService.createManyFields({
- createFieldInputs: parameters.fields as Parameters<
+ createFieldInputs: parameters.fields.map(
+ ({ icon, relationCreationPayload, ...createFieldInput }) => ({
+ ...createFieldInput,
+ icon:
+ normalizeIconName(icon) ??
+ FIELD_TYPE_DEFAULT_ICONS[createFieldInput.type],
+ relationCreationPayload: normalizeRelationCreationPayloadIcon(
+ relationCreationPayload,
+ ),
+ }),
+ ) as Parameters<
typeof this.fieldMetadataService.createManyFields
>[0]['createFieldInputs'],
workspaceId,
@@ -396,9 +464,17 @@ export class FieldMetadataToolsFactory {
}) => {
try {
await Promise.all(
- parameters.fields.map(async ({ id, ...update }) => {
+ parameters.fields.map(async ({ id, icon, ...update }) => {
+ const normalizedIcon = normalizeIconName(icon);
+
await this.fieldMetadataService.updateOneField({
- updateFieldInput: { id, ...update } as Parameters<
+ updateFieldInput: {
+ id,
+ ...update,
+ ...(isDefined(normalizedIcon)
+ ? { icon: normalizedIcon }
+ : {}),
+ } as Parameters<
typeof this.fieldMetadataService.updateOneField
>[0]['updateFieldInput'],
workspaceId,
@@ -439,12 +515,16 @@ export class FieldMetadataToolsFactory {
name: relation.name,
label: relation.label,
description: relation.description,
- icon: relation.icon,
+ icon:
+ normalizeIconName(relation.icon) ??
+ FIELD_TYPE_DEFAULT_ICONS[FieldMetadataType.RELATION],
relationCreationPayload: {
type: relation.type,
targetObjectMetadataId: relation.targetObjectMetadataId,
targetFieldLabel: relation.targetFieldLabel,
- targetFieldIcon: relation.targetFieldIcon,
+ targetFieldIcon:
+ normalizeIconName(relation.targetFieldIcon) ??
+ FIELD_TYPE_DEFAULT_ICONS[FieldMetadataType.RELATION],
},
})) as Parameters<
typeof this.fieldMetadataService.createManyFields
diff --git a/packages/twenty-server/src/engine/metadata-modules/object-metadata/tools/object-metadata-tools.factory.ts b/packages/twenty-server/src/engine/metadata-modules/object-metadata/tools/object-metadata-tools.factory.ts
index 820d0caf83..216ff3523d 100644
--- a/packages/twenty-server/src/engine/metadata-modules/object-metadata/tools/object-metadata-tools.factory.ts
+++ b/packages/twenty-server/src/engine/metadata-modules/object-metadata/tools/object-metadata-tools.factory.ts
@@ -8,6 +8,7 @@ import { z } from 'zod';
import { METADATA_TOOL_EXCLUDED_FIELD_NAMES } from 'src/engine/core-modules/tool-provider/constants/metadata-tool-excluded-field-names.constant';
import { compactMetadataOutput } from 'src/engine/core-modules/tool-provider/utils/compact-metadata-output.util';
import { formatValidationErrors } from 'src/engine/core-modules/tool-provider/utils/format-validation-errors.util';
+import { normalizeIconName } from 'src/engine/core-modules/tool-provider/utils/normalize-icon-name.util';
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
import { fromFlatObjectMetadataToObjectMetadataDto } from 'src/engine/metadata-modules/flat-object-metadata/utils/from-flat-object-metadata-to-object-metadata-dto.util';
import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service';
@@ -65,7 +66,12 @@ const CreateObjectMetadataInputSchema = z.object({
labelSingular: z.string().describe('Singular label (e.g. "Company")'),
labelPlural: z.string().describe('Plural label (e.g. "Companies")'),
description: z.string().optional().describe('Description'),
- icon: z.string().optional().describe('Icon name'),
+ icon: z
+ .string()
+ .optional()
+ .describe(
+ 'Tabler icon name, PascalCase with "Icon" prefix (e.g. IconBuildingSkyscraper, IconPaw, IconTargetArrow). Always set one matching what the object represents.',
+ ),
shortcut: z.string().optional().describe('Keyboard shortcut'),
isRemote: z.boolean().optional().describe('Remote object'),
isLabelSyncedWithName: z
@@ -81,7 +87,10 @@ const UpdateObjectMetadataInputSchema = z.object({
nameSingular: z.string().optional().describe('Singular name'),
namePlural: z.string().optional().describe('Plural name'),
description: z.string().optional().describe('Description'),
- icon: z.string().optional().describe('Icon name'),
+ icon: z
+ .string()
+ .optional()
+ .describe('Tabler icon name (e.g. IconBuildingSkyscraper)'),
shortcut: z.string().optional().describe('Keyboard shortcut'),
isActive: z.boolean().optional().describe('Active state'),
labelIdentifierFieldMetadataId: z
@@ -240,9 +249,14 @@ export class ObjectMetadataToolsFactory {
isLabelSyncedWithName?: boolean;
}) => {
try {
+ const { icon, ...createObjectInput } = parameters;
+
const flatObjectMetadata =
await this.objectMetadataService.createOneObject({
- createObjectInput: parameters as Parameters<
+ createObjectInput: {
+ ...createObjectInput,
+ icon: normalizeIconName(icon),
+ } as Parameters<
typeof this.objectMetadataService.createOneObject
>[0]['createObjectInput'],
workspaceId,
@@ -280,11 +294,20 @@ export class ObjectMetadataToolsFactory {
isLabelSyncedWithName?: boolean;
}) => {
try {
- const { id, ...update } = parameters;
+ const { id, icon, ...update } = parameters;
+ const normalizedIcon = normalizeIconName(icon);
const flatObjectMetadata =
await this.objectMetadataService.updateOneObject({
- updateObjectInput: { id, update },
+ updateObjectInput: {
+ id,
+ update: {
+ ...update,
+ ...(isDefined(normalizedIcon)
+ ? { icon: normalizedIcon }
+ : {}),
+ },
+ },
workspaceId,
});
@@ -340,9 +363,12 @@ export class ObjectMetadataToolsFactory {
}) => {
try {
await Promise.all(
- parameters.objects.map(async (createObjectInput) => {
+ parameters.objects.map(async ({ icon, ...createObjectInput }) => {
await this.objectMetadataService.createOneObject({
- createObjectInput: createObjectInput as Parameters<
+ createObjectInput: {
+ ...createObjectInput,
+ icon: normalizeIconName(icon),
+ } as Parameters<
typeof this.objectMetadataService.createOneObject
>[0]['createObjectInput'],
workspaceId,
@@ -381,9 +407,19 @@ export class ObjectMetadataToolsFactory {
}) => {
try {
await Promise.all(
- parameters.objects.map(async ({ id, ...update }) => {
+ parameters.objects.map(async ({ id, icon, ...update }) => {
+ const normalizedIcon = normalizeIconName(icon);
+
await this.objectMetadataService.updateOneObject({
- updateObjectInput: { id, update },
+ updateObjectInput: {
+ id,
+ update: {
+ ...update,
+ ...(isDefined(normalizedIcon)
+ ? { icon: normalizedIcon }
+ : {}),
+ },
+ },
workspaceId,
});
}),
diff --git a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/skill-metadata/create-standard-flat-skill-metadata.util.ts b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/skill-metadata/create-standard-flat-skill-metadata.util.ts
index d2a2dc5330..c766ab6fd9 100644
--- a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/skill-metadata/create-standard-flat-skill-metadata.util.ts
+++ b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/skill-metadata/create-standard-flat-skill-metadata.util.ts
@@ -622,6 +622,12 @@ You help users manage their workspace data model by creating, updating, and orga
- Choose appropriate field types for the data being stored
- Consider relationships between objects when designing the data model
+## Icons
+
+- Always set the \`icon\` property when creating objects and fields — otherwise they render with a meaningless default icon
+- Icons are Tabler icon names: PascalCase with an \`Icon\` prefix (e.g. \`IconBuildingSkyscraper\`, \`IconPaw\`, \`IconCurrencyDollar\`)
+- Pick an icon matching the meaning: a Pets object → \`IconPaw\`, a budget field → \`IconCurrencyDollar\`, a deadline field → \`IconCalendarDue\`
+
## Approach
- Ask clarifying questions to understand the user's data modeling needs
diff --git a/packages/twenty-shared/src/constants/FieldTypeDefaultIcons.ts b/packages/twenty-shared/src/constants/FieldTypeDefaultIcons.ts
new file mode 100644
index 0000000000..baa3946fa6
--- /dev/null
+++ b/packages/twenty-shared/src/constants/FieldTypeDefaultIcons.ts
@@ -0,0 +1,29 @@
+import { FieldMetadataType } from '@/types';
+
+export const FIELD_TYPE_DEFAULT_ICONS: Record = {
+ [FieldMetadataType.ACTOR]: 'IconSettings',
+ [FieldMetadataType.ADDRESS]: 'IconMap',
+ [FieldMetadataType.ARRAY]: 'IconBrackets',
+ [FieldMetadataType.BOOLEAN]: 'IconToggleLeft',
+ [FieldMetadataType.CURRENCY]: 'IconCurrencyDollar',
+ [FieldMetadataType.DATE]: 'IconCalendarEvent',
+ [FieldMetadataType.DATE_TIME]: 'IconCalendarTime',
+ [FieldMetadataType.EMAILS]: 'IconMail',
+ [FieldMetadataType.FILES]: 'IconFile',
+ [FieldMetadataType.FULL_NAME]: 'IconUser',
+ [FieldMetadataType.LINKS]: 'IconLink',
+ [FieldMetadataType.MORPH_RELATION]: 'IconRelationOneToMany',
+ [FieldMetadataType.MULTI_SELECT]: 'IconTags',
+ [FieldMetadataType.NUMBER]: 'IconNumbers',
+ [FieldMetadataType.NUMERIC]: 'IconNumbers',
+ [FieldMetadataType.PHONES]: 'IconPhone',
+ [FieldMetadataType.POSITION]: 'IconArrowsSort',
+ [FieldMetadataType.RATING]: 'IconStar',
+ [FieldMetadataType.RAW_JSON]: 'IconJson',
+ [FieldMetadataType.RELATION]: 'IconRelationOneToMany',
+ [FieldMetadataType.RICH_TEXT]: 'IconBlockquote',
+ [FieldMetadataType.SELECT]: 'IconTag',
+ [FieldMetadataType.TEXT]: 'IconAbc',
+ [FieldMetadataType.TS_VECTOR]: 'IconSearch',
+ [FieldMetadataType.UUID]: 'IconId',
+};
diff --git a/packages/twenty-shared/src/constants/__tests__/FieldTypeDefaultIcons.test.ts b/packages/twenty-shared/src/constants/__tests__/FieldTypeDefaultIcons.test.ts
new file mode 100644
index 0000000000..bb305d6b73
--- /dev/null
+++ b/packages/twenty-shared/src/constants/__tests__/FieldTypeDefaultIcons.test.ts
@@ -0,0 +1,19 @@
+import { FieldMetadataType } from '@/types';
+
+import { FIELD_TYPE_DEFAULT_ICONS } from '../FieldTypeDefaultIcons';
+
+describe('FIELD_TYPE_DEFAULT_ICONS', () => {
+ it('should define a default icon for every field type', () => {
+ Object.values(FieldMetadataType).forEach((fieldMetadataType) => {
+ expect(FIELD_TYPE_DEFAULT_ICONS[fieldMetadataType]).toEqual(
+ expect.any(String),
+ );
+ });
+ });
+
+ it('should only contain canonically shaped icon names', () => {
+ Object.values(FIELD_TYPE_DEFAULT_ICONS).forEach((iconName) => {
+ expect(iconName).toMatch(/^Icon[A-Za-z0-9]+$/);
+ });
+ });
+});
diff --git a/packages/twenty-shared/src/constants/index.ts b/packages/twenty-shared/src/constants/index.ts
index bd870a71a2..09cc09160a 100644
--- a/packages/twenty-shared/src/constants/index.ts
+++ b/packages/twenty-shared/src/constants/index.ts
@@ -37,6 +37,7 @@ export { FIELD_FOR_TOTAL_COUNT_AGGREGATE_OPERATION } from './FieldForTotalCountA
export { MAX_OPTIONS_TO_DISPLAY } from './FieldMetadataMaxOptionsToDisplay';
export { FIELD_METADATA_TYPES_NOT_SUPPORTED_IN_GROUP_BY } from './FieldMetadataTypesNotSupportedInGroupBy';
export { FIELD_RESTRICTED_ADDITIONAL_PERMISSIONS_REQUIRED } from './FieldRestrictedAdditionalPermissionsRequired';
+export { FIELD_TYPE_DEFAULT_ICONS } from './FieldTypeDefaultIcons';
export { FILES_FIELD_MAX_NUMBER_OF_VALUES } from './FilesFieldMaxNumberOfValues';
export { GIN_COMPATIBLE_FIELD_TYPES } from './GinCompatibleFieldTypes';
export { GROUP_BY_DATE_GRANULARITY_THAT_REQUIRE_TIME_ZONE } from './GroupByDateGranularityThatRequireTimeZone';