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, - }, - ], - }; - } } }