New name not appearing when renaming "Stages" in data-model settings (#20246)

## Summary
- Resolve standard field `label`, `description`, and `icon` overrides
through dedicated GraphQL field resolvers.
- Fall back to the source locale safely when the request locale is
missing, and allow direct overrides to apply for non-source locales when
translations are absent.
- Enrich metadata subscription payloads for both `before` and `after`,
reusing the same override application path for field and object
metadata.
- Update and extend tests to cover the revised override behavior.

## Testing
- Updated unit coverage for standard override resolution, including the
non-source-locale fallback path.
- Not run (not requested).

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Thomas des Francs
2026-05-04 22:16:00 +02:00
committed by GitHub
parent e6399b180e
commit 01b4754e62
5 changed files with 99 additions and 33 deletions
@@ -22,9 +22,16 @@ import { RelationDTO } from 'src/engine/metadata-modules/field-metadata/dtos/rel
import { UpdateOneFieldMetadataInput } from 'src/engine/metadata-modules/field-metadata/dtos/update-field.input';
import { FieldMetadataService } from 'src/engine/metadata-modules/field-metadata/services/field-metadata.service';
import { fieldMetadataGraphqlApiExceptionHandler } from 'src/engine/metadata-modules/field-metadata/utils/field-metadata-graphql-api-exception-handler.util';
import { resolveFieldMetadataStandardOverride } from 'src/engine/metadata-modules/field-metadata/utils/resolve-field-metadata-standard-override.util';
import { fromFlatFieldMetadataToFieldMetadataDto } from 'src/engine/metadata-modules/flat-field-metadata/utils/from-flat-field-metadata-to-field-metadata-dto.util';
import { PermissionsGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/permissions/utils/permissions-graphql-api-exception.filter';
// Keep @Parent() structurally typed so ResolverValidationPipe does not validate
// FieldMetadataDTO date decorators on already-loaded parent records.
type FieldMetadataStandardOverrideParent = Parameters<
typeof resolveFieldMetadataStandardOverride
>[0];
@UseGuards(WorkspaceAuthGuard)
@UsePipes(ResolverValidationPipe)
@MetadataResolver(() => FieldMetadataDTO)
@@ -38,6 +45,51 @@ export class FieldMetadataResolver {
private readonly i18nService: I18nService,
) {}
@ResolveField(() => String, { nullable: true })
async label(
@Parent() fieldMetadata: FieldMetadataStandardOverrideParent,
@Context() context: I18nContext,
): Promise<string> {
const i18n = this.i18nService.getI18nInstance(context.req.locale);
return resolveFieldMetadataStandardOverride(
fieldMetadata,
'label',
context.req.locale,
i18n,
);
}
@ResolveField(() => String, { nullable: true })
async description(
@Parent() fieldMetadata: FieldMetadataStandardOverrideParent,
@Context() context: I18nContext,
): Promise<string> {
const i18n = this.i18nService.getI18nInstance(context.req.locale);
return resolveFieldMetadataStandardOverride(
fieldMetadata,
'description',
context.req.locale,
i18n,
);
}
@ResolveField(() => String, { nullable: true })
async icon(
@Parent() fieldMetadata: FieldMetadataStandardOverrideParent,
@Context() context: I18nContext,
): Promise<string> {
const i18n = this.i18nService.getI18nInstance(context.req.locale);
return resolveFieldMetadataStandardOverride(
fieldMetadata,
'icon',
context.req.locale,
i18n,
);
}
@UseGuards(SettingsPermissionGuard(PermissionFlagType.DATA_MODEL))
@Mutation(() => FieldMetadataDTO)
async createOneField(
@@ -262,7 +262,7 @@ describe('resolveFieldMetadataStandardOverride', () => {
).toBe('overridden-icon');
});
it('should not use direct override for non-SOURCE_LOCALE', () => {
it('should use direct override for non-SOURCE_LOCALE when translation override is missing', () => {
const fieldMetadata = {
label: 'Standard Label',
description: 'Standard Description',
@@ -283,7 +283,9 @@ describe('resolveFieldMetadataStandardOverride', () => {
mockI18n,
);
expect(result).toBe('Standard Label');
expect(result).toBe('Overridden Label');
expect(mockGenerateMessageId).not.toHaveBeenCalled();
expect(mockI18n._).not.toHaveBeenCalled();
});
it('should not use empty string override for SOURCE_LOCALE', () => {
@@ -16,6 +16,8 @@ export const resolveFieldMetadataStandardOverride = (
locale: keyof typeof APP_LOCALES | undefined,
i18nInstance: I18n,
): string => {
const safeLocale = locale ?? SOURCE_LOCALE;
if (fieldMetadata.isCustom) {
return fieldMetadata[labelKey] ?? '';
}
@@ -26,21 +28,17 @@ export const resolveFieldMetadataStandardOverride = (
if (
isDefined(fieldMetadata.standardOverrides?.translations) &&
isDefined(locale) &&
labelKey !== 'icon'
) {
const translationValue =
fieldMetadata.standardOverrides.translations[locale]?.[labelKey];
fieldMetadata.standardOverrides.translations[safeLocale]?.[labelKey];
if (isDefined(translationValue)) {
return translationValue;
}
}
if (
locale === SOURCE_LOCALE &&
isNonEmptyString(fieldMetadata.standardOverrides?.[labelKey])
) {
if (isNonEmptyString(fieldMetadata.standardOverrides?.[labelKey])) {
return fieldMetadata.standardOverrides[labelKey] ?? '';
}