Fix calendar field picker state handling (#23595)

## Context

Changing a calendar date field already updated the record-index calendar
state immediately, so the calendar moved to the new field before the
`updateView` mutation completed.

The options dropdown still derived its selected checkmark and field
labels from `currentView`, which remains unchanged while persistence is
pending. On slower environments this left the previous field name
visible even though the calendar was already using the new field.
Locally the same mismatch existed, but was only visible briefly because
the mutation completed faster.

## What changed

- Read the active start and end date field IDs from the record-index
calendar component state in the calendar options dropdown.
- Use that state for the main options label, the two-field submenu, and
both field-picker selections.
- Use the optimistic end-field state when filtering compatible start
fields and deciding whether an incompatible end field must be cleared.
- Keep the existing view mutation and calendar-state writes unchanged.

## Why

The calendar and its configuration UI now share the same source of truth
while persistence is pending. A field selection updates the calendar,
checkmark, and contextual labels together instead of temporarily mixing
optimistic calendar state with stale persisted view metadata.

## Safety and expected impact

This is frontend state synchronization only. It does not change the
metadata schema, API payloads, or persistence flow. Existing date and
datetime compatibility rules remain in place.

Users should see the selected field name update immediately, including
when the metadata mutation is slow.

## Limitations

This does not change mutation error handling or add rollback behavior.
The calendar atoms were already updated optimistically before this
change, this PR only makes the configuration UI reflect those same
values.

## Validation

- Reproduced the stale selection on qacoco and locally.
- Verified locally that the checkmark moves immediately after selecting
another date field, before the mutation closes the dropdown.
- `npx nx typecheck twenty-front`
- Focused type-aware oxlint on the four changed files, 0 warnings and 0
errors.
- `npx oxfmt --check` on the four changed files.
- `git diff --check`
This commit is contained in:
Weiko
2026-07-30 18:56:34 +02:00
committed by GitHub
parent b4102946f4
commit 0d63906a58
4 changed files with 67 additions and 31 deletions
@@ -1,5 +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 { recordIndexCalendarEndFieldMetadataIdComponentState } from '@/object-record/record-index/states/recordIndexCalendarEndFieldMetadataIdComponentState';
import { recordIndexCalendarFieldMetadataIdComponentState } from '@/object-record/record-index/states/recordIndexCalendarFieldMetadataIdComponentState';
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';
@@ -22,6 +24,12 @@ export const ObjectOptionsDropdownCalendarDateFieldsContent = () => {
useObjectOptionsDropdown();
const { currentView } = useGetCurrentViewOnly();
const recordIndexCalendarFieldMetadataId = useAtomComponentStateValue(
recordIndexCalendarFieldMetadataIdComponentState,
);
const recordIndexCalendarEndFieldMetadataId = useAtomComponentStateValue(
recordIndexCalendarEndFieldMetadataIdComponentState,
);
const selectedItemId = useAtomComponentStateValue(
selectedItemIdComponentState,
@@ -33,11 +41,11 @@ export const ObjectOptionsDropdownCalendarDateFieldsContent = () => {
}
const calendarFieldMetadata = objectMetadataItem.fields.find(
(field) => field.id === currentView.calendarFieldMetadataId,
(field) => field.id === recordIndexCalendarFieldMetadataId,
);
const calendarEndFieldMetadata = objectMetadataItem.fields.find(
(field) => field.id === currentView.calendarEndFieldMetadataId,
(field) => field.id === recordIndexCalendarEndFieldMetadataId,
);
const selectableItemIdArray = ['CalendarDateField', 'CalendarEndDateField'];
@@ -1,7 +1,7 @@
import { useSetAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useSetAtomComponentState';
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { useObjectOptionsDropdown } from '@/object-record/object-options-dropdown/hooks/useObjectOptionsDropdown';
import { recordIndexCalendarEndFieldMetadataIdComponentState } from '@/object-record/record-index/states/recordIndexCalendarEndFieldMetadataIdComponentState';
import { recordIndexCalendarFieldMetadataIdComponentState } from '@/object-record/record-index/states/recordIndexCalendarFieldMetadataIdComponentState';
import { groupCalendarFieldMetadataItemsByType } from '@/object-record/record-calendar/utils/groupCalendarFieldMetadataItemsByType';
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader/DropdownMenuHeader';
@@ -10,7 +10,8 @@ import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/Drop
import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput';
import { DropdownMenuSectionLabel } from '@/ui/layout/dropdown/components/DropdownMenuSectionLabel';
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
import { useGetCurrentViewOnly } from '@/views/hooks/useGetCurrentViewOnly';
import { useAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentState';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
import { useUpdateCurrentView } from '@/views/hooks/useUpdateCurrentView';
import { useGetAvailableFieldsForCalendar } from '@/views/view-picker/hooks/useGetAvailableFieldsForCalendar';
import { getAvailableCalendarEndFieldMetadataItems } from '@/views/view-picker/utils/getAvailableCalendarEndFieldMetadataItems';
@@ -32,18 +33,23 @@ export const ObjectOptionsDropdownCalendarEndFieldsContent = () => {
const { onContentChange, closeDropdown } = useObjectOptionsDropdown();
const { currentView } = useGetCurrentViewOnly();
const { updateCurrentView } = useUpdateCurrentView();
const { availableFieldsForCalendar } = useGetAvailableFieldsForCalendar();
const setRecordIndexCalendarEndFieldMetadataId = useSetAtomComponentState(
const recordIndexCalendarFieldMetadataId = useAtomComponentStateValue(
recordIndexCalendarFieldMetadataIdComponentState,
);
const [
recordIndexCalendarEndFieldMetadataId,
setRecordIndexCalendarEndFieldMetadataId,
] = useAtomComponentState(
recordIndexCalendarEndFieldMetadataIdComponentState,
);
const availableCalendarEndFieldMetadataItems =
getAvailableCalendarEndFieldMetadataItems({
availableFieldsForCalendar,
calendarFieldMetadataId: currentView?.calendarFieldMetadataId,
calendarFieldMetadataId: recordIndexCalendarFieldMetadataId,
});
const filteredCalendarEndFields =
@@ -65,7 +71,14 @@ export const ObjectOptionsDropdownCalendarEndFieldsContent = () => {
const calendarEndFieldMetadataId = fieldMetadataItem?.id ?? null;
setRecordIndexCalendarEndFieldMetadataId(calendarEndFieldMetadataId);
await updateCurrentView({ calendarEndFieldMetadataId });
try {
await updateCurrentView({ calendarEndFieldMetadataId });
} catch (error) {
setRecordIndexCalendarEndFieldMetadataId(
recordIndexCalendarEndFieldMetadataId,
);
throw error;
}
closeDropdown();
};
@@ -94,7 +107,7 @@ export const ObjectOptionsDropdownCalendarEndFieldsContent = () => {
<DropdownMenuSeparator />
<DropdownMenuItemsContainer>
<MenuItemSelect
selected={!isDefined(currentView?.calendarEndFieldMetadataId)}
selected={!isDefined(recordIndexCalendarEndFieldMetadataId)}
onClick={() => handleCalendarEndFieldChange(null)}
text={t`None`}
/>
@@ -105,8 +118,7 @@ export const ObjectOptionsDropdownCalendarEndFieldsContent = () => {
<MenuItemSelect
key={fieldMetadataItem.id}
selected={
fieldMetadataItem.id ===
currentView?.calendarEndFieldMetadataId
fieldMetadataItem.id === recordIndexCalendarEndFieldMetadataId
}
onClick={() => handleCalendarEndFieldChange(fieldMetadataItem)}
LeftIcon={getIcon(fieldMetadataItem.icon)}
@@ -1,4 +1,3 @@
import { useSetAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useSetAtomComponentState';
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { useObjectOptionsDropdown } from '@/object-record/object-options-dropdown/hooks/useObjectOptionsDropdown';
import { recordIndexCalendarEndFieldMetadataIdComponentState } from '@/object-record/record-index/states/recordIndexCalendarEndFieldMetadataIdComponentState';
@@ -11,7 +10,7 @@ import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/Drop
import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput';
import { DropdownMenuSectionLabel } from '@/ui/layout/dropdown/components/DropdownMenuSectionLabel';
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
import { useGetCurrentViewOnly } from '@/views/hooks/useGetCurrentViewOnly';
import { useAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentState';
import { useUpdateCurrentView } from '@/views/hooks/useUpdateCurrentView';
import { useGetAvailableFieldsForCalendar } from '@/views/view-picker/hooks/useGetAvailableFieldsForCalendar';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
@@ -33,27 +32,30 @@ export const ObjectOptionsDropdownCalendarFieldsContent = () => {
const { objectMetadataItem, resetContent, onContentChange, closeDropdown } =
useObjectOptionsDropdown();
const { currentView } = useGetCurrentViewOnly();
const { updateCurrentView } = useUpdateCurrentView();
const { availableFieldsForCalendar, navigateToDateFieldSettings } =
useGetAvailableFieldsForCalendar();
const setRecordIndexCalendarFieldMetadataId = useSetAtomComponentState(
recordIndexCalendarFieldMetadataIdComponentState,
);
const setRecordIndexCalendarEndFieldMetadataId = useSetAtomComponentState(
const [
recordIndexCalendarFieldMetadataId,
setRecordIndexCalendarFieldMetadataId,
] = useAtomComponentState(recordIndexCalendarFieldMetadataIdComponentState);
const [
recordIndexCalendarEndFieldMetadataId,
setRecordIndexCalendarEndFieldMetadataId,
] = useAtomComponentState(
recordIndexCalendarEndFieldMetadataIdComponentState,
);
const calendarFieldMetadata = currentView?.calendarFieldMetadataId
const calendarFieldMetadata = recordIndexCalendarFieldMetadataId
? objectMetadataItem.fields.find(
(field) => field.id === currentView.calendarFieldMetadataId,
(field) => field.id === recordIndexCalendarFieldMetadataId,
)
: undefined;
const calendarEndFieldMetadata = currentView?.calendarEndFieldMetadataId
const calendarEndFieldMetadata = recordIndexCalendarEndFieldMetadataId
? objectMetadataItem.fields.find(
(field) => field.id === currentView.calendarEndFieldMetadataId,
(field) => field.id === recordIndexCalendarEndFieldMetadataId,
)
: undefined;
@@ -83,7 +85,7 @@ export const ObjectOptionsDropdownCalendarFieldsContent = () => {
fieldMetadataItem: FieldMetadataItem,
) => {
const shouldClearCalendarEndField =
isDefined(currentView?.calendarEndFieldMetadataId) &&
isDefined(recordIndexCalendarEndFieldMetadataId) &&
(!isDefined(calendarEndFieldMetadata) ||
calendarEndFieldMetadata.id === fieldMetadataItem.id ||
calendarEndFieldMetadata.type !== fieldMetadataItem.type);
@@ -93,12 +95,22 @@ export const ObjectOptionsDropdownCalendarFieldsContent = () => {
setRecordIndexCalendarEndFieldMetadataId(null);
}
await updateCurrentView({
calendarFieldMetadataId: fieldMetadataItem.id,
...(shouldClearCalendarEndField
? { calendarEndFieldMetadataId: null }
: {}),
});
try {
await updateCurrentView({
calendarFieldMetadataId: fieldMetadataItem.id,
...(shouldClearCalendarEndField
? { calendarEndFieldMetadataId: null }
: {}),
});
} catch (error) {
setRecordIndexCalendarFieldMetadataId(recordIndexCalendarFieldMetadataId);
if (shouldClearCalendarEndField) {
setRecordIndexCalendarEndFieldMetadataId(
recordIndexCalendarEndFieldMetadataId,
);
}
throw error;
}
closeDropdown();
};
@@ -4,6 +4,7 @@ import { useObjectOptionsDropdown } from '@/object-record/object-options-dropdow
import { useObjectOptionsForBoard } from '@/object-record/object-options-dropdown/hooks/useObjectOptionsForBoard';
import { getSupportedRecordCalendarLayout } from '@/object-record/record-calendar/utils/getSupportedRecordCalendarLayout';
import { recordIndexCalendarLayoutComponentState } from '@/object-record/record-index/states/recordIndexCalendarLayoutComponentState';
import { recordIndexCalendarFieldMetadataIdComponentState } from '@/object-record/record-index/states/recordIndexCalendarFieldMetadataIdComponentState';
import { recordIndexGroupFieldMetadataItemComponentState } from '@/object-record/record-index/states/recordIndexGroupFieldMetadataComponentState';
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
@@ -67,10 +68,13 @@ export const ObjectOptionsDropdownCustomView = ({
const recordIndexGroupFieldMetadataItem = useAtomComponentStateValue(
recordIndexGroupFieldMetadataItemComponentState,
);
const recordIndexCalendarFieldMetadataId = useAtomComponentStateValue(
recordIndexCalendarFieldMetadataIdComponentState,
);
const calendarFieldMetadata = currentView?.calendarFieldMetadataId
const calendarFieldMetadata = recordIndexCalendarFieldMetadataId
? objectMetadataItem.fields.find(
(field) => field.id === currentView.calendarFieldMetadataId,
(field) => field.id === recordIndexCalendarFieldMetadataId,
)
: undefined;