Refactored Date to Temporal in critical date zones (#16544)

Fixes https://github.com/twentyhq/twenty/issues/16110

This PR implements Temporal to replace the legacy Date object, in all
features that are time zone sensitive. (around 80% of the app)

Here we define a few utils to handle Temporal primitives and obtain an
easier DX for timezone manipulation, front end and back end.

This PR deactivates the usage of timezone from the graph configuration,
because for now it's always UTC and is not really relevant, let's handle
that later.

Workflows code and backend only code that don't take user input are
using UTC time zone, the affected utils have not been refactored yet
because this PR is big enough.

# New way of filtering on date intervals

As we'll progressively rollup Temporal everywhere in the codebase and
remove `Date` JS object everywhere possible, we'll use the way to filter
that is recommended by Temporal.

This way of filtering on date intervals involves half-open intervals,
and is the preferred way to avoid edge-cases with DST and smallest time
increment edge-case.

## Filtering endOfX with DST edge-cases

Some day-light save time shifts involve having no existing hour, or even
day on certain days, for example Samoa Islands have no 30th of December
2011 : https://www.timeanddate.com/news/time/samoa-dateline.html, it
jumps from 29th to 31st, so filtering on `< next period start` makes it
easier to let the date library handle the strict inferior comparison,
than filtering on `≤ end of period` and trying to compute manually the
end of the period.

For example for Samoa Islands, is end of day `2011-12-29T23:59:59.999`
or is it `2011-12-30T23:59:59.999` ? If you say I don't need to know and
compute it, because I want everything strictly before
`2011-12-29T00:00:00 + start of next day (according to the library which
knows those edge-cases)`, then you have a 100% deterministic way of
computing date intervals in any timezone, for any day of any year.

Of course the Samoa example is an extreme one, but more common ones
involve DST shifts of 1 hour, which are still problematic on certain
days of the year.

## Computing the exact _end of period_

Having an open interval filtering, with `[included - included]` instead
of half-open `[included - excluded)`, forces to compute the open end of
an interval, which often involves taking an arbitrary unit like minute,
second, microsecond or nanosecond, which will lead to edge-case of
unhandled values.

For example, let's say my code computes endOfDay by setting the time to
`23:59:59.999`, if another library, API, or anything else, ends up
giving me a date-time with another time precision `23:59:59.999999999`
(down to the nanosecond), then this date-time will be filtered out,
while it should not.

The good deterministic way to avoid 100% of those complex bugs is to
create a half-open filter :

`≥ start of period` to `< start of next period` 

For example : 

`≥ 2025-01-01T00:00:00` to `< 2025-01-02T00:00:00` instead of `≥
2025-01-01T00:00:00` to `≤ 2025-01-01T23:59:59.999`

Because, `2025-01-01T00:00:00` = `2025-01-01T00:00:00.000` =
`2025-01-01T00:00:00.000000` = `2025-01-01T00:00:00.000000000` => no
risk of error in computing start of period

But `2025-01-01T23:59:59` ≠ `2025-01-01T23:59:59.999` ≠
`2025-01-01T23:59:59.999999` ≠ `2025-01-01T23:59:59.999999999` =>
existing risk of error in computing end of period

This is why an half-open interval has no risk of error in computing a
date-time interval filter.

Here is a link to this debate :
https://github.com/tc39/proposal-temporal/issues/2568

> For this reason, we recommend not calculating the exact nanosecond at
the end of the day if it's not absolutely necessary. For example, if
it's needed for <= comparisons, we recommend just changing the
comparison code. So instead of <= zdtEndOfDay your code could be <
zdtStartOfNextDay which is easier to calculate and not subject to the
issue of not knowing which unit is the right one.
>
> [Justin Grant](https://github.com/justingrant), top contributor of
Temporal

## Application to our codebase

Applying this half-open filtering paradigm to our codebase means we
would have to rename `IS_AFTER` to `IS_AFTER_OR_EQUAL` and to keep
`IS_BEFORE` (or even `IS_STRICTLY_BEFORE`) to make this half-open
interval self-explanatory everywhere in the codebase, this will avoid
any confusion.

See the relevant issue :
https://github.com/twentyhq/core-team-issues/issues/2010

In the mean time, we'll keep this operand and add this semantic in the
naming everywhere possible.

## Example with a different user timezone 

Example on a graph grouped by week in timezone Pacific/Samoa, on a
computer running on Europe/Paris :

<img width="342" height="511" alt="image"
src="https://github.com/user-attachments/assets/9e7d5121-ecc4-4233-835b-f59293fbd8c8"
/>

Then the associated data in the table view, with our **half-open
date-time filter** :

<img width="804" height="262" alt="image"
src="https://github.com/user-attachments/assets/28efe1d7-d2fc-4aec-b521-bada7f980447"
/>

And the associated SQL query result to see how DATE_TRUNC in Postgres
applies its internal start of week logic :

<img width="709" height="220" alt="image"
src="https://github.com/user-attachments/assets/4d0542e1-eaae-4b4b-afa9-5005f48ffdca"
/>

The associated SQL query without parameters to test in your SQL client :

```SQL
SELECT "opportunity"."closeDate" as "close_date", TO_CHAR(DATE_TRUNC('week', "opportunity"."closeDate", 'Pacific/Samoa') AT TIME ZONE 'Pacific/Samoa', 'YYYY-MM-DD') AS "DATE_TRUNC by week start in timezone Pacific/Samoa", "opportunity"."name" FROM "workspace_1wgvd1injqtife6y4rvfbu3h5"."opportunity" "opportunity" ORDER BY "opportunity"."closeDate" ASC NULLS LAST
```

# Date picker simplification (not in this PR)

Our DatePicker component, which is wrapping `react-datepicker` library
component, is now exposing plain dates as string instead of Date object.
The Date object is still used internally to manage the library
component, but since the date picker calendar is only manipulating plain
dates, there is no need to add timezone management to it, and no need to
expose a handleChange with Date object.

The timezone management relies on date time inputs now.

The modification has been made in a previous PR :
https://github.com/twentyhq/twenty/issues/15377 but it's good to
reference it here.

# Calendar feature refactor

Calendar feature has been refactored to rely on Temporal.PlainDate as
much as possible, while leaving some date-fns utils to avoid re-coding
them.

Since the trick is to use utils to convert back and from Date object in
exec env reliably, we can do it everywhere we need to interface legacy
Date object utils and Temporal related code.

## TimeZone is now shown on Calendar :

<img width="894" height="958" alt="image"
src="https://github.com/user-attachments/assets/231f8107-fad6-4786-b532-456692c20f1d"
/>

## Month picker has been refactored 

<img width="503" height="266" alt="image"
src="https://github.com/user-attachments/assets/cb90bc34-6c4d-436d-93bc-4b6fb00de7f5"
/>

Since the days weren't useful, the picker has been refactored to remove
the days.

# Miscellaneous 
- Fixed a bug with drag and drop edge-case with 2 items in a list.

# Improvements 

## Lots of chained operations
It would be nice to create small utils to avoid repeated chained
operations, but that is how Temporal is designed, a very small set of
primitive operations that allow to compose everything needed. Maybe
we'll have wrappers on top of Temporal in the coming years.

## Creation of Temporal objects is throwing errors

If the input is badly formatted Temporal will throw, we might want to
adopt a global strategy to avoid that.

Example : 

```ts
const newPlainDate = Temporal.PlainDate.from('bad-string'); // Will throw
```
This commit is contained in:
Lucas Bordeau
2025-12-23 07:40:26 -10:00
committed by GitHub
parent 4d5d2233bc
commit 0b5be7caa3
205 changed files with 3813 additions and 1755 deletions
@@ -16,7 +16,7 @@ type AdvancedFilterCommandMenuRecordFilterOperandSelectProps = {
export const AdvancedFilterCommandMenuRecordFilterOperandSelect = ({
recordFilterId,
}: AdvancedFilterCommandMenuRecordFilterOperandSelectProps) => {
const { readonly } = useContext(AdvancedFilterContext);
const { readonly, isWorkflowFindRecords } = useContext(AdvancedFilterContext);
const currentRecordFilters = useRecoilComponentValue(
currentRecordFiltersComponentState,
);
@@ -36,12 +36,15 @@ export const AdvancedFilterCommandMenuRecordFilterOperandSelect = ({
})
: [];
const shouldUseUTCTimeZone = isWorkflowFindRecords === true;
const timeZoneAbbreviation = shouldUseUTCTimeZone ? 'UTC' : undefined;
if (isDisabled === true) {
return (
<SelectControl
selectedOption={{
label: filter?.operand
? getOperandLabel(filter.operand)
? getOperandLabel(filter.operand, timeZoneAbbreviation)
: t`Select operand`,
value: null,
}}
@@ -20,6 +20,7 @@ import { currentRecordFiltersComponentState } from '@/object-record/record-filte
import { RecordFilterOperand } from '@/object-record/record-filter/types/RecordFilterOperand';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { stringifyRelativeDateFilter } from '@/views/view-filter-value/utils/stringifyRelativeDateFilter';
import { WORKFLOW_TIMEZONE } from '@/workflow/constants/WorkflowTimeZone';
import { isObject, isString } from '@sniptt/guards';
import { useContext } from 'react';
import { FieldMetadataType } from 'twenty-shared/types';
@@ -32,9 +33,12 @@ export const AdvancedFilterCommandMenuValueFormInput = ({
}: {
recordFilterId: string;
}) => {
const { readonly, VariablePicker, objectMetadataItem } = useContext(
AdvancedFilterContext,
);
const {
readonly,
VariablePicker,
objectMetadataItem,
isWorkflowFindRecords,
} = useContext(AdvancedFilterContext);
const currentRecordFilters = useRecoilComponentValue(
currentRecordFiltersComponentState,
@@ -179,6 +183,9 @@ export const AdvancedFilterCommandMenuValueFormInput = ({
metadata: fieldDefinition?.metadata as FieldMetadata,
};
const shouldUseUTCTimeZone = isWorkflowFindRecords === true;
const timeZone = shouldUseUTCTimeZone ? WORKFLOW_TIMEZONE : undefined;
return (
<FormFieldInput
field={field}
@@ -187,6 +194,7 @@ export const AdvancedFilterCommandMenuValueFormInput = ({
readonly={readonly}
// VariablePicker is not supported for date filters yet
VariablePicker={isFilterableByDateValue ? undefined : VariablePicker}
timeZone={timeZone}
/>
);
};
@@ -1,7 +1,9 @@
import { AdvancedFilterRecordFilterOperandSelectContent } from '@/object-record/advanced-filter/components/AdvancedFilterRecordFilterOperandSelectContent';
import { getOperandLabel } from '@/object-record/object-filter-dropdown/utils/getOperandLabel';
import { useTimeZoneAbbreviationForNowInUserTimeZone } from '@/object-record/record-filter/hooks/useTimeZoneAbbreviationForNowInUserTimeZone';
import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
import { getRecordFilterOperands } from '@/object-record/record-filter/utils/getRecordFilterOperands';
import { useUserTimezone } from '@/ui/input/components/internal/date/hooks/useUserTimezone';
import { SelectControl } from '@/ui/input/components/SelectControl';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import styled from '@emotion/styled';
@@ -38,12 +40,21 @@ export const AdvancedFilterRecordFilterOperandSelect = ({
})
: [];
const { userTimeZoneAbbreviation } =
useTimeZoneAbbreviationForNowInUserTimeZone();
const { isSystemTimezone } = useUserTimezone();
const timeZoneAbbreviation = !isSystemTimezone
? userTimeZoneAbbreviation
: null;
if (isDisabled) {
return (
<SelectControl
selectedOption={{
label: filter?.operand
? getOperandLabel(filter.operand)
? getOperandLabel(filter.operand, timeZoneAbbreviation)
: t`Select operand`,
value: null,
}}
@@ -1,9 +1,12 @@
import { DEFAULT_ADVANCED_FILTER_DROPDOWN_OFFSET } from '@/object-record/advanced-filter/constants/DefaultAdvancedFilterDropdownOffset';
import { AdvancedFilterContext } from '@/object-record/advanced-filter/states/context/AdvancedFilterContext';
import { useApplyObjectFilterDropdownOperand } from '@/object-record/object-filter-dropdown/hooks/useApplyObjectFilterDropdownOperand';
import { getOperandLabel } from '@/object-record/object-filter-dropdown/utils/getOperandLabel';
import { useTimeZoneAbbreviationForNowInUserTimeZone } from '@/object-record/record-filter/hooks/useTimeZoneAbbreviationForNowInUserTimeZone';
import { type RecordFilter } from '@/object-record/record-filter/types/RecordFilter';
import { type RecordFilterOperand } from '@/object-record/record-filter/types/RecordFilterOperand';
import { useUserTimezone } from '@/ui/input/components/internal/date/hooks/useUserTimezone';
import { SelectControl } from '@/ui/input/components/SelectControl';
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
@@ -15,6 +18,7 @@ import { SelectableListItem } from '@/ui/layout/selectable-list/components/Selec
import { selectedItemIdComponentState } from '@/ui/layout/selectable-list/states/selectedItemIdComponentState';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { t } from '@lingui/core/macro';
import { useContext } from 'react';
import { type ViewFilterOperand } from 'twenty-shared/types';
import { MenuItem } from 'twenty-ui/navigation';
@@ -31,6 +35,8 @@ export const AdvancedFilterRecordFilterOperandSelectContent = ({
}: AdvancedFilterRecordFilterOperandSelectContentProps) => {
const dropdownId = `advanced-filter-view-filter-operand-${recordFilterId}`;
const { isWorkflowFindRecords } = useContext(AdvancedFilterContext);
const { closeDropdown } = useCloseDropdown();
const { applyObjectFilterDropdownOperand } =
@@ -47,6 +53,18 @@ export const AdvancedFilterRecordFilterOperandSelectContent = ({
dropdownId,
);
const { userTimeZoneAbbreviation } =
useTimeZoneAbbreviationForNowInUserTimeZone();
const { isSystemTimezone } = useUserTimezone();
const timeZoneAbbreviation =
isWorkflowFindRecords === true
? 'UTC'
: !isSystemTimezone
? userTimeZoneAbbreviation
: null;
return (
<Dropdown
dropdownId={dropdownId}
@@ -54,7 +72,7 @@ export const AdvancedFilterRecordFilterOperandSelectContent = ({
<SelectControl
selectedOption={{
label: filter?.operand
? getOperandLabel(filter.operand)
? getOperandLabel(filter.operand, timeZoneAbbreviation)
: t`Select operand`,
value: null,
}}
@@ -83,7 +101,7 @@ export const AdvancedFilterRecordFilterOperandSelectContent = ({
onClick={() => {
handleOperandChange(filterOperand);
}}
text={getOperandLabel(filterOperand)}
text={getOperandLabel(filterOperand, timeZoneAbbreviation)}
/>
</SelectableListItem>
))}
@@ -1,5 +1,5 @@
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { CalendarStartDay } from 'twenty-shared';
import { CalendarStartDay } from 'twenty-shared/constants';
import { detectCalendarStartDay } from '@/localization/utils/detection/detectCalendarStartDay';
import { useApplyObjectFilterDropdownFilterValue } from '@/object-record/object-filter-dropdown/hooks/useApplyObjectFilterDropdownFilterValue';
@@ -40,6 +40,7 @@ export const ObjectFilterDropdownDateInput = () => {
const handleAbsoluteDateChange = (newPlainDate: string | null) => {
const newFilterValue = newPlainDate ?? '';
// TODO: remove this and use getDisplayValue instead
const formattedDate = formatDateString({
value: newPlainDate,
timeZone,
@@ -109,7 +110,7 @@ export const ObjectFilterDropdownDateInput = () => {
instanceId={`object-filter-dropdown-date-input`}
relativeDate={relativeDate}
isRelative={isRelativeOperand}
date={plainDateValue ?? null}
plainDateString={plainDateValue ?? null}
onChange={handleAbsoluteDateChange}
onRelativeDateChange={handleRelativeDateChange}
onClear={handleClear}
@@ -1,5 +1,5 @@
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { CalendarStartDay } from 'twenty-shared';
import { CalendarStartDay } from 'twenty-shared/constants';
import { detectCalendarStartDay } from '@/localization/utils/detection/detectCalendarStartDay';
import { useApplyObjectFilterDropdownFilterValue } from '@/object-record/object-filter-dropdown/hooks/useApplyObjectFilterDropdownFilterValue';
@@ -9,7 +9,7 @@ import { DateTimePicker } from '@/ui/input/components/internal/date/components/D
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { UserContext } from '@/users/contexts/UserContext';
import { stringifyRelativeDateFilter } from '@/views/view-filter-value/utils/stringifyRelativeDateFilter';
import { useContext, useState } from 'react';
import { useContext } from 'react';
import { useRecoilValue } from 'recoil';
import { ViewFilterOperand, type FirstDayOfTheWeek } from 'twenty-shared/types';
import {
@@ -18,6 +18,9 @@ import {
type RelativeDateFilter,
} from 'twenty-shared/utils';
import { useUserTimezone } from '@/ui/input/components/internal/date/hooks/useUserTimezone';
import { isNonEmptyString } from '@sniptt/guards';
import { Temporal } from 'temporal-polyfill';
import { dateLocaleState } from '~/localization/states/dateLocaleState';
import { formatDateTimeString } from '~/utils/string/formatDateTimeString';
@@ -26,6 +29,8 @@ export const ObjectFilterDropdownDateTimeInput = () => {
const dateLocale = useRecoilValue(dateLocaleState);
const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState);
const { userTimezone } = useUserTimezone();
const objectFilterDropdownCurrentRecordFilter = useRecoilComponentValue(
objectFilterDropdownCurrentRecordFilterComponentState,
);
@@ -33,21 +38,11 @@ export const ObjectFilterDropdownDateTimeInput = () => {
const { applyObjectFilterDropdownFilterValue } =
useApplyObjectFilterDropdownFilterValue();
const initialFilterValue = isDefined(objectFilterDropdownCurrentRecordFilter)
? resolveDateTimeFilter(objectFilterDropdownCurrentRecordFilter)
: null;
const [internalDate, setInternalDate] = useState<Date | null>(
initialFilterValue instanceof Date ? initialFilterValue : null,
);
const handleAbsoluteDateChange = (newDate: Date | null) => {
setInternalDate(newDate);
const newFilterValue = newDate?.toISOString() ?? '';
const handleAbsoluteDateChange = (newDate: Temporal.ZonedDateTime | null) => {
const newFilterValue = newDate?.toInstant().toString() ?? '';
const formattedDateTime = formatDateTimeString({
value: newDate?.toISOString(),
value: newFilterValue,
timeZone,
dateFormat,
timeFormat,
@@ -89,33 +84,39 @@ export const ObjectFilterDropdownDateTimeInput = () => {
applyObjectFilterDropdownFilterValue(newFilterValue, newDisplayValue);
};
const isRelativeOperand =
objectFilterDropdownCurrentRecordFilter?.operand ===
ViewFilterOperand.IS_RELATIVE;
const handleClear = () => {
isRelativeOperand
? handleRelativeDateChange(null)
: handleAbsoluteDateChange(null);
};
const resolvedValue = objectFilterDropdownCurrentRecordFilter
? resolveDateTimeFilter(objectFilterDropdownCurrentRecordFilter)
: null;
const relativeDate =
resolvedValue && !(resolvedValue instanceof Date)
const isRelativeDateFilter =
objectFilterDropdownCurrentRecordFilter?.operand ===
ViewFilterOperand.IS_RELATIVE &&
isDefined(resolvedValue) &&
typeof resolvedValue === 'object';
const relativeDate = isRelativeDateFilter ? resolvedValue : undefined;
const stringFilterValue =
objectFilterDropdownCurrentRecordFilter?.operand !==
ViewFilterOperand.IS_RELATIVE && typeof resolvedValue === 'string'
? resolvedValue
: undefined;
const internalZonedDateTime =
!isRelativeDateFilter && isNonEmptyString(stringFilterValue)
? Temporal.Instant.from(stringFilterValue).toZonedDateTimeISO(
timeZone ?? userTimezone,
)
: null;
return (
<DateTimePicker
instanceId={`object-filter-dropdown-date-time-input`}
relativeDate={relativeDate}
isRelative={isRelativeOperand}
date={internalDate}
isRelative={isRelativeDateFilter}
date={internalZonedDateTime}
onChange={handleAbsoluteDateChange}
onRelativeDateChange={handleRelativeDateChange}
onClear={handleClear}
clearable={false}
/>
);
};
@@ -5,8 +5,10 @@ import { fieldMetadataItemUsedInDropdownComponentSelector } from '@/object-recor
import { selectedOperandInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/selectedOperandInDropdownComponentState';
import { subFieldNameUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/subFieldNameUsedInDropdownComponentState';
import { getOperandLabel } from '@/object-record/object-filter-dropdown/utils/getOperandLabel';
import { useTimeZoneAbbreviationForNowInUserTimeZone } from '@/object-record/record-filter/hooks/useTimeZoneAbbreviationForNowInUserTimeZone';
import { type RecordFilterOperand } from '@/object-record/record-filter/types/RecordFilterOperand';
import { getRecordFilterOperands } from '@/object-record/record-filter/utils/getRecordFilterOperands';
import { useUserTimezone } from '@/ui/input/components/internal/date/hooks/useUserTimezone';
import { DropdownMenuInnerSelect } from '@/ui/layout/dropdown/components/DropdownMenuInnerSelect';
import { GenericDropdownContentWidth } from '@/ui/layout/dropdown/constants/GenericDropdownContentWidth';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
@@ -38,8 +40,17 @@ export const ObjectFilterDropdownInnerSelectOperandDropdown = () => {
})
: [];
const { userTimeZoneAbbreviation } =
useTimeZoneAbbreviationForNowInUserTimeZone();
const { isSystemTimezone } = useUserTimezone();
const timeZoneAbbreviation = !isSystemTimezone
? userTimeZoneAbbreviation
: null;
const options = operandsForFilterType.map((operand) => ({
label: getOperandLabel(operand),
label: getOperandLabel(operand, timeZoneAbbreviation),
value: operand,
})) as SelectOption[];
@@ -1,23 +1,27 @@
import { DATE_OPERANDS_THAT_SHOULD_BE_INITIALIZED_WITH_NOW } from '@/object-record/object-filter-dropdown/constants/DateOperandsThatShouldBeInitializedWithNow';
import { useGetInitialFilterValue } from '@/object-record/object-filter-dropdown/hooks/useGetInitialFilterValue';
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';
import { selectedOperandInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/selectedOperandInDropdownComponentState';
import { getRelativeDateDisplayValue } from '@/object-record/object-filter-dropdown/utils/getRelativeDateDisplayValue';
import { useCreateEmptyRecordFilterFromFieldMetadataItem } from '@/object-record/record-filter/hooks/useCreateEmptyRecordFilterFromFieldMetadataItem';
import { useGetRelativeDateFilterWithUserTimezone } from '@/object-record/record-filter/hooks/useGetRelativeDateFilterWithUserTimezone';
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 { isDefined } from 'twenty-shared/utils';
import {
isDefined,
relativeDateFilterStringifiedSchema,
} from 'twenty-shared/utils';
export const useApplyObjectFilterDropdownOperand = () => {
const { userTimezone } = useUserTimezone();
const objectFilterDropdownCurrentRecordFilter = useRecoilComponentValue(
objectFilterDropdownCurrentRecordFilterComponentState,
);
@@ -40,8 +44,6 @@ export const useApplyObjectFilterDropdownOperand = () => {
const { createEmptyRecordFilterFromFieldMetadataItem } =
useCreateEmptyRecordFilterFromFieldMetadataItem();
const { getInitialFilterValue } = useGetInitialFilterValue();
const { getRelativeDateFilterWithUserTimezone } =
useGetRelativeDateFilterWithUserTimezone();
@@ -86,24 +88,7 @@ export const useApplyObjectFilterDropdownOperand = () => {
(recordFilterToUpsert.type === 'DATE' ||
recordFilterToUpsert.type === 'DATE_TIME')
) {
if (
DATE_OPERANDS_THAT_SHOULD_BE_INITIALIZED_WITH_NOW.includes(newOperand)
) {
// TODO: allow to keep same value when switching between is after, is before, is and is not
// For now we reset with now each time we switch operand
const dateToUseAsISOString = new Date().toISOString();
const { displayValue, value } = getInitialFilterValue(
recordFilterToUpsert.type,
newOperand,
dateToUseAsISOString,
);
recordFilterToUpsert.value = value;
recordFilterToUpsert.displayValue = displayValue;
} else if (newOperand === RecordFilterOperand.IS_RELATIVE) {
if (newOperand === RecordFilterOperand.IS_RELATIVE) {
const newRelativeDateFilter = getRelativeDateFilterWithUserTimezone(
DEFAULT_RELATIVE_DATE_FILTER_VALUE,
);
@@ -111,13 +96,33 @@ export const useApplyObjectFilterDropdownOperand = () => {
recordFilterToUpsert.value = stringifyRelativeDateFilter(
newRelativeDateFilter,
);
recordFilterToUpsert.displayValue = getRelativeDateDisplayValue(
newRelativeDateFilter,
);
} else {
recordFilterToUpsert.value = '';
recordFilterToUpsert.displayValue = '';
const filterValueIsEmpty = !isNonEmptyString(
recordFilterToUpsert.value,
);
const isStillRelativeFilterValue =
relativeDateFilterStringifiedSchema.safeParse(
recordFilterToUpsert.value,
);
if (filterValueIsEmpty || isStillRelativeFilterValue.success) {
const zonedDateToUse = Temporal.Now.zonedDateTimeISO(userTimezone);
if (recordFilterToUpsert.type === 'DATE') {
const initialNowDateFilterValue = zonedDateToUse
.toPlainDate()
.toString();
recordFilterToUpsert.value = initialNowDateFilterValue;
} else {
const initialNowDateTimeFilterValue = zonedDateToUse
.toInstant()
.toString();
recordFilterToUpsert.value = initialNowDateTimeFilterValue;
}
}
}
}
@@ -0,0 +1,17 @@
import { useUserDateFormat } from '@/ui/input/components/internal/date/hooks/useUserDateFormat';
import { type Temporal } from 'temporal-polyfill';
import { formatZonedDateTimeDatePart } from '~/utils/dates/formatZonedDateTimeDatePart';
export const useGetDateFilterDisplayValue = () => {
const { userDateFormat } = useUserDateFormat();
const getDateFilterDisplayValue = (zonedDateTime: Temporal.ZonedDateTime) => {
const displayValue = `${formatZonedDateTimeDatePart(zonedDateTime, userDateFormat)}`;
return { displayValue };
};
return {
getDateFilterDisplayValue,
};
};
@@ -1,46 +1,27 @@
import { getDateFormatFromWorkspaceDateFormat } from '@/localization/utils/format-preferences/getDateFormatFromWorkspaceDateFormat';
import { getTimeFormatFromWorkspaceTimeFormat } from '@/localization/utils/format-preferences/getTimeFormatFromWorkspaceTimeFormat';
import { useUserDateFormat } from '@/ui/input/components/internal/date/hooks/useUserDateFormat';
import { useUserTimeFormat } from '@/ui/input/components/internal/date/hooks/useUserTimeFormat';
import { useUserTimezone } from '@/ui/input/components/internal/date/hooks/useUserTimezone';
import { format } from 'date-fns';
import { shiftPointInTimeFromTimezoneDifferenceInMinutesWithSystemTimezone } from 'twenty-shared/utils';
import { getTimezoneAbbreviationForZonedDateTime } from '@/ui/input/components/internal/date/utils/getTimeZoneAbbreviationForZonedDateTime';
import { type Temporal } from 'temporal-polyfill';
import { formatZonedDateTimeDatePart } from '~/utils/dates/formatZonedDateTimeDatePart';
import { formatZonedDateTimeTimePart } from '~/utils/dates/formatZonedDateTimeTimePart';
export const useGetDateTimeFilterDisplayValue = () => {
const {
userTimezone,
isSystemTimezone,
getTimezoneAbbreviationForPointInTime,
} = useUserTimezone();
const { isSystemTimezone } = useUserTimezone();
const { userDateFormat } = useUserDateFormat();
const { userTimeFormat } = useUserTimeFormat();
const getDateTimeFilterDisplayValue = (correctPointInTime: Date) => {
const shiftedDate =
shiftPointInTimeFromTimezoneDifferenceInMinutesWithSystemTimezone(
correctPointInTime,
userTimezone,
'sub',
);
const dateFormatString =
getDateFormatFromWorkspaceDateFormat(userDateFormat);
const timeFormatString =
getTimeFormatFromWorkspaceTimeFormat(userTimeFormat);
const formatToUse = `${dateFormatString} ${timeFormatString}`;
const getDateTimeFilterDisplayValue = (
referenceZonedDateTime: Temporal.ZonedDateTime,
) => {
const timezoneSuffix = !isSystemTimezone
? ` (${getTimezoneAbbreviationForPointInTime(shiftedDate)})`
? ` (${getTimezoneAbbreviationForZonedDateTime(referenceZonedDateTime)})`
: '';
const displayValue = `${format(shiftedDate, formatToUse)}${timezoneSuffix}`;
const displayValue = `${formatZonedDateTimeDatePart(referenceZonedDateTime, userDateFormat)} ${formatZonedDateTimeTimePart(referenceZonedDateTime, userTimeFormat)}${timezoneSuffix}`;
return {
correctPointInTime,
displayValue,
};
return { displayValue };
};
return {
@@ -1,14 +1,9 @@
import { getDateFormatFromWorkspaceDateFormat } from '@/localization/utils/format-preferences/getDateFormatFromWorkspaceDateFormat';
import { getTimeFormatFromWorkspaceTimeFormat } from '@/localization/utils/format-preferences/getTimeFormatFromWorkspaceTimeFormat';
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 { useUserDateFormat } from '@/ui/input/components/internal/date/hooks/useUserDateFormat';
import { useUserTimeFormat } from '@/ui/input/components/internal/date/hooks/useUserTimeFormat';
import { useUserTimezone } from '@/ui/input/components/internal/date/hooks/useUserTimezone';
import { TZDate } from '@date-fns/tz';
import { isNonEmptyString } from '@sniptt/guards';
import { format } from 'date-fns';
import { DATE_TYPE_FORMAT } from 'twenty-shared/constants';
import { Temporal } from 'temporal-polyfill';
import { type FilterableAndTSVectorFieldType } from 'twenty-shared/types';
const activeDatePickerOperands = [
@@ -18,41 +13,25 @@ const activeDatePickerOperands = [
];
export const useGetInitialFilterValue = () => {
const {
userTimezone,
isSystemTimezone,
getTimezoneAbbreviationForPointInTime,
} = useUserTimezone();
const { userDateFormat } = useUserDateFormat();
const { userTimeFormat } = useUserTimeFormat();
const { userTimezone } = useUserTimezone();
const { getDateFilterDisplayValue } = useGetDateFilterDisplayValue();
const { getDateTimeFilterDisplayValue } = useGetDateTimeFilterDisplayValue();
const getInitialFilterValue = (
newType: FilterableAndTSVectorFieldType,
newOperand: RecordFilterOperand,
alreadyExistingISODate?: string,
alreadyExistingZonedDateTime?: Temporal.ZonedDateTime,
): Pick<RecordFilter, 'value' | 'displayValue'> | Record<string, never> => {
switch (newType) {
case 'DATE': {
if (activeDatePickerOperands.includes(newOperand)) {
const referenceDate = isNonEmptyString(alreadyExistingISODate)
? new Date(alreadyExistingISODate)
: new Date();
const referenceDate =
alreadyExistingZonedDateTime ??
Temporal.Now.zonedDateTimeISO(userTimezone);
const shiftedDate = new TZDate(
referenceDate.getFullYear(),
referenceDate.getMonth(),
referenceDate.getDate(),
userTimezone,
);
const value = referenceDate.toPlainDate().toString();
const dateFormatString =
getDateFormatFromWorkspaceDateFormat(userDateFormat);
shiftedDate.setSeconds(0);
shiftedDate.setMilliseconds(0);
const value = format(shiftedDate, DATE_TYPE_FORMAT);
const displayValue = format(shiftedDate, dateFormatString);
const { displayValue } = getDateFilterDisplayValue(referenceDate);
return { value, displayValue };
}
@@ -61,36 +40,13 @@ export const useGetInitialFilterValue = () => {
}
case 'DATE_TIME': {
if (activeDatePickerOperands.includes(newOperand)) {
const referenceDate = isNonEmptyString(alreadyExistingISODate)
? new Date(alreadyExistingISODate)
: new Date();
const referenceDate =
alreadyExistingZonedDateTime ??
Temporal.Now.zonedDateTimeISO(userTimezone);
const shiftedDate = new TZDate(
referenceDate.getFullYear(),
referenceDate.getMonth(),
referenceDate.getDate(),
referenceDate.getHours(),
referenceDate.getMinutes(),
userTimezone,
);
const value = referenceDate.toInstant().toString();
shiftedDate.setSeconds(0);
shiftedDate.setMilliseconds(0);
const dateFormatString =
getDateFormatFromWorkspaceDateFormat(userDateFormat);
const timeFormatString =
getTimeFormatFromWorkspaceTimeFormat(userTimeFormat);
const formatToUse = `${dateFormatString} ${timeFormatString}`;
const timezoneSuffix = !isSystemTimezone
? ` (${getTimezoneAbbreviationForPointInTime(shiftedDate)})`
: '';
const value = shiftedDate.toISOString();
const displayValue = `${format(shiftedDate, formatToUse)}${timezoneSuffix}`;
const { displayValue } = getDateTimeFilterDisplayValue(referenceDate);
return { value, displayValue };
}
@@ -1,9 +1,18 @@
import { t } from '@lingui/core/macro';
import { isNonEmptyString } from '@sniptt/guards';
import { ViewFilterOperand } from 'twenty-shared/types';
export const getOperandLabel = (
operand: ViewFilterOperand | null | undefined,
timeZoneAbbreviation?: string | null | undefined,
) => {
const shouldDisplayTimeZoneAbbreviation =
isNonEmptyString(timeZoneAbbreviation);
const timeZoneAbbreviationSuffix = shouldDisplayTimeZoneAbbreviation
? ` (${timeZoneAbbreviation})`
: '';
switch (operand) {
case ViewFilterOperand.CONTAINS:
return t`Contains`;
@@ -16,7 +25,7 @@ export const getOperandLabel = (
case ViewFilterOperand.IS_BEFORE:
return t`Is before`;
case ViewFilterOperand.IS_AFTER:
return t`Is after`;
return t`Is after or equal`;
case ViewFilterOperand.IS:
return t`Is`;
case ViewFilterOperand.IS_NOT:
@@ -34,7 +43,7 @@ export const getOperandLabel = (
case ViewFilterOperand.IS_IN_FUTURE:
return t`Is in future`;
case ViewFilterOperand.IS_TODAY:
return t`Is today`;
return t`Is today${timeZoneAbbreviationSuffix}`;
default:
return '';
}
@@ -42,7 +51,15 @@ export const getOperandLabel = (
export const getOperandLabelShort = (
operand: ViewFilterOperand | null | undefined,
timeZoneAbbreviation?: string | null | undefined,
) => {
const shouldDisplayTimeZoneAbbreviation =
isNonEmptyString(timeZoneAbbreviation);
const timeZoneAbbreviationSuffix = shouldDisplayTimeZoneAbbreviation
? ` (${timeZoneAbbreviation})`
: '';
switch (operand) {
case ViewFilterOperand.IS:
case ViewFilterOperand.CONTAINS:
@@ -63,13 +80,13 @@ export const getOperandLabelShort = (
case ViewFilterOperand.IS_BEFORE:
return '\u00A0< ';
case ViewFilterOperand.IS_AFTER:
return '\u00A0> ';
return '\u00A0 ';
case ViewFilterOperand.IS_IN_PAST:
return t`: Past`;
case ViewFilterOperand.IS_IN_FUTURE:
return t`: Future`;
case ViewFilterOperand.IS_TODAY:
return t`: Today`;
return t`: Today${timeZoneAbbreviationSuffix}`;
default:
return ': ';
}
@@ -1,16 +1,22 @@
import { getRelativeDateFilterTimeZoneAbbreviation } from '@/object-record/object-filter-dropdown/utils/getRelativeDateFilterTimeZoneAbbreviation';
import { plural } from 'pluralize';
import { capitalize, type RelativeDateFilter } from 'twenty-shared/utils';
import {
capitalize,
isDefined,
type RelativeDateFilter,
} from 'twenty-shared/utils';
export const getRelativeDateDisplayValue = (
relativeDate: RelativeDateFilter | null,
relativeDate: RelativeDateFilter,
shouldDisplayTimeZoneAbbreviation?: boolean,
) => {
if (!relativeDate) return '';
const { direction, amount, unit } = relativeDate;
const directionStr = capitalize(direction.toLowerCase());
const amountStr = direction === 'THIS' ? '' : amount;
const unitStr =
const directionFormatted = capitalize(direction.toLowerCase());
const amountFormatted = direction === 'THIS' ? '' : amount;
let unitFormatted =
direction === 'THIS'
? unit.toLowerCase()
: amount
@@ -19,7 +25,14 @@ export const getRelativeDateDisplayValue = (
: unit.toLowerCase()
: undefined;
return [directionStr, amountStr, unitStr]
if (isDefined(relativeDate.timezone)) {
const timeZoneAbbreviation =
getRelativeDateFilterTimeZoneAbbreviation(relativeDate);
unitFormatted = `${unitFormatted ?? ''} ${shouldDisplayTimeZoneAbbreviation ? `(${timeZoneAbbreviation})` : ''}`;
}
return [directionFormatted, amountFormatted, unitFormatted]
.filter((item) => item !== undefined)
.join(' ');
};
@@ -0,0 +1,17 @@
import { getTimezoneAbbreviationForZonedDateTime } from '@/ui/input/components/internal/date/utils/getTimeZoneAbbreviationForZonedDateTime';
import { Temporal } from 'temporal-polyfill';
import { type RelativeDateFilter } from 'twenty-shared/utils';
export const getRelativeDateFilterTimeZoneAbbreviation = (
relativeDate: RelativeDateFilter,
) => {
const nowZonedDateTime = Temporal.Now.zonedDateTimeISO(
relativeDate.timezone ?? 'UTC',
);
const timeZoneAbbreviation =
getTimezoneAbbreviationForZonedDateTime(nowZonedDateTime);
return timeZoneAbbreviation;
};
@@ -1,12 +1,14 @@
import { useObjectPermissionsForObject } from '@/object-record/hooks/useObjectPermissionsForObject';
import { hasAnySoftDeleteFilterOnViewComponentSelector } from '@/object-record/record-filter/states/hasAnySoftDeleteFilterOnView';
import { isFieldMetadataReadOnlyByPermissions } from '@/object-record/read-only/utils/internal/isFieldMetadataReadOnlyByPermissions';
import { hasAnySoftDeleteFilterOnViewComponentSelector } from '@/object-record/record-filter/states/hasAnySoftDeleteFilterOnView';
import { recordIndexCalendarFieldMetadataIdState } from '@/object-record/record-index/states/recordIndexCalendarFieldMetadataIdState';
import { useCreateNewIndexRecord } from '@/object-record/record-table/hooks/useCreateNewIndexRecord';
import { useUserTimezone } from '@/ui/input/components/internal/date/hooks/useUserTimezone';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { useRecoilValue } from 'recoil';
import { type Temporal } from 'temporal-polyfill';
import { IconPlus } from 'twenty-ui/display';
import { Button } from 'twenty-ui/input';
import { useRecordCalendarContextOrThrow } from '../contexts/RecordCalendarContext';
@@ -18,12 +20,13 @@ const StyledButton = styled(Button)`
`;
type RecordCalendarAddNewProps = {
cardDate: string;
cardDate: Temporal.PlainDate;
};
export const RecordCalendarAddNew = ({
cardDate,
}: RecordCalendarAddNewProps) => {
const { userTimezone } = useUserTimezone();
const { objectMetadataItem } = useRecordCalendarContextOrThrow();
const theme = useTheme();
@@ -69,7 +72,10 @@ export const RecordCalendarAddNew = ({
<StyledButton
onClick={() => {
createNewIndexRecord({
[calendarFieldMetadataItem.name]: cardDate,
[calendarFieldMetadataItem.name]: cardDate
.toZonedDateTime(userTimezone)
.toInstant()
.toString(),
});
}}
variant="tertiary"
@@ -1,7 +1,8 @@
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/DateTimePicker';
import { DatePickerWithoutCalendar } from '@/ui/input/components/internal/date/components/DatePickerWithoutCalendar';
import { TimeZoneAbbreviation } from '@/ui/input/components/internal/date/components/TimeZoneAbbreviation';
import { Select } from '@/ui/input/components/Select';
import { SelectControl } from '@/ui/input/components/SelectControl';
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
@@ -12,10 +13,14 @@ import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/com
import { useRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentState';
import styled from '@emotion/styled';
import { t } from '@lingui/core/macro';
import { addMonths, format, subMonths } from 'date-fns';
import { format } from 'date-fns';
import { useRecoilValue } from 'recoil';
import { Temporal } from 'temporal-polyfill';
import { type Nullable } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import {
isDefined,
turnPlainDateToShiftedDateInSystemTimeZone,
} from 'twenty-shared/utils';
import { IconChevronLeft, IconChevronRight } from 'twenty-ui/display';
import { Button } from 'twenty-ui/input';
import { ViewCalendarLayout } from '~/generated/graphql';
@@ -60,26 +65,33 @@ export const RecordCalendarTopBar = () => {
const datePickerDropdownId = `record-calendar-date-picker-${recordCalendarId}`;
const { closeDropdown } = useCloseDropdown();
const handleDateChange = (date: Nullable<Date>) => {
if (isDefined(date)) {
setRecordCalendarSelectedDate(date);
const handleDateChange = (plainDateString: Nullable<string>) => {
if (isDefined(plainDateString)) {
setRecordCalendarSelectedDate(Temporal.PlainDate.from(plainDateString));
}
closeDropdown(datePickerDropdownId);
};
const handlePreviousMonth = () => {
setRecordCalendarSelectedDate(subMonths(recordCalendarSelectedDate, 1));
setRecordCalendarSelectedDate(
recordCalendarSelectedDate.subtract({ months: 1 }),
);
};
const handleNextMonth = () => {
setRecordCalendarSelectedDate(addMonths(recordCalendarSelectedDate, 1));
setRecordCalendarSelectedDate(
recordCalendarSelectedDate?.add({ months: 1 }),
);
};
const handleTodayClick = () => {
setRecordCalendarSelectedDate(new Date());
setRecordCalendarSelectedDate(Temporal.Now.plainDateISO());
};
const formattedDate = format(recordCalendarSelectedDate, 'MMMM yyyy');
const formattedDate = format(
turnPlainDateToShiftedDateInSystemTimeZone(recordCalendarSelectedDate),
'MMMM yyyy',
);
const dropdownContentOffset = { x: 140, y: 0 } satisfies DropdownOffset;
@@ -120,20 +132,19 @@ export const RecordCalendarTopBar = () => {
}
dropdownComponents={
<DropdownContent widthInPixels={280}>
<DateTimePicker
<DatePickerWithoutCalendar
instanceId={recordCalendarId}
date={recordCalendarSelectedDate}
date={recordCalendarSelectedDate.toString()}
onChange={handleDateChange}
onClose={handleDateChange}
onEnter={handleDateChange}
onEscape={handleDateChange}
clearable={false}
hideHeaderInput
/>
</DropdownContent>
}
dropdownOffset={dropdownContentOffset}
/>
<TimeZoneAbbreviation instant={Temporal.Now.instant()} />
</StyledLeftSection>
<StyledNavigationSection>
@@ -1,6 +1,7 @@
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
import { useRecordCalendarGroupByRecords } from '@/object-record/record-calendar/hooks/useRecordCalendarGroupByRecords';
import { RecordCalendarComponentInstanceContext } from '@/object-record/record-calendar/states/contexts/RecordCalendarComponentInstanceContext';
import { hasInitializedRecordCalendarSelectedDateComponentState } from '@/object-record/record-calendar/states/hasInitializedRecordCalendarSelectedDateComponentState';
import { recordCalendarRecordIdsComponentState } from '@/object-record/record-calendar/states/recordCalendarRecordIdsComponentState';
import { recordCalendarSelectedDateComponentState } from '@/object-record/record-calendar/states/recordCalendarSelectedDateComponentState';
import { recordCalendarSelectedRecordIdsComponentSelector } from '@/object-record/record-calendar/states/selectors/recordCalendarSelectedRecordIdsComponentSelector';
@@ -38,11 +39,24 @@ export const RecordIndexCalendarDataLoaderEffect = () => {
recordCalendarSelectedDate,
);
const hasInitializedRecordCalendarSelectedDate = useRecoilComponentValue(
hasInitializedRecordCalendarSelectedDateComponentState,
);
useEffect(() => {
if (!hasInitializedRecordCalendarSelectedDate) {
return;
}
upsertRecordsInStore({ partialRecords: records });
const recordIds = records.map((record) => record.id);
setRecordCalendarRecordIds(recordIds);
}, [records, setRecordCalendarRecordIds, upsertRecordsInStore]);
}, [
hasInitializedRecordCalendarSelectedDate,
records,
setRecordCalendarRecordIds,
upsertRecordsInStore,
]);
useEffect(() => {
setContextStoreTargetedRecords({
@@ -0,0 +1,37 @@
import { hasInitializedRecordCalendarSelectedDateComponentState } from '@/object-record/record-calendar/states/hasInitializedRecordCalendarSelectedDateComponentState';
import { recordCalendarSelectedDateComponentState } from '@/object-record/record-calendar/states/recordCalendarSelectedDateComponentState';
import { useUserTimezone } from '@/ui/input/components/internal/date/hooks/useUserTimezone';
import { useRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentState';
import { useEffect } from 'react';
import { Temporal } from 'temporal-polyfill';
export const RecordIndexCalendarSelectedDateInitEffect = () => {
const [, setRecordCalendarSelectedDate] = useRecoilComponentState(
recordCalendarSelectedDateComponentState,
);
const [
hasInitializedRecordCalendarSelectedDate,
setHasInitializedRecordCalendarSelectedDate,
] = useRecoilComponentState(
hasInitializedRecordCalendarSelectedDateComponentState,
);
const { userTimezone } = useUserTimezone();
useEffect(() => {
if (!hasInitializedRecordCalendarSelectedDate) {
setRecordCalendarSelectedDate(
Temporal.Now.zonedDateTimeISO(userTimezone).toPlainDate(),
);
setHasInitializedRecordCalendarSelectedDate(true);
}
}, [
hasInitializedRecordCalendarSelectedDate,
setHasInitializedRecordCalendarSelectedDate,
setRecordCalendarSelectedDate,
userTimezone,
]);
return <></>;
};
@@ -9,15 +9,21 @@ import { useRecordsFieldVisibleGqlFields } from '@/object-record/record-field/ho
import { recordIndexCalendarFieldMetadataIdState } from '@/object-record/record-index/states/recordIndexCalendarFieldMetadataIdState';
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
import { buildGroupByFieldObject } from '@/page-layout/widgets/graph/utils/buildGroupByFieldObject';
import { useUserTimezone } from '@/ui/input/components/internal/date/hooks/useUserTimezone';
import { useQuery } from '@apollo/client';
import { useMemo } from 'react';
import { useRecoilValue } from 'recoil';
import { type Temporal } from 'temporal-polyfill';
import { ObjectRecordGroupByDateGranularity } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
export const useRecordCalendarGroupByRecords = (selectedDate: Date) => {
export const useRecordCalendarGroupByRecords = (
selectedDate: Temporal.PlainDate,
) => {
const { objectMetadataItem } = useRecordCalendarContextOrThrow();
const { userTimezone } = useUserTimezone();
const recordIndexCalendarFieldMetadataId = useRecoilValue(
recordIndexCalendarFieldMetadataIdState,
);
@@ -40,6 +46,7 @@ export const useRecordCalendarGroupByRecords = (selectedDate: Date) => {
buildGroupByFieldObject({
field: calendarFieldMetadataItem,
dateGranularity: ObjectRecordGroupByDateGranularity.DAY,
timeZone: userTimezone,
}),
];
@@ -13,6 +13,7 @@ import {
type DragStart,
type OnDragEndResponder,
} from '@hello-pangea/dnd';
import { isDefined } from 'twenty-shared/utils';
const StyledContainer = styled.div`
display: flex;
@@ -26,6 +27,10 @@ export const RecordCalendarMonth = () => {
recordCalendarSelectedDateComponentState,
);
if (!isDefined(recordCalendarSelectedDate)) {
throw new Error(`Cannot show RecordCalendarMonth without a selected date`);
}
const { processCalendarCardDrop } = useProcessCalendarCardDrop();
const { startRecordDrag } = useStartRecordDrag();
const { endRecordDrag } = useEndRecordDrag();
@@ -1,8 +1,6 @@
import { RecordCalendarMonthBodyWeek } from '@/object-record/record-calendar/month/components/RecordCalendarMonthBodyWeek';
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;
@@ -20,7 +18,7 @@ export const RecordCalendarMonthBody = () => {
<StyledContainer>
{weekFirstDays.map((weekFirstDay) => (
<RecordCalendarMonthBodyWeek
key={`week-${format(weekFirstDay, DATE_TYPE_FORMAT)}`}
key={`week-${weekFirstDay.toString()}`}
startDayOfWeek={weekFirstDay}
/>
))}
@@ -1,14 +1,20 @@
import { RecordCalendarCardDraggableContainer } from '@/object-record/record-calendar/record-calendar-card/components/RecordCalendarCardDraggableContainer';
import { recordCalendarSelectedDateComponentState } from '@/object-record/record-calendar/states/recordCalendarSelectedDateComponentState';
import { calendarDayRecordIdsComponentFamilySelector } from '@/object-record/record-calendar/states/selectors/calendarDayRecordsComponentFamilySelector';
import { useUserTimezone } from '@/ui/input/components/internal/date/hooks/useUserTimezone';
import { useRecoilComponentFamilyValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValue';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { css } from '@emotion/react';
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 { Temporal } from 'temporal-polyfill';
import {
isDefined,
isPlainDateInSameMonth,
isPlainDateInWeekend,
isSamePlainDate,
} from 'twenty-shared/utils';
import { RecordCalendarAddNew } from '../../components/RecordCalendarAddNew';
const StyledContainer = styled.div<{
@@ -94,34 +100,40 @@ const StyledCardsContainer = styled.div<{ isDraggedOver?: boolean }>`
`;
type RecordCalendarMonthBodyDayProps = {
day: Date;
day: Temporal.PlainDate;
};
export const RecordCalendarMonthBodyDay = ({
day,
}: RecordCalendarMonthBodyDayProps) => {
const { userTimezone } = useUserTimezone();
const recordCalendarSelectedDate = useRecoilComponentValue(
recordCalendarSelectedDateComponentState,
);
const dayKey = format(day, DATE_TYPE_FORMAT);
const dayKey = day.toString();
const recordIds = useRecoilComponentFamilyValue(
calendarDayRecordIdsComponentFamilySelector,
dayKey,
{
day: day,
timeZone: userTimezone,
},
);
const todayInUserTimeZone =
Temporal.Now.zonedDateTimeISO(userTimezone).toPlainDate();
const [hovered, setHovered] = useState(false);
const isToday = isSameDay(day, new Date());
const isToday = isSamePlainDate(day, todayInUserTimeZone);
const isOtherMonth = !isSameMonth(day, recordCalendarSelectedDate);
const isOtherMonth = isDefined(recordCalendarSelectedDate)
? !isPlainDateInSameMonth(day, recordCalendarSelectedDate)
: false;
const isDayOfWeekend = isWeekend(day);
const utcDate = new Date(
Date.UTC(day.getFullYear(), day.getMonth(), day.getDate()),
);
const isDayOfWeekend = isPlainDateInWeekend(day);
return (
<StyledContainer
@@ -131,11 +143,9 @@ export const RecordCalendarMonthBodyDay = ({
onMouseLeave={() => setHovered(false)}
>
<StyledDayHeader>
{hovered && <RecordCalendarAddNew cardDate={utcDate.toISOString()} />}
{hovered && <RecordCalendarAddNew cardDate={day} />}
<StyledDayHeaderDayContainer>
<StyledDayHeaderDay isToday={isToday}>
{day.getDate()}
</StyledDayHeaderDay>
<StyledDayHeaderDay isToday={isToday}>{day.day}</StyledDayHeaderDay>
</StyledDayHeaderDayContainer>
</StyledDayHeader>
<Droppable droppableId={dayKey}>
@@ -2,6 +2,11 @@ import { RecordCalendarMonthBodyDay } from '@/object-record/record-calendar/mont
import { useRecordCalendarMonthContextOrThrow } from '@/object-record/record-calendar/month/contexts/RecordCalendarMonthContext';
import styled from '@emotion/styled';
import { eachDayOfInterval, endOfWeek } from 'date-fns';
import { type Temporal } from 'temporal-polyfill';
import {
turnJSDateToPlainDate,
turnPlainDateToShiftedDateInSystemTimeZone,
} from 'twenty-shared/utils';
const StyledContainer = styled.div`
display: flex;
@@ -14,7 +19,7 @@ const StyledContainer = styled.div`
`;
type RecordCalendarMonthBodyWeekProps = {
startDayOfWeek: Date;
startDayOfWeek: Temporal.PlainDate;
};
export const RecordCalendarMonthBodyWeek = ({
@@ -23,8 +28,8 @@ export const RecordCalendarMonthBodyWeek = ({
const { weekStartsOnDayIndex } = useRecordCalendarMonthContextOrThrow();
const daysOfWeek = eachDayOfInterval({
start: startDayOfWeek,
end: endOfWeek(startDayOfWeek, {
start: turnPlainDateToShiftedDateInSystemTimeZone(startDayOfWeek),
end: endOfWeek(turnPlainDateToShiftedDateInSystemTimeZone(startDayOfWeek), {
weekStartsOn: weekStartsOnDayIndex,
}),
});
@@ -32,7 +37,10 @@ export const RecordCalendarMonthBodyWeek = ({
return (
<StyledContainer>
{daysOfWeek.map((day, index) => (
<RecordCalendarMonthBodyDay key={`day-${index}`} day={day} />
<RecordCalendarMonthBodyDay
key={`day-${index}`}
day={turnJSDateToPlainDate(day)}
/>
))}
</StyledContainer>
);
@@ -1,12 +1,13 @@
import { type Temporal } from 'temporal-polyfill';
import { createRequiredContext } from '~/utils/createRequiredContext';
type RecordCalendarMonthContextValue = {
firstDayOfMonth: Date;
lastDayOfMonth: Date;
firstDayOfFirstWeek: Date;
lastDayOfLastWeek: Date;
firstDayOfMonth: Temporal.PlainDate;
lastDayOfMonth: Temporal.PlainDate;
firstDayOfFirstWeek: Temporal.PlainDate;
lastDayOfLastWeek: Temporal.PlainDate;
weekDayLabels: string[];
weekFirstDays: Date[];
weekFirstDays: Temporal.PlainDate[];
weekStartsOnDayIndex: 0 | 1 | 2 | 3 | 4 | 5 | 6;
};
@@ -1,5 +1,5 @@
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { CalendarStartDay } from 'twenty-shared';
import { CalendarStartDay } from 'twenty-shared/constants';
import { detectCalendarStartDay } from '@/localization/utils/detection/detectCalendarStartDay';
import {
@@ -7,15 +7,22 @@ import {
eachWeekOfInterval,
endOfWeek,
format,
lastDayOfMonth as lastDayOfMonthFn,
startOfMonth,
startOfWeek,
} from 'date-fns';
import { useRecoilValue } from 'recoil';
import { type Temporal } from 'temporal-polyfill';
import {
turnJSDateToPlainDate,
turnPlainDateToShiftedDateInSystemTimeZone,
} from 'twenty-shared/utils';
import { dateLocaleState } from '~/localization/states/dateLocaleState';
export const useRecordCalendarMonthDaysRange = (selectedDate: Date) => {
// TODO: we could refactor this to use Temporal.PlainDate directly
// But it would require recoding the utils here, not really worth it for now
export const useRecordCalendarMonthDaysRange = (
selectedDate: Temporal.PlainDate,
) => {
const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState);
const dateLocale = useRecoilValue(dateLocaleState);
@@ -29,36 +36,52 @@ export const useRecordCalendarMonthDaysRange = (selectedDate: Date) => {
: (currentWorkspaceMember?.calendarStartDay ?? 0)
) as 0 | 1 | 2 | 3 | 4 | 5 | 6;
const firstDayOfMonth = startOfMonth(selectedDate);
const lastDayOfMonth = lastDayOfMonthFn(selectedDate);
const firstDayOfMonth = selectedDate.with({ day: 1 });
const lastDayOfMonth = selectedDate
.with({ day: 1 })
.add({ months: 1 })
.subtract({ days: 1 });
const firstDayOfFirstWeek = startOfWeek(firstDayOfMonth, {
weekStartsOn: weekStartsOnDayIndex,
locale: dateLocale.localeCatalog,
});
const shiftedFirstDayOfMonth =
turnPlainDateToShiftedDateInSystemTimeZone(firstDayOfMonth);
const lastDayOfLastWeek = endOfWeek(lastDayOfMonth, {
weekStartsOn: weekStartsOnDayIndex,
locale: dateLocale.localeCatalog,
});
const firstDayOfFirstWeek = turnJSDateToPlainDate(
startOfWeek(shiftedFirstDayOfMonth, {
weekStartsOn: weekStartsOnDayIndex,
locale: dateLocale.localeCatalog,
}),
);
const shiftedLastDayOfMonth =
turnPlainDateToShiftedDateInSystemTimeZone(lastDayOfMonth);
const lastDayOfLastWeek = turnJSDateToPlainDate(
endOfWeek(shiftedLastDayOfMonth, {
weekStartsOn: weekStartsOnDayIndex,
locale: dateLocale.localeCatalog,
}),
);
const daysOfWeekLabels: string[] = [];
for (let i = 0; i < 7; i++) {
const day = addDays(firstDayOfFirstWeek, i);
const day = addDays(
turnPlainDateToShiftedDateInSystemTimeZone(firstDayOfFirstWeek),
i,
);
const label = format(day, 'EEE', { locale: dateLocale.localeCatalog });
daysOfWeekLabels.push(label);
}
const weekFirstDays = eachWeekOfInterval(
{
start: firstDayOfFirstWeek,
end: lastDayOfLastWeek,
start: turnPlainDateToShiftedDateInSystemTimeZone(firstDayOfFirstWeek),
end: turnPlainDateToShiftedDateInSystemTimeZone(lastDayOfLastWeek),
},
{
weekStartsOn: weekStartsOnDayIndex,
},
);
).map(turnJSDateToPlainDate);
return {
weekStartsOnDayIndex,
@@ -6,24 +6,31 @@ import { anyFieldFilterValueComponentState } from '@/object-record/record-filter
import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
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 { t } from '@lingui/core/macro';
import { useGetCurrentViewOnly } from '@/views/hooks/useGetCurrentViewOnly';
import { t } from '@lingui/core/macro';
import { type Temporal } from 'temporal-polyfill';
import {
combineFilters,
computeRecordGqlOperationFilter,
isDefined,
turnAnyFieldFilterIntoRecordGqlFilter,
turnPlainDateIntoUserTimeZoneInstantString,
} from 'twenty-shared/utils';
const DATE_RANGE_FILTER_AFTER_ID = 'DATE_RANGE_FILTER_AFTER_ID';
const DATE_RANGE_FILTER_BEFORE_ID = 'DATE_RANGE_FILTER_BEFORE_ID';
export const useRecordCalendarQueryDateRangeFilter = (selectedDate: Date) => {
export const useRecordCalendarQueryDateRangeFilter = (
selectedDate: Temporal.PlainDate,
) => {
const { objectMetadataItem } = useRecordCalendarContextOrThrow();
const { firstDayOfFirstWeek, lastDayOfLastWeek } =
useRecordCalendarMonthDaysRange(selectedDate);
const { userTimezone } = useUserTimezone();
const { currentView } = useGetCurrentViewOnly();
const currentRecordFilterGroups = useRecoilComponentValue(
@@ -49,26 +56,38 @@ export const useRecordCalendarQueryDateRangeFilter = (selectedDate: Date) => {
};
}
const firstDayOfFirstWeekISOString =
turnPlainDateIntoUserTimeZoneInstantString(
firstDayOfFirstWeek,
userTimezone,
);
const nextDayAfterLastDayOfLastWeekISOString =
turnPlainDateIntoUserTimeZoneInstantString(
lastDayOfLastWeek.add({ days: 1 }),
userTimezone,
);
const dateRangeFilterFieldMetadataId = currentView.calendarFieldMetadataId;
const dateRangeFilterAfter: RecordFilter = {
id: DATE_RANGE_FILTER_AFTER_ID,
fieldMetadataId: dateRangeFilterFieldMetadataId,
value: `${firstDayOfFirstWeek.toISOString()}`,
value: `${firstDayOfFirstWeekISOString}`,
operand: RecordFilterOperand.IS_AFTER,
type: 'DATE',
label: t`After`,
displayValue: `${firstDayOfFirstWeek.toISOString()}`,
type: 'DATE_TIME',
label: t`After or equal`,
displayValue: `${firstDayOfFirstWeek.toString()}`,
};
const dateRangeFilterBefore: RecordFilter = {
id: DATE_RANGE_FILTER_BEFORE_ID,
fieldMetadataId: dateRangeFilterFieldMetadataId,
value: `${lastDayOfLastWeek.toISOString()}`,
value: `${nextDayAfterLastDayOfLastWeekISOString}`,
operand: RecordFilterOperand.IS_BEFORE,
type: 'DATE',
type: 'DATE_TIME',
label: t`Before`,
displayValue: `${lastDayOfLastWeek.toISOString()}`,
displayValue: `${lastDayOfLastWeek.toString()}`,
};
const dateRangeFilter = computeRecordGqlOperationFilter({
@@ -0,0 +1,9 @@
import { RecordCalendarComponentInstanceContext } from '@/object-record/record-calendar/states/contexts/RecordCalendarComponentInstanceContext';
import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState';
export const hasInitializedRecordCalendarSelectedDateComponentState =
createComponentState<boolean>({
key: 'hasInitializedRecordCalendarSelectedDateComponentState',
defaultValue: false,
componentInstanceContext: RecordCalendarComponentInstanceContext,
});
@@ -1,9 +1,10 @@
import { RecordCalendarComponentInstanceContext } from '@/object-record/record-calendar/states/contexts/RecordCalendarComponentInstanceContext';
import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState';
import { Temporal } from 'temporal-polyfill';
export const recordCalendarSelectedDateComponentState =
createComponentState<Date>({
createComponentState<Temporal.PlainDate>({
key: 'recordCalendarSelectedDateComponentState',
defaultValue: new Date(),
defaultValue: Temporal.Now.plainDateISO(),
componentInstanceContext: RecordCalendarComponentInstanceContext,
});
@@ -1,19 +1,26 @@
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { hasObjectMetadataItemPositionField } from '@/object-metadata/utils/hasObjectMetadataItemPositionField';
import { RecordCalendarComponentInstanceContext } from '@/object-record/record-calendar/states/contexts/RecordCalendarComponentInstanceContext';
import { recordCalendarRecordIdsComponentState } from '@/object-record/record-calendar/states/recordCalendarRecordIdsComponentState';
import { recordIndexCalendarFieldMetadataIdState } from '@/object-record/record-index/states/recordIndexCalendarFieldMetadataIdState';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { createComponentFamilySelector } from '@/ui/utilities/state/component-state/utils/createComponentFamilySelector';
import { isSameDay, parse } from 'date-fns';
import { isDefined } from 'twenty-shared/utils';
import { isNonEmptyString } from '@sniptt/guards';
import { Temporal } from 'temporal-polyfill';
import { isDefined, isSamePlainDate } from 'twenty-shared/utils';
import { FieldMetadataType } from '~/generated-metadata/graphql';
export const calendarDayRecordIdsComponentFamilySelector =
createComponentFamilySelector<string[], string>({
createComponentFamilySelector<
string[],
{ day: Temporal.PlainDate; timeZone: string }
>({
key: 'calendarDayRecordsComponentFamilySelector',
componentInstanceContext: RecordCalendarComponentInstanceContext,
get:
({ instanceId, familyKey: dayAsString }) =>
({ instanceId, familyKey: { day, timeZone } }) =>
({ get }) => {
const calendarFieldMetadataId = get(
recordIndexCalendarFieldMetadataIdState,
@@ -51,14 +58,18 @@ export const calendarDayRecordIdsComponentFamilySelector =
const record = get(recordStoreFamilyState(recordId));
const recordDate = record?.[fieldMetadataItem.name];
if (!recordDate) {
if (!isNonEmptyString(recordDate)) {
return false;
}
const dayDate = parse(dayAsString, 'yyyy-MM-dd', new Date());
const recordDateObj = new Date(recordDate);
const recordDateAsPlainDateInTimeZone =
fieldMetadataItem.type === FieldMetadataType.DATE
? Temporal.PlainDate.from(recordDate)
: Temporal.Instant.from(recordDate)
.toZonedDateTimeISO(timeZone)
.toPlainDate();
return isSameDay(recordDateObj, dayDate);
return isSamePlainDate(day, recordDateAsPlainDateInTimeZone);
});
if (
@@ -6,20 +6,12 @@ import { useRecordCalendarContextOrThrow } from '@/object-record/record-calendar
import { calendarDayRecordIdsComponentFamilySelector } from '@/object-record/record-calendar/states/selectors/calendarDayRecordsComponentFamilySelector';
import { extractRecordPositions } from '@/object-record/record-drag/utils/extractRecordPositions';
import { isFieldDateTime } from '@/object-record/record-field/ui/types/guards/isFieldDateTime';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { computeNewPositionOfDraggedRecord } from '@/object-record/utils/computeNewPositionOfDraggedRecord';
import { useUserTimezone } from '@/ui/input/components/internal/date/hooks/useUserTimezone';
import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState';
import { useGetCurrentViewOnly } from '@/views/hooks/useGetCurrentViewOnly';
import {
formatISO,
getHours,
getMilliseconds,
getMinutes,
getSeconds,
parse,
set,
} from 'date-fns';
import { Temporal } from 'temporal-polyfill';
import { isDefined } from 'twenty-shared/utils';
export const useProcessCalendarCardDrop = () => {
@@ -29,6 +21,8 @@ export const useProcessCalendarCardDrop = () => {
objectNameSingular: objectMetadataItem.nameSingular,
});
const { userTimezone } = useUserTimezone();
const calendarDayRecordIdsSelector = useRecoilComponentCallbackState(
calendarDayRecordIdsComponentFamilySelector,
);
@@ -46,6 +40,8 @@ export const useProcessCalendarCardDrop = () => {
const destinationDate = calendarCardDropResult.destination.droppableId;
const destinationIndex = calendarCardDropResult.destination.index;
const destinationPlainDate = Temporal.PlainDate.from(destinationDate);
const record = snapshot
.getLoadable(recordStoreFamilyState(recordId))
.getValue();
@@ -59,7 +55,12 @@ export const useProcessCalendarCardDrop = () => {
if (!calendarFieldMetadata) return;
const destinationRecordIds = snapshot
.getLoadable(calendarDayRecordIdsSelector(destinationDate))
.getLoadable(
calendarDayRecordIdsSelector({
day: destinationPlainDate,
timeZone: userTimezone,
}),
)
.getValue() as string[];
const targetDayIsEmpty = destinationRecordIds.length === 0;
@@ -73,9 +74,15 @@ export const useProcessCalendarCardDrop = () => {
destinationRecordIds,
snapshot,
);
const droppedRecordIsFromAnotherList = !recordsWithPosition
.map((recordWithPosition) => recordWithPosition.id)
.includes(recordId);
const isDroppedAfterList =
destinationIndex >= recordsWithPosition.length;
(recordsWithPosition.length === 2 &&
destinationIndex === 1 &&
!droppedRecordIsFromAnotherList) ||
destinationIndex === recordsWithPosition.length;
const targetRecord = isDroppedAfterList
? recordsWithPosition.at(-1)
@@ -95,38 +102,38 @@ export const useProcessCalendarCardDrop = () => {
});
}
const targetDate = parse(destinationDate, 'yyyy-MM-dd', new Date());
const currentFieldValue = record[calendarFieldMetadata.name];
let newDate: Date;
if (
isDefined(currentFieldValue) &&
isFieldDateTime(calendarFieldMetadata)
) {
const currentDateTime = new Date(currentFieldValue);
newDate = set(targetDate, {
hours: getHours(currentDateTime),
minutes: getMinutes(currentDateTime),
seconds: getSeconds(currentDateTime),
milliseconds: getMilliseconds(currentDateTime),
});
} else {
newDate = targetDate;
}
const currentZonedDateTime = isDefined(currentFieldValue)
? Temporal.Instant.from(currentFieldValue).toZonedDateTimeISO(
userTimezone,
)
: null;
const newDate = isDefined(currentZonedDateTime)
? currentZonedDateTime.with({
day: destinationPlainDate.day,
month: destinationPlainDate.month,
year: destinationPlainDate.year,
})
: Temporal.PlainDate.from(destinationPlainDate).toZonedDateTime(
userTimezone,
);
await updateOneRecord({
idToUpdate: recordId,
updateOneRecordInput: {
[calendarFieldMetadata.name]: formatISO(newDate),
[calendarFieldMetadata.name]: newDate.toInstant().toString(),
position: newPosition,
},
});
},
[
objectMetadataItem,
currentView,
updateOneRecord,
objectMetadataItem.fields,
calendarDayRecordIdsSelector,
userTimezone,
updateOneRecord,
],
);
@@ -62,6 +62,7 @@ type FormFieldInputProps = {
placeholder?: string;
error?: string;
onError?: (error: string | undefined) => void;
timeZone?: string;
};
export const FormFieldInput = ({
@@ -73,6 +74,7 @@ export const FormFieldInput = ({
placeholder,
error,
onError,
timeZone,
}: FormFieldInputProps) => {
return isFieldNumber(field) || field.type === FieldMetadataType.NUMERIC ? (
<FormNumberFieldInput
@@ -167,6 +169,7 @@ export const FormFieldInput = ({
onChange={onChange}
VariablePicker={VariablePicker}
readonly={readonly}
timeZone={timeZone}
/>
) : isFieldMultiSelect(field) ? (
<FormMultiSelectFieldInput
@@ -338,7 +338,7 @@ export const FormDateFieldInput = ({
<OverlayContainer>
<DatePicker
instanceId={instanceId}
date={pickerDate}
plainDateString={pickerDate}
onChange={handlePickerChange}
onClose={handlePickerMouseSelect}
onEnter={handlePickerEnter}
@@ -9,29 +9,20 @@ import {
MONTH_AND_YEAR_DROPDOWN_MONTH_SELECT_ID,
MONTH_AND_YEAR_DROPDOWN_YEAR_SELECT_ID,
} from '@/ui/input/components/internal/date/components/DateTimePicker';
import { MAX_DATE } from '@/ui/input/components/internal/date/constants/MaxDate';
import { MIN_DATE } from '@/ui/input/components/internal/date/constants/MinDate';
import { useParseDateTimeInputStringToJSDate } from '@/ui/input/components/internal/date/hooks/useParseDateTimeInputStringToJSDate';
import { useParseJSDateToIMaskDateTimeInputString } from '@/ui/input/components/internal/date/hooks/useParseJSDateToIMaskDateTimeInputString';
import { DateTimePickerInput } from '@/ui/input/components/internal/date/components/DateTimePickerInput';
import { useUserTimezone } from '@/ui/input/components/internal/date/hooks/useUserTimezone';
import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
import { OverlayContainer } from '@/ui/layout/overlay/components/OverlayContainer';
import { useHotkeysOnFocusedElement } from '@/ui/utilities/hotkey/hooks/useHotkeysOnFocusedElement';
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
import { isStandaloneVariableString } from '@/workflow/utils/isStandaloneVariableString';
import { css } from '@emotion/react';
import styled from '@emotion/styled';
import { isNonEmptyString } from '@sniptt/guards';
import {
useId,
useRef,
useState,
type ChangeEvent,
type KeyboardEvent,
} from 'react';
import { useId, useRef, useState } from 'react';
import { Temporal } from 'temporal-polyfill';
import { Key } from 'ts-key-enum';
import { isDefined } from 'twenty-shared/utils';
import { TEXT_INPUT_STYLE } from 'twenty-ui/theme';
import { type Nullable } from 'twenty-ui/utilities';
const StyledInputContainer = styled(FormFieldInputInnerContainer)`
@@ -47,18 +38,9 @@ const StyledDateInputAbsoluteContainer = styled.div`
top: ${({ theme }) => theme.spacing(1)};
`;
const StyledDateInput = styled.input<{ hasError?: boolean }>`
${TEXT_INPUT_STYLE}
&:disabled {
color: ${({ theme }) => theme.font.color.tertiary};
}
${({ hasError, theme }) =>
hasError &&
css`
color: ${theme.color.red};
`};
const StyledDateInputTextContainer = styled.div`
align-items: center;
display: flex;
`;
const StyledDateInputContainer = styled.div`
@@ -84,6 +66,7 @@ type FormDateTimeFieldInputProps = {
placeholder?: string;
VariablePicker?: VariablePickerComponent;
readonly?: boolean;
timeZone?: string;
};
export const FormDateTimeFieldInput = ({
@@ -92,15 +75,10 @@ export const FormDateTimeFieldInput = ({
onChange,
VariablePicker,
readonly,
placeholder,
timeZone,
}: FormDateTimeFieldInputProps) => {
const instanceId = useId();
const { parseJSDateToDateTimeInputString: parseDateTimeToString } =
useParseJSDateToIMaskDateTimeInputString();
const { parseDateTimeInputStringToJSDate: parseStringToDateTime } =
useParseDateTimeInputStringToJSDate();
const [draftValue, setDraftValue] = useState<DraftValue>(
isStandaloneVariableString(defaultValue)
? {
@@ -109,34 +87,18 @@ export const FormDateTimeFieldInput = ({
}
: {
type: 'static',
value: defaultValue ?? null,
value: defaultValue !== 'null' ? (defaultValue ?? null) : null,
mode: 'view',
},
);
const draftValueAsDate =
isDefined(draftValue.value) &&
isNonEmptyString(draftValue.value) &&
draftValue.type === 'static'
? new Date(draftValue.value)
: null;
const [pickerDate, setPickerDate] =
useState<Nullable<Date>>(draftValueAsDate);
const datePickerWrapperRef = useRef<HTMLDivElement>(null);
const [inputDateTime, setInputDateTime] = useState(
isDefined(draftValueAsDate) && !isStandaloneVariableString(defaultValue)
? parseDateTimeToString(draftValueAsDate)
: '',
);
const persistDate = (newDate: Nullable<Date>) => {
const persistDate = (newDate: Nullable<Temporal.ZonedDateTime>) => {
if (!isDefined(newDate)) {
onChange(null);
} else {
const newDateISO = newDate.toISOString();
const newDateISO = newDate.toInstant().toString();
onChange(newDateISO);
}
@@ -148,8 +110,6 @@ export const FormDateTimeFieldInput = ({
const displayDatePicker =
draftValue.type === 'static' && draftValue.mode === 'edit';
const placeholderToDisplay = placeholder ?? 'mm/dd/yyyy hh:mm';
useListenClickOutside({
refs: [datePickerWrapperRef],
listenerId: 'FormDateTimeFieldInputBase',
@@ -167,17 +127,13 @@ export const FormDateTimeFieldInput = ({
],
});
const handlePickerChange = (newDate: Nullable<Date>) => {
const handlePickerChange = (newDate: Nullable<Temporal.ZonedDateTime>) => {
setDraftValue({
type: 'static',
mode: 'edit',
value: newDate?.toDateString() ?? null,
value: newDate?.toPlainDate().toString() ?? null,
});
setInputDateTime(isDefined(newDate) ? parseDateTimeToString(newDate) : '');
setPickerDate(newDate);
persistDate(newDate);
};
@@ -208,24 +164,18 @@ export const FormDateTimeFieldInput = ({
mode: 'view',
});
setPickerDate(null);
setInputDateTime('');
persistDate(null);
};
const handlePickerMouseSelect = (newDate: Nullable<Date>) => {
const handlePickerMouseSelect = (
newDate: Nullable<Temporal.ZonedDateTime>,
) => {
setDraftValue({
type: 'static',
value: newDate?.toDateString() ?? null,
value: newDate?.toPlainDate().toString() ?? null,
mode: 'view',
});
setPickerDate(newDate);
setInputDateTime(isDefined(newDate) ? parseDateTimeToString(newDate) : '');
persistDate(newDate);
};
@@ -237,46 +187,18 @@ export const FormDateTimeFieldInput = ({
});
};
const handleInputChange = (event: ChangeEvent<HTMLInputElement>) => {
setInputDateTime(event.target.value);
};
const handleInputKeydown = (event: KeyboardEvent<HTMLInputElement>) => {
if (event.key !== 'Enter') {
const handleInputChange = (newDate: Temporal.ZonedDateTime | null) => {
if (!isDefined(newDate)) {
return;
}
const inputDateTimeTrimmed = inputDateTime.trim();
if (inputDateTimeTrimmed === '') {
handlePickerClear();
return;
}
const parsedInputDateTime = parseStringToDateTime(inputDateTimeTrimmed);
if (!isDefined(parsedInputDateTime)) {
return;
}
let validatedDate = parsedInputDateTime;
if (parsedInputDateTime < MIN_DATE) {
validatedDate = MIN_DATE;
} else if (parsedInputDateTime > MAX_DATE) {
validatedDate = MAX_DATE;
}
setDraftValue({
type: 'static',
value: validatedDate.toDateString(),
mode: 'edit',
value: newDate.toPlainDate().toString(),
});
setPickerDate(validatedDate);
setInputDateTime(parseDateTimeToString(validatedDate));
persistDate(validatedDate);
persistDate(newDate);
};
const handleVariableTagInsert = (variableName: string) => {
@@ -285,8 +207,6 @@ export const FormDateTimeFieldInput = ({
value: variableName,
});
setInputDateTime('');
onChange(variableName);
};
@@ -297,8 +217,6 @@ export const FormDateTimeFieldInput = ({
mode: 'view',
});
setPickerDate(null);
onChange(null);
};
@@ -309,6 +227,16 @@ export const FormDateTimeFieldInput = ({
dependencies: [handlePickerEscape],
});
const { userTimezone } = useUserTimezone();
const dateValue = isStandaloneVariableString(defaultValue)
? null
: defaultValue === 'null' || defaultValue === '' || !isDefined(defaultValue)
? null
: Temporal.Instant.from(defaultValue).toZonedDateTimeISO(
timeZone ?? userTimezone,
);
return (
<FormFieldInputContainer>
{label ? <InputLabel>{label}</InputLabel> : null}
@@ -321,29 +249,29 @@ export const FormDateTimeFieldInput = ({
>
{draftValue.type === 'static' ? (
<>
<StyledDateInput
type="text"
placeholder={placeholderToDisplay}
value={inputDateTime}
onFocus={handleInputFocus}
onChange={handleInputChange}
onKeyDown={handleInputKeydown}
disabled={readonly}
/>
<StyledDateInputTextContainer>
<DateTimePickerInput
date={dateValue}
onChange={handleInputChange}
onFocus={handleInputFocus}
readonly={readonly}
timeZone={timeZone}
/>
</StyledDateInputTextContainer>
{draftValue.mode === 'edit' ? (
<StyledDateInputContainer>
<StyledDateInputAbsoluteContainer>
<OverlayContainer>
<DateTimePicker
instanceId={instanceId}
date={pickerDate ?? new Date()}
date={dateValue}
onChange={handlePickerChange}
onClose={handlePickerMouseSelect}
onEnter={handlePickerEnter}
onEscape={handlePickerEscape}
onClear={handlePickerClear}
hideHeaderInput
timeZone={timeZone}
/>
</OverlayContainer>
</StyledDateInputAbsoluteContainer>
@@ -357,7 +285,6 @@ export const FormDateTimeFieldInput = ({
/>
)}
</StyledInputContainer>
{VariablePicker && !readonly ? (
<VariablePicker
instanceId={instanceId}
@@ -2,7 +2,9 @@ import { FieldInputEventContext } from '@/object-record/record-field/ui/contexts
import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/ui/states/contexts/RecordFieldComponentInstanceContext';
import { DateTimeInput } from '@/ui/field/input/components/DateTimeInput';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { isNonEmptyString } from '@sniptt/guards';
import { useContext } from 'react';
import { Temporal } from 'temporal-polyfill';
import { type Nullable } from 'twenty-ui/utilities';
import { useDateTimeField } from '../../hooks/useDateTimeField';
@@ -17,44 +19,46 @@ export const DateTimeFieldInput = () => {
RecordFieldComponentInstanceContext,
);
const getDateToPersist = (newDate: Nullable<Date>) => {
if (!newDate) {
const getDateToPersist = (newInstant: Nullable<Temporal.Instant>) => {
if (!newInstant) {
return null;
} else {
const newDateISO = newDate?.toISOString();
const newDateISO = newInstant?.toString();
return newDateISO;
}
};
const handleEnter = (newDate: Nullable<Date>) => {
const handleEnter = (newDate: Nullable<Temporal.Instant>) => {
onEnter?.({ newValue: getDateToPersist(newDate) });
};
const handleEscape = (newDate: Nullable<Date>) => {
const handleEscape = (newDate: Nullable<Temporal.Instant>) => {
onEscape?.({ newValue: getDateToPersist(newDate) });
};
const handleClickOutside = (
event: MouseEvent | TouchEvent,
newDate: Nullable<Date>,
newDate: Nullable<Temporal.Instant>,
) => {
onClickOutside?.({ newValue: getDateToPersist(newDate), event });
};
const handleChange = (newDate: Nullable<Date>) => {
setDraftValue(newDate?.toDateString() ?? '');
const handleChange = (newInstant: Nullable<Temporal.Instant>) => {
setDraftValue(newInstant?.toString() ?? '');
};
const handleClear = () => {
onSubmit?.({ newValue: null });
};
const handleSubmit = (newDate: Nullable<Date>) => {
onSubmit?.({ newValue: getDateToPersist(newDate) });
const handleSubmit = (newInstant: Nullable<Temporal.Instant>) => {
onSubmit?.({ newValue: getDateToPersist(newInstant) });
};
const dateValue = fieldValue ? new Date(fieldValue) : null;
const dateValue = isNonEmptyString(fieldValue)
? Temporal.Instant.from(fieldValue)
: null;
return (
<DateTimeInput
@@ -1,4 +1,5 @@
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { useUserTimezone } from '@/ui/input/components/internal/date/hooks/useUserTimezone';
import { useRecoilValue } from 'recoil';
import { type RecordFilterValueDependencies } from 'twenty-shared/types';
@@ -8,9 +9,12 @@ export const useFilterValueDependencies = (): {
const { id: currentWorkspaceMemberId } =
useRecoilValue(currentWorkspaceMemberState) ?? {};
const { userTimezone } = useUserTimezone();
return {
filterValueDependencies: {
currentWorkspaceMemberId,
timeZone: userTimezone,
},
};
};
@@ -1,33 +1,28 @@
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';
import { getRelativeDateDisplayValue } from '@/object-record/object-filter-dropdown/utils/getRelativeDateDisplayValue';
import { type RecordFilter } from '@/object-record/record-filter/types/RecordFilter';
import { RecordFilterOperand } from '@/object-record/record-filter/types/RecordFilterOperand';
import { isRecordFilterConsideredEmpty } from '@/object-record/record-filter/utils/isRecordFilterConsideredEmpty';
import { useUserTimezone } from '@/ui/input/components/internal/date/hooks/useUserTimezone';
import { UserContext } from '@/users/contexts/UserContext';
import { isValid } from 'date-fns';
import { useContext } from 'react';
import { useRecoilValue } from 'recoil';
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 {
getDateFromPlainDate,
isDefined,
isEmptinessOperand,
parseJson,
relativeDateFilterStringifiedSchema,
shiftPointInTimeFromTimezoneDifferenceInMinutesWithSystemTimezone,
} from 'twenty-shared/utils';
import { dateLocaleState } from '~/localization/states/dateLocaleState';
import { formatDateString } from '~/utils/string/formatDateString';
// TODO: finish the implementation of this hook to obtain filter display value and remove deprecated display value property
export const useGetRecordFilterDisplayValue = () => {
const { dateFormat, timeZone } = useContext(UserContext);
const dateLocale = useRecoilValue(dateLocaleState);
const { userTimezone } = useUserTimezone();
const { isSystemTimezone, userTimezone } = useUserTimezone();
const { getDateTimeFilterDisplayValue } = useGetDateTimeFilterDisplayValue();
const { getDateFilterDisplayValue } = useGetDateFilterDisplayValue();
const { getFieldMetadataItemByIdOrThrow } =
useGetFieldMetadataItemByIdOrThrow();
@@ -44,65 +39,25 @@ export const useGetRecordFilterDisplayValue = () => {
const operandIsEmptiness = isEmptinessOperand(recordFilter.operand);
const recordFilterIsEmpty = isRecordFilterConsideredEmpty(recordFilter);
const nowInZonedDateTime = Temporal.Now.zonedDateTimeISO(userTimezone);
const shouldDisplayTimeZoneAbbreviation = !isSystemTimezone;
const timeZoneAbbreviation =
getTimezoneAbbreviationForZonedDateTime(nowInZonedDateTime);
if (filterType === 'DATE') {
switch (recordFilter.operand) {
case RecordFilterOperand.IS: {
const date = getDateFromPlainDate(recordFilter.value);
const shiftedDate =
shiftPointInTimeFromTimezoneDifferenceInMinutesWithSystemTimezone(
date,
userTimezone,
'add',
);
if (!isValid(date)) {
if (!isDefined(recordFilter.value)) {
return '';
}
const formattedDate = formatDateString({
value: shiftedDate.toISOString(),
timeZone,
dateFormat,
localeCatalog: dateLocale.localeCatalog,
});
const plainDate = Temporal.PlainDate.from(recordFilter.value);
return `${formattedDate}`;
}
case RecordFilterOperand.IS_RELATIVE: {
const relativeDateFilter =
relativeDateFilterStringifiedSchema.safeParse(recordFilter.value);
if (!relativeDateFilter.success) {
return ``;
}
const relativeDateDisplayValue = getRelativeDateDisplayValue(
relativeDateFilter.data,
const { displayValue } = getDateFilterDisplayValue(
plainDate.toZonedDateTime(userTimezone),
);
return ` ${relativeDateDisplayValue}`;
}
case RecordFilterOperand.IS_TODAY:
case RecordFilterOperand.IS_IN_FUTURE:
case RecordFilterOperand.IS_IN_PAST:
return '';
default:
return ` ${recordFilter.displayValue}`;
}
} else if (recordFilter.type === 'DATE_TIME') {
switch (recordFilter.operand) {
case RecordFilterOperand.IS:
case RecordFilterOperand.IS_AFTER:
case RecordFilterOperand.IS_BEFORE: {
const pointInTime = new Date(recordFilter.value);
if (!isValid(pointInTime)) {
return '';
}
const { displayValue } = getDateTimeFilterDisplayValue(pointInTime);
return `${displayValue}`;
}
case RecordFilterOperand.IS_RELATIVE: {
@@ -115,11 +70,57 @@ export const useGetRecordFilterDisplayValue = () => {
const relativeDateDisplayValue = getRelativeDateDisplayValue(
relativeDateFilter.data,
shouldDisplayTimeZoneAbbreviation,
);
return ` ${relativeDateDisplayValue}`;
}
case RecordFilterOperand.IS_TODAY:
return shouldDisplayTimeZoneAbbreviation
? `(${timeZoneAbbreviation})`
: '';
case RecordFilterOperand.IS_IN_FUTURE:
case RecordFilterOperand.IS_IN_PAST:
return '';
default:
return ` ${recordFilter.displayValue}`;
}
} else if (recordFilter.type === 'DATE_TIME') {
switch (recordFilter.operand) {
case RecordFilterOperand.IS:
case RecordFilterOperand.IS_AFTER:
case RecordFilterOperand.IS_BEFORE: {
if (!isNonEmptyString(recordFilter.value)) {
return '';
}
const zonedDateTime = Temporal.Instant.from(
recordFilter.value,
).toZonedDateTimeISO(userTimezone);
const { displayValue } = getDateTimeFilterDisplayValue(zonedDateTime);
return `${displayValue}`;
}
case RecordFilterOperand.IS_RELATIVE: {
const relativeDateFilter =
relativeDateFilterStringifiedSchema.safeParse(recordFilter.value);
if (!relativeDateFilter.success) {
return ``;
}
const relativeDateDisplayValue = getRelativeDateDisplayValue(
relativeDateFilter.data,
shouldDisplayTimeZoneAbbreviation,
);
return `${relativeDateDisplayValue}`;
}
case RecordFilterOperand.IS_TODAY:
return shouldDisplayTimeZoneAbbreviation
? `(${timeZoneAbbreviation})`
: '';
case RecordFilterOperand.IS_IN_FUTURE:
case RecordFilterOperand.IS_IN_PAST:
return '';
@@ -2,7 +2,7 @@ import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMembe
import { detectCalendarStartDay } from '@/localization/utils/detection/detectCalendarStartDay';
import { useUserTimezone } from '@/ui/input/components/internal/date/hooks/useUserTimezone';
import { useRecoilValue } from 'recoil';
import { CalendarStartDay } from 'twenty-shared';
import { CalendarStartDay } from 'twenty-shared/constants';
import { type FirstDayOfTheWeek } from 'twenty-shared/types';
import { type RelativeDateFilter } from 'twenty-shared/utils';
@@ -0,0 +1,14 @@
import { useUserTimezone } from '@/ui/input/components/internal/date/hooks/useUserTimezone';
import { getTimezoneAbbreviationForZonedDateTime } from '@/ui/input/components/internal/date/utils/getTimeZoneAbbreviationForZonedDateTime';
import { Temporal } from 'temporal-polyfill';
export const useTimeZoneAbbreviationForNowInUserTimeZone = () => {
const { userTimezone } = useUserTimezone();
const nowZonedDateTime = Temporal.Now.zonedDateTimeISO(userTimezone);
const userTimeZoneAbbreviation =
getTimezoneAbbreviationForZonedDateTime(nowZonedDateTime);
return { userTimeZoneAbbreviation };
};
@@ -26,6 +26,7 @@ const personMockObjectMetadataItem = getMockObjectMetadataItemOrThrow('person');
const mockFilterValueDependencies: RecordFilterValueDependencies = {
currentWorkspaceMemberId: '32219445-f587-4c40-b2b1-6d3205ed96da',
timeZone: 'Europe/Paris',
};
jest.useFakeTimers().setSystemTime(new Date('2020-01-01'));
@@ -1068,7 +1069,7 @@ describe('should work as expected for the different field types', () => {
and: [
{
createdAt: {
gt: '2024-09-17T20:46:58.922Z',
gte: '2024-09-17T20:46:58.922Z',
},
},
{
@@ -1080,12 +1081,12 @@ describe('should work as expected for the different field types', () => {
and: [
{
createdAt: {
lte: '2024-09-17T20:46:59.999Z',
lt: '2024-09-17T20:47:00Z',
},
},
{
createdAt: {
gte: '2024-09-17T20:46:00.000Z',
gte: '2024-09-17T20:46:00Z',
},
},
],
@@ -3,6 +3,7 @@ import { RecordComponentInstanceContextsWrapper } from '@/object-record/componen
import { useObjectPermissionsForObject } from '@/object-record/hooks/useObjectPermissionsForObject';
import { RecordCalendar } from '@/object-record/record-calendar/components/RecordCalendar';
import { RecordIndexCalendarDataLoaderEffect } from '@/object-record/record-calendar/components/RecordIndexCalendarDataLoaderEffect';
import { RecordIndexCalendarSelectedDateInitEffect } from '@/object-record/record-calendar/components/RecordIndexCalendarSelectedDateInitEffect';
import { RecordCalendarContextProvider } from '@/object-record/record-calendar/contexts/RecordCalendarContext';
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
@@ -52,6 +53,7 @@ export const RecordIndexCalendarContainer = ({
>
<RecordCalendar />
<RecordIndexCalendarDataLoaderEffect />
<RecordIndexCalendarSelectedDateInitEffect />
</RecordCalendarContextProvider>
</RecordComponentInstanceContextsWrapper>
);
@@ -13,6 +13,7 @@ import {
computeRecordGqlOperationFilter,
turnAnyFieldFilterIntoRecordGqlFilter,
} from 'twenty-shared/utils';
export const useFindManyRecordIndexTableParams = (
objectNameSingular: string,
) => {
@@ -4,6 +4,7 @@ import { useRecordsFieldVisibleGqlFields } from '@/object-record/record-field/ho
import { useFindManyRecordIndexTableParams } from '@/object-record/record-index/hooks/useFindManyRecordIndexTableParams';
import { SIGN_IN_BACKGROUND_MOCK_COMPANIES } from '@/sign-in-background-mock/constants/SignInBackgroundMockCompanies';
import { useShowAuthModal } from '@/ui/layout/hooks/useShowAuthModal';
export const useRecordIndexTableQuery = (objectNameSingular: string) => {
const showAuthModal = useShowAuthModal();
@@ -123,6 +123,7 @@ const computeValueFromFilterText = (
}
};
// TODO: fix this with Temporal
const computeValueFromFilterDate = (
operand: RecordFilterToRecordInputOperand<'DATE_TIME'>,
value: string,