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:
+1
@@ -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',
|
||||
|
||||
+15
@@ -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(
|
||||
|
||||
+85
@@ -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 = {
|
||||
|
||||
+7
-5
@@ -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];
|
||||
|
||||
Reference in New Issue
Block a user