Enable editing for calendar event custom fields (#17063)

## Summary
- Enable editing for custom calendar event fields while keeping standard
fields read-only
- Load calendar event fields dynamically so custom field values are
editable everywhere they appear
- Preserve calendar event participants rendering

## Testing
- npx nx run twenty-front:lint:diff-with-main --skip-nx-cache
--output-style=stream
- npx jest --config packages/twenty-front/jest.config.mjs
--runTestsByPath
packages/twenty-front/src/modules/activities/calendar/hooks/__tests__/useCalendarEvents.test.tsx

## Notes
- Full nx lint/test are very slow locally after barrel generation; CI
should run the full suite

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
salmonumbrella
2026-01-12 02:21:42 -08:00
committed by GitHub
parent ead846d57f
commit 94e5d93e60
4 changed files with 98 additions and 67 deletions
@@ -1,6 +1,7 @@
import { css, useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { useLingui } from '@lingui/react/macro';
import { useCallback, useState } from 'react';
import { CalendarEventParticipantsResponseStatus } from '@/activities/calendar/components/CalendarEventParticipantsResponseStatus';
import { type CalendarEvent } from '@/activities/calendar/types/CalendarEvent';
@@ -8,13 +9,20 @@ import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadata
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { formatFieldMetadataItemAsFieldDefinition } from '@/object-metadata/utils/formatFieldMetadataItemAsFieldDefinition';
import { useIsRecordReadOnly } from '@/object-record/read-only/hooks/useIsRecordReadOnly';
import { useObjectPermissionsForObject } from '@/object-record/hooks/useObjectPermissionsForObject';
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
import { RecordFieldsScopeContextProvider } from '@/object-record/record-field-list/contexts/RecordFieldsScopeContext';
import { useFieldListFieldMetadataItems } from '@/object-record/record-field-list/hooks/useFieldListFieldMetadataItems';
import { FieldContext } from '@/object-record/record-field/ui/contexts/FieldContext';
import {
FieldContext,
type RecordUpdateHook,
type RecordUpdateHookParams,
} from '@/object-record/record-field/ui/contexts/FieldContext';
import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/ui/states/contexts/RecordFieldComponentInstanceContext';
import { RecordInlineCell } from '@/object-record/record-inline-cell/components/RecordInlineCell';
import { PropertyBox } from '@/object-record/record-inline-cell/property-box/components/PropertyBox';
import { useIsRecordReadOnly } from '@/object-record/read-only/hooks/useIsRecordReadOnly';
import { isRecordFieldReadOnly } from '@/object-record/read-only/utils/isRecordFieldReadOnly';
import { getRecordFieldInputInstanceId } from '@/object-record/utils/getRecordFieldInputId';
import { isDefined } from 'twenty-shared/utils';
import { Chip, ChipAccent, ChipSize, ChipVariant } from 'twenty-ui/components';
@@ -113,42 +121,77 @@ export const CalendarEventDetails = ({
const { calendarEventParticipants } = calendarEvent;
const { updateOneRecord } = useUpdateOneRecord({
objectNameSingular: CoreObjectNameSingular.CalendarEvent,
});
const [isUpdating, setIsUpdating] = useState(false);
const updateEntity = useCallback(
({ variables }: RecordUpdateHookParams) => {
setIsUpdating(true);
void updateOneRecord({
idToUpdate: variables.where.id as string,
updateOneRecordInput: variables.updateOneRecordInput,
}).finally(() => setIsUpdating(false));
},
[updateOneRecord],
);
const useUpdateOneCalendarEventRecordMutation: RecordUpdateHook = () => [
updateEntity,
{ loading: isUpdating },
];
const objectPermissions = useObjectPermissionsForObject(
objectMetadataItem.id,
);
const isRecordReadOnly = useIsRecordReadOnly({
recordId: calendarEvent.id,
objectMetadataId: objectMetadataItem.id,
});
const renderField = (fieldMetadataItem: FieldMetadataItem) => (
<StyledPropertyBox key={fieldMetadataItem.id}>
<FieldContext.Provider
value={{
recordId: calendarEvent.id,
isLabelIdentifier: false,
fieldDefinition: formatFieldMetadataItemAsFieldDefinition({
field: fieldMetadataItem,
objectMetadataItem,
showLabel: true,
labelWidth: 72,
}),
useUpdateRecord: () => [() => undefined, { loading: false }],
maxWidth: 300,
isRecordFieldReadOnly: isRecordReadOnly,
}}
>
<RecordFieldComponentInstanceContext.Provider
const renderField = (fieldMetadataItem: FieldMetadataItem) => {
const isReadOnly = isRecordFieldReadOnly({
isRecordReadOnly,
objectPermissions,
fieldMetadataItem: {
id: fieldMetadataItem.id,
isUIReadOnly: fieldMetadataItem.isUIReadOnly ?? false,
},
});
return (
<StyledPropertyBox key={fieldMetadataItem.id}>
<FieldContext.Provider
value={{
instanceId: getRecordFieldInputInstanceId({
recordId: calendarEvent.id,
fieldName: fieldMetadataItem.name,
prefix: INPUT_ID_PREFIX,
recordId: calendarEvent.id,
isLabelIdentifier: false,
fieldDefinition: formatFieldMetadataItemAsFieldDefinition({
field: fieldMetadataItem,
objectMetadataItem,
showLabel: true,
labelWidth: 72,
}),
useUpdateRecord: useUpdateOneCalendarEventRecordMutation,
maxWidth: 300,
isRecordFieldReadOnly: isReadOnly,
}}
>
<RecordInlineCell />
</RecordFieldComponentInstanceContext.Provider>
</FieldContext.Provider>
</StyledPropertyBox>
);
<RecordFieldComponentInstanceContext.Provider
value={{
instanceId: getRecordFieldInputInstanceId({
recordId: calendarEvent.id,
fieldName: fieldMetadataItem.name,
prefix: INPUT_ID_PREFIX,
}),
}}
>
<RecordInlineCell />
</RecordFieldComponentInstanceContext.Provider>
</FieldContext.Provider>
</StyledPropertyBox>
);
};
return (
<RecordFieldsScopeContextProvider
@@ -1,33 +0,0 @@
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { type RecordGqlOperationSignature } from 'twenty-shared/types';
export const FIND_ONE_CALENDAR_EVENT_OPERATION_SIGNATURE: RecordGqlOperationSignature =
{
objectNameSingular: CoreObjectNameSingular.CalendarEvent,
variables: {},
fields: {
conferenceLink: true,
description: true,
endsAt: true,
externalCreatedAt: true,
id: true,
isCanceled: true,
isFullDay: true,
location: true,
startsAt: true,
title: true,
visibility: true,
calendarEventParticipants: {
id: true,
person: true,
workspaceMember: true,
isOrganizer: true,
responseStatus: true,
handle: true,
createdAt: true,
calendarEventId: true,
updatedAt: true,
displayName: true,
},
},
};
@@ -1,8 +1,9 @@
import { CalendarEventDetails } from '@/activities/calendar/components/CalendarEventDetails';
import { CalendarEventDetailsEffect } from '@/activities/calendar/components/CalendarEventDetailsEffect';
import { FIND_ONE_CALENDAR_EVENT_OPERATION_SIGNATURE } from '@/activities/calendar/graphql/operation-signatures/FindOneCalendarEventOperationSignature';
import { type CalendarEvent } from '@/activities/calendar/types/CalendarEvent';
import { viewableRecordIdComponentState } from '@/command-menu/pages/record-page/states/viewableRecordIdComponentState';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useGenerateDepthRecordGqlFieldsFromObject } from '@/object-record/graphql/record-gql-fields/hooks/useGenerateDepthRecordGqlFieldsFromObject';
import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord';
import { useUpsertRecordsInStore } from '@/object-record/record-store/hooks/useUpsertRecordsInStore';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
@@ -13,11 +14,31 @@ export const CommandMenuCalendarEventPage = () => {
viewableRecordIdComponentState,
);
const { recordGqlFields } = useGenerateDepthRecordGqlFieldsFromObject({
objectNameSingular: CoreObjectNameSingular.CalendarEvent,
depth: 1,
});
const calendarEventRecordGqlFields = {
...recordGqlFields,
calendarEventParticipants: {
id: true,
person: true,
workspaceMember: true,
isOrganizer: true,
responseStatus: true,
handle: true,
createdAt: true,
calendarEventId: true,
updatedAt: true,
displayName: true,
},
};
const { record: calendarEvent } = useFindOneRecord<CalendarEvent>({
objectNameSingular:
FIND_ONE_CALENDAR_EVENT_OPERATION_SIGNATURE.objectNameSingular,
objectNameSingular: CoreObjectNameSingular.CalendarEvent,
objectRecordId: viewableRecordId ?? '',
recordGqlFields: FIND_ONE_CALENDAR_EVENT_OPERATION_SIGNATURE.fields,
recordGqlFields: calendarEventRecordGqlFields,
// TODO: this is not executed on sub-sequent runs, make sure that it is intended
onCompleted: (record) => {
upsertRecordsInStore({ partialRecords: [record] });
@@ -153,7 +153,7 @@ export const Catalog: CatalogStory<
<div>
<RecoilRoot
initializeState={({ set }) => {
if (args.selected) {
if (args.selected === true) {
set(
workflowSelectedNodeComponentState.atomFamily({
instanceId: 'workflow-visualizer-instance-id',