Add calendar date field submenu with grouped field options (#23445)

## Summary

- Add a submenu for selecting calendar date fields as it was not clear
enough that you could pick 2 date fields.
- Group date and datetime fields with non-selectable headers

### Before
<img width="240" height="252" alt="Screenshot 2026-07-29 at 11 32 54"
src="https://github.com/user-attachments/assets/169bd965-676f-4ef9-8ba6-9ecac80547c6"
/>

### After
<img width="260" height="246" alt="Screenshot 2026-07-29 at 11 31 58"
src="https://github.com/user-attachments/assets/a4471bb0-5c1f-4f5e-bddb-165a5c9da4c7"
/>
This commit is contained in:
Weiko
2026-07-29 15:56:39 +02:00
committed by GitHub
parent e99452e00d
commit e2e2142998
8 changed files with 248 additions and 93 deletions
@@ -0,0 +1,95 @@
import { OBJECT_OPTIONS_DROPDOWN_ID } from '@/object-record/object-options-dropdown/constants/ObjectOptionsDropdownId';
import { useObjectOptionsDropdown } from '@/object-record/object-options-dropdown/hooks/useObjectOptionsDropdown';
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader/DropdownMenuHeader';
import { DropdownMenuHeaderLeftComponent } from '@/ui/layout/dropdown/components/DropdownMenuHeader/internal/DropdownMenuHeaderLeftComponent';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { GenericDropdownContentWidth } from '@/ui/layout/dropdown/constants/GenericDropdownContentWidth';
import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList';
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
import { selectedItemIdComponentState } from '@/ui/layout/selectable-list/states/selectedItemIdComponentState';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
import { useGetCurrentViewOnly } from '@/views/hooks/useGetCurrentViewOnly';
import { useLingui } from '@lingui/react/macro';
import { isDefined } from 'twenty-shared/utils';
import { IconCalendar, IconChevronLeft } from 'twenty-ui/icon';
import { MenuItem } from 'twenty-ui/navigation';
export const ObjectOptionsDropdownCalendarDateFieldsContent = () => {
const { t } = useLingui();
const { objectMetadataItem, resetContent, onContentChange } =
useObjectOptionsDropdown();
const { currentView } = useGetCurrentViewOnly();
const selectedItemId = useAtomComponentStateValue(
selectedItemIdComponentState,
OBJECT_OPTIONS_DROPDOWN_ID,
);
if (!isDefined(currentView)) {
return null;
}
const calendarFieldMetadata = objectMetadataItem.fields.find(
(field) => field.id === currentView.calendarFieldMetadataId,
);
const calendarEndFieldMetadata = objectMetadataItem.fields.find(
(field) => field.id === currentView.calendarEndFieldMetadataId,
);
const selectableItemIdArray = ['CalendarDateField', 'CalendarEndDateField'];
return (
<DropdownContent widthInPixels={GenericDropdownContentWidth.Large}>
<DropdownMenuHeader
StartComponent={
<DropdownMenuHeaderLeftComponent
onClick={() => resetContent()}
Icon={IconChevronLeft}
/>
}
>
{t`Date fields`}
</DropdownMenuHeader>
<DropdownMenuItemsContainer scrollable={false}>
<SelectableList
selectableListInstanceId={OBJECT_OPTIONS_DROPDOWN_ID}
focusId={OBJECT_OPTIONS_DROPDOWN_ID}
selectableItemIdArray={selectableItemIdArray}
>
<SelectableListItem
itemId="CalendarDateField"
onEnter={() => onContentChange('calendarFields')}
>
<MenuItem
focused={selectedItemId === 'CalendarDateField'}
onClick={() => onContentChange('calendarFields')}
LeftIcon={IconCalendar}
text={t`Date field`}
contextualText={calendarFieldMetadata?.label}
contextualTextPosition="right"
hasSubMenu
/>
</SelectableListItem>
<SelectableListItem
itemId="CalendarEndDateField"
onEnter={() => onContentChange('calendarEndFields')}
>
<MenuItem
focused={selectedItemId === 'CalendarEndDateField'}
onClick={() => onContentChange('calendarEndFields')}
LeftIcon={IconCalendar}
text={t`End date field`}
contextualText={calendarEndFieldMetadata?.label ?? t`None`}
contextualTextPosition="right"
hasSubMenu
/>
</SelectableListItem>
</SelectableList>
</DropdownMenuItemsContainer>
</DropdownContent>
);
};
@@ -2,11 +2,13 @@ import { useSetAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useSe
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { useObjectOptionsDropdown } from '@/object-record/object-options-dropdown/hooks/useObjectOptionsDropdown';
import { recordIndexCalendarEndFieldMetadataIdComponentState } from '@/object-record/record-index/states/recordIndexCalendarEndFieldMetadataIdComponentState';
import { groupCalendarFieldMetadataItemsByType } from '@/object-record/record-calendar/utils/groupCalendarFieldMetadataItemsByType';
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader/DropdownMenuHeader';
import { DropdownMenuHeaderLeftComponent } from '@/ui/layout/dropdown/components/DropdownMenuHeader/internal/DropdownMenuHeaderLeftComponent';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput';
import { DropdownMenuSectionLabel } from '@/ui/layout/dropdown/components/DropdownMenuSectionLabel';
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
import { useGetCurrentViewOnly } from '@/views/hooks/useGetCurrentViewOnly';
import { useUpdateCurrentView } from '@/views/hooks/useUpdateCurrentView';
@@ -14,7 +16,7 @@ import { useGetAvailableFieldsForCalendar } from '@/views/view-picker/hooks/useG
import { getAvailableCalendarEndFieldMetadataItems } from '@/views/view-picker/utils/getAvailableCalendarEndFieldMetadataItems';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { useLingui } from '@lingui/react/macro';
import { useState } from 'react';
import { Fragment, useState } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { IconChevronLeft, useIcons } from 'twenty-ui/icon';
import { MenuItemSelect } from 'twenty-ui/navigation';
@@ -28,7 +30,7 @@ export const ObjectOptionsDropdownCalendarEndFieldsContent = () => {
FeatureFlagKey.IS_CALENDAR_WEEK_VIEW_ENABLED,
);
const { resetContent, closeDropdown } = useObjectOptionsDropdown();
const { onContentChange, closeDropdown } = useObjectOptionsDropdown();
const { currentView } = useGetCurrentViewOnly();
const { updateCurrentView } = useUpdateCurrentView();
@@ -49,6 +51,10 @@ export const ObjectOptionsDropdownCalendarEndFieldsContent = () => {
field.label.toLowerCase().includes(searchInput.toLowerCase()),
);
const calendarEndFieldGroups = groupCalendarFieldMetadataItemsByType(
filteredCalendarEndFields,
);
const handleCalendarEndFieldChange = async (
fieldMetadataItem: FieldMetadataItem | null,
) => {
@@ -72,7 +78,7 @@ export const ObjectOptionsDropdownCalendarEndFieldsContent = () => {
<DropdownMenuHeader
StartComponent={
<DropdownMenuHeaderLeftComponent
onClick={() => resetContent()}
onClick={() => onContentChange('calendarDateFields')}
Icon={IconChevronLeft}
/>
}
@@ -92,16 +98,22 @@ export const ObjectOptionsDropdownCalendarEndFieldsContent = () => {
onClick={() => handleCalendarEndFieldChange(null)}
text={t`None`}
/>
{filteredCalendarEndFields.map((fieldMetadataItem) => (
<MenuItemSelect
key={fieldMetadataItem.id}
selected={
fieldMetadataItem.id === currentView?.calendarEndFieldMetadataId
}
onClick={() => handleCalendarEndFieldChange(fieldMetadataItem)}
LeftIcon={getIcon(fieldMetadataItem.icon)}
text={fieldMetadataItem.label}
/>
{calendarEndFieldGroups.map((group) => (
<Fragment key={group.label}>
<DropdownMenuSectionLabel label={group.label} />
{group.fields.map((fieldMetadataItem) => (
<MenuItemSelect
key={fieldMetadataItem.id}
selected={
fieldMetadataItem.id ===
currentView?.calendarEndFieldMetadataId
}
onClick={() => handleCalendarEndFieldChange(fieldMetadataItem)}
LeftIcon={getIcon(fieldMetadataItem.icon)}
text={fieldMetadataItem.label}
/>
))}
</Fragment>
))}
</DropdownMenuItemsContainer>
</DropdownContent>
@@ -3,27 +3,34 @@ import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataIte
import { useObjectOptionsDropdown } from '@/object-record/object-options-dropdown/hooks/useObjectOptionsDropdown';
import { recordIndexCalendarEndFieldMetadataIdComponentState } from '@/object-record/record-index/states/recordIndexCalendarEndFieldMetadataIdComponentState';
import { recordIndexCalendarFieldMetadataIdComponentState } from '@/object-record/record-index/states/recordIndexCalendarFieldMetadataIdComponentState';
import { groupCalendarFieldMetadataItemsByType } from '@/object-record/record-calendar/utils/groupCalendarFieldMetadataItemsByType';
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader/DropdownMenuHeader';
import { DropdownMenuHeaderLeftComponent } from '@/ui/layout/dropdown/components/DropdownMenuHeader/internal/DropdownMenuHeaderLeftComponent';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput';
import { DropdownMenuSectionLabel } from '@/ui/layout/dropdown/components/DropdownMenuSectionLabel';
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
import { useGetCurrentViewOnly } from '@/views/hooks/useGetCurrentViewOnly';
import { useUpdateCurrentView } from '@/views/hooks/useUpdateCurrentView';
import { useGetAvailableFieldsForCalendar } from '@/views/view-picker/hooks/useGetAvailableFieldsForCalendar';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { useLingui } from '@lingui/react/macro';
import { useState } from 'react';
import { Fragment, useState } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { IconChevronLeft, IconSettings, useIcons } from 'twenty-ui/icon';
import { MenuItem, MenuItemSelect } from 'twenty-ui/navigation';
import { FeatureFlagKey } from '~/generated-metadata/graphql';
export const ObjectOptionsDropdownCalendarFieldsContent = () => {
const { t } = useLingui();
const { getIcon } = useIcons();
const [searchInput, setSearchInput] = useState('');
const isCalendarWeekViewEnabled = useIsFeatureEnabled(
FeatureFlagKey.IS_CALENDAR_WEEK_VIEW_ENABLED,
);
const { objectMetadataItem, resetContent, closeDropdown } =
const { objectMetadataItem, resetContent, onContentChange, closeDropdown } =
useObjectOptionsDropdown();
const { currentView } = useGetCurrentViewOnly();
@@ -50,10 +57,28 @@ export const ObjectOptionsDropdownCalendarFieldsContent = () => {
)
: undefined;
const filteredCalendarFields = availableFieldsForCalendar.filter((field) =>
const availableCalendarFields = isDefined(calendarEndFieldMetadata)
? availableFieldsForCalendar.filter(
(field) => field.type === calendarEndFieldMetadata.type,
)
: availableFieldsForCalendar;
const filteredCalendarFields = availableCalendarFields.filter((field) =>
field.label.toLowerCase().includes(searchInput.toLowerCase()),
);
const calendarFieldGroups = groupCalendarFieldMetadataItemsByType(
filteredCalendarFields,
);
const handleBack = () => {
if (isCalendarWeekViewEnabled) {
onContentChange('calendarDateFields');
return;
}
resetContent();
};
const handleCalendarFieldChange = async (
fieldMetadataItem: FieldMetadataItem,
) => {
@@ -82,7 +107,7 @@ export const ObjectOptionsDropdownCalendarFieldsContent = () => {
<DropdownMenuHeader
StartComponent={
<DropdownMenuHeaderLeftComponent
onClick={() => resetContent()}
onClick={handleBack}
Icon={IconChevronLeft}
/>
}
@@ -97,14 +122,19 @@ export const ObjectOptionsDropdownCalendarFieldsContent = () => {
/>
<DropdownMenuSeparator />
<DropdownMenuItemsContainer>
{filteredCalendarFields.map((fieldMetadataItem) => (
<MenuItemSelect
key={fieldMetadataItem.id}
selected={fieldMetadataItem.id === calendarFieldMetadata?.id}
onClick={() => handleCalendarFieldChange(fieldMetadataItem)}
LeftIcon={getIcon(fieldMetadataItem.icon)}
text={fieldMetadataItem.label}
/>
{calendarFieldGroups.map((group) => (
<Fragment key={group.label}>
<DropdownMenuSectionLabel label={group.label} />
{group.fields.map((fieldMetadataItem) => (
<MenuItemSelect
key={fieldMetadataItem.id}
selected={fieldMetadataItem.id === calendarFieldMetadata?.id}
onClick={() => handleCalendarFieldChange(fieldMetadataItem)}
LeftIcon={getIcon(fieldMetadataItem.icon)}
text={fieldMetadataItem.label}
/>
))}
</Fragment>
))}
</DropdownMenuItemsContainer>
<DropdownMenuSeparator />
@@ -1,4 +1,5 @@
import { ObjectOptionsDropdownAddRecordGroupContent } from '@/object-record/object-options-dropdown/components/ObjectOptionsDropdownAddRecordGroupContent';
import { ObjectOptionsDropdownCalendarDateFieldsContent } from '@/object-record/object-options-dropdown/components/ObjectOptionsDropdownCalendarDateFieldsContent';
import { ObjectOptionsDropdownCalendarFieldsContent } from '@/object-record/object-options-dropdown/components/ObjectOptionsDropdownCalendarFieldsContent';
import { ObjectOptionsDropdownCalendarEndFieldsContent } from '@/object-record/object-options-dropdown/components/ObjectOptionsDropdownCalendarEndFieldsContent';
import { ObjectOptionsDropdownCalendarViewContent } from '@/object-record/object-options-dropdown/components/ObjectOptionsDropdownCalendarViewContent';
@@ -43,6 +44,12 @@ export const ObjectOptionsDropdownContent = () => {
return <ObjectOptionsDropdownAddRecordGroupContent />;
case 'calendarView':
return <ObjectOptionsDropdownCalendarViewContent />;
case 'calendarDateFields':
return isCalendarWeekViewEnabled ? (
<ObjectOptionsDropdownCalendarDateFieldsContent />
) : (
<ObjectOptionsDropdownMenuContent />
);
case 'calendarFields':
return <ObjectOptionsDropdownCalendarFieldsContent />;
case 'calendarEndFields':
@@ -74,12 +74,6 @@ export const ObjectOptionsDropdownCustomView = ({
)
: undefined;
const calendarEndFieldMetadata = currentView?.calendarEndFieldMetadataId
? objectMetadataItem.fields.find(
(field) => field.id === currentView.calendarEndFieldMetadataId,
)
: undefined;
const viewsOnCurrentObject = useAtomFamilySelectorValue(
viewsFromObjectMetadataItemFamilySelector,
{ objectMetadataItemId: objectMetadataItem.id },
@@ -129,8 +123,9 @@ export const ObjectOptionsDropdownCustomView = ({
'Fields',
...(customViewData?.type === ViewType.CALENDAR
? [
'CalendarDateField',
...(isCalendarWeekViewEnabled ? ['CalendarEndDateField'] : []),
isCalendarWeekViewEnabled
? 'CalendarDateFields'
: 'CalendarDateField',
'CalendarView',
]
: []),
@@ -196,42 +191,43 @@ export const ObjectOptionsDropdownCustomView = ({
<DropdownMenuItemsContainer scrollable={false}>
{customViewData?.type === ViewType.CALENDAR && (
<>
<div id="calendar-date-field-picker-menu-item">
<SelectableListItem
itemId="CalendarDateField"
onEnter={() => onContentChange('calendarFields')}
>
<MenuItem
focused={selectedItemId === 'CalendarDateField'}
onClick={() => onContentChange('calendarFields')}
LeftIcon={IconCalendar}
text={t`Date field`}
contextualText={
isDefaultView
? t`Not available on Default View`
: calendarFieldMetadata?.label
}
contextualTextPosition="right"
hasSubMenu
disabled={isDefaultView}
/>
</SelectableListItem>
</div>
{isCalendarWeekViewEnabled && (
<div id="calendar-end-date-field-picker-menu-item">
{isCalendarWeekViewEnabled ? (
<div id="calendar-date-fields-picker-menu-item">
<SelectableListItem
itemId="CalendarEndDateField"
onEnter={() => onContentChange('calendarEndFields')}
itemId="CalendarDateFields"
onEnter={() => onContentChange('calendarDateFields')}
>
<MenuItem
focused={selectedItemId === 'CalendarEndDateField'}
onClick={() => onContentChange('calendarEndFields')}
focused={selectedItemId === 'CalendarDateFields'}
onClick={() => onContentChange('calendarDateFields')}
LeftIcon={IconCalendar}
text={t`End date field`}
text={t`Date fields`}
contextualText={
isDefaultView
? t`Not available on Default View`
: (calendarEndFieldMetadata?.label ?? t`None`)
: calendarFieldMetadata?.label
}
contextualTextPosition="right"
hasSubMenu
disabled={isDefaultView}
/>
</SelectableListItem>
</div>
) : (
<div id="calendar-date-field-picker-menu-item">
<SelectableListItem
itemId="CalendarDateField"
onEnter={() => onContentChange('calendarFields')}
>
<MenuItem
focused={selectedItemId === 'CalendarDateField'}
onClick={() => onContentChange('calendarFields')}
LeftIcon={IconCalendar}
text={t`Date field`}
contextualText={
isDefaultView
? t`Not available on Default View`
: calendarFieldMetadata?.label
}
contextualTextPosition="right"
hasSubMenu
@@ -87,12 +87,6 @@ export const ObjectOptionsDropdownLayoutContent = () => {
)
: undefined;
const calendarEndFieldMetadata = currentView?.calendarEndFieldMetadataId
? objectMetadataItem.fields.find(
(field) => field.id === currentView.calendarEndFieldMetadataId,
)
: undefined;
const { setAndPersistViewType } = useSetViewTypeFromLayoutOptionsMenu();
const { availableFieldsForGrouping, navigateToSelectSettings } =
useGetAvailableFieldsToGroupRecordsBy();
@@ -140,8 +134,9 @@ export const ObjectOptionsDropdownLayoutContent = () => {
...(currentView?.type === ViewType.CALENDAR
? [
'CalendarView',
'CalendarDateField',
...(isCalendarWeekViewEnabled ? ['CalendarEndDateField'] : []),
isCalendarWeekViewEnabled
? 'CalendarDateFields'
: 'CalendarDateField',
]
: []),
...(currentView?.type !== ViewType.TABLE ? ['Compact view'] : []),
@@ -237,33 +232,32 @@ export const ObjectOptionsDropdownLayoutContent = () => {
<DropdownMenuItemsContainer scrollable={false}>
{currentView?.type === ViewType.CALENDAR && (
<>
<SelectableListItem
itemId="CalendarDateField"
onEnter={() => onContentChange('calendarFields')}
>
<MenuItem
focused={selectedItemId === 'CalendarDateField'}
onClick={() => onContentChange('calendarFields')}
LeftIcon={IconCalendar}
text={t`Date field`}
contextualText={calendarFieldMetadata?.label}
contextualTextPosition="right"
hasSubMenu
/>
</SelectableListItem>
{isCalendarWeekViewEnabled && (
{isCalendarWeekViewEnabled ? (
<SelectableListItem
itemId="CalendarEndDateField"
onEnter={() => onContentChange('calendarEndFields')}
itemId="CalendarDateFields"
onEnter={() => onContentChange('calendarDateFields')}
>
<MenuItem
focused={selectedItemId === 'CalendarEndDateField'}
onClick={() => onContentChange('calendarEndFields')}
focused={selectedItemId === 'CalendarDateFields'}
onClick={() => onContentChange('calendarDateFields')}
LeftIcon={IconCalendar}
text={t`End date field`}
contextualText={
calendarEndFieldMetadata?.label ?? t`None`
}
text={t`Date fields`}
contextualText={calendarFieldMetadata?.label}
contextualTextPosition="right"
hasSubMenu
/>
</SelectableListItem>
) : (
<SelectableListItem
itemId="CalendarDateField"
onEnter={() => onContentChange('calendarFields')}
>
<MenuItem
focused={selectedItemId === 'CalendarDateField'}
onClick={() => onContentChange('calendarFields')}
LeftIcon={IconCalendar}
text={t`Date field`}
contextualText={calendarFieldMetadata?.label}
contextualTextPosition="right"
hasSubMenu
/>
@@ -8,6 +8,7 @@ export type ObjectOptionsContentId =
| 'recordGroupFields'
| 'recordGroupSort'
| 'addRecordGroup'
| 'calendarDateFields'
| 'calendarFields'
| 'calendarEndFields'
| 'calendarView'
@@ -0,0 +1,20 @@
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { SETTINGS_NON_COMPOSITE_FIELD_TYPE_CONFIGS } from '@/settings/data-model/constants/SettingsNonCompositeFieldTypeConfigs';
import { FieldMetadataType } from '~/generated-metadata/graphql';
const CALENDAR_FIELD_METADATA_TYPES = [
FieldMetadataType.DATE,
FieldMetadataType.DATE_TIME,
] as const;
export const groupCalendarFieldMetadataItemsByType = <
T extends Pick<FieldMetadataItem, 'type'>,
>(
fieldMetadataItems: T[],
) =>
CALENDAR_FIELD_METADATA_TYPES.map((fieldMetadataType) => ({
label: SETTINGS_NON_COMPOSITE_FIELD_TYPE_CONFIGS[fieldMetadataType].label,
fields: fieldMetadataItems.filter(
(fieldMetadataItem) => fieldMetadataItem.type === fieldMetadataType,
),
})).filter((group) => group.fields.length > 0);