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:
+13
@@ -2,6 +2,7 @@ import { useLinkedObjectsTitle } from '@/activities/timeline-activities/hooks/us
|
||||
import { type TimelineActivity } from '@/activities/timeline-activities/types/TimelineActivity';
|
||||
import { type ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
|
||||
import { getActivityTargetObjectFieldIdName } from '@/activities/utils/getActivityTargetObjectFieldIdName';
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { useGenerateDepthRecordGqlFieldsFromObject } from '@/object-record/graphql/record-gql-fields/hooks/useGenerateDepthRecordGqlFieldsFromObject';
|
||||
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
|
||||
@@ -23,6 +24,17 @@ export const useTimelineActivities = (
|
||||
nameSingular: targetableObject.targetObjectNameSingular,
|
||||
});
|
||||
|
||||
const { objectMetadataItem: timelineActivityMetadata } =
|
||||
useObjectMetadataItem({
|
||||
objectNameSingular: CoreObjectNameSingular.TimelineActivity,
|
||||
});
|
||||
|
||||
const hasTimelineActivityField = timelineActivityMetadata.fields.some(
|
||||
(field) =>
|
||||
field.relation?.targetObjectMetadata?.nameSingular ===
|
||||
targetableObject.targetObjectNameSingular,
|
||||
);
|
||||
|
||||
const { recordGqlFields: depthOneRecordGqlFields } =
|
||||
useGenerateDepthRecordGqlFieldsFromObject({
|
||||
objectNameSingular: CoreObjectNameSingular.TimelineActivity,
|
||||
@@ -34,6 +46,7 @@ export const useTimelineActivities = (
|
||||
loading: loadingTimelineActivities,
|
||||
fetchMoreRecords,
|
||||
} = useFindManyRecords<TimelineActivity>({
|
||||
skip: !hasTimelineActivityField,
|
||||
objectNameSingular: CoreObjectNameSingular.TimelineActivity,
|
||||
filter: {
|
||||
[targetableObjectFieldIdName]: {
|
||||
|
||||
+1
-1
@@ -26,7 +26,7 @@ export const RecordTableCellEditButton = () => {
|
||||
|
||||
const handleMainButtonClick = () => {
|
||||
if (!isFieldInputOnly && isFirstColumn) {
|
||||
openTableCell(undefined, false, true);
|
||||
openTableCell(undefined, true);
|
||||
} else {
|
||||
openTableCell();
|
||||
}
|
||||
|
||||
+15
-10
@@ -18,27 +18,28 @@ import { BORDER_COMMON } from 'twenty-ui/theme';
|
||||
import { useIsMobile } from 'twenty-ui/utilities';
|
||||
|
||||
const StyledRecordTableCellHoveredPortalContent = styled.div<{
|
||||
isReadOnly: boolean;
|
||||
showInteractiveStyle: boolean;
|
||||
isRowActive: boolean;
|
||||
}>`
|
||||
align-items: center;
|
||||
background: ${({ theme }) => theme.background.transparent.secondary};
|
||||
background-color: ${({ theme, isRowActive }) =>
|
||||
isRowActive ? theme.accent.quaternary : theme.background.primary};
|
||||
border-radius: ${({ isReadOnly }) =>
|
||||
!isReadOnly ? BORDER_COMMON.radius.sm : 'none'};
|
||||
border-radius: ${({ showInteractiveStyle }) =>
|
||||
showInteractiveStyle ? BORDER_COMMON.radius.sm : 'none'};
|
||||
box-sizing: border-box;
|
||||
cursor: ${({ isReadOnly }) => (isReadOnly ? 'default' : 'pointer')};
|
||||
cursor: ${({ showInteractiveStyle }) =>
|
||||
showInteractiveStyle ? 'pointer' : 'default'};
|
||||
display: flex;
|
||||
|
||||
height: ${RECORD_TABLE_ROW_HEIGHT}px;
|
||||
|
||||
outline: ${({ theme, isReadOnly, isRowActive }) =>
|
||||
outline: ${({ theme, showInteractiveStyle, isRowActive }) =>
|
||||
isRowActive
|
||||
? 'none'
|
||||
: isReadOnly
|
||||
? `1px solid ${theme.border.color.medium}`
|
||||
: `1px solid ${theme.font.color.extraLight}`};
|
||||
: showInteractiveStyle
|
||||
? `1px solid ${theme.font.color.extraLight}`
|
||||
: `1px solid ${theme.border.color.medium}`};
|
||||
|
||||
user-select: none;
|
||||
`;
|
||||
@@ -57,7 +58,11 @@ export const RecordTableCellHoveredPortalContent = () => {
|
||||
const isFieldInputOnly = useIsFieldInputOnly();
|
||||
|
||||
const showButton =
|
||||
!isFieldInputOnly && !isReadOnly && !(isMobile && isFirstColumn);
|
||||
!isFieldInputOnly &&
|
||||
(!isReadOnly || isFirstColumn) &&
|
||||
!(isMobile && isFirstColumn);
|
||||
|
||||
const showInteractiveStyle = !isReadOnly || (isFirstColumn && showButton);
|
||||
|
||||
const { rowIndex } = useRecordTableRowContextOrThrow();
|
||||
|
||||
@@ -68,7 +73,7 @@ export const RecordTableCellHoveredPortalContent = () => {
|
||||
|
||||
return (
|
||||
<StyledRecordTableCellHoveredPortalContent
|
||||
isReadOnly={isReadOnly}
|
||||
showInteractiveStyle={showInteractiveStyle}
|
||||
isRowActive={isRowActive}
|
||||
>
|
||||
{isFieldInputOnly ? (
|
||||
|
||||
+4
-28
@@ -1,10 +1,9 @@
|
||||
import { useRecoilCallback, useSetRecoilState } from 'recoil';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
|
||||
import { useInitDraftValue } from '@/object-record/record-field/ui/hooks/useInitDraftValue';
|
||||
import { type FieldDefinition } from '@/object-record/record-field/ui/types/FieldDefinition';
|
||||
import { type FieldMetadata } from '@/object-record/record-field/ui/types/FieldMetadata';
|
||||
import { isFieldValueEmpty } from '@/object-record/record-field/ui/utils/isFieldValueEmpty';
|
||||
import { viewableRecordIdState } from '@/object-record/record-right-drawer/states/viewableRecordIdState';
|
||||
import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector';
|
||||
import { FOCUS_CLICK_OUTSIDE_LISTENER_ID } from '@/object-record/record-table/constants/FocusClickOutsideListenerId';
|
||||
import { RECORD_TABLE_CELL_INPUT_ID_PREFIX } from '@/object-record/record-table/constants/RecordTableCellInputIdPrefix';
|
||||
@@ -16,7 +15,6 @@ import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue';
|
||||
|
||||
import { useOpenFieldInputEditMode } from '@/object-record/record-field/ui/hooks/useOpenFieldInputEditMode';
|
||||
import { recordIndexOpenRecordInState } from '@/object-record/record-index/states/recordIndexOpenRecordInState';
|
||||
import { viewableRecordNameSingularState } from '@/object-record/record-right-drawer/states/viewableRecordNameSingularState';
|
||||
import { RECORD_TABLE_CLICK_OUTSIDE_LISTENER_ID } from '@/object-record/record-table/constants/RecordTableClickOutsideListenerId';
|
||||
import { recordTableCellEditModePositionComponentState } from '@/object-record/record-table/states/recordTableCellEditModePositionComponentState';
|
||||
import { getDropdownFocusIdForRecordField } from '@/object-record/utils/getDropdownFocusIdForRecordField';
|
||||
@@ -37,11 +35,8 @@ export type OpenTableCellArgs = {
|
||||
initialValue?: string;
|
||||
cellPosition: TableCellPosition;
|
||||
isReadOnly: boolean;
|
||||
pathToShowPage: string;
|
||||
objectNameSingular: string;
|
||||
fieldDefinition: FieldDefinition<FieldMetadata>;
|
||||
recordId: string;
|
||||
isActionButtonClick: boolean;
|
||||
isNavigating: boolean;
|
||||
};
|
||||
|
||||
@@ -64,11 +59,6 @@ export const useOpenRecordTableCell = (recordTableId: string) => {
|
||||
|
||||
const initDraftValue = useInitDraftValue();
|
||||
|
||||
const setViewableRecordId = useSetRecoilState(viewableRecordIdState);
|
||||
const setViewableRecordNameSingular = useSetRecoilState(
|
||||
viewableRecordNameSingularState,
|
||||
);
|
||||
|
||||
const { setActiveDropdownFocusIdAndMemorizePrevious } =
|
||||
useSetActiveDropdownFocusIdAndMemorizePrevious();
|
||||
|
||||
@@ -94,16 +84,10 @@ export const useOpenRecordTableCell = (recordTableId: string) => {
|
||||
initialValue,
|
||||
cellPosition,
|
||||
isReadOnly,
|
||||
objectNameSingular,
|
||||
fieldDefinition,
|
||||
recordId,
|
||||
isActionButtonClick,
|
||||
isNavigating,
|
||||
}: OpenTableCellArgs) => {
|
||||
if (isReadOnly) {
|
||||
return;
|
||||
}
|
||||
|
||||
set(clickOutsideListenerIsActivatedState, false);
|
||||
|
||||
const isFirstColumnCell = cellPosition.column === 0;
|
||||
@@ -121,10 +105,7 @@ export const useOpenRecordTableCell = (recordTableId: string) => {
|
||||
fieldValue,
|
||||
});
|
||||
|
||||
if (
|
||||
(isFirstColumnCell && !isEmpty && !isActionButtonClick) ||
|
||||
isNavigating
|
||||
) {
|
||||
if ((isFirstColumnCell && !isEmpty) || isNavigating) {
|
||||
leaveTableFocus();
|
||||
|
||||
const openRecordIn = snapshot
|
||||
@@ -141,11 +122,8 @@ export const useOpenRecordTableCell = (recordTableId: string) => {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isFirstColumnCell && !isEmpty && isActionButtonClick) {
|
||||
leaveTableFocus();
|
||||
setViewableRecordId(recordId);
|
||||
setViewableRecordNameSingular(objectNameSingular);
|
||||
|
||||
// Block editing for read-only records, but allow navigation (handled above)
|
||||
if (isReadOnly) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -200,8 +178,6 @@ export const useOpenRecordTableCell = (recordTableId: string) => {
|
||||
leaveTableFocus,
|
||||
activateRecordTableRow,
|
||||
unfocusRecordTableRow,
|
||||
setViewableRecordId,
|
||||
setViewableRecordNameSingular,
|
||||
openRecordFromIndexView,
|
||||
],
|
||||
);
|
||||
|
||||
+1
-25
@@ -1,23 +1,9 @@
|
||||
import { useContext } from 'react';
|
||||
|
||||
import { FieldContext } from '@/object-record/record-field/ui/contexts/FieldContext';
|
||||
import { type FieldDefinition } from '@/object-record/record-field/ui/types/FieldDefinition';
|
||||
import { type FieldMetadata } from '@/object-record/record-field/ui/types/FieldMetadata';
|
||||
import { useRecordTableRowContextOrThrow } from '@/object-record/record-table/contexts/RecordTableRowContext';
|
||||
import { type TableCellPosition } from '@/object-record/record-table/types/TableCellPosition';
|
||||
|
||||
import { useRecordTableBodyContextOrThrow } from '@/object-record/record-table/contexts/RecordTableBodyContext';
|
||||
import { RecordTableCellContext } from '@/object-record/record-table/contexts/RecordTableCellContext';
|
||||
|
||||
export type OpenTableCellArgs = {
|
||||
initialValue?: string;
|
||||
cellPosition: TableCellPosition;
|
||||
isReadOnly: boolean;
|
||||
pathToShowPage: string;
|
||||
fieldDefinition: FieldDefinition<FieldMetadata>;
|
||||
recordId: string;
|
||||
};
|
||||
|
||||
export const useOpenRecordTableCellFromCell = () => {
|
||||
const {
|
||||
recordId,
|
||||
@@ -25,27 +11,17 @@ export const useOpenRecordTableCellFromCell = () => {
|
||||
isRecordFieldReadOnly: isReadOnly,
|
||||
} = useContext(FieldContext);
|
||||
|
||||
const { pathToShowPage, objectNameSingular } =
|
||||
useRecordTableRowContextOrThrow();
|
||||
|
||||
const { onOpenTableCell } = useRecordTableBodyContextOrThrow();
|
||||
|
||||
const { cellPosition } = useContext(RecordTableCellContext);
|
||||
|
||||
const openTableCell = (
|
||||
initialValue?: string,
|
||||
isActionButtonClick = false,
|
||||
isNavigating = false,
|
||||
) => {
|
||||
const openTableCell = (initialValue?: string, isNavigating = false) => {
|
||||
onOpenTableCell({
|
||||
cellPosition,
|
||||
recordId,
|
||||
fieldDefinition,
|
||||
isReadOnly,
|
||||
pathToShowPage,
|
||||
objectNameSingular,
|
||||
initialValue,
|
||||
isActionButtonClick,
|
||||
isNavigating,
|
||||
});
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user