From 033d297695747fcb86ba7fc9e2b15174f75264b6 Mon Sep 17 00:00:00 2001
From: BugIsGod <87571967+bugisthegod@users.noreply.github.com>
Date: Tue, 10 Mar 2026 13:39:46 +0000
Subject: [PATCH] feat: add visual time picker to DateTimePicker (#15057)
(#17952)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## Summary
Added a visual time picker dropdown for DateTime fields, replacing the
previous text input. Users can now select hours and minutes through an
intuitive scrollable interface. (Fixes #15057
## Changes
- **New component**: Add a `TimePickerDropdown` - Visual picker with
scrollable hour/minute columns
- **Updated**: `DateTimePickerHeader` - Implemented time picker dropdown
in `DateTimePickerHeader` and Move month/year picker to right side
## Snapshots
https://github.com/user-attachments/assets/daa5cba5-c86c-46aa-a634-0f5c04523af1
**If there is no enough place at right, auto-move month/year selector to
the left side**
Hi, @Bonapara I followed the Figma you shared to complete this feature.
Could you please review it for me? Thanks a lot.
---------
Co-authored-by: Lucas Bordeau
---
.../DateTimeFieldInput.stories.tsx | 47 ++-
.../field/input/components/DateTimeInput.tsx | 6 +
.../date/components/DatePickerHeader.tsx | 21 +-
.../date/components/DateTimePicker.tsx | 196 ++++++------
.../date/components/DateTimePickerHeader.tsx | 281 ++++++++++++++----
.../InternalDatePicker.stories.tsx | 42 ++-
.../date/hooks/__tests__/useTimeInput.test.ts | 129 ++++++++
.../internal/date/hooks/useTimeInput.ts | 75 +++++
.../internal/date/utils/getTimeBlocks.ts | 4 +-
.../useCloseAnyOpenDropdown.test.tsx | 66 ++++
.../dropdown/hooks/useCloseAnyOpenDropdown.ts | 19 +-
.../useGoBackToPreviousDropdownFocusId.ts | 15 +-
...SetFocusedDropdownIdAndMemorizePrevious.ts | 12 +-
.../previousDropdownFocusIdStackState.ts | 6 +
.../states/previousDropdownFocusIdState.ts | 6 -
.../display/icon/components/TablerIcons.ts | 1 +
packages/twenty-ui/src/display/index.ts | 1 +
17 files changed, 731 insertions(+), 196 deletions(-)
create mode 100644 packages/twenty-front/src/modules/ui/input/components/internal/date/hooks/__tests__/useTimeInput.test.ts
create mode 100644 packages/twenty-front/src/modules/ui/input/components/internal/date/hooks/useTimeInput.ts
create mode 100644 packages/twenty-front/src/modules/ui/layout/dropdown/states/previousDropdownFocusIdStackState.ts
delete mode 100644 packages/twenty-front/src/modules/ui/layout/dropdown/states/previousDropdownFocusIdState.ts
diff --git a/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/input/components/__stories__/DateTimeFieldInput.stories.tsx b/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/input/components/__stories__/DateTimeFieldInput.stories.tsx
index 86be3cf205..32502cb95d 100644
--- a/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/input/components/__stories__/DateTimeFieldInput.stories.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/input/components/__stories__/DateTimeFieldInput.stories.tsx
@@ -135,8 +135,17 @@ type Story = StoryObj;
export const Default: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
+ const body = within(canvasElement.ownerDocument.body);
+
+ const calendarButton = await canvas.findByRole(
+ 'button',
+ { name: 'Select month and year' },
+ { timeout: 10000 },
+ );
+ await userEvent.click(calendarButton);
+
// Increased timeout to account for lazy-loaded react-datepicker on slower CI runners
- const div = await canvas.findByText('January', {}, { timeout: 10000 });
+ const div = await body.findByText('January', {}, { timeout: 10000 });
await expect(div.innerText).toContain('January');
},
@@ -145,13 +154,23 @@ export const Default: Story = {
export const ClickOutside: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
+ const body = within(canvasElement.ownerDocument.body);
await expect(handleClickoutsideMocked).toHaveBeenCalledTimes(0);
+ const calendarButton = await canvas.findByRole(
+ 'button',
+ { name: 'Select month and year' },
+ { timeout: 10000 },
+ );
+ await userEvent.click(calendarButton);
+
// Increased timeout to account for lazy-loaded react-datepicker on slower CI runners
- await canvas.findByText('January', {}, { timeout: 10000 });
+ await body.findByText('January', {}, { timeout: 10000 });
+
const emptyDiv = canvas.getByTestId('data-field-input-click-outside-div');
await userEvent.click(emptyDiv);
+ await userEvent.click(emptyDiv);
await expect(handleClickoutsideMocked).toHaveBeenCalledTimes(1);
},
@@ -161,9 +180,19 @@ export const Escape: Story = {
play: async ({ canvasElement }) => {
await expect(handleEscapeMocked).toHaveBeenCalledTimes(0);
const canvas = within(canvasElement);
+ const body = within(canvasElement.ownerDocument.body);
+
+ const calendarButton = await canvas.findByRole(
+ 'button',
+ { name: 'Select month and year' },
+ { timeout: 10000 },
+ );
+ await userEvent.click(calendarButton);
// Increased timeout to account for lazy-loaded react-datepicker on slower CI runners
- await canvas.findByText('January', {}, { timeout: 10000 });
+ await body.findByText('January', {}, { timeout: 10000 });
+
+ await userEvent.keyboard('{escape}');
await userEvent.keyboard('{escape}');
await expect(handleEscapeMocked).toHaveBeenCalledTimes(1);
@@ -174,9 +203,19 @@ export const Enter: Story = {
play: async ({ canvasElement }) => {
await expect(handleEnterMocked).toHaveBeenCalledTimes(0);
const canvas = within(canvasElement);
+ const body = within(canvasElement.ownerDocument.body);
+
+ const calendarButton = await canvas.findByRole(
+ 'button',
+ { name: 'Select month and year' },
+ { timeout: 10000 },
+ );
+ await userEvent.click(calendarButton);
// Increased timeout to account for lazy-loaded react-datepicker on slower CI runners
- await canvas.findByText('January', {}, { timeout: 10000 });
+ await body.findByText('January', {}, { timeout: 10000 });
+
+ await userEvent.keyboard('{enter}');
await userEvent.keyboard('{enter}');
await expect(handleEnterMocked).toHaveBeenCalledTimes(1);
diff --git a/packages/twenty-front/src/modules/ui/field/input/components/DateTimeInput.tsx b/packages/twenty-front/src/modules/ui/field/input/components/DateTimeInput.tsx
index 41357b80f2..b59a6fc320 100644
--- a/packages/twenty-front/src/modules/ui/field/input/components/DateTimeInput.tsx
+++ b/packages/twenty-front/src/modules/ui/field/input/components/DateTimeInput.tsx
@@ -2,6 +2,7 @@ import { useCallback, useRef, useState } from 'react';
import { useRegisterInputEvents } from '@/object-record/record-field/ui/meta-types/input/hooks/useRegisterInputEvents';
import {
+ DATE_TIME_PICKER_MONTH_YEAR_PANEL_DROPDOWN_ID,
DateTimePicker,
MONTH_AND_YEAR_DROPDOWN_MONTH_SELECT_ID,
MONTH_AND_YEAR_DROPDOWN_YEAR_SELECT_ID,
@@ -65,10 +66,12 @@ export const DateTimeInput = ({
const { closeDropdown: closeDropdownMonthSelect } = useCloseDropdown();
const { closeDropdown: closeDropdownYearSelect } = useCloseDropdown();
+ const { closeDropdown: closeMonthYearPanel } = useCloseDropdown();
const handleEnter = () => {
closeDropdownYearSelect(MONTH_AND_YEAR_DROPDOWN_YEAR_SELECT_ID);
closeDropdownMonthSelect(MONTH_AND_YEAR_DROPDOWN_MONTH_SELECT_ID);
+ closeMonthYearPanel(DATE_TIME_PICKER_MONTH_YEAR_PANEL_DROPDOWN_ID);
onEnter(internalValue);
};
@@ -76,6 +79,7 @@ export const DateTimeInput = ({
const handleEscape = () => {
closeDropdownYearSelect(MONTH_AND_YEAR_DROPDOWN_YEAR_SELECT_ID);
closeDropdownMonthSelect(MONTH_AND_YEAR_DROPDOWN_MONTH_SELECT_ID);
+ closeMonthYearPanel(DATE_TIME_PICKER_MONTH_YEAR_PANEL_DROPDOWN_ID);
onEscape(internalValue);
};
@@ -87,6 +91,7 @@ export const DateTimeInput = ({
if (currentFocusId === instanceId) {
closeDropdownYearSelect(MONTH_AND_YEAR_DROPDOWN_YEAR_SELECT_ID);
closeDropdownMonthSelect(MONTH_AND_YEAR_DROPDOWN_MONTH_SELECT_ID);
+ closeMonthYearPanel(DATE_TIME_PICKER_MONTH_YEAR_PANEL_DROPDOWN_ID);
onClickOutside(event, internalValue);
}
},
@@ -94,6 +99,7 @@ export const DateTimeInput = ({
instanceId,
closeDropdownYearSelect,
closeDropdownMonthSelect,
+ closeMonthYearPanel,
onClickOutside,
internalValue,
store,
diff --git a/packages/twenty-front/src/modules/ui/input/components/internal/date/components/DatePickerHeader.tsx b/packages/twenty-front/src/modules/ui/input/components/internal/date/components/DatePickerHeader.tsx
index df5defaac8..b10e7c5649 100644
--- a/packages/twenty-front/src/modules/ui/input/components/internal/date/components/DatePickerHeader.tsx
+++ b/packages/twenty-front/src/modules/ui/input/components/internal/date/components/DatePickerHeader.tsx
@@ -7,11 +7,6 @@ import { getMonthSelectOptions } from '@/ui/input/components/internal/date/utils
import { ClickOutsideListenerContext } from '@/ui/utilities/pointer-event/contexts/ClickOutsideListenerContext';
import { IconChevronLeft, IconChevronRight } from 'twenty-ui/display';
import { LightIconButton } from 'twenty-ui/input';
-import {
- MONTH_AND_YEAR_DROPDOWN_MONTH_SELECT_ID,
- MONTH_AND_YEAR_DROPDOWN_YEAR_SELECT_ID,
-} from './DateTimePicker';
-
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { Temporal } from 'temporal-polyfill';
@@ -19,6 +14,15 @@ import { SOURCE_LOCALE } from 'twenty-shared/translations';
import { isDefined } from 'twenty-shared/utils';
import { themeCssVariables } from 'twenty-ui/theme-constants';
+const MONTH_AND_YEAR_DROPDOWN_MONTH_SELECT_ID =
+ 'date-picker-month-and-year-dropdown-month-select';
+const MONTH_AND_YEAR_DROPDOWN_YEAR_SELECT_ID =
+ 'date-picker-month-and-year-dropdown-year-select';
+const YEARS_SELECT_OPTIONS = Array.from(
+ { length: 200 },
+ (_, i) => new Date().getFullYear() + 50 - i,
+).map((year) => ({ label: year.toString(), value: year }));
+
const StyledCustomDatePickerHeader = styled.div`
align-items: center;
display: flex;
@@ -30,11 +34,6 @@ const StyledCustomDatePickerHeader = styled.div`
padding-top: ${themeCssVariables.spacing[2]};
`;
-const years = Array.from(
- { length: 200 },
- (_, i) => new Date().getFullYear() + 50 - i,
-).map((year) => ({ label: year.toString(), value: year }));
-
type DatePickerHeaderProps = {
date: string | null;
onChange?: (date: string | null) => void;
@@ -89,7 +88,7 @@ export const DatePickerHeader = ({
dropdownId={MONTH_AND_YEAR_DROPDOWN_YEAR_SELECT_ID}
onChange={onChangeYear}
value={dateParsed?.year}
- options={years}
+ options={YEARS_SELECT_OPTIONS}
fullWidth
/>
diff --git a/packages/twenty-front/src/modules/ui/input/components/internal/date/components/DateTimePicker.tsx b/packages/twenty-front/src/modules/ui/input/components/internal/date/components/DateTimePicker.tsx
index 2d7a12d54d..043e005b67 100644
--- a/packages/twenty-front/src/modules/ui/input/components/internal/date/components/DateTimePicker.tsx
+++ b/packages/twenty-front/src/modules/ui/input/components/internal/date/components/DateTimePicker.tsx
@@ -5,7 +5,10 @@ import {
type RelativeDateFilter,
} from 'twenty-shared/utils';
-import { DateTimePickerHeader } from '@/ui/input/components/internal/date/components/DateTimePickerHeader';
+import {
+ DATE_TIME_PICKER_MONTH_YEAR_PANEL_DROPDOWN_ID,
+ DateTimePickerHeader,
+} from '@/ui/input/components/internal/date/components/DateTimePickerHeader';
import { RelativeDatePickerHeader } from '@/ui/input/components/internal/date/components/RelativeDatePickerHeader';
import { getHighlightedDates } from '@/ui/input/components/internal/date/utils/getHighlightedDates';
import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
@@ -24,11 +27,22 @@ import { useGetShiftedDateToSystemTimeZone } from '@/ui/input/components/interna
import { useUserFirstDayOfTheWeek } from '@/ui/input/components/internal/date/hooks/useUserFirstDayOfTheWeek';
import { useUserTimezone } from '@/ui/input/components/internal/date/hooks/useUserTimezone';
import { Temporal } from 'temporal-polyfill';
+
+export {
+ MONTH_AND_YEAR_DROPDOWN_MONTH_SELECT_ID,
+ MONTH_AND_YEAR_DROPDOWN_YEAR_SELECT_ID,
+} from '@/ui/input/components/internal/date/components/DatePicker';
+export { DATE_TIME_PICKER_MONTH_YEAR_PANEL_DROPDOWN_ID } from '@/ui/input/components/internal/date/components/DateTimePickerHeader';
+
import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants';
-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';
+
+const StyledOuterWrapper = styled.div`
+ align-items: flex-start;
+ display: flex;
+ flex-direction: row;
+ position: relative;
+ width: 280px;
+`;
const StyledContainer = styled.div<{
calendarDisabled?: boolean;
@@ -361,6 +375,8 @@ export const DateTimePicker = ({
const dateToUse =
date ?? Temporal.Now.zonedDateTimeISO(timeZone ?? userTimezone);
+ const { closeDropdown: closeMonthYearPanel } = useCloseDropdown();
+
const { getShiftedDateToSystemTimeZone } =
useGetShiftedDateToSystemTimeZone();
@@ -381,21 +397,13 @@ export const DateTimePicker = ({
return { zonedDateTime };
};
- const { closeDropdown: closeDropdownMonthSelect } = useCloseDropdown();
- const { closeDropdown: closeDropdownYearSelect } = useCloseDropdown();
-
const handleClear = () => {
- closeDropdowns();
+ closeMonthYearPanel(DATE_TIME_PICKER_MONTH_YEAR_PANEL_DROPDOWN_ID);
onClear?.();
};
- const closeDropdowns = () => {
- closeDropdownYearSelect(MONTH_AND_YEAR_DROPDOWN_YEAR_SELECT_ID);
- closeDropdownMonthSelect(MONTH_AND_YEAR_DROPDOWN_MONTH_SELECT_ID);
- };
-
const handleClose = (newDate: Temporal.ZonedDateTime) => {
- closeDropdowns();
+ closeMonthYearPanel(DATE_TIME_PICKER_MONTH_YEAR_PANEL_DROPDOWN_ID);
onClose?.(newDate);
};
@@ -472,84 +480,86 @@ export const DateTimePicker = ({
convertFirstDayOfTheWeekToCalendarStartDayNumber(userFirstDayOfTheWeek);
return (
-
-
-
-
-
-
-
-
-
- }
- >
-
- isRelative ? (
-
- ) : (
-
- )
+
+
+
+
+
+
+
+
+
+
}
- onSelect={handleDateSelect}
- selectsMultiple={isRelative}
- />
-
- {clearable && (
- <>
-
-
-
-
-
-
- >
- )}
-
+ >
+
+ isRelative ? (
+
+ ) : (
+
+ )
+ }
+ onSelect={handleDateSelect}
+ selectsMultiple={isRelative}
+ />
+
+ {clearable && (
+ <>
+
+
+
+
+
+
+ >
+ )}
+
+
);
};
diff --git a/packages/twenty-front/src/modules/ui/input/components/internal/date/components/DateTimePickerHeader.tsx b/packages/twenty-front/src/modules/ui/input/components/internal/date/components/DateTimePickerHeader.tsx
index 5e7ed4f67f..e669fcca1b 100644
--- a/packages/twenty-front/src/modules/ui/input/components/internal/date/components/DateTimePickerHeader.tsx
+++ b/packages/twenty-front/src/modules/ui/input/components/internal/date/components/DateTimePickerHeader.tsx
@@ -1,70 +1,202 @@
+import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { styled } from '@linaria/react';
+import { useEffect } from 'react';
+import { useIMask } from 'react-imask';
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
-import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
+import { useDateTimeFormat } from '@/localization/hooks/useDateTimeFormat';
import { Select } from '@/ui/input/components/Select';
-
import { DateTimePickerInput } from '@/ui/input/components/internal/date/components/DateTimePickerInput';
+import { useTimeInput } from '@/ui/input/components/internal/date/hooks/useTimeInput';
import { getMonthSelectOptions } from '@/ui/input/components/internal/date/utils/getMonthSelectOptions';
+import { getTimeBlocks } from '@/ui/input/components/internal/date/utils/getTimeBlocks';
+import { getTimeMask } from '@/ui/input/components/internal/date/utils/getTimeMask';
+import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
+import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
import { ClickOutsideListenerContext } from '@/ui/utilities/pointer-event/contexts/ClickOutsideListenerContext';
+import { t } from '@lingui/core/macro';
import { type Temporal } from 'temporal-polyfill';
import { SOURCE_LOCALE } from 'twenty-shared/translations';
-import { IconChevronLeft, IconChevronRight } from 'twenty-ui/display';
+import { isDefined } from 'twenty-shared/utils';
+import {
+ IconCalendar,
+ IconChevronLeft,
+ IconChevronRight,
+ IconClock,
+} from 'twenty-ui/display';
import { LightIconButton } from 'twenty-ui/input';
+import { themeCssVariables } from 'twenty-ui/theme-constants';
+
import {
MONTH_AND_YEAR_DROPDOWN_MONTH_SELECT_ID,
MONTH_AND_YEAR_DROPDOWN_YEAR_SELECT_ID,
} from './DateTimePicker';
-import { themeCssVariables } from 'twenty-ui/theme-constants';
-const StyledCustomDatePickerHeader = styled.div`
+export const DATE_TIME_PICKER_MONTH_YEAR_PANEL_DROPDOWN_ID =
+ 'date-time-picker-month-year-panel';
+
+const YEARS_SELECT_OPTIONS = Array.from(
+ { length: 200 },
+ (_, i) => new Date().getFullYear() + 50 - i,
+).map((year) => ({ label: year.toString(), value: year }));
+
+const StyledTimeRow = styled.div`
align-items: center;
display: flex;
gap: ${themeCssVariables.spacing[1]};
- justify-content: flex-end;
+ justify-content: flex-start;
+ padding-bottom: ${themeCssVariables.spacing[2]};
padding-left: ${themeCssVariables.spacing[2]};
padding-right: ${themeCssVariables.spacing[2]};
-
padding-top: ${themeCssVariables.spacing[2]};
`;
+const StyledTimeInputWrapper = styled.div`
+ flex-grow: 1;
+`;
+
+const StyledTimeInputContainer = styled.div`
+ align-items: center;
+ background-color: ${themeCssVariables.background.transparent.lighter};
+ border: 1px solid ${themeCssVariables.border.color.medium};
+ border-radius: ${themeCssVariables.border.radius.sm};
+ box-sizing: border-box;
+ display: flex;
+ gap: ${themeCssVariables.spacing[1]};
+ height: ${themeCssVariables.spacing[8]};
+ padding: 0 ${themeCssVariables.spacing[2]};
+ transition: border-color 0.15s ease;
+
+ &:hover {
+ border-color: ${themeCssVariables.border.color.strong};
+ }
+`;
+
+const StyledClockIcon = styled.div`
+ align-items: center;
+ color: ${themeCssVariables.font.color.tertiary};
+ display: flex;
+ flex-shrink: 0;
+`;
+
+const StyledTimeInput = styled.input`
+ background: transparent;
+ border: none;
+ color: ${themeCssVariables.font.color.primary};
+ flex: 1;
+ font-family: ${themeCssVariables.font.family};
+ font-size: ${themeCssVariables.font.size.md};
+ font-weight: ${themeCssVariables.font.weight.regular};
+ letter-spacing: 0.05em;
+ outline: none;
+ width: 100%;
+
+ &::placeholder {
+ color: ${themeCssVariables.font.color.light};
+ font-weight: ${themeCssVariables.font.weight.medium};
+ }
+
+ &:disabled {
+ color: ${themeCssVariables.font.color.tertiary};
+ }
+`;
+
+const StyledRightControls = styled.div`
+ align-items: center;
+ display: flex;
+ gap: ${themeCssVariables.spacing[1]};
+`;
+
+const StyledNavigationButtons = styled.div`
+ display: flex;
+ gap: ${themeCssVariables.spacing[1]};
+`;
+
const StyledSeparator = styled.div`
border-bottom: 1px solid ${themeCssVariables.border.color.light};
height: 1px;
width: 100%;
`;
-const years = Array.from(
- { length: 200 },
- (_, i) => new Date().getFullYear() + 50 - i,
-).map((year) => ({ label: year.toString(), value: year }));
+const StyledMonthYearSelector = styled.div`
+ display: flex;
+ flex-direction: column;
+ gap: ${themeCssVariables.spacing[1]};
+ padding: ${themeCssVariables.spacing[1]};
+ width: 160px;
+`;
type DateTimePickerHeaderProps = {
date: Temporal.ZonedDateTime | null;
onChange?: (date: Temporal.ZonedDateTime | null) => void;
- onChangeMonth: (month: number) => void;
- onChangeYear: (year: number) => void;
onAddMonth: () => void;
onSubtractMonth: () => void;
prevMonthButtonDisabled: boolean;
nextMonthButtonDisabled: boolean;
hideInput?: boolean;
+ onChangeMonth: (month: number) => void;
+ onChangeYear: (year: number) => void;
};
export const DateTimePickerHeader = ({
date,
onChange,
- onChangeMonth,
- onChangeYear,
onAddMonth,
onSubtractMonth,
prevMonthButtonDisabled,
nextMonthButtonDisabled,
hideInput = false,
+ onChangeMonth,
+ onChangeYear,
}: DateTimePickerHeaderProps) => {
+ const { timeFormat } = useDateTimeFormat();
+ const { formatTime, parseTime, isHour12 } = useTimeInput(timeFormat);
+
const currentWorkspaceMember = useAtomStateValue(currentWorkspaceMemberState);
const userLocale = currentWorkspaceMember?.locale ?? SOURCE_LOCALE;
+ const { closeDropdown: closeMonthSelect } = useCloseDropdown();
+ const { closeDropdown: closeYearSelect } = useCloseDropdown();
+
+ const closeInnerDropdowns = () => {
+ closeMonthSelect(MONTH_AND_YEAR_DROPDOWN_MONTH_SELECT_ID);
+ closeYearSelect(MONTH_AND_YEAR_DROPDOWN_YEAR_SELECT_ID);
+ };
+
+ const { ref: iMaskRef, setValue } = useIMask(
+ {
+ mask: getTimeMask(timeFormat),
+ blocks: getTimeBlocks(timeFormat),
+ lazy: false,
+ autofix: true,
+ },
+ {
+ defaultValue: isDefined(date)
+ ? formatTime(date.hour, date.minute)
+ : undefined,
+ onComplete: (value) => {
+ if (!date) return;
+
+ const parsedTime = parseTime(value);
+ if (!parsedTime) {
+ return;
+ }
+
+ onChange?.(
+ date.with({ hour: parsedTime.hour, minute: parsedTime.minute }),
+ );
+ },
+ },
+ );
+
+ useEffect(() => {
+ if (isDefined(date)) {
+ setValue(formatTime(date.hour, date.minute));
+ }
+ }, [date, formatTime, setValue]);
+
+ const timeInputRef = iMaskRef as React.Ref;
+
return (
<>
{!hideInput && (
@@ -73,46 +205,87 @@ export const DateTimePickerHeader = ({
>
)}
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
>
);
};
diff --git a/packages/twenty-front/src/modules/ui/input/components/internal/date/components/__stories__/InternalDatePicker.stories.tsx b/packages/twenty-front/src/modules/ui/input/components/internal/date/components/__stories__/InternalDatePicker.stories.tsx
index 598702c012..96f27ed260 100644
--- a/packages/twenty-front/src/modules/ui/input/components/internal/date/components/__stories__/InternalDatePicker.stories.tsx
+++ b/packages/twenty-front/src/modules/ui/input/components/internal/date/components/__stories__/InternalDatePicker.stories.tsx
@@ -38,13 +38,20 @@ export const WithOpenMonthSelect: Story = {
const canvas = within(canvasElement);
const body = within(canvasElement.ownerDocument.body);
- // Increased timeout to account for lazy-loaded react-datepicker on slower CI runners
- const monthSelect = await canvas.findByText(
+ // Wait for date picker to load and click calendar icon to open month/year selector
+ const calendarButton = await canvas.findByRole(
+ 'button',
+ { name: 'Select month and year' },
+ { timeout: 10000 },
+ );
+ await userEvent.click(calendarButton);
+
+ // Now find and click the month select
+ const monthSelect = await body.findByText(
'January',
{},
{ timeout: 10000 },
);
-
await userEvent.click(monthSelect);
for (const monthLabel of [
@@ -65,7 +72,7 @@ export const WithOpenMonthSelect: Story = {
await userEvent.click(await body.findByText('February'));
- expect(await canvas.findByText('February')).toBeInTheDocument();
+ expect(await body.findByText('February')).toBeInTheDocument();
},
};
@@ -74,9 +81,15 @@ export const WithOpenYearSelect: Story = {
const canvas = within(canvasElement);
const body = within(canvasElement.ownerDocument.body);
- // Increased timeout to account for lazy-loaded react-datepicker on slower CI runners
- const yearSelect = await canvas.findByText('2023', {}, { timeout: 10000 });
+ // Wait for date picker to load and click calendar icon to open month/year selector
+ const calendarButton = await canvas.findByRole(
+ 'button',
+ { name: 'Select month and year' },
+ { timeout: 10000 },
+ );
+ await userEvent.click(calendarButton);
+ const yearSelect = await body.findByText('2023', {}, { timeout: 10000 });
await userEvent.click(yearSelect);
for (const yearLabel of ['2024', '2025', '2026']) {
@@ -85,6 +98,21 @@ export const WithOpenYearSelect: Story = {
await userEvent.click(await body.findByText('2024'));
- expect(await canvas.findByText('2024')).toBeInTheDocument();
+ expect(await body.findByText('2024')).toBeInTheDocument();
+ },
+};
+
+export const WithTimeInput: Story = {
+ play: async ({ canvasElement }) => {
+ const canvas = within(canvasElement);
+
+ // Increased timeout to account for lazy-loaded react-datepicker on slower CI runners
+ const timeInput = await canvas.findByPlaceholderText(
+ /HH:mm/,
+ {},
+ { timeout: 10000 },
+ );
+
+ expect(timeInput).toBeInTheDocument();
},
};
diff --git a/packages/twenty-front/src/modules/ui/input/components/internal/date/hooks/__tests__/useTimeInput.test.ts b/packages/twenty-front/src/modules/ui/input/components/internal/date/hooks/__tests__/useTimeInput.test.ts
new file mode 100644
index 0000000000..127b0212e0
--- /dev/null
+++ b/packages/twenty-front/src/modules/ui/input/components/internal/date/hooks/__tests__/useTimeInput.test.ts
@@ -0,0 +1,129 @@
+import { renderHook } from '@testing-library/react';
+
+import { TimeFormat } from '@/localization/constants/TimeFormat';
+import { useTimeInput } from '@/ui/input/components/internal/date/hooks/useTimeInput';
+
+describe('useTimeInput', () => {
+ describe('24-hour format', () => {
+ const render24 = () =>
+ renderHook(() => useTimeInput(TimeFormat.HOUR_24)).result.current;
+
+ describe('formatTime', () => {
+ it('should format with zero-padded hours and minutes', () => {
+ const { formatTime } = render24();
+ expect(formatTime(9, 5)).toBe('09:05');
+ });
+
+ it('should format midnight as 00:00', () => {
+ const { formatTime } = render24();
+ expect(formatTime(0, 0)).toBe('00:00');
+ });
+
+ it('should format 23:59', () => {
+ const { formatTime } = render24();
+ expect(formatTime(23, 59)).toBe('23:59');
+ });
+ });
+
+ describe('parseTime', () => {
+ it('should parse valid 24h time', () => {
+ const { parseTime } = render24();
+ expect(parseTime('14:30')).toEqual({ hour: 14, minute: 30 });
+ });
+
+ it('should parse midnight', () => {
+ const { parseTime } = render24();
+ expect(parseTime('00:00')).toEqual({ hour: 0, minute: 0 });
+ });
+
+ it('should return null for invalid format', () => {
+ const { parseTime } = render24();
+ expect(parseTime('invalid')).toBeNull();
+ });
+
+ it('should return null for out-of-range hour', () => {
+ const { parseTime } = render24();
+ expect(parseTime('25:00')).toBeNull();
+ });
+
+ it('should return null for out-of-range minute', () => {
+ const { parseTime } = render24();
+ expect(parseTime('12:60')).toBeNull();
+ });
+ });
+
+ it('should set isHour12 to false', () => {
+ const { isHour12 } = render24();
+ expect(isHour12).toBe(false);
+ });
+ });
+
+ describe('12-hour format', () => {
+ const render12 = () =>
+ renderHook(() => useTimeInput(TimeFormat.HOUR_12)).result.current;
+
+ describe('formatTime', () => {
+ it('should format AM time', () => {
+ const { formatTime } = render12();
+ expect(formatTime(9, 5)).toBe('09:05 AM');
+ });
+
+ it('should format PM time', () => {
+ const { formatTime } = render12();
+ expect(formatTime(14, 30)).toBe('02:30 PM');
+ });
+
+ it('should format midnight as 12:00 AM', () => {
+ const { formatTime } = render12();
+ expect(formatTime(0, 0)).toBe('12:00 AM');
+ });
+
+ it('should format noon as 12:00 PM', () => {
+ const { formatTime } = render12();
+ expect(formatTime(12, 0)).toBe('12:00 PM');
+ });
+ });
+
+ describe('parseTime', () => {
+ it('should parse AM time', () => {
+ const { parseTime } = render12();
+ expect(parseTime('09:30 AM')).toEqual({ hour: 9, minute: 30 });
+ });
+
+ it('should parse PM time', () => {
+ const { parseTime } = render12();
+ expect(parseTime('02:30 PM')).toEqual({ hour: 14, minute: 30 });
+ });
+
+ it('should parse 12:00 AM as midnight', () => {
+ const { parseTime } = render12();
+ expect(parseTime('12:00 AM')).toEqual({ hour: 0, minute: 0 });
+ });
+
+ it('should parse 12:00 PM as noon', () => {
+ const { parseTime } = render12();
+ expect(parseTime('12:00 PM')).toEqual({ hour: 12, minute: 0 });
+ });
+
+ it('should return null for invalid format', () => {
+ const { parseTime } = render12();
+ expect(parseTime('14:30')).toBeNull();
+ });
+
+ it('should return null for out-of-range hour', () => {
+ const { parseTime } = render12();
+ expect(parseTime('13:00 AM')).toBeNull();
+ });
+
+ it('should return null for out-of-range minute', () => {
+ const { parseTime } = render12();
+ expect(parseTime('12:60 PM')).toBeNull();
+ });
+ });
+
+ it('should set isHour12 to true', () => {
+ const { isHour12 } = render12();
+ expect(isHour12).toBe(true);
+ });
+ });
+});
diff --git a/packages/twenty-front/src/modules/ui/input/components/internal/date/hooks/useTimeInput.ts b/packages/twenty-front/src/modules/ui/input/components/internal/date/hooks/useTimeInput.ts
new file mode 100644
index 0000000000..65ec783a74
--- /dev/null
+++ b/packages/twenty-front/src/modules/ui/input/components/internal/date/hooks/useTimeInput.ts
@@ -0,0 +1,75 @@
+import { TimeFormat } from '@/localization/constants/TimeFormat';
+import { useCallback } from 'react';
+
+type TimeValue = {
+ hour: number;
+ minute: number;
+};
+
+type UseTimeInputReturn = {
+ formatTime: (hour: number, minute: number) => string;
+ parseTime: (timeString: string) => TimeValue | null;
+ isHour12: boolean;
+};
+
+export const useTimeInput = (timeFormat: TimeFormat): UseTimeInputReturn => {
+ const isHour12 = timeFormat === TimeFormat.HOUR_12;
+
+ const formatTime = useCallback(
+ (hour: number, minute: number): string => {
+ const hh = isHour12
+ ? (hour % 12 || 12).toString().padStart(2, '0')
+ : hour.toString().padStart(2, '0');
+ const mm = minute.toString().padStart(2, '0');
+
+ if (isHour12) {
+ const amPm = hour >= 12 ? 'PM' : 'AM';
+ return `${hh}:${mm} ${amPm}`;
+ }
+ return `${hh}:${mm}`;
+ },
+ [isHour12],
+ );
+
+ const parseTime = (timeString: string): TimeValue | null => {
+ if (isHour12) {
+ const match = timeString.match(/^(\d{1,2}):(\d{2})\s*(AM|PM)$/i);
+ if (!match) return null;
+
+ const [, hoursStr, minutesStr, amPmStr] = match;
+ const hours = parseInt(hoursStr, 10);
+ const minutes = parseInt(minutesStr, 10);
+
+ const isValidHour = hours >= 1 && hours <= 12;
+ const isValidMinute = minutes >= 0 && minutes <= 59;
+ if (isNaN(hours) || isNaN(minutes) || !isValidHour || !isValidMinute) {
+ return null;
+ }
+
+ const isPM = amPmStr.toUpperCase() === 'PM';
+ const hour24 = (hours % 12) + (isPM ? 12 : 0);
+ return { hour: hour24, minute: minutes };
+ } else {
+ const match = timeString.match(/^(\d{1,2}):(\d{2})$/);
+ if (!match) return null;
+
+ const [, hoursStr, minutesStr] = match;
+ const hours = parseInt(hoursStr, 10);
+ const minutes = parseInt(minutesStr, 10);
+
+ const isValidHour = hours >= 0 && hours <= 23;
+ const isValidMinute = minutes >= 0 && minutes <= 59;
+ if (isNaN(hours) || isNaN(minutes) || !isValidHour || !isValidMinute) {
+ return null;
+ }
+
+ return { hour: hours, minute: minutes };
+ }
+ };
+
+ return {
+ formatTime,
+ parseTime,
+ isHour12,
+ };
+};
diff --git a/packages/twenty-front/src/modules/ui/input/components/internal/date/utils/getTimeBlocks.ts b/packages/twenty-front/src/modules/ui/input/components/internal/date/utils/getTimeBlocks.ts
index f4b734992d..d9f54b4e2c 100644
--- a/packages/twenty-front/src/modules/ui/input/components/internal/date/utils/getTimeBlocks.ts
+++ b/packages/twenty-front/src/modules/ui/input/components/internal/date/utils/getTimeBlocks.ts
@@ -18,7 +18,9 @@ export const getTimeBlocks = (timeFormat: TimeFormat) => {
maxLength: 2,
},
aa: {
- mask: '**',
+ mask: IMask.MaskedEnum,
+ enum: ['AM', 'PM'],
+ prepareChar: (str: string) => str.toUpperCase(),
},
};
};
diff --git a/packages/twenty-front/src/modules/ui/layout/dropdown/hooks/__tests__/useCloseAnyOpenDropdown.test.tsx b/packages/twenty-front/src/modules/ui/layout/dropdown/hooks/__tests__/useCloseAnyOpenDropdown.test.tsx
index aab39666b1..77b88c9928 100644
--- a/packages/twenty-front/src/modules/ui/layout/dropdown/hooks/__tests__/useCloseAnyOpenDropdown.test.tsx
+++ b/packages/twenty-front/src/modules/ui/layout/dropdown/hooks/__tests__/useCloseAnyOpenDropdown.test.tsx
@@ -5,11 +5,16 @@ import { act } from 'react';
import { DropdownComponentInstanceContext } from '@/ui/layout/dropdown/contexts/DropdownComponentInstanceContext';
import { useCloseAnyOpenDropdown } from '@/ui/layout/dropdown/hooks/useCloseAnyOpenDropdown';
import { useOpenDropdown } from '@/ui/layout/dropdown/hooks/useOpenDropdown';
+import { activeDropdownFocusIdState } from '@/ui/layout/dropdown/states/activeDropdownFocusIdState';
import { isDropdownOpenComponentState } from '@/ui/layout/dropdown/states/isDropdownOpenComponentState';
+import { previousDropdownFocusIdStackState } from '@/ui/layout/dropdown/states/previousDropdownFocusIdStackState';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
import { jotaiStore } from '@/ui/utilities/state/jotai/jotaiStore';
const dropdownId = 'test-dropdown-id';
+const dropdownIdA = 'test-dropdown-id-a';
+const dropdownIdB = 'test-dropdown-id-b';
+const dropdownIdC = 'test-dropdown-id-c';
const Wrapper = ({ children }: { children: React.ReactNode }) => {
return (
@@ -29,6 +34,8 @@ describe('useCloseAnyOpenDropdown', () => {
isDropdownOpenComponentState.atomFamily({ instanceId: dropdownId }),
false,
);
+ jotaiStore.set(activeDropdownFocusIdState.atom, null);
+ jotaiStore.set(previousDropdownFocusIdStackState.atom, []);
});
it('should open dropdown and then close it with closeAnyOpenDropdown', async () => {
@@ -64,4 +71,63 @@ describe('useCloseAnyOpenDropdown', () => {
expect(result.current.isDropdownOpen).toBe(false);
});
+
+ it('should close all nested dropdowns when multiple are open', () => {
+ jotaiStore.set(
+ isDropdownOpenComponentState.atomFamily({ instanceId: dropdownIdA }),
+ true,
+ );
+ jotaiStore.set(
+ isDropdownOpenComponentState.atomFamily({ instanceId: dropdownIdB }),
+ true,
+ );
+ jotaiStore.set(
+ isDropdownOpenComponentState.atomFamily({ instanceId: dropdownIdC }),
+ true,
+ );
+ jotaiStore.set(activeDropdownFocusIdState.atom, dropdownIdC);
+ jotaiStore.set(previousDropdownFocusIdStackState.atom, [
+ dropdownIdA,
+ dropdownIdB,
+ ]);
+
+ const { result } = renderHook(() => useCloseAnyOpenDropdown(), {
+ wrapper: Wrapper,
+ });
+
+ act(() => {
+ result.current.closeAnyOpenDropdown();
+ });
+
+ expect(
+ jotaiStore.get(
+ isDropdownOpenComponentState.atomFamily({ instanceId: dropdownIdA }),
+ ),
+ ).toBe(false);
+ expect(
+ jotaiStore.get(
+ isDropdownOpenComponentState.atomFamily({ instanceId: dropdownIdB }),
+ ),
+ ).toBe(false);
+ expect(
+ jotaiStore.get(
+ isDropdownOpenComponentState.atomFamily({ instanceId: dropdownIdC }),
+ ),
+ ).toBe(false);
+ expect(jotaiStore.get(activeDropdownFocusIdState.atom)).toBeNull();
+ expect(jotaiStore.get(previousDropdownFocusIdStackState.atom)).toEqual([]);
+ });
+
+ it('should do nothing when no dropdowns are open', () => {
+ const { result } = renderHook(() => useCloseAnyOpenDropdown(), {
+ wrapper: Wrapper,
+ });
+
+ act(() => {
+ result.current.closeAnyOpenDropdown();
+ });
+
+ expect(jotaiStore.get(activeDropdownFocusIdState.atom)).toBeNull();
+ expect(jotaiStore.get(previousDropdownFocusIdStackState.atom)).toEqual([]);
+ });
});
diff --git a/packages/twenty-front/src/modules/ui/layout/dropdown/hooks/useCloseAnyOpenDropdown.ts b/packages/twenty-front/src/modules/ui/layout/dropdown/hooks/useCloseAnyOpenDropdown.ts
index ce42ccbfae..3f05ec7fa2 100644
--- a/packages/twenty-front/src/modules/ui/layout/dropdown/hooks/useCloseAnyOpenDropdown.ts
+++ b/packages/twenty-front/src/modules/ui/layout/dropdown/hooks/useCloseAnyOpenDropdown.ts
@@ -2,7 +2,7 @@ import { useCallback } from 'react';
import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
import { activeDropdownFocusIdState } from '@/ui/layout/dropdown/states/activeDropdownFocusIdState';
-import { previousDropdownFocusIdState } from '@/ui/layout/dropdown/states/previousDropdownFocusIdState';
+import { previousDropdownFocusIdStackState } from '@/ui/layout/dropdown/states/previousDropdownFocusIdStackState';
import { useRemoveFocusItemFromFocusStackById } from '@/ui/utilities/focus/hooks/useRemoveFocusItemFromFocusStackById';
import { useStore } from 'jotai';
import { isDefined } from 'twenty-shared/utils';
@@ -16,21 +16,16 @@ export const useCloseAnyOpenDropdown = () => {
const store = useStore();
const closeAnyOpenDropdown = useCallback(() => {
- const previousDropdownFocusId = store.get(
- previousDropdownFocusIdState.atom,
- );
-
+ const previousStack = store.get(previousDropdownFocusIdStackState.atom);
const activeDropdownFocusId = store.get(activeDropdownFocusIdState.atom);
const thereIsNoDropdownOpen =
- !isDefined(activeDropdownFocusId) && !isDefined(previousDropdownFocusId);
+ !isDefined(activeDropdownFocusId) && previousStack.length === 0;
if (thereIsNoDropdownOpen) {
return;
}
- const thereIsOneNestedDropdownOpen = isDefined(previousDropdownFocusId);
-
if (isDefined(activeDropdownFocusId)) {
closeDropdown(activeDropdownFocusId);
removeFocusItemFromFocusStackById({
@@ -38,14 +33,14 @@ export const useCloseAnyOpenDropdown = () => {
});
}
- if (thereIsOneNestedDropdownOpen) {
- closeDropdown(previousDropdownFocusId);
+ for (const previousId of previousStack) {
+ closeDropdown(previousId);
removeFocusItemFromFocusStackById({
- focusId: previousDropdownFocusId,
+ focusId: previousId,
});
}
- store.set(previousDropdownFocusIdState.atom, null);
+ store.set(previousDropdownFocusIdStackState.atom, []);
store.set(activeDropdownFocusIdState.atom, null);
}, [closeDropdown, removeFocusItemFromFocusStackById, store]);
diff --git a/packages/twenty-front/src/modules/ui/layout/dropdown/hooks/useGoBackToPreviousDropdownFocusId.ts b/packages/twenty-front/src/modules/ui/layout/dropdown/hooks/useGoBackToPreviousDropdownFocusId.ts
index 2dde02bdcd..ae653fee2a 100644
--- a/packages/twenty-front/src/modules/ui/layout/dropdown/hooks/useGoBackToPreviousDropdownFocusId.ts
+++ b/packages/twenty-front/src/modules/ui/layout/dropdown/hooks/useGoBackToPreviousDropdownFocusId.ts
@@ -1,20 +1,23 @@
import { useCallback } from 'react';
import { activeDropdownFocusIdState } from '@/ui/layout/dropdown/states/activeDropdownFocusIdState';
-import { previousDropdownFocusIdState } from '@/ui/layout/dropdown/states/previousDropdownFocusIdState';
+import { previousDropdownFocusIdStackState } from '@/ui/layout/dropdown/states/previousDropdownFocusIdStackState';
import { useStore } from 'jotai';
-// TODO: this won't work for more than 1 nested dropdown
export const useGoBackToPreviousDropdownFocusId = () => {
const store = useStore();
const goBackToPreviousDropdownFocusId = useCallback(() => {
- const previouslyFocusedDropdownId = store.get(
- previousDropdownFocusIdState.atom,
- );
+ const previousStack = store.get(previousDropdownFocusIdStackState.atom);
+
+ const previouslyFocusedDropdownId =
+ previousStack[previousStack.length - 1] ?? null;
store.set(activeDropdownFocusIdState.atom, previouslyFocusedDropdownId);
- store.set(previousDropdownFocusIdState.atom, null);
+ store.set(
+ previousDropdownFocusIdStackState.atom,
+ previousStack.slice(0, -1),
+ );
}, [store]);
return {
diff --git a/packages/twenty-front/src/modules/ui/layout/dropdown/hooks/useSetFocusedDropdownIdAndMemorizePrevious.ts b/packages/twenty-front/src/modules/ui/layout/dropdown/hooks/useSetFocusedDropdownIdAndMemorizePrevious.ts
index 15da4c07d9..c2b5280eec 100644
--- a/packages/twenty-front/src/modules/ui/layout/dropdown/hooks/useSetFocusedDropdownIdAndMemorizePrevious.ts
+++ b/packages/twenty-front/src/modules/ui/layout/dropdown/hooks/useSetFocusedDropdownIdAndMemorizePrevious.ts
@@ -1,8 +1,9 @@
import { useCallback } from 'react';
import { activeDropdownFocusIdState } from '@/ui/layout/dropdown/states/activeDropdownFocusIdState';
-import { previousDropdownFocusIdState } from '@/ui/layout/dropdown/states/previousDropdownFocusIdState';
+import { previousDropdownFocusIdStackState } from '@/ui/layout/dropdown/states/previousDropdownFocusIdStackState';
import { useStore } from 'jotai';
+import { isDefined } from 'twenty-shared/utils';
export const useSetActiveDropdownFocusIdAndMemorizePrevious = () => {
const store = useStore();
@@ -15,7 +16,14 @@ export const useSetActiveDropdownFocusIdAndMemorizePrevious = () => {
return;
}
- store.set(previousDropdownFocusIdState.atom, activeDropdownFocusId);
+ if (isDefined(activeDropdownFocusId) && isDefined(dropdownId)) {
+ const previousStack = store.get(previousDropdownFocusIdStackState.atom);
+ store.set(previousDropdownFocusIdStackState.atom, [
+ ...previousStack,
+ activeDropdownFocusId,
+ ]);
+ }
+
store.set(activeDropdownFocusIdState.atom, dropdownId);
},
[store],
diff --git a/packages/twenty-front/src/modules/ui/layout/dropdown/states/previousDropdownFocusIdStackState.ts b/packages/twenty-front/src/modules/ui/layout/dropdown/states/previousDropdownFocusIdStackState.ts
new file mode 100644
index 0000000000..3853eef73f
--- /dev/null
+++ b/packages/twenty-front/src/modules/ui/layout/dropdown/states/previousDropdownFocusIdStackState.ts
@@ -0,0 +1,6 @@
+import { createAtomState } from '@/ui/utilities/state/jotai/utils/createAtomState';
+
+export const previousDropdownFocusIdStackState = createAtomState({
+ key: 'previousDropdownFocusIdStackState',
+ defaultValue: [],
+});
diff --git a/packages/twenty-front/src/modules/ui/layout/dropdown/states/previousDropdownFocusIdState.ts b/packages/twenty-front/src/modules/ui/layout/dropdown/states/previousDropdownFocusIdState.ts
deleted file mode 100644
index 043f4e6f2f..0000000000
--- a/packages/twenty-front/src/modules/ui/layout/dropdown/states/previousDropdownFocusIdState.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-import { createAtomState } from '@/ui/utilities/state/jotai/utils/createAtomState';
-
-export const previousDropdownFocusIdState = createAtomState({
- key: 'previousDropdownFocusIdState',
- defaultValue: null,
-});
diff --git a/packages/twenty-ui/src/display/icon/components/TablerIcons.ts b/packages/twenty-ui/src/display/icon/components/TablerIcons.ts
index 7c143f0d97..80306f1634 100644
--- a/packages/twenty-ui/src/display/icon/components/TablerIcons.ts
+++ b/packages/twenty-ui/src/display/icon/components/TablerIcons.ts
@@ -84,6 +84,7 @@ export {
IconCirclePlus,
IconCircleX,
IconClick,
+ IconClock,
IconClockHour8,
IconClockPlay,
IconClockShare,
diff --git a/packages/twenty-ui/src/display/index.ts b/packages/twenty-ui/src/display/index.ts
index 1168da1383..9f23791674 100644
--- a/packages/twenty-ui/src/display/index.ts
+++ b/packages/twenty-ui/src/display/index.ts
@@ -159,6 +159,7 @@ export {
IconCirclePlus,
IconCircleX,
IconClick,
+ IconClock,
IconClockHour8,
IconClockPlay,
IconClockShare,