feat: add color property to ObjectMetadata for object icon customization (#18672)
## Summary - Adds a `color` column to `ObjectMetadataEntity` with full GraphQL support so object icon colors are persisted at the metadata level - Adds a `type` column to `NavigationMenuItemEntity` (enum: `OBJECT`, `VIEW`, `FOLDER`, `LINK`, `RECORD`) replacing field-based type inference - Updates frontend to read object colors from `objectMetadata.color` (falling back to standard defaults) in the sidebar nav, record index header, and record show breadcrumb - Simplifies `NavigationMenuItemIcon` color resolution via `getEffectiveNavigationMenuItemColor` util ## Color rules | Item type | Color source | Editable in sidebar? | |-----------|-------------|---------------------| | **Object** | `objectMetadata.color` | Yes — persisted to `objectMetadata.color` on Save | | **Folder** | `navigationMenuItem.color` | Yes | | **Link** | Fixed default (`DEFAULT_NAVIGATION_MENU_ITEM_COLOR_LINK`) | No | | **View** | `objectMetadata.color` (from the parent object) | No | | **Record** | None | No | - **Object** items represent the whole object (e.g. "Companies") and point to the INDEX view. Changing their color updates `objectMetadata.color` via `useSaveObjectMetadataColorsFromDraft`. - **View** items represent specific non-INDEX views. Their color comes from the parent object's metadata (read-only). - Only **folders** store their color on `navigationMenuItem.color` — enforced by `hasNavigationMenuItemOwnColor` util. - `getEffectiveNavigationMenuItemColor` returns `objectColor` for both OBJECT and VIEW items, folder's own color for folders, and the fixed default for links. ## NavigationMenuItemType enum - Shared enum created in `twenty-shared` with values: `OBJECT`, `VIEW`, `FOLDER`, `LINK`, `RECORD` - Registered as a GraphQL enum on the backend - Replaces string literals across entity, DTOs, input, converters, and frontend hooks - Migration backfills existing rows: INDEX views → `OBJECT`, non-INDEX views → `VIEW`, based on join with the view table ## Design decisions - **OBJECT vs VIEW distinction**: Items pointing to INDEX views are typed as `OBJECT` (represent the whole object, color editable). Items pointing to non-INDEX views are typed as `VIEW` (specific view, color read-only from parent object). - **Dual color storage**: `navigationMenuItem.color` is preserved for folders only. Objects use `objectMetadata.color` as their source of truth. - **Type discriminator**: The `type` column replaces field-based inference (checking `viewId`, `link`, `targetRecordId` presence) with an explicit enum, simplifying `isNavigationMenuItemLink` / `isNavigationMenuItemFolder` to simple `item.type ===` checks. - **No settings page color picker**: Object color editing is done from the sidebar edit panel, not the data model settings page. ## Test plan - [ ] Verify objects display their default standard colors in the sidebar - [ ] Verify object color editing works in the sidebar edit panel (persists to objectMetadata.color) - [ ] Verify folder color editing works in the sidebar edit panel - [ ] Verify views, links, and records do NOT show a color picker in the sidebar edit panel - [ ] Run `npx nx typecheck twenty-front` and `npx nx typecheck twenty-server` - [ ] Verify the database migrations add `color` to `objectMetadata` and `type` to `navigationMenuItem` Made with [Cursor](https://cursor.com)
This commit is contained in:
+3
@@ -1,4 +1,5 @@
|
||||
import { fromNavigationMenuItemManifestToUniversalFlatNavigationMenuItem } from 'src/engine/core-modules/application/application-manifest/converters/from-navigation-menu-item-manifest-to-universal-flat-navigation-menu-item.util';
|
||||
import { NavigationMenuItemType } from 'src/engine/metadata-modules/navigation-menu-item/enums/navigation-menu-item-type.enum';
|
||||
|
||||
describe('fromNavigationMenuItemManifestToUniversalFlatNavigationMenuItem', () => {
|
||||
const now = '2026-01-01T00:00:00.000Z';
|
||||
@@ -9,6 +10,7 @@ describe('fromNavigationMenuItemManifestToUniversalFlatNavigationMenuItem', () =
|
||||
fromNavigationMenuItemManifestToUniversalFlatNavigationMenuItem({
|
||||
navigationMenuItemManifest: {
|
||||
universalIdentifier: 'nav-uuid-1',
|
||||
type: NavigationMenuItemType.VIEW,
|
||||
position: 0,
|
||||
},
|
||||
applicationUniversalIdentifier,
|
||||
@@ -34,6 +36,7 @@ describe('fromNavigationMenuItemManifestToUniversalFlatNavigationMenuItem', () =
|
||||
fromNavigationMenuItemManifestToUniversalFlatNavigationMenuItem({
|
||||
navigationMenuItemManifest: {
|
||||
universalIdentifier: 'nav-uuid-2',
|
||||
type: NavigationMenuItemType.VIEW,
|
||||
name: 'Recipes Board',
|
||||
position: 1,
|
||||
viewUniversalIdentifier: 'view-uuid-1',
|
||||
|
||||
+1
@@ -15,6 +15,7 @@ export const fromNavigationMenuItemManifestToUniversalFlatNavigationMenuItem =
|
||||
return {
|
||||
universalIdentifier: navigationMenuItemManifest.universalIdentifier,
|
||||
applicationUniversalIdentifier,
|
||||
type: navigationMenuItemManifest.type,
|
||||
name: navigationMenuItemManifest.name ?? null,
|
||||
icon: navigationMenuItemManifest.icon ?? null,
|
||||
color: navigationMenuItemManifest.color ?? null,
|
||||
|
||||
+1
@@ -18,6 +18,7 @@ export const fromObjectManifestToUniversalFlatObjectMetadata = ({
|
||||
namePlural: objectManifest.namePlural,
|
||||
labelSingular: objectManifest.labelSingular,
|
||||
labelPlural: objectManifest.labelPlural,
|
||||
color: null,
|
||||
description: objectManifest.description ?? null,
|
||||
icon: objectManifest.icon ?? null,
|
||||
standardOverrides: null,
|
||||
|
||||
Reference in New Issue
Block a user