Add weekly layout to record calendar (#22819)
## Summary - Add a week layout to record calendar views and persist the selected layout. - Render `DATE` calendars as an all-day week and `DATE_TIME` calendars as an hourly week. - Add an optional end date field across calendar configuration, metadata, persistence, and complete-view upserts. - Use configured end values for ranged and multi-day events, with a one-hour fallback when a `DATE_TIME` end is absent or invalid. - Keep calendar cards consistent with the existing compact view, including checkbox selection and whole-card record opening. - Gate the weekly layout and end-date behavior behind the public Labs `IS_CALENDAR_WEEK_VIEW_ENABLED` workspace feature flag. ## Week interactions - Show overlapping timed events side by side and cap the visible records at two per day. - Display start and end times on timed cards, enforce a readable 30-minute minimum height, and keep today’s text contrast stronger. - Drag timed events between days and times with 30-minute snapping while preserving their duration, including zero-duration events. - Show a create button when hovering a 30-minute slot; keyboard users can focus a day, move the slot with the arrow keys, and reach the same contextual action. - Initialize new records with the selected slot time and a compatible writable end value one hour later. - Show the workspace time zone and current-time indicator in timed weeks; date-only weeks keep the all-day section without an hourly grid. ## Configuration and data loading - Only allow end fields that match the start field type, and prevent selecting the same field for both boundaries. - Load records whose ranges overlap the visible period so month and week layouts display the same relevant records. - Resolve and persist calendar end fields when updating existing views through `upsert_complete_view`. - Fall back to Month and ignore the configured end field while the flag is disabled, without overwriting either persisted setting, so re-enabling restores the previous configuration. - Expose the flag in Labs and keep it default-off for workspaces without a stored value; enable it in the development seeder. <img width="1285" height="808" alt="Screenshot 2026-07-15 at 15 50 17" src="https://github.com/user-attachments/assets/b7e3f7f1-ca77-492f-8cce-cca186ebca0b" />
This commit is contained in:
+109
@@ -0,0 +1,109 @@
|
||||
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { useObjectOptionsDropdown } from '@/object-record/object-options-dropdown/hooks/useObjectOptionsDropdown';
|
||||
import { recordIndexCalendarEndFieldMetadataIdState } from '@/object-record/record-index/states/recordIndexCalendarEndFieldMetadataIdState';
|
||||
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 { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
|
||||
import { useGetCurrentViewOnly } from '@/views/hooks/useGetCurrentViewOnly';
|
||||
import { useUpdateCurrentView } from '@/views/hooks/useUpdateCurrentView';
|
||||
import { useGetAvailableFieldsForCalendar } from '@/views/view-picker/hooks/useGetAvailableFieldsForCalendar';
|
||||
import { getAvailableCalendarEndFieldMetadataItems } from '@/views/view-picker/utils/getAvailableCalendarEndFieldMetadataItems';
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { useState } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { IconChevronLeft, useIcons } from 'twenty-ui/icon';
|
||||
import { MenuItemSelect } from 'twenty-ui/navigation';
|
||||
import { FeatureFlagKey } from '~/generated-metadata/graphql';
|
||||
|
||||
export const ObjectOptionsDropdownCalendarEndFieldsContent = () => {
|
||||
const { t } = useLingui();
|
||||
const { getIcon } = useIcons();
|
||||
const [searchInput, setSearchInput] = useState('');
|
||||
const isCalendarWeekViewEnabled = useIsFeatureEnabled(
|
||||
FeatureFlagKey.IS_CALENDAR_WEEK_VIEW_ENABLED,
|
||||
);
|
||||
|
||||
const { resetContent, closeDropdown } = useObjectOptionsDropdown();
|
||||
|
||||
const { currentView } = useGetCurrentViewOnly();
|
||||
const { updateCurrentView } = useUpdateCurrentView();
|
||||
const { availableFieldsForCalendar } = useGetAvailableFieldsForCalendar();
|
||||
|
||||
const setRecordIndexCalendarEndFieldMetadataId = useSetAtomState(
|
||||
recordIndexCalendarEndFieldMetadataIdState,
|
||||
);
|
||||
|
||||
const availableCalendarEndFieldMetadataItems =
|
||||
getAvailableCalendarEndFieldMetadataItems({
|
||||
availableFieldsForCalendar,
|
||||
calendarFieldMetadataId: currentView?.calendarFieldMetadataId,
|
||||
});
|
||||
|
||||
const filteredCalendarEndFields =
|
||||
availableCalendarEndFieldMetadataItems.filter((field) =>
|
||||
field.label.toLowerCase().includes(searchInput.toLowerCase()),
|
||||
);
|
||||
|
||||
const handleCalendarEndFieldChange = async (
|
||||
fieldMetadataItem: FieldMetadataItem | null,
|
||||
) => {
|
||||
if (!isCalendarWeekViewEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const calendarEndFieldMetadataId = fieldMetadataItem?.id ?? null;
|
||||
|
||||
setRecordIndexCalendarEndFieldMetadataId(calendarEndFieldMetadataId);
|
||||
await updateCurrentView({ calendarEndFieldMetadataId });
|
||||
closeDropdown();
|
||||
};
|
||||
|
||||
if (!isCalendarWeekViewEnabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<DropdownContent>
|
||||
<DropdownMenuHeader
|
||||
StartComponent={
|
||||
<DropdownMenuHeaderLeftComponent
|
||||
onClick={() => resetContent()}
|
||||
Icon={IconChevronLeft}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{t`End date field`}
|
||||
</DropdownMenuHeader>
|
||||
<DropdownMenuSearchInput
|
||||
autoFocus
|
||||
value={searchInput}
|
||||
placeholder={t`Search fields`}
|
||||
onChange={(event) => setSearchInput(event.target.value)}
|
||||
/>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItemsContainer>
|
||||
<MenuItemSelect
|
||||
selected={!isDefined(currentView?.calendarEndFieldMetadataId)}
|
||||
onClick={() => handleCalendarEndFieldChange(null)}
|
||||
text={t`None`}
|
||||
/>
|
||||
{filteredCalendarEndFields.map((fieldMetadataItem) => (
|
||||
<MenuItemSelect
|
||||
key={fieldMetadataItem.id}
|
||||
selected={
|
||||
fieldMetadataItem.id === currentView?.calendarEndFieldMetadataId
|
||||
}
|
||||
onClick={() => handleCalendarEndFieldChange(fieldMetadataItem)}
|
||||
LeftIcon={getIcon(fieldMetadataItem.icon)}
|
||||
text={fieldMetadataItem.label}
|
||||
/>
|
||||
))}
|
||||
</DropdownMenuItemsContainer>
|
||||
</DropdownContent>
|
||||
);
|
||||
};
|
||||
+24
@@ -1,5 +1,6 @@
|
||||
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { useObjectOptionsDropdown } from '@/object-record/object-options-dropdown/hooks/useObjectOptionsDropdown';
|
||||
import { recordIndexCalendarEndFieldMetadataIdState } from '@/object-record/record-index/states/recordIndexCalendarEndFieldMetadataIdState';
|
||||
import { recordIndexCalendarFieldMetadataIdState } from '@/object-record/record-index/states/recordIndexCalendarFieldMetadataIdState';
|
||||
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
|
||||
import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader/DropdownMenuHeader';
|
||||
@@ -13,6 +14,7 @@ import { useGetAvailableFieldsForCalendar } from '@/views/view-picker/hooks/useG
|
||||
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { useState } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { IconChevronLeft, IconSettings, useIcons } from 'twenty-ui/icon';
|
||||
import { MenuItem, MenuItemSelect } from 'twenty-ui/navigation';
|
||||
|
||||
@@ -32,6 +34,9 @@ export const ObjectOptionsDropdownCalendarFieldsContent = () => {
|
||||
const setRecordIndexCalendarFieldMetadataId = useSetAtomState(
|
||||
recordIndexCalendarFieldMetadataIdState,
|
||||
);
|
||||
const setRecordIndexCalendarEndFieldMetadataId = useSetAtomState(
|
||||
recordIndexCalendarEndFieldMetadataIdState,
|
||||
);
|
||||
|
||||
const calendarFieldMetadata = currentView?.calendarFieldMetadataId
|
||||
? objectMetadataItem.fields.find(
|
||||
@@ -39,6 +44,12 @@ export const ObjectOptionsDropdownCalendarFieldsContent = () => {
|
||||
)
|
||||
: undefined;
|
||||
|
||||
const calendarEndFieldMetadata = currentView?.calendarEndFieldMetadataId
|
||||
? objectMetadataItem.fields.find(
|
||||
(field) => field.id === currentView.calendarEndFieldMetadataId,
|
||||
)
|
||||
: undefined;
|
||||
|
||||
const filteredCalendarFields = availableFieldsForCalendar.filter((field) =>
|
||||
field.label.toLowerCase().includes(searchInput.toLowerCase()),
|
||||
);
|
||||
@@ -46,9 +57,22 @@ export const ObjectOptionsDropdownCalendarFieldsContent = () => {
|
||||
const handleCalendarFieldChange = async (
|
||||
fieldMetadataItem: FieldMetadataItem,
|
||||
) => {
|
||||
const shouldClearCalendarEndField =
|
||||
isDefined(currentView?.calendarEndFieldMetadataId) &&
|
||||
(!isDefined(calendarEndFieldMetadata) ||
|
||||
calendarEndFieldMetadata.id === fieldMetadataItem.id ||
|
||||
calendarEndFieldMetadata.type !== fieldMetadataItem.type);
|
||||
|
||||
setRecordIndexCalendarFieldMetadataId(fieldMetadataItem.id);
|
||||
if (shouldClearCalendarEndField) {
|
||||
setRecordIndexCalendarEndFieldMetadataId(null);
|
||||
}
|
||||
|
||||
await updateCurrentView({
|
||||
calendarFieldMetadataId: fieldMetadataItem.id,
|
||||
...(shouldClearCalendarEndField
|
||||
? { calendarEndFieldMetadataId: null }
|
||||
: {}),
|
||||
});
|
||||
closeDropdown();
|
||||
};
|
||||
|
||||
+39
-6
@@ -1,5 +1,6 @@
|
||||
import { OBJECT_OPTIONS_DROPDOWN_ID } from '@/object-record/object-options-dropdown/constants/ObjectOptionsDropdownId';
|
||||
import { useObjectOptionsDropdown } from '@/object-record/object-options-dropdown/hooks/useObjectOptionsDropdown';
|
||||
import { getSupportedRecordCalendarLayout } from '@/object-record/record-calendar/utils/getSupportedRecordCalendarLayout';
|
||||
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';
|
||||
@@ -12,6 +13,7 @@ import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/use
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
|
||||
import { useUpdateCurrentView } from '@/views/hooks/useUpdateCurrentView';
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { Pill } from 'twenty-ui/data-display';
|
||||
import {
|
||||
@@ -21,13 +23,23 @@ import {
|
||||
IconTimelineEvent,
|
||||
} from 'twenty-ui/icon';
|
||||
import { MenuItemSelect } from 'twenty-ui/navigation';
|
||||
import { ViewCalendarLayout } from '~/generated-metadata/graphql';
|
||||
import {
|
||||
FeatureFlagKey,
|
||||
ViewCalendarLayout,
|
||||
} from '~/generated-metadata/graphql';
|
||||
|
||||
export const ObjectOptionsDropdownCalendarViewContent = () => {
|
||||
const { resetContent } = useObjectOptionsDropdown();
|
||||
const recordIndexCalendarLayout = useAtomStateValue(
|
||||
recordIndexCalendarLayoutState,
|
||||
);
|
||||
const isCalendarWeekViewEnabled = useIsFeatureEnabled(
|
||||
FeatureFlagKey.IS_CALENDAR_WEEK_VIEW_ENABLED,
|
||||
);
|
||||
const supportedCalendarLayout = getSupportedRecordCalendarLayout({
|
||||
calendarLayout: recordIndexCalendarLayout,
|
||||
isCalendarWeekViewEnabled,
|
||||
});
|
||||
const setRecordIndexCalendarLayout = useSetAtomState(
|
||||
recordIndexCalendarLayoutState,
|
||||
);
|
||||
@@ -47,6 +59,18 @@ export const ObjectOptionsDropdownCalendarViewContent = () => {
|
||||
];
|
||||
|
||||
const handleCalendarViewChange = async (calendarView: ViewCalendarLayout) => {
|
||||
if (
|
||||
calendarView === ViewCalendarLayout.WEEK &&
|
||||
!isCalendarWeekViewEnabled
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (calendarView === supportedCalendarLayout) {
|
||||
closeDropdown();
|
||||
return;
|
||||
}
|
||||
|
||||
setRecordIndexCalendarLayout(calendarView);
|
||||
await updateCurrentView({
|
||||
calendarLayout: calendarView,
|
||||
@@ -75,17 +99,26 @@ export const ObjectOptionsDropdownCalendarViewContent = () => {
|
||||
<SelectableListItem
|
||||
itemId={ViewCalendarLayout.WEEK}
|
||||
onEnter={() => {
|
||||
handleCalendarViewChange(ViewCalendarLayout.WEEK);
|
||||
if (isCalendarWeekViewEnabled) {
|
||||
handleCalendarViewChange(ViewCalendarLayout.WEEK);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<MenuItemSelect
|
||||
LeftIcon={IconCalendarWeek}
|
||||
text={t`Week`}
|
||||
selected={recordIndexCalendarLayout === ViewCalendarLayout.WEEK}
|
||||
selected={supportedCalendarLayout === ViewCalendarLayout.WEEK}
|
||||
onClick={
|
||||
isCalendarWeekViewEnabled
|
||||
? () => handleCalendarViewChange(ViewCalendarLayout.WEEK)
|
||||
: undefined
|
||||
}
|
||||
focused={selectedItemId === ViewCalendarLayout.WEEK}
|
||||
contextualText={<Pill label={t`Soon`} />}
|
||||
contextualText={
|
||||
isCalendarWeekViewEnabled ? undefined : <Pill label={t`Soon`} />
|
||||
}
|
||||
contextualTextPosition="right"
|
||||
disabled
|
||||
disabled={!isCalendarWeekViewEnabled}
|
||||
/>
|
||||
</SelectableListItem>
|
||||
<SelectableListItem
|
||||
@@ -95,7 +128,7 @@ export const ObjectOptionsDropdownCalendarViewContent = () => {
|
||||
<MenuItemSelect
|
||||
LeftIcon={IconCalendarMonth}
|
||||
text={t`Month`}
|
||||
selected={recordIndexCalendarLayout === ViewCalendarLayout.MONTH}
|
||||
selected={supportedCalendarLayout === ViewCalendarLayout.MONTH}
|
||||
onClick={() => handleCalendarViewChange(ViewCalendarLayout.MONTH)}
|
||||
focused={selectedItemId === ViewCalendarLayout.MONTH}
|
||||
/>
|
||||
|
||||
+12
@@ -1,5 +1,6 @@
|
||||
import { ObjectOptionsDropdownAddRecordGroupContent } from '@/object-record/object-options-dropdown/components/ObjectOptionsDropdownAddRecordGroupContent';
|
||||
import { ObjectOptionsDropdownCalendarFieldsContent } from '@/object-record/object-options-dropdown/components/ObjectOptionsDropdownCalendarFieldsContent';
|
||||
import { ObjectOptionsDropdownCalendarEndFieldsContent } from '@/object-record/object-options-dropdown/components/ObjectOptionsDropdownCalendarEndFieldsContent';
|
||||
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';
|
||||
@@ -12,9 +13,14 @@ import { ObjectOptionsDropdownRecordGroupsContent } from '@/object-record/object
|
||||
import { ObjectOptionsDropdownRecordGroupSortContent } from '@/object-record/object-options-dropdown/components/ObjectOptionsDropdownRecordGroupSortContent';
|
||||
import { ObjectOptionsDropdownVisibilityContent } from '@/object-record/object-options-dropdown/components/ObjectOptionsDropdownVisibilityContent';
|
||||
import { useObjectOptionsDropdown } from '@/object-record/object-options-dropdown/hooks/useObjectOptionsDropdown';
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
import { FeatureFlagKey } from '~/generated-metadata/graphql';
|
||||
|
||||
export const ObjectOptionsDropdownContent = () => {
|
||||
const { currentContentId } = useObjectOptionsDropdown();
|
||||
const isCalendarWeekViewEnabled = useIsFeatureEnabled(
|
||||
FeatureFlagKey.IS_CALENDAR_WEEK_VIEW_ENABLED,
|
||||
);
|
||||
|
||||
switch (currentContentId) {
|
||||
case 'layout':
|
||||
@@ -39,6 +45,12 @@ export const ObjectOptionsDropdownContent = () => {
|
||||
return <ObjectOptionsDropdownCalendarViewContent />;
|
||||
case 'calendarFields':
|
||||
return <ObjectOptionsDropdownCalendarFieldsContent />;
|
||||
case 'calendarEndFields':
|
||||
return isCalendarWeekViewEnabled ? (
|
||||
<ObjectOptionsDropdownCalendarEndFieldsContent />
|
||||
) : (
|
||||
<ObjectOptionsDropdownMenuContent />
|
||||
);
|
||||
case 'visibility':
|
||||
return <ObjectOptionsDropdownVisibilityContent />;
|
||||
default:
|
||||
|
||||
+49
-4
@@ -2,6 +2,7 @@ import { ObjectOptionsDropdownMenuViewName } from '@/object-record/object-option
|
||||
import { OBJECT_OPTIONS_DROPDOWN_ID } from '@/object-record/object-options-dropdown/constants/ObjectOptionsDropdownId';
|
||||
import { useObjectOptionsDropdown } from '@/object-record/object-options-dropdown/hooks/useObjectOptionsDropdown';
|
||||
import { useObjectOptionsForBoard } from '@/object-record/object-options-dropdown/hooks/useObjectOptionsForBoard';
|
||||
import { getSupportedRecordCalendarLayout } from '@/object-record/record-calendar/utils/getSupportedRecordCalendarLayout';
|
||||
import { recordIndexCalendarLayoutState } from '@/object-record/record-index/states/recordIndexCalendarLayoutState';
|
||||
import { recordIndexGroupFieldMetadataItemComponentState } from '@/object-record/record-index/states/recordIndexGroupFieldMetadataComponentState';
|
||||
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
|
||||
@@ -22,6 +23,7 @@ import {
|
||||
viewTypeIconMapping,
|
||||
} from '@/views/types/ViewType';
|
||||
import { useDestroyViewFromCurrentState } from '@/views/view-picker/hooks/useDestroyViewFromCurrentState';
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
import { viewPickerReferenceViewIdComponentState } from '@/views/view-picker/states/viewPickerReferenceViewIdComponentState';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { useAtomFamilySelectorValue } from '@/ui/utilities/state/jotai/hooks/useAtomFamilySelectorValue';
|
||||
@@ -37,7 +39,10 @@ import {
|
||||
} from 'twenty-ui/icon';
|
||||
import { AppTooltip } from 'twenty-ui/surfaces';
|
||||
import { MenuItem } from 'twenty-ui/navigation';
|
||||
import { ViewCalendarLayout } from '~/generated-metadata/graphql';
|
||||
import {
|
||||
FeatureFlagKey,
|
||||
ViewCalendarLayout,
|
||||
} from '~/generated-metadata/graphql';
|
||||
|
||||
interface ObjectOptionsDropdownCustomViewProps {
|
||||
onBackToDefault?: () => void;
|
||||
@@ -70,6 +75,12 @@ export const ObjectOptionsDropdownCustomView = ({
|
||||
)
|
||||
: undefined;
|
||||
|
||||
const calendarEndFieldMetadata = currentView?.calendarEndFieldMetadataId
|
||||
? objectMetadataItem.fields.find(
|
||||
(field) => field.id === currentView.calendarEndFieldMetadataId,
|
||||
)
|
||||
: undefined;
|
||||
|
||||
const viewsOnCurrentObject = useAtomFamilySelectorValue(
|
||||
viewsFromObjectMetadataItemFamilySelector,
|
||||
{ objectMetadataItemId: objectMetadataItem.id },
|
||||
@@ -81,6 +92,13 @@ export const ObjectOptionsDropdownCustomView = ({
|
||||
const recordIndexCalendarLayout = useAtomStateValue(
|
||||
recordIndexCalendarLayoutState,
|
||||
);
|
||||
const isCalendarWeekViewEnabled = useIsFeatureEnabled(
|
||||
FeatureFlagKey.IS_CALENDAR_WEEK_VIEW_ENABLED,
|
||||
);
|
||||
const supportedCalendarLayout = getSupportedRecordCalendarLayout({
|
||||
calendarLayout: recordIndexCalendarLayout,
|
||||
isCalendarWeekViewEnabled,
|
||||
});
|
||||
|
||||
const { visibleBoardFields } = useObjectOptionsForBoard({
|
||||
objectNameSingular: objectMetadataItem.nameSingular,
|
||||
@@ -111,7 +129,11 @@ export const ObjectOptionsDropdownCustomView = ({
|
||||
'Visibility',
|
||||
'Fields',
|
||||
...(customViewData?.type === ViewType.CALENDAR
|
||||
? ['CalendarDateField', 'CalendarView']
|
||||
? [
|
||||
'CalendarDateField',
|
||||
...(isCalendarWeekViewEnabled ? ['CalendarEndDateField'] : []),
|
||||
'CalendarView',
|
||||
]
|
||||
: []),
|
||||
...(customViewData?.type !== ViewType.CALENDAR ? ['Group'] : []),
|
||||
'Delete view',
|
||||
@@ -196,6 +218,29 @@ export const ObjectOptionsDropdownCustomView = ({
|
||||
/>
|
||||
</SelectableListItem>
|
||||
</div>
|
||||
{isCalendarWeekViewEnabled && (
|
||||
<div id="calendar-end-date-field-picker-menu-item">
|
||||
<SelectableListItem
|
||||
itemId="CalendarEndDateField"
|
||||
onEnter={() => onContentChange('calendarEndFields')}
|
||||
>
|
||||
<MenuItem
|
||||
focused={selectedItemId === 'CalendarEndDateField'}
|
||||
onClick={() => onContentChange('calendarEndFields')}
|
||||
LeftIcon={IconCalendar}
|
||||
text={t`End date field`}
|
||||
contextualText={
|
||||
isDefaultView
|
||||
? t`Not available on Default View`
|
||||
: (calendarEndFieldMetadata?.label ?? t`None`)
|
||||
}
|
||||
contextualTextPosition="right"
|
||||
hasSubMenu
|
||||
disabled={isDefaultView}
|
||||
/>
|
||||
</SelectableListItem>
|
||||
</div>
|
||||
)}
|
||||
<SelectableListItem
|
||||
itemId="CalendarView"
|
||||
onEnter={() => onContentChange('calendarView')}
|
||||
@@ -206,9 +251,9 @@ export const ObjectOptionsDropdownCustomView = ({
|
||||
LeftIcon={IconCalendarWeek}
|
||||
text={t`Calendar view`}
|
||||
contextualText={
|
||||
recordIndexCalendarLayout === ViewCalendarLayout.MONTH
|
||||
supportedCalendarLayout === ViewCalendarLayout.MONTH
|
||||
? t`Month`
|
||||
: recordIndexCalendarLayout === ViewCalendarLayout.WEEK
|
||||
: supportedCalendarLayout === ViewCalendarLayout.WEEK
|
||||
? t`Week`
|
||||
: t`Day`
|
||||
}
|
||||
|
||||
+41
-3
@@ -1,6 +1,7 @@
|
||||
import { OBJECT_OPTIONS_DROPDOWN_ID } from '@/object-record/object-options-dropdown/constants/ObjectOptionsDropdownId';
|
||||
import { useObjectOptionsDropdown } from '@/object-record/object-options-dropdown/hooks/useObjectOptionsDropdown';
|
||||
import { useSetViewTypeFromLayoutOptionsMenu } from '@/object-record/object-options-dropdown/hooks/useSetViewTypeFromLayoutOptionsMenu';
|
||||
import { getSupportedRecordCalendarLayout } from '@/object-record/record-calendar/utils/getSupportedRecordCalendarLayout';
|
||||
import { recordIndexCalendarLayoutState } from '@/object-record/record-index/states/recordIndexCalendarLayoutState';
|
||||
import { recordIndexGroupFieldMetadataItemComponentState } from '@/object-record/record-index/states/recordIndexGroupFieldMetadataComponentState';
|
||||
import { recordIndexOpenRecordInState } from '@/object-record/record-index/states/recordIndexOpenRecordInState';
|
||||
@@ -24,6 +25,7 @@ import {
|
||||
} from '@/views/types/ViewType';
|
||||
import { useGetAvailableFieldsForCalendar } from '@/views/view-picker/hooks/useGetAvailableFieldsForCalendar';
|
||||
import { useGetAvailableFieldsToGroupRecordsBy } from '@/views/view-picker/hooks/useGetAvailableFieldsToGroupRecordsBy';
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { useCallback } from 'react';
|
||||
@@ -41,6 +43,7 @@ import {
|
||||
import { OverflowingTextWithTooltip } from 'twenty-ui/surfaces';
|
||||
import { MenuItem, MenuItemSelect, MenuItemToggle } from 'twenty-ui/navigation';
|
||||
import {
|
||||
FeatureFlagKey,
|
||||
ViewCalendarLayout,
|
||||
ViewOpenRecordIn,
|
||||
} from '~/generated-metadata/graphql';
|
||||
@@ -72,6 +75,13 @@ export const ObjectOptionsDropdownLayoutContent = () => {
|
||||
const recordIndexCalendarLayout = useAtomStateValue(
|
||||
recordIndexCalendarLayoutState,
|
||||
);
|
||||
const isCalendarWeekViewEnabled = useIsFeatureEnabled(
|
||||
FeatureFlagKey.IS_CALENDAR_WEEK_VIEW_ENABLED,
|
||||
);
|
||||
const supportedCalendarLayout = getSupportedRecordCalendarLayout({
|
||||
calendarLayout: recordIndexCalendarLayout,
|
||||
isCalendarWeekViewEnabled,
|
||||
});
|
||||
const recordIndexGroupFieldMetadataItem = useAtomComponentStateValue(
|
||||
recordIndexGroupFieldMetadataItemComponentState,
|
||||
);
|
||||
@@ -82,6 +92,12 @@ export const ObjectOptionsDropdownLayoutContent = () => {
|
||||
)
|
||||
: undefined;
|
||||
|
||||
const calendarEndFieldMetadata = currentView?.calendarEndFieldMetadataId
|
||||
? objectMetadataItem.fields.find(
|
||||
(field) => field.id === currentView.calendarEndFieldMetadataId,
|
||||
)
|
||||
: undefined;
|
||||
|
||||
const { setAndPersistViewType } = useSetViewTypeFromLayoutOptionsMenu();
|
||||
const { availableFieldsForGrouping, navigateToSelectSettings } =
|
||||
useGetAvailableFieldsToGroupRecordsBy();
|
||||
@@ -127,7 +143,11 @@ export const ObjectOptionsDropdownLayoutContent = () => {
|
||||
ViewOpenRecordIn.SIDE_PANEL,
|
||||
...(currentView?.type === ViewType.KANBAN ? ['Group'] : []),
|
||||
...(currentView?.type === ViewType.CALENDAR
|
||||
? ['CalendarView', 'CalendarDateField']
|
||||
? [
|
||||
'CalendarView',
|
||||
'CalendarDateField',
|
||||
...(isCalendarWeekViewEnabled ? ['CalendarEndDateField'] : []),
|
||||
]
|
||||
: []),
|
||||
...(currentView?.type !== ViewType.TABLE ? ['Compact view'] : []),
|
||||
];
|
||||
@@ -236,6 +256,24 @@ export const ObjectOptionsDropdownLayoutContent = () => {
|
||||
hasSubMenu
|
||||
/>
|
||||
</SelectableListItem>
|
||||
{isCalendarWeekViewEnabled && (
|
||||
<SelectableListItem
|
||||
itemId="CalendarEndDateField"
|
||||
onEnter={() => onContentChange('calendarEndFields')}
|
||||
>
|
||||
<MenuItem
|
||||
focused={selectedItemId === 'CalendarEndDateField'}
|
||||
onClick={() => onContentChange('calendarEndFields')}
|
||||
LeftIcon={IconCalendar}
|
||||
text={t`End date field`}
|
||||
contextualText={
|
||||
calendarEndFieldMetadata?.label ?? t`None`
|
||||
}
|
||||
contextualTextPosition="right"
|
||||
hasSubMenu
|
||||
/>
|
||||
</SelectableListItem>
|
||||
)}
|
||||
<SelectableListItem
|
||||
itemId="CalendarView"
|
||||
onEnter={() => onContentChange('calendarView')}
|
||||
@@ -246,9 +284,9 @@ export const ObjectOptionsDropdownLayoutContent = () => {
|
||||
LeftIcon={IconCalendarWeek}
|
||||
text={t`Calendar view`}
|
||||
contextualText={
|
||||
recordIndexCalendarLayout === ViewCalendarLayout.MONTH
|
||||
supportedCalendarLayout === ViewCalendarLayout.MONTH
|
||||
? t`Month`
|
||||
: recordIndexCalendarLayout === ViewCalendarLayout.WEEK
|
||||
: supportedCalendarLayout === ViewCalendarLayout.WEEK
|
||||
? t`Week`
|
||||
: t`Day`
|
||||
}
|
||||
|
||||
+2
@@ -97,6 +97,7 @@ export const useSetViewTypeFromLayoutOptionsMenu = () => {
|
||||
...currentView,
|
||||
type: viewType,
|
||||
calendarFieldMetadataId,
|
||||
calendarEndFieldMetadataId: null,
|
||||
calendarLayout: ViewCalendarLayout.MONTH,
|
||||
},
|
||||
objectMetadataItem,
|
||||
@@ -109,6 +110,7 @@ export const useSetViewTypeFromLayoutOptionsMenu = () => {
|
||||
updateCurrentViewParams.calendarLayout = ViewCalendarLayout.MONTH;
|
||||
updateCurrentViewParams.calendarFieldMetadataId =
|
||||
calendarFieldMetadataId;
|
||||
updateCurrentViewParams.calendarEndFieldMetadataId = null;
|
||||
updateCurrentViewParams.mainGroupByFieldMetadataId = null;
|
||||
return await updateCurrentView(updateCurrentViewParams);
|
||||
}
|
||||
|
||||
+1
@@ -9,5 +9,6 @@ export type ObjectOptionsContentId =
|
||||
| 'recordGroupSort'
|
||||
| 'addRecordGroup'
|
||||
| 'calendarFields'
|
||||
| 'calendarEndFields'
|
||||
| 'calendarView'
|
||||
| 'visibility';
|
||||
|
||||
Reference in New Issue
Block a user