mirror of
https://github.com/twentyhq/twenty.git
synced 2024-09-11 15:35:36 +03:00
Date picker for Date and DateTime field input (#4981)
- Implemented correct mask for Date and DateTime field in InternalDatePicker - Use only keyDown event and click outside in InternalDatePicker and DateInput - Refactored InternalDatePicker UI to have month and year displayed - Fixed bug and synchronized date value between the different inputs that can change it --------- 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>
This commit is contained in:
parent
d63937ec6f
commit
19a3be7b1b
@ -144,7 +144,7 @@
|
||||
"qs": "^6.11.2",
|
||||
"react": "^18.2.0",
|
||||
"react-data-grid": "7.0.0-beta.13",
|
||||
"react-datepicker": "^4.11.0",
|
||||
"react-datepicker": "^6.7.1",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-dropzone": "^14.2.3",
|
||||
"react-error-boundary": "^4.0.11",
|
||||
@ -252,7 +252,7 @@
|
||||
"@types/passport-google-oauth20": "^2.0.11",
|
||||
"@types/passport-jwt": "^3.0.8",
|
||||
"@types/react": "^18.2.39",
|
||||
"@types/react-datepicker": "^4.11.2",
|
||||
"@types/react-datepicker": "^6.2.0",
|
||||
"@types/react-dom": "^18.2.15",
|
||||
"@types/scroll-into-view": "^1.16.0",
|
||||
"@types/supertest": "^2.0.11",
|
||||
|
@ -106,12 +106,14 @@ export const FieldInput = ({
|
||||
onEnter={onEnter}
|
||||
onEscape={onEscape}
|
||||
onClickOutside={onClickOutside}
|
||||
onClear={onSubmit}
|
||||
/>
|
||||
) : isFieldDate(fieldDefinition) ? (
|
||||
<DateFieldInput
|
||||
onEnter={onEnter}
|
||||
onEscape={onEscape}
|
||||
onClickOutside={onClickOutside}
|
||||
onClear={onSubmit}
|
||||
/>
|
||||
) : isFieldNumber(fieldDefinition) ? (
|
||||
<NumberFieldInput
|
||||
|
@ -1,7 +1,5 @@
|
||||
import { useContext } from 'react';
|
||||
|
||||
import { isFieldDateTime } from '@/object-record/record-field/types/guards/isFieldDateTime';
|
||||
|
||||
import { FieldContext } from '../contexts/FieldContext';
|
||||
|
||||
// TODO: have a better clearable settings in metadata ?
|
||||
@ -9,13 +7,9 @@ import { FieldContext } from '../contexts/FieldContext';
|
||||
// Instead of passing it in the context
|
||||
// See: https://github.com/twentyhq/twenty/issues/4403
|
||||
export const useIsFieldClearable = (): boolean => {
|
||||
const { clearable, isLabelIdentifier, fieldDefinition } =
|
||||
useContext(FieldContext);
|
||||
const { clearable, isLabelIdentifier } = useContext(FieldContext);
|
||||
|
||||
const isDateField = isFieldDateTime(fieldDefinition);
|
||||
|
||||
const fieldCanBeCleared =
|
||||
!isLabelIdentifier && !isDateField && clearable !== false;
|
||||
const fieldCanBeCleared = !isLabelIdentifier && clearable !== false;
|
||||
|
||||
return fieldCanBeCleared;
|
||||
};
|
||||
|
@ -1,9 +1,9 @@
|
||||
import { DateDisplay } from '@/ui/field/display/components/DateDisplay';
|
||||
import { DateTimeDisplay } from '@/ui/field/display/components/DateTimeDisplay';
|
||||
|
||||
import { useDateTimeField } from '../../hooks/useDateTimeField';
|
||||
|
||||
export const DateTimeFieldDisplay = () => {
|
||||
const { fieldValue } = useDateTimeField();
|
||||
|
||||
return <DateDisplay value={fieldValue} />;
|
||||
return <DateTimeDisplay value={fieldValue} />;
|
||||
};
|
||||
|
@ -2,6 +2,7 @@ import { Nullable } from 'twenty-ui';
|
||||
|
||||
import { useDateField } from '@/object-record/record-field/meta-types/hooks/useDateField';
|
||||
import { DateInput } from '@/ui/field/input/components/DateInput';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
import { usePersistField } from '../../../hooks/usePersistField';
|
||||
|
||||
@ -11,19 +12,21 @@ export type DateFieldInputProps = {
|
||||
onClickOutside?: FieldInputEvent;
|
||||
onEnter?: FieldInputEvent;
|
||||
onEscape?: FieldInputEvent;
|
||||
onClear?: FieldInputEvent;
|
||||
};
|
||||
|
||||
export const DateFieldInput = ({
|
||||
onEnter,
|
||||
onEscape,
|
||||
onClickOutside,
|
||||
onClear,
|
||||
}: DateFieldInputProps) => {
|
||||
const { fieldValue, hotkeyScope, setDraftValue } = useDateField();
|
||||
const { fieldValue, setDraftValue } = useDateField();
|
||||
|
||||
const persistField = usePersistField();
|
||||
|
||||
const persistDate = (newDate: Nullable<Date>) => {
|
||||
if (!newDate) {
|
||||
if (!isDefined(newDate)) {
|
||||
persistField(null);
|
||||
} else {
|
||||
const newDateISO = newDate?.toISOString();
|
||||
@ -51,17 +54,21 @@ export const DateFieldInput = ({
|
||||
setDraftValue(newDate?.toDateString() ?? '');
|
||||
};
|
||||
|
||||
const handleClear = () => {
|
||||
onClear?.(() => persistDate(null));
|
||||
};
|
||||
|
||||
const dateValue = fieldValue ? new Date(fieldValue) : null;
|
||||
|
||||
return (
|
||||
<DateInput
|
||||
hotkeyScope={hotkeyScope}
|
||||
onClickOutside={handleClickOutside}
|
||||
onEnter={handleEnter}
|
||||
onEscape={handleEscape}
|
||||
value={dateValue}
|
||||
clearable
|
||||
onChange={handleChange}
|
||||
onClear={handleClear}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
@ -11,15 +11,16 @@ export type DateTimeFieldInputProps = {
|
||||
onClickOutside?: FieldInputEvent;
|
||||
onEnter?: FieldInputEvent;
|
||||
onEscape?: FieldInputEvent;
|
||||
onClear?: FieldInputEvent;
|
||||
};
|
||||
|
||||
export const DateTimeFieldInput = ({
|
||||
onEnter,
|
||||
onEscape,
|
||||
onClickOutside,
|
||||
onClear,
|
||||
}: DateTimeFieldInputProps) => {
|
||||
const { fieldValue, hotkeyScope, clearable, setDraftValue } =
|
||||
useDateTimeField();
|
||||
const { fieldValue, setDraftValue } = useDateTimeField();
|
||||
|
||||
const persistField = usePersistField();
|
||||
|
||||
@ -52,18 +53,22 @@ export const DateTimeFieldInput = ({
|
||||
setDraftValue(newDate?.toDateString() ?? '');
|
||||
};
|
||||
|
||||
const handleClear = () => {
|
||||
onClear?.(() => persistDate(null));
|
||||
};
|
||||
|
||||
const dateValue = fieldValue ? new Date(fieldValue) : null;
|
||||
|
||||
return (
|
||||
<DateInput
|
||||
hotkeyScope={hotkeyScope}
|
||||
onClickOutside={handleClickOutside}
|
||||
onEnter={handleEnter}
|
||||
onEscape={handleEscape}
|
||||
value={dateValue}
|
||||
clearable={clearable}
|
||||
clearable
|
||||
onChange={handleChange}
|
||||
isDateTimeInput
|
||||
onClear={handleClear}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
@ -0,0 +1,13 @@
|
||||
import { formatToHumanReadableDateTime } from '~/utils';
|
||||
|
||||
import { EllipsisDisplay } from './EllipsisDisplay';
|
||||
|
||||
type DateTimeDisplayProps = {
|
||||
value: Date | string | null | undefined;
|
||||
};
|
||||
|
||||
export const DateTimeDisplay = ({ value }: DateTimeDisplayProps) => (
|
||||
<EllipsisDisplay>
|
||||
{value && formatToHumanReadableDateTime(value)}
|
||||
</EllipsisDisplay>
|
||||
);
|
@ -4,9 +4,15 @@ import styled from '@emotion/styled';
|
||||
import { flip, offset, useFloating } from '@floating-ui/react';
|
||||
import { Nullable } from 'twenty-ui';
|
||||
|
||||
import { useRegisterInputEvents } from '@/object-record/record-field/meta-types/input/hooks/useRegisterInputEvents';
|
||||
import { DateDisplay } from '@/ui/field/display/components/DateDisplay';
|
||||
import { InternalDatePicker } from '@/ui/input/components/internal/date/components/InternalDatePicker';
|
||||
import {
|
||||
MONTH_AND_YEAR_DROPDOWN_ID,
|
||||
MONTH_AND_YEAR_DROPDOWN_MONTH_SELECT_ID,
|
||||
MONTH_AND_YEAR_DROPDOWN_YEAR_SELECT_ID,
|
||||
} from '@/ui/input/components/internal/date/components/MonthAndYearDropdown';
|
||||
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
|
||||
import { useListenClickOutsideV2 } from '@/ui/utilities/pointer-event/hooks/useListenClickOutsideV2';
|
||||
|
||||
const StyledCalendarContainer = styled.div`
|
||||
background: ${({ theme }) => theme.background.secondary};
|
||||
@ -33,21 +39,21 @@ export type DateInputProps = {
|
||||
event: MouseEvent | TouchEvent,
|
||||
newDate: Nullable<Date>,
|
||||
) => void;
|
||||
hotkeyScope: string;
|
||||
clearable?: boolean;
|
||||
onChange?: (newDate: Nullable<Date>) => void;
|
||||
isDateTimeInput?: boolean;
|
||||
onClear?: () => void;
|
||||
};
|
||||
|
||||
export const DateInput = ({
|
||||
value,
|
||||
onEnter,
|
||||
onEscape,
|
||||
hotkeyScope,
|
||||
onClickOutside,
|
||||
clearable,
|
||||
onChange,
|
||||
isDateTimeInput,
|
||||
onClear,
|
||||
}: DateInputProps) => {
|
||||
const theme = useTheme();
|
||||
|
||||
@ -70,13 +76,30 @@ export const DateInput = ({
|
||||
onChange?.(newDate);
|
||||
};
|
||||
|
||||
useRegisterInputEvents({
|
||||
inputRef: wrapperRef,
|
||||
inputValue: internalValue,
|
||||
onEnter,
|
||||
onEscape,
|
||||
onClickOutside,
|
||||
hotkeyScope,
|
||||
const handleClear = () => {
|
||||
setInternalValue(null);
|
||||
onClear?.();
|
||||
};
|
||||
|
||||
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,
|
||||
);
|
||||
|
||||
useListenClickOutsideV2({
|
||||
refs: [wrapperRef],
|
||||
listenerId: 'DateInput',
|
||||
callback: (event) => {
|
||||
event.stopImmediatePropagation();
|
||||
|
||||
closeDropdownYearSelect();
|
||||
closeDropdownMonthSelect();
|
||||
closeDropdown();
|
||||
onClickOutside(event, internalValue);
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
@ -96,7 +119,9 @@ export const DateInput = ({
|
||||
}}
|
||||
clearable={clearable ? clearable : false}
|
||||
isDateTimeInput={isDateTimeInput}
|
||||
onClickOutside={onClickOutside}
|
||||
onEnter={onEnter}
|
||||
onEscape={onEscape}
|
||||
onClear={handleClear}
|
||||
/>
|
||||
</StyledCalendarContainer>
|
||||
</div>
|
||||
|
@ -16,6 +16,7 @@ export type LightIconButtonGroupProps = Pick<
|
||||
iconButtons: {
|
||||
Icon: IconComponent;
|
||||
onClick?: (event: MouseEvent<any>) => void;
|
||||
disabled?: boolean;
|
||||
}[];
|
||||
};
|
||||
|
||||
|
@ -1,10 +1,14 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useIMask } from 'react-imask';
|
||||
import styled from '@emotion/styled';
|
||||
import { DateTime } from 'luxon';
|
||||
|
||||
import { DATE_BLOCKS } from '@/ui/input/components/internal/date/constants/DateBlocks';
|
||||
import { DATE_MASK } from '@/ui/input/components/internal/date/constants/DateMask';
|
||||
import { DATE_TIME_BLOCKS } from '@/ui/input/components/internal/date/constants/DateTimeBlocks';
|
||||
import { DATE_TIME_MASK } from '@/ui/input/components/internal/date/constants/DateTimeMask';
|
||||
import { MAX_DATE } from '@/ui/input/components/internal/date/constants/MaxDate';
|
||||
import { MIN_DATE } from '@/ui/input/components/internal/date/constants/MinDate';
|
||||
|
||||
const StyledInputContainer = styled.div`
|
||||
width: 100%;
|
||||
@ -37,20 +41,27 @@ export const DateTimeInput = ({
|
||||
onChange,
|
||||
isDateTimeInput,
|
||||
}: DateTimeInputProps) => {
|
||||
const parsingFormat = isDateTimeInput ? 'MM/dd/yyyy HH:mm' : 'MM/dd/yyyy';
|
||||
|
||||
const [hasError, setHasError] = useState(false);
|
||||
|
||||
const parseDateToString = (date: any) => {
|
||||
const parseDateToString = useCallback(
|
||||
(date: any) => {
|
||||
const dateParsed = DateTime.fromJSDate(date);
|
||||
|
||||
const formattedDate = dateParsed.toFormat('MM/dd/yyyy HH:mm');
|
||||
const formattedDate = dateParsed.toFormat(parsingFormat);
|
||||
|
||||
return formattedDate;
|
||||
};
|
||||
},
|
||||
[parsingFormat],
|
||||
);
|
||||
|
||||
const parseStringToDate = (str: string) => {
|
||||
setHasError(false);
|
||||
|
||||
const parsedDate = DateTime.fromFormat(str, 'MM/dd/yyyy HH:mm');
|
||||
const parsedDate = isDateTimeInput
|
||||
? DateTime.fromFormat(str, parsingFormat)
|
||||
: DateTime.fromFormat(str, parsingFormat, { zone: 'utc' });
|
||||
|
||||
const isValid = parsedDate.isValid;
|
||||
|
||||
@ -65,13 +76,16 @@ export const DateTimeInput = ({
|
||||
return jsDate;
|
||||
};
|
||||
|
||||
const pattern = isDateTimeInput ? DATE_TIME_MASK : DATE_MASK;
|
||||
const blocks = isDateTimeInput ? DATE_TIME_BLOCKS : DATE_BLOCKS;
|
||||
|
||||
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),
|
||||
pattern,
|
||||
blocks,
|
||||
min: MIN_DATE,
|
||||
max: MAX_DATE,
|
||||
format: parseDateToString,
|
||||
parse: parseStringToDate,
|
||||
lazy: false,
|
||||
@ -91,7 +105,7 @@ export const DateTimeInput = ({
|
||||
|
||||
useEffect(() => {
|
||||
setValue(parseDateToString(date));
|
||||
}, [date, setValue]);
|
||||
}, [date, setValue, parseDateToString]);
|
||||
|
||||
return (
|
||||
<StyledInputContainer>
|
||||
@ -102,6 +116,7 @@ export const DateTimeInput = ({
|
||||
isDateTimeInput ? ' and time' : ' (mm/dd/yyyy)'
|
||||
}`}
|
||||
value={value}
|
||||
onChange={() => {}} // Prevent React warning
|
||||
hasError={hasError}
|
||||
/>
|
||||
</StyledInputContainer>
|
||||
|
@ -1,6 +1,7 @@
|
||||
import ReactDatePicker from 'react-datepicker';
|
||||
import styled from '@emotion/styled';
|
||||
import { DateTime } from 'luxon';
|
||||
import { Key } from 'ts-key-enum';
|
||||
import { IconCalendarX, IconChevronLeft, IconChevronRight } from 'twenty-ui';
|
||||
|
||||
import { LightIconButton } from '@/ui/input/button/components/LightIconButton';
|
||||
@ -16,6 +17,7 @@ import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
|
||||
import { MenuItemLeftContent } from '@/ui/navigation/menu-item/internals/components/MenuItemLeftContent';
|
||||
import { StyledHoverableMenuItemBase } from '@/ui/navigation/menu-item/internals/components/StyledMenuItemBase';
|
||||
import { OVERLAY_BACKGROUND } from '@/ui/theme/constants/OverlayBackground';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
import 'react-datepicker/dist/react-datepicker.css';
|
||||
|
||||
@ -37,6 +39,10 @@ const StyledContainer = styled.div`
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
& .react-datepicker__triangle {
|
||||
display: none;
|
||||
}
|
||||
|
||||
& .react-datepicker__triangle::after {
|
||||
display: none;
|
||||
}
|
||||
@ -259,7 +265,7 @@ const StyledButton = styled(MenuItemLeftContent)`
|
||||
const StyledCustomDatePickerHeader = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
justify-content: flex-end;
|
||||
padding-left: ${({ theme }) => theme.spacing(2)};
|
||||
padding-right: ${({ theme }) => theme.spacing(2)};
|
||||
padding-top: ${({ theme }) => theme.spacing(2)};
|
||||
@ -267,29 +273,40 @@ const StyledCustomDatePickerHeader = styled.div`
|
||||
gap: ${({ theme }) => theme.spacing(1)};
|
||||
`;
|
||||
|
||||
const StyledMonthText = styled.div`
|
||||
color: ${({ theme }) => theme.font.color.secondary};
|
||||
font-family: ${({ theme }) => theme.font.family};
|
||||
font-size: ${({ theme }) => theme.font.size.md};
|
||||
padding: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
export type InternalDatePickerProps = {
|
||||
date: Date;
|
||||
onMouseSelect?: (date: Date | null) => void;
|
||||
onChange?: (date: Date | null) => void;
|
||||
clearable?: boolean;
|
||||
isDateTimeInput?: boolean;
|
||||
onClickOutside?: (event: MouseEvent | TouchEvent, date: Date | null) => void;
|
||||
onEnter?: (date: Date | null) => void;
|
||||
onEscape?: (date: Date | null) => void;
|
||||
keyboardEventsDisabled?: boolean;
|
||||
onClear?: () => void;
|
||||
};
|
||||
|
||||
const PICKER_DATE_FORMAT = 'MM/dd/yyyy';
|
||||
|
||||
export const InternalDatePicker = ({
|
||||
date,
|
||||
onChange,
|
||||
onMouseSelect,
|
||||
onEnter,
|
||||
onEscape,
|
||||
clearable = true,
|
||||
isDateTimeInput,
|
||||
onClickOutside,
|
||||
keyboardEventsDisabled,
|
||||
onClear,
|
||||
}: InternalDatePickerProps) => {
|
||||
const internalDate = date ?? new Date();
|
||||
|
||||
const dateFormatted =
|
||||
DateTime.fromJSDate(internalDate).toFormat(PICKER_DATE_FORMAT);
|
||||
const monthLabel = DateTime.fromJSDate(internalDate).toFormat('LLLL');
|
||||
const yearLabel = DateTime.fromJSDate(internalDate).toFormat('yyyy');
|
||||
|
||||
const { closeDropdown } = useDropdown(MONTH_AND_YEAR_DROPDOWN_ID);
|
||||
const { closeDropdown: closeDropdownMonthSelect } = useDropdown(
|
||||
@ -301,7 +318,7 @@ export const InternalDatePicker = ({
|
||||
|
||||
const handleClear = () => {
|
||||
closeDropdowns();
|
||||
onMouseSelect?.(null);
|
||||
onClear?.();
|
||||
};
|
||||
|
||||
const closeDropdowns = () => {
|
||||
@ -310,28 +327,59 @@ export const InternalDatePicker = ({
|
||||
closeDropdown();
|
||||
};
|
||||
|
||||
const handleClickOutside = (event: any) => {
|
||||
closeDropdowns();
|
||||
onClickOutside?.(event, internalDate);
|
||||
};
|
||||
|
||||
const handleMouseSelect = (newDate: Date) => {
|
||||
closeDropdowns();
|
||||
onMouseSelect?.(newDate);
|
||||
};
|
||||
|
||||
// TODO: implement keyboard events here
|
||||
const handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {
|
||||
if (isDefined(keyboardEventsDisabled) && keyboardEventsDisabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (event.key) {
|
||||
case Key.Enter: {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
|
||||
closeDropdowns();
|
||||
onEnter?.(internalDate);
|
||||
break;
|
||||
}
|
||||
case Key.Escape: {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
|
||||
closeDropdowns();
|
||||
onEscape?.(internalDate);
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledContainer>
|
||||
<StyledContainer onKeyDown={handleKeyDown}>
|
||||
<div className={clearable ? 'clearable ' : ''}>
|
||||
<ReactDatePicker
|
||||
open={true}
|
||||
selected={internalDate}
|
||||
value={dateFormatted}
|
||||
openToDate={internalDate}
|
||||
onChange={(newDate) => {
|
||||
onChange?.(newDate);
|
||||
}}
|
||||
customInput={
|
||||
<DateTimeInput
|
||||
date={internalDate}
|
||||
isDateTimeInput={isDateTimeInput}
|
||||
onChange={onChange}
|
||||
/>
|
||||
}
|
||||
onMonthChange={(newDate) => {
|
||||
onChange?.(newDate);
|
||||
}}
|
||||
onYearChange={(newDate) => {
|
||||
onChange?.(newDate);
|
||||
}}
|
||||
renderCustomHeader={({
|
||||
decreaseMonth,
|
||||
increaseMonth,
|
||||
@ -345,7 +393,9 @@ export const InternalDatePicker = ({
|
||||
onChange={onChange}
|
||||
/>
|
||||
<StyledCustomDatePickerHeader>
|
||||
{isDateTimeInput && (
|
||||
<TimeInput date={internalDate} onChange={onChange} />
|
||||
)}
|
||||
<MonthAndYearDropdown date={internalDate} onChange={onChange} />
|
||||
<LightIconButton
|
||||
Icon={IconChevronLeft}
|
||||
@ -360,9 +410,11 @@ export const InternalDatePicker = ({
|
||||
disabled={nextMonthButtonDisabled}
|
||||
/>
|
||||
</StyledCustomDatePickerHeader>
|
||||
<StyledMonthText>
|
||||
{monthLabel} - {yearLabel}
|
||||
</StyledMonthText>
|
||||
</>
|
||||
)}
|
||||
customInput={<></>}
|
||||
onSelect={(date: Date, event) => {
|
||||
const dateUTC = DateTime.fromJSDate(date, {
|
||||
zone: 'utc',
|
||||
@ -374,8 +426,7 @@ export const InternalDatePicker = ({
|
||||
onChange?.(dateUTC);
|
||||
}
|
||||
}}
|
||||
onClickOutside={handleClickOutside}
|
||||
></ReactDatePicker>
|
||||
/>
|
||||
</div>
|
||||
{clearable && (
|
||||
<StyledButtonContainer onClick={handleClear} isMenuOpen={false}>
|
||||
|
@ -1,6 +1,5 @@
|
||||
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';
|
||||
@ -57,7 +56,7 @@ export const MonthAndYearDropdown = ({
|
||||
<Dropdown
|
||||
dropdownId={MONTH_AND_YEAR_DROPDOWN_ID}
|
||||
dropdownHotkeyScope={{
|
||||
scope: TableHotkeyScope.CellEditMode,
|
||||
scope: MONTH_AND_YEAR_DROPDOWN_ID,
|
||||
}}
|
||||
dropdownPlacement="bottom-start"
|
||||
clickableComponent={
|
||||
|
@ -0,0 +1,22 @@
|
||||
import { IMask } from 'react-imask';
|
||||
|
||||
import { MAX_DATE } from '@/ui/input/components/internal/date/constants/MaxDate';
|
||||
import { MIN_DATE } from '@/ui/input/components/internal/date/constants/MinDate';
|
||||
|
||||
export const DATE_BLOCKS = {
|
||||
YYYY: {
|
||||
mask: IMask.MaskedRange,
|
||||
from: MIN_DATE.getFullYear(),
|
||||
to: MAX_DATE.getFullYear(),
|
||||
},
|
||||
MM: {
|
||||
mask: IMask.MaskedRange,
|
||||
from: 1,
|
||||
to: 12,
|
||||
},
|
||||
DD: {
|
||||
mask: IMask.MaskedRange,
|
||||
from: 1,
|
||||
to: 31,
|
||||
},
|
||||
};
|
@ -0,0 +1 @@
|
||||
export const DATE_MASK = 'm`/d`/Y`'; // See https://imask.js.org/guide.html#masked-date
|
@ -1,22 +1,7 @@
|
||||
import { IMask } from 'react-imask';
|
||||
|
||||
import { DATE_BLOCKS } from '@/ui/input/components/internal/date/constants/DateBlocks';
|
||||
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,
|
||||
},
|
||||
...DATE_BLOCKS,
|
||||
...TIME_BLOCKS,
|
||||
};
|
||||
|
@ -1,3 +1,3 @@
|
||||
import { TIME_MASK } from '@/ui/input/components/internal/date/constants/TimeMask';
|
||||
|
||||
export const DATE_TIME_MASK = `MM/DD/YYYY ${TIME_MASK}`;
|
||||
export const DATE_TIME_MASK = `m\`/d\`/Y\` ${TIME_MASK}`;
|
||||
|
@ -0,0 +1 @@
|
||||
export const MAX_DATE = new Date(2100, 11, 31);
|
@ -0,0 +1 @@
|
||||
export const MIN_DATE = new Date(1900, 0, 1);
|
@ -9,6 +9,6 @@ export const TIME_BLOCKS = {
|
||||
mm: {
|
||||
mask: IMask.MaskedRange, // Use MaskedRange for valid minute range (0-59)
|
||||
from: 0,
|
||||
to: 61,
|
||||
to: 59,
|
||||
},
|
||||
};
|
||||
|
@ -1 +1 @@
|
||||
export const TIME_MASK = 'HH:mm'; // Define blocks for hours and minutes
|
||||
export const TIME_MASK = 'HH`:mm`'; // Define blocks for hours and minutes
|
||||
|
@ -10,6 +10,18 @@ export const formatToHumanReadableDate = (date: Date | string) => {
|
||||
}).format(parsedJSDate);
|
||||
};
|
||||
|
||||
export const formatToHumanReadableDateTime = (date: Date | string) => {
|
||||
const parsedJSDate = parseDate(date).toJSDate();
|
||||
|
||||
return new Intl.DateTimeFormat(undefined, {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
hour: 'numeric',
|
||||
minute: 'numeric',
|
||||
}).format(parsedJSDate);
|
||||
};
|
||||
|
||||
export const sanitizeURL = (link: string | null | undefined) => {
|
||||
return link
|
||||
? link.replace(/(https?:\/\/)|(www\.)/g, '').replace(/\/$/, '')
|
||||
|
81
yarn.lock
81
yarn.lock
@ -5533,6 +5533,20 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@floating-ui/react@npm:^0.26.2":
|
||||
version: 0.26.12
|
||||
resolution: "@floating-ui/react@npm:0.26.12"
|
||||
dependencies:
|
||||
"@floating-ui/react-dom": "npm:^2.0.0"
|
||||
"@floating-ui/utils": "npm:^0.2.0"
|
||||
tabbable: "npm:^6.0.0"
|
||||
peerDependencies:
|
||||
react: ">=16.8.0"
|
||||
react-dom: ">=16.8.0"
|
||||
checksum: f47e133b9fe5dd404886c4e69e760dc0d6ab30774de82b5547cba83d0cd6e7a3bb4c221587e115a3c0c466ac344578c3db78b5808b5acd70ae5d43d587de9fd1
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@floating-ui/react@npm:^0.26.4":
|
||||
version: 0.26.8
|
||||
resolution: "@floating-ui/react@npm:0.26.8"
|
||||
@ -5554,7 +5568,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@floating-ui/utils@npm:^0.2.1":
|
||||
"@floating-ui/utils@npm:^0.2.0, @floating-ui/utils@npm:^0.2.1":
|
||||
version: 0.2.1
|
||||
resolution: "@floating-ui/utils@npm:0.2.1"
|
||||
checksum: ee77756712cf5b000c6bacf11992ffb364f3ea2d0d51cc45197a7e646a17aeb86ea4b192c0b42f3fbb29487aee918a565e84f710b8c3645827767f406a6b4cc9
|
||||
@ -9791,7 +9805,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@popperjs/core@npm:^2.11.8, @popperjs/core@npm:^2.9.0, @popperjs/core@npm:^2.9.2":
|
||||
"@popperjs/core@npm:^2.9.0":
|
||||
version: 2.11.8
|
||||
resolution: "@popperjs/core@npm:2.11.8"
|
||||
checksum: 4681e682abc006d25eb380d0cf3efc7557043f53b6aea7a5057d0d1e7df849a00e281cd8ea79c902a35a414d7919621fc2ba293ecec05f413598e0b23d5a1e63
|
||||
@ -16975,15 +16989,14 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/react-datepicker@npm:^4.11.2":
|
||||
version: 4.19.5
|
||||
resolution: "@types/react-datepicker@npm:4.19.5"
|
||||
"@types/react-datepicker@npm:^6.2.0":
|
||||
version: 6.2.0
|
||||
resolution: "@types/react-datepicker@npm:6.2.0"
|
||||
dependencies:
|
||||
"@popperjs/core": "npm:^2.9.2"
|
||||
"@floating-ui/react": "npm:^0.26.2"
|
||||
"@types/react": "npm:*"
|
||||
date-fns: "npm:^2.0.1"
|
||||
react-popper: "npm:^2.2.5"
|
||||
checksum: 8b3402338e842a2d5c4d4c0ceefc9fc280607662d9073db860b59e31a3b3977eec6ec3d36b388ec5a3a3874adec00ab0f6a9405cd0de1f8dcf75de3328a84eb8
|
||||
date-fns: "npm:^3.3.1"
|
||||
checksum: 40342f0a5e15bac6f2b35072a2f7041db20b924692123aface78f2cbf0e896c0c02c299a9173583e01e5b6dad38e3da86ad0e56e943e74e0c0dc7d5c60129bdf
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@ -22376,7 +22389,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"clsx@npm:^2.0.0":
|
||||
"clsx@npm:^2.0.0, clsx@npm:^2.1.0":
|
||||
version: 2.1.0
|
||||
resolution: "clsx@npm:2.1.0"
|
||||
checksum: c09c00ad14f638366ca814097e6cab533dfa1972a358da5b557be487168acbb25b4c1395e89ffa842a8a61ba87a462d2b4885bc9d4f8410b598f3cb339599cdb
|
||||
@ -23986,7 +23999,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"date-fns@npm:^2.0.1, date-fns@npm:^2.30.0":
|
||||
"date-fns@npm:^2.30.0":
|
||||
version: 2.30.0
|
||||
resolution: "date-fns@npm:2.30.0"
|
||||
dependencies:
|
||||
@ -23995,6 +24008,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"date-fns@npm:^3.3.1":
|
||||
version: 3.6.0
|
||||
resolution: "date-fns@npm:3.6.0"
|
||||
checksum: 0b5fb981590ef2f8e5a3ba6cd6d77faece0ea7f7158948f2eaae7bbb7c80a8f63ae30b01236c2923cf89bb3719c33aeb150c715ea4fe4e86e37dcf06bed42fb6
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"dateformat@npm:^4.5.0":
|
||||
version: 4.6.3
|
||||
resolution: "dateformat@npm:4.6.3"
|
||||
@ -40815,20 +40835,19 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"react-datepicker@npm:^4.11.0":
|
||||
version: 4.25.0
|
||||
resolution: "react-datepicker@npm:4.25.0"
|
||||
"react-datepicker@npm:^6.7.1":
|
||||
version: 6.7.1
|
||||
resolution: "react-datepicker@npm:6.7.1"
|
||||
dependencies:
|
||||
"@popperjs/core": "npm:^2.11.8"
|
||||
classnames: "npm:^2.2.6"
|
||||
date-fns: "npm:^2.30.0"
|
||||
"@floating-ui/react": "npm:^0.26.2"
|
||||
clsx: "npm:^2.1.0"
|
||||
date-fns: "npm:^3.3.1"
|
||||
prop-types: "npm:^15.7.2"
|
||||
react-onclickoutside: "npm:^6.13.0"
|
||||
react-popper: "npm:^2.3.0"
|
||||
peerDependencies:
|
||||
react: ^16.9.0 || ^17 || ^18
|
||||
react-dom: ^16.9.0 || ^17 || ^18
|
||||
checksum: 714fc6cffdf9fa76d1a74581375acb4c86ac81e6c2cde372448ab447370b7e9ffb1840cf388020485028e7b0f43edf82c1cfd2dab7d2c6b964cdd6f4dd560e92
|
||||
checksum: 9029c358254ef011e3a6b82fd542a87efa30a67b7aa2814178d28354e2b18f69b863861fb852f236a91be57a0b684455c9127c87232b7ed2f4fae6737e3248e6
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@ -40957,7 +40976,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"react-fast-compare@npm:3.2.2, react-fast-compare@npm:^3.0.1, react-fast-compare@npm:^3.2.0, react-fast-compare@npm:^3.2.2":
|
||||
"react-fast-compare@npm:3.2.2, react-fast-compare@npm:^3.2.0, react-fast-compare@npm:^3.2.2":
|
||||
version: 3.2.2
|
||||
resolution: "react-fast-compare@npm:3.2.2"
|
||||
checksum: 0bbd2f3eb41ab2ff7380daaa55105db698d965c396df73e6874831dbafec8c4b5b08ba36ff09df01526caa3c61595247e3269558c284e37646241cba2b90a367
|
||||
@ -41157,20 +41176,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"react-popper@npm:^2.2.5, react-popper@npm:^2.3.0":
|
||||
version: 2.3.0
|
||||
resolution: "react-popper@npm:2.3.0"
|
||||
dependencies:
|
||||
react-fast-compare: "npm:^3.0.1"
|
||||
warning: "npm:^4.0.2"
|
||||
peerDependencies:
|
||||
"@popperjs/core": ^2.0.0
|
||||
react: ^16.8.0 || ^17 || ^18
|
||||
react-dom: ^16.8.0 || ^17 || ^18
|
||||
checksum: 23f93540537ca4c035425bb8d5e51b11131fbc921d7ac1d041d0ae557feac8c877f3a012d36b94df8787803f52ed81e6df9257ac9e58719875f7805518d6db3f
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"react-query@npm:^3.34.19":
|
||||
version: 3.39.3
|
||||
resolution: "react-query@npm:3.39.3"
|
||||
@ -45046,7 +45051,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"tabbable@npm:^6.0.1, tabbable@npm:^6.2.0":
|
||||
"tabbable@npm:^6.0.0, tabbable@npm:^6.0.1, tabbable@npm:^6.2.0":
|
||||
version: 6.2.0
|
||||
resolution: "tabbable@npm:6.2.0"
|
||||
checksum: ced8b38f05f2de62cd46836d77c2646c42b8c9713f5bd265daf0e78ff5ac73d3ba48a7ca45f348bafeef29b23da7187c72250742d37627883ef89cbd7fa76898
|
||||
@ -46222,7 +46227,7 @@ __metadata:
|
||||
"@types/passport-google-oauth20": "npm:^2.0.11"
|
||||
"@types/passport-jwt": "npm:^3.0.8"
|
||||
"@types/react": "npm:^18.2.39"
|
||||
"@types/react-datepicker": "npm:^4.11.2"
|
||||
"@types/react-datepicker": "npm:^6.2.0"
|
||||
"@types/react-dom": "npm:^18.2.15"
|
||||
"@types/scroll-into-view": "npm:^1.16.0"
|
||||
"@types/supertest": "npm:^2.0.11"
|
||||
@ -46346,7 +46351,7 @@ __metadata:
|
||||
raw-loader: "npm:^4.0.2"
|
||||
react: "npm:^18.2.0"
|
||||
react-data-grid: "npm:7.0.0-beta.13"
|
||||
react-datepicker: "npm:^4.11.0"
|
||||
react-datepicker: "npm:^6.7.1"
|
||||
react-dom: "npm:^18.2.0"
|
||||
react-dropzone: "npm:^14.2.3"
|
||||
react-error-boundary: "npm:^4.0.11"
|
||||
@ -48240,7 +48245,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"warning@npm:^4.0.2, warning@npm:^4.0.3":
|
||||
"warning@npm:^4.0.3":
|
||||
version: 4.0.3
|
||||
resolution: "warning@npm:4.0.3"
|
||||
dependencies:
|
||||
|
Loading…
Reference in New Issue
Block a user