feat: gate record creation on isUICreatable only, decoupled from isSystem (#21527)

## Context

The generic "create a record" UI affordance previously required
`!isSystem`, conflating two distinct concerns: **"hidden from Data
Model"** and **"not user-creatable"**. This blocked legitimately
creatable system objects (e.g. marketing message lists kept `isSystem:
true` only to stay out of the Data Model).

This PR makes creatability depend on `isUICreatable` alone, so
visibility (`isSystem`) and creatability (`isUICreatable`) become
independent.

## Changes

- **Front gate** (`canCreateRecordsForObjectMetadataItem.ts`): drop the
`!isSystem` clause — creatability is now `isUICreatable && !readOnly`.
Updated comment + unit test.
- **Command menu** (`standard-command-menu-item.constant.ts`): drop the
matching `not objectMetadataItem.isSystem` clause from the
`createNewRecord` availability expression so the "Create new X" command
mirrors the front gate. Existing workspaces get this via the
already-present `SyncCreateRecordCommandAvailabilityExpressionCommand`,
which re-syncs from the live definition.
- **Standard object audit**
(`create-standard-flat-object-metadata.util.ts`): the 15
sync/system-created standard objects that relied on `!isSystem` to stay
non-creatable now set `isUICreatable: false` (attachment, blocklist,
calendar*/message*/note/task targets, message, messageThread,
messageParticipant, timelineActivity, callRecording,
workflowAutomatedTrigger, …).
`workflowRun`/`workflowVersion`/`workspaceMember` were already `false`.
Non-system objects (company, person, note, opportunity, dashboard, task,
workflow) are untouched.
- **Backfill**: new fast instance command
(`2-13-…-1781277480000-backfill-non-ui-creatable-standard-system-objects.ts`)
runs `UPDATE core.objectMetadata SET isUICreatable = false` for those
standard system objects (symmetric `down`), registered in
`instance-commands.constant.ts`.

`isSystem` and Data-Model visibility logic are unchanged.

## Verification

- Front gate unit test (7 pass), standard-application suite incl.
callRecording (10 pass)
- `oxlint` clean on all changed files
- `twenty-server` and `twenty-front` typecheck green

🤖 https://claude.ai/code/session_01TF4kjD56hHjP31wkHPxPv3

---
_Generated by [Claude
Code](https://claude.ai/code/session_01TF4kjD56hHjP31wkHPxPv3)_

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21527?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->

---------

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Félix Malfait
2026-06-13 14:05:42 +02:00
committed by GitHub
parent e4a0dfe262
commit 036d9a2bcf
6 changed files with 74 additions and 4 deletions
@@ -44,7 +44,7 @@ export const STANDARD_COMMAND_MENU_ITEMS = {
shortLabel: 'New ${capitalize(objectMetadataItem.labelSingular)}',
availabilityType: CommandMenuItemAvailabilityType.GLOBAL_OBJECT_CONTEXT,
conditionalAvailabilityExpression:
'pageType == "INDEX_PAGE" and objectPermissions.canUpdateObjectRecords and not hasAnySoftDeleteFilterOnView and objectMetadataItem.isUICreatable and objectMetadataItem.isUIEditable and not objectMetadataItem.isSystem and not objectMetadataItem.isRemote',
'pageType == "INDEX_PAGE" and objectPermissions.canUpdateObjectRecords and not hasAnySoftDeleteFilterOnView and objectMetadataItem.isUICreatable and objectMetadataItem.isUIEditable and not objectMetadataItem.isRemote',
availabilityObjectMetadataUniversalIdentifier: null,
frontComponentUniversalIdentifier: null,
engineComponentKey: EngineComponentKey.CREATE_NEW_RECORD,
@@ -29,6 +29,7 @@ export const STANDARD_FLAT_OBJECT_METADATA_BUILDERS_BY_OBJECT_NAME = {
description: i18nLabel(msg`An attachment`),
icon: 'IconFileImport',
isSystem: true,
isUICreatable: false,
labelIdentifierFieldMetadataName: 'name',
},
workspaceId,
@@ -55,6 +56,7 @@ export const STANDARD_FLAT_OBJECT_METADATA_BUILDERS_BY_OBJECT_NAME = {
description: i18nLabel(msg`Blocklist`),
icon: 'IconForbid2',
isSystem: true,
isUICreatable: false,
labelIdentifierFieldMetadataName: 'handle',
},
workspaceId,
@@ -86,6 +88,7 @@ export const STANDARD_FLAT_OBJECT_METADATA_BUILDERS_BY_OBJECT_NAME = {
icon: 'IconCalendar',
isSystem: true,
isAuditLogged: false,
isUICreatable: false,
labelIdentifierFieldMetadataName: 'id',
},
workspaceId,
@@ -117,6 +120,7 @@ export const STANDARD_FLAT_OBJECT_METADATA_BUILDERS_BY_OBJECT_NAME = {
icon: 'IconCalendar',
isSystem: true,
isAuditLogged: false,
isUICreatable: false,
labelIdentifierFieldMetadataName: 'handle',
},
workspaceId,
@@ -147,6 +151,7 @@ export const STANDARD_FLAT_OBJECT_METADATA_BUILDERS_BY_OBJECT_NAME = {
icon: 'IconCalendar',
isSystem: true,
isAuditLogged: false,
isUICreatable: false,
labelIdentifierFieldMetadataName: 'title',
},
workspaceId,
@@ -177,6 +182,7 @@ export const STANDARD_FLAT_OBJECT_METADATA_BUILDERS_BY_OBJECT_NAME = {
icon: 'IconVideo',
isSystem: true,
isAuditLogged: false,
isUICreatable: false,
labelIdentifierFieldMetadataName: 'title',
},
workspaceId,
@@ -263,6 +269,7 @@ export const STANDARD_FLAT_OBJECT_METADATA_BUILDERS_BY_OBJECT_NAME = {
icon: 'IconMessage',
isSystem: true,
isAuditLogged: false,
isUICreatable: false,
labelIdentifierFieldMetadataName: 'id',
},
workspaceId,
@@ -301,6 +308,7 @@ export const STANDARD_FLAT_OBJECT_METADATA_BUILDERS_BY_OBJECT_NAME = {
icon: 'IconFolder',
isSystem: true,
isAuditLogged: false,
isUICreatable: false,
labelIdentifierFieldMetadataName: 'id',
},
workspaceId,
@@ -332,6 +340,7 @@ export const STANDARD_FLAT_OBJECT_METADATA_BUILDERS_BY_OBJECT_NAME = {
icon: 'IconUserCircle',
isSystem: true,
isAuditLogged: false,
isUICreatable: false,
labelIdentifierFieldMetadataName: 'handle',
},
workspaceId,
@@ -362,6 +371,7 @@ export const STANDARD_FLAT_OBJECT_METADATA_BUILDERS_BY_OBJECT_NAME = {
icon: 'IconMessage',
isSystem: true,
isAuditLogged: false,
isUICreatable: false,
labelIdentifierFieldMetadataName: 'subject',
},
workspaceId,
@@ -389,6 +399,7 @@ export const STANDARD_FLAT_OBJECT_METADATA_BUILDERS_BY_OBJECT_NAME = {
icon: 'IconMessage',
isSystem: true,
isAuditLogged: false,
isUICreatable: false,
labelIdentifierFieldMetadataName: 'subject',
},
workspaceId,
@@ -442,6 +453,7 @@ export const STANDARD_FLAT_OBJECT_METADATA_BUILDERS_BY_OBJECT_NAME = {
description: i18nLabel(msg`A note target`),
icon: 'IconCheckbox',
isSystem: true,
isUICreatable: false,
labelIdentifierFieldMetadataName: 'id',
},
workspaceId,
@@ -555,6 +567,7 @@ export const STANDARD_FLAT_OBJECT_METADATA_BUILDERS_BY_OBJECT_NAME = {
description: i18nLabel(msg`A task target`),
icon: 'IconCheckbox',
isSystem: true,
isUICreatable: false,
labelIdentifierFieldMetadataName: 'id',
},
workspaceId,
@@ -588,6 +601,7 @@ export const STANDARD_FLAT_OBJECT_METADATA_BUILDERS_BY_OBJECT_NAME = {
icon: 'IconTimelineEvent',
isSystem: true,
isAuditLogged: false,
isUICreatable: false,
labelIdentifierFieldMetadataName: 'name',
},
workspaceId,
@@ -645,6 +659,7 @@ export const STANDARD_FLAT_OBJECT_METADATA_BUILDERS_BY_OBJECT_NAME = {
description: i18nLabel(msg`A workflow automated trigger`),
icon: 'IconSettingsAutomation',
isSystem: true,
isUICreatable: false,
labelIdentifierFieldMetadataName: 'id',
},
workspaceId,