fix: route object color through standardOverrides for standard objects (#18717)

## Summary

- Fixes object `color` to use the `standardOverrides` mechanism for
standard objects, matching how `label`, `description`, and `icon`
already work
- Previously, color was written directly to the `objectMetadata.color`
column for **both** standard and custom objects, which meant user color
customizations on standard objects could be overwritten during metadata
syncs
- Custom objects continue to have `color` updated directly on the entity
(no change)

## Changes

| File | What changed |
|------|-------------|
| `object-metadata-standard-overrides-properties.constant.ts` | Added
`'color'` to `OBJECT_METADATA_STANDARD_OVERRIDES_PROPERTIES` so
`sanitizeRawUpdateObjectInput` routes color into `standardOverrides` for
standard objects |
| `resolve-object-metadata-standard-override.util.ts` | Extended to
support `'color'` as a key — handled like `icon` (no i18n/translation,
just direct override check) |
| `object-metadata.resolver.ts` | Added `@ResolveField` for `color` that
resolves through `resolveObjectMetadataStandardOverride`, matching the
existing `labelSingular`/`labelPlural`/`description`/`icon` resolve
fields |
| `flat-object-metadata-validator.service.ts` | Removed `'color'` from
`allowedOverrideKeys` for system objects since it now flows through
`standardOverrides` |
| `resolve-object-metadata-standard-override.util.spec.ts` | Added test
cases for custom object color, standard object color override, and
standard object color fallback |
| `successful-update-one-standard-object-metadata.integration-spec.ts` |
Added `'when updating color'` test case, included `color` in GraphQL
queries and `standardOverrides` fragment, reset color in `afterEach`
cleanup |

## How it works now

| Object type | Color update flow |
|---|---|
| **Custom** | Written directly to `objectMetadata.color` column |
| **Standard** | Stored in `objectMetadata.standardOverrides.color`,
resolved via `@ResolveField` at query time |

This is identical to how `label`, `description`, and `icon` have always
worked.

## Test plan

- [x] Unit tests pass
(`resolve-object-metadata-standard-override.util.spec.ts` — 21 tests)
- [x] Typecheck passes (`npx nx typecheck twenty-server`)
- [x] Lint passes (`npx nx lint:diff-with-main twenty-server`)
- [ ] Integration test snapshot regenerates correctly
(`successful-update-one-standard-object-metadata`)
- [ ] Verify standard object color editing from sidebar persists via
`standardOverrides`
- [ ] Verify custom object color editing from sidebar persists directly
on entity

Made with [Cursor](https://cursor.com)
This commit is contained in:
Charles Bochet
2026-03-18 10:40:57 +01:00
committed by GitHub
parent 0ae62898a3
commit a74edbf715
9 changed files with 248 additions and 47 deletions
@@ -1,6 +1,7 @@
import { type MetadataUniversalFlatEntityPropertiesToCompare } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/metadata-universal-flat-entity-properties-to-compare.type';
export const OBJECT_METADATA_STANDARD_OVERRIDES_PROPERTIES = [
'color',
'labelSingular',
'labelPlural',
'description',
@@ -117,6 +117,21 @@ export class ObjectMetadataResolver {
);
}
@ResolveField(() => String, { nullable: true })
async color(
@Parent() objectMetadata: ObjectMetadataDTO,
@Context() context: I18nContext,
): Promise<string> {
const i18n = this.i18nService.getI18nInstance(context.req.locale);
return resolveObjectMetadataStandardOverride(
objectMetadata,
'color',
context.req.locale,
i18n,
);
}
@UseGuards(SettingsPermissionGuard(PermissionFlagType.DATA_MODEL))
@Mutation(() => ObjectMetadataDTO)
async createOneObject(
@@ -28,10 +28,12 @@ describe('resolveObjectMetadataStandardOverride', () => {
labelPlural: 'My Customs',
description: 'Custom Description',
icon: 'custom-icon',
color: 'blue',
isCustom: true,
standardOverrides: undefined,
} satisfies Pick<
ObjectMetadataDTO,
| 'color'
| 'labelPlural'
| 'labelSingular'
| 'description'
@@ -56,10 +58,12 @@ describe('resolveObjectMetadataStandardOverride', () => {
labelPlural: 'My Customs',
description: 'Custom Description',
icon: 'custom-icon',
color: 'blue',
isCustom: true,
standardOverrides: undefined,
} satisfies Pick<
ObjectMetadataDTO,
| 'color'
| 'labelPlural'
| 'labelSingular'
| 'description'
@@ -84,10 +88,12 @@ describe('resolveObjectMetadataStandardOverride', () => {
labelPlural: 'My Customs',
description: 'Custom Description',
icon: 'custom-icon',
color: 'blue',
isCustom: true,
standardOverrides: undefined,
} satisfies Pick<
ObjectMetadataDTO,
| 'color'
| 'labelPlural'
| 'labelSingular'
| 'description'
@@ -105,6 +111,36 @@ describe('resolveObjectMetadataStandardOverride', () => {
expect(result).toBe('custom-icon');
});
it('should return the object value for custom color object', () => {
const objectMetadata = {
labelSingular: 'My Custom',
labelPlural: 'My Customs',
description: 'Custom Description',
icon: 'custom-icon',
color: 'green',
isCustom: true,
standardOverrides: undefined,
} satisfies Pick<
ObjectMetadataDTO,
| 'color'
| 'labelPlural'
| 'labelSingular'
| 'description'
| 'icon'
| 'isCustom'
| 'standardOverrides'
>;
const result = resolveObjectMetadataStandardOverride(
objectMetadata,
'color',
SOURCE_LOCALE,
mockI18n,
);
expect(result).toBe('green');
});
});
describe('Standard objects - Icon overrides', () => {
@@ -131,6 +167,55 @@ describe('resolveObjectMetadataStandardOverride', () => {
});
});
describe('Standard objects - Color overrides', () => {
it('should return override color when available for standard object', () => {
const objectMetadata = {
labelSingular: 'Company',
labelPlural: 'Companies',
description: 'Standard Description',
icon: 'default-icon',
color: 'blue',
isCustom: false,
standardOverrides: {
color: 'red',
},
};
const result = resolveObjectMetadataStandardOverride(
objectMetadata,
'color',
'fr-FR',
mockI18n,
);
expect(result).toBe('red');
});
it('should return base color when no override exists for standard object', () => {
const objectMetadata = {
labelSingular: 'Company',
labelPlural: 'Companies',
description: 'Standard Description',
icon: 'default-icon',
color: 'blue',
isCustom: false,
standardOverrides: undefined,
};
mockGenerateMessageId.mockReturnValue('generated-message-id');
mockI18n._.mockReturnValue('generated-message-id');
const result = resolveObjectMetadataStandardOverride(
objectMetadata,
'color',
SOURCE_LOCALE,
mockI18n,
);
expect(result).toBe('blue');
});
});
describe('Standard objects - Translation overrides', () => {
it('should return translation override when available for non-icon objects', () => {
const objectMetadata = {
@@ -9,6 +9,7 @@ import { type ObjectMetadataDTO } from 'src/engine/metadata-modules/object-metad
export const resolveObjectMetadataStandardOverride = (
objectMetadata: Pick<
ObjectMetadataDTO,
| 'color'
| 'labelPlural'
| 'labelSingular'
| 'description'
@@ -16,7 +17,7 @@ export const resolveObjectMetadataStandardOverride = (
| 'isCustom'
| 'standardOverrides'
>,
labelKey: 'labelPlural' | 'labelSingular' | 'description' | 'icon',
labelKey: 'color' | 'labelPlural' | 'labelSingular' | 'description' | 'icon',
locale: keyof typeof APP_LOCALES | undefined,
i18nInstance: I18n,
): string => {
@@ -27,15 +28,16 @@ export const resolveObjectMetadataStandardOverride = (
}
if (
labelKey === 'icon' &&
isDefined(objectMetadata.standardOverrides?.icon)
(labelKey === 'icon' || labelKey === 'color') &&
isDefined(objectMetadata.standardOverrides?.[labelKey])
) {
return objectMetadata.standardOverrides.icon;
return objectMetadata.standardOverrides[labelKey];
}
if (
isDefined(objectMetadata.standardOverrides?.translations) &&
labelKey !== 'icon'
labelKey !== 'icon' &&
labelKey !== 'color'
) {
const translationValue =
objectMetadata.standardOverrides.translations[safeLocale]?.[labelKey];