Allow DATE_TIME IS operand to filter on a whole day (#17529)

Fixes:  https://github.com/twentyhq/core-team-issues/issues/2027

We've replaced the DATE_TIME picker with DATE picker, and changed the
logic to filter for complete day period.



https://github.com/user-attachments/assets/ba7e1078-bab3-4c62-a803-d6a851f14b7d

---------

Co-authored-by: Arun kumar <arunkumar@Aruns-MacBook-Air.local>
Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
This commit is contained in:
Arun
2026-02-18 19:14:05 +05:30
committed by GitHub
parent 88146c2170
commit 8e6b267ff3
15 changed files with 245 additions and 52 deletions
@@ -19,6 +19,7 @@ import {
import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
import { RecordFilterOperand } from '@/object-record/record-filter/types/RecordFilterOperand';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { useFeatureFlagsMap } from '@/workspace/hooks/useFeatureFlagsMap';
import { stringifyRelativeDateFilter } from '@/views/view-filter-value/utils/stringifyRelativeDateFilter';
import { WORKFLOW_TIMEZONE } from '@/workflow/constants/WorkflowTimeZone';
import { isObject, isString } from '@sniptt/guards';
@@ -62,6 +63,10 @@ export const AdvancedFilterCommandMenuValueFormInput = ({
const { applyObjectFilterDropdownFilterValue } =
useApplyObjectFilterDropdownFilterValue();
const featureFlags = useFeatureFlagsMap();
const isWholeDayFilterEnabled =
featureFlags.IS_DATE_TIME_WHOLE_DAY_FILTER_ENABLED ?? false;
const handleChange = (newValue: JsonValue) => {
if (isString(newValue)) {
applyObjectFilterDropdownFilterValue(newValue);
@@ -178,7 +183,12 @@ export const AdvancedFilterCommandMenuValueFormInput = ({
}
const field = {
type: recordFilter.type as FieldMetadataType,
type:
isWholeDayFilterEnabled === true &&
recordFilter.type === FieldMetadataType.DATE_TIME &&
recordFilter.operand === RecordFilterOperand.IS
? FieldMetadataType.DATE
: (recordFilter.type as FieldMetadataType),
label: '',
metadata: fieldDefinition?.metadata as FieldMetadata,
};
@@ -38,7 +38,12 @@ export const ObjectFilterDropdownDateInput = () => {
useApplyObjectFilterDropdownFilterValue();
const handleAbsoluteDateChange = (newPlainDate: string | null) => {
const newFilterValue = newPlainDate ?? '';
if (!isDefined(newPlainDate)) {
applyObjectFilterDropdownFilterValue('', '');
return;
}
const newFilterValue = newPlainDate;
// TODO: remove this and use getDisplayValue instead
const formattedDate = formatDateString({
@@ -91,6 +96,7 @@ export const ObjectFilterDropdownDateInput = () => {
? handleRelativeDateChange(null)
: handleAbsoluteDateChange(null);
};
const resolvedValue = objectFilterDropdownCurrentRecordFilter
? resolveDateFilter(objectFilterDropdownCurrentRecordFilter)
: null;
@@ -100,7 +106,7 @@ export const ObjectFilterDropdownDateInput = () => {
? resolvedValue
: undefined;
const plainDateValue =
const safePlainDateValue: string | undefined =
resolvedValue && typeof resolvedValue === 'string'
? resolvedValue
: undefined;
@@ -110,7 +116,7 @@ export const ObjectFilterDropdownDateInput = () => {
instanceId={`object-filter-dropdown-date-input`}
relativeDate={relativeDate}
isRelative={isRelativeOperand}
plainDateString={plainDateValue ?? null}
plainDateString={safePlainDateValue ?? null}
onChange={handleAbsoluteDateChange}
onRelativeDateChange={handleRelativeDateChange}
onClear={handleClear}
@@ -5,6 +5,7 @@ import { ObjectFilterDropdownRatingInput } from '@/object-record/object-filter-d
import { ObjectFilterDropdownRecordSelect } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownRecordSelect';
import { ObjectFilterDropdownSearchInput } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownSearchInput';
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
import { useFeatureFlagsMap } from '@/workspace/hooks/useFeatureFlagsMap';
import { ViewFilterOperand } from 'twenty-shared/types';
@@ -28,6 +29,10 @@ export const ObjectFilterDropdownFilterInput = ({
filterDropdownId,
recordFilterId,
}: ObjectFilterDropdownFilterInputProps) => {
const featureFlags = useFeatureFlagsMap();
const isWholeDayFilterEnabled =
featureFlags.IS_DATE_TIME_WHOLE_DAY_FILTER_ENABLED ?? false;
const fieldMetadataItemUsedInDropdown = useRecoilComponentValue(
fieldMetadataItemUsedInDropdownComponentSelector,
);
@@ -76,6 +81,18 @@ export const ObjectFilterDropdownFilterInput = ({
</>
);
} else if (filterType === 'DATE_TIME') {
if (
isWholeDayFilterEnabled &&
selectedOperandInDropdown === ViewFilterOperand.IS
) {
return (
<>
<ObjectFilterDropdownInnerSelectOperandDropdown />
<DropdownMenuSeparator />
<ObjectFilterDropdownDateInput />
</>
);
}
return (
<>
<ObjectFilterDropdownInnerSelectOperandDropdown />
@@ -1,3 +1,6 @@
import { isNonEmptyString } from '@sniptt/guards';
import { Temporal } from 'temporal-polyfill';
import { useUpsertObjectFilterDropdownCurrentFilter } from '@/object-record/object-filter-dropdown/hooks/useUpsertObjectFilterDropdownCurrentFilter';
import { fieldMetadataItemUsedInDropdownComponentSelector } from '@/object-record/object-filter-dropdown/states/fieldMetadataItemUsedInDropdownComponentSelector';
import { objectFilterDropdownCurrentRecordFilterComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownCurrentRecordFilterComponentState';
@@ -7,14 +10,12 @@ import { useGetRelativeDateFilterWithUserTimezone } from '@/object-record/record
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 { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState';
import { stringifyRelativeDateFilter } from '@/views/view-filter-value/utils/stringifyRelativeDateFilter';
import { isNonEmptyString } from '@sniptt/guards';
import { Temporal } from 'temporal-polyfill';
import { DEFAULT_RELATIVE_DATE_FILTER_VALUE } from 'twenty-shared/constants';
import { useFeatureFlagsMap } from '@/workspace/hooks/useFeatureFlagsMap';
import { DEFAULT_RELATIVE_DATE_FILTER_VALUE } from 'twenty-shared/constants';
import {
isDefined,
relativeDateFilterStringifiedSchema,
@@ -47,6 +48,10 @@ export const useApplyObjectFilterDropdownOperand = () => {
const { getRelativeDateFilterWithUserTimezone } =
useGetRelativeDateFilterWithUserTimezone();
const featureFlags = useFeatureFlagsMap();
const isWholeDayFilterEnabled =
featureFlags.IS_DATE_TIME_WHOLE_DAY_FILTER_ENABLED ?? false;
const applyObjectFilterDropdownOperand = (
newOperand: RecordFilterOperand,
) => {
@@ -106,7 +111,24 @@ export const useApplyObjectFilterDropdownOperand = () => {
recordFilterToUpsert.value,
);
if (filterValueIsEmpty || isStillRelativeFilterValue.success) {
const previousOperand =
objectFilterDropdownCurrentRecordFilter?.operand;
const isDateTimeOperandFormatChange =
recordFilterToUpsert.type === 'DATE_TIME' &&
!filterValueIsEmpty &&
!isStillRelativeFilterValue.success &&
(previousOperand === RecordFilterOperand.IS ||
newOperand === RecordFilterOperand.IS);
if (isDateTimeOperandFormatChange) {
recordFilterToUpsert.value = convertDateTimeFilterValue(
recordFilterToUpsert.value,
newOperand,
userTimezone,
isWholeDayFilterEnabled,
);
} else if (filterValueIsEmpty || isStillRelativeFilterValue.success) {
const zonedDateToUse = Temporal.Now.zonedDateTimeISO(userTimezone);
if (recordFilterToUpsert.type === 'DATE') {
@@ -116,11 +138,18 @@ export const useApplyObjectFilterDropdownOperand = () => {
recordFilterToUpsert.value = initialNowDateFilterValue;
} else {
const initialNowDateTimeFilterValue = zonedDateToUse
.toInstant()
.toString();
recordFilterToUpsert.value = initialNowDateTimeFilterValue;
if (
newOperand === RecordFilterOperand.IS &&
isWholeDayFilterEnabled
) {
recordFilterToUpsert.value = zonedDateToUse
.toPlainDate()
.toString();
} else {
recordFilterToUpsert.value = zonedDateToUse
.toInstant()
.toString();
}
}
}
}
@@ -137,3 +166,40 @@ export const useApplyObjectFilterDropdownOperand = () => {
applyObjectFilterDropdownOperand,
};
};
const convertDateTimeFilterValue = (
currentValue: string,
targetOperand: RecordFilterOperand,
userTimezone: string,
isWholeDayFilterEnabled = false,
): string => {
const zonedDateToUse = Temporal.Now.zonedDateTimeISO(userTimezone);
if (targetOperand === RecordFilterOperand.IS) {
try {
const existingZoned = currentValue.includes('T')
? Temporal.Instant.from(currentValue).toZonedDateTimeISO(userTimezone)
: Temporal.PlainDate.from(currentValue).toZonedDateTime(userTimezone);
if (isWholeDayFilterEnabled) {
return existingZoned.toPlainDate().toString();
} else {
return existingZoned.toInstant().toString();
}
} catch {
return zonedDateToUse.toPlainDate().toString();
}
} else {
try {
const existingPlainDate = Temporal.PlainDate.from(currentValue);
const currentTime = zonedDateToUse.toPlainTime();
const zonedFromPlain = existingPlainDate.toZonedDateTime({
timeZone: userTimezone,
plainTime: currentTime,
});
return zonedFromPlain.toInstant().toString();
} catch {
return zonedDateToUse.toInstant().toString();
}
}
};
@@ -1,9 +1,12 @@
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 { 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 { Temporal } from 'temporal-polyfill';
import { useFeatureFlagsMap } from '@/workspace/hooks/useFeatureFlagsMap';
import { type FilterableAndTSVectorFieldType } from 'twenty-shared/types';
const activeDatePickerOperands = [
@@ -16,6 +19,9 @@ export const useGetInitialFilterValue = () => {
const { userTimezone } = useUserTimezone();
const { getDateFilterDisplayValue } = useGetDateFilterDisplayValue();
const { getDateTimeFilterDisplayValue } = useGetDateTimeFilterDisplayValue();
const featureFlags = useFeatureFlagsMap();
const isWholeDayFilterEnabled =
featureFlags.IS_DATE_TIME_WHOLE_DAY_FILTER_ENABLED ?? false;
const getInitialFilterValue = (
newType: FilterableAndTSVectorFieldType,
@@ -44,6 +50,17 @@ export const useGetInitialFilterValue = () => {
alreadyExistingZonedDateTime ??
Temporal.Now.zonedDateTimeISO(userTimezone);
if (
isWholeDayFilterEnabled === true &&
newOperand === RecordFilterOperand.IS
) {
const value = referenceDate.toPlainDate().toString();
const { displayValue } = getDateFilterDisplayValue(referenceDate);
return { value, displayValue };
}
const value = referenceDate.toInstant().toString();
const { displayValue } = getDateTimeFilterDisplayValue(referenceDate);
@@ -1,3 +1,6 @@
import { isNonEmptyString } from '@sniptt/guards';
import { Temporal } from 'temporal-polyfill';
import { useGetFieldMetadataItemByIdOrThrow } from '@/object-metadata/hooks/useGetFieldMetadataItemById';
import { useGetDateFilterDisplayValue } from '@/object-record/object-filter-dropdown/hooks/useGetDateFilterDisplayValue';
import { useGetDateTimeFilterDisplayValue } from '@/object-record/object-filter-dropdown/hooks/useGetDateTimeFilterDisplayValue';
@@ -7,8 +10,7 @@ import { RecordFilterOperand } from '@/object-record/record-filter/types/RecordF
import { isRecordFilterConsideredEmpty } from '@/object-record/record-filter/utils/isRecordFilterConsideredEmpty';
import { useUserTimezone } from '@/ui/input/components/internal/date/hooks/useUserTimezone';
import { getTimezoneAbbreviationForZonedDateTime } from '@/ui/input/components/internal/date/utils/getTimeZoneAbbreviationForZonedDateTime';
import { isNonEmptyString } from '@sniptt/guards';
import { Temporal } from 'temporal-polyfill';
import { type Nullable } from 'twenty-shared/types';
import {
isDefined,
@@ -87,7 +89,23 @@ export const useGetRecordFilterDisplayValue = () => {
}
} else if (recordFilter.type === 'DATE_TIME') {
switch (recordFilter.operand) {
case RecordFilterOperand.IS:
case RecordFilterOperand.IS: {
if (!isNonEmptyString(recordFilter.value)) {
return '';
}
const zonedDateTime = recordFilter.value.includes('T')
? Temporal.Instant.from(recordFilter.value).toZonedDateTimeISO(
userTimezone,
)
: Temporal.PlainDate.from(recordFilter.value).toZonedDateTime(
userTimezone,
);
const { displayValue } = getDateFilterDisplayValue(zonedDateTime);
return `${displayValue}`;
}
case RecordFilterOperand.IS_AFTER:
case RecordFilterOperand.IS_BEFORE: {
if (!isNonEmptyString(recordFilter.value)) {
@@ -1024,9 +1024,9 @@ describe('should work as expected for the different field types', () => {
const dateFilterIs: RecordFilter = {
id: 'company-date-filter-is',
value: '2024-09-17T20:46:58.922Z',
value: '2024-09-17',
fieldMetadataId: companyMockDateFieldMetadataId?.id,
displayValue: '2024-09-17T20:46:58.922Z',
displayValue: '2024-09-17',
operand: ViewFilterOperand.IS,
label: 'Created At',
type: FieldMetadataType.DATE_TIME,
@@ -1081,12 +1081,12 @@ describe('should work as expected for the different field types', () => {
and: [
{
createdAt: {
lt: '2024-09-17T20:47:00Z',
gte: '2024-09-16T22:00:00Z',
},
},
{
createdAt: {
gte: '2024-09-17T20:46:00Z',
lt: '2024-09-17T22:00:00Z',
},
},
],