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 5ddf33f6c..f23244514 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
@@ -5,7 +5,7 @@ import { selectedFilterComponentState } from '@/object-record/object-filter-drop
import { selectedOperandInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/selectedOperandInDropdownComponentState';
import { getRelativeDateDisplayValue } from '@/object-record/object-filter-dropdown/utils/getRelativeDateDisplayValue';
import { useApplyRecordFilter } from '@/object-record/record-filter/hooks/useApplyRecordFilter';
-import { InternalDatePicker } from '@/ui/input/components/internal/date/components/InternalDatePicker';
+import { DateTimePicker } from '@/ui/input/components/internal/date/components/InternalDatePicker';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
import { computeVariableDateViewFilterValue } from '@/views/view-filter-value/utils/computeVariableDateViewFilterValue';
@@ -104,14 +104,14 @@ export const ObjectFilterDropdownDateInput = () => {
: undefined;
return (
-
);
diff --git a/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormDateTimeFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormDateTimeFieldInput.tsx
index bb80b458f..9fd25f7f5 100644
--- a/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormDateTimeFieldInput.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormDateTimeFieldInput.tsx
@@ -5,7 +5,7 @@ import { VariableChip } from '@/object-record/record-field/form-types/components
import { VariablePickerComponent } from '@/object-record/record-field/form-types/types/VariablePickerComponent';
import { InputLabel } from '@/ui/input/components/InputLabel';
import {
- InternalDatePicker,
+ DateTimePicker,
MONTH_AND_YEAR_DROPDOWN_ID,
MONTH_AND_YEAR_DROPDOWN_MONTH_SELECT_ID,
MONTH_AND_YEAR_DROPDOWN_YEAR_SELECT_ID,
@@ -351,11 +351,11 @@ export const FormDateTimeFieldInput = ({
- {
+ const handleClose = (newDate: Date | null) => {
setInternalValue(newDate);
onSubmit?.(newDate);
};
@@ -104,16 +104,16 @@ export const DateInput = ({
return (
-
);
diff --git a/packages/twenty-front/src/modules/ui/input/components/internal/date/components/InternalDatePicker.tsx b/packages/twenty-front/src/modules/ui/input/components/internal/date/components/InternalDatePicker.tsx
index 69876727b..d54d8a976 100644
--- a/packages/twenty-front/src/modules/ui/input/components/internal/date/components/InternalDatePicker.tsx
+++ b/packages/twenty-front/src/modules/ui/input/components/internal/date/components/InternalDatePicker.tsx
@@ -270,7 +270,7 @@ const StyledButton = styled(MenuItemLeftContent)`
justify-content: start;
`;
-type InternalDatePickerProps = {
+type DateTimePickerProps = {
isRelative?: boolean;
hideHeaderInput?: boolean;
date: Date | null;
@@ -283,7 +283,7 @@ type InternalDatePickerProps = {
start: Date;
end: Date;
};
- onMouseSelect?: (date: Date | null) => void;
+ onClose?: (date: Date | null) => void;
onChange?: (date: Date | null) => void;
onRelativeDateChange?: (
relativeDate: {
@@ -300,10 +300,10 @@ type InternalDatePickerProps = {
onClear?: () => void;
};
-export const InternalDatePicker = ({
+export const DateTimePicker = ({
date,
onChange,
- onMouseSelect,
+ onClose,
clearable = true,
isDateTimeInput,
onClear,
@@ -312,7 +312,7 @@ export const InternalDatePicker = ({
onRelativeDateChange,
highlightedDateRange,
hideHeaderInput,
-}: InternalDatePickerProps) => {
+}: DateTimePickerProps) => {
const internalDate = date ?? new Date();
const { timeZone } = useContext(UserContext);
@@ -336,9 +336,9 @@ export const InternalDatePicker = ({
closeDropdown();
};
- const handleMouseSelect = (newDate: Date) => {
+ const handleClose = (newDate: Date) => {
closeDropdowns();
- onMouseSelect?.(newDate);
+ onClose?.(newDate);
};
const handleChangeMonth = (month: number) => {
@@ -396,7 +396,7 @@ export const InternalDatePicker = ({
})
.toJSDate();
- handleMouseSelect?.(dateParsed);
+ handleClose?.(dateParsed);
};
const dateWithoutTime = DateTime.fromJSDate(internalDate)
diff --git a/packages/twenty-front/src/modules/ui/input/components/internal/date/components/__stories__/InternalDatePicker.stories.tsx b/packages/twenty-front/src/modules/ui/input/components/internal/date/components/__stories__/InternalDatePicker.stories.tsx
index 74d54be0f..3bed37543 100644
--- a/packages/twenty-front/src/modules/ui/input/components/internal/date/components/__stories__/InternalDatePicker.stories.tsx
+++ b/packages/twenty-front/src/modules/ui/input/components/internal/date/components/__stories__/InternalDatePicker.stories.tsx
@@ -4,11 +4,11 @@ import { expect, userEvent, within } from '@storybook/test';
import { ComponentDecorator } from 'twenty-ui';
import { isDefined } from '~/utils/isDefined';
-import { InternalDatePicker } from '../InternalDatePicker';
+import { DateTimePicker } from '../InternalDatePicker';
-const meta: Meta = {
+const meta: Meta = {
title: 'UI/Input/Internal/InternalDatePicker',
- component: InternalDatePicker,
+ component: DateTimePicker,
decorators: [ComponentDecorator],
argTypes: {
date: { control: 'date' },
@@ -17,7 +17,7 @@ const meta: Meta = {
// eslint-disable-next-line react-hooks/rules-of-hooks
const [, updateArgs] = useArgs();
return (
- updateArgs({ date: newDate })}
/>
@@ -27,7 +27,7 @@ const meta: Meta = {
};
export default meta;
-type Story = StoryObj;
+type Story = StoryObj;
export const Default: Story = {};
diff --git a/packages/twenty-server/src/engine/twenty-orm/utils/format-result.util.ts b/packages/twenty-server/src/engine/twenty-orm/utils/format-result.util.ts
index 954712fdf..956880cff 100644
--- a/packages/twenty-server/src/engine/twenty-orm/utils/format-result.util.ts
+++ b/packages/twenty-server/src/engine/twenty-orm/utils/format-result.util.ts
@@ -1,5 +1,7 @@
import { isPlainObject } from '@nestjs/common/utils/shared.utils';
+import { isNonEmptyString } from '@sniptt/guards';
+import { isDefined } from 'class-validator';
import { FieldMetadataType } from 'twenty-shared';
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
@@ -12,10 +14,12 @@ import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-met
import { computeRelationType } from 'src/engine/twenty-orm/utils/compute-relation-type.util';
import { getCompositeFieldMetadataCollection } from 'src/engine/twenty-orm/utils/get-composite-field-metadata-collection';
import { isRelationFieldMetadataType } from 'src/engine/utils/is-relation-field-metadata-type.util';
+import { isDate } from 'src/utils/date/isDate';
+import { isValidDate } from 'src/utils/date/isValidDate';
export function formatResult(
data: any,
- ObjectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps,
+ objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps,
objectMetadataMaps: ObjectMetadataMaps,
): T {
if (!data) {
@@ -24,7 +28,7 @@ export function formatResult(
if (Array.isArray(data)) {
return data.map((item) =>
- formatResult(item, ObjectMetadataItemWithFieldMaps, objectMetadataMaps),
+ formatResult(item, objectMetadataItemWithFieldMaps, objectMetadataMaps),
) as T;
}
@@ -32,12 +36,12 @@ export function formatResult(
return data;
}
- if (!ObjectMetadataItemWithFieldMaps) {
+ if (!objectMetadataItemWithFieldMaps) {
throw new Error('Object metadata is missing');
}
const compositeFieldMetadataCollection = getCompositeFieldMetadataCollection(
- ObjectMetadataItemWithFieldMaps,
+ objectMetadataItemWithFieldMaps,
);
const compositeFieldMetadataMap = new Map(
@@ -58,7 +62,7 @@ export function formatResult(
);
const relationMetadataMap = new Map(
- Object.values(ObjectMetadataItemWithFieldMaps.fieldsById)
+ Object.values(objectMetadataItemWithFieldMaps.fieldsById)
.filter(({ type }) => isRelationFieldMetadataType(type))
.map((fieldMetadata) => [
fieldMetadata.name,
@@ -76,7 +80,7 @@ export function formatResult(
);
const newData: object = {};
const objectMetadaItemFieldsByName =
- objectMetadataMaps.byId[ObjectMetadataItemWithFieldMaps.id]?.fieldsByName;
+ objectMetadataMaps.byId[objectMetadataItemWithFieldMaps.id]?.fieldsByName;
for (const [key, value] of Object.entries(data)) {
const compositePropertyArgs = compositeFieldMetadataMap.get(key);
@@ -87,7 +91,7 @@ export function formatResult(
if (isPlainObject(value)) {
newData[key] = formatResult(
value,
- ObjectMetadataItemWithFieldMaps,
+ objectMetadataItemWithFieldMaps,
objectMetadataMaps,
);
} else if (objectMetadaItemFieldsByName[key]) {
@@ -142,6 +146,56 @@ export function formatResult(
newData[parentField][compositeProperty.name] = value;
}
+ const dateFieldMetadataCollection =
+ objectMetadataItemWithFieldMaps.fields.filter(
+ (field) => field.type === FieldMetadataType.DATE,
+ );
+
+ // This is a temporary fix to handle a bug in the frontend where the date gets returned in the wrong timezone,
+ // thus returning the wrong date.
+ //
+ // In short, for example :
+ // - DB stores `2025-01-01`
+ // - TypeORM .returning() returns `2024-12-31T23:00:00.000Z`
+ // - we shift +1h (or whatever the timezone offset is on the server)
+ // - we return `2025-01-01T00:00:00.000Z`
+ //
+ // See this PR for more details: https://github.com/twentyhq/twenty/pull/9700
+ const serverOffsetInMillisecondsToCounterActTypeORMAutomaticTimezoneShift =
+ new Date().getTimezoneOffset() * 60 * 1000;
+
+ for (const dateFieldMetadata of dateFieldMetadataCollection) {
+ const rawUpdatedDate = newData[dateFieldMetadata.name] as
+ | string
+ | null
+ | undefined
+ | Date;
+
+ if (!isDefined(rawUpdatedDate)) {
+ continue;
+ }
+
+ if (isDate(rawUpdatedDate)) {
+ if (isValidDate(rawUpdatedDate)) {
+ const shiftedDate = new Date(
+ rawUpdatedDate.getTime() -
+ serverOffsetInMillisecondsToCounterActTypeORMAutomaticTimezoneShift,
+ );
+
+ newData[dateFieldMetadata.name] = shiftedDate;
+ }
+ } else if (isNonEmptyString(rawUpdatedDate)) {
+ const currentDate = new Date(newData[dateFieldMetadata.name]);
+
+ const shiftedDate = new Date(
+ new Date(currentDate).getTime() -
+ serverOffsetInMillisecondsToCounterActTypeORMAutomaticTimezoneShift,
+ );
+
+ newData[dateFieldMetadata.name] = shiftedDate;
+ }
+ }
+
return newData as T;
}
diff --git a/packages/twenty-server/src/utils/date/isDate.ts b/packages/twenty-server/src/utils/date/isDate.ts
new file mode 100644
index 000000000..8c2b80f47
--- /dev/null
+++ b/packages/twenty-server/src/utils/date/isDate.ts
@@ -0,0 +1,3 @@
+export const isDate = (date: any): date is Date => {
+ return date instanceof Date;
+};
diff --git a/packages/twenty-server/src/utils/date/isValidDate.ts b/packages/twenty-server/src/utils/date/isValidDate.ts
new file mode 100644
index 000000000..9f1775243
--- /dev/null
+++ b/packages/twenty-server/src/utils/date/isValidDate.ts
@@ -0,0 +1,3 @@
+export const isValidDate = (date: any): date is Date => {
+ return date instanceof Date && !isNaN(date.getTime());
+};