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:
+10
-2
@@ -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'];
|
||||
|
||||
+21
-9
@@ -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)}
|
||||
|
||||
+30
-18
@@ -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();
|
||||
};
|
||||
|
||||
|
||||
+6
-2
@@ -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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user