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 Review in cubic --- .../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';