From f6cd51ba97c8480ce9d7746b4f9923ae52759923 Mon Sep 17 00:00:00 2001 From: Thomas des Francs Date: Tue, 18 Nov 2025 18:11:07 +0100 Subject: [PATCH] feat: Support custom fields in calendar event detail panel (#15853) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Overview This PR refactors the `CalendarEventDetails` component to dynamically display both standard and custom fields added via the metadata API, instead of using a hardcoded field list. ## Changes - Replaced hardcoded `fieldsToDisplay` array with dynamic field fetching using `useFieldListFieldMetadataItems` hook - Split fields into `standardFields` (maintaining original order) and `customFields` - Introduced `renderField` helper function to eliminate code duplication - Custom fields now automatically appear at the bottom of the detail panel after standard fields - Maintained exact field order and all existing functionality including participant response status display ## Technical Details - Uses existing `useFieldListFieldMetadataItems` pattern already established in the codebase - Standard field order explicitly defined: startsAt, endsAt, conferenceLink, location, description - Participant response status (Yes/Maybe/No) correctly positioned between first 2 and last 3 standard fields - All fields respect permissions and visibility settings from metadata ## Testing - ✅ Verified all standard fields display in correct order - ✅ Added custom field `mycustomfieldtest` via metadata API - displays correctly at bottom - ✅ Participant responses (Yes/Maybe/No) render correctly with avatars - ✅ Event title, creation date, and event chip display properly - ✅ Canceled event styling (strikethrough) works - ✅ No console errors or regressions detected - ✅ Read-only behavior maintained (calendar events sync from external sources) ## Screenshots CleanShot 2025-11-17 at 11 23 57 ## Related This enables users to extend calendar events with custom metadata fields that will automatically display in the UI without code changes. --------- Co-authored-by: Charles Bochet --- .cursor/rules/code-style.mdc | 61 ++++++++++++++++++- .../components/CalendarEventDetails.tsx | 40 ++++++++---- 2 files changed, 86 insertions(+), 15 deletions(-) diff --git a/.cursor/rules/code-style.mdc b/.cursor/rules/code-style.mdc index 8b2440fea2..c23c952844 100644 --- a/.cursor/rules/code-style.mdc +++ b/.cursor/rules/code-style.mdc @@ -30,6 +30,18 @@ type ButtonProps = {}; // Component props suffix with 'Props' // ✅ Files and directories - kebab-case // user-profile.component.tsx // user-profile.styles.ts + +// ❌ NEVER use abbreviations in variable names +// Bad +const users = data.map((u) => u.name); +const field = items.find((f) => f.id === id); + +// Good +const users = data.map((user) => user.name); +const field = items.find((item) => item.id === id); +const fieldMetadata = inlineFields.find( + (fieldMetadataItem) => fieldMetadataItem.name === fieldName, +); ``` ## Import Organization @@ -59,11 +71,11 @@ const processUserData = ( ): ProcessedUser => { const processedUser = transformUserData(user); applyOptions(processedUser, options); - + if (callback) { callback(processedUser); } - + return processedUser; }; ``` @@ -71,7 +83,7 @@ const processUserData = ( ## Comments ```typescript // ✅ Use short-form comments, NOT JSDoc blocks -// ✅ Explain business logic and non-obvious intentions +// ✅ Explain business logic and non-obvious intentions (WHY, not WHAT) // Apply 15% discount for premium users with orders > $100 const discount = isPremiumUser && orderTotal > 100 ? 0.15 : 0; @@ -85,12 +97,55 @@ const calculateTotalPrice = (basePrice: number): number => { // Implementation }; +// ❌ AVOID obvious comments that just describe what code does +// Bad: Get all inline fields dynamically +const { inlineFieldMetadataItems } = useFieldListFieldMetadataItems({...}); + +// Bad: Define standard fields in display order +const standardFieldOrder = ['startsAt', 'endsAt', 'conferenceLink']; + +// Bad: Split fields into standard and custom +const standardFields = standardFieldOrder.map(...) + +// ✅ GOOD: Only comment if explaining non-obvious business logic +// Calendar events display standard fields first, then custom fields after participants +// to maintain consistency with the legacy UI behavior +const standardFields = standardFieldOrder.map(...) + // ❌ AVOID JSDoc blocks - use short comments instead /** * This style is NOT preferred in this codebase */ ``` +**Comment Guidelines:** +- **DO** comment complex business rules or domain-specific logic +- **DO** comment non-obvious algorithmic decisions +- **DO** add TODOs for future improvements +- **DON'T** comment obvious variable declarations or function calls +- **DON'T** comment what is already clear from well-named variables/functions +- **DON'T** add comments that just repeat what the code says + +## Utility Helpers +```typescript +// ✅ Use existing utility helpers instead of manual checks +import { isDefined } from 'twenty-shared/utils'; +import { isNonEmptyString, isNonEmptyArray } from '@sniptt/guards'; + +// ❌ Manual type guards +const validItems = items.filter((item): item is Item => item !== undefined); +const hasValue = value !== null && value !== undefined; + +// ✅ Use utility helpers +const validItems = items.filter(isDefined); +const hasValue = isDefined(value); + +// Other useful helpers: +// - isDefined(value) - checks !== null && !== undefined +// - isNonEmptyString(value) - checks string is defined and not empty +// - isNonEmptyArray(value) - checks array is defined and has items +``` + ## Security Patterns ```typescript // ✅ CSV Export: Always apply security first, then formatting diff --git a/packages/twenty-front/src/modules/activities/calendar/components/CalendarEventDetails.tsx b/packages/twenty-front/src/modules/activities/calendar/components/CalendarEventDetails.tsx index 29e4bda862..4cd8c53bda 100644 --- a/packages/twenty-front/src/modules/activities/calendar/components/CalendarEventDetails.tsx +++ b/packages/twenty-front/src/modules/activities/calendar/components/CalendarEventDetails.tsx @@ -5,16 +5,18 @@ import { CalendarEventParticipantsResponseStatus } from '@/activities/calendar/c import { type CalendarEvent } from '@/activities/calendar/types/CalendarEvent'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; 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 { useFieldListFieldMetadataItems } from '@/object-record/record-field-list/hooks/useFieldListFieldMetadataItems'; import { FieldContext } 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 { getRecordFieldInputInstanceId } from '@/object-record/utils/getRecordFieldInputId'; +import { isDefined } from 'twenty-shared/utils'; import { Chip, ChipAccent, ChipSize, ChipVariant } from 'twenty-ui/components'; import { IconCalendarEvent } from 'twenty-ui/display'; -import { mapArrayToObject } from '~/utils/array/mapArrayToObject'; import { beautifyPastDateRelativeToNow } from '~/utils/date-utils'; type CalendarEventDetailsProps = { @@ -80,7 +82,13 @@ export const CalendarEventDetails = ({ objectNameSingular: CoreObjectNameSingular.CalendarEvent, }); - const fieldsToDisplay = [ + const { inlineFieldMetadataItems } = useFieldListFieldMetadataItems({ + objectNameSingular: CoreObjectNameSingular.CalendarEvent, + showRelationSections: false, + excludeCreatedAtAndUpdatedAt: true, + }); + + const standardFieldOrder = [ 'startsAt', 'endsAt', 'conferenceLink', @@ -88,9 +96,16 @@ export const CalendarEventDetails = ({ 'description', ]; - const fieldsByName = mapArrayToObject( - objectMetadataItem.fields, - ({ name }) => name, + const standardFields = standardFieldOrder + .map((fieldName) => + inlineFieldMetadataItems.find( + (fieldMetadataItem) => fieldMetadataItem.name === fieldName, + ), + ) + .filter(isDefined); + + const customFields = inlineFieldMetadataItems.filter( + (field) => field.isCustom && !standardFieldOrder.includes(field.name), ); const { calendarEventParticipants } = calendarEvent; @@ -100,14 +115,14 @@ export const CalendarEventDetails = ({ objectMetadataId: objectMetadataItem.id, }); - const Fields = fieldsToDisplay.map((fieldName) => ( - + const renderField = (fieldMetadataItem: FieldMetadataItem) => ( + - )); + ); return ( @@ -154,13 +169,14 @@ export const CalendarEventDetails = ({ - {Fields.slice(0, 2)} + {standardFields.slice(0, 2).map(renderField)} {calendarEventParticipants && ( )} - {Fields.slice(2)} + {standardFields.slice(2).map(renderField)} + {customFields.map(renderField)} );