Add calendar view type in picker (#14477)

This commit is contained in:
Weiko
2025-09-14 17:46:42 +02:00
committed by GitHub
parent 72da21217f
commit a81eb854d1
11 changed files with 109 additions and 8 deletions
@@ -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 = () => {
}}
/>
</SelectableListItem>
{hasCalendarViewEnabled && (
<SelectableListItem
itemId={ViewType.Calendar}
onEnter={() => {
setAndPersistViewType(ViewType.Calendar);
}}
>
<MenuItemSelect
LeftIcon={viewTypeIconMapping(ViewType.Calendar)}
text={t`Calendar`}
selected={currentView?.type === ViewType.Calendar}
focused={selectedItemId === ViewType.Calendar}
onClick={() => {
setAndPersistViewType(ViewType.Calendar);
}}
/>
</SelectableListItem>
)}
<SelectableListItem
itemId={ViewType.Kanban}
onEnter={() => {
@@ -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;
};
@@ -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,
},
},
});
@@ -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;
};
@@ -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;
@@ -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
);
};
@@ -37,5 +37,6 @@ export const convertUpdateViewInputToCore = (
...(convertedKey && { key: convertedKey }),
...(convertedOpenRecordIn && { openRecordIn: convertedOpenRecordIn }),
...(convertedType && { type: convertedType }),
...(view.calendarLayout && { calendarLayout: view.calendarLayout }),
};
};
@@ -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
);
};
@@ -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),
}))}
@@ -64,7 +64,7 @@ export const ViewPickerCreateButton = () => {
}
if (
viewPickerType === ViewType.Table ||
viewPickerType !== ViewType.Kanban ||
viewPickerKanbanFieldMetadataId !== ''
) {
return (
@@ -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),
},
];