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 dac1262d70d97ce84b39730454dd3579c494994a. 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:
+5
@@ -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';
|
||||
@@ -43,6 +44,7 @@ export class CalendarChannelEventAssociationWorkspaceEntity extends BaseWorkspac
|
||||
description: msg`Event external ID`,
|
||||
icon: 'IconCalendar',
|
||||
})
|
||||
@WorkspaceIsFieldUIReadOnly()
|
||||
@WorkspaceIsNullable()
|
||||
eventExternalId: string | null;
|
||||
|
||||
@@ -54,6 +56,7 @@ export class CalendarChannelEventAssociationWorkspaceEntity extends BaseWorkspac
|
||||
description: msg`Recurring Event ID`,
|
||||
icon: 'IconHistory',
|
||||
})
|
||||
@WorkspaceIsFieldUIReadOnly()
|
||||
@WorkspaceIsNullable()
|
||||
recurringEventExternalId: string | null;
|
||||
|
||||
@@ -68,6 +71,7 @@ export class CalendarChannelEventAssociationWorkspaceEntity extends BaseWorkspac
|
||||
inverseSideFieldKey: 'calendarChannelEventAssociations',
|
||||
onDelete: RelationOnDeleteAction.CASCADE,
|
||||
})
|
||||
@WorkspaceIsFieldUIReadOnly()
|
||||
calendarChannel: Relation<CalendarChannelWorkspaceEntity>;
|
||||
|
||||
@WorkspaceJoinColumn('calendarChannel')
|
||||
@@ -84,6 +88,7 @@ export class CalendarChannelEventAssociationWorkspaceEntity extends BaseWorkspac
|
||||
inverseSideFieldKey: 'calendarChannelEventAssociations',
|
||||
onDelete: RelationOnDeleteAction.CASCADE,
|
||||
})
|
||||
@WorkspaceIsFieldUIReadOnly()
|
||||
calendarEvent: Relation<CalendarEventWorkspaceEntity>;
|
||||
|
||||
@WorkspaceJoinColumn('calendarEvent')
|
||||
|
||||
+14
@@ -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';
|
||||
@@ -87,6 +88,7 @@ export class CalendarChannelWorkspaceEntity extends BaseWorkspaceEntity {
|
||||
description: msg`Handle`,
|
||||
icon: 'IconAt',
|
||||
})
|
||||
@WorkspaceIsFieldUIReadOnly()
|
||||
@WorkspaceIsNullable()
|
||||
handle: string | null;
|
||||
|
||||
@@ -129,6 +131,7 @@ export class CalendarChannelWorkspaceEntity extends BaseWorkspaceEntity {
|
||||
},
|
||||
],
|
||||
})
|
||||
@WorkspaceIsFieldUIReadOnly()
|
||||
@WorkspaceIsNullable()
|
||||
syncStatus: CalendarChannelSyncStatus | null;
|
||||
|
||||
@@ -190,6 +193,7 @@ export class CalendarChannelWorkspaceEntity extends BaseWorkspaceEntity {
|
||||
],
|
||||
defaultValue: `'${CalendarChannelSyncStage.PENDING_CONFIGURATION}'`,
|
||||
})
|
||||
@WorkspaceIsFieldUIReadOnly()
|
||||
syncStage: CalendarChannelSyncStage;
|
||||
|
||||
@WorkspaceField({
|
||||
@@ -214,6 +218,7 @@ export class CalendarChannelWorkspaceEntity extends BaseWorkspaceEntity {
|
||||
],
|
||||
defaultValue: `'${CalendarChannelVisibility.SHARE_EVERYTHING}'`,
|
||||
})
|
||||
@WorkspaceIsFieldUIReadOnly()
|
||||
visibility: string;
|
||||
|
||||
@WorkspaceField({
|
||||
@@ -225,6 +230,7 @@ export class CalendarChannelWorkspaceEntity extends BaseWorkspaceEntity {
|
||||
icon: 'IconUserCircle',
|
||||
defaultValue: true,
|
||||
})
|
||||
@WorkspaceIsFieldUIReadOnly()
|
||||
isContactAutoCreationEnabled: boolean;
|
||||
|
||||
@WorkspaceField({
|
||||
@@ -262,6 +268,7 @@ export class CalendarChannelWorkspaceEntity extends BaseWorkspaceEntity {
|
||||
],
|
||||
defaultValue: `'${CalendarChannelContactAutoCreationPolicy.AS_PARTICIPANT_AND_ORGANIZER}'`,
|
||||
})
|
||||
@WorkspaceIsFieldUIReadOnly()
|
||||
contactAutoCreationPolicy: CalendarChannelContactAutoCreationPolicy;
|
||||
|
||||
@WorkspaceField({
|
||||
@@ -272,6 +279,7 @@ export class CalendarChannelWorkspaceEntity extends BaseWorkspaceEntity {
|
||||
icon: 'IconRefresh',
|
||||
defaultValue: true,
|
||||
})
|
||||
@WorkspaceIsFieldUIReadOnly()
|
||||
isSyncEnabled: boolean;
|
||||
|
||||
@WorkspaceField({
|
||||
@@ -281,6 +289,7 @@ export class CalendarChannelWorkspaceEntity extends BaseWorkspaceEntity {
|
||||
description: msg`Sync Cursor. Used for syncing events from the calendar provider`,
|
||||
icon: 'IconReload',
|
||||
})
|
||||
@WorkspaceIsFieldUIReadOnly()
|
||||
@WorkspaceIsNullable()
|
||||
syncCursor: string | null;
|
||||
|
||||
@@ -291,6 +300,7 @@ export class CalendarChannelWorkspaceEntity extends BaseWorkspaceEntity {
|
||||
description: msg`Last sync date`,
|
||||
icon: 'IconHistory',
|
||||
})
|
||||
@WorkspaceIsFieldUIReadOnly()
|
||||
@WorkspaceIsNullable()
|
||||
syncedAt: string | null;
|
||||
|
||||
@@ -301,6 +311,7 @@ export class CalendarChannelWorkspaceEntity extends BaseWorkspaceEntity {
|
||||
description: msg`Sync stage started at`,
|
||||
icon: 'IconHistory',
|
||||
})
|
||||
@WorkspaceIsFieldUIReadOnly()
|
||||
@WorkspaceIsNullable()
|
||||
syncStageStartedAt: string | null;
|
||||
|
||||
@@ -312,6 +323,7 @@ export class CalendarChannelWorkspaceEntity extends BaseWorkspaceEntity {
|
||||
icon: 'IconX',
|
||||
defaultValue: 0,
|
||||
})
|
||||
@WorkspaceIsFieldUIReadOnly()
|
||||
throttleFailureCount: number;
|
||||
|
||||
@WorkspaceRelation({
|
||||
@@ -324,6 +336,7 @@ export class CalendarChannelWorkspaceEntity extends BaseWorkspaceEntity {
|
||||
inverseSideFieldKey: 'calendarChannels',
|
||||
onDelete: RelationOnDeleteAction.CASCADE,
|
||||
})
|
||||
@WorkspaceIsFieldUIReadOnly()
|
||||
connectedAccount: Relation<ConnectedAccountWorkspaceEntity>;
|
||||
|
||||
@WorkspaceJoinColumn('connectedAccount')
|
||||
@@ -339,6 +352,7 @@ export class CalendarChannelWorkspaceEntity extends BaseWorkspaceEntity {
|
||||
inverseSideTarget: () => CalendarChannelEventAssociationWorkspaceEntity,
|
||||
onDelete: RelationOnDeleteAction.CASCADE,
|
||||
})
|
||||
@WorkspaceIsFieldUIReadOnly()
|
||||
calendarChannelEventAssociations: Relation<
|
||||
CalendarChannelEventAssociationWorkspaceEntity[]
|
||||
>;
|
||||
|
||||
+8
@@ -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';
|
||||
@@ -47,6 +48,7 @@ export class CalendarEventParticipantWorkspaceEntity extends BaseWorkspaceEntity
|
||||
description: msg`Handle`,
|
||||
icon: 'IconMail',
|
||||
})
|
||||
@WorkspaceIsFieldUIReadOnly()
|
||||
@WorkspaceIsNullable()
|
||||
handle: string | null;
|
||||
|
||||
@@ -57,6 +59,7 @@ export class CalendarEventParticipantWorkspaceEntity extends BaseWorkspaceEntity
|
||||
description: msg`Display Name`,
|
||||
icon: 'IconUser',
|
||||
})
|
||||
@WorkspaceIsFieldUIReadOnly()
|
||||
@WorkspaceIsNullable()
|
||||
displayName: string | null;
|
||||
|
||||
@@ -68,6 +71,7 @@ export class CalendarEventParticipantWorkspaceEntity extends BaseWorkspaceEntity
|
||||
icon: 'IconUser',
|
||||
defaultValue: false,
|
||||
})
|
||||
@WorkspaceIsFieldUIReadOnly()
|
||||
isOrganizer: boolean;
|
||||
|
||||
@WorkspaceField({
|
||||
@@ -104,6 +108,7 @@ export class CalendarEventParticipantWorkspaceEntity extends BaseWorkspaceEntity
|
||||
],
|
||||
defaultValue: `'${CalendarEventParticipantResponseStatus.NEEDS_ACTION}'`,
|
||||
})
|
||||
@WorkspaceIsFieldUIReadOnly()
|
||||
responseStatus: string;
|
||||
|
||||
@WorkspaceRelation({
|
||||
@@ -116,6 +121,7 @@ export class CalendarEventParticipantWorkspaceEntity extends BaseWorkspaceEntity
|
||||
inverseSideFieldKey: 'calendarEventParticipants',
|
||||
onDelete: RelationOnDeleteAction.CASCADE,
|
||||
})
|
||||
@WorkspaceIsFieldUIReadOnly()
|
||||
calendarEvent: Relation<CalendarEventWorkspaceEntity>;
|
||||
|
||||
@WorkspaceJoinColumn('calendarEvent')
|
||||
@@ -131,6 +137,7 @@ export class CalendarEventParticipantWorkspaceEntity extends BaseWorkspaceEntity
|
||||
inverseSideFieldKey: 'calendarEventParticipants',
|
||||
onDelete: RelationOnDeleteAction.SET_NULL,
|
||||
})
|
||||
@WorkspaceIsFieldUIReadOnly()
|
||||
@WorkspaceIsNullable()
|
||||
person: Relation<PersonWorkspaceEntity> | null;
|
||||
|
||||
@@ -147,6 +154,7 @@ export class CalendarEventParticipantWorkspaceEntity extends BaseWorkspaceEntity
|
||||
inverseSideFieldKey: 'calendarEventParticipants',
|
||||
onDelete: RelationOnDeleteAction.SET_NULL,
|
||||
})
|
||||
@WorkspaceIsFieldUIReadOnly()
|
||||
@WorkspaceIsNullable()
|
||||
workspaceMember: Relation<WorkspaceMemberWorkspaceEntity> | null;
|
||||
|
||||
|
||||
+15
-2
@@ -12,9 +12,9 @@ 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 { WorkspaceIsObjectUIReadOnly } from 'src/engine/twenty-orm/decorators/workspace-is-object-ui-readonly.decorator';
|
||||
import { WorkspaceIsSystem } from 'src/engine/twenty-orm/decorators/workspace-is-system.decorator';
|
||||
import { WorkspaceRelation } from 'src/engine/twenty-orm/decorators/workspace-relation.decorator';
|
||||
import { CALENDAR_EVENT_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids';
|
||||
@@ -34,7 +34,6 @@ import { CalendarEventParticipantWorkspaceEntity } from 'src/modules/calendar/co
|
||||
})
|
||||
@WorkspaceIsSystem()
|
||||
@WorkspaceIsNotAuditLogged()
|
||||
@WorkspaceIsObjectUIReadOnly()
|
||||
export class CalendarEventWorkspaceEntity extends BaseWorkspaceEntity {
|
||||
@WorkspaceField({
|
||||
standardId: CALENDAR_EVENT_STANDARD_FIELD_IDS.title,
|
||||
@@ -43,6 +42,7 @@ export class CalendarEventWorkspaceEntity extends BaseWorkspaceEntity {
|
||||
description: msg`Title`,
|
||||
icon: 'IconH1',
|
||||
})
|
||||
@WorkspaceIsFieldUIReadOnly()
|
||||
@WorkspaceIsNullable()
|
||||
title: string | null;
|
||||
|
||||
@@ -54,6 +54,7 @@ export class CalendarEventWorkspaceEntity extends BaseWorkspaceEntity {
|
||||
icon: 'IconCalendarCancel',
|
||||
defaultValue: false,
|
||||
})
|
||||
@WorkspaceIsFieldUIReadOnly()
|
||||
isCanceled: boolean;
|
||||
|
||||
@WorkspaceField({
|
||||
@@ -64,6 +65,7 @@ export class CalendarEventWorkspaceEntity extends BaseWorkspaceEntity {
|
||||
icon: 'IconHours24',
|
||||
defaultValue: false,
|
||||
})
|
||||
@WorkspaceIsFieldUIReadOnly()
|
||||
isFullDay: boolean;
|
||||
|
||||
@WorkspaceField({
|
||||
@@ -73,6 +75,7 @@ export class CalendarEventWorkspaceEntity extends BaseWorkspaceEntity {
|
||||
description: msg`Start Date`,
|
||||
icon: 'IconCalendarClock',
|
||||
})
|
||||
@WorkspaceIsFieldUIReadOnly()
|
||||
@WorkspaceIsNullable()
|
||||
startsAt: string | null;
|
||||
|
||||
@@ -83,6 +86,7 @@ export class CalendarEventWorkspaceEntity extends BaseWorkspaceEntity {
|
||||
description: msg`End Date`,
|
||||
icon: 'IconCalendarClock',
|
||||
})
|
||||
@WorkspaceIsFieldUIReadOnly()
|
||||
@WorkspaceIsNullable()
|
||||
endsAt: string | null;
|
||||
|
||||
@@ -93,6 +97,7 @@ export class CalendarEventWorkspaceEntity extends BaseWorkspaceEntity {
|
||||
description: msg`Creation DateTime`,
|
||||
icon: 'IconCalendarPlus',
|
||||
})
|
||||
@WorkspaceIsFieldUIReadOnly()
|
||||
@WorkspaceIsNullable()
|
||||
externalCreatedAt: string | null;
|
||||
|
||||
@@ -103,6 +108,7 @@ export class CalendarEventWorkspaceEntity extends BaseWorkspaceEntity {
|
||||
description: msg`Update DateTime`,
|
||||
icon: 'IconCalendarCog',
|
||||
})
|
||||
@WorkspaceIsFieldUIReadOnly()
|
||||
@WorkspaceIsNullable()
|
||||
externalUpdatedAt: string | null;
|
||||
|
||||
@@ -113,6 +119,7 @@ export class CalendarEventWorkspaceEntity extends BaseWorkspaceEntity {
|
||||
description: msg`Description`,
|
||||
icon: 'IconFileDescription',
|
||||
})
|
||||
@WorkspaceIsFieldUIReadOnly()
|
||||
@WorkspaceIsNullable()
|
||||
description: string | null;
|
||||
|
||||
@@ -123,6 +130,7 @@ export class CalendarEventWorkspaceEntity extends BaseWorkspaceEntity {
|
||||
description: msg`Location`,
|
||||
icon: 'IconMapPin',
|
||||
})
|
||||
@WorkspaceIsFieldUIReadOnly()
|
||||
@WorkspaceIsNullable()
|
||||
location: string | null;
|
||||
|
||||
@@ -133,6 +141,7 @@ export class CalendarEventWorkspaceEntity extends BaseWorkspaceEntity {
|
||||
description: msg`iCal UID`,
|
||||
icon: 'IconKey',
|
||||
})
|
||||
@WorkspaceIsFieldUIReadOnly()
|
||||
@WorkspaceIsNullable()
|
||||
iCalUid: string | null;
|
||||
|
||||
@@ -143,6 +152,7 @@ export class CalendarEventWorkspaceEntity extends BaseWorkspaceEntity {
|
||||
description: msg`Conference Solution`,
|
||||
icon: 'IconScreenShare',
|
||||
})
|
||||
@WorkspaceIsFieldUIReadOnly()
|
||||
@WorkspaceIsNullable()
|
||||
conferenceSolution: string | null;
|
||||
|
||||
@@ -153,6 +163,7 @@ export class CalendarEventWorkspaceEntity extends BaseWorkspaceEntity {
|
||||
description: msg`Meet Link`,
|
||||
icon: 'IconLink',
|
||||
})
|
||||
@WorkspaceIsFieldUIReadOnly()
|
||||
@WorkspaceIsNullable()
|
||||
conferenceLink: LinksMetadata;
|
||||
|
||||
@@ -166,6 +177,7 @@ export class CalendarEventWorkspaceEntity extends BaseWorkspaceEntity {
|
||||
inverseSideTarget: () => CalendarChannelEventAssociationWorkspaceEntity,
|
||||
onDelete: RelationOnDeleteAction.CASCADE,
|
||||
})
|
||||
@WorkspaceIsFieldUIReadOnly()
|
||||
calendarChannelEventAssociations: Relation<
|
||||
CalendarChannelEventAssociationWorkspaceEntity[]
|
||||
>;
|
||||
@@ -179,6 +191,7 @@ export class CalendarEventWorkspaceEntity extends BaseWorkspaceEntity {
|
||||
inverseSideTarget: () => CalendarEventParticipantWorkspaceEntity,
|
||||
onDelete: RelationOnDeleteAction.CASCADE,
|
||||
})
|
||||
@WorkspaceIsFieldUIReadOnly()
|
||||
calendarEventParticipants: Relation<
|
||||
CalendarEventParticipantWorkspaceEntity[]
|
||||
>;
|
||||
|
||||
Reference in New Issue
Block a user