fix: relative date picker calendar display (#21895)

Part of
https://github.com/twentyhq/twenty/issues/19739#issuecomment-4652034526
(Bug 1-3). Maybe it feels like theses bugs are not actually bugs, but we
can maybe say it as UX improvements: specially needed in case when an
user will choose any past options.

### Bug 1: calendar open on wrong month
With Is Relative (e.g. Past 1 Quarter), the calendar opened on today’s
month instead of the range start. After the fix, it now opens on the
first month of the filtered range.

**Testing:**
View filter → Date field → Is Relative → Past 1 Quarter. Calendar opens
on January (range start), not today’s month


https://github.com/user-attachments/assets/8849d00a-4d5c-4f8a-8d31-3a62535eb311


### Bug 2: Dates not highlighted
Ranges older than ~2 months (e.g. Q1 when today is June) showed no
highlighted days. Highlighting now covers the full resolved range.

**Testing:**
Same setup: past 1 Quarter on a date when Q1 is outside the old 2‑month
window. Jan 1 - Mar 31 will highlight.


https://github.com/user-attachments/assets/d21e2272-c923-4493-80ff-bdf4228842b1


### Bug 3: No month navigation
Relative mode only showed Past - 1 - Quarter controls with no way to
browse months. Now see the new arrows move through months without
changing the filter.

<img width="377" height="455" alt="Screenshot 2026-06-20 181107"
src="https://github.com/user-attachments/assets/eb51feb9-af10-489a-b166-8b8d6c642e05"
/>


> [!NOTE]
> 1. We can't do the fixes by one by one, i have to fix them within one
PR because all the fixes are inter-related, like we can't test the bug 1
fix alone without implementing bug 3.
> 2. Bug 4 will be done in a separate PR which is actually the issue
#19739. See
https://github.com/twentyhq/twenty/issues/19739#issuecomment-4652034526
for better understanding.
> 3. If you see the screen recordings, they are actually done with the
alignment fixes from #21881 . So without that changes you will see the
alignmemt issues in the calendar grid in your local.

---------

Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
This commit is contained in:
Parship Chowdhury
2026-06-26 13:33:21 +05:30
committed by GitHub
parent 6e2df0654b
commit b3e39e2198
30 changed files with 969 additions and 662 deletions
@@ -4,12 +4,12 @@ import { useGetDefaultFieldMetadataItemForFilter } from '@/object-record/advance
import { useSetRecordFilterUsedInAdvancedFilterDropdownRow } from '@/object-record/advanced-filter/hooks/useSetRecordFilterUsedInAdvancedFilterDropdownRow';
import { AdvancedFilterContext } from '@/object-record/advanced-filter/states/context/AdvancedFilterContext';
import { getAdvancedFilterAddFilterRuleSelectDropdownId } from '@/object-record/advanced-filter/utils/getAdvancedFilterAddFilterRuleSelectDropdownId';
import { getDefaultAdvancedFilterOperand } from '@/object-record/advanced-filter/utils/getDefaultAdvancedFilterOperand';
import { useUpsertRecordFilterGroup } from '@/object-record/record-filter-group/hooks/useUpsertRecordFilterGroup';
import { type RecordFilterGroup } from '@/object-record/record-filter-group/types/RecordFilterGroup';
import { useUpsertRecordFilter } from '@/object-record/record-filter/hooks/useUpsertRecordFilter';
import { type RecordFilter } from '@/object-record/record-filter/types/RecordFilter';
import { getDefaultSubFieldNameForCompositeFilterableFieldType } from '@/object-record/record-filter/utils/getDefaultSubFieldNameForCompositeFilterableFieldType';
import { getRecordFilterOperands } from '@/object-record/record-filter/utils/getRecordFilterOperands';
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
@@ -77,9 +77,7 @@ export const AdvancedFilterAddFilterRuleSelect = ({
id: v4(),
fieldMetadataId: defaultFieldMetadataItemForFilter.id,
type: filterType,
operand: getRecordFilterOperands({
filterType,
})[0],
operand: getDefaultAdvancedFilterOperand({ filterType }),
value: '',
displayValue: '',
recordFilterGroupId: recordFilterGroup.id,
@@ -126,9 +124,7 @@ export const AdvancedFilterAddFilterRuleSelect = ({
id: v4(),
fieldMetadataId: defaultFieldMetadataItemForFilter.id,
type: filterType,
operand: getRecordFilterOperands({
filterType,
})[0],
operand: getDefaultAdvancedFilterOperand({ filterType }),
value: '',
displayValue: '',
recordFilterGroupId: newRecordFilterGroupId,
@@ -1,4 +1,5 @@
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { getDefaultAdvancedFilterOperand } from '@/object-record/advanced-filter/utils/getDefaultAdvancedFilterOperand';
import { useGetInitialFilterValue } from '@/object-record/object-filter-dropdown/hooks/useGetInitialFilterValue';
import { fieldMetadataItemIdUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/fieldMetadataItemIdUsedInDropdownComponentState';
import { objectFilterDropdownCurrentRecordFilterComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownCurrentRecordFilterComponentState';
@@ -9,7 +10,6 @@ import { subFieldNameUsedInDropdownComponentState } from '@/object-record/object
import { useUpsertRecordFilter } from '@/object-record/record-filter/hooks/useUpsertRecordFilter';
import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
import { type RecordFilter } from '@/object-record/record-filter/types/RecordFilter';
import { getRecordFilterOperands } from '@/object-record/record-filter/utils/getRecordFilterOperands';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
import { useSetAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useSetAtomComponentState';
import { getFilterTypeFromFieldType, isDefined } from 'twenty-shared/utils';
@@ -65,20 +65,20 @@ export const useApplyAdvancedFilterSourceField = () => {
const filterType = getFilterTypeFromFieldType(sourceFieldMetadataItem.type);
const firstOperand = getRecordFilterOperands({
const defaultOperand = getDefaultAdvancedFilterOperand({
filterType,
subFieldName: null,
})?.[0];
});
if (!isDefined(firstOperand)) {
if (!isDefined(defaultOperand)) {
throw new Error(`No valid operand found for filter type: ${filterType}`);
}
setSelectedOperandInDropdown(firstOperand);
setSelectedOperandInDropdown(defaultOperand);
const { value, displayValue } = getInitialFilterValue(
filterType,
firstOperand,
defaultOperand,
);
const existingRecordFilter = currentRecordFilters.find(
@@ -89,7 +89,7 @@ export const useApplyAdvancedFilterSourceField = () => {
id: recordFilterId,
fieldMetadataId: sourceFieldMetadataItem.id,
displayValue,
operand: firstOperand,
operand: defaultOperand,
value,
recordFilterGroupId: existingRecordFilter?.recordFilterGroupId,
positionInRecordFilterGroup:
@@ -144,6 +144,7 @@ export const AdvancedFilterSidePanelValueFormInput = ({
defaultValue={recordFilter.value}
onChange={handleRelativeDateFilterChange}
readonly={readonly}
isDateTimeField={recordFilter.type === FieldMetadataType.DATE_TIME}
/>
);
}
@@ -0,0 +1,25 @@
import { getDefaultAdvancedFilterOperand } from '@/object-record/advanced-filter/utils/getDefaultAdvancedFilterOperand';
import { getRecordFilterOperands } from '@/object-record/record-filter/utils/getRecordFilterOperands';
import { ViewFilterOperand } from 'twenty-shared/types';
describe('getDefaultAdvancedFilterOperand', () => {
it('should default DATE fields to IS_RELATIVE', () => {
expect(getDefaultAdvancedFilterOperand({ filterType: 'DATE' })).toBe(
ViewFilterOperand.IS_RELATIVE,
);
});
it('should default DATE_TIME fields to IS_RELATIVE', () => {
expect(getDefaultAdvancedFilterOperand({ filterType: 'DATE_TIME' })).toBe(
ViewFilterOperand.IS_RELATIVE,
);
});
it('should keep the first available operand for non-date fields', () => {
const filterType = 'TEXT';
expect(getDefaultAdvancedFilterOperand({ filterType })).toBe(
getRecordFilterOperands({ filterType })[0],
);
});
});
@@ -0,0 +1,29 @@
import { getRecordFilterOperands } from '@/object-record/record-filter/utils/getRecordFilterOperands';
import {
type FilterableAndTSVectorFieldType,
ViewFilterOperand as RecordFilterOperand,
} from 'twenty-shared/types';
export const getDefaultAdvancedFilterOperand = ({
filterType,
subFieldName,
}: {
filterType: FilterableAndTSVectorFieldType;
subFieldName?: string | null;
}): RecordFilterOperand => {
const availableOperands = getRecordFilterOperands({
filterType,
subFieldName,
});
const isDateFilterType = filterType === 'DATE' || filterType === 'DATE_TIME';
if (
isDateFilterType &&
availableOperands.includes(RecordFilterOperand.IS_RELATIVE)
) {
return RecordFilterOperand.IS_RELATIVE;
}
return availableOperands[0];
};
@@ -2,9 +2,13 @@ import { Temporal } from 'temporal-polyfill';
import { useGetDateFilterDisplayValue } from '@/object-record/object-filter-dropdown/hooks/useGetDateFilterDisplayValue';
import { useGetDateTimeFilterDisplayValue } from '@/object-record/object-filter-dropdown/hooks/useGetDateTimeFilterDisplayValue';
import { getRelativeDateDisplayValue } from '@/object-record/object-filter-dropdown/utils/getRelativeDateDisplayValue';
import { useGetRelativeDateFilterWithUserTimezone } from '@/object-record/record-filter/hooks/useGetRelativeDateFilterWithUserTimezone';
import { type RecordFilter } from '@/object-record/record-filter/types/RecordFilter';
import { RecordFilterOperand } from '@/object-record/record-filter/types/RecordFilterOperand';
import { useUserTimezone } from '@/ui/input/components/internal/date/hooks/useUserTimezone';
import { stringifyRelativeDateFilter } from '@/views/view-filter-value/utils/stringifyRelativeDateFilter';
import { DEFAULT_RELATIVE_DATE_FILTER_VALUE } from 'twenty-shared/constants';
import { type FilterableAndTSVectorFieldType } from 'twenty-shared/types';
const activeDatePickerOperands = [
@@ -17,12 +21,25 @@ export const useGetInitialFilterValue = () => {
const { userTimezone } = useUserTimezone();
const { getDateFilterDisplayValue } = useGetDateFilterDisplayValue();
const { getDateTimeFilterDisplayValue } = useGetDateTimeFilterDisplayValue();
const { getRelativeDateFilterWithUserTimezone } =
useGetRelativeDateFilterWithUserTimezone();
const getInitialFilterValue = (
newType: FilterableAndTSVectorFieldType,
newOperand: RecordFilterOperand,
alreadyExistingZonedDateTime?: Temporal.ZonedDateTime,
): Pick<RecordFilter, 'value' | 'displayValue'> | Record<string, never> => {
if (newOperand === RecordFilterOperand.IS_RELATIVE) {
const newRelativeDateFilter = getRelativeDateFilterWithUserTimezone(
DEFAULT_RELATIVE_DATE_FILTER_VALUE,
);
return {
value: stringifyRelativeDateFilter(newRelativeDateFilter),
displayValue: getRelativeDateDisplayValue(newRelativeDateFilter),
};
}
switch (newType) {
case 'DATE': {
if (activeDatePickerOperands.includes(newOperand)) {
@@ -60,10 +77,6 @@ export const useGetInitialFilterValue = () => {
return { value, displayValue };
}
if (newOperand === RecordFilterOperand.IS_RELATIVE) {
return { value: '', displayValue: '' };
}
break;
}
case 'BOOLEAN': {
@@ -1,15 +1,25 @@
import { RelativeDateFilterRangeHint } from '@/object-record/record-field/ui/form-types/components/RelativeDateFilterRangeHint';
import { useGetRelativeDateFilterWithUserTimezone } from '@/object-record/record-filter/hooks/useGetRelativeDateFilterWithUserTimezone';
import { RelativeDatePickerHeader } from '@/ui/input/components/internal/date/components/RelativeDatePickerHeader';
import { stringifyRelativeDateFilter } from '@/views/view-filter-value/utils/stringifyRelativeDateFilter';
import { styled } from '@linaria/react';
import { isNonEmptyString } from '@sniptt/guards';
import { useId } from 'react';
import { DEFAULT_RELATIVE_DATE_FILTER_VALUE } from 'twenty-shared/constants';
import { themeCssVariables } from 'twenty-ui/theme-constants';
import {
type RelativeDateFilter,
resolveRelativeDateFilterStringified,
} from 'twenty-shared/utils';
const StyledContainer = styled.div`
display: flex;
flex-direction: column;
gap: ${themeCssVariables.spacing[1]};
`;
export type FormRelativeDatePickerProps = {
label?: string;
defaultValue?: string;
@@ -33,6 +43,14 @@ export const FormRelativeDatePicker = ({
? resolveRelativeDateFilterStringified(defaultValue)
: DEFAULT_RELATIVE_DATE_FILTER_VALUE;
const effectiveRelativeDateFilterValue = isNonEmptyString(defaultValue)
? defaultValue
: stringifyRelativeDateFilter(
getRelativeDateFilterWithUserTimezone(
DEFAULT_RELATIVE_DATE_FILTER_VALUE,
),
);
const handleValueChange = (newValue: RelativeDateFilter) => {
const newValueWithTimezone =
getRelativeDateFilterWithUserTimezone(newValue);
@@ -41,16 +59,22 @@ export const FormRelativeDatePicker = ({
};
return (
<RelativeDatePickerHeader
instanceId={instanceId}
onChange={handleValueChange}
direction={valueParsed?.direction ?? 'THIS'}
unit={valueParsed?.unit ?? 'DAY'}
amount={valueParsed?.amount ?? undefined}
isFormField={true}
readonly={readonly}
unitDropdownWidth={150}
allowIntraDayUnits={isDateTimeField}
/>
<StyledContainer>
<RelativeDatePickerHeader
instanceId={instanceId}
onChange={handleValueChange}
direction={valueParsed?.direction ?? 'THIS'}
unit={valueParsed?.unit ?? 'DAY'}
amount={valueParsed?.amount ?? undefined}
isFormField={true}
readonly={readonly}
unitDropdownWidth={150}
allowIntraDayUnits={isDateTimeField}
/>
<RelativeDateFilterRangeHint
relativeDateFilterValue={effectiveRelativeDateFilterValue}
isDateTimeField={isDateTimeField}
/>
</StyledContainer>
);
};
@@ -0,0 +1,86 @@
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { InputHint } from '@/ui/input/components/InputHint';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { Temporal } from 'temporal-polyfill';
import { SOURCE_LOCALE } from 'twenty-shared/translations';
import {
isDefined,
isSubDayRelativeDateFilterUnit,
resolveRelativeDateFilterStringified,
resolveRelativeDateTimeFilterStringified,
} from 'twenty-shared/utils';
type RelativeDateFilterRangeHintProps = {
relativeDateFilterValue?: string | null;
isDateTimeField?: boolean;
};
export const RelativeDateFilterRangeHint = ({
relativeDateFilterValue,
isDateTimeField,
}: RelativeDateFilterRangeHintProps) => {
const currentWorkspaceMember = useAtomStateValue(currentWorkspaceMemberState);
const userLocale = currentWorkspaceMember?.locale ?? SOURCE_LOCALE;
const formatPlainDate = (plainDate: Temporal.PlainDate) =>
new Intl.DateTimeFormat(userLocale, { dateStyle: 'medium' }).format(
new Date(plainDate.year, plainDate.month - 1, plainDate.day),
);
const formatPlainDateRange = (
start: Temporal.PlainDate,
endInclusive: Temporal.PlainDate,
) =>
start.equals(endInclusive)
? formatPlainDate(start)
: `${formatPlainDate(start)} ${formatPlainDate(endInclusive)}`;
const formatZonedDateTime = (zonedDateTime: Temporal.ZonedDateTime) =>
new Intl.DateTimeFormat(userLocale, {
dateStyle: 'medium',
timeStyle: 'short',
timeZone: zonedDateTime.timeZoneId,
}).format(new Date(zonedDateTime.epochMilliseconds));
let rangeLabel: string | null = null;
if (isDateTimeField === true) {
const resolved = resolveRelativeDateTimeFilterStringified(
relativeDateFilterValue,
);
if (
isDefined(resolved) &&
isDefined(resolved.start) &&
isDefined(resolved.end)
) {
rangeLabel = isSubDayRelativeDateFilterUnit(resolved.unit)
? `${formatZonedDateTime(resolved.start)}${formatZonedDateTime(resolved.end)}`
: formatPlainDateRange(
resolved.start.toPlainDate(),
resolved.end.subtract({ nanoseconds: 1 }).toPlainDate(),
);
}
} else {
const resolved = resolveRelativeDateFilterStringified(
relativeDateFilterValue,
);
if (
isDefined(resolved) &&
isDefined(resolved.start) &&
isDefined(resolved.end)
) {
rangeLabel = formatPlainDateRange(
Temporal.PlainDate.from(resolved.start),
Temporal.PlainDate.from(resolved.end).subtract({ days: 1 }),
);
}
}
if (!isDefined(rangeLabel)) {
return null;
}
return <InputHint>{rangeLabel}</InputHint>;
};