fix: restore isCustom gate in metadata label resolvers (#21432)
## Context #21228 removed the stored `isCustom` column and, with it, the `isCustom` early-return in `resolveObjectMetadataStandardOverride` / `resolveFieldMetadataStandardOverride`, on the assumption that falling through the `standardOverrides` checks was equivalent. It isn't: custom object/field labels now reach the Lingui lookup. A custom label that collides with a standard catalog string (e.g. a custom field labeled "Status") gets translated for non-English locales against the user's intent, and every other custom label pays a hash + catalog miss — and, in production, an "Uncompiled message detected" warning (#21415) — on each metadata resolution. ## Fix Restore the gate. `isCustom` is no longer stored, so call sites that build the resolver input from flat entities (dataloader, minimal-metadata, view controller, command-menu-item navigation context) derive it via `belongsToTwentyStandardApp`; GraphQL resolvers keep passing DTOs, which already carry the derived value. ## Testing - Unit tests for both resolvers, including a new regression test: a custom label matching a standard catalog entry is returned verbatim, Lingui never called. --------- Co-authored-by: Weiko <corentin@twenty.com>
This commit is contained in:
+63
-3
@@ -29,6 +29,7 @@ describe('resolveObjectMetadataStandardOverride', () => {
|
||||
description: 'Custom Description',
|
||||
icon: 'custom-icon',
|
||||
color: 'blue',
|
||||
isCustom: true,
|
||||
standardOverrides: undefined,
|
||||
} satisfies Pick<
|
||||
ObjectMetadataDTO,
|
||||
@@ -37,6 +38,7 @@ describe('resolveObjectMetadataStandardOverride', () => {
|
||||
| 'labelSingular'
|
||||
| 'description'
|
||||
| 'icon'
|
||||
| 'isCustom'
|
||||
| 'standardOverrides'
|
||||
>;
|
||||
|
||||
@@ -50,13 +52,14 @@ describe('resolveObjectMetadataStandardOverride', () => {
|
||||
expect(result).toBe('My Custom');
|
||||
});
|
||||
|
||||
it('should return the object value for custom description object', () => {
|
||||
it('should never translate a custom label even when it matches a standard catalog entry', () => {
|
||||
const objectMetadata = {
|
||||
labelSingular: 'My Custom',
|
||||
labelPlural: 'My Customs',
|
||||
labelSingular: 'Company',
|
||||
labelPlural: 'Companies',
|
||||
description: 'Custom Description',
|
||||
icon: 'custom-icon',
|
||||
color: 'blue',
|
||||
isCustom: true,
|
||||
standardOverrides: undefined,
|
||||
} satisfies Pick<
|
||||
ObjectMetadataDTO,
|
||||
@@ -65,6 +68,42 @@ describe('resolveObjectMetadataStandardOverride', () => {
|
||||
| 'labelSingular'
|
||||
| 'description'
|
||||
| 'icon'
|
||||
| 'isCustom'
|
||||
| 'standardOverrides'
|
||||
>;
|
||||
|
||||
mockGenerateMessageId.mockReturnValue('company.message.id');
|
||||
mockI18n._.mockReturnValue('Entreprise');
|
||||
|
||||
const result = resolveObjectMetadataStandardOverride(
|
||||
objectMetadata,
|
||||
'labelSingular',
|
||||
'fr-FR',
|
||||
mockI18n,
|
||||
);
|
||||
|
||||
expect(result).toBe('Company');
|
||||
expect(mockGenerateMessageId).not.toHaveBeenCalled();
|
||||
expect(mockI18n._).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return the object value for custom description object', () => {
|
||||
const objectMetadata = {
|
||||
labelSingular: 'My Custom',
|
||||
labelPlural: 'My Customs',
|
||||
description: 'Custom Description',
|
||||
icon: 'custom-icon',
|
||||
color: 'blue',
|
||||
isCustom: true,
|
||||
standardOverrides: undefined,
|
||||
} satisfies Pick<
|
||||
ObjectMetadataDTO,
|
||||
| 'color'
|
||||
| 'labelPlural'
|
||||
| 'labelSingular'
|
||||
| 'description'
|
||||
| 'icon'
|
||||
| 'isCustom'
|
||||
| 'standardOverrides'
|
||||
>;
|
||||
|
||||
@@ -85,6 +124,7 @@ describe('resolveObjectMetadataStandardOverride', () => {
|
||||
description: 'Custom Description',
|
||||
icon: 'custom-icon',
|
||||
color: 'blue',
|
||||
isCustom: true,
|
||||
standardOverrides: undefined,
|
||||
} satisfies Pick<
|
||||
ObjectMetadataDTO,
|
||||
@@ -93,6 +133,7 @@ describe('resolveObjectMetadataStandardOverride', () => {
|
||||
| 'labelSingular'
|
||||
| 'description'
|
||||
| 'icon'
|
||||
| 'isCustom'
|
||||
| 'standardOverrides'
|
||||
>;
|
||||
|
||||
@@ -113,6 +154,7 @@ describe('resolveObjectMetadataStandardOverride', () => {
|
||||
description: 'Custom Description',
|
||||
icon: 'custom-icon',
|
||||
color: 'green',
|
||||
isCustom: true,
|
||||
standardOverrides: undefined,
|
||||
} satisfies Pick<
|
||||
ObjectMetadataDTO,
|
||||
@@ -121,6 +163,7 @@ describe('resolveObjectMetadataStandardOverride', () => {
|
||||
| 'labelSingular'
|
||||
| 'description'
|
||||
| 'icon'
|
||||
| 'isCustom'
|
||||
| 'standardOverrides'
|
||||
>;
|
||||
|
||||
@@ -142,6 +185,7 @@ describe('resolveObjectMetadataStandardOverride', () => {
|
||||
labelPlural: 'My Customs',
|
||||
description: 'Standard Description',
|
||||
icon: 'default-icon',
|
||||
isCustom: false,
|
||||
standardOverrides: {
|
||||
icon: 'override-icon',
|
||||
},
|
||||
@@ -166,6 +210,7 @@ describe('resolveObjectMetadataStandardOverride', () => {
|
||||
description: 'Standard Description',
|
||||
icon: 'default-icon',
|
||||
color: 'blue',
|
||||
isCustom: false,
|
||||
standardOverrides: {
|
||||
color: 'red',
|
||||
},
|
||||
@@ -188,6 +233,7 @@ describe('resolveObjectMetadataStandardOverride', () => {
|
||||
description: 'Standard Description',
|
||||
icon: 'default-icon',
|
||||
color: 'blue',
|
||||
isCustom: false,
|
||||
standardOverrides: undefined,
|
||||
};
|
||||
|
||||
@@ -212,6 +258,7 @@ describe('resolveObjectMetadataStandardOverride', () => {
|
||||
labelPlural: 'Standard Labels',
|
||||
description: 'Standard Description',
|
||||
icon: 'default-icon',
|
||||
isCustom: false,
|
||||
standardOverrides: {
|
||||
translations: {
|
||||
'fr-FR': {
|
||||
@@ -255,6 +302,7 @@ describe('resolveObjectMetadataStandardOverride', () => {
|
||||
labelPlural: 'Standard Labels',
|
||||
description: 'Standard Description',
|
||||
icon: 'default-icon',
|
||||
isCustom: false,
|
||||
standardOverrides: {
|
||||
translations: {
|
||||
'es-ES': {
|
||||
@@ -285,6 +333,7 @@ describe('resolveObjectMetadataStandardOverride', () => {
|
||||
labelPlural: 'Standard Labels',
|
||||
description: 'Standard Description',
|
||||
icon: 'default-icon',
|
||||
isCustom: false,
|
||||
standardOverrides: {
|
||||
translations: {
|
||||
'fr-FR': {
|
||||
@@ -314,6 +363,7 @@ describe('resolveObjectMetadataStandardOverride', () => {
|
||||
labelPlural: 'Standard Labels',
|
||||
description: 'Standard Description',
|
||||
icon: 'default-icon',
|
||||
isCustom: false,
|
||||
standardOverrides: {
|
||||
translations: {
|
||||
'fr-FR': {
|
||||
@@ -346,6 +396,7 @@ describe('resolveObjectMetadataStandardOverride', () => {
|
||||
labelPlural: 'Standard Labels',
|
||||
description: 'Standard Description',
|
||||
icon: 'default-icon',
|
||||
isCustom: false,
|
||||
standardOverrides: {
|
||||
labelSingular: 'Overridden Label',
|
||||
labelPlural: 'Overridden Labels',
|
||||
@@ -394,6 +445,7 @@ describe('resolveObjectMetadataStandardOverride', () => {
|
||||
labelPlural: 'Standard Labels',
|
||||
description: 'Standard Description',
|
||||
icon: 'default-icon',
|
||||
isCustom: false,
|
||||
standardOverrides: {
|
||||
labelSingular: 'Overridden Label',
|
||||
labelPlural: 'Overridden Labels',
|
||||
@@ -416,6 +468,7 @@ describe('resolveObjectMetadataStandardOverride', () => {
|
||||
labelPlural: 'Standard Labels',
|
||||
description: 'Standard Description',
|
||||
icon: 'default-icon',
|
||||
isCustom: false,
|
||||
standardOverrides: {
|
||||
labelSingular: undefined,
|
||||
},
|
||||
@@ -442,6 +495,7 @@ describe('resolveObjectMetadataStandardOverride', () => {
|
||||
labelPlural: 'Standard Labels',
|
||||
description: 'Standard Description',
|
||||
icon: 'default-icon',
|
||||
isCustom: false,
|
||||
standardOverrides: undefined,
|
||||
};
|
||||
|
||||
@@ -466,6 +520,7 @@ describe('resolveObjectMetadataStandardOverride', () => {
|
||||
labelPlural: 'Standard Labels',
|
||||
description: 'Standard Description',
|
||||
icon: 'default-icon',
|
||||
isCustom: false,
|
||||
standardOverrides: undefined,
|
||||
};
|
||||
|
||||
@@ -492,6 +547,7 @@ describe('resolveObjectMetadataStandardOverride', () => {
|
||||
labelPlural: 'Standard Labels',
|
||||
description: 'Standard Description',
|
||||
icon: 'default-icon',
|
||||
isCustom: false,
|
||||
standardOverrides: {
|
||||
labelSingular: 'Source Override',
|
||||
labelPlural: 'Source Overrides',
|
||||
@@ -522,6 +578,7 @@ describe('resolveObjectMetadataStandardOverride', () => {
|
||||
labelPlural: 'Standard Labels',
|
||||
description: 'Standard Description',
|
||||
icon: 'default-icon',
|
||||
isCustom: false,
|
||||
standardOverrides: {
|
||||
labelSingular: 'Source Override',
|
||||
labelPlural: 'Source Overrides',
|
||||
@@ -546,6 +603,7 @@ describe('resolveObjectMetadataStandardOverride', () => {
|
||||
labelPlural: 'Standard Labels',
|
||||
description: 'Standard Description',
|
||||
icon: 'default-icon',
|
||||
isCustom: false,
|
||||
standardOverrides: {},
|
||||
};
|
||||
|
||||
@@ -572,6 +630,7 @@ describe('resolveObjectMetadataStandardOverride', () => {
|
||||
labelPlural: 'Standard Labels',
|
||||
description: 'Standard Description',
|
||||
icon: 'default-icon',
|
||||
isCustom: false,
|
||||
standardOverrides: {
|
||||
labelSingular: 'Source Override',
|
||||
},
|
||||
@@ -598,6 +657,7 @@ describe('resolveObjectMetadataStandardOverride', () => {
|
||||
labelPlural: 'Standard Labels',
|
||||
description: 'Standard Description',
|
||||
icon: 'default-icon',
|
||||
isCustom: false,
|
||||
standardOverrides: undefined,
|
||||
};
|
||||
|
||||
|
||||
+8
@@ -14,6 +14,7 @@ export const resolveObjectMetadataStandardOverride = (
|
||||
| 'labelSingular'
|
||||
| 'description'
|
||||
| 'icon'
|
||||
| 'isCustom'
|
||||
| 'standardOverrides'
|
||||
>,
|
||||
labelKey: 'color' | 'labelPlural' | 'labelSingular' | 'description' | 'icon',
|
||||
@@ -22,6 +23,13 @@ export const resolveObjectMetadataStandardOverride = (
|
||||
): string => {
|
||||
const safeLocale = locale ?? SOURCE_LOCALE;
|
||||
|
||||
// Custom object labels are user-authored: never overridden nor translated.
|
||||
// Without this gate, a label colliding with a standard catalog string
|
||||
// (e.g. "Company") would get translated against the user's intent.
|
||||
if (objectMetadata.isCustom) {
|
||||
return objectMetadata[labelKey] ?? '';
|
||||
}
|
||||
|
||||
if (
|
||||
(labelKey === 'icon' || labelKey === 'color') &&
|
||||
isDefined(objectMetadata.standardOverrides?.[labelKey])
|
||||
|
||||
Reference in New Issue
Block a user