fix(metadata): remove deprecated isCustom from Objects and Fields (#21799)

## Context

Follow-up to #21228, which deprecated `isCustom` on object/field
metadata but kept it exposed because the frontend still relied on it.
This removes it from the GraphQL API and the frontend entirely.

## Implementation

### Server
- Remove `isCustom` `@Field` from the `Object`, `Field`, and
`MinimalObjectMetadata` GraphQL types
- Remove the `isCustom` `@ResolveField` resolvers and the
`isCustomLoader` dataloader (+ payload/interface)
- Remove `isCustom` as an internal `@HideField()` on the Object/Field
DTOs used by the i18n standard-override gate > Use an explicit
isStandard instead (which is the correct gating)

### Frontend
- Add `getIsMetadataItemCustom` helper + `useGetIsMetadataItemCustom`
hook: an item is custom when `applicationId ===
currentWorkspace.workspaceCustomApplication.id`
- Migrate all consumers off `objectMetadataItem.isCustom` /
`fieldMetadataItem.isCustom`; `isRecordFieldReadOnly` now takes a
precomputed `isFieldCustom`
- Drop `isCustom` from the metadata fragment/mutations/minimal query, FE
types, zod schemas, and mock generators; regenerate GraphQL types

## Notes
- Breaking change on the (already-deprecated) `Object.isCustom` /
`Field.isCustom` GraphQL fields and the `isCustom` filter
- FE semantic is "belongs to the workspace custom app" (third-party-app
objects/fields are treated as non-custom)
- `isCustom` on IndexMetadata / View / Skill / Agent is a separate
column and is untouched
- Breaking changes on REST metadata API
This commit is contained in:
Weiko
2026-06-19 01:19:49 +02:00
committed by GitHub
parent 7b5ee8a7bc
commit 9f30915f6f
109 changed files with 544 additions and 1071 deletions
@@ -50,6 +50,8 @@ export class CommandMenuItemResolver {
commandMenuItem,
fieldName: 'label',
objectMetadataLoader: context.loaders.objectMetadataLoader,
standardApplicationIdLoader:
context.loaders.standardApplicationIdLoader,
workspaceId: workspace.id,
locale: context.req.locale,
})) ?? ''
@@ -66,6 +68,7 @@ export class CommandMenuItemResolver {
commandMenuItem,
fieldName: 'shortLabel',
objectMetadataLoader: context.loaders.objectMetadataLoader,
standardApplicationIdLoader: context.loaders.standardApplicationIdLoader,
workspaceId: workspace.id,
locale: context.req.locale,
});
@@ -81,6 +84,7 @@ export class CommandMenuItemResolver {
commandMenuItem,
fieldName: 'icon',
objectMetadataLoader: context.loaders.objectMetadataLoader,
standardApplicationIdLoader: context.loaders.standardApplicationIdLoader,
workspaceId: workspace.id,
locale: context.req.locale,
});
@@ -6,7 +6,10 @@ import { isDefined } from 'twenty-shared/utils';
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
import { I18nService } from 'src/engine/core-modules/i18n/i18n.service';
import { type ObjectMetadataLoaderPayload } from 'src/engine/dataloaders/dataloader.service';
import {
type ObjectMetadataLoaderPayload,
type StandardApplicationIdLoaderPayload,
} from 'src/engine/dataloaders/dataloader.service';
import {
CommandMenuItemException,
CommandMenuItemExceptionCode,
@@ -15,10 +18,9 @@ import { type CommandMenuItemDTO } from 'src/engine/metadata-modules/command-men
import { type CreateCommandMenuItemInput } from 'src/engine/metadata-modules/command-menu-item/dtos/create-command-menu-item.input';
import { type UpdateCommandMenuItemInput } from 'src/engine/metadata-modules/command-menu-item/dtos/update-command-menu-item.input';
import { EngineComponentKey } from 'src/engine/metadata-modules/command-menu-item/enums/engine-component-key.enum';
import { isObjectMetadataCommandMenuItemPayload } from 'src/engine/metadata-modules/command-menu-item/utils/is-object-metadata-command-menu-item-payload.util';
import { interpolateNavigationCommandMenuItemField } from 'src/engine/metadata-modules/command-menu-item/utils/interpolate-navigation-command-menu-item-field.util';
import { isObjectMetadataCommandMenuItemPayload } from 'src/engine/metadata-modules/command-menu-item/utils/is-object-metadata-command-menu-item-payload.util';
import { type FlatCommandMenuItem } from 'src/engine/metadata-modules/flat-command-menu-item/types/flat-command-menu-item.type';
import { type ObjectMetadataDTO } from 'src/engine/metadata-modules/object-metadata/dtos/object-metadata.dto';
import { fromCreateCommandMenuItemInputToFlatCommandMenuItemToCreate } from 'src/engine/metadata-modules/flat-command-menu-item/utils/from-create-command-menu-item-input-to-flat-command-menu-item-to-create.util';
import { fromDeleteCommandMenuItemInputToFlatCommandMenuItemOrThrow } from 'src/engine/metadata-modules/flat-command-menu-item/utils/from-delete-command-menu-item-input-to-flat-command-menu-item-or-throw.util';
import { fromFlatCommandMenuItemToCommandMenuItemDto } from 'src/engine/metadata-modules/flat-command-menu-item/utils/from-flat-command-menu-item-to-command-menu-item-dto.util';
@@ -26,6 +28,7 @@ import { fromUpdateCommandMenuItemInputToFlatCommandMenuItemToUpdateOrThrow } fr
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
import { type ObjectMetadataDTO } from 'src/engine/metadata-modules/object-metadata/dtos/object-metadata.dto';
import { isCallerOverridingEntity } from 'src/engine/metadata-modules/utils/is-caller-overriding-entity.util';
import { WorkspaceMigrationBuilderException } from 'src/engine/workspace-manager/workspace-migration/exceptions/workspace-migration-builder-exception';
import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service';
@@ -445,6 +448,7 @@ export class CommandMenuItemService {
commandMenuItem,
fieldName,
objectMetadataLoader,
standardApplicationIdLoader,
workspaceId,
locale,
}: {
@@ -454,6 +458,10 @@ export class CommandMenuItemService {
ObjectMetadataLoaderPayload,
ObjectMetadataDTO | null
>;
standardApplicationIdLoader: DataLoader<
StandardApplicationIdLoaderPayload,
string
>;
workspaceId: string;
locale: keyof typeof APP_LOCALES | undefined;
}): Promise<string | undefined> {
@@ -463,10 +471,19 @@ export class CommandMenuItemService {
workspaceId,
});
const standardApplicationId = await standardApplicationIdLoader.load({
workspaceId,
});
const isStandardApp = isDefined(objectMetadata)
? objectMetadata.applicationId === standardApplicationId
: false;
return interpolateNavigationCommandMenuItemField({
commandMenuItem,
fieldName,
objectMetadata,
isStandardApp,
locale,
i18nInstance: this.i18nService.getI18nInstance(locale ?? SOURCE_LOCALE),
});
@@ -47,6 +47,7 @@ describe('interpolateNavigationCommandMenuItemField', () => {
fieldName: 'label',
objectMetadata: mockObjectMetadata,
locale: undefined,
isStandardApp: true,
i18nInstance: mockI18nInstance,
});
@@ -59,6 +60,7 @@ describe('interpolateNavigationCommandMenuItemField', () => {
fieldName: 'shortLabel',
objectMetadata: mockObjectMetadata,
locale: undefined,
isStandardApp: true,
i18nInstance: mockI18nInstance,
});
@@ -71,6 +73,7 @@ describe('interpolateNavigationCommandMenuItemField', () => {
fieldName: 'icon',
objectMetadata: mockObjectMetadata,
locale: undefined,
isStandardApp: true,
i18nInstance: mockI18nInstance,
});
@@ -90,6 +93,7 @@ describe('interpolateNavigationCommandMenuItemField', () => {
fieldName: 'label',
objectMetadata: null,
locale: undefined,
isStandardApp: true,
i18nInstance: mockI18nInstance,
});
@@ -102,6 +106,7 @@ describe('interpolateNavigationCommandMenuItemField', () => {
fieldName: 'label',
objectMetadata: null,
locale: undefined,
isStandardApp: true,
i18nInstance: mockI18nInstance,
});
@@ -119,6 +124,7 @@ describe('interpolateNavigationCommandMenuItemField', () => {
fieldName: 'shortLabel',
objectMetadata: mockObjectMetadata,
locale: undefined,
isStandardApp: true,
i18nInstance: mockI18nInstance,
});
@@ -137,6 +143,7 @@ describe('interpolateNavigationCommandMenuItemField', () => {
fieldName: 'label',
objectMetadata: customObjectMetadata,
locale: undefined,
isStandardApp: true,
i18nInstance: mockI18nInstance,
});
@@ -154,6 +161,7 @@ describe('interpolateNavigationCommandMenuItemField', () => {
fieldName: 'icon',
objectMetadata: customObjectMetadata,
locale: undefined,
isStandardApp: true,
i18nInstance: mockI18nInstance,
});
@@ -171,6 +179,7 @@ describe('interpolateNavigationCommandMenuItemField', () => {
fieldName: 'label',
objectMetadata: null,
locale: undefined,
isStandardApp: true,
i18nInstance: mockI18nInstance,
});
@@ -188,6 +197,7 @@ describe('interpolateNavigationCommandMenuItemField', () => {
fieldName: 'label',
objectMetadata: mockObjectMetadata,
locale: undefined,
isStandardApp: true,
i18nInstance: mockI18nInstance,
});
@@ -9,16 +9,17 @@ export type NavigationInterpolationObjectMetadata = {
labelSingular: string;
description?: string | null;
icon?: string | null;
isCustom: boolean;
standardOverrides?: ObjectStandardOverridesDTO | null;
};
export const buildNavigationInterpolationContext = ({
objectMetadata,
isStandardApp,
locale,
i18nInstance,
}: {
objectMetadata: NavigationInterpolationObjectMetadata;
isStandardApp: boolean;
locale: keyof typeof APP_LOCALES | undefined;
i18nInstance: I18n;
}): Record<string, unknown> => {
@@ -27,7 +28,6 @@ export const buildNavigationInterpolationContext = ({
labelSingular: objectMetadata.labelSingular,
description: objectMetadata.description ?? undefined,
icon: objectMetadata.icon ?? undefined,
isCustom: objectMetadata.isCustom,
standardOverrides: objectMetadata.standardOverrides ?? undefined,
};
@@ -36,6 +36,7 @@ export const buildNavigationInterpolationContext = ({
'labelPlural',
locale,
i18nInstance,
isStandardApp,
);
const resolvedIcon = resolveObjectMetadataStandardOverride(
@@ -43,6 +44,7 @@ export const buildNavigationInterpolationContext = ({
'icon',
locale,
i18nInstance,
isStandardApp,
);
return {
@@ -16,12 +16,14 @@ export const interpolateNavigationCommandMenuItemField = ({
commandMenuItem,
fieldName,
objectMetadata,
isStandardApp,
locale,
i18nInstance,
}: {
commandMenuItem: CommandMenuItemDTO;
fieldName: 'label' | 'shortLabel' | 'icon';
objectMetadata: ObjectMetadataDTO | null;
isStandardApp: boolean;
locale: keyof typeof APP_LOCALES | undefined;
i18nInstance: I18n;
}): string | undefined => {
@@ -44,6 +46,7 @@ export const interpolateNavigationCommandMenuItemField = ({
const context = buildNavigationInterpolationContext({
objectMetadata,
isStandardApp,
locale,
i18nInstance,
});