Add calendar view layout picker in view options menu (#14495)

<img width="234" height="303" alt="Screenshot 2025-09-15 at 12 55 20"
src="https://github.com/user-attachments/assets/ac9fe1b6-4f48-4d3c-82bf-e2044f437c2e"
/>
<img width="243" height="198" alt="Screenshot 2025-09-15 at 12 55 12"
src="https://github.com/user-attachments/assets/2c23d87d-2f0b-43cc-bf79-18618fbf74d6"
/>
<img width="256" height="267" alt="Screenshot 2025-09-15 at 12 55 08"
src="https://github.com/user-attachments/assets/6a0dfc2a-8cba-46ba-bc02-a79ca621b810"
/>
This commit is contained in:
Weiko
2025-09-15 13:58:06 +02:00
committed by GitHub
parent 302dd01333
commit 1cdfc0fd62
9 changed files with 256 additions and 82 deletions
@@ -11,7 +11,6 @@ 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';
@@ -53,13 +52,6 @@ export const ObjectOptionsDropdownCalendarFieldsContent = () => {
closeDropdown();
};
const handleResetCalendarField = async () => {
await updateCurrentView({
calendarFieldMetadataId: null,
});
closeDropdown();
};
return (
<DropdownContent>
<DropdownMenuHeader
@@ -80,11 +72,6 @@ export const ObjectOptionsDropdownCalendarFieldsContent = () => {
/>
<DropdownMenuSeparator />
<DropdownMenuItemsContainer>
<MenuItemSelect
text={t`None`}
selected={!isDefined(calendarFieldMetadata)}
onClick={handleResetCalendarField}
/>
{filteredCalendarFields.map((fieldMetadataItem) => (
<MenuItemSelect
key={fieldMetadataItem.id}
@@ -0,0 +1,115 @@
import { OBJECT_OPTIONS_DROPDOWN_ID } from '@/object-record/object-options-dropdown/constants/ObjectOptionsDropdownId';
import { useObjectOptionsDropdown } from '@/object-record/object-options-dropdown/hooks/useObjectOptionsDropdown';
import { recordIndexCalendarLayoutState } from '@/object-record/record-index/states/recordIndexCalendarLayoutState';
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 { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList';
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
import { selectedItemIdComponentState } from '@/ui/layout/selectable-list/states/selectedItemIdComponentState';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { useUpdateCurrentView } from '@/views/hooks/useUpdateCurrentView';
import { t } from '@lingui/core/macro';
import { useRecoilValue, useSetRecoilState } from 'recoil';
import {
IconCalendarMonth,
IconCalendarWeek,
IconChevronLeft,
IconTimelineEvent,
} from 'twenty-ui/display';
import { MenuItemSelect } from 'twenty-ui/navigation';
import { ViewCalendarLayout } from '~/generated-metadata/graphql';
export const ObjectOptionsDropdownCalendarViewContent = () => {
const { resetContent } = useObjectOptionsDropdown();
const recordIndexCalendarLayout = useRecoilValue(
recordIndexCalendarLayoutState,
);
const setRecordIndexCalendarLayout = useSetRecoilState(
recordIndexCalendarLayoutState,
);
const { updateCurrentView } = useUpdateCurrentView();
const selectedItemId = useRecoilComponentValue(
selectedItemIdComponentState,
OBJECT_OPTIONS_DROPDOWN_ID,
);
const { closeDropdown } = useObjectOptionsDropdown();
const selectableItemIdArray = [
ViewCalendarLayout.WEEK,
ViewCalendarLayout.MONTH,
ViewCalendarLayout.DAY,
];
const handleCalendarViewChange = async (calendarView: ViewCalendarLayout) => {
setRecordIndexCalendarLayout(calendarView);
await updateCurrentView({
calendarLayout: calendarView,
});
closeDropdown();
};
return (
<DropdownContent>
<DropdownMenuHeader
StartComponent={
<DropdownMenuHeaderLeftComponent
onClick={resetContent}
Icon={IconChevronLeft}
/>
}
>
{t`Calendar View`}
</DropdownMenuHeader>
<DropdownMenuItemsContainer>
<SelectableList
selectableListInstanceId={OBJECT_OPTIONS_DROPDOWN_ID}
focusId={OBJECT_OPTIONS_DROPDOWN_ID}
selectableItemIdArray={selectableItemIdArray}
>
<SelectableListItem
itemId={ViewCalendarLayout.WEEK}
onEnter={() => {
handleCalendarViewChange(ViewCalendarLayout.WEEK);
}}
>
<MenuItemSelect
LeftIcon={IconCalendarWeek}
text={t`Week`}
selected={recordIndexCalendarLayout === ViewCalendarLayout.WEEK}
focused={selectedItemId === ViewCalendarLayout.WEEK}
disabled
/>
</SelectableListItem>
<SelectableListItem
itemId={ViewCalendarLayout.MONTH}
onEnter={() => handleCalendarViewChange(ViewCalendarLayout.MONTH)}
>
<MenuItemSelect
LeftIcon={IconCalendarMonth}
text={t`Month`}
selected={recordIndexCalendarLayout === ViewCalendarLayout.MONTH}
onClick={() => handleCalendarViewChange(ViewCalendarLayout.MONTH)}
focused={selectedItemId === ViewCalendarLayout.MONTH}
/>
</SelectableListItem>
<SelectableListItem
itemId={ViewCalendarLayout.DAY}
onEnter={() => handleCalendarViewChange(ViewCalendarLayout.DAY)}
>
<MenuItemSelect
LeftIcon={IconTimelineEvent}
text={t`Timeline`}
selected={recordIndexCalendarLayout === ViewCalendarLayout.DAY}
focused={selectedItemId === ViewCalendarLayout.DAY}
disabled
/>
</SelectableListItem>
</SelectableList>
</DropdownMenuItemsContainer>
</DropdownContent>
);
};
@@ -1,4 +1,5 @@
import { ObjectOptionsDropdownCalendarFieldsContent } from '@/object-record/object-options-dropdown/components/ObjectOptionsDropdownCalendarFieldsContent';
import { ObjectOptionsDropdownCalendarViewContent } from '@/object-record/object-options-dropdown/components/ObjectOptionsDropdownCalendarViewContent';
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';
@@ -30,6 +31,8 @@ export const ObjectOptionsDropdownContent = () => {
return <ObjectOptionsDropdownRecordGroupSortContent />;
case 'hiddenRecordGroups':
return <ObjectOptionsDropdownHiddenRecordGroupsContent />;
case 'calendarView':
return <ObjectOptionsDropdownCalendarViewContent />;
case 'calendarFields':
return <ObjectOptionsDropdownCalendarFieldsContent />;
default:
@@ -3,6 +3,7 @@ import { OBJECT_OPTIONS_DROPDOWN_ID } from '@/object-record/object-options-dropd
import { useObjectOptionsDropdown } from '@/object-record/object-options-dropdown/hooks/useObjectOptionsDropdown';
import { useObjectOptionsForBoard } from '@/object-record/object-options-dropdown/hooks/useObjectOptionsForBoard';
import { recordGroupFieldMetadataComponentState } from '@/object-record/record-group/states/recordGroupFieldMetadataComponentState';
import { recordIndexCalendarLayoutState } from '@/object-record/record-index/states/recordIndexCalendarLayoutState';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
@@ -19,15 +20,19 @@ import { useDeleteViewFromCurrentState } from '@/views/view-picker/hooks/useDele
import { viewPickerReferenceViewIdComponentState } from '@/views/view-picker/states/viewPickerReferenceViewIdComponentState';
import { useTheme } from '@emotion/react';
import { useLingui } from '@lingui/react/macro';
import { useRecoilValue } from 'recoil';
import { capitalize, isDefined } from 'twenty-shared/utils';
import {
AppTooltip,
IconCalendar,
IconCalendarWeek,
IconCopy,
IconLayoutList,
IconListDetails,
IconTrash,
} from 'twenty-ui/display';
import { MenuItem } from 'twenty-ui/navigation';
import { ViewCalendarLayout } from '~/generated/graphql';
interface ObjectOptionsDropdownCustomViewProps {
onBackToDefault?: () => void;
@@ -62,6 +67,10 @@ export const ObjectOptionsDropdownCustomView = ({
const isDefaultView = currentView?.key === ViewKey.Index;
const recordIndexCalendarLayout = useRecoilValue(
recordIndexCalendarLayoutState,
);
const { visibleBoardFields } = useObjectOptionsForBoard({
objectNameSingular: objectMetadataItem.nameSingular,
recordBoardId: recordIndexId,
@@ -90,7 +99,7 @@ export const ObjectOptionsDropdownCustomView = ({
'Layout',
...(customViewData?.type !== ViewType.Calendar ? ['Fields'] : []),
...(customViewData?.type === ViewType.Calendar
? ['CalendarDateField']
? ['CalendarDateField', 'CalendarView']
: []),
...(customViewData?.type !== ViewType.Calendar ? ['Group'] : []),
'Copy link to view',
@@ -134,72 +143,92 @@ export const ObjectOptionsDropdownCustomView = ({
</DropdownMenuItemsContainer>
<DropdownMenuSeparator />
<DropdownMenuItemsContainer scrollable={false}>
{customViewData?.type !== ViewType.Calendar && (
<SelectableListItem
itemId="Fields"
onEnter={() => onContentChange('fields')}
>
<MenuItem
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">
<>
<div id="calendar-date-field-picker-menu-item">
<SelectableListItem
itemId="CalendarDateField"
onEnter={() => onContentChange('calendarFields')}
>
<MenuItem
focused={selectedItemId === 'CalendarDateField'}
onClick={() => onContentChange('calendarFields')}
LeftIcon={IconCalendar}
text={t`Date field`}
contextualText={
isDefaultView
? t`Not available on Default View`
: calendarFieldMetadata?.label
}
hasSubMenu
disabled={isDefaultView}
/>
</SelectableListItem>
</div>
<SelectableListItem
itemId="CalendarDateField"
onEnter={() => onContentChange('calendarFields')}
itemId="CalendarView"
onEnter={() => onContentChange('calendarView')}
>
<MenuItem
focused={selectedItemId === 'CalendarDateField'}
onClick={() => onContentChange('calendarFields')}
LeftIcon={IconLayoutList}
text={t`Date field`}
focused={selectedItemId === 'CalendarView'}
onClick={() => onContentChange('calendarView')}
LeftIcon={IconCalendarWeek}
text={t`Calendar view`}
contextualText={
isDefaultView
? t`Not available on Default View`
: calendarFieldMetadata?.label
recordIndexCalendarLayout === ViewCalendarLayout.MONTH
? t`Month`
: recordIndexCalendarLayout === ViewCalendarLayout.WEEK
? t`Week`
: t`Day`
}
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')
}
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>
<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
@@ -3,6 +3,7 @@ import { useObjectOptionsDropdown } from '@/object-record/object-options-dropdow
import { useObjectOptionsForBoard } from '@/object-record/object-options-dropdown/hooks/useObjectOptionsForBoard';
import { useSetViewTypeFromLayoutOptionsMenu } from '@/object-record/object-options-dropdown/hooks/useSetViewTypeFromLayoutOptionsMenu';
import { recordGroupFieldMetadataComponentState } from '@/object-record/record-group/states/recordGroupFieldMetadataComponentState';
import { recordIndexCalendarLayoutState } from '@/object-record/record-index/states/recordIndexCalendarLayoutState';
import { recordIndexOpenRecordInState } from '@/object-record/record-index/states/recordIndexOpenRecordInState';
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader/DropdownMenuHeader';
@@ -25,6 +26,8 @@ import { useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
import {
IconBaselineDensitySmall,
IconCalendar,
IconCalendarWeek,
IconChevronLeft,
IconLayoutList,
IconLayoutNavbar,
@@ -33,7 +36,7 @@ import {
OverflowingTextWithTooltip,
} from 'twenty-ui/display';
import { MenuItem, MenuItemSelect, MenuItemToggle } from 'twenty-ui/navigation';
import { FeatureFlagKey } from '~/generated/graphql';
import { FeatureFlagKey, ViewCalendarLayout } from '~/generated/graphql';
export const ObjectOptionsDropdownLayoutContent = () => {
const { t } = useLingui();
@@ -55,7 +58,9 @@ export const ObjectOptionsDropdownLayoutContent = () => {
});
const recordIndexOpenRecordIn = useRecoilValue(recordIndexOpenRecordInState);
const recordIndexCalendarLayout = useRecoilValue(
recordIndexCalendarLayoutState,
);
const recordGroupFieldMetadata = useRecoilComponentValue(
recordGroupFieldMetadataComponentState,
);
@@ -112,7 +117,9 @@ export const ObjectOptionsDropdownLayoutContent = () => {
...(!isDefaultView && hasCalendarViewEnabled ? [ViewType.Calendar] : []),
ViewOpenRecordInType.SIDE_PANEL,
...(currentView?.type === ViewType.Kanban ? ['Group', 'Compact view'] : []),
...(currentView?.type === ViewType.Calendar ? ['CalendarDateField'] : []),
...(currentView?.type === ViewType.Calendar
? ['CalendarView', 'CalendarDateField']
: []),
];
const selectedItemId = useRecoilComponentValue(
@@ -204,6 +211,42 @@ export const ObjectOptionsDropdownLayoutContent = () => {
</DropdownMenuItemsContainer>
<DropdownMenuSeparator />
<DropdownMenuItemsContainer scrollable={false}>
{currentView?.type === ViewType.Calendar && (
<>
<SelectableListItem
itemId="CalendarDateField"
onEnter={() => onContentChange('calendarFields')}
>
<MenuItem
focused={selectedItemId === 'CalendarDateField'}
onClick={() => onContentChange('calendarFields')}
LeftIcon={IconCalendar}
text={t`Date field`}
contextualText={calendarFieldMetadata?.label}
hasSubMenu
/>
</SelectableListItem>
<SelectableListItem
itemId="CalendarView"
onEnter={() => onContentChange('calendarView')}
>
<MenuItem
focused={selectedItemId === 'CalendarView'}
onClick={() => onContentChange('calendarView')}
LeftIcon={IconCalendarWeek}
text={t`Calendar view`}
contextualText={
recordIndexCalendarLayout === ViewCalendarLayout.MONTH
? t`Month`
: recordIndexCalendarLayout === ViewCalendarLayout.WEEK
? t`Week`
: t`Day`
}
hasSubMenu
/>
</SelectableListItem>
</>
)}
<SelectableListItem
itemId={ViewOpenRecordInType.SIDE_PANEL}
onEnter={() => {
@@ -278,21 +321,6 @@ 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>
)}
@@ -7,4 +7,5 @@ export type ObjectOptionsContentId =
| 'hiddenRecordGroups'
| 'recordGroupFields'
| 'recordGroupSort'
| 'calendarFields';
| 'calendarFields'
| 'calendarView';
@@ -0,0 +1,7 @@
import { createState } from 'twenty-ui/utilities';
import { ViewCalendarLayout } from '~/generated-metadata/graphql';
export const recordIndexCalendarLayoutState = createState<ViewCalendarLayout>({
key: 'recordIndexCalendarLayoutState',
defaultValue: ViewCalendarLayout.MONTH,
});
@@ -44,6 +44,8 @@ export {
IconCalendarRepeat,
IconCalendarTime,
IconCalendarX,
IconCalendarMonth,
IconCalendarWeek,
IconChartBar,
IconChartCandle,
IconChartDots3,
+2
View File
@@ -106,6 +106,8 @@ export {
IconCalendarRepeat,
IconCalendarTime,
IconCalendarX,
IconCalendarMonth,
IconCalendarWeek,
IconChartBar,
IconChartCandle,
IconChartDots3,