diff --git a/packages/twenty-front/src/generated-metadata/graphql.ts b/packages/twenty-front/src/generated-metadata/graphql.ts
index 3e072c3092..3eb5ef11e5 100644
--- a/packages/twenty-front/src/generated-metadata/graphql.ts
+++ b/packages/twenty-front/src/generated-metadata/graphql.ts
@@ -1480,6 +1480,7 @@ export enum FeatureFlagKey {
IS_COMMAND_MENU_ITEM_ENABLED = 'IS_COMMAND_MENU_ITEM_ENABLED',
IS_CORE_PICTURE_MIGRATED = 'IS_CORE_PICTURE_MIGRATED',
IS_DASHBOARD_V2_ENABLED = 'IS_DASHBOARD_V2_ENABLED',
+ IS_DATE_TIME_WHOLE_DAY_FILTER_ENABLED = 'IS_DATE_TIME_WHOLE_DAY_FILTER_ENABLED',
IS_DRAFT_EMAIL_ENABLED = 'IS_DRAFT_EMAIL_ENABLED',
IS_EMAILING_DOMAIN_ENABLED = 'IS_EMAILING_DOMAIN_ENABLED',
IS_FILES_FIELD_MIGRATED = 'IS_FILES_FIELD_MIGRATED',
diff --git a/packages/twenty-front/src/modules/object-record/advanced-filter/command-menu/components/AdvancedFilterCommandMenuValueFormInput.tsx b/packages/twenty-front/src/modules/object-record/advanced-filter/command-menu/components/AdvancedFilterCommandMenuValueFormInput.tsx
index 153cd26260..89891f65a2 100644
--- a/packages/twenty-front/src/modules/object-record/advanced-filter/command-menu/components/AdvancedFilterCommandMenuValueFormInput.tsx
+++ b/packages/twenty-front/src/modules/object-record/advanced-filter/command-menu/components/AdvancedFilterCommandMenuValueFormInput.tsx
@@ -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,
};
diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownDateInput.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownDateInput.tsx
index f56dbd328b..3c06332dd5 100644
--- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownDateInput.tsx
+++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownDateInput.tsx
@@ -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}
diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterInput.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterInput.tsx
index e76d838b7e..906f14155b 100644
--- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterInput.tsx
+++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterInput.tsx
@@ -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 (
+ <>
+
+
+
+ >
+ );
+ }
return (
<>
diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/hooks/useApplyObjectFilterDropdownOperand.ts b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/hooks/useApplyObjectFilterDropdownOperand.ts
index 884a60d3d3..e46c6c7a09 100644
--- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/hooks/useApplyObjectFilterDropdownOperand.ts
+++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/hooks/useApplyObjectFilterDropdownOperand.ts
@@ -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();
+ }
+ }
+};
diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/hooks/useGetInitialFilterValue.ts b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/hooks/useGetInitialFilterValue.ts
index 5e1af325fa..d3d6e164fe 100644
--- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/hooks/useGetInitialFilterValue.ts
+++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/hooks/useGetInitialFilterValue.ts
@@ -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);
diff --git a/packages/twenty-front/src/modules/object-record/record-filter/hooks/useGetRecordFilterDisplayValue.ts b/packages/twenty-front/src/modules/object-record/record-filter/hooks/useGetRecordFilterDisplayValue.ts
index c575521656..194032d936 100644
--- a/packages/twenty-front/src/modules/object-record/record-filter/hooks/useGetRecordFilterDisplayValue.ts
+++ b/packages/twenty-front/src/modules/object-record/record-filter/hooks/useGetRecordFilterDisplayValue.ts
@@ -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)) {
diff --git a/packages/twenty-front/src/modules/object-record/record-filter/utils/__tests__/computeViewRecordGqlOperationFilter.test.ts b/packages/twenty-front/src/modules/object-record/record-filter/utils/__tests__/computeViewRecordGqlOperationFilter.test.ts
index 6a5e54279b..373eca889d 100644
--- a/packages/twenty-front/src/modules/object-record/record-filter/utils/__tests__/computeViewRecordGqlOperationFilter.test.ts
+++ b/packages/twenty-front/src/modules/object-record/record-filter/utils/__tests__/computeViewRecordGqlOperationFilter.test.ts
@@ -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',
},
},
],
diff --git a/packages/twenty-server/src/engine/core-modules/feature-flag/enums/feature-flag-key.enum.ts b/packages/twenty-server/src/engine/core-modules/feature-flag/enums/feature-flag-key.enum.ts
index 3d1e882988..436f8cab6d 100644
--- a/packages/twenty-server/src/engine/core-modules/feature-flag/enums/feature-flag-key.enum.ts
+++ b/packages/twenty-server/src/engine/core-modules/feature-flag/enums/feature-flag-key.enum.ts
@@ -20,6 +20,7 @@ export enum FeatureFlagKey {
IS_SSE_DB_EVENTS_ENABLED = 'IS_SSE_DB_EVENTS_ENABLED',
IS_COMMAND_MENU_ITEM_ENABLED = 'IS_COMMAND_MENU_ITEM_ENABLED',
IS_NAVIGATION_MENU_ITEM_ENABLED = 'IS_NAVIGATION_MENU_ITEM_ENABLED',
+ IS_DATE_TIME_WHOLE_DAY_FILTER_ENABLED = 'IS_DATE_TIME_WHOLE_DAY_FILTER_ENABLED',
IS_NAVIGATION_MENU_ITEM_EDITING_ENABLED = 'IS_NAVIGATION_MENU_ITEM_EDITING_ENABLED',
IS_DRAFT_EMAIL_ENABLED = 'IS_DRAFT_EMAIL_ENABLED',
}
diff --git a/packages/twenty-server/src/engine/metadata-modules/view/constants/default-timezone.constant.ts b/packages/twenty-server/src/engine/metadata-modules/view/constants/default-timezone.constant.ts
new file mode 100644
index 0000000000..ab8d748873
--- /dev/null
+++ b/packages/twenty-server/src/engine/metadata-modules/view/constants/default-timezone.constant.ts
@@ -0,0 +1 @@
+export const DEFAULT_TIMEZONE = 'UTC';
diff --git a/packages/twenty-server/src/engine/metadata-modules/view/services/__tests__/view-query-params.service.spec.ts b/packages/twenty-server/src/engine/metadata-modules/view/services/__tests__/view-query-params.service.spec.ts
index 4a2af03071..1198594b58 100644
--- a/packages/twenty-server/src/engine/metadata-modules/view/services/__tests__/view-query-params.service.spec.ts
+++ b/packages/twenty-server/src/engine/metadata-modules/view/services/__tests__/view-query-params.service.spec.ts
@@ -12,6 +12,7 @@ import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadat
import { ViewSortDirection } from 'src/engine/metadata-modules/view-sort/enums/view-sort-direction';
import { ViewQueryParamsService } from 'src/engine/metadata-modules/view/services/view-query-params.service';
import { ViewService } from 'src/engine/metadata-modules/view/services/view.service';
+import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
describe('ViewQueryParamsService', () => {
let viewQueryParamsService: ViewQueryParamsService;
@@ -73,6 +74,12 @@ describe('ViewQueryParamsService', () => {
getOrRecomputeManyOrAllFlatEntityMaps: jest.fn(),
},
},
+ {
+ provide: GlobalWorkspaceOrmManager,
+ useValue: {
+ getRepository: jest.fn(),
+ },
+ },
],
}).compile();
diff --git a/packages/twenty-server/src/engine/metadata-modules/view/services/view-query-params.service.ts b/packages/twenty-server/src/engine/metadata-modules/view/services/view-query-params.service.ts
index 70b586826f..eaa9afeddf 100644
--- a/packages/twenty-server/src/engine/metadata-modules/view/services/view-query-params.service.ts
+++ b/packages/twenty-server/src/engine/metadata-modules/view/services/view-query-params.service.ts
@@ -20,7 +20,10 @@ import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadat
import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
import { ViewSortDirection } from 'src/engine/metadata-modules/view-sort/enums/view-sort-direction';
+import { DEFAULT_TIMEZONE } from 'src/engine/metadata-modules/view/constants/default-timezone.constant';
import { ViewService } from 'src/engine/metadata-modules/view/services/view.service';
+import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
+import { type WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
export type ViewQueryParams = {
objectNameSingular: string;
@@ -35,6 +38,7 @@ export class ViewQueryParamsService {
constructor(
private readonly viewService: ViewService,
private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
+ private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
) {}
async resolveViewToQueryParams(
@@ -61,6 +65,11 @@ export class ViewQueryParamsService {
flatEntityMaps: flatObjectMetadataMaps,
});
+ const timeZone = await this.getWorkspaceMemberTimezoneIfAvailable(
+ workspaceId,
+ currentWorkspaceMemberId,
+ );
+
const recordFilters: RecordFilter[] = (view.viewFilters ?? [])
.map((viewFilter) => {
const field = findFlatEntityByIdInFlatEntityMaps({
@@ -122,7 +131,7 @@ export class ViewQueryParamsService {
fields,
recordFilters,
recordFilterGroups,
- filterValueDependencies: { currentWorkspaceMemberId, timeZone: 'UTC' }, // TODO: check if we need to put workspace member timezone here
+ filterValueDependencies: { currentWorkspaceMemberId, timeZone },
});
const orderBy: ObjectRecordOrderBy = (view.viewSorts ?? [])
@@ -151,4 +160,29 @@ export class ViewQueryParamsService {
viewType: view.type,
};
}
+
+ private async getWorkspaceMemberTimezoneIfAvailable(
+ workspaceId: string,
+ currentWorkspaceMemberId?: string,
+ ): Promise {
+ if (!isDefined(currentWorkspaceMemberId)) {
+ return DEFAULT_TIMEZONE;
+ }
+
+ try {
+ const workspaceMemberRepository =
+ await this.globalWorkspaceOrmManager.getRepository(
+ workspaceId,
+ 'workspaceMember',
+ );
+
+ const workspaceMember = await workspaceMemberRepository.findOne({
+ where: { id: currentWorkspaceMemberId },
+ });
+
+ return workspaceMember?.timeZone ?? DEFAULT_TIMEZONE;
+ } catch {
+ return DEFAULT_TIMEZONE;
+ }
+ }
}
diff --git a/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.spec.ts b/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.spec.ts
index e6b6950716..536561ffa9 100644
--- a/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.spec.ts
+++ b/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.spec.ts
@@ -243,6 +243,7 @@ describe('WorkspaceEntityManager', () => {
IS_APPLICATION_INSTALLATION_FROM_TARBALL_ENABLED: false,
IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED: false,
IS_MARKETPLACE_ENABLED: false,
+ IS_DATE_TIME_WHOLE_DAY_FILTER_ENABLED: false,
IS_FILES_FIELD_MIGRATED: false,
IS_DRAFT_EMAIL_ENABLED: false,
IS_CORE_PICTURE_MIGRATED: false,
diff --git a/packages/twenty-server/src/engine/workspace-manager/dev-seeder/core/utils/seed-feature-flags.util.ts b/packages/twenty-server/src/engine/workspace-manager/dev-seeder/core/utils/seed-feature-flags.util.ts
index 1bb9d1cbc4..4c5c5585b7 100644
--- a/packages/twenty-server/src/engine/workspace-manager/dev-seeder/core/utils/seed-feature-flags.util.ts
+++ b/packages/twenty-server/src/engine/workspace-manager/dev-seeder/core/utils/seed-feature-flags.util.ts
@@ -101,6 +101,11 @@ export const seedFeatureFlags = async ({
workspaceId: workspaceId,
value: true,
},
+ {
+ key: FeatureFlagKey.IS_DATE_TIME_WHOLE_DAY_FILTER_ENABLED,
+ workspaceId: workspaceId,
+ value: true,
+ },
{
key: FeatureFlagKey.IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED,
workspaceId: workspaceId,
diff --git a/packages/twenty-shared/src/utils/filter/turnRecordFilterIntoGqlOperationFilter.ts b/packages/twenty-shared/src/utils/filter/turnRecordFilterIntoGqlOperationFilter.ts
index c4edb41954..93a3675b55 100644
--- a/packages/twenty-shared/src/utils/filter/turnRecordFilterIntoGqlOperationFilter.ts
+++ b/packages/twenty-shared/src/utils/filter/turnRecordFilterIntoGqlOperationFilter.ts
@@ -1,4 +1,5 @@
import { isNonEmptyString } from '@sniptt/guards';
+import { Temporal } from 'temporal-polyfill';
import {
FieldMetadataType,
@@ -52,7 +53,6 @@ import {
import { arrayOfStringsOrVariablesSchema } from '@/utils/filter/utils/validation-schemas/arrayOfStringsOrVariablesSchema';
import { arrayOfUuidOrVariableSchema } from '@/utils/filter/utils/validation-schemas/arrayOfUuidsOrVariablesSchema';
import { jsonRelationFilterValueSchema } from '@/utils/filter/utils/validation-schemas/jsonRelationFilterValueSchema';
-import { Temporal } from 'temporal-polyfill';
type FieldShared = {
id: string;
@@ -390,6 +390,43 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
throw new Error(`Date filter is empty`);
}
+ if (recordFilter.operand === RecordFilterOperand.IS) {
+ const timeZone = filterValueDependencies.timeZone ?? 'UTC';
+
+ let parsedPlainDate = null;
+
+ try {
+ parsedPlainDate = recordFilter.value.includes('T')
+ ? Temporal.Instant.from(recordFilter.value)
+ .toZonedDateTimeISO(timeZone)
+ .toPlainDate()
+ : Temporal.PlainDate.from(recordFilter.value);
+ } catch {
+ throw new Error(
+ `Cannot parse "${recordFilter.value}" for ${filterType} filter`,
+ );
+ }
+
+ const zonedDateTime = parsedPlainDate.toZonedDateTime(timeZone);
+ const start = zonedDateTime.toInstant();
+ const end = zonedDateTime.add({ days: 1 }).toInstant();
+
+ return {
+ and: [
+ {
+ [correspondingFieldMetadataItem.name]: {
+ gte: start.toString(),
+ } as DateTimeFilter,
+ },
+ {
+ [correspondingFieldMetadataItem.name]: {
+ lt: end.toString(),
+ } as DateTimeFilter,
+ },
+ ],
+ };
+ }
+
const resolvedDateTime = Temporal.Instant.from(recordFilter.value);
switch (recordFilter.operand) {
@@ -407,34 +444,6 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
} as DateTimeFilter,
};
}
- case RecordFilterOperand.IS: {
- const start = resolvedDateTime
- .toZonedDateTimeISO('UTC')
- .with({
- second: 0,
- millisecond: 0,
- microsecond: 0,
- nanosecond: 0,
- })
- .toInstant();
-
- const end = start.add({ minutes: 1 });
-
- return {
- and: [
- {
- [correspondingFieldMetadataItem.name]: {
- lt: end.toString(),
- } as DateTimeFilter,
- },
- {
- [correspondingFieldMetadataItem.name]: {
- gte: start.toString(),
- } as DateTimeFilter,
- },
- ],
- };
- }
}
}