-
- {
- const inputValue = e.target.value;
- setDateValue(inputValue);
-
- if (!isDateTimeInput) {
- const parsedInputDate = DateTime.fromFormat(
- inputValue,
- PICKER_DATE_FORMAT,
- { zone: 'utc' },
- );
-
- const isValid = parsedInputDate.isValid;
-
- if (isValid) {
- onChange?.(parsedInputDate.toJSDate());
- }
- } else {
- // TODO: implement time also
- const parsedInputDate = DateTime.fromFormat(
- inputValue,
- PICKER_DATE_FORMAT,
- { zone: 'utc' },
- );
-
- const isValid = parsedInputDate.isValid;
-
- if (isValid) {
- onChange?.(parsedInputDate.toJSDate());
- }
- }
- }}
- />
-
-
{
- // We need to use onSelect here but onChange is almost redundant with onSelect but is require
+ selected={internalDate}
+ value={dateFormatted}
+ onChange={(newDate) => {
+ onChange?.(newDate);
}}
+ renderCustomHeader={({
+ decreaseMonth,
+ increaseMonth,
+ prevMonthButtonDisabled,
+ nextMonthButtonDisabled,
+ }) => (
+ <>
+
+
+
+
+ decreaseMonth()}
+ size="medium"
+ disabled={prevMonthButtonDisabled}
+ />
+ increaseMonth()}
+ size="medium"
+ disabled={nextMonthButtonDisabled}
+ />
+
+ >
+ )}
customInput={<>>}
onSelect={(date: Date, event) => {
- // Setting the time to midnight might sometimes return the previous day
- // We set to 21:00 to avoid any timezone issues
- const dateForDateField = new Date(date.setHours(21, 0, 0, 0));
-
- setDateValue(
- DateTime.fromJSDate(date).toFormat(PICKER_DATE_FORMAT),
- );
+ const dateUTC = DateTime.fromJSDate(date, {
+ zone: 'utc',
+ }).toJSDate();
if (event?.type === 'click') {
- onMouseSelect?.(isDateTimeInput ? date : dateForDateField);
+ handleMouseSelect?.(dateUTC);
} else {
- onChange?.(isDateTimeInput ? date : dateForDateField);
+ onChange?.(dateUTC);
}
}}
+ onClickOutside={handleClickOutside}
>
{clearable && (
diff --git a/packages/twenty-front/src/modules/ui/input/components/internal/date/components/MonthAndYearDropdown.tsx b/packages/twenty-front/src/modules/ui/input/components/internal/date/components/MonthAndYearDropdown.tsx
new file mode 100644
index 000000000000..d92abebd78f7
--- /dev/null
+++ b/packages/twenty-front/src/modules/ui/input/components/internal/date/components/MonthAndYearDropdown.tsx
@@ -0,0 +1,88 @@
+import { IconCalendarDue } from 'twenty-ui';
+
+import { TableHotkeyScope } from '@/object-record/record-table/types/TableHotkeyScope';
+import { LightIconButton } from '@/ui/input/button/components/LightIconButton';
+import { Select } from '@/ui/input/components/Select';
+import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
+import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
+
+type MonthAndYearDropdownProps = {
+ date: Date;
+ onChange?: (newDate: Date) => void;
+};
+
+const months = [
+ { label: 'January', value: 0 },
+ { label: 'February', value: 1 },
+ { label: 'March', value: 2 },
+ { label: 'April', value: 3 },
+ { label: 'May', value: 4 },
+ { label: 'June', value: 5 },
+ { label: 'July', value: 6 },
+ { label: 'August', value: 7 },
+ { label: 'September', value: 8 },
+ { label: 'October', value: 9 },
+ { label: 'November', value: 10 },
+ { label: 'December', value: 11 },
+];
+
+const years = Array.from(
+ { length: 200 },
+ (_, i) => new Date().getFullYear() + 5 - i,
+).map((year) => ({ label: year.toString(), value: year }));
+
+export const MONTH_AND_YEAR_DROPDOWN_ID = 'date-picker-month-and-year-dropdown';
+export const MONTH_AND_YEAR_DROPDOWN_MONTH_SELECT_ID =
+ 'date-picker-month-and-year-dropdown-month-select';
+export const MONTH_AND_YEAR_DROPDOWN_YEAR_SELECT_ID =
+ 'date-picker-month-and-year-dropdown-year-select';
+
+export const MonthAndYearDropdown = ({
+ date,
+ onChange,
+}: MonthAndYearDropdownProps) => {
+ const handleChangeMonth = (month: number) => {
+ const newDate = new Date(date);
+ newDate.setMonth(month);
+ onChange?.(newDate);
+ };
+
+ const handleChangeYear = (year: number) => {
+ const newDate = new Date(date);
+ newDate.setFullYear(year);
+ onChange?.(newDate);
+ };
+
+ return (
+
+ }
+ dropdownComponents={
+
+
+
+
+ }
+ />
+ );
+};
diff --git a/packages/twenty-front/src/modules/ui/input/components/internal/date/components/TimeInput.tsx b/packages/twenty-front/src/modules/ui/input/components/internal/date/components/TimeInput.tsx
new file mode 100644
index 000000000000..b8c9ba54f63e
--- /dev/null
+++ b/packages/twenty-front/src/modules/ui/input/components/internal/date/components/TimeInput.tsx
@@ -0,0 +1,80 @@
+import { useEffect } from 'react';
+import { useIMask } from 'react-imask';
+import styled from '@emotion/styled';
+import { DateTime } from 'luxon';
+import { IconClockHour8 } from 'twenty-ui';
+
+import { TIME_BLOCKS } from '@/ui/input/components/internal/date/constants/TimeBlocks';
+import { TIME_MASK } from '@/ui/input/components/internal/date/constants/TimeMask';
+
+const StyledIconClock = styled(IconClockHour8)`
+ position: absolute;
+`;
+
+const StyledTimeInputContainer = styled.div`
+ align-items: center;
+ background-color: ${({ theme }) => theme.background.tertiary};
+ border-radius: ${({ theme }) => theme.border.radius.sm};
+ display: flex;
+ margin-right: 0;
+ padding: 0 ${({ theme }) => theme.spacing(2)};
+
+ text-align: left;
+ width: 136px;
+ height: 32px;
+ gap: ${({ theme }) => theme.spacing(1)};
+
+ z-index: 10;
+`;
+
+const StyledTimeInput = styled.input`
+ background: transparent;
+ border: none;
+ color: ${({ theme }) => theme.font.color.primary};
+ outline: none;
+ font-weight: 500;
+ font-size: ${({ theme }) => theme.font.size.md};
+ margin-left: ${({ theme }) => theme.spacing(5)};
+`;
+
+type TimeInputProps = {
+ onChange?: (date: Date) => void;
+ date: Date;
+};
+
+export const TimeInput = ({ date, onChange }: TimeInputProps) => {
+ const handleComplete = (value: string) => {
+ const [hours, minutes] = value.split(':');
+
+ const newDate = new Date(date);
+
+ newDate.setHours(parseInt(hours, 10));
+ newDate.setMinutes(parseInt(minutes, 10));
+
+ onChange?.(newDate);
+ };
+
+ const { ref, setValue } = useIMask(
+ {
+ mask: TIME_MASK,
+ blocks: TIME_BLOCKS,
+ lazy: false,
+ },
+ {
+ onComplete: handleComplete,
+ },
+ );
+
+ useEffect(() => {
+ const formattedDate = DateTime.fromJSDate(date).toFormat('HH:mm');
+
+ setValue(formattedDate);
+ }, [date, setValue]);
+
+ return (
+
+
+
+
+ );
+};
diff --git a/packages/twenty-front/src/modules/ui/input/components/internal/date/constants/DateTimeBlocks.ts b/packages/twenty-front/src/modules/ui/input/components/internal/date/constants/DateTimeBlocks.ts
new file mode 100644
index 000000000000..53f2ef6f82f7
--- /dev/null
+++ b/packages/twenty-front/src/modules/ui/input/components/internal/date/constants/DateTimeBlocks.ts
@@ -0,0 +1,22 @@
+import { IMask } from 'react-imask';
+
+import { TIME_BLOCKS } from '@/ui/input/components/internal/date/constants/TimeBlocks';
+
+export const DATE_TIME_BLOCKS = {
+ YYYY: {
+ mask: IMask.MaskedRange,
+ from: 1970,
+ to: 2100,
+ },
+ MM: {
+ mask: IMask.MaskedRange,
+ from: 1,
+ to: 12,
+ },
+ DD: {
+ mask: IMask.MaskedRange,
+ from: 1,
+ to: 31,
+ },
+ ...TIME_BLOCKS,
+};
diff --git a/packages/twenty-front/src/modules/ui/input/components/internal/date/constants/DateTimeMask.ts b/packages/twenty-front/src/modules/ui/input/components/internal/date/constants/DateTimeMask.ts
new file mode 100644
index 000000000000..80a62d78849b
--- /dev/null
+++ b/packages/twenty-front/src/modules/ui/input/components/internal/date/constants/DateTimeMask.ts
@@ -0,0 +1,3 @@
+import { TIME_MASK } from '@/ui/input/components/internal/date/constants/TimeMask';
+
+export const DATE_TIME_MASK = `MM/DD/YYYY ${TIME_MASK}`;
diff --git a/packages/twenty-front/src/modules/ui/input/components/internal/date/constants/TimeBlocks.ts b/packages/twenty-front/src/modules/ui/input/components/internal/date/constants/TimeBlocks.ts
new file mode 100644
index 000000000000..681c3ec0428b
--- /dev/null
+++ b/packages/twenty-front/src/modules/ui/input/components/internal/date/constants/TimeBlocks.ts
@@ -0,0 +1,14 @@
+import { IMask } from 'react-imask';
+
+export const TIME_BLOCKS = {
+ HH: {
+ mask: IMask.MaskedRange, // Use MaskedRange for valid hour range (0-23)
+ from: 0,
+ to: 23,
+ },
+ mm: {
+ mask: IMask.MaskedRange, // Use MaskedRange for valid minute range (0-59)
+ from: 0,
+ to: 61,
+ },
+};
diff --git a/packages/twenty-front/src/modules/ui/input/components/internal/date/constants/TimeMask.ts b/packages/twenty-front/src/modules/ui/input/components/internal/date/constants/TimeMask.ts
new file mode 100644
index 000000000000..f5a864c09285
--- /dev/null
+++ b/packages/twenty-front/src/modules/ui/input/components/internal/date/constants/TimeMask.ts
@@ -0,0 +1 @@
+export const TIME_MASK = 'HH:mm'; // Define blocks for hours and minutes
diff --git a/packages/twenty-ui/src/display/icon/components/TablerIcons.ts b/packages/twenty-ui/src/display/icon/components/TablerIcons.ts
index 1a5f4247bf07..481ee1d52f2d 100644
--- a/packages/twenty-ui/src/display/icon/components/TablerIcons.ts
+++ b/packages/twenty-ui/src/display/icon/components/TablerIcons.ts
@@ -26,7 +26,9 @@ export {
IconBriefcase,
IconBuildingSkyscraper,
IconCalendar,
+ IconCalendarDue,
IconCalendarEvent,
+ IconCalendarTime,
IconCalendarX,
IconCheck,
IconCheckbox,
@@ -40,6 +42,7 @@ export {
IconCirclePlus,
IconCircleX,
IconClick,
+ IconClockHour8,
IconCode,
IconCoins,
IconColorSwatch,
diff --git a/yarn.lock b/yarn.lock
index cc7ad961a7fa..c27369545f39 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3387,6 +3387,16 @@ __metadata:
languageName: node
linkType: hard
+"@babel/runtime-corejs3@npm:^7.24.4":
+ version: 7.24.4
+ resolution: "@babel/runtime-corejs3@npm:7.24.4"
+ dependencies:
+ core-js-pure: "npm:^3.30.2"
+ regenerator-runtime: "npm:^0.14.0"
+ checksum: 121bec9a0b505e2995c4b71cf480167e006e8ee423f77bccc38975bfbfbfdb191192ff03557c18fad6de8f2b85c12c49aaa4b92d1d5fe0c0e136da664129be1e
+ languageName: node
+ linkType: hard
+
"@babel/runtime@npm:^7.0.0, @babel/runtime@npm:^7.1.2, @babel/runtime@npm:^7.10.3, @babel/runtime@npm:^7.12.1, @babel/runtime@npm:^7.12.13, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.13.10, @babel/runtime@npm:^7.17.8, @babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.20.13, @babel/runtime@npm:^7.21.0, @babel/runtime@npm:^7.22.6, @babel/runtime@npm:^7.23.2, @babel/runtime@npm:^7.8.4, @babel/runtime@npm:^7.9.2":
version: 7.23.5
resolution: "@babel/runtime@npm:7.23.5"
@@ -30042,6 +30052,15 @@ __metadata:
languageName: node
linkType: hard
+"imask@npm:^7.6.0":
+ version: 7.6.0
+ resolution: "imask@npm:7.6.0"
+ dependencies:
+ "@babel/runtime-corejs3": "npm:^7.24.4"
+ checksum: c754210124efbb5dcaa37e9e21497dc9f166e7fb5759853840e49c4cde1bac61bc12f23ca5c6150a2fa57246d762d3849ad3203f5c803fc22d928e8b546b95d1
+ languageName: node
+ linkType: hard
+
"immer@npm:^10.0.2":
version: 10.0.3
resolution: "immer@npm:10.0.3"
@@ -40738,6 +40757,18 @@ __metadata:
languageName: node
linkType: hard
+"react-imask@npm:^7.6.0":
+ version: 7.6.0
+ resolution: "react-imask@npm:7.6.0"
+ dependencies:
+ imask: "npm:^7.6.0"
+ prop-types: "npm:^15.8.1"
+ peerDependencies:
+ react: ">=0.14.0"
+ checksum: f5e7d9a865943ebf05d1c28819d8489a9f36cdeb2a005de340121636dbcb5e3b265017f2b64d7c40268bb5b16b9cf969721f371f18528bfe68a3b86cd8be2373
+ languageName: node
+ linkType: hard
+
"react-intersection-observer@npm:^9.5.2":
version: 9.5.3
resolution: "react-intersection-observer@npm:9.5.3"
@@ -45980,6 +46011,7 @@ __metadata:
react-hook-form: "npm:^7.45.1"
react-hotkeys-hook: "npm:^4.4.4"
react-icons: "npm:^4.12.0"
+ react-imask: "npm:^7.6.0"
react-intersection-observer: "npm:^9.5.2"
react-loading-skeleton: "npm:^3.3.1"
react-phone-number-input: "npm:^3.3.4"