Solves issue #5917. This PR is now ready for the first review! Filters do not fully work yet, there's a problem applying multiple filters like the following: ``` { and: [ { [correspondingField.name]: { gte: start.toISOString(), } as DateFilter, }, { [correspondingField.name]: { lte: end.toISOString(), } as DateFilter, }, ], } ``` I'll do my best to dig into it tonight! --------- Co-authored-by: Félix Malfait <felix@twenty.com>
This commit is contained in:
@ -0,0 +1,108 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { DateTime } from 'luxon';
|
||||
import { IconChevronLeft, IconChevronRight } from 'twenty-ui';
|
||||
|
||||
import { LightIconButton } from '@/ui/input/button/components/LightIconButton';
|
||||
import { Select } from '@/ui/input/components/Select';
|
||||
import { DateTimeInput } from '@/ui/input/components/internal/date/components/DateTimeInput';
|
||||
|
||||
import { getMonthSelectOptions } from '@/ui/input/components/internal/date/utils/getMonthSelectOptions';
|
||||
import {
|
||||
MONTH_AND_YEAR_DROPDOWN_MONTH_SELECT_ID,
|
||||
MONTH_AND_YEAR_DROPDOWN_YEAR_SELECT_ID,
|
||||
} from './InternalDatePicker';
|
||||
|
||||
const StyledCustomDatePickerHeader = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding-left: ${({ theme }) => theme.spacing(2)};
|
||||
padding-right: ${({ theme }) => theme.spacing(2)};
|
||||
padding-top: ${({ theme }) => theme.spacing(2)};
|
||||
|
||||
gap: ${({ theme }) => theme.spacing(1)};
|
||||
`;
|
||||
|
||||
const years = Array.from(
|
||||
{ length: 200 },
|
||||
(_, i) => new Date().getFullYear() + 5 - i,
|
||||
).map((year) => ({ label: year.toString(), value: year }));
|
||||
|
||||
type AbsoluteDatePickerHeaderProps = {
|
||||
date: Date;
|
||||
onChange?: (date: Date | null) => void;
|
||||
onChangeMonth: (month: number) => void;
|
||||
onChangeYear: (year: number) => void;
|
||||
onAddMonth: () => void;
|
||||
onSubtractMonth: () => void;
|
||||
prevMonthButtonDisabled: boolean;
|
||||
nextMonthButtonDisabled: boolean;
|
||||
isDateTimeInput?: boolean;
|
||||
timeZone: string;
|
||||
};
|
||||
|
||||
export const AbsoluteDatePickerHeader = ({
|
||||
date,
|
||||
onChange,
|
||||
onChangeMonth,
|
||||
onChangeYear,
|
||||
onAddMonth,
|
||||
onSubtractMonth,
|
||||
prevMonthButtonDisabled,
|
||||
nextMonthButtonDisabled,
|
||||
isDateTimeInput,
|
||||
timeZone,
|
||||
}: AbsoluteDatePickerHeaderProps) => {
|
||||
const endOfDayDateTimeInLocalTimezone = DateTime.now().set({
|
||||
day: date.getDate(),
|
||||
month: date.getMonth() + 1,
|
||||
year: date.getFullYear(),
|
||||
hour: 23,
|
||||
minute: 59,
|
||||
second: 59,
|
||||
millisecond: 999,
|
||||
});
|
||||
|
||||
const endOfDayInLocalTimezone = endOfDayDateTimeInLocalTimezone.toJSDate();
|
||||
|
||||
return (
|
||||
<>
|
||||
<DateTimeInput
|
||||
date={date}
|
||||
isDateTimeInput={isDateTimeInput}
|
||||
onChange={onChange}
|
||||
userTimezone={timeZone}
|
||||
/>
|
||||
<StyledCustomDatePickerHeader>
|
||||
<Select
|
||||
dropdownId={MONTH_AND_YEAR_DROPDOWN_MONTH_SELECT_ID}
|
||||
options={getMonthSelectOptions()}
|
||||
disableBlur
|
||||
onChange={onChangeMonth}
|
||||
value={endOfDayInLocalTimezone.getMonth()}
|
||||
fullWidth
|
||||
/>
|
||||
<Select
|
||||
dropdownId={MONTH_AND_YEAR_DROPDOWN_YEAR_SELECT_ID}
|
||||
onChange={onChangeYear}
|
||||
value={endOfDayInLocalTimezone.getFullYear()}
|
||||
options={years}
|
||||
disableBlur
|
||||
fullWidth
|
||||
/>
|
||||
<LightIconButton
|
||||
Icon={IconChevronLeft}
|
||||
onClick={onSubtractMonth}
|
||||
size="medium"
|
||||
disabled={prevMonthButtonDisabled}
|
||||
/>
|
||||
<LightIconButton
|
||||
Icon={IconChevronRight}
|
||||
onClick={onAddMonth}
|
||||
size="medium"
|
||||
disabled={nextMonthButtonDisabled}
|
||||
/>
|
||||
</StyledCustomDatePickerHeader>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -2,52 +2,32 @@ import styled from '@emotion/styled';
|
||||
import { DateTime } from 'luxon';
|
||||
import ReactDatePicker from 'react-datepicker';
|
||||
import { Key } from 'ts-key-enum';
|
||||
import {
|
||||
IconCalendarX,
|
||||
IconChevronLeft,
|
||||
IconChevronRight,
|
||||
OVERLAY_BACKGROUND,
|
||||
} from 'twenty-ui';
|
||||
import { IconCalendarX, OVERLAY_BACKGROUND } from 'twenty-ui';
|
||||
|
||||
import { LightIconButton } from '@/ui/input/button/components/LightIconButton';
|
||||
import { DateTimeInput } from '@/ui/input/components/internal/date/components/DateTimeInput';
|
||||
import { Select } from '@/ui/input/components/Select';
|
||||
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 { isDefined } from '~/utils/isDefined';
|
||||
|
||||
import { AbsoluteDatePickerHeader } from '@/ui/input/components/internal/date/components/AbsoluteDatePickerHeader';
|
||||
import { RelativeDatePickerHeader } from '@/ui/input/components/internal/date/components/RelativeDatePickerHeader';
|
||||
import { getHighlightedDates } from '@/ui/input/components/internal/date/utils/getHighlightedDates';
|
||||
import { UserContext } from '@/users/contexts/UserContext';
|
||||
import {
|
||||
VariableDateViewFilterValueDirection,
|
||||
VariableDateViewFilterValueUnit,
|
||||
} from '@/views/utils/view-filter-value/resolveDateViewFilterValue';
|
||||
import { useContext } from 'react';
|
||||
import 'react-datepicker/dist/react-datepicker.css';
|
||||
|
||||
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';
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
const StyledContainer = styled.div<{ calendarDisabled?: boolean }>`
|
||||
& .react-datepicker {
|
||||
border-color: ${({ theme }) => theme.border.color.light};
|
||||
background: transparent;
|
||||
@ -207,6 +187,10 @@ const StyledContainer = styled.div`
|
||||
|
||||
& .react-datepicker__month {
|
||||
margin-top: 0;
|
||||
|
||||
pointer-events: ${({ calendarDisabled }) =>
|
||||
calendarDisabled ? 'none' : 'auto'};
|
||||
opacity: ${({ calendarDisabled }) => (calendarDisabled ? '0.5' : '1')};
|
||||
}
|
||||
|
||||
& .react-datepicker__day {
|
||||
@ -288,21 +272,27 @@ const StyledButton = styled(MenuItemLeftContent)`
|
||||
justify-content: start;
|
||||
`;
|
||||
|
||||
const StyledCustomDatePickerHeader = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding-left: ${({ theme }) => theme.spacing(2)};
|
||||
padding-right: ${({ theme }) => theme.spacing(2)};
|
||||
padding-top: ${({ theme }) => theme.spacing(2)};
|
||||
|
||||
gap: ${({ theme }) => theme.spacing(1)};
|
||||
`;
|
||||
|
||||
type InternalDatePickerProps = {
|
||||
isRelative?: boolean;
|
||||
date: Date | null;
|
||||
relativeDate?: {
|
||||
direction: VariableDateViewFilterValueDirection;
|
||||
amount?: number;
|
||||
unit: VariableDateViewFilterValueUnit;
|
||||
};
|
||||
highlightedDateRange?: {
|
||||
start: Date;
|
||||
end: Date;
|
||||
};
|
||||
onMouseSelect?: (date: Date | null) => void;
|
||||
onChange?: (date: Date | null) => void;
|
||||
onRelativeDateChange?: (
|
||||
relativeDate: {
|
||||
direction: VariableDateViewFilterValueDirection;
|
||||
amount?: number;
|
||||
unit: VariableDateViewFilterValueUnit;
|
||||
} | null,
|
||||
) => void;
|
||||
clearable?: boolean;
|
||||
isDateTimeInput?: boolean;
|
||||
onEnter?: (date: Date | null) => void;
|
||||
@ -321,6 +311,10 @@ export const InternalDatePicker = ({
|
||||
isDateTimeInput,
|
||||
keyboardEventsDisabled,
|
||||
onClear,
|
||||
isRelative,
|
||||
relativeDate,
|
||||
onRelativeDateChange,
|
||||
highlightedDateRange,
|
||||
}: InternalDatePickerProps) => {
|
||||
const internalDate = date ?? new Date();
|
||||
|
||||
@ -469,15 +463,20 @@ export const InternalDatePicker = ({
|
||||
|
||||
const dateToUse = isDateTimeInput ? endOfDayInLocalTimezone : dateWithoutTime;
|
||||
|
||||
const highlightedDates = getHighlightedDates(highlightedDateRange);
|
||||
|
||||
const selectedDates = isRelative ? highlightedDates : [dateToUse];
|
||||
|
||||
return (
|
||||
<StyledContainer onKeyDown={handleKeyDown}>
|
||||
<StyledContainer onKeyDown={handleKeyDown} calendarDisabled={isRelative}>
|
||||
<div className={clearable ? 'clearable ' : ''}>
|
||||
<ReactDatePicker
|
||||
open={true}
|
||||
selected={dateToUse}
|
||||
selectedDates={selectedDates}
|
||||
openToDate={isDefined(dateToUse) ? dateToUse : undefined}
|
||||
disabledKeyboardNavigation
|
||||
onChange={handleDateChange}
|
||||
onChange={handleDateChange as any}
|
||||
customInput={
|
||||
<DateTimeInput
|
||||
date={internalDate}
|
||||
@ -489,47 +488,31 @@ export const InternalDatePicker = ({
|
||||
renderCustomHeader={({
|
||||
prevMonthButtonDisabled,
|
||||
nextMonthButtonDisabled,
|
||||
}) => (
|
||||
<>
|
||||
<DateTimeInput
|
||||
date={internalDate}
|
||||
isDateTimeInput={isDateTimeInput}
|
||||
onChange={onChange}
|
||||
userTimezone={timeZone}
|
||||
}) =>
|
||||
isRelative ? (
|
||||
<RelativeDatePickerHeader
|
||||
direction={relativeDate?.direction ?? 'PAST'}
|
||||
amount={relativeDate?.amount}
|
||||
unit={relativeDate?.unit ?? 'DAY'}
|
||||
onChange={onRelativeDateChange}
|
||||
/>
|
||||
<StyledCustomDatePickerHeader>
|
||||
<Select
|
||||
dropdownId={MONTH_AND_YEAR_DROPDOWN_MONTH_SELECT_ID}
|
||||
options={months}
|
||||
disableBlur
|
||||
onChange={handleChangeMonth}
|
||||
value={endOfDayInLocalTimezone.getMonth()}
|
||||
fullWidth
|
||||
/>
|
||||
<Select
|
||||
dropdownId={MONTH_AND_YEAR_DROPDOWN_YEAR_SELECT_ID}
|
||||
onChange={handleChangeYear}
|
||||
value={endOfDayInLocalTimezone.getFullYear()}
|
||||
options={years}
|
||||
disableBlur
|
||||
fullWidth
|
||||
/>
|
||||
<LightIconButton
|
||||
Icon={IconChevronLeft}
|
||||
onClick={handleSubtractMonth}
|
||||
size="medium"
|
||||
disabled={prevMonthButtonDisabled}
|
||||
/>
|
||||
<LightIconButton
|
||||
Icon={IconChevronRight}
|
||||
onClick={handleAddMonth}
|
||||
size="medium"
|
||||
disabled={nextMonthButtonDisabled}
|
||||
/>
|
||||
</StyledCustomDatePickerHeader>
|
||||
</>
|
||||
)}
|
||||
) : (
|
||||
<AbsoluteDatePickerHeader
|
||||
date={internalDate}
|
||||
onChange={onChange}
|
||||
onChangeMonth={handleChangeMonth}
|
||||
onChangeYear={handleChangeYear}
|
||||
onAddMonth={handleAddMonth}
|
||||
onSubtractMonth={handleSubtractMonth}
|
||||
prevMonthButtonDisabled={prevMonthButtonDisabled}
|
||||
nextMonthButtonDisabled={nextMonthButtonDisabled}
|
||||
isDateTimeInput={isDateTimeInput}
|
||||
timeZone={timeZone}
|
||||
/>
|
||||
)
|
||||
}
|
||||
onSelect={handleDateSelect}
|
||||
selectsMultiple={isRelative}
|
||||
/>
|
||||
</div>
|
||||
{clearable && (
|
||||
|
||||
@ -0,0 +1,113 @@
|
||||
import { RELATIVE_DATE_DIRECTION_SELECT_OPTIONS } from '@/ui/input/components/internal/date/constants/RelativeDateDirectionSelectOptions';
|
||||
import { RELATIVE_DATE_UNITS_SELECT_OPTIONS } from '@/ui/input/components/internal/date/constants/RelativeDateUnitSelectOptions';
|
||||
import { Select } from '@/ui/input/components/Select';
|
||||
import { TextInput } from '@/ui/input/components/TextInput';
|
||||
import {
|
||||
VariableDateViewFilterValueDirection,
|
||||
variableDateViewFilterValuePartsSchema,
|
||||
VariableDateViewFilterValueUnit,
|
||||
} from '@/views/utils/view-filter-value/resolveDateViewFilterValue';
|
||||
import styled from '@emotion/styled';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: ${({ theme }) => theme.spacing(1)};
|
||||
padding: ${({ theme }) => theme.spacing(2)};
|
||||
padding-bottom: 0;
|
||||
`;
|
||||
|
||||
type RelativeDatePickerHeaderProps = {
|
||||
direction: VariableDateViewFilterValueDirection;
|
||||
amount?: number;
|
||||
unit: VariableDateViewFilterValueUnit;
|
||||
onChange?: (value: {
|
||||
direction: VariableDateViewFilterValueDirection;
|
||||
amount?: number;
|
||||
unit: VariableDateViewFilterValueUnit;
|
||||
}) => void;
|
||||
};
|
||||
|
||||
export const RelativeDatePickerHeader = (
|
||||
props: RelativeDatePickerHeaderProps,
|
||||
) => {
|
||||
const [direction, setDirection] = useState(props.direction);
|
||||
const [amountString, setAmountString] = useState(
|
||||
props.amount ? props.amount.toString() : '',
|
||||
);
|
||||
const [unit, setUnit] = useState(props.unit);
|
||||
|
||||
useEffect(() => {
|
||||
setAmountString(props.amount ? props.amount.toString() : '');
|
||||
setUnit(props.unit);
|
||||
setDirection(props.direction);
|
||||
}, [props.amount, props.unit, props.direction]);
|
||||
|
||||
const textInputValue = direction === 'THIS' ? '' : amountString;
|
||||
const textInputPlaceholder = direction === 'THIS' ? '-' : 'Number';
|
||||
|
||||
const isUnitPlural = props.amount && props.amount > 1 && direction !== 'THIS';
|
||||
const unitSelectOptions = RELATIVE_DATE_UNITS_SELECT_OPTIONS.map((unit) => ({
|
||||
...unit,
|
||||
label: `${unit.label}${isUnitPlural ? 's' : ''}`,
|
||||
}));
|
||||
|
||||
return (
|
||||
<StyledContainer>
|
||||
<Select
|
||||
disableBlur
|
||||
dropdownId="direction-select"
|
||||
value={direction}
|
||||
onChange={(newDirection) => {
|
||||
setDirection(newDirection);
|
||||
if (props.amount === undefined && newDirection !== 'THIS') return;
|
||||
props.onChange?.({
|
||||
direction: newDirection,
|
||||
amount: props.amount,
|
||||
unit: unit,
|
||||
});
|
||||
}}
|
||||
options={RELATIVE_DATE_DIRECTION_SELECT_OPTIONS}
|
||||
/>
|
||||
<TextInput
|
||||
value={textInputValue}
|
||||
onChange={(text) => {
|
||||
const amountString = text.replace(/[^0-9]|^0+/g, '');
|
||||
const amount = parseInt(amountString);
|
||||
|
||||
setAmountString(amountString);
|
||||
|
||||
const valueParts = {
|
||||
direction,
|
||||
amount,
|
||||
unit,
|
||||
};
|
||||
|
||||
if (
|
||||
variableDateViewFilterValuePartsSchema.safeParse(valueParts).success
|
||||
) {
|
||||
props.onChange?.(valueParts);
|
||||
}
|
||||
}}
|
||||
placeholder={textInputPlaceholder}
|
||||
disabled={direction === 'THIS'}
|
||||
/>
|
||||
<Select
|
||||
disableBlur
|
||||
dropdownId="unit-select"
|
||||
value={unit}
|
||||
onChange={(newUnit) => {
|
||||
setUnit(newUnit);
|
||||
if (direction !== 'THIS' && props.amount === undefined) return;
|
||||
props.onChange?.({
|
||||
direction,
|
||||
amount: props.amount,
|
||||
unit: newUnit,
|
||||
});
|
||||
}}
|
||||
options={unitSelectOptions}
|
||||
/>
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,13 @@
|
||||
import { VariableDateViewFilterValueDirection } from '@/views/utils/view-filter-value/resolveDateViewFilterValue';
|
||||
|
||||
type RelativeDateDirectionOption = {
|
||||
value: VariableDateViewFilterValueDirection;
|
||||
label: string;
|
||||
};
|
||||
|
||||
export const RELATIVE_DATE_DIRECTION_SELECT_OPTIONS: RelativeDateDirectionOption[] =
|
||||
[
|
||||
{ value: 'PAST', label: 'Past' },
|
||||
{ value: 'THIS', label: 'This' },
|
||||
{ value: 'NEXT', label: 'Next' },
|
||||
];
|
||||
@ -0,0 +1,13 @@
|
||||
import { VariableDateViewFilterValueUnit } from '@/views/utils/view-filter-value/resolveDateViewFilterValue';
|
||||
|
||||
type RelativeDateUnit = {
|
||||
value: VariableDateViewFilterValueUnit;
|
||||
label: string;
|
||||
};
|
||||
|
||||
export const RELATIVE_DATE_UNITS_SELECT_OPTIONS: RelativeDateUnit[] = [
|
||||
{ value: 'DAY', label: 'Day' },
|
||||
{ value: 'WEEK', label: 'Week' },
|
||||
{ value: 'MONTH', label: 'Month' },
|
||||
{ value: 'YEAR', label: 'Year' },
|
||||
];
|
||||
@ -0,0 +1,24 @@
|
||||
import { addDays, addMonths, startOfDay, subMonths } from 'date-fns';
|
||||
|
||||
export const getHighlightedDates = (highlightedDateRange?: {
|
||||
start: Date;
|
||||
end: Date;
|
||||
}): Date[] => {
|
||||
if (!highlightedDateRange) return [];
|
||||
const { start, end } = highlightedDateRange;
|
||||
|
||||
const highlightedDates: Date[] = [];
|
||||
const currentDate = startOfDay(new Date());
|
||||
const minDate = subMonths(currentDate, 2);
|
||||
const maxDate = addMonths(currentDate, 2);
|
||||
|
||||
let dateToHighlight = start < minDate ? minDate : start;
|
||||
const lastDate = end > maxDate ? maxDate : end;
|
||||
|
||||
while (dateToHighlight <= lastDate) {
|
||||
highlightedDates.push(dateToHighlight);
|
||||
dateToHighlight = addDays(dateToHighlight, 1);
|
||||
}
|
||||
|
||||
return highlightedDates;
|
||||
};
|
||||
@ -0,0 +1,16 @@
|
||||
const getMonthName = (index: number): string =>
|
||||
new Intl.DateTimeFormat('en-US', { month: 'long' }).format(
|
||||
new Date(0, index, 1),
|
||||
);
|
||||
|
||||
const getMonthNames = (monthNames: string[] = []): string[] => {
|
||||
if (monthNames.length === 12) return monthNames;
|
||||
|
||||
return getMonthNames([...monthNames, getMonthName(monthNames.length)]);
|
||||
};
|
||||
|
||||
export const getMonthSelectOptions = (): { label: string; value: number }[] =>
|
||||
getMonthNames().map((month, index) => ({
|
||||
label: month,
|
||||
value: index,
|
||||
}));
|
||||
Reference in New Issue
Block a user