diff --git a/packages/twenty-front/src/modules/object-record/utils/__tests__/canCreateRecordsForObjectMetadataItem.test.ts b/packages/twenty-front/src/modules/object-record/utils/__tests__/canCreateRecordsForObjectMetadataItem.test.ts index 2dc691ca75..0ef345e183 100644 --- a/packages/twenty-front/src/modules/object-record/utils/__tests__/canCreateRecordsForObjectMetadataItem.test.ts +++ b/packages/twenty-front/src/modules/object-record/utils/__tests__/canCreateRecordsForObjectMetadataItem.test.ts @@ -48,7 +48,7 @@ describe('canCreateRecordsForObjectMetadataItem', () => { expect(result).toBe(false); }); - it('should return false when the object is a system object', () => { + it('should return true for a UI-creatable system object (isSystem only controls Data-Model visibility)', () => { const result = canCreateRecordsForObjectMetadataItem({ objectPermissions: objectPermissionsAllowingUpdate, objectMetadataItem: { @@ -57,7 +57,7 @@ describe('canCreateRecordsForObjectMetadataItem', () => { }, }); - expect(result).toBe(false); + expect(result).toBe(true); }); it('should return false when the object is remote', () => { diff --git a/packages/twenty-front/src/modules/object-record/utils/canCreateRecordsForObjectMetadataItem.ts b/packages/twenty-front/src/modules/object-record/utils/canCreateRecordsForObjectMetadataItem.ts index 1c2fc78a18..5c5383086d 100644 --- a/packages/twenty-front/src/modules/object-record/utils/canCreateRecordsForObjectMetadataItem.ts +++ b/packages/twenty-front/src/modules/object-record/utils/canCreateRecordsForObjectMetadataItem.ts @@ -11,6 +11,9 @@ type CanCreateRecordsForObjectMetadataItemParams = { }; // Single predicate for every generic "create a record" UI affordance. +// Creatability is driven solely by isUICreatable: isSystem only controls +// Data-Model visibility, so a system object can still be user-creatable +// (e.g. marketing message lists kept out of the Data Model). // Creation requires effective editability because today's inline creation UX // creates a blank record that the user must then be able to edit. // There is no CREATE permission yet, so canUpdateObjectRecords (checked @@ -21,7 +24,6 @@ export const canCreateRecordsForObjectMetadataItem = ({ }: CanCreateRecordsForObjectMetadataItemParams): boolean => { return ( objectMetadataItem.isUICreatable && - !objectMetadataItem.isSystem && !isObjectMetadataReadOnly({ objectPermissions, objectMetadataItem }) ); }; diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/2-13/2-13-instance-command-slow-1781277480000-backfill-non-ui-creatable-standard-system-objects.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/2-13/2-13-instance-command-slow-1781277480000-backfill-non-ui-creatable-standard-system-objects.ts new file mode 100644 index 0000000000..d347170af3 --- /dev/null +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/2-13/2-13-instance-command-slow-1781277480000-backfill-non-ui-creatable-standard-system-objects.ts @@ -0,0 +1,51 @@ +import { DataSource, QueryRunner } from 'typeorm'; + +import { RegisteredInstanceCommand } from 'src/engine/core-modules/upgrade/decorators/registered-instance-command.decorator'; +import { SlowInstanceCommand } from 'src/engine/core-modules/upgrade/interfaces/slow-instance-command.interface'; + +// The create-record UI affordance is now gated on isUICreatable alone (the +// !isSystem clause was dropped). Standard system objects relied on that clause +// to stay non-creatable, so their isUICreatable (default true) must be flipped +// to false to preserve behavior. Sync/system-created objects only — the +// user-creatable system objects (e.g. marketing message lists) keep true. +const NON_UI_CREATABLE_STANDARD_SYSTEM_OBJECT_NAMES = [ + 'attachment', + 'blocklist', + 'calendarChannelEventAssociation', + 'calendarEventParticipant', + 'calendarEvent', + 'callRecording', + 'messageChannelMessageAssociation', + 'messageChannelMessageAssociationMessageFolder', + 'messageParticipant', + 'messageThread', + 'message', + 'noteTarget', + 'taskTarget', + 'timelineActivity', + 'workflowAutomatedTrigger', +]; + +@RegisteredInstanceCommand('2.13.0', 1781277480000, { type: 'slow' }) +export class BackfillNonUiCreatableStandardSystemObjectsSlowInstanceCommand + implements SlowInstanceCommand +{ + async runDataMigration(dataSource: DataSource): Promise { + await dataSource.query( + `UPDATE "core"."objectMetadata" + SET "isUICreatable" = false + WHERE "isSystem" = true + AND "isUICreatable" = true + AND "nameSingular" = ANY($1)`, + [NON_UI_CREATABLE_STANDARD_SYSTEM_OBJECT_NAMES], + ); + } + + public async up(_queryRunner: QueryRunner): Promise { + return; + } + + public async down(_queryRunner: QueryRunner): Promise { + return; + } +} diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/instance-commands.constant.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/instance-commands.constant.ts index 4913d85ee5..3fa916c856 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/instance-commands.constant.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/instance-commands.constant.ts @@ -63,6 +63,7 @@ import { DropIsCustomFromObjectAndFieldMetadataFastInstanceCommand } from 'src/d import { DropEmailingDomainDriverColumnFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-11/2-11-instance-command-fast-1780926908000-drop-emailing-domain-driver-column'; import { ViewOverridableEntityFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-12/2-12-instance-command-fast-1781114009075-view-overridable-entity'; import { RenameIsUiReadOnlyToIsUiEditableFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-13/2-13-instance-command-fast-1781277453604-rename-is-ui-read-only-to-is-ui-editable'; +import { BackfillNonUiCreatableStandardSystemObjectsSlowInstanceCommand } from 'src/database/commands/upgrade-version-command/2-13/2-13-instance-command-slow-1781277480000-backfill-non-ui-creatable-standard-system-objects'; export const INSTANCE_COMMANDS = [ AddViewFieldGroupIdIndexOnViewFieldFastInstanceCommand, @@ -128,4 +129,5 @@ export const INSTANCE_COMMANDS = [ DropEmailingDomainDriverColumnFastInstanceCommand, ViewOverridableEntityFastInstanceCommand, RenameIsUiReadOnlyToIsUiEditableFastInstanceCommand, + BackfillNonUiCreatableStandardSystemObjectsSlowInstanceCommand, ]; diff --git a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/constants/standard-command-menu-item.constant.ts b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/constants/standard-command-menu-item.constant.ts index a52dfaa8e6..1bd69672f8 100644 --- a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/constants/standard-command-menu-item.constant.ts +++ b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/constants/standard-command-menu-item.constant.ts @@ -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, diff --git a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/object-metadata/create-standard-flat-object-metadata.util.ts b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/object-metadata/create-standard-flat-object-metadata.util.ts index 76edb9eccd..fe1eecd9c2 100644 --- a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/object-metadata/create-standard-flat-object-metadata.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/object-metadata/create-standard-flat-object-metadata.util.ts @@ -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,