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:
+111
@@ -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>
|
||||
);
|
||||
};
|
||||
+3
@@ -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 />;
|
||||
}
|
||||
|
||||
+71
-36
@@ -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`}
|
||||
|
||||
+22
@@ -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>
|
||||
)}
|
||||
|
||||
+2
-1
@@ -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,
|
||||
|
||||
+26
-1
@@ -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 === '')
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user