From a81eb854d14e33d25cdd0c20ca069444eeba18bc Mon Sep 17 00:00:00 2001 From: Weiko Date: Sun, 14 Sep 2025 17:46:42 +0200 Subject: [PATCH] Add calendar view type in picker (#14477) --- .../ObjectOptionsDropdownLayoutContent.tsx | 25 ++++++++++++++++ .../useSetViewTypeFromLayoutOptionsMenu.ts | 30 ++++++++++++++++++- .../hooks/useCreateViewFromCurrentView.ts | 9 +++++- .../src/modules/views/types/GraphQLView.ts | 2 ++ .../src/modules/views/types/ViewType.ts | 3 ++ .../utils/convertCoreViewTypeToViewType.ts | 14 +++++++-- .../utils/convertUpdateViewInputToCore.ts | 1 + .../views/utils/convertViewTypeToCore.ts | 11 ++++++- .../ViewPickerContentCreateMode.tsx | 15 +++++++++- .../components/ViewPickerCreateButton.tsx | 2 +- .../constants/ViewPickerTypeSelectOptions.ts | 5 ++++ 11 files changed, 109 insertions(+), 8 deletions(-) diff --git a/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownLayoutContent.tsx b/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownLayoutContent.tsx index 02000a1ec0..23ad6767f5 100644 --- a/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownLayoutContent.tsx +++ b/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownLayoutContent.tsx @@ -18,6 +18,7 @@ import { useGetCurrentViewOnly } from '@/views/hooks/useGetCurrentViewOnly'; import { ViewOpenRecordInType } from '@/views/types/ViewOpenRecordInType'; import { ViewType, viewTypeIconMapping } from '@/views/types/ViewType'; import { useGetAvailableFieldsForKanban } from '@/views/view-picker/hooks/useGetAvailableFieldsForKanban'; +import { useFeatureFlagsMap } from '@/workspace/hooks/useFeatureFlagsMap'; import { useLingui } from '@lingui/react/macro'; import { useRecoilValue } from 'recoil'; import { isDefined } from 'twenty-shared/utils'; @@ -31,6 +32,7 @@ import { OverflowingTextWithTooltip, } from 'twenty-ui/display'; import { MenuItem, MenuItemSelect, MenuItemToggle } from 'twenty-ui/navigation'; +import { FeatureFlagKey } from '~/generated/graphql'; export const ObjectOptionsDropdownLayoutContent = () => { const { t } = useLingui(); @@ -79,9 +81,14 @@ export const ObjectOptionsDropdownLayoutContent = () => { const isDefaultView = currentView?.key === 'INDEX'; const nbsp = '\u00A0'; + const featureFlags = useFeatureFlagsMap(); + const hasCalendarViewEnabled = + featureFlags[FeatureFlagKey.IS_CALENDAR_VIEW_ENABLED]; + const selectableItemIdArray = [ ViewType.Table, ...(isDefaultView ? [] : [ViewType.Kanban]), + ...(!isDefaultView && hasCalendarViewEnabled ? [ViewType.Calendar] : []), ViewOpenRecordInType.SIDE_PANEL, ...(currentView?.type === ViewType.Kanban ? ['Group', 'Compact view'] : []), ]; @@ -129,6 +136,24 @@ export const ObjectOptionsDropdownLayoutContent = () => { }} /> + {hasCalendarViewEnabled && ( + { + setAndPersistViewType(ViewType.Calendar); + }} + > + { + setAndPersistViewType(ViewType.Calendar); + }} + /> + + )} { diff --git a/packages/twenty-front/src/modules/object-record/object-options-dropdown/hooks/useSetViewTypeFromLayoutOptionsMenu.ts b/packages/twenty-front/src/modules/object-record/object-options-dropdown/hooks/useSetViewTypeFromLayoutOptionsMenu.ts index 42dd5cc124..16f76173a2 100644 --- a/packages/twenty-front/src/modules/object-record/object-options-dropdown/hooks/useSetViewTypeFromLayoutOptionsMenu.ts +++ b/packages/twenty-front/src/modules/object-record/object-options-dropdown/hooks/useSetViewTypeFromLayoutOptionsMenu.ts @@ -17,6 +17,7 @@ import { useCallback } from 'react'; import { useRecoilCallback, useSetRecoilState } from 'recoil'; import { assertUnreachable, isDefined } from 'twenty-shared/utils'; import { v4 } from 'uuid'; +import { ViewCalendarLayout } from '~/generated/graphql'; export const useSetViewTypeFromLayoutOptionsMenu = () => { const { updateCurrentView } = useUpdateCurrentView(); @@ -129,7 +130,7 @@ export const useSetViewTypeFromLayoutOptionsMenu = () => { } return await updateCurrentView(updateCurrentViewParams); } - case ViewType.Table: + case ViewType.Table: { setRecordIndexViewType(viewType); set(coreViewsState, [ ...existingCoreViews.filter( @@ -146,6 +147,27 @@ export const useSetViewTypeFromLayoutOptionsMenu = () => { viewTypeIconMapping(viewType).displayName; } return await updateCurrentView(updateCurrentViewParams); + } + case ViewType.Calendar: { + setRecordIndexViewType(viewType); + set(coreViewsState, [ + ...existingCoreViews.filter( + (coreView) => coreView.id !== currentView.id, + ), + { + ...currentCoreView, + type: convertViewTypeToCore(viewType), + calendarLayout: ViewCalendarLayout.MONTH, + }, + ]); + + if (shouldChangeIcon(currentView.icon, currentView.type)) { + updateCurrentViewParams.icon = + viewTypeIconMapping(viewType).displayName; + } + updateCurrentViewParams.calendarLayout = ViewCalendarLayout.MONTH; + return await updateCurrentView(updateCurrentViewParams); + } default: { return assertUnreachable(viewType); } @@ -177,6 +199,12 @@ export const useSetViewTypeFromLayoutOptionsMenu = () => { ) { return true; } + if ( + oldViewType === ViewType.Calendar && + oldIcon === viewTypeIconMapping(ViewType.Calendar).displayName + ) { + return true; + } return false; }; diff --git a/packages/twenty-front/src/modules/views/hooks/useCreateViewFromCurrentView.ts b/packages/twenty-front/src/modules/views/hooks/useCreateViewFromCurrentView.ts index 9205270ae0..84e5966031 100644 --- a/packages/twenty-front/src/modules/views/hooks/useCreateViewFromCurrentView.ts +++ b/packages/twenty-front/src/modules/views/hooks/useCreateViewFromCurrentView.ts @@ -27,6 +27,7 @@ import { mapRecordSortToViewSort } from '@/views/utils/mapRecordSortToViewSort'; import { useRecoilCallback } from 'recoil'; import { isDefined } from 'twenty-shared/utils'; import { v4 } from 'uuid'; +import { ViewCalendarLayout } from '~/generated-metadata/graphql'; import { type CreateCoreViewFieldMutationVariables, useCreateCoreViewMutation, @@ -111,6 +112,8 @@ export const useCreateViewFromCurrentView = (viewBarComponentId?: string) => { set(isPersistingViewFieldsState, true); + const viewType = type ?? sourceView.type; + const result = await createCoreViewMutation({ variables: { input: { @@ -125,12 +128,16 @@ export const useCreateViewFromCurrentView = (viewBarComponentId?: string) => { shouldCopyFiltersAndSortsAndAggregate ? sourceView.kanbanAggregateOperationFieldMetadataId : undefined, - type: convertViewTypeToCore(type ?? sourceView.type), + type: convertViewTypeToCore(viewType), objectMetadataId: sourceView.objectMetadataId, openRecordIn: convertViewOpenRecordInToCore( sourceView.openRecordIn, ), anyFieldFilterValue: anyFieldFilterValue, + calendarLayout: + viewType === ViewType.Calendar + ? ViewCalendarLayout.MONTH + : undefined, }, }, }); diff --git a/packages/twenty-front/src/modules/views/types/GraphQLView.ts b/packages/twenty-front/src/modules/views/types/GraphQLView.ts index 3d444929a4..590668647e 100644 --- a/packages/twenty-front/src/modules/views/types/GraphQLView.ts +++ b/packages/twenty-front/src/modules/views/types/GraphQLView.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 GraphQLView = { id: string; @@ -30,4 +31,5 @@ export type GraphQLView = { position: number; icon: string; anyFieldFilterValue?: string | null; + calendarLayout?: ViewCalendarLayout | null; }; diff --git a/packages/twenty-front/src/modules/views/types/ViewType.ts b/packages/twenty-front/src/modules/views/types/ViewType.ts index 8dc12d41b1..1a2e1fbb27 100644 --- a/packages/twenty-front/src/modules/views/types/ViewType.ts +++ b/packages/twenty-front/src/modules/views/types/ViewType.ts @@ -1,4 +1,5 @@ import { + IconCalendar, type IconComponent, IconLayoutKanban, IconTable, @@ -6,11 +7,13 @@ import { export enum ViewType { Table = 'table', Kanban = 'kanban', + Calendar = 'calendar', } const VIEW_TYPE_ICON_MAPPING = [ { icon: IconLayoutKanban, value: ViewType.Kanban }, { icon: IconTable, value: ViewType.Table }, + { icon: IconCalendar, value: ViewType.Calendar }, ] as const satisfies { icon: IconComponent; value: ViewType; diff --git a/packages/twenty-front/src/modules/views/utils/convertCoreViewTypeToViewType.ts b/packages/twenty-front/src/modules/views/utils/convertCoreViewTypeToViewType.ts index ab2cf58e51..bb32be8f14 100644 --- a/packages/twenty-front/src/modules/views/utils/convertCoreViewTypeToViewType.ts +++ b/packages/twenty-front/src/modules/views/utils/convertCoreViewTypeToViewType.ts @@ -4,7 +4,15 @@ import { ViewType as CoreViewType } from '~/generated/graphql'; export const convertCoreViewTypeToViewType = ( coreViewType: CoreViewType, ): ViewType => { - return coreViewType === CoreViewType.KANBAN - ? ViewType.Kanban - : ViewType.Table; + const coreViewTypeToViewTypeMapping = { + [CoreViewType.KANBAN]: ViewType.Kanban, + [CoreViewType.TABLE]: ViewType.Table, + [CoreViewType.CALENDAR]: ViewType.Calendar, + }; + + return ( + coreViewTypeToViewTypeMapping[ + coreViewType as keyof typeof coreViewTypeToViewTypeMapping + ] ?? ViewType.Table + ); }; diff --git a/packages/twenty-front/src/modules/views/utils/convertUpdateViewInputToCore.ts b/packages/twenty-front/src/modules/views/utils/convertUpdateViewInputToCore.ts index 2ee0db0dd5..4669e4c18a 100644 --- a/packages/twenty-front/src/modules/views/utils/convertUpdateViewInputToCore.ts +++ b/packages/twenty-front/src/modules/views/utils/convertUpdateViewInputToCore.ts @@ -37,5 +37,6 @@ export const convertUpdateViewInputToCore = ( ...(convertedKey && { key: convertedKey }), ...(convertedOpenRecordIn && { openRecordIn: convertedOpenRecordIn }), ...(convertedType && { type: convertedType }), + ...(view.calendarLayout && { calendarLayout: view.calendarLayout }), }; }; diff --git a/packages/twenty-front/src/modules/views/utils/convertViewTypeToCore.ts b/packages/twenty-front/src/modules/views/utils/convertViewTypeToCore.ts index fbc568c50d..c2e85e249c 100644 --- a/packages/twenty-front/src/modules/views/utils/convertViewTypeToCore.ts +++ b/packages/twenty-front/src/modules/views/utils/convertViewTypeToCore.ts @@ -1,5 +1,14 @@ import { ViewType } from '~/generated-metadata/graphql'; export const convertViewTypeToCore = (viewType: string): ViewType => { - return viewType === 'kanban' ? ViewType.KANBAN : ViewType.TABLE; + const viewTypeToCoreMapping = { + kanban: ViewType.KANBAN, + table: ViewType.TABLE, + calendar: ViewType.CALENDAR, + }; + + return ( + viewTypeToCoreMapping[viewType as keyof typeof viewTypeToCoreMapping] ?? + ViewType.TABLE + ); }; diff --git a/packages/twenty-front/src/modules/views/view-picker/components/ViewPickerContentCreateMode.tsx b/packages/twenty-front/src/modules/views/view-picker/components/ViewPickerContentCreateMode.tsx index 2dce44fc9b..aab45ae55c 100644 --- a/packages/twenty-front/src/modules/views/view-picker/components/ViewPickerContentCreateMode.tsx +++ b/packages/twenty-front/src/modules/views/view-picker/components/ViewPickerContentCreateMode.tsx @@ -33,9 +33,11 @@ import { viewPickerIsPersistingComponentState } from '@/views/view-picker/states import { viewPickerKanbanFieldMetadataIdComponentState } from '@/views/view-picker/states/viewPickerKanbanFieldMetadataIdComponentState'; import { viewPickerSelectedIconComponentState } from '@/views/view-picker/states/viewPickerSelectedIconComponentState'; import { viewPickerTypeComponentState } from '@/views/view-picker/states/viewPickerTypeComponentState'; +import { useFeatureFlagsMap } from '@/workspace/hooks/useFeatureFlagsMap'; import { useLingui } from '@lingui/react/macro'; import { useMemo, useState } from 'react'; import { IconX } from 'twenty-ui/display'; +import { FeatureFlagKey } from '~/generated/graphql'; const StyledNoKanbanFieldAvailableContainer = styled.div` color: ${({ theme }) => theme.font.color.light}; @@ -82,6 +84,17 @@ export const ViewPickerContentCreateMode = () => { const { availableFieldsForKanban } = useGetAvailableFieldsForKanban(); + const featureFlags = useFeatureFlagsMap(); + + const hasCalendarViewEnabled = + featureFlags[FeatureFlagKey.IS_CALENDAR_VIEW_ENABLED]; + + const viewPickerTypeOptions = VIEW_PICKER_TYPE_SELECT_OPTIONS.filter( + (option) => { + return option.value !== ViewType.Calendar || hasCalendarViewEnabled; + }, + ); + useHotkeysOnFocusedElement({ keys: [Key.Enter], callback: async () => { @@ -164,7 +177,7 @@ export const ViewPickerContentCreateMode = () => { setViewPickerIsDirty(true); setViewPickerType(value); }} - options={VIEW_PICKER_TYPE_SELECT_OPTIONS.map((option) => ({ + options={viewPickerTypeOptions.map((option) => ({ ...option, label: t(option.label), }))} 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 a57015f2e5..4626955d46 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 @@ -64,7 +64,7 @@ export const ViewPickerCreateButton = () => { } if ( - viewPickerType === ViewType.Table || + viewPickerType !== ViewType.Kanban || viewPickerKanbanFieldMetadataId !== '' ) { return ( diff --git a/packages/twenty-front/src/modules/views/view-picker/constants/ViewPickerTypeSelectOptions.ts b/packages/twenty-front/src/modules/views/view-picker/constants/ViewPickerTypeSelectOptions.ts index 3025a19ba1..3caf6269b5 100644 --- a/packages/twenty-front/src/modules/views/view-picker/constants/ViewPickerTypeSelectOptions.ts +++ b/packages/twenty-front/src/modules/views/view-picker/constants/ViewPickerTypeSelectOptions.ts @@ -12,4 +12,9 @@ export const VIEW_PICKER_TYPE_SELECT_OPTIONS = [ label: msg`Kanban`, Icon: viewTypeIconMapping(ViewType.Kanban), }, + { + value: ViewType.Calendar, + label: msg`Calendar`, + Icon: viewTypeIconMapping(ViewType.Calendar), + }, ];