Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
"semver": "^7.5.4",
"slash": "^5.1.0",
"storybook-addon-mock-date": "^0.6.0",
"temporal-polyfill": "^0.3.0",
"ts-key-enum": "^2.0.12",
"tslib": "^2.8.1",
"type-fest": "4.10.1",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,27 @@ import { fieldMetadataItemUsedInDropdownComponentSelector } from '@/object-recor
import { objectFilterDropdownCurrentRecordFilterComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownCurrentRecordFilterComponentState';
import { selectedOperandInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/selectedOperandInDropdownComponentState';
import { getRelativeDateDisplayValue } from '@/object-record/object-filter-dropdown/utils/getRelativeDateDisplayValue';
import { DateTimePicker } from '@/ui/input/components/internal/date/components/InternalDatePicker';
import { DateTimePicker } from '@/ui/input/components/internal/date/components/DateTimePicker';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { UserContext } from '@/users/contexts/UserContext';
import { computeVariableDateViewFilterValue } from '@/views/view-filter-value/utils/computeVariableDateViewFilterValue';
import { useState } from 'react';
import { useContext, useState } from 'react';
import { useRecoilValue } from 'recoil';
import {
ViewFilterOperand,
type VariableDateViewFilterValueDirection,
type VariableDateViewFilterValueUnit,
} from 'twenty-shared/types';
import { isDefined, resolveDateViewFilterValue } from 'twenty-shared/utils';
import { FieldMetadataType } from '~/generated-metadata/graphql';
import { dateLocaleState } from '~/localization/states/dateLocaleState';
import { formatDateString } from '~/utils/string/formatDateString';
import { formatDateTimeString } from '~/utils/string/formatDateTimeString';

export const ObjectFilterDropdownDateInput = () => {
const { dateFormat, timeFormat, timeZone } = useContext(UserContext);
const dateLocale = useRecoilValue(dateLocaleState);

const fieldMetadataItemUsedInDropdown = useRecoilComponentValue(
fieldMetadataItemUsedInDropdownComponentSelector,
);
Expand Down Expand Up @@ -46,10 +54,26 @@ export const ObjectFilterDropdownDateInput = () => {
setInternalDate(newDate);

const newFilterValue = newDate?.toISOString() ?? '';

const formattedDateTime = formatDateTimeString({
value: newDate?.toISOString(),
timeZone,
Comment on lines 46 to +60

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: The date filter value is created with newDate.toISOString(), which can cause the date to shift by a day for users in non-UTC timezones, leading to incorrect filtering.
(Severity: High 0.70 | Confidence: 0.95)

🔍 Detailed Analysis

When a user in a non-UTC timezone selects a date for a filter, the value is generated using newDate.toISOString(). This converts the locally constructed Date object to a UTC string, which can shift the date by one day (e.g., January 1st becomes December 31st). This leads to incorrect filter results, as the backend receives a different date than the one selected by the user. Other parts of the application correctly handle this by constructing the date with new Date(Date.UTC(...)) to prevent such timezone-related mismatches.

💡 Suggested Fix

To ensure the date is not affected by the user's local timezone, construct the date value using Date.UTC. Replace newDate?.toISOString() with newDate ? new Date(Date.UTC(newDate.getFullYear(), newDate.getMonth(), newDate.getDate())).toISOString() : ''.

🤖 Prompt for AI Agent
Fix this bug. In
packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownDateInput.tsx
at lines 54-60: When a user in a non-UTC timezone selects a date for a filter, the value
is generated using `newDate.toISOString()`. This converts the locally constructed `Date`
object to a UTC string, which can shift the date by one day (e.g., January 1st becomes
December 31st). This leads to incorrect filter results, as the backend receives a
different date than the one selected by the user. Other parts of the application
correctly handle this by constructing the date with `new Date(Date.UTC(...))` to prevent
such timezone-related mismatches.

Did we get this right? 👍 / 👎 to inform future reviews.

dateFormat,
timeFormat,
localeCatalog: dateLocale.localeCatalog,
});

const formattedDate = formatDateString({
value: newDate?.toISOString(),
timeZone,
dateFormat,
localeCatalog: dateLocale.localeCatalog,
});

const newDisplayValue = isDefined(newDate)
? isDateTimeInput
? newDate.toLocaleString()
: newDate.toLocaleDateString()
? formattedDateTime
: formattedDate
: '';

applyObjectFilterDropdownFilterValue(newFilterValue, newDisplayValue);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ export const RecordBoardCardCellEditModePortal = () => {

return (
<RecordInlineCellAnchoredPortal
position={editModePosition}
fieldMetadataItem={editedFieldMetadataItem}
objectMetadataItem={objectMetadataItem}
recordId={recordId}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';

import { RecordBoardContext } from '@/object-record/record-board/contexts/RecordBoardContext';
import { RecordBoardCardCellHoveredPortalContent } from '@/object-record/record-board/record-board-card/anchored-portal/components/RecordBoardCardCellHoveredPortalContent';
import { RecordBoardCardInputContextProvider } from '@/object-record/record-board/record-board-card/anchored-portal/components/RecordBoardCardInputContextProvider';
import { RECORD_BOARD_CARD_INPUT_ID_PREFIX } from '@/object-record/record-board/record-board-card/constants/RecordBoardCardInputIdPrefix';
import { RecordBoardCardContext } from '@/object-record/record-board/record-board-card/contexts/RecordBoardCardContext';
import { useRecordBoardCardMetadataFromPosition } from '@/object-record/record-board/record-board-card/hooks/useRecordBoardCardMetadataFromPosition';
import { recordBoardCardHoverPositionComponentState } from '@/object-record/record-board/record-board-card/states/recordBoardCardHoverPositionComponentState';
import { RecordInlineCellAnchoredPortal } from '@/object-record/record-inline-cell/components/RecordInlineCellAnchoredPortal';
import { useContext } from 'react';
import { isDefined } from 'twenty-shared/utils';
Expand All @@ -15,19 +12,14 @@ export const RecordBoardCardCellHoveredPortal = () => {
const { objectMetadataItem } = useContext(RecordBoardContext);
const { recordId } = useContext(RecordBoardCardContext);

const hoverPosition = useRecoilComponentValue(
recordBoardCardHoverPositionComponentState,
);

const { hoveredFieldMetadataItem } = useRecordBoardCardMetadataFromPosition();

if (!isDefined(hoverPosition) || !isDefined(hoveredFieldMetadataItem)) {
if (!isDefined(hoveredFieldMetadataItem)) {
return null;
}

return (
<RecordInlineCellAnchoredPortal
position={hoverPosition}
fieldMetadataItem={hoveredFieldMetadataItem}
objectMetadataItem={objectMetadataItem}
recordId={recordId}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { RecordCalendarComponentInstanceContext } from '@/object-record/record-calendar/states/contexts/RecordCalendarComponentInstanceContext';
import { recordCalendarSelectedDateComponentState } from '@/object-record/record-calendar/states/recordCalendarSelectedDateComponentState';
import { recordIndexCalendarLayoutState } from '@/object-record/record-index/states/recordIndexCalendarLayoutState';
import { DateTimePicker } from '@/ui/input/components/internal/date/components/InternalDatePicker';
import { DateTimePicker } from '@/ui/input/components/internal/date/components/DateTimePicker';
import { Select } from '@/ui/input/components/Select';
import { SelectControl } from '@/ui/input/components/SelectControl';
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { RecordCalendarMonthBodyWeek } from '@/object-record/record-calendar/mon
import { useRecordCalendarMonthContextOrThrow } from '@/object-record/record-calendar/month/contexts/RecordCalendarMonthContext';
import styled from '@emotion/styled';
import { format } from 'date-fns';
import { DATE_TYPE_FORMAT } from 'twenty-shared/constants';

const StyledContainer = styled.div`
display: flex;
Expand All @@ -19,7 +20,7 @@ export const RecordCalendarMonthBody = () => {
<StyledContainer>
{weekFirstDays.map((weekFirstDay) => (
<RecordCalendarMonthBodyWeek
key={`week-${format(weekFirstDay, 'yyyy-MM-dd')}`}
key={`week-${format(weekFirstDay, DATE_TYPE_FORMAT)}`}
startDayOfWeek={weekFirstDay}
/>
))}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import styled from '@emotion/styled';
import { Droppable } from '@hello-pangea/dnd';
import { format, isSameDay, isSameMonth, isWeekend } from 'date-fns';
import { useState } from 'react';
import { DATE_TYPE_FORMAT } from 'twenty-shared/constants';
import { RecordCalendarAddNew } from '../../components/RecordCalendarAddNew';

const StyledContainer = styled.div<{
Expand Down Expand Up @@ -103,7 +104,7 @@ export const RecordCalendarMonthBodyDay = ({
recordCalendarSelectedDateComponentState,
);

const dayKey = format(day, 'yyyy-MM-dd');
const dayKey = format(day, DATE_TYPE_FORMAT);

const recordIds = useRecoilComponentFamilyValue(
calendarDayRecordIdsComponentFamilySelector,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ export const RecordCalendarCardCellEditModePortal = ({

return (
<RecordInlineCellAnchoredPortal
position={editModePosition}
fieldMetadataItem={editedFieldMetadataItem}
objectMetadataItem={objectMetadataItem}
recordId={recordId}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ export const RecordCalendarCardCellHoveredPortal = ({

return (
<RecordInlineCellAnchoredPortal
position={hoverPosition}
fieldMetadataItem={hoveredFieldMetadataItem}
objectMetadataItem={objectMetadataItem}
recordId={recordId}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@ export const RecordFieldListCellEditModePortal = ({

return (
<RecordInlineCellAnchoredPortal
position={editModePosition}
fieldMetadataItem={editedFieldMetadataItem}
objectMetadataItem={objectMetadataItem}
recordId={recordId}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ export const RecordFieldListCellHoveredPortal = ({

return (
<RecordInlineCellAnchoredPortal
position={hoverPosition}
fieldMetadataItem={hoveredFieldMetadataItem}
objectMetadataItem={objectMetadataItem}
recordId={recordId}
Expand Down
Loading
Loading