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:
+72
-29
@@ -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
|
||||
|
||||
-33
@@ -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,
|
||||
},
|
||||
},
|
||||
};
|
||||
+25
-4
@@ -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] });
|
||||
|
||||
+1
-1
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user