Add calendar date field picker in view options menu (#14492)

<img width="612" height="352" alt="Screenshot 2025-09-15 at 10 36 02"
src="https://github.com/user-attachments/assets/d1da5ddf-cb9a-49f2-8407-e3d22cad808b"
/>
<img width="612" height="255" alt="Screenshot 2025-09-15 at 10 35 57"
src="https://github.com/user-attachments/assets/3fca9e04-6a61-42ce-96a8-4dcd93e06f66"
/>
This commit is contained in:
Weiko
2025-09-15 10:58:07 +02:00
committed by GitHub
parent 155aa4995e
commit b40de0fa07
9 changed files with 243 additions and 38 deletions
@@ -0,0 +1,111 @@
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
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 { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput';
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 { useLingui } from '@lingui/react/macro';
import { useState } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { IconChevronLeft, IconSettings, useIcons } from 'twenty-ui/display';
import { MenuItem, MenuItemSelect } from 'twenty-ui/navigation';
import { FieldMetadataType } from '~/generated-metadata/graphql';
export const ObjectOptionsDropdownCalendarFieldsContent = () => {
const { t } = useLingui();
const { getIcon } = useIcons();
const [searchInput, setSearchInput] = useState('');
const { objectMetadataItem, resetContent, closeDropdown } =
useObjectOptionsDropdown();
const { currentView } = useGetCurrentViewOnly();
const { updateCurrentView } = useUpdateCurrentView();
const { navigateToDateFieldSettings } = useGetAvailableFieldsForCalendar();
const availableFieldsForCalendar = objectMetadataItem.fields.filter(
(field) =>
field.type === FieldMetadataType.DATE ||
field.type === FieldMetadataType.DATE_TIME,
);
const calendarFieldMetadata = currentView?.calendarFieldMetadataId
? objectMetadataItem.fields.find(
(field) => field.id === currentView.calendarFieldMetadataId,
)
: undefined;
const filteredCalendarFields = availableFieldsForCalendar.filter((field) =>
field.label.toLowerCase().includes(searchInput.toLowerCase()),
);
const handleCalendarFieldChange = async (
fieldMetadataItem: FieldMetadataItem,
) => {
await updateCurrentView({
calendarFieldMetadataId: fieldMetadataItem.id,
});
closeDropdown();
};
const handleResetCalendarField = async () => {
await updateCurrentView({
calendarFieldMetadataId: null,
});
closeDropdown();
};
return (
<DropdownContent>
<DropdownMenuHeader
StartComponent={
<DropdownMenuHeaderLeftComponent
onClick={() => resetContent()}
Icon={IconChevronLeft}
/>
}
>
{t`Date field`}
</DropdownMenuHeader>
<DropdownMenuSearchInput
autoFocus
value={searchInput}
placeholder={t`Search fields`}
onChange={(event) => setSearchInput(event.target.value)}
/>
<DropdownMenuSeparator />
<DropdownMenuItemsContainer>
<MenuItemSelect
text={t`None`}
selected={!isDefined(calendarFieldMetadata)}
onClick={handleResetCalendarField}
/>
{filteredCalendarFields.map((fieldMetadataItem) => (
<MenuItemSelect
key={fieldMetadataItem.id}
selected={fieldMetadataItem.id === calendarFieldMetadata?.id}
onClick={() => handleCalendarFieldChange(fieldMetadataItem)}
LeftIcon={getIcon(fieldMetadataItem.icon)}
text={fieldMetadataItem.label}
/>
))}
</DropdownMenuItemsContainer>
<DropdownMenuSeparator />
<DropdownMenuItemsContainer scrollable={false}>
<MenuItem
LeftIcon={IconSettings}
text={t`Create date field`}
onClick={() => {
navigateToDateFieldSettings();
closeDropdown();
}}
/>
</DropdownMenuItemsContainer>
</DropdownContent>
);
};
@@ -1,3 +1,4 @@
import { ObjectOptionsDropdownCalendarFieldsContent } from '@/object-record/object-options-dropdown/components/ObjectOptionsDropdownCalendarFieldsContent';
import { ObjectOptionsDropdownFieldsContent } from '@/object-record/object-options-dropdown/components/ObjectOptionsDropdownFieldsContent';
import { ObjectOptionsDropdownHiddenFieldsContent } from '@/object-record/object-options-dropdown/components/ObjectOptionsDropdownHiddenFieldsContent';
import { ObjectOptionsDropdownHiddenRecordGroupsContent } from '@/object-record/object-options-dropdown/components/ObjectOptionsDropdownHiddenRecordGroupsContent';
@@ -29,6 +30,8 @@ export const ObjectOptionsDropdownContent = () => {
return <ObjectOptionsDropdownRecordGroupSortContent />;
case 'hiddenRecordGroups':
return <ObjectOptionsDropdownHiddenRecordGroupsContent />;
case 'calendarFields':
return <ObjectOptionsDropdownCalendarFieldsContent />;
default:
return <ObjectOptionsDropdownMenuContent />;
}
@@ -54,6 +54,12 @@ export const ObjectOptionsDropdownCustomView = ({
recordGroupFieldMetadataComponentState,
);
const calendarFieldMetadata = currentView?.calendarFieldMetadataId
? objectMetadataItem.fields.find(
(field) => field.id === currentView.calendarFieldMetadataId,
)
: undefined;
const isDefaultView = currentView?.key === ViewKey.Index;
const { visibleBoardFields } = useObjectOptionsForBoard({
@@ -82,8 +88,11 @@ export const ObjectOptionsDropdownCustomView = ({
const selectableItemIdArray = [
'Layout',
'Fields',
'Group',
...(customViewData?.type !== ViewType.Calendar ? ['Fields'] : []),
...(customViewData?.type === ViewType.Calendar
? ['CalendarDateField']
: []),
...(customViewData?.type !== ViewType.Calendar ? ['Group'] : []),
'Copy link to view',
'Delete view',
];
@@ -125,47 +134,73 @@ export const ObjectOptionsDropdownCustomView = ({
</DropdownMenuItemsContainer>
<DropdownMenuSeparator />
<DropdownMenuItemsContainer scrollable={false}>
<SelectableListItem
itemId="Fields"
onEnter={() => onContentChange('fields')}
>
<MenuItem
focused={selectedItemId === 'Fields'}
onClick={() => onContentChange('fields')}
LeftIcon={IconListDetails}
text={t`Fields`}
contextualText={`${visibleBoardFields.length} shown`}
hasSubMenu
/>
</SelectableListItem>
<div id="group-by-menu-item">
{customViewData?.type !== ViewType.Calendar && (
<SelectableListItem
itemId="Group"
onEnter={() =>
isDefined(recordGroupFieldMetadata)
? onContentChange('recordGroups')
: onContentChange('recordGroupFields')
}
itemId="Fields"
onEnter={() => onContentChange('fields')}
>
<MenuItem
focused={selectedItemId === 'Group'}
onClick={() =>
focused={selectedItemId === 'Fields'}
onClick={() => onContentChange('fields')}
LeftIcon={IconListDetails}
text={t`Fields`}
contextualText={`${visibleBoardFields.length} shown`}
hasSubMenu
/>
</SelectableListItem>
)}
{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={IconLayoutList}
text={t`Date field`}
contextualText={
isDefaultView
? t`Not available on Default View`
: calendarFieldMetadata?.label
}
hasSubMenu
disabled={isDefaultView}
/>
</SelectableListItem>
</div>
)}
{customViewData?.type !== ViewType.Calendar && (
<div id="group-by-menu-item">
<SelectableListItem
itemId="Group"
onEnter={() =>
isDefined(recordGroupFieldMetadata)
? onContentChange('recordGroups')
: onContentChange('recordGroupFields')
}
LeftIcon={IconLayoutList}
text={t`Group`}
contextualText={
isDefaultView
? t`Not available on Default View`
: recordGroupFieldMetadata?.label
}
hasSubMenu
disabled={isDefaultView}
/>
</SelectableListItem>
</div>
>
<MenuItem
focused={selectedItemId === 'Group'}
onClick={() =>
isDefined(recordGroupFieldMetadata)
? onContentChange('recordGroups')
: onContentChange('recordGroupFields')
}
LeftIcon={IconLayoutList}
text={t`Group`}
contextualText={
isDefaultView
? t`Not available on Default View`
: recordGroupFieldMetadata?.label
}
hasSubMenu
disabled={isDefaultView}
/>
</SelectableListItem>
</div>
)}
{isDefaultView && (
<AppTooltip
anchorSelect={`#group-by-menu-item`}
@@ -60,6 +60,12 @@ export const ObjectOptionsDropdownLayoutContent = () => {
recordGroupFieldMetadataComponentState,
);
const calendarFieldMetadata = currentView?.calendarFieldMetadataId
? objectMetadataItem.fields.find(
(field) => field.id === currentView.calendarFieldMetadataId,
)
: undefined;
const { setAndPersistViewType } = useSetViewTypeFromLayoutOptionsMenu();
const { availableFieldsForKanban, navigateToSelectSettings } =
useGetAvailableFieldsForKanban();
@@ -106,6 +112,7 @@ export const ObjectOptionsDropdownLayoutContent = () => {
...(!isDefaultView && hasCalendarViewEnabled ? [ViewType.Calendar] : []),
ViewOpenRecordInType.SIDE_PANEL,
...(currentView?.type === ViewType.Kanban ? ['Group', 'Compact view'] : []),
...(currentView?.type === ViewType.Calendar ? ['CalendarDateField'] : []),
];
const selectedItemId = useRecoilComponentValue(
@@ -271,6 +278,21 @@ export const ObjectOptionsDropdownLayoutContent = () => {
</SelectableListItem>
</>
)}
{currentView?.type === ViewType.Calendar && (
<SelectableListItem
itemId="CalendarDateField"
onEnter={() => onContentChange('calendarFields')}
>
<MenuItem
focused={selectedItemId === 'CalendarDateField'}
onClick={() => onContentChange('calendarFields')}
LeftIcon={IconLayoutList}
text={t`Date field`}
contextualText={calendarFieldMetadata?.label}
hasSubMenu
/>
</SelectableListItem>
)}
</DropdownMenuItemsContainer>
</SelectableList>
)}
@@ -6,4 +6,5 @@ export type ObjectOptionsContentId =
| 'recordGroups'
| 'hiddenRecordGroups'
| 'recordGroupFields'
| 'recordGroupSort';
| 'recordGroupSort'
| 'calendarFields';
@@ -5,6 +5,7 @@ import {
type CoreViewFilter,
type CoreViewFilterGroup,
type CoreViewGroup,
type ViewCalendarLayout,
type ViewKey,
type ViewOpenRecordIn,
type ViewType,
@@ -30,6 +31,8 @@ export type CoreViewWithRelations = {
viewSorts: CoreViewSortEssential[];
kanbanAggregateOperation?: AggregateOperations | null;
kanbanAggregateOperationFieldMetadataId?: string | null;
calendarFieldMetadataId?: string | null;
calendarLayout?: ViewCalendarLayout | null;
position: number;
icon: string;
openRecordIn: ViewOpenRecordIn;
@@ -7,6 +7,7 @@ import { type ViewGroup } from '@/views/types/ViewGroup';
import { type ViewKey } from '@/views/types/ViewKey';
import { type ViewOpenRecordInType } from '@/views/types/ViewOpenRecordInType';
import { type ViewType } from '@/views/types/ViewType';
import { type ViewCalendarLayout } from '~/generated/graphql';
export type View = {
id: string;
@@ -26,6 +27,8 @@ export type View = {
kanbanFieldMetadataId: string;
kanbanAggregateOperation: AggregateOperations | null;
kanbanAggregateOperationFieldMetadataId: string | null;
calendarFieldMetadataId?: string | null;
calendarLayout?: ViewCalendarLayout | null;
position: number;
icon: string;
openRecordIn: ViewOpenRecordInType;
@@ -41,6 +41,8 @@ export const convertCoreViewToView = (
kanbanAggregateOperation: coreView.kanbanAggregateOperation ?? null,
kanbanAggregateOperationFieldMetadataId:
coreView.kanbanAggregateOperationFieldMetadataId ?? null,
calendarFieldMetadataId: coreView.calendarFieldMetadataId ?? null,
calendarLayout: coreView.calendarLayout ?? null,
position: coreView.position,
icon: coreView.icon,
openRecordIn: convertedOpenRecordIn,
@@ -2,8 +2,10 @@ import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/ho
import { ViewType } from '@/views/types/ViewType';
import { useCreateViewFromCurrentState } from '@/views/view-picker/hooks/useCreateViewFromCurrentState';
import { useDeleteViewFromCurrentState } from '@/views/view-picker/hooks/useDeleteViewFromCurrentState';
import { useGetAvailableFieldsForCalendar } from '@/views/view-picker/hooks/useGetAvailableFieldsForCalendar';
import { useGetAvailableFieldsForKanban } from '@/views/view-picker/hooks/useGetAvailableFieldsForKanban';
import { useViewPickerMode } from '@/views/view-picker/hooks/useViewPickerMode';
import { viewPickerCalendarFieldMetadataIdComponentState } from '@/views/view-picker/states/viewPickerCalendarFieldMetadataIdComponentState';
import { viewPickerIsPersistingComponentState } from '@/views/view-picker/states/viewPickerIsPersistingComponentState';
import { viewPickerKanbanFieldMetadataIdComponentState } from '@/views/view-picker/states/viewPickerKanbanFieldMetadataIdComponentState';
import { viewPickerTypeComponentState } from '@/views/view-picker/states/viewPickerTypeComponentState';
@@ -14,6 +16,8 @@ export const ViewPickerCreateButton = () => {
const { t } = useLingui();
const { availableFieldsForKanban, navigateToSelectSettings } =
useGetAvailableFieldsForKanban();
const { availableFieldsForCalendar, navigateToDateFieldSettings } =
useGetAvailableFieldsForCalendar();
const { viewPickerMode } = useViewPickerMode();
const viewPickerType = useRecoilComponentValue(viewPickerTypeComponentState);
@@ -23,6 +27,9 @@ export const ViewPickerCreateButton = () => {
const viewPickerKanbanFieldMetadataId = useRecoilComponentValue(
viewPickerKanbanFieldMetadataIdComponentState,
);
const viewPickerCalendarFieldMetadataId = useRecoilComponentValue(
viewPickerCalendarFieldMetadataIdComponentState,
);
const { createViewFromCurrentState } = useCreateViewFromCurrentState();
const { deleteViewFromCurrentState } = useDeleteViewFromCurrentState();
@@ -63,6 +70,22 @@ export const ViewPickerCreateButton = () => {
);
}
if (
viewPickerType === ViewType.Calendar &&
availableFieldsForCalendar.length === 0
) {
return (
<Button
title={t`Go to Settings`}
onClick={navigateToDateFieldSettings}
size="small"
accent="blue"
fullWidth
justify="center"
/>
);
}
if (
viewPickerType !== ViewType.Kanban ||
viewPickerKanbanFieldMetadataId !== ''
@@ -78,7 +101,9 @@ export const ViewPickerCreateButton = () => {
disabled={
viewPickerIsPersisting ||
(viewPickerType === ViewType.Kanban &&
viewPickerKanbanFieldMetadataId === '')
viewPickerKanbanFieldMetadataId === '') ||
(viewPickerType === ViewType.Calendar &&
viewPickerCalendarFieldMetadataId === '')
}
/>
);