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>;
};
@@ -14,18 +14,18 @@ import {
DATE_PICKER_CONTAINER_WIDTH,
StyledDatePickerContainer,
} from '@/ui/input/components/internal/date/components/StyledDatePickerContainer';
import { getHighlightedDates } from '@/ui/input/components/internal/date/utils/getHighlightedDates';
import { getRelativeDatePickerCalendarRange } from '@/ui/input/components/internal/date/utils/getRelativeDatePickerCalendarRange';
import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
import { t } from '@lingui/core/macro';
import 'react-datepicker/dist/react-datepicker.css';
import { useUserTimezone } from '@/ui/input/components/internal/date/hooks/useUserTimezone';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { Temporal } from 'temporal-polyfill';
import { type Nullable } from 'twenty-shared/types';
import {
isDefined,
turnJSDateToPlainDate,
turnPlainDateToShiftedDateInSystemTimeZone,
type RelativeDateFilter,
} from 'twenty-shared/utils';
import { IconCalendarX } from 'twenty-ui/icon';
@@ -97,16 +97,14 @@ type DatePickerProps = {
};
// react-datepicker v9 types its props as a discriminated union keyed on
// selectsRange/selectsMultiple. We drive selectsMultiple dynamically, which TS
// cannot narrow to a single union branch, so collapse the discriminants to plain
// optionals (selectedDates is accepted but ignored by the library at runtime).
// selectsRange/selectsMultiple. We drive selectsRange dynamically (relative
// filters highlight a contiguous range), which TS cannot narrow to a single
// union branch, so collapse the discriminants to plain optionals.
type DatePickerPropsType = Omit<
ReactDatePickerLibProps,
'selectsRange' | 'selectsMultiple' | 'onChange' | 'formatMultipleDates'
> & {
selectsRange?: boolean;
selectsMultiple?: boolean;
selectedDates?: Date[];
onChange?: (date: Date | null) => void;
};
@@ -137,7 +135,25 @@ export const DatePicker = ({
? Temporal.PlainDate.from(plainDateString)
: Temporal.Now.plainDateISO();
const { userTimezone } = useUserTimezone();
const relativeRangeStart = isRelative ? relativeDate?.start : undefined;
const relativeRangeEnd = isRelative ? relativeDate?.end : undefined;
const relativeRangeStartPlainDate = isDefined(relativeRangeStart)
? Temporal.PlainDate.from(relativeRangeStart)
: null;
const relativeRangeEndPlainDate = isDefined(relativeRangeEnd)
? Temporal.PlainDate.from(relativeRangeEnd).subtract({ days: 1 })
: null;
const {
startDate: relativeRangeStartDate,
endDate: relativeRangeEndDate,
rangeKey: relativeDateRangeKey,
} = getRelativeDatePickerCalendarRange(
relativeRangeStartPlainDate,
relativeRangeEndPlainDate,
);
const { closeDropdown: closeDropdownMonthSelect } = useCloseDropdown();
const { closeDropdown: closeDropdownYearSelect } = useCloseDropdown();
@@ -199,38 +215,13 @@ export const DatePicker = ({
handleClose?.(plainDatePicked.toString());
};
const highlightedDates =
isRelative && isDefined(relativeDate?.end) && isDefined(relativeDate?.start)
? getHighlightedDates(
Temporal.PlainDate.from(relativeDate.start),
Temporal.PlainDate.from(relativeDate.end).subtract({ days: 1 }),
userTimezone,
)
: [];
const dateAsDate = new Date(plainDate.toString());
const selectedDates = isRelative
? highlightedDates.map((plainDate) => new Date(plainDate.toString()))
: isDefined(dateAsDate)
? [dateAsDate]
: [];
const calendarStartDay =
currentWorkspaceMember?.calendarStartDay === CalendarStartDay.SYSTEM
? CalendarStartDay[detectCalendarStartDay()]
: (currentWorkspaceMember?.calendarStartDay ?? undefined);
const systemTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
const dateShiftedToISOString = plainDate
?.toZonedDateTime(systemTimeZone)
.toInstant()
.toString();
const dateForDatePicker = isDefined(dateShiftedToISOString)
? new Date(dateShiftedToISOString)
: null;
const dateForDatePicker =
turnPlainDateToShiftedDateInSystemTimeZone(plainDate);
return (
<StyledDatePickerContainer calendarDisabled={isRelative}>
@@ -264,16 +255,23 @@ export const DatePicker = ({
}
>
<ReactDatePicker
key={relativeDateRangeKey}
open={true}
selected={dateForDatePicker}
selectedDates={selectedDates}
openToDate={dateForDatePicker ?? undefined}
disabledKeyboardNavigation
onChange={handleDateChange}
onSelect={handleDateSelect}
openToDate={isRelative ? relativeRangeStartDate : dateForDatePicker}
selectsRange={isRelative ? true : undefined}
startDate={isRelative ? relativeRangeStartDate : undefined}
endDate={isRelative ? relativeRangeEndDate : undefined}
selected={isRelative ? undefined : dateForDatePicker}
calendarStartDay={
calendarStartDay as 0 | 1 | 2 | 3 | 4 | 5 | 6 | undefined
}
renderCustomHeader={({
monthDate,
decreaseMonth,
increaseMonth,
prevMonthButtonDisabled,
nextMonthButtonDisabled,
}) =>
@@ -284,6 +282,11 @@ export const DatePicker = ({
amount={relativeDate?.amount}
unit={relativeDate?.unit ?? 'DAY'}
onChange={onRelativeDateChange}
calendarMonthDate={monthDate}
onPreviousMonth={decreaseMonth}
onNextMonth={increaseMonth}
prevMonthButtonDisabled={prevMonthButtonDisabled}
nextMonthButtonDisabled={nextMonthButtonDisabled}
/>
) : (
<DatePickerHeader
@@ -299,8 +302,6 @@ export const DatePicker = ({
/>
)
}
onSelect={handleDateSelect}
selectsMultiple={isRelative}
/>
</Suspense>
</div>
@@ -2,6 +2,7 @@ import { SKELETON_LOADER_HEIGHT_SIZES } from '@/activities/components/SkeletonLo
import {
convertFirstDayOfTheWeekToCalendarStartDayNumber,
isDefined,
isSubDayRelativeDateFilterUnit,
type RelativeDateFilter,
} from 'twenty-shared/utils';
@@ -10,8 +11,9 @@ import {
DateTimePickerHeader,
} from '@/ui/input/components/internal/date/components/DateTimePickerHeader';
import { RelativeDatePickerHeader } from '@/ui/input/components/internal/date/components/RelativeDatePickerHeader';
import { RelativeDateTimeRangeText } from '@/ui/input/components/internal/date/components/RelativeDateTimeRangeText';
import { StyledDatePickerContainer } from '@/ui/input/components/internal/date/components/StyledDatePickerContainer';
import { getHighlightedDates } from '@/ui/input/components/internal/date/utils/getHighlightedDates';
import { getRelativeDatePickerCalendarRange } from '@/ui/input/components/internal/date/utils/getRelativeDatePickerCalendarRange';
import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
import { styled } from '@linaria/react';
import { t } from '@lingui/core/macro';
@@ -110,16 +112,14 @@ type DateTimePickerProps = {
};
// react-datepicker v9 types its props as a discriminated union keyed on
// selectsRange/selectsMultiple. We drive selectsMultiple dynamically, which TS
// cannot narrow to a single union branch, so collapse the discriminants to plain
// optionals (selectedDates is accepted but ignored by the library at runtime).
// selectsRange/selectsMultiple. We drive selectsRange dynamically (relative
// filters highlight a contiguous range), which TS cannot narrow to a single
// union branch, so collapse the discriminants to plain optionals.
type DatePickerPropsType = Omit<
ReactDatePickerLibProps,
'selectsRange' | 'selectsMultiple' | 'onChange' | 'formatMultipleDates'
> & {
selectsRange?: boolean;
selectsMultiple?: boolean;
selectedDates?: Date[];
onChange?: (date: Date | null) => void;
};
@@ -228,14 +228,29 @@ export const DateTimePicker = ({
handleClose?.(zonedDateTime);
};
const highlightedDates =
isRelative && isDefined(relativeDate?.end) && isDefined(relativeDate?.start)
? getHighlightedDates(
relativeDate?.start.toPlainDate(),
relativeDate?.end.subtract({ days: 1 }).toPlainDate(),
timeZone ?? userTimezone,
)
: [];
const relativeUnit = relativeDate?.unit ?? 'DAY';
const relativeRangeStart = isRelative ? relativeDate?.start : undefined;
const relativeRangeEnd = isRelative ? relativeDate?.end : undefined;
const isSubDayRelativeUnit =
isRelative === true && isSubDayRelativeDateFilterUnit(relativeUnit);
const relativeRangeStartPlainDate = isDefined(relativeRangeStart)
? relativeRangeStart.toPlainDate()
: null;
const relativeRangeEndPlainDate = isDefined(relativeRangeEnd)
? relativeRangeEnd.subtract({ nanoseconds: 1 }).toPlainDate()
: null;
const {
startDate: relativeRangeStartDate,
endDate: relativeRangeEndDate,
rangeKey: relativeDateRangeKey,
} = getRelativeDatePickerCalendarRange(
relativeRangeStartPlainDate,
relativeRangeEndPlainDate,
);
const nonShiftedDateForReactDatePicker = new Date(
dateToUse.toInstant().toString(),
@@ -246,96 +261,116 @@ export const DateTimePicker = ({
timeZone ?? userTimezone,
);
const selectedDates = isRelative
? highlightedDates.map((plainDate) => {
const date = new Date();
date.setDate(1);
date.setFullYear(plainDate.year);
date.setMonth(plainDate.month - 1);
date.setDate(plainDate.day);
return date;
})
: [shiftedDateForReactDatePicker];
const calendarStartDayNumber =
convertFirstDayOfTheWeekToCalendarStartDayNumber(userFirstDayOfTheWeek);
return (
<StyledOuterWrapper>
<StyledDatePickerContainer calendarDisabled={isRelative}>
<Suspense
fallback={
<StyledDatePickerFallback>
<SkeletonTheme
baseColor={theme.background.tertiary}
highlightColor={theme.background.transparent.lighter}
borderRadius={4}
>
<Skeleton
width={200}
height={SKELETON_LOADER_HEIGHT_SIZES.standard.m}
/>
<Skeleton
width={240}
height={SKELETON_LOADER_HEIGHT_SIZES.standard.l}
/>
<Skeleton
width={220}
height={SKELETON_LOADER_HEIGHT_SIZES.standard.m}
/>
<Skeleton
width={180}
height={SKELETON_LOADER_HEIGHT_SIZES.standard.s}
/>
</SkeletonTheme>
</StyledDatePickerFallback>
}
>
<ReactDatePicker
open={true}
selected={shiftedDateForReactDatePicker}
selectedDates={selectedDates}
openToDate={shiftedDateForReactDatePicker}
disabledKeyboardNavigation
onChange={handleDateChange}
calendarStartDay={
calendarStartDayNumber as 0 | 1 | 2 | 3 | 4 | 5 | 6 | undefined
<StyledDatePickerContainer
calendarDisabled={isRelative && !isSubDayRelativeUnit}
>
{isSubDayRelativeUnit ? (
<>
<RelativeDatePickerHeader
instanceId={instanceId}
direction={relativeDate?.direction ?? 'PAST'}
amount={relativeDate?.amount}
unit={relativeUnit}
onChange={onRelativeDateChange}
allowIntraDayUnits={true}
/>
{isDefined(relativeRangeStart) && isDefined(relativeRangeEnd) && (
<RelativeDateTimeRangeText
start={relativeRangeStart}
end={relativeRangeEnd}
/>
)}
</>
) : (
<Suspense
fallback={
<StyledDatePickerFallback>
<SkeletonTheme
baseColor={theme.background.tertiary}
highlightColor={theme.background.transparent.lighter}
borderRadius={4}
>
<Skeleton
width={200}
height={SKELETON_LOADER_HEIGHT_SIZES.standard.m}
/>
<Skeleton
width={240}
height={SKELETON_LOADER_HEIGHT_SIZES.standard.l}
/>
<Skeleton
width={220}
height={SKELETON_LOADER_HEIGHT_SIZES.standard.m}
/>
<Skeleton
width={180}
height={SKELETON_LOADER_HEIGHT_SIZES.standard.s}
/>
</SkeletonTheme>
</StyledDatePickerFallback>
}
renderCustomHeader={({
prevMonthButtonDisabled,
nextMonthButtonDisabled,
}) =>
isRelative ? (
<RelativeDatePickerHeader
instanceId={instanceId}
direction={relativeDate?.direction ?? 'PAST'}
amount={relativeDate?.amount}
unit={relativeDate?.unit ?? 'DAY'}
onChange={onRelativeDateChange}
allowIntraDayUnits={true}
/>
) : (
<DateTimePickerHeader
date={dateToUse}
onChange={onChange}
onAddMonth={handleAddMonth}
onSubtractMonth={handleSubtractMonth}
prevMonthButtonDisabled={prevMonthButtonDisabled}
nextMonthButtonDisabled={nextMonthButtonDisabled}
hideInput={hideHeaderInput}
onChangeMonth={handleChangeMonth}
onChangeYear={handleChangeYear}
/>
)
}
onSelect={handleDateSelect}
selectsMultiple={isRelative}
/>
</Suspense>
>
<ReactDatePicker
key={relativeDateRangeKey}
open={true}
disabledKeyboardNavigation
onChange={handleDateChange}
onSelect={handleDateSelect}
openToDate={
isRelative
? relativeRangeStartDate
: shiftedDateForReactDatePicker
}
selectsRange={isRelative ? true : undefined}
startDate={isRelative ? relativeRangeStartDate : undefined}
endDate={isRelative ? relativeRangeEndDate : undefined}
selected={isRelative ? undefined : shiftedDateForReactDatePicker}
calendarStartDay={
calendarStartDayNumber as 0 | 1 | 2 | 3 | 4 | 5 | 6 | undefined
}
renderCustomHeader={({
monthDate,
decreaseMonth,
increaseMonth,
prevMonthButtonDisabled,
nextMonthButtonDisabled,
}) =>
isRelative ? (
<RelativeDatePickerHeader
instanceId={instanceId}
direction={relativeDate?.direction ?? 'PAST'}
amount={relativeDate?.amount}
unit={relativeUnit}
onChange={onRelativeDateChange}
allowIntraDayUnits={true}
calendarMonthDate={monthDate}
onPreviousMonth={decreaseMonth}
onNextMonth={increaseMonth}
prevMonthButtonDisabled={prevMonthButtonDisabled}
nextMonthButtonDisabled={nextMonthButtonDisabled}
/>
) : (
<DateTimePickerHeader
date={dateToUse}
onChange={onChange}
onAddMonth={handleAddMonth}
onSubtractMonth={handleSubtractMonth}
prevMonthButtonDisabled={prevMonthButtonDisabled}
nextMonthButtonDisabled={nextMonthButtonDisabled}
hideInput={hideHeaderInput}
onChangeMonth={handleChangeMonth}
onChangeYear={handleChangeYear}
/>
)
}
/>
</Suspense>
)}
{clearable && (
<>
<StyledSeparator />
@@ -0,0 +1,63 @@
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { styled } from '@linaria/react';
import { SOURCE_LOCALE } from 'twenty-shared/translations';
import { IconChevronLeft, IconChevronRight } from 'twenty-ui/icon';
import { LightIconButton } from 'twenty-ui/input';
import { themeCssVariables } from 'twenty-ui/theme-constants';
const StyledContainer = styled.div`
align-items: center;
display: flex;
gap: ${themeCssVariables.spacing[1]};
`;
const StyledMonthYearLabel = styled.span`
color: ${themeCssVariables.font.color.primary};
flex: 1;
font-size: ${themeCssVariables.font.size.md};
font-weight: ${themeCssVariables.font.weight.medium};
text-align: center;
`;
type RelativeDatePickerCalendarNavigationProps = {
monthLabelDate: Date;
onPreviousMonth: () => void;
onNextMonth: () => void;
prevMonthButtonDisabled: boolean;
nextMonthButtonDisabled: boolean;
};
export const RelativeDatePickerCalendarNavigation = ({
monthLabelDate,
onPreviousMonth,
onNextMonth,
prevMonthButtonDisabled,
nextMonthButtonDisabled,
}: RelativeDatePickerCalendarNavigationProps) => {
const currentWorkspaceMember = useAtomStateValue(currentWorkspaceMemberState);
const userLocale = currentWorkspaceMember?.locale ?? SOURCE_LOCALE;
const monthYearLabel = new Intl.DateTimeFormat(userLocale, {
month: 'long',
year: 'numeric',
}).format(monthLabelDate);
return (
<StyledContainer>
<LightIconButton
Icon={IconChevronLeft}
onClick={onPreviousMonth}
size="medium"
disabled={prevMonthButtonDisabled}
/>
<StyledMonthYearLabel>{monthYearLabel}</StyledMonthYearLabel>
<LightIconButton
Icon={IconChevronRight}
onClick={onNextMonth}
size="medium"
disabled={nextMonthButtonDisabled}
/>
</StyledContainer>
);
};
@@ -1,14 +1,16 @@
import { Select } from '@/ui/input/components/Select';
import { SettingsTextInput } from '@/ui/input/components/SettingsTextInput';
import { RelativeDatePickerCalendarNavigation } from '@/ui/input/components/internal/date/components/RelativeDatePickerCalendarNavigation';
import { RELATIVE_DATE_DIRECTION_SELECT_OPTIONS } from '@/ui/input/components/internal/date/constants/RelativeDateDirectionSelectOptions';
import { RELATIVE_DATETIME_UNITS_SELECT_OPTIONS } from '@/ui/input/components/internal/date/constants/RelativeDateTimeUnitSelectOptions';
import { RELATIVE_DATE_UNITS_SELECT_OPTIONS } from '@/ui/input/components/internal/date/constants/RelativeDateUnitSelectOptions';
import { RELATIVE_DATETIME_UNITS } from '@/ui/input/components/internal/date/constants/RelativeDateTimeUnits';
import { RELATIVE_DATE_UNITS } from '@/ui/input/components/internal/date/constants/RelativeDateUnits';
import { t } from '@lingui/core/macro';
import { plural, t } from '@lingui/core/macro';
import { styled } from '@linaria/react';
import { useState } from 'react';
import { type Nullable } from 'twenty-shared/types';
import {
assertUnreachable,
isDefined,
relativeDateFilterSchema,
type RelativeDateFilter,
@@ -18,14 +20,20 @@ import {
import { themeCssVariables } from 'twenty-ui/theme-constants';
const StyledContainer = styled.div<{ noPadding: boolean }>`
align-items: center;
display: flex;
flex-direction: column;
gap: ${themeCssVariables.spacing[1]};
padding: ${({ noPadding }) =>
noPadding ? '0' : themeCssVariables.spacing[2]};
padding-bottom: 0;
`;
const StyledControlsRow = styled.div`
align-items: center;
display: flex;
gap: ${themeCssVariables.spacing[1]};
`;
type RelativeDatePickerHeaderProps = {
instanceId: string;
direction: RelativeDateFilterDirection;
@@ -36,6 +44,11 @@ type RelativeDatePickerHeaderProps = {
readonly?: boolean;
unitDropdownWidth?: number;
allowIntraDayUnits?: boolean;
calendarMonthDate?: Date;
onPreviousMonth?: () => void;
onNextMonth?: () => void;
prevMonthButtonDisabled?: boolean;
nextMonthButtonDisabled?: boolean;
};
export const RelativeDatePickerHeader = ({
@@ -48,6 +61,11 @@ export const RelativeDatePickerHeader = ({
readonly,
unitDropdownWidth,
allowIntraDayUnits,
calendarMonthDate,
onPreviousMonth,
onNextMonth,
prevMonthButtonDisabled,
nextMonthButtonDisabled,
}: RelativeDatePickerHeaderProps) => {
const amountString = amount?.toString() ?? '';
@@ -57,88 +75,128 @@ export const RelativeDatePickerHeader = ({
const [draftAmountValue, setDraftAmountValue] = useState(amountTextValue);
const isUnitPlural = isDefined(amount) && amount > 1 && direction !== 'THIS';
const unitCount = isUnitPlural ? 2 : 1;
const getUnitLabel = (unitToLabel: RelativeDateFilterUnit): string => {
switch (unitToLabel) {
case 'SECOND':
return plural(unitCount, { one: 'Second', other: 'Seconds' });
case 'MINUTE':
return plural(unitCount, { one: 'Minute', other: 'Minutes' });
case 'HOUR':
return plural(unitCount, { one: 'Hour', other: 'Hours' });
case 'DAY':
return plural(unitCount, { one: 'Day', other: 'Days' });
case 'WEEK':
return plural(unitCount, { one: 'Week', other: 'Weeks' });
case 'MONTH':
return plural(unitCount, { one: 'Month', other: 'Months' });
case 'QUARTER':
return plural(unitCount, { one: 'Quarter', other: 'Quarters' });
case 'YEAR':
return plural(unitCount, { one: 'Year', other: 'Years' });
default:
return assertUnreachable(unitToLabel);
}
};
const unitOptionsSource = allowIntraDayUnits
? RELATIVE_DATETIME_UNITS_SELECT_OPTIONS
: RELATIVE_DATE_UNITS_SELECT_OPTIONS;
const unitSelectOptions = unitOptionsSource.map((unit) => ({
...unit,
label: `${unit.label}${isUnitPlural ? 's' : ''}`,
? RELATIVE_DATETIME_UNITS
: RELATIVE_DATE_UNITS;
const unitSelectOptions = unitOptionsSource.map((unitOption) => ({
value: unitOption,
label: getUnitLabel(unitOption),
}));
return (
<StyledContainer noPadding={isFormField ?? false}>
<Select
dropdownId={`direction-select-${instanceId}`}
value={direction}
onChange={(newDirection) => {
if (amount === undefined && newDirection !== 'THIS') {
return;
}
<StyledControlsRow>
<Select
dropdownId={`direction-select-${instanceId}`}
value={direction}
onChange={(newDirection) => {
if (amount === undefined && newDirection !== 'THIS') {
return;
}
if (draftAmountValue === '') {
setDraftAmountValue('1');
}
if (draftAmountValue === '') {
setDraftAmountValue('1');
}
if (newDirection === 'THIS') {
setDraftAmountValue('');
}
if (newDirection === 'THIS') {
setDraftAmountValue('');
}
onChange?.({
direction: newDirection,
amount: amount,
unit: unit,
});
}}
options={RELATIVE_DATE_DIRECTION_SELECT_OPTIONS}
fullWidth
disabled={readonly}
/>
<SettingsTextInput
instanceId={`relative-date-picker-amount-${instanceId}`}
width={50}
value={draftAmountValue}
onChange={(text) => {
const amountString = text.replace(/[^0-9]|^0+/g, '');
setDraftAmountValue(amountString);
onChange?.({
direction: newDirection,
amount: amount,
unit: unit,
});
}}
options={RELATIVE_DATE_DIRECTION_SELECT_OPTIONS}
fullWidth
disabled={readonly}
/>
<SettingsTextInput
instanceId={`relative-date-picker-amount-${instanceId}`}
width={50}
value={draftAmountValue}
onChange={(text) => {
const amountString = text.replace(/[^0-9]|^0+/g, '');
setDraftAmountValue(amountString);
const amount = parseInt(amountString);
const amount = parseInt(amountString);
const valueParts = {
direction,
amount,
unit,
};
const valueParts = {
direction,
amount,
unit,
};
if (relativeDateFilterSchema.safeParse(valueParts).success === true) {
onChange?.(valueParts);
}
}}
placeholder={amountInputPlaceholder}
disabled={direction === 'THIS' || readonly}
/>
<Select
dropdownId={`unit-select-${instanceId}`}
value={unit}
onChange={(newUnit) => {
if (direction !== 'THIS' && amount === undefined) {
return;
}
if (
relativeDateFilterSchema.safeParse(valueParts).success === true
) {
onChange?.(valueParts);
}
}}
placeholder={amountInputPlaceholder}
disabled={direction === 'THIS' || readonly}
/>
<Select
dropdownId={`unit-select-${instanceId}`}
value={unit}
onChange={(newUnit) => {
if (direction !== 'THIS' && amount === undefined) {
return;
}
if (draftAmountValue === '' && direction !== 'THIS') {
setDraftAmountValue('1');
}
if (draftAmountValue === '' && direction !== 'THIS') {
setDraftAmountValue('1');
}
onChange?.({
direction,
amount: amount,
unit: newUnit,
});
}}
fullWidth
options={unitSelectOptions}
disabled={readonly}
dropdownWidth={unitDropdownWidth}
/>
onChange?.({
direction,
amount: amount,
unit: newUnit,
});
}}
fullWidth
options={unitSelectOptions}
disabled={readonly}
dropdownWidth={unitDropdownWidth}
/>
</StyledControlsRow>
{isDefined(calendarMonthDate) &&
isDefined(onPreviousMonth) &&
isDefined(onNextMonth) && (
<RelativeDatePickerCalendarNavigation
monthLabelDate={calendarMonthDate}
onPreviousMonth={onPreviousMonth}
onNextMonth={onNextMonth}
prevMonthButtonDisabled={prevMonthButtonDisabled ?? false}
nextMonthButtonDisabled={nextMonthButtonDisabled ?? false}
/>
)}
</StyledContainer>
);
};
@@ -0,0 +1,56 @@
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { styled } from '@linaria/react';
import { type Temporal } from 'temporal-polyfill';
import { SOURCE_LOCALE } from 'twenty-shared/translations';
import { IconArrowDown } from 'twenty-ui/icon';
import { themeCssVariables } from 'twenty-ui/theme-constants';
const StyledContainer = styled.div`
align-items: center;
color: ${themeCssVariables.font.color.tertiary};
display: flex;
flex-direction: column;
gap: ${themeCssVariables.spacing[2]};
justify-content: center;
min-height: 96px;
padding: ${themeCssVariables.spacing[4]} ${themeCssVariables.spacing[2]};
text-align: center;
`;
const StyledBound = styled.span`
color: ${themeCssVariables.font.color.primary};
font-size: ${themeCssVariables.font.size.md};
font-weight: ${themeCssVariables.font.weight.medium};
`;
type RelativeDateTimeRangeTextProps = {
start: Temporal.ZonedDateTime;
end: Temporal.ZonedDateTime;
};
export const RelativeDateTimeRangeText = ({
start,
end,
}: RelativeDateTimeRangeTextProps) => {
const currentWorkspaceMember = useAtomStateValue(currentWorkspaceMemberState);
const userLocale = currentWorkspaceMember?.locale ?? SOURCE_LOCALE;
const formatter = new Intl.DateTimeFormat(userLocale, {
dateStyle: 'medium',
timeStyle: 'short',
timeZone: start.timeZoneId,
});
return (
<StyledContainer>
<StyledBound>
{formatter.format(new Date(start.epochMilliseconds))}
</StyledBound>
<IconArrowDown size={14} color={themeCssVariables.font.color.tertiary} />
<StyledBound>
{formatter.format(new Date(end.epochMilliseconds))}
</StyledBound>
</StyledContainer>
);
};
@@ -3,12 +3,6 @@ import { themeCssVariables } from 'twenty-ui/theme-constants';
export const DATE_PICKER_CONTAINER_WIDTH = 280;
// Shared react-datepicker overrides for every internal date picker
// (DatePicker, DateTimePicker, DatePickerWithoutCalendar). Keep the calendar
// grid styling in one place so day/day-name spacing stays consistent across all
// pickers instead of drifting between per-component copies.
// - calendarDisabled: dims and freezes the grid (used for relative date mode)
// - hideCalendar: hides the day grid, keeping only the month/year header
export const StyledDatePickerContainer = styled.div<{
calendarDisabled?: boolean;
hideCalendar?: boolean;
@@ -240,7 +234,10 @@ export const StyledDatePickerContainer = styled.div<{
color: ${themeCssVariables.font.color.primary};
}
& .react-datepicker__day--selected {
& .react-datepicker__day--selected,
& .react-datepicker__day--in-range,
& .react-datepicker__day--range-start,
& .react-datepicker__day--range-end {
background-color: ${themeCssVariables.color.blue};
color: ${themeCssVariables.background.primary};
@@ -9,6 +9,22 @@ const INITIAL_DATE = Temporal.ZonedDateTime.from(
'2023-01-01T02:00:00+00:00[UTC]',
);
const RELATIVE_CALENDAR_RANGE = {
direction: 'PAST' as const,
amount: 1,
unit: 'MONTH' as const,
start: Temporal.ZonedDateTime.from('2022-12-01T00:00:00+00:00[UTC]'),
end: Temporal.ZonedDateTime.from('2023-01-01T00:00:00+00:00[UTC]'),
};
const RELATIVE_SUB_DAY_RANGE = {
direction: 'PAST' as const,
amount: 3,
unit: 'HOUR' as const,
start: Temporal.ZonedDateTime.from('2023-01-01T00:00:00+00:00[UTC]'),
end: Temporal.ZonedDateTime.from('2023-01-01T03:00:00+00:00[UTC]'),
};
const DateTimePickerStory = () => {
const [date, setDate] = useState<Temporal.ZonedDateTime | null>(INITIAL_DATE);
@@ -125,3 +141,29 @@ export const WithTimeInput: Story = {
expect(timeInput).toBeInTheDocument();
},
};
export const RelativeWithCalendarRange: Story = {
render: () => (
<DateTimePicker
instanceId="story-relative-date-time-picker"
date={null}
isRelative
relativeDate={RELATIVE_CALENDAR_RANGE}
onChange={() => {}}
onRelativeDateChange={() => {}}
/>
),
};
export const RelativeWithSubDayText: Story = {
render: () => (
<DateTimePicker
instanceId="story-relative-sub-day-picker"
date={null}
isRelative
relativeDate={RELATIVE_SUB_DAY_RANGE}
onChange={() => {}}
onRelativeDateChange={() => {}}
/>
),
};
@@ -1,15 +0,0 @@
import { RELATIVE_DATE_UNITS_SELECT_OPTIONS } from '@/ui/input/components/internal/date/constants/RelativeDateUnitSelectOptions';
import { type RelativeDateFilterUnit } from 'twenty-shared/utils';
type RelativeDateUnitOption = {
value: RelativeDateFilterUnit;
label: string;
};
export const RELATIVE_DATETIME_UNITS_SELECT_OPTIONS: RelativeDateUnitOption[] =
[
...RELATIVE_DATE_UNITS_SELECT_OPTIONS,
{ value: 'HOUR', label: 'Hour' },
{ value: 'MINUTE', label: 'Minute' },
{ value: 'SECOND', label: 'Second' },
];
@@ -0,0 +1,9 @@
import { RELATIVE_DATE_UNITS } from '@/ui/input/components/internal/date/constants/RelativeDateUnits';
import { type RelativeDateFilterUnit } from 'twenty-shared/utils';
export const RELATIVE_DATETIME_UNITS: RelativeDateFilterUnit[] = [
'SECOND',
'MINUTE',
'HOUR',
...RELATIVE_DATE_UNITS,
];
@@ -1,14 +0,0 @@
import { type RelativeDateFilterUnit } from 'twenty-shared/utils';
type RelativeDateUnitOption = {
value: RelativeDateFilterUnit;
label: string;
};
export const RELATIVE_DATE_UNITS_SELECT_OPTIONS: RelativeDateUnitOption[] = [
{ value: 'DAY', label: 'Day' },
{ value: 'WEEK', label: 'Week' },
{ value: 'MONTH', label: 'Month' },
{ value: 'QUARTER', label: 'Quarter' },
{ value: 'YEAR', label: 'Year' },
];
@@ -0,0 +1,9 @@
import { type RelativeDateFilterUnit } from 'twenty-shared/utils';
export const RELATIVE_DATE_UNITS: RelativeDateFilterUnit[] = [
'DAY',
'WEEK',
'MONTH',
'QUARTER',
'YEAR',
];
@@ -1,68 +0,0 @@
import { getHighlightedDates } from '@/ui/input/components/internal/date/utils/getHighlightedDates';
import { Temporal } from 'temporal-polyfill';
jest.useFakeTimers().setSystemTime(new Date('2024-10-01T00:00:00.000Z'));
const TIME_ZONE = 'UTC';
const getUTCPlainDateFromISO = (isoStringDate: string) => {
return Temporal.Instant.from(isoStringDate)
.toZonedDateTimeISO('UTC')
.toPlainDate();
};
describe('getHighlightedDates', () => {
it('should should return one day if range is one day', () => {
const dateRange = {
start: getUTCPlainDateFromISO('2024-10-12T00:00:00.000Z'),
end: getUTCPlainDateFromISO('2024-10-12T00:00:00.000Z'),
};
expect(
getHighlightedDates(dateRange.start, dateRange.end, TIME_ZONE),
).toEqual([getUTCPlainDateFromISO('2024-10-12T00:00:00.000Z')]);
});
it('should should return two days if range is 2 days', () => {
const dateRange = {
start: getUTCPlainDateFromISO('2024-10-12T00:00:00.000Z'),
end: getUTCPlainDateFromISO('2024-10-13T00:00:00.000Z'),
};
expect(
getHighlightedDates(dateRange.start, dateRange.end, TIME_ZONE),
).toEqual([
getUTCPlainDateFromISO('2024-10-12T00:00:00.000Z'),
getUTCPlainDateFromISO('2024-10-13T00:00:00.000Z'),
]);
});
it('should should return 10 days if range is 10 days', () => {
const dateRange = {
start: getUTCPlainDateFromISO('2024-10-12T00:00:00.000Z'),
end: getUTCPlainDateFromISO('2024-10-21T00:00:00.000Z'),
};
expect(
getHighlightedDates(dateRange.start, dateRange.end, TIME_ZONE),
).toEqual([
getUTCPlainDateFromISO('2024-10-12T00:00:00.000Z'),
getUTCPlainDateFromISO('2024-10-13T00:00:00.000Z'),
getUTCPlainDateFromISO('2024-10-14T00:00:00.000Z'),
getUTCPlainDateFromISO('2024-10-15T00:00:00.000Z'),
getUTCPlainDateFromISO('2024-10-16T00:00:00.000Z'),
getUTCPlainDateFromISO('2024-10-17T00:00:00.000Z'),
getUTCPlainDateFromISO('2024-10-18T00:00:00.000Z'),
getUTCPlainDateFromISO('2024-10-19T00:00:00.000Z'),
getUTCPlainDateFromISO('2024-10-20T00:00:00.000Z'),
getUTCPlainDateFromISO('2024-10-21T00:00:00.000Z'),
]);
});
it('should should return empty if range is 10 days but out of range', () => {
const dateRange = {
start: getUTCPlainDateFromISO('2023-10-01T00:00:00.000Z'),
end: getUTCPlainDateFromISO('2023-10-10T00:00:00.000Z'),
};
expect(
getHighlightedDates(dateRange.start, dateRange.end, TIME_ZONE),
).toEqual([]);
});
});
@@ -1,29 +0,0 @@
import { Temporal } from 'temporal-polyfill';
import { isPlainDateAfter, isPlainDateBefore } from 'twenty-shared/utils';
export const getHighlightedDates = (
start: Temporal.PlainDate,
end: Temporal.PlainDate,
timeZone: string,
): Temporal.PlainDate[] => {
const highlightedDates: Temporal.PlainDate[] = [];
const currentDate = Temporal.Now.zonedDateTimeISO(timeZone)
.startOfDay()
.toPlainDate();
const minDate = currentDate.subtract({ months: 2 });
const maxDate = currentDate.add({ months: 2 });
const startDate = isPlainDateBefore(start, minDate) ? minDate : start;
const lastDate = isPlainDateAfter(end, maxDate) ? maxDate : end;
let dateToHighlight = startDate;
while (isPlainDateBefore(dateToHighlight, lastDate.add({ days: 1 }))) {
highlightedDates.push(dateToHighlight);
dateToHighlight = dateToHighlight.add({ days: 1 });
}
return highlightedDates;
};
@@ -0,0 +1,27 @@
import { type Temporal } from 'temporal-polyfill';
import {
isDefined,
turnPlainDateToShiftedDateInSystemTimeZone,
} from 'twenty-shared/utils';
type RelativeDatePickerCalendarRange = {
startDate: Date | undefined;
endDate: Date | undefined;
rangeKey: string | undefined;
};
export const getRelativeDatePickerCalendarRange = (
startPlainDate: Temporal.PlainDate | null,
endInclusivePlainDate: Temporal.PlainDate | null,
): RelativeDatePickerCalendarRange => ({
startDate: isDefined(startPlainDate)
? turnPlainDateToShiftedDateInSystemTimeZone(startPlainDate)
: undefined,
endDate: isDefined(endInclusivePlainDate)
? turnPlainDateToShiftedDateInSystemTimeZone(endInclusivePlainDate)
: undefined,
rangeKey:
isDefined(startPlainDate) && isDefined(endInclusivePlainDate)
? `${startPlainDate.toString()}-${endInclusivePlainDate.toString()}`
: undefined,
});