diff --git a/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownCalendarFieldsContent.tsx b/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownCalendarFieldsContent.tsx new file mode 100644 index 0000000000..5f724f3b5f --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownCalendarFieldsContent.tsx @@ -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 ( + + resetContent()} + Icon={IconChevronLeft} + /> + } + > + {t`Date field`} + + setSearchInput(event.target.value)} + /> + + + + {filteredCalendarFields.map((fieldMetadataItem) => ( + handleCalendarFieldChange(fieldMetadataItem)} + LeftIcon={getIcon(fieldMetadataItem.icon)} + text={fieldMetadataItem.label} + /> + ))} + + + + { + navigateToDateFieldSettings(); + closeDropdown(); + }} + /> + + + ); +}; diff --git a/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownContent.tsx b/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownContent.tsx index c45d8f5512..8eed2e017b 100644 --- a/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownContent.tsx +++ b/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownContent.tsx @@ -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 ; case 'hiddenRecordGroups': return ; + case 'calendarFields': + return ; default: return ; } diff --git a/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownCustomView.tsx b/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownCustomView.tsx index 8da219a604..dbc91780e0 100644 --- a/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownCustomView.tsx +++ b/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownCustomView.tsx @@ -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 = ({ - onContentChange('fields')} - > - onContentChange('fields')} - LeftIcon={IconListDetails} - text={t`Fields`} - contextualText={`${visibleBoardFields.length} shown`} - hasSubMenu - /> - -
+ {customViewData?.type !== ViewType.Calendar && ( - isDefined(recordGroupFieldMetadata) - ? onContentChange('recordGroups') - : onContentChange('recordGroupFields') - } + itemId="Fields" + onEnter={() => onContentChange('fields')} > + focused={selectedItemId === 'Fields'} + onClick={() => onContentChange('fields')} + LeftIcon={IconListDetails} + text={t`Fields`} + contextualText={`${visibleBoardFields.length} shown`} + hasSubMenu + /> + + )} + {customViewData?.type === ViewType.Calendar && ( +
+ onContentChange('calendarFields')} + > + onContentChange('calendarFields')} + LeftIcon={IconLayoutList} + text={t`Date field`} + contextualText={ + isDefaultView + ? t`Not available on Default View` + : calendarFieldMetadata?.label + } + hasSubMenu + disabled={isDefaultView} + /> + +
+ )} + {customViewData?.type !== ViewType.Calendar && ( +
+ isDefined(recordGroupFieldMetadata) ? onContentChange('recordGroups') : onContentChange('recordGroupFields') } - LeftIcon={IconLayoutList} - text={t`Group`} - contextualText={ - isDefaultView - ? t`Not available on Default View` - : recordGroupFieldMetadata?.label - } - hasSubMenu - disabled={isDefaultView} - /> - -
+ > + + isDefined(recordGroupFieldMetadata) + ? onContentChange('recordGroups') + : onContentChange('recordGroupFields') + } + LeftIcon={IconLayoutList} + text={t`Group`} + contextualText={ + isDefaultView + ? t`Not available on Default View` + : recordGroupFieldMetadata?.label + } + hasSubMenu + disabled={isDefaultView} + /> + +
+ )} {isDefaultView && ( { 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 = () => { )} + {currentView?.type === ViewType.Calendar && ( + onContentChange('calendarFields')} + > + onContentChange('calendarFields')} + LeftIcon={IconLayoutList} + text={t`Date field`} + contextualText={calendarFieldMetadata?.label} + hasSubMenu + /> + + )}
)} diff --git a/packages/twenty-front/src/modules/object-record/object-options-dropdown/types/ObjectOptionsContentId.ts b/packages/twenty-front/src/modules/object-record/object-options-dropdown/types/ObjectOptionsContentId.ts index 92ecb6fd92..4457c56b33 100644 --- a/packages/twenty-front/src/modules/object-record/object-options-dropdown/types/ObjectOptionsContentId.ts +++ b/packages/twenty-front/src/modules/object-record/object-options-dropdown/types/ObjectOptionsContentId.ts @@ -6,4 +6,5 @@ export type ObjectOptionsContentId = | 'recordGroups' | 'hiddenRecordGroups' | 'recordGroupFields' - | 'recordGroupSort'; + | 'recordGroupSort' + | 'calendarFields'; diff --git a/packages/twenty-front/src/modules/views/types/CoreViewWithRelations.ts b/packages/twenty-front/src/modules/views/types/CoreViewWithRelations.ts index c45c1f6a1e..a983753a7d 100644 --- a/packages/twenty-front/src/modules/views/types/CoreViewWithRelations.ts +++ b/packages/twenty-front/src/modules/views/types/CoreViewWithRelations.ts @@ -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; diff --git a/packages/twenty-front/src/modules/views/types/View.ts b/packages/twenty-front/src/modules/views/types/View.ts index 7e335fe52d..bde7957f13 100644 --- a/packages/twenty-front/src/modules/views/types/View.ts +++ b/packages/twenty-front/src/modules/views/types/View.ts @@ -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; diff --git a/packages/twenty-front/src/modules/views/utils/convertCoreViewToView.ts b/packages/twenty-front/src/modules/views/utils/convertCoreViewToView.ts index c88d01c644..d1ed0fa67a 100644 --- a/packages/twenty-front/src/modules/views/utils/convertCoreViewToView.ts +++ b/packages/twenty-front/src/modules/views/utils/convertCoreViewToView.ts @@ -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, diff --git a/packages/twenty-front/src/modules/views/view-picker/components/ViewPickerCreateButton.tsx b/packages/twenty-front/src/modules/views/view-picker/components/ViewPickerCreateButton.tsx index 4626955d46..1cacc63beb 100644 --- a/packages/twenty-front/src/modules/views/view-picker/components/ViewPickerCreateButton.tsx +++ b/packages/twenty-front/src/modules/views/view-picker/components/ViewPickerCreateButton.tsx @@ -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 ( +