Allow custom fields to be editable for system objects and prevent the error for timeline activity on system object pages. (#16268)

Solves #16015 

- Added a check in `useTimelineActivities.ts` to see if the system
object page we're viewing has timelineActivity being tracked before
querying to get the activity history.
- Removed @WorkspaceIsObjectUIReadOnly decorator from system objects and
added @WorkspaceIsFieldUIReadOnly to standard and system fields to allow
custom field edits as requested in the issue.
- Did not add calendarEvents or other system objects to timelineActivity
just yet since keeping track of timeline activity for every one of them
felt counter-intuitive and bloated. In order to determine which objects
need timelineActivity, I think we need to fix the broken views of system
objects first, such as the one in the attached screenshots - a good
number of them are broken. The check I added to
`useTimelineActivities.ts` hook displays timeline activity as empty for
the time - error would not be shown as suggested by Thomas.

<img width="1062" height="858" alt="image"
src="https://github.com/user-attachments/assets/e877e0fe-b665-46e3-b785-e84f2af7f833"
/>

<br />

<img width="1061" height="858" alt="image"
src="https://github.com/user-attachments/assets/0eba8c1c-444a-4b13-beda-64b95cf39077"
/>

- Editing custom fields on calendar events (and other objects without
position fields) crashed with TypeError: Cannot convert undefined or
null to object in sortCachedObjectEdges. This happened because some
cached queries had empty orderBy arrays ([]), and the optimistic effect
tried to sort with them. Objects without position fields returned
empty orderBy arrays when there were no sorts, while objects with
position fields automatically got [{ position: 'AscNullsFirst' }].
The backend always adds { id: 'AscNullsFirst' } as a fallback, but
the frontend didn't match this. So, I added { id: 'AscNullsFirst' } to
the Frontend as default orderBy when there are no sorts and no position
field.

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> Apply per-field UI read-only to system/standard fields and skip
timeline activity fetching when the target object isn’t related; refine
record-table cell open/navigation behavior.
> 
> - Server: Replace object-level UI read-only with per-field
`isUIReadOnly` across many standard objects (e.g., `calendarEvent`,
`workspaceMember`, messaging, calendar, favorites, attachments,
workflows, etc.), and mirror this via `@WorkspaceIsFieldUIReadOnly` on
workspace entities.
> - Frontend: In `useTimelineActivities.ts`, check object metadata for a
relation to `timelineActivity` and `skip` the query when absent,
preventing errors on system object pages.
> - Frontend: Simplify/adjust record-table cell logic—remove unused
args, allow navigation from first column when non-empty, block editing
for read-only records (while still allowing navigation), and update
button handlers/hover styles accordingly.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
dac1262d70. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

---------

Co-authored-by: Félix Malfait <felix@twenty.com>
This commit is contained in:
Abdullah.
2025-12-22 22:22:30 +00:00
committed by GitHub
co-authored by Félix Malfait
parent 5003fc4196
commit c6ea7ae288
52 changed files with 465 additions and 77 deletions
@@ -1,6 +1,6 @@
import { msg } from '@lingui/core/macro';
import { FieldMetadataType, RelationOnDeleteAction } from 'twenty-shared/types';
import { STANDARD_OBJECT_IDS } from 'twenty-shared/metadata';
import { FieldMetadataType, RelationOnDeleteAction } from 'twenty-shared/types';
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
import { type Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/relation.interface';
@@ -9,6 +9,7 @@ import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity
import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator';
import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator';
import { WorkspaceIndex } from 'src/engine/twenty-orm/decorators/workspace-index.decorator';
import { WorkspaceIsFieldUIReadOnly } from 'src/engine/twenty-orm/decorators/workspace-is-field-ui-readonly.decorator';
import { WorkspaceIsNotAuditLogged } from 'src/engine/twenty-orm/decorators/workspace-is-not-audit-logged.decorator';
import { WorkspaceIsNullable } from 'src/engine/twenty-orm/decorators/workspace-is-nullable.decorator';
import { WorkspaceIsSystem } from 'src/engine/twenty-orm/decorators/workspace-is-system.decorator';
@@ -47,6 +48,7 @@ export class MessageChannelMessageAssociationWorkspaceEntity extends BaseWorkspa
description: msg`Message id from the messaging provider`,
icon: 'IconHash',
})
@WorkspaceIsFieldUIReadOnly()
@WorkspaceIsNullable()
messageExternalId: string | null;
@@ -58,6 +60,7 @@ export class MessageChannelMessageAssociationWorkspaceEntity extends BaseWorkspa
description: msg`Thread id from the messaging provider`,
icon: 'IconHash',
})
@WorkspaceIsFieldUIReadOnly()
@WorkspaceIsNullable()
messageThreadExternalId: string | null;
@@ -83,6 +86,7 @@ export class MessageChannelMessageAssociationWorkspaceEntity extends BaseWorkspa
],
defaultValue: `'${MessageDirection.INCOMING}'`,
})
@WorkspaceIsFieldUIReadOnly()
direction: MessageDirection;
@WorkspaceRelation({
@@ -96,6 +100,7 @@ export class MessageChannelMessageAssociationWorkspaceEntity extends BaseWorkspa
inverseSideFieldKey: 'messageChannelMessageAssociations',
onDelete: RelationOnDeleteAction.CASCADE,
})
@WorkspaceIsFieldUIReadOnly()
@WorkspaceIsNullable()
messageChannel: Relation<MessageChannelWorkspaceEntity> | null;
@@ -112,6 +117,7 @@ export class MessageChannelMessageAssociationWorkspaceEntity extends BaseWorkspa
inverseSideFieldKey: 'messageChannelMessageAssociations',
onDelete: RelationOnDeleteAction.CASCADE,
})
@WorkspaceIsFieldUIReadOnly()
@WorkspaceIsNullable()
message: Relation<MessageWorkspaceEntity> | null;
@@ -10,6 +10,7 @@ import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/i
import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity';
import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator';
import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator';
import { WorkspaceIsFieldUIReadOnly } from 'src/engine/twenty-orm/decorators/workspace-is-field-ui-readonly.decorator';
import { WorkspaceIsNotAuditLogged } from 'src/engine/twenty-orm/decorators/workspace-is-not-audit-logged.decorator';
import { WorkspaceIsNullable } from 'src/engine/twenty-orm/decorators/workspace-is-nullable.decorator';
import { WorkspaceIsSystem } from 'src/engine/twenty-orm/decorators/workspace-is-system.decorator';
@@ -137,6 +138,7 @@ export class MessageChannelWorkspaceEntity extends BaseWorkspaceEntity {
],
defaultValue: `'${MessageChannelVisibility.SHARE_EVERYTHING}'`,
})
@WorkspaceIsFieldUIReadOnly()
visibility: string;
@WorkspaceField({
@@ -146,6 +148,7 @@ export class MessageChannelWorkspaceEntity extends BaseWorkspaceEntity {
description: msg`Handle`,
icon: 'IconAt',
})
@WorkspaceIsFieldUIReadOnly()
@WorkspaceIsNullable()
handle: string | null;
@@ -171,6 +174,7 @@ export class MessageChannelWorkspaceEntity extends BaseWorkspaceEntity {
],
defaultValue: `'${MessageChannelType.EMAIL}'`,
})
@WorkspaceIsFieldUIReadOnly()
type: string;
// TODO: Deprecate this field and migrate data to contactAutoCreationFor
@@ -182,6 +186,7 @@ export class MessageChannelWorkspaceEntity extends BaseWorkspaceEntity {
icon: 'IconUserCircle',
defaultValue: true,
})
@WorkspaceIsFieldUIReadOnly()
isContactAutoCreationEnabled: boolean;
@WorkspaceField({
@@ -212,6 +217,7 @@ export class MessageChannelWorkspaceEntity extends BaseWorkspaceEntity {
],
defaultValue: `'${MessageChannelContactAutoCreationPolicy.SENT}'`,
})
@WorkspaceIsFieldUIReadOnly()
contactAutoCreationPolicy: MessageChannelContactAutoCreationPolicy;
@WorkspaceField({
@@ -236,6 +242,7 @@ export class MessageChannelWorkspaceEntity extends BaseWorkspaceEntity {
],
defaultValue: `'${MessageFolderImportPolicy.ALL_FOLDERS}'`,
})
@WorkspaceIsFieldUIReadOnly()
messageFolderImportPolicy: MessageFolderImportPolicy;
@WorkspaceField({
@@ -246,6 +253,7 @@ export class MessageChannelWorkspaceEntity extends BaseWorkspaceEntity {
icon: 'IconBriefcase',
defaultValue: true,
})
@WorkspaceIsFieldUIReadOnly()
excludeNonProfessionalEmails: boolean;
@WorkspaceField({
@@ -256,6 +264,7 @@ export class MessageChannelWorkspaceEntity extends BaseWorkspaceEntity {
icon: 'IconUsersGroup',
defaultValue: true,
})
@WorkspaceIsFieldUIReadOnly()
excludeGroupEmails: boolean;
@WorkspaceField({
@@ -286,6 +295,7 @@ export class MessageChannelWorkspaceEntity extends BaseWorkspaceEntity {
],
defaultValue: `'${MessageChannelPendingGroupEmailsAction.NONE}'`,
})
@WorkspaceIsFieldUIReadOnly()
pendingGroupEmailsAction: MessageChannelPendingGroupEmailsAction;
@WorkspaceField({
@@ -296,6 +306,7 @@ export class MessageChannelWorkspaceEntity extends BaseWorkspaceEntity {
icon: 'IconRefresh',
defaultValue: true,
})
@WorkspaceIsFieldUIReadOnly()
isSyncEnabled: boolean;
@WorkspaceField({
@@ -305,6 +316,7 @@ export class MessageChannelWorkspaceEntity extends BaseWorkspaceEntity {
description: msg`Last sync cursor`,
icon: 'IconHistory',
})
@WorkspaceIsFieldUIReadOnly()
@WorkspaceIsNullable()
syncCursor: string | null;
@@ -315,6 +327,7 @@ export class MessageChannelWorkspaceEntity extends BaseWorkspaceEntity {
description: msg`Last sync date`,
icon: 'IconHistory',
})
@WorkspaceIsFieldUIReadOnly()
@WorkspaceIsNullable()
syncedAt: string | null;
@@ -357,6 +370,7 @@ export class MessageChannelWorkspaceEntity extends BaseWorkspaceEntity {
},
],
})
@WorkspaceIsFieldUIReadOnly()
@WorkspaceIsNullable()
syncStatus: MessageChannelSyncStatus | null;
@@ -418,6 +432,7 @@ export class MessageChannelWorkspaceEntity extends BaseWorkspaceEntity {
],
defaultValue: `'${MessageChannelSyncStage.PENDING_CONFIGURATION}'`,
})
@WorkspaceIsFieldUIReadOnly()
syncStage: MessageChannelSyncStage;
@WorkspaceField({
@@ -427,6 +442,7 @@ export class MessageChannelWorkspaceEntity extends BaseWorkspaceEntity {
description: msg`Sync stage started at`,
icon: 'IconHistory',
})
@WorkspaceIsFieldUIReadOnly()
@WorkspaceIsNullable()
syncStageStartedAt: string | null;
@@ -438,6 +454,7 @@ export class MessageChannelWorkspaceEntity extends BaseWorkspaceEntity {
icon: 'IconX',
defaultValue: 0,
})
@WorkspaceIsFieldUIReadOnly()
throttleFailureCount: number;
@WorkspaceRelation({
@@ -450,6 +467,7 @@ export class MessageChannelWorkspaceEntity extends BaseWorkspaceEntity {
inverseSideFieldKey: 'messageChannels',
onDelete: RelationOnDeleteAction.CASCADE,
})
@WorkspaceIsFieldUIReadOnly()
connectedAccount: Relation<ConnectedAccountWorkspaceEntity>;
@WorkspaceJoinColumn('connectedAccount')
@@ -465,6 +483,7 @@ export class MessageChannelWorkspaceEntity extends BaseWorkspaceEntity {
inverseSideTarget: () => MessageChannelMessageAssociationWorkspaceEntity,
onDelete: RelationOnDeleteAction.CASCADE,
})
@WorkspaceIsFieldUIReadOnly()
@WorkspaceIsNullable()
messageChannelMessageAssociations: Relation<
MessageChannelMessageAssociationWorkspaceEntity[]
@@ -479,6 +498,7 @@ export class MessageChannelWorkspaceEntity extends BaseWorkspaceEntity {
inverseSideTarget: () => MessageFolderWorkspaceEntity,
onDelete: RelationOnDeleteAction.CASCADE,
})
@WorkspaceIsFieldUIReadOnly()
@WorkspaceIsNullable()
messageFolders: Relation<MessageFolderWorkspaceEntity[]>;
}
@@ -11,6 +11,7 @@ import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity
import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator';
import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator';
import { WorkspaceIndex } from 'src/engine/twenty-orm/decorators/workspace-index.decorator';
import { WorkspaceIsFieldUIReadOnly } from 'src/engine/twenty-orm/decorators/workspace-is-field-ui-readonly.decorator';
import { WorkspaceIsNotAuditLogged } from 'src/engine/twenty-orm/decorators/workspace-is-not-audit-logged.decorator';
import { WorkspaceIsNullable } from 'src/engine/twenty-orm/decorators/workspace-is-nullable.decorator';
import { WorkspaceIsSystem } from 'src/engine/twenty-orm/decorators/workspace-is-system.decorator';
@@ -52,6 +53,7 @@ export class MessageFolderWorkspaceEntity extends BaseWorkspaceEntity {
description: msg`Folder name`,
icon: 'IconFolder',
})
@WorkspaceIsFieldUIReadOnly()
@WorkspaceIsNullable()
name: string | null;
@@ -65,6 +67,7 @@ export class MessageFolderWorkspaceEntity extends BaseWorkspaceEntity {
inverseSideFieldKey: 'messageFolders',
onDelete: RelationOnDeleteAction.CASCADE,
})
@WorkspaceIsFieldUIReadOnly()
messageChannel: Relation<MessageChannelWorkspaceEntity>;
@WorkspaceField({
@@ -74,6 +77,7 @@ export class MessageFolderWorkspaceEntity extends BaseWorkspaceEntity {
description: msg`Sync Cursor`,
icon: 'IconHash',
})
@WorkspaceIsFieldUIReadOnly()
@WorkspaceIsNullable()
syncCursor: string | null;
@@ -85,6 +89,7 @@ export class MessageFolderWorkspaceEntity extends BaseWorkspaceEntity {
icon: 'IconCheck',
defaultValue: false,
})
@WorkspaceIsFieldUIReadOnly()
isSentFolder: boolean;
@WorkspaceField({
@@ -95,6 +100,7 @@ export class MessageFolderWorkspaceEntity extends BaseWorkspaceEntity {
icon: 'IconCheck',
defaultValue: false,
})
@WorkspaceIsFieldUIReadOnly()
isSynced: boolean;
@WorkspaceField({
@@ -105,6 +111,7 @@ export class MessageFolderWorkspaceEntity extends BaseWorkspaceEntity {
icon: 'IconFolder',
defaultValue: null,
})
@WorkspaceIsFieldUIReadOnly()
@WorkspaceIsNullable()
parentFolderId: string | null;
@@ -116,6 +123,7 @@ export class MessageFolderWorkspaceEntity extends BaseWorkspaceEntity {
icon: 'IconHash',
defaultValue: null,
})
@WorkspaceIsFieldUIReadOnly()
@WorkspaceIsNullable()
externalId: string | null;
@@ -141,6 +149,7 @@ export class MessageFolderWorkspaceEntity extends BaseWorkspaceEntity {
],
defaultValue: `'${MessageFolderPendingSyncAction.NONE}'`,
})
@WorkspaceIsFieldUIReadOnly()
pendingSyncAction: MessageFolderPendingSyncAction;
@WorkspaceJoinColumn('messageChannel')
@@ -12,6 +12,7 @@ import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/i
import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity';
import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator';
import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator';
import { WorkspaceIsFieldUIReadOnly } from 'src/engine/twenty-orm/decorators/workspace-is-field-ui-readonly.decorator';
import { WorkspaceIsNotAuditLogged } from 'src/engine/twenty-orm/decorators/workspace-is-not-audit-logged.decorator';
import { WorkspaceIsNullable } from 'src/engine/twenty-orm/decorators/workspace-is-nullable.decorator';
import { WorkspaceIsSystem } from 'src/engine/twenty-orm/decorators/workspace-is-system.decorator';
@@ -70,6 +71,7 @@ export class MessageParticipantWorkspaceEntity extends BaseWorkspaceEntity {
],
defaultValue: `'${MessageParticipantRole.FROM}'`,
})
@WorkspaceIsFieldUIReadOnly()
role: MessageParticipantRole;
@WorkspaceField({
@@ -79,6 +81,7 @@ export class MessageParticipantWorkspaceEntity extends BaseWorkspaceEntity {
description: msg`Handle`,
icon: 'IconAt',
})
@WorkspaceIsFieldUIReadOnly()
@WorkspaceIsNullable()
handle: string | null;
@@ -89,6 +92,7 @@ export class MessageParticipantWorkspaceEntity extends BaseWorkspaceEntity {
description: msg`Display Name`,
icon: 'IconUser',
})
@WorkspaceIsFieldUIReadOnly()
@WorkspaceIsNullable()
displayName: string | null;
@@ -102,6 +106,7 @@ export class MessageParticipantWorkspaceEntity extends BaseWorkspaceEntity {
inverseSideFieldKey: 'messageParticipants',
onDelete: RelationOnDeleteAction.CASCADE,
})
@WorkspaceIsFieldUIReadOnly()
message: Relation<MessageWorkspaceEntity>;
@WorkspaceJoinColumn('message')
@@ -117,6 +122,7 @@ export class MessageParticipantWorkspaceEntity extends BaseWorkspaceEntity {
inverseSideFieldKey: 'messageParticipants',
onDelete: RelationOnDeleteAction.SET_NULL,
})
@WorkspaceIsFieldUIReadOnly()
@WorkspaceIsNullable()
person: Relation<PersonWorkspaceEntity> | null;
@@ -133,6 +139,7 @@ export class MessageParticipantWorkspaceEntity extends BaseWorkspaceEntity {
inverseSideFieldKey: 'messageParticipants',
onDelete: RelationOnDeleteAction.SET_NULL,
})
@WorkspaceIsFieldUIReadOnly()
@WorkspaceIsNullable()
workspaceMember: Relation<WorkspaceMemberWorkspaceEntity> | null;
@@ -7,6 +7,7 @@ import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/i
import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity';
import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator';
import { WorkspaceIsFieldUIReadOnly } from 'src/engine/twenty-orm/decorators/workspace-is-field-ui-readonly.decorator';
import { WorkspaceIsNotAuditLogged } from 'src/engine/twenty-orm/decorators/workspace-is-not-audit-logged.decorator';
import { WorkspaceIsNullable } from 'src/engine/twenty-orm/decorators/workspace-is-nullable.decorator';
import { WorkspaceIsSystem } from 'src/engine/twenty-orm/decorators/workspace-is-system.decorator';
@@ -36,6 +37,7 @@ export class MessageThreadWorkspaceEntity extends BaseWorkspaceEntity {
inverseSideTarget: () => MessageWorkspaceEntity,
onDelete: RelationOnDeleteAction.CASCADE,
})
@WorkspaceIsFieldUIReadOnly()
@WorkspaceIsNullable()
messages: Relation<MessageWorkspaceEntity[]>;
}
@@ -8,6 +8,7 @@ import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/i
import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity';
import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator';
import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator';
import { WorkspaceIsFieldUIReadOnly } from 'src/engine/twenty-orm/decorators/workspace-is-field-ui-readonly.decorator';
import { WorkspaceIsNotAuditLogged } from 'src/engine/twenty-orm/decorators/workspace-is-not-audit-logged.decorator';
import { WorkspaceIsNullable } from 'src/engine/twenty-orm/decorators/workspace-is-nullable.decorator';
import { WorkspaceIsSystem } from 'src/engine/twenty-orm/decorators/workspace-is-system.decorator';
@@ -39,6 +40,7 @@ export class MessageWorkspaceEntity extends BaseWorkspaceEntity {
description: msg`Message id from the message header`,
icon: 'IconHash',
})
@WorkspaceIsFieldUIReadOnly()
@WorkspaceIsNullable()
headerMessageId: string | null;
@@ -49,6 +51,7 @@ export class MessageWorkspaceEntity extends BaseWorkspaceEntity {
description: msg`Subject`,
icon: 'IconMessage',
})
@WorkspaceIsFieldUIReadOnly()
@WorkspaceIsNullable()
subject: string | null;
@@ -59,6 +62,7 @@ export class MessageWorkspaceEntity extends BaseWorkspaceEntity {
description: msg`Text`,
icon: 'IconMessage',
})
@WorkspaceIsFieldUIReadOnly()
@WorkspaceIsNullable()
text: string | null;
@@ -69,6 +73,7 @@ export class MessageWorkspaceEntity extends BaseWorkspaceEntity {
description: msg`The date the message was received`,
icon: 'IconCalendar',
})
@WorkspaceIsFieldUIReadOnly()
@WorkspaceIsNullable()
receivedAt: Date | null;
@@ -82,6 +87,7 @@ export class MessageWorkspaceEntity extends BaseWorkspaceEntity {
inverseSideFieldKey: 'messages',
onDelete: RelationOnDeleteAction.CASCADE,
})
@WorkspaceIsFieldUIReadOnly()
@WorkspaceIsNullable()
messageThread: Relation<MessageThreadWorkspaceEntity> | null;
@@ -97,6 +103,7 @@ export class MessageWorkspaceEntity extends BaseWorkspaceEntity {
inverseSideTarget: () => MessageParticipantWorkspaceEntity,
onDelete: RelationOnDeleteAction.CASCADE,
})
@WorkspaceIsFieldUIReadOnly()
@WorkspaceIsNullable()
messageParticipants: Relation<MessageParticipantWorkspaceEntity[]>;
@@ -109,6 +116,7 @@ export class MessageWorkspaceEntity extends BaseWorkspaceEntity {
inverseSideTarget: () => MessageChannelMessageAssociationWorkspaceEntity,
onDelete: RelationOnDeleteAction.CASCADE,
})
@WorkspaceIsFieldUIReadOnly()
@WorkspaceIsNullable()
messageChannelMessageAssociations: Relation<
MessageChannelMessageAssociationWorkspaceEntity[]