New Datetime field picker (#4907)
### Description New Datetime field picker ### Refs https://github.com/twentyhq/twenty/issues/4376 ### Demo https://github.com/twentyhq/twenty/assets/140154534/32656323-972c-413a-9986-a78efffae1b4 Fixes #4376 --------- Co-authored-by: gitstart-twenty <gitstart-twenty@users.noreply.github.com> Co-authored-by: v1b3m <vibenjamin6@gmail.com> Co-authored-by: Matheus <matheus_benini@hotmail.com> Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com> Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
committed by
GitHub
parent
464a2d5998
commit
efcb5dc6d4
@ -152,6 +152,7 @@
|
|||||||
"react-hook-form": "^7.45.1",
|
"react-hook-form": "^7.45.1",
|
||||||
"react-hotkeys-hook": "^4.4.4",
|
"react-hotkeys-hook": "^4.4.4",
|
||||||
"react-icons": "^4.12.0",
|
"react-icons": "^4.12.0",
|
||||||
|
"react-imask": "^7.6.0",
|
||||||
"react-intersection-observer": "^9.5.2",
|
"react-intersection-observer": "^9.5.2",
|
||||||
"react-loading-skeleton": "^3.3.1",
|
"react-loading-skeleton": "^3.3.1",
|
||||||
"react-phone-number-input": "^3.3.4",
|
"react-phone-number-input": "^3.3.4",
|
||||||
|
|||||||
@ -6,8 +6,6 @@ import SearchBar from '@theme-original/SearchBar';
|
|||||||
const CustomComponents = {
|
const CustomComponents = {
|
||||||
'search-bar': () => {
|
'search-bar': () => {
|
||||||
const openSearchModal = () => {
|
const openSearchModal = () => {
|
||||||
console.log('yo');
|
|
||||||
|
|
||||||
const searchInput = document.querySelector('#search-bar');
|
const searchInput = document.querySelector('#search-bar');
|
||||||
if (searchInput) {
|
if (searchInput) {
|
||||||
searchInput.focus();
|
searchInput.focus();
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import { IconBell } from "@tabler/icons-react";
|
import { IconBell } from '@tabler/icons-react';
|
||||||
import { MenuItemToggle } from "@/ui/navigation/menu-item/components/MenuItemToggle";
|
|
||||||
|
import { MenuItemToggle } from '@/ui/navigation/menu-item/components/MenuItemToggle';
|
||||||
|
|
||||||
export const MyComponent = () => {
|
export const MyComponent = () => {
|
||||||
const handleToggleChange = (toggled) => {
|
const handleToggleChange = (toggled) => {
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
IconCalendarEvent,
|
IconCalendarEvent,
|
||||||
|
IconCalendarTime,
|
||||||
IconCheck,
|
IconCheck,
|
||||||
IconCoins,
|
IconCoins,
|
||||||
IconComponent,
|
IconComponent,
|
||||||
@ -67,8 +68,8 @@ export const SETTINGS_FIELD_TYPE_CONFIGS: Record<
|
|||||||
defaultValue: true,
|
defaultValue: true,
|
||||||
},
|
},
|
||||||
[FieldMetadataType.DateTime]: {
|
[FieldMetadataType.DateTime]: {
|
||||||
label: 'Date & Time',
|
label: 'Date and Time',
|
||||||
Icon: IconCalendarEvent,
|
Icon: IconCalendarTime,
|
||||||
defaultValue: DEFAULT_DATE_VALUE.toISOString(),
|
defaultValue: DEFAULT_DATE_VALUE.toISOString(),
|
||||||
},
|
},
|
||||||
[FieldMetadataType.Date]: {
|
[FieldMetadataType.Date]: {
|
||||||
|
|||||||
@ -41,9 +41,9 @@ export type DateInputProps = {
|
|||||||
|
|
||||||
export const DateInput = ({
|
export const DateInput = ({
|
||||||
value,
|
value,
|
||||||
hotkeyScope,
|
|
||||||
onEnter,
|
onEnter,
|
||||||
onEscape,
|
onEscape,
|
||||||
|
hotkeyScope,
|
||||||
onClickOutside,
|
onClickOutside,
|
||||||
clearable,
|
clearable,
|
||||||
onChange,
|
onChange,
|
||||||
@ -65,7 +65,7 @@ export const DateInput = ({
|
|||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleChange = (newDate: Date) => {
|
const handleChange = (newDate: Date | null) => {
|
||||||
setInternalValue(newDate);
|
setInternalValue(newDate);
|
||||||
onChange?.(newDate);
|
onChange?.(newDate);
|
||||||
};
|
};
|
||||||
@ -96,6 +96,7 @@ export const DateInput = ({
|
|||||||
}}
|
}}
|
||||||
clearable={clearable ? clearable : false}
|
clearable={clearable ? clearable : false}
|
||||||
isDateTimeInput={isDateTimeInput}
|
isDateTimeInput={isDateTimeInput}
|
||||||
|
onClickOutside={onClickOutside}
|
||||||
/>
|
/>
|
||||||
</StyledCalendarContainer>
|
</StyledCalendarContainer>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -0,0 +1,109 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { useIMask } from 'react-imask';
|
||||||
|
import styled from '@emotion/styled';
|
||||||
|
import { DateTime } from 'luxon';
|
||||||
|
|
||||||
|
import { DATE_TIME_BLOCKS } from '@/ui/input/components/internal/date/constants/DateTimeBlocks';
|
||||||
|
import { DATE_TIME_MASK } from '@/ui/input/components/internal/date/constants/DateTimeMask';
|
||||||
|
|
||||||
|
const StyledInputContainer = styled.div`
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
border-bottom: 1px solid ${({ theme }) => theme.border.color.light};
|
||||||
|
height: ${({ theme }) => theme.spacing(8)};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledInput = styled.input<{ hasError?: boolean }>`
|
||||||
|
background: ${({ theme }) => theme.background.secondary};
|
||||||
|
border: none;
|
||||||
|
color: ${({ theme }) => theme.font.color.primary};
|
||||||
|
outline: none;
|
||||||
|
padding: 8px;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: ${({ theme }) => theme.font.size.md};
|
||||||
|
width: 100%;
|
||||||
|
color: ${({ hasError, theme }) => (hasError ? theme.color.red : 'inherit')};
|
||||||
|
`;
|
||||||
|
|
||||||
|
type DateTimeInputProps = {
|
||||||
|
onChange?: (date: Date | null) => void;
|
||||||
|
date: Date | null;
|
||||||
|
isDateTimeInput?: boolean;
|
||||||
|
onError?: (error: Error) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DateTimeInput = ({
|
||||||
|
date,
|
||||||
|
onChange,
|
||||||
|
isDateTimeInput,
|
||||||
|
}: DateTimeInputProps) => {
|
||||||
|
const [hasError, setHasError] = useState(false);
|
||||||
|
|
||||||
|
const parseDateToString = (date: any) => {
|
||||||
|
const dateParsed = DateTime.fromJSDate(date);
|
||||||
|
|
||||||
|
const formattedDate = dateParsed.toFormat('MM/dd/yyyy HH:mm');
|
||||||
|
|
||||||
|
return formattedDate;
|
||||||
|
};
|
||||||
|
|
||||||
|
const parseStringToDate = (str: string) => {
|
||||||
|
setHasError(false);
|
||||||
|
|
||||||
|
const parsedDate = DateTime.fromFormat(str, 'MM/dd/yyyy HH:mm');
|
||||||
|
|
||||||
|
const isValid = parsedDate.isValid;
|
||||||
|
|
||||||
|
if (!isValid) {
|
||||||
|
setHasError(true);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const jsDate = parsedDate.toJSDate();
|
||||||
|
|
||||||
|
return jsDate;
|
||||||
|
};
|
||||||
|
|
||||||
|
const { ref, setValue, value } = useIMask(
|
||||||
|
{
|
||||||
|
mask: Date,
|
||||||
|
pattern: DATE_TIME_MASK,
|
||||||
|
blocks: DATE_TIME_BLOCKS,
|
||||||
|
min: new Date(1970, 0, 1),
|
||||||
|
max: new Date(2100, 0, 1),
|
||||||
|
format: parseDateToString,
|
||||||
|
parse: parseStringToDate,
|
||||||
|
lazy: false,
|
||||||
|
autofix: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onComplete: (value) => {
|
||||||
|
const parsedDate = parseStringToDate(value);
|
||||||
|
|
||||||
|
onChange?.(parsedDate);
|
||||||
|
},
|
||||||
|
onAccept: () => {
|
||||||
|
setHasError(false);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setValue(parseDateToString(date));
|
||||||
|
}, [date, setValue]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyledInputContainer>
|
||||||
|
<StyledInput
|
||||||
|
type="text"
|
||||||
|
ref={ref as any}
|
||||||
|
placeholder={`Type date${
|
||||||
|
isDateTimeInput ? ' and time' : ' (mm/dd/yyyy)'
|
||||||
|
}`}
|
||||||
|
value={value}
|
||||||
|
hasError={hasError}
|
||||||
|
/>
|
||||||
|
</StyledInputContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -1,9 +1,18 @@
|
|||||||
import { useState } from 'react';
|
|
||||||
import ReactDatePicker from 'react-datepicker';
|
import ReactDatePicker from 'react-datepicker';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
import { IconCalendarX } from 'twenty-ui';
|
import { IconCalendarX, IconChevronLeft, IconChevronRight } from 'twenty-ui';
|
||||||
|
|
||||||
|
import { LightIconButton } from '@/ui/input/button/components/LightIconButton';
|
||||||
|
import { DateTimeInput } from '@/ui/input/components/internal/date/components/DateTimeInput';
|
||||||
|
import {
|
||||||
|
MONTH_AND_YEAR_DROPDOWN_ID,
|
||||||
|
MONTH_AND_YEAR_DROPDOWN_MONTH_SELECT_ID,
|
||||||
|
MONTH_AND_YEAR_DROPDOWN_YEAR_SELECT_ID,
|
||||||
|
MonthAndYearDropdown,
|
||||||
|
} from '@/ui/input/components/internal/date/components/MonthAndYearDropdown';
|
||||||
|
import { TimeInput } from '@/ui/input/components/internal/date/components/TimeInput';
|
||||||
|
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
|
||||||
import { MenuItemLeftContent } from '@/ui/navigation/menu-item/internals/components/MenuItemLeftContent';
|
import { MenuItemLeftContent } from '@/ui/navigation/menu-item/internals/components/MenuItemLeftContent';
|
||||||
import { StyledHoverableMenuItemBase } from '@/ui/navigation/menu-item/internals/components/StyledMenuItemBase';
|
import { StyledHoverableMenuItemBase } from '@/ui/navigation/menu-item/internals/components/StyledMenuItemBase';
|
||||||
import { OVERLAY_BACKGROUND } from '@/ui/theme/constants/OverlayBackground';
|
import { OVERLAY_BACKGROUND } from '@/ui/theme/constants/OverlayBackground';
|
||||||
@ -45,13 +54,21 @@ const StyledContainer = styled.div`
|
|||||||
& .react-datepicker__header {
|
& .react-datepicker__header {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border: none;
|
border: none;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&
|
||||||
|
.react-datepicker__input-time-container
|
||||||
|
.react-datepicker-time__input-container
|
||||||
|
.react-datepicker-time__input {
|
||||||
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
& .react-datepicker__header__dropdown {
|
& .react-datepicker__header__dropdown {
|
||||||
display: flex;
|
display: flex;
|
||||||
color: ${({ theme }) => theme.font.color.primary};
|
color: ${({ theme }) => theme.font.color.primary};
|
||||||
margin-left: ${({ theme }) => theme.spacing(1)};
|
margin-left: ${({ theme }) => theme.spacing(1)};
|
||||||
margin-bottom: ${({ theme }) => theme.spacing(1)};
|
margin-bottom: ${({ theme }) => theme.spacing(10)};
|
||||||
}
|
}
|
||||||
|
|
||||||
& .react-datepicker__month-dropdown-container,
|
& .react-datepicker__month-dropdown-container,
|
||||||
@ -177,7 +194,7 @@ const StyledContainer = styled.div`
|
|||||||
}
|
}
|
||||||
& .react-datepicker__navigation--previous {
|
& .react-datepicker__navigation--previous {
|
||||||
right: 38px;
|
right: 38px;
|
||||||
top: 8px;
|
top: 6px;
|
||||||
left: auto;
|
left: auto;
|
||||||
|
|
||||||
& > span {
|
& > span {
|
||||||
@ -187,7 +204,7 @@ const StyledContainer = styled.div`
|
|||||||
|
|
||||||
& .react-datepicker__navigation--next {
|
& .react-datepicker__navigation--next {
|
||||||
right: 6px;
|
right: 6px;
|
||||||
top: 8px;
|
top: 6px;
|
||||||
|
|
||||||
& > span {
|
& > span {
|
||||||
margin-left: 6px;
|
margin-left: 6px;
|
||||||
@ -239,32 +256,26 @@ const StyledButton = styled(MenuItemLeftContent)`
|
|||||||
justify-content: start;
|
justify-content: start;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const StyledCustomDatePickerHeader = styled.div`
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding-left: ${({ theme }) => theme.spacing(2)};
|
||||||
|
padding-right: ${({ theme }) => theme.spacing(2)};
|
||||||
|
padding-top: ${({ theme }) => theme.spacing(2)};
|
||||||
|
|
||||||
|
gap: ${({ theme }) => theme.spacing(1)};
|
||||||
|
`;
|
||||||
|
|
||||||
export type InternalDatePickerProps = {
|
export type InternalDatePickerProps = {
|
||||||
date: Date | null;
|
date: Date;
|
||||||
onMouseSelect?: (date: Date | null) => void;
|
onMouseSelect?: (date: Date | null) => void;
|
||||||
onChange?: (date: Date) => void;
|
onChange?: (date: Date | null) => void;
|
||||||
clearable?: boolean;
|
clearable?: boolean;
|
||||||
isDateTimeInput?: boolean;
|
isDateTimeInput?: boolean;
|
||||||
|
onClickOutside?: (event: MouseEvent | TouchEvent, date: Date | null) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const StyledInputContainer = styled.div`
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
border-bottom: 1px solid ${({ theme }) => theme.border.color.light};
|
|
||||||
height: ${({ theme }) => theme.spacing(8)};
|
|
||||||
`;
|
|
||||||
|
|
||||||
const StyledInput = styled.input`
|
|
||||||
background: ${({ theme }) => theme.background.secondary};
|
|
||||||
border: none;
|
|
||||||
color: ${({ theme }) => theme.font.color.primary};
|
|
||||||
outline: none;
|
|
||||||
padding: 8px;
|
|
||||||
font-weight: 500;
|
|
||||||
font-size: ${({ theme }) => theme.font.size.md};
|
|
||||||
width: 100%;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const PICKER_DATE_FORMAT = 'MM/dd/yyyy';
|
const PICKER_DATE_FORMAT = 'MM/dd/yyyy';
|
||||||
|
|
||||||
export const InternalDatePicker = ({
|
export const InternalDatePicker = ({
|
||||||
@ -273,92 +284,97 @@ export const InternalDatePicker = ({
|
|||||||
onMouseSelect,
|
onMouseSelect,
|
||||||
clearable = true,
|
clearable = true,
|
||||||
isDateTimeInput,
|
isDateTimeInput,
|
||||||
|
onClickOutside,
|
||||||
}: InternalDatePickerProps) => {
|
}: InternalDatePickerProps) => {
|
||||||
|
const internalDate = date ?? new Date();
|
||||||
|
|
||||||
|
const dateFormatted =
|
||||||
|
DateTime.fromJSDate(internalDate).toFormat(PICKER_DATE_FORMAT);
|
||||||
|
|
||||||
|
const { closeDropdown } = useDropdown(MONTH_AND_YEAR_DROPDOWN_ID);
|
||||||
|
const { closeDropdown: closeDropdownMonthSelect } = useDropdown(
|
||||||
|
MONTH_AND_YEAR_DROPDOWN_MONTH_SELECT_ID,
|
||||||
|
);
|
||||||
|
const { closeDropdown: closeDropdownYearSelect } = useDropdown(
|
||||||
|
MONTH_AND_YEAR_DROPDOWN_YEAR_SELECT_ID,
|
||||||
|
);
|
||||||
|
|
||||||
const handleClear = () => {
|
const handleClear = () => {
|
||||||
|
closeDropdowns();
|
||||||
onMouseSelect?.(null);
|
onMouseSelect?.(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
const initialDate = date
|
const closeDropdowns = () => {
|
||||||
? DateTime.fromJSDate(date).toFormat(PICKER_DATE_FORMAT)
|
closeDropdownYearSelect();
|
||||||
: DateTime.now().toFormat(PICKER_DATE_FORMAT);
|
closeDropdownMonthSelect();
|
||||||
|
closeDropdown();
|
||||||
|
};
|
||||||
|
|
||||||
const [dateValue, setDateValue] = useState(initialDate);
|
const handleClickOutside = (event: any) => {
|
||||||
|
closeDropdowns();
|
||||||
|
onClickOutside?.(event, internalDate);
|
||||||
|
};
|
||||||
|
|
||||||
const dateValueAsJSDate = DateTime.fromFormat(dateValue, PICKER_DATE_FORMAT)
|
const handleMouseSelect = (newDate: Date) => {
|
||||||
.isValid
|
closeDropdowns();
|
||||||
? DateTime.fromFormat(dateValue, PICKER_DATE_FORMAT).toJSDate()
|
onMouseSelect?.(newDate);
|
||||||
: null;
|
};
|
||||||
|
|
||||||
|
// TODO: implement keyboard events here
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledContainer>
|
<StyledContainer>
|
||||||
<div className={clearable ? 'clearable ' : ''}>
|
<div className={clearable ? 'clearable ' : ''}>
|
||||||
<StyledInputContainer>
|
|
||||||
<StyledInput
|
|
||||||
type="text"
|
|
||||||
placeholder={`Type date${
|
|
||||||
isDateTimeInput ? ' and time' : ' (mm/dd/yyyy)'
|
|
||||||
}`}
|
|
||||||
inputMode="numeric"
|
|
||||||
value={dateValue}
|
|
||||||
onChange={(e) => {
|
|
||||||
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());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</StyledInputContainer>
|
|
||||||
|
|
||||||
<ReactDatePicker
|
<ReactDatePicker
|
||||||
open={true}
|
open={true}
|
||||||
selected={dateValueAsJSDate}
|
selected={internalDate}
|
||||||
value={dateValue}
|
value={dateFormatted}
|
||||||
showMonthDropdown
|
onChange={(newDate) => {
|
||||||
showYearDropdown
|
onChange?.(newDate);
|
||||||
onChange={() => {
|
|
||||||
// We need to use onSelect here but onChange is almost redundant with onSelect but is require
|
|
||||||
}}
|
}}
|
||||||
|
renderCustomHeader={({
|
||||||
|
decreaseMonth,
|
||||||
|
increaseMonth,
|
||||||
|
prevMonthButtonDisabled,
|
||||||
|
nextMonthButtonDisabled,
|
||||||
|
}) => (
|
||||||
|
<>
|
||||||
|
<DateTimeInput
|
||||||
|
date={internalDate}
|
||||||
|
isDateTimeInput={isDateTimeInput}
|
||||||
|
onChange={onChange}
|
||||||
|
/>
|
||||||
|
<StyledCustomDatePickerHeader>
|
||||||
|
<TimeInput date={internalDate} onChange={onChange} />
|
||||||
|
<MonthAndYearDropdown date={internalDate} onChange={onChange} />
|
||||||
|
<LightIconButton
|
||||||
|
Icon={IconChevronLeft}
|
||||||
|
onClick={() => decreaseMonth()}
|
||||||
|
size="medium"
|
||||||
|
disabled={prevMonthButtonDisabled}
|
||||||
|
/>
|
||||||
|
<LightIconButton
|
||||||
|
Icon={IconChevronRight}
|
||||||
|
onClick={() => increaseMonth()}
|
||||||
|
size="medium"
|
||||||
|
disabled={nextMonthButtonDisabled}
|
||||||
|
/>
|
||||||
|
</StyledCustomDatePickerHeader>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
customInput={<></>}
|
customInput={<></>}
|
||||||
onSelect={(date: Date, event) => {
|
onSelect={(date: Date, event) => {
|
||||||
// Setting the time to midnight might sometimes return the previous day
|
const dateUTC = DateTime.fromJSDate(date, {
|
||||||
// We set to 21:00 to avoid any timezone issues
|
zone: 'utc',
|
||||||
const dateForDateField = new Date(date.setHours(21, 0, 0, 0));
|
}).toJSDate();
|
||||||
|
|
||||||
setDateValue(
|
|
||||||
DateTime.fromJSDate(date).toFormat(PICKER_DATE_FORMAT),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (event?.type === 'click') {
|
if (event?.type === 'click') {
|
||||||
onMouseSelect?.(isDateTimeInput ? date : dateForDateField);
|
handleMouseSelect?.(dateUTC);
|
||||||
} else {
|
} else {
|
||||||
onChange?.(isDateTimeInput ? date : dateForDateField);
|
onChange?.(dateUTC);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
onClickOutside={handleClickOutside}
|
||||||
></ReactDatePicker>
|
></ReactDatePicker>
|
||||||
</div>
|
</div>
|
||||||
{clearable && (
|
{clearable && (
|
||||||
|
|||||||
@ -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 (
|
||||||
|
<Dropdown
|
||||||
|
dropdownId={MONTH_AND_YEAR_DROPDOWN_ID}
|
||||||
|
dropdownHotkeyScope={{
|
||||||
|
scope: TableHotkeyScope.CellEditMode,
|
||||||
|
}}
|
||||||
|
dropdownPlacement="bottom-start"
|
||||||
|
clickableComponent={
|
||||||
|
<LightIconButton Icon={IconCalendarDue} size="medium" />
|
||||||
|
}
|
||||||
|
dropdownComponents={
|
||||||
|
<DropdownMenuItemsContainer>
|
||||||
|
<Select
|
||||||
|
dropdownId={MONTH_AND_YEAR_DROPDOWN_MONTH_SELECT_ID}
|
||||||
|
options={months}
|
||||||
|
fullWidth
|
||||||
|
disableBlur
|
||||||
|
onChange={handleChangeMonth}
|
||||||
|
value={date.getMonth()}
|
||||||
|
/>
|
||||||
|
<Select
|
||||||
|
dropdownId={MONTH_AND_YEAR_DROPDOWN_YEAR_SELECT_ID}
|
||||||
|
onChange={handleChangeYear}
|
||||||
|
value={date.getFullYear()}
|
||||||
|
options={years}
|
||||||
|
fullWidth
|
||||||
|
disableBlur
|
||||||
|
/>
|
||||||
|
</DropdownMenuItemsContainer>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -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 (
|
||||||
|
<StyledTimeInputContainer>
|
||||||
|
<StyledIconClock size={16} />
|
||||||
|
<StyledTimeInput type="text" ref={ref as any} />
|
||||||
|
</StyledTimeInputContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -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,
|
||||||
|
};
|
||||||
@ -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}`;
|
||||||
@ -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,
|
||||||
|
},
|
||||||
|
};
|
||||||
@ -0,0 +1 @@
|
|||||||
|
export const TIME_MASK = 'HH:mm'; // Define blocks for hours and minutes
|
||||||
@ -26,7 +26,9 @@ export {
|
|||||||
IconBriefcase,
|
IconBriefcase,
|
||||||
IconBuildingSkyscraper,
|
IconBuildingSkyscraper,
|
||||||
IconCalendar,
|
IconCalendar,
|
||||||
|
IconCalendarDue,
|
||||||
IconCalendarEvent,
|
IconCalendarEvent,
|
||||||
|
IconCalendarTime,
|
||||||
IconCalendarX,
|
IconCalendarX,
|
||||||
IconCheck,
|
IconCheck,
|
||||||
IconCheckbox,
|
IconCheckbox,
|
||||||
@ -40,6 +42,7 @@ export {
|
|||||||
IconCirclePlus,
|
IconCirclePlus,
|
||||||
IconCircleX,
|
IconCircleX,
|
||||||
IconClick,
|
IconClick,
|
||||||
|
IconClockHour8,
|
||||||
IconCode,
|
IconCode,
|
||||||
IconCoins,
|
IconCoins,
|
||||||
IconColorSwatch,
|
IconColorSwatch,
|
||||||
|
|||||||
32
yarn.lock
32
yarn.lock
@ -3387,6 +3387,16 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"@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
|
version: 7.23.5
|
||||||
resolution: "@babel/runtime@npm:7.23.5"
|
resolution: "@babel/runtime@npm:7.23.5"
|
||||||
@ -30042,6 +30052,15 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"immer@npm:^10.0.2":
|
||||||
version: 10.0.3
|
version: 10.0.3
|
||||||
resolution: "immer@npm:10.0.3"
|
resolution: "immer@npm:10.0.3"
|
||||||
@ -40738,6 +40757,18 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"react-intersection-observer@npm:^9.5.2":
|
||||||
version: 9.5.3
|
version: 9.5.3
|
||||||
resolution: "react-intersection-observer@npm:9.5.3"
|
resolution: "react-intersection-observer@npm:9.5.3"
|
||||||
@ -45980,6 +46011,7 @@ __metadata:
|
|||||||
react-hook-form: "npm:^7.45.1"
|
react-hook-form: "npm:^7.45.1"
|
||||||
react-hotkeys-hook: "npm:^4.4.4"
|
react-hotkeys-hook: "npm:^4.4.4"
|
||||||
react-icons: "npm:^4.12.0"
|
react-icons: "npm:^4.12.0"
|
||||||
|
react-imask: "npm:^7.6.0"
|
||||||
react-intersection-observer: "npm:^9.5.2"
|
react-intersection-observer: "npm:^9.5.2"
|
||||||
react-loading-skeleton: "npm:^3.3.1"
|
react-loading-skeleton: "npm:^3.3.1"
|
||||||
react-phone-number-input: "npm:^3.3.4"
|
react-phone-number-input: "npm:^3.3.4"
|
||||||
|
|||||||
Reference in New Issue
Block a user