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:
committed by
GitHub
parent
e6399b180e
commit
01b4754e62
+52
@@ -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(
|
||||
|
||||
+4
-2
@@ -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', () => {
|
||||
|
||||
+4
-6
@@ -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] ?? '';
|
||||
}
|
||||
|
||||
|
||||
+36
-22
@@ -6,6 +6,7 @@ import { isDefined, isNonEmptyArray } from 'twenty-shared/utils';
|
||||
import { I18nService } from 'src/engine/core-modules/i18n/i18n.service';
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
|
||||
import { NavigationMenuItemRecordIdentifierService } from 'src/engine/metadata-modules/navigation-menu-item/services/navigation-menu-item-record-identifier.service';
|
||||
import { FIELD_METADATA_STANDARD_OVERRIDES_PROPERTIES } from 'src/engine/metadata-modules/field-metadata/constants/field-metadata-standard-overrides-properties.constant';
|
||||
import { OBJECT_METADATA_STANDARD_OVERRIDES_PROPERTIES } from 'src/engine/metadata-modules/object-metadata/constants/object-metadata-standard-overrides-properties.constant';
|
||||
import { type MetadataEventBatch } from 'src/engine/subscriptions/metadata-event/types/metadata-event-batch.type';
|
||||
import { type FlatCommandMenuItem } from 'src/engine/metadata-modules/flat-command-menu-item/types/flat-command-menu-item.type';
|
||||
@@ -80,25 +81,37 @@ export class MetadataEventPublisher {
|
||||
);
|
||||
|
||||
const enrichedEvents = metadataEventBatch.events.map((event) => {
|
||||
const enrichedProperties = { ...event.properties };
|
||||
|
||||
if (
|
||||
!('after' in event.properties) ||
|
||||
!isDefined(event.properties.after)
|
||||
'before' in enrichedProperties &&
|
||||
isDefined(enrichedProperties.before)
|
||||
) {
|
||||
return event;
|
||||
enrichedProperties.before = this.applyStandardOverridesToMetadataRecord(
|
||||
enrichedProperties.before as Record<string, unknown>,
|
||||
FIELD_METADATA_STANDARD_OVERRIDES_PROPERTIES,
|
||||
) as typeof enrichedProperties.before;
|
||||
}
|
||||
|
||||
const enrichedAfter = enrichFieldMetadataEventWithRelations({
|
||||
record: event.properties.after as Record<string, unknown>,
|
||||
flatFieldMetadataMaps,
|
||||
flatObjectMetadataMaps,
|
||||
});
|
||||
if (
|
||||
'after' in enrichedProperties &&
|
||||
isDefined(enrichedProperties.after)
|
||||
) {
|
||||
const enrichedAfter = enrichFieldMetadataEventWithRelations({
|
||||
record: enrichedProperties.after as Record<string, unknown>,
|
||||
flatFieldMetadataMaps,
|
||||
flatObjectMetadataMaps,
|
||||
});
|
||||
|
||||
enrichedProperties.after = this.applyStandardOverridesToMetadataRecord(
|
||||
enrichedAfter,
|
||||
FIELD_METADATA_STANDARD_OVERRIDES_PROPERTIES,
|
||||
) as typeof enrichedProperties.after;
|
||||
}
|
||||
|
||||
return {
|
||||
...event,
|
||||
properties: {
|
||||
...event.properties,
|
||||
after: enrichedAfter,
|
||||
},
|
||||
properties: enrichedProperties,
|
||||
} as typeof event;
|
||||
});
|
||||
|
||||
@@ -204,20 +217,20 @@ export class MetadataEventPublisher {
|
||||
'before' in enrichedProperties &&
|
||||
isDefined(enrichedProperties.before)
|
||||
) {
|
||||
enrichedProperties.before =
|
||||
this.applyStandardOverridesToObjectMetadataRecord(
|
||||
enrichedProperties.before as Record<string, unknown>,
|
||||
) as typeof enrichedProperties.before;
|
||||
enrichedProperties.before = this.applyStandardOverridesToMetadataRecord(
|
||||
enrichedProperties.before as Record<string, unknown>,
|
||||
OBJECT_METADATA_STANDARD_OVERRIDES_PROPERTIES,
|
||||
) as typeof enrichedProperties.before;
|
||||
}
|
||||
|
||||
if (
|
||||
'after' in enrichedProperties &&
|
||||
isDefined(enrichedProperties.after)
|
||||
) {
|
||||
enrichedProperties.after =
|
||||
this.applyStandardOverridesToObjectMetadataRecord(
|
||||
enrichedProperties.after as Record<string, unknown>,
|
||||
) as typeof enrichedProperties.after;
|
||||
enrichedProperties.after = this.applyStandardOverridesToMetadataRecord(
|
||||
enrichedProperties.after as Record<string, unknown>,
|
||||
OBJECT_METADATA_STANDARD_OVERRIDES_PROPERTIES,
|
||||
) as typeof enrichedProperties.after;
|
||||
}
|
||||
|
||||
return { ...event, properties: enrichedProperties } as typeof event;
|
||||
@@ -226,8 +239,9 @@ export class MetadataEventPublisher {
|
||||
return { ...metadataEventBatch, events: enrichedEvents };
|
||||
}
|
||||
|
||||
private applyStandardOverridesToObjectMetadataRecord(
|
||||
private applyStandardOverridesToMetadataRecord(
|
||||
record: Record<string, unknown>,
|
||||
standardOverrideProperties: readonly string[],
|
||||
): Record<string, unknown> {
|
||||
const standardOverrides = record.standardOverrides as
|
||||
| Record<string, unknown>
|
||||
@@ -240,7 +254,7 @@ export class MetadataEventPublisher {
|
||||
|
||||
const resolved = { ...record };
|
||||
|
||||
for (const key of OBJECT_METADATA_STANDARD_OVERRIDES_PROPERTIES) {
|
||||
for (const key of standardOverrideProperties) {
|
||||
if (isDefined(standardOverrides[key])) {
|
||||
resolved[key] = standardOverrides[key];
|
||||
}
|
||||
|
||||
+3
-3
@@ -105,7 +105,7 @@ exports[`Standard field metadata update should succeed Atomic update test suite
|
||||
exports[`Standard field metadata update should succeed Atomic update test suite when updating description 1`] = `
|
||||
{
|
||||
"defaultValue": "'NEW'",
|
||||
"description": "Opportunity stage",
|
||||
"description": "Updated test description for company name field",
|
||||
"icon": "IconProgressCheck",
|
||||
"id": Any<String>,
|
||||
"isActive": true,
|
||||
@@ -161,7 +161,7 @@ exports[`Standard field metadata update should succeed Atomic update test suite
|
||||
{
|
||||
"defaultValue": "'NEW'",
|
||||
"description": "Opportunity stage",
|
||||
"icon": "IconProgressCheck",
|
||||
"icon": "IconBuildingFactory",
|
||||
"id": Any<String>,
|
||||
"isActive": true,
|
||||
"isCustom": false,
|
||||
@@ -220,7 +220,7 @@ exports[`Standard field metadata update should succeed Atomic update test suite
|
||||
"id": Any<String>,
|
||||
"isActive": true,
|
||||
"isCustom": false,
|
||||
"label": "Stage",
|
||||
"label": "Business Name",
|
||||
"name": "stage",
|
||||
"options": [
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user