feat(ai-tool): resolve and default icons in AI metadata tools (#23480)
## 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 <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23480?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. -->
This commit is contained in:
+46
@@ -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();
|
||||
});
|
||||
});
|
||||
+37
@@ -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;
|
||||
};
|
||||
+92
-12
@@ -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
|
||||
|
||||
+45
-9
@@ -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,
|
||||
});
|
||||
}),
|
||||
|
||||
+6
@@ -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
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import { FieldMetadataType } from '@/types';
|
||||
|
||||
export const FIELD_TYPE_DEFAULT_ICONS: Record<FieldMetadataType, string> = {
|
||||
[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',
|
||||
};
|
||||
@@ -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]+$/);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user