Add date form field (#9021)
- Add an option to hide the input from the `packages/twenty-front/src/modules/ui/input/components/internal/date/components/AbsoluteDatePickerHeader.tsx` component - Create a form field for dates - Integrate the picker - Create an input **without a mask** to let the user types a date and validate it by pressing the Enter key - Extract some utils to parse and format dates
This commit is contained in:
committed by
GitHub
parent
5dfcc413cf
commit
5bd73e0df1
@ -3,6 +3,7 @@ import { FormBooleanFieldInput } from '@/object-record/record-field/form-types/c
|
|||||||
import { FormEmailsFieldInput } from '@/object-record/record-field/form-types/components/FormEmailsFieldInput';
|
import { FormEmailsFieldInput } from '@/object-record/record-field/form-types/components/FormEmailsFieldInput';
|
||||||
import { FormFullNameFieldInput } from '@/object-record/record-field/form-types/components/FormFullNameFieldInput';
|
import { FormFullNameFieldInput } from '@/object-record/record-field/form-types/components/FormFullNameFieldInput';
|
||||||
import { FormLinksFieldInput } from '@/object-record/record-field/form-types/components/FormLinksFieldInput';
|
import { FormLinksFieldInput } from '@/object-record/record-field/form-types/components/FormLinksFieldInput';
|
||||||
|
import { FormDateFieldInput } from '@/object-record/record-field/form-types/components/FormDateFieldInput';
|
||||||
import { FormNumberFieldInput } from '@/object-record/record-field/form-types/components/FormNumberFieldInput';
|
import { FormNumberFieldInput } from '@/object-record/record-field/form-types/components/FormNumberFieldInput';
|
||||||
import { FormSelectFieldInput } from '@/object-record/record-field/form-types/components/FormSelectFieldInput';
|
import { FormSelectFieldInput } from '@/object-record/record-field/form-types/components/FormSelectFieldInput';
|
||||||
import { FormTextFieldInput } from '@/object-record/record-field/form-types/components/FormTextFieldInput';
|
import { FormTextFieldInput } from '@/object-record/record-field/form-types/components/FormTextFieldInput';
|
||||||
@ -20,6 +21,7 @@ import { isFieldBoolean } from '@/object-record/record-field/types/guards/isFiel
|
|||||||
import { isFieldEmails } from '@/object-record/record-field/types/guards/isFieldEmails';
|
import { isFieldEmails } from '@/object-record/record-field/types/guards/isFieldEmails';
|
||||||
import { isFieldFullName } from '@/object-record/record-field/types/guards/isFieldFullName';
|
import { isFieldFullName } from '@/object-record/record-field/types/guards/isFieldFullName';
|
||||||
import { isFieldLinks } from '@/object-record/record-field/types/guards/isFieldLinks';
|
import { isFieldLinks } from '@/object-record/record-field/types/guards/isFieldLinks';
|
||||||
|
import { isFieldDate } from '@/object-record/record-field/types/guards/isFieldDate';
|
||||||
import { isFieldNumber } from '@/object-record/record-field/types/guards/isFieldNumber';
|
import { isFieldNumber } from '@/object-record/record-field/types/guards/isFieldNumber';
|
||||||
import { isFieldSelect } from '@/object-record/record-field/types/guards/isFieldSelect';
|
import { isFieldSelect } from '@/object-record/record-field/types/guards/isFieldSelect';
|
||||||
import { isFieldText } from '@/object-record/record-field/types/guards/isFieldText';
|
import { isFieldText } from '@/object-record/record-field/types/guards/isFieldText';
|
||||||
@ -98,5 +100,12 @@ export const FormFieldInput = ({
|
|||||||
onPersist={onPersist}
|
onPersist={onPersist}
|
||||||
VariablePicker={VariablePicker}
|
VariablePicker={VariablePicker}
|
||||||
/>
|
/>
|
||||||
|
) : isFieldDate(field) ? (
|
||||||
|
<FormDateFieldInput
|
||||||
|
label={field.label}
|
||||||
|
defaultValue={defaultValue as string | undefined}
|
||||||
|
onPersist={onPersist}
|
||||||
|
VariablePicker={VariablePicker}
|
||||||
|
/>
|
||||||
) : null;
|
) : null;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -0,0 +1,374 @@
|
|||||||
|
import { FormFieldInputContainer } from '@/object-record/record-field/form-types/components/FormFieldInputContainer';
|
||||||
|
import { FormFieldInputInputContainer } from '@/object-record/record-field/form-types/components/FormFieldInputInputContainer';
|
||||||
|
import { FormFieldInputRowContainer } from '@/object-record/record-field/form-types/components/FormFieldInputRowContainer';
|
||||||
|
import { VariableChip } from '@/object-record/record-field/form-types/components/VariableChip';
|
||||||
|
import { VariablePickerComponent } from '@/object-record/record-field/form-types/types/VariablePickerComponent';
|
||||||
|
import { StyledCalendarContainer } from '@/ui/field/input/components/DateInput';
|
||||||
|
import { InputLabel } from '@/ui/input/components/InputLabel';
|
||||||
|
import {
|
||||||
|
InternalDatePicker,
|
||||||
|
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/InternalDatePicker';
|
||||||
|
import { MAX_DATE } from '@/ui/input/components/internal/date/constants/MaxDate';
|
||||||
|
import { MIN_DATE } from '@/ui/input/components/internal/date/constants/MinDate';
|
||||||
|
import { parseDateToString } from '@/ui/input/components/internal/date/utils/parseDateToString';
|
||||||
|
import { parseStringToDate } from '@/ui/input/components/internal/date/utils/parseStringToDate';
|
||||||
|
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
|
||||||
|
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
|
||||||
|
import { UserContext } from '@/users/contexts/UserContext';
|
||||||
|
import { isStandaloneVariableString } from '@/workflow/utils/isStandaloneVariableString';
|
||||||
|
import { css } from '@emotion/react';
|
||||||
|
import styled from '@emotion/styled';
|
||||||
|
import {
|
||||||
|
ChangeEvent,
|
||||||
|
KeyboardEvent,
|
||||||
|
useContext,
|
||||||
|
useId,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
|
import { isDefined, Nullable, TEXT_INPUT_STYLE } from 'twenty-ui';
|
||||||
|
|
||||||
|
const StyledInputContainer = styled(FormFieldInputInputContainer)`
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
grid-template-rows: 1fr 0px;
|
||||||
|
overflow: visible;
|
||||||
|
position: relative;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledDateInputAbsoluteContainer = styled.div`
|
||||||
|
position: absolute;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledDateInput = styled.input<{ hasError?: boolean }>`
|
||||||
|
${TEXT_INPUT_STYLE}
|
||||||
|
|
||||||
|
${({ hasError, theme }) =>
|
||||||
|
hasError &&
|
||||||
|
css`
|
||||||
|
color: ${theme.color.red};
|
||||||
|
`};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledDateInputContainer = styled.div`
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
`;
|
||||||
|
|
||||||
|
type DraftValue =
|
||||||
|
| {
|
||||||
|
type: 'static';
|
||||||
|
value: string | null;
|
||||||
|
mode: 'view' | 'edit';
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: 'variable';
|
||||||
|
value: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type FormDateFieldInputProps = {
|
||||||
|
label?: string;
|
||||||
|
defaultValue: string | undefined;
|
||||||
|
onPersist: (value: string | null) => void;
|
||||||
|
VariablePicker?: VariablePickerComponent;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const FormDateFieldInput = ({
|
||||||
|
label,
|
||||||
|
defaultValue,
|
||||||
|
onPersist,
|
||||||
|
VariablePicker,
|
||||||
|
}: FormDateFieldInputProps) => {
|
||||||
|
const { timeZone } = useContext(UserContext);
|
||||||
|
|
||||||
|
const inputId = useId();
|
||||||
|
|
||||||
|
const [draftValue, setDraftValue] = useState<DraftValue>(
|
||||||
|
isStandaloneVariableString(defaultValue)
|
||||||
|
? {
|
||||||
|
type: 'variable',
|
||||||
|
value: defaultValue,
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
type: 'static',
|
||||||
|
value: defaultValue ?? null,
|
||||||
|
mode: 'view',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const draftValueAsDate = isDefined(draftValue.value)
|
||||||
|
? new Date(draftValue.value)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
const [pickerDate, setPickerDate] =
|
||||||
|
useState<Nullable<Date>>(draftValueAsDate);
|
||||||
|
|
||||||
|
const datePickerWrapperRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const [inputDateTime, setInputDateTime] = useState(
|
||||||
|
isDefined(draftValueAsDate) && !isStandaloneVariableString(defaultValue)
|
||||||
|
? parseDateToString({
|
||||||
|
date: draftValueAsDate,
|
||||||
|
isDateTimeInput: false,
|
||||||
|
userTimezone: timeZone,
|
||||||
|
})
|
||||||
|
: '',
|
||||||
|
);
|
||||||
|
|
||||||
|
const persistDate = (newDate: Nullable<Date>) => {
|
||||||
|
if (!isDefined(newDate)) {
|
||||||
|
onPersist(null);
|
||||||
|
} else {
|
||||||
|
const newDateISO = newDate.toISOString();
|
||||||
|
|
||||||
|
onPersist(newDateISO);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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 displayDatePicker =
|
||||||
|
draftValue.type === 'static' && draftValue.mode === 'edit';
|
||||||
|
|
||||||
|
useListenClickOutside({
|
||||||
|
refs: [datePickerWrapperRef],
|
||||||
|
listenerId: 'FormDateFieldInput',
|
||||||
|
callback: (event) => {
|
||||||
|
event.stopImmediatePropagation();
|
||||||
|
|
||||||
|
closeDropdownYearSelect();
|
||||||
|
closeDropdownMonthSelect();
|
||||||
|
closeDropdown();
|
||||||
|
handlePickerClickOutside();
|
||||||
|
},
|
||||||
|
enabled: displayDatePicker,
|
||||||
|
});
|
||||||
|
|
||||||
|
const handlePickerChange = (newDate: Nullable<Date>) => {
|
||||||
|
setDraftValue({
|
||||||
|
type: 'static',
|
||||||
|
mode: 'edit',
|
||||||
|
value: newDate?.toDateString() ?? null,
|
||||||
|
});
|
||||||
|
|
||||||
|
setInputDateTime(
|
||||||
|
isDefined(newDate)
|
||||||
|
? parseDateToString({
|
||||||
|
date: newDate,
|
||||||
|
isDateTimeInput: false,
|
||||||
|
userTimezone: timeZone,
|
||||||
|
})
|
||||||
|
: '',
|
||||||
|
);
|
||||||
|
|
||||||
|
setPickerDate(newDate);
|
||||||
|
|
||||||
|
persistDate(newDate);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePickerEnter = () => {};
|
||||||
|
|
||||||
|
const handlePickerEscape = () => {
|
||||||
|
// FIXME: Escape key is not handled properly by the underlying DateInput component. We need to solve that.
|
||||||
|
|
||||||
|
setDraftValue({
|
||||||
|
type: 'static',
|
||||||
|
value: draftValue.value,
|
||||||
|
mode: 'view',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePickerClickOutside = () => {
|
||||||
|
setDraftValue({
|
||||||
|
type: 'static',
|
||||||
|
value: draftValue.value,
|
||||||
|
mode: 'view',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePickerClear = () => {
|
||||||
|
setDraftValue({
|
||||||
|
type: 'static',
|
||||||
|
value: null,
|
||||||
|
mode: 'view',
|
||||||
|
});
|
||||||
|
|
||||||
|
setPickerDate(null);
|
||||||
|
|
||||||
|
setInputDateTime('');
|
||||||
|
|
||||||
|
persistDate(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePickerMouseSelect = (newDate: Nullable<Date>) => {
|
||||||
|
setDraftValue({
|
||||||
|
type: 'static',
|
||||||
|
value: newDate?.toDateString() ?? null,
|
||||||
|
mode: 'view',
|
||||||
|
});
|
||||||
|
|
||||||
|
setPickerDate(newDate);
|
||||||
|
|
||||||
|
setInputDateTime(
|
||||||
|
isDefined(newDate)
|
||||||
|
? parseDateToString({
|
||||||
|
date: newDate,
|
||||||
|
isDateTimeInput: false,
|
||||||
|
userTimezone: timeZone,
|
||||||
|
})
|
||||||
|
: '',
|
||||||
|
);
|
||||||
|
|
||||||
|
persistDate(newDate);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleInputFocus = () => {
|
||||||
|
setDraftValue({
|
||||||
|
type: 'static',
|
||||||
|
mode: 'edit',
|
||||||
|
value: draftValue.value,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleInputChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setInputDateTime(event.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleInputKeydown = (event: KeyboardEvent<HTMLInputElement>) => {
|
||||||
|
if (event.key !== 'Enter') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const inputDateTimeTrimmed = inputDateTime.trim();
|
||||||
|
|
||||||
|
if (inputDateTimeTrimmed === '') {
|
||||||
|
handlePickerClear();
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsedInputDateTime = parseStringToDate({
|
||||||
|
dateAsString: inputDateTimeTrimmed,
|
||||||
|
isDateTimeInput: false,
|
||||||
|
userTimezone: timeZone,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!isDefined(parsedInputDateTime)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let validatedDate = parsedInputDateTime;
|
||||||
|
if (parsedInputDateTime < MIN_DATE) {
|
||||||
|
validatedDate = MIN_DATE;
|
||||||
|
} else if (parsedInputDateTime > MAX_DATE) {
|
||||||
|
validatedDate = MAX_DATE;
|
||||||
|
}
|
||||||
|
|
||||||
|
setDraftValue({
|
||||||
|
type: 'static',
|
||||||
|
value: validatedDate.toDateString(),
|
||||||
|
mode: 'edit',
|
||||||
|
});
|
||||||
|
|
||||||
|
setPickerDate(validatedDate);
|
||||||
|
|
||||||
|
setInputDateTime(
|
||||||
|
parseDateToString({
|
||||||
|
date: validatedDate,
|
||||||
|
isDateTimeInput: false,
|
||||||
|
userTimezone: timeZone,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
persistDate(validatedDate);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleVariableTagInsert = (variableName: string) => {
|
||||||
|
setDraftValue({
|
||||||
|
type: 'variable',
|
||||||
|
value: variableName,
|
||||||
|
});
|
||||||
|
|
||||||
|
setInputDateTime('');
|
||||||
|
|
||||||
|
onPersist(variableName);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUnlinkVariable = () => {
|
||||||
|
setDraftValue({
|
||||||
|
type: 'static',
|
||||||
|
value: null,
|
||||||
|
mode: 'view',
|
||||||
|
});
|
||||||
|
|
||||||
|
setPickerDate(null);
|
||||||
|
|
||||||
|
onPersist(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormFieldInputContainer>
|
||||||
|
{label ? <InputLabel>{label}</InputLabel> : null}
|
||||||
|
|
||||||
|
<FormFieldInputRowContainer>
|
||||||
|
<StyledInputContainer
|
||||||
|
ref={datePickerWrapperRef}
|
||||||
|
hasRightElement={isDefined(VariablePicker)}
|
||||||
|
>
|
||||||
|
{draftValue.type === 'static' ? (
|
||||||
|
<>
|
||||||
|
<StyledDateInput
|
||||||
|
type="text"
|
||||||
|
placeholder="mm/dd/yyyy"
|
||||||
|
value={inputDateTime}
|
||||||
|
onFocus={handleInputFocus}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
onKeyDown={handleInputKeydown}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{draftValue.mode === 'edit' ? (
|
||||||
|
<StyledDateInputContainer>
|
||||||
|
<StyledDateInputAbsoluteContainer>
|
||||||
|
<StyledCalendarContainer>
|
||||||
|
<InternalDatePicker
|
||||||
|
date={pickerDate ?? new Date()}
|
||||||
|
isDateTimeInput={false}
|
||||||
|
onChange={handlePickerChange}
|
||||||
|
onMouseSelect={handlePickerMouseSelect}
|
||||||
|
onEnter={handlePickerEnter}
|
||||||
|
onEscape={handlePickerEscape}
|
||||||
|
onClear={handlePickerClear}
|
||||||
|
hideHeaderInput
|
||||||
|
/>
|
||||||
|
</StyledCalendarContainer>
|
||||||
|
</StyledDateInputAbsoluteContainer>
|
||||||
|
</StyledDateInputContainer>
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<VariableChip
|
||||||
|
rawVariableName={draftValue.value}
|
||||||
|
onRemove={handleUnlinkVariable}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</StyledInputContainer>
|
||||||
|
|
||||||
|
{VariablePicker ? (
|
||||||
|
<VariablePicker
|
||||||
|
inputId={inputId}
|
||||||
|
onVariableSelect={handleVariableTagInsert}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</FormFieldInputRowContainer>
|
||||||
|
</FormFieldInputContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,370 @@
|
|||||||
|
import { MAX_DATE } from '@/ui/input/components/internal/date/constants/MaxDate';
|
||||||
|
import { MIN_DATE } from '@/ui/input/components/internal/date/constants/MinDate';
|
||||||
|
import { parseDateToString } from '@/ui/input/components/internal/date/utils/parseDateToString';
|
||||||
|
import { expect } from '@storybook/jest';
|
||||||
|
import { Meta, StoryObj } from '@storybook/react';
|
||||||
|
import {
|
||||||
|
fn,
|
||||||
|
userEvent,
|
||||||
|
waitFor,
|
||||||
|
waitForElementToBeRemoved,
|
||||||
|
within,
|
||||||
|
} from '@storybook/test';
|
||||||
|
import { DateTime } from 'luxon';
|
||||||
|
import { FormDateFieldInput } from '../FormDateFieldInput';
|
||||||
|
|
||||||
|
const meta: Meta<typeof FormDateFieldInput> = {
|
||||||
|
title: 'UI/Data/Field/Form/Input/FormDateFieldInput',
|
||||||
|
component: FormDateFieldInput,
|
||||||
|
args: {},
|
||||||
|
argTypes: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
|
||||||
|
type Story = StoryObj<typeof FormDateFieldInput>;
|
||||||
|
|
||||||
|
export const Default: Story = {
|
||||||
|
args: {
|
||||||
|
label: 'Created At',
|
||||||
|
defaultValue: '2024-12-09T13:20:19.631Z',
|
||||||
|
},
|
||||||
|
play: async ({ canvasElement }) => {
|
||||||
|
const canvas = within(canvasElement);
|
||||||
|
|
||||||
|
await canvas.findByText('Created At');
|
||||||
|
await canvas.findByDisplayValue('12/09/2024');
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const WithDefaultEmptyValue: Story = {
|
||||||
|
args: {
|
||||||
|
label: 'Created At',
|
||||||
|
defaultValue: undefined,
|
||||||
|
},
|
||||||
|
play: async ({ canvasElement }) => {
|
||||||
|
const canvas = within(canvasElement);
|
||||||
|
|
||||||
|
await canvas.findByText('Created At');
|
||||||
|
await canvas.findByDisplayValue('');
|
||||||
|
await canvas.findByPlaceholderText('mm/dd/yyyy');
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SetsDateWithInput: Story = {
|
||||||
|
args: {
|
||||||
|
label: 'Created At',
|
||||||
|
defaultValue: undefined,
|
||||||
|
onPersist: fn(),
|
||||||
|
},
|
||||||
|
play: async ({ canvasElement, args }) => {
|
||||||
|
const canvas = within(canvasElement);
|
||||||
|
|
||||||
|
const input = await canvas.findByPlaceholderText('mm/dd/yyyy');
|
||||||
|
|
||||||
|
await userEvent.click(input);
|
||||||
|
|
||||||
|
const dialog = await canvas.findByRole('dialog');
|
||||||
|
expect(dialog).toBeVisible();
|
||||||
|
|
||||||
|
await userEvent.type(input, '12/08/2024{enter}');
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(args.onPersist).toHaveBeenCalledWith('2024-12-08T00:00:00.000Z');
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(dialog).toBeVisible();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SetsDateWithDatePicker: Story = {
|
||||||
|
args: {
|
||||||
|
label: 'Created At',
|
||||||
|
defaultValue: undefined,
|
||||||
|
onPersist: fn(),
|
||||||
|
},
|
||||||
|
play: async ({ canvasElement, args }) => {
|
||||||
|
const canvas = within(canvasElement);
|
||||||
|
|
||||||
|
const input = await canvas.findByPlaceholderText('mm/dd/yyyy');
|
||||||
|
expect(input).toBeVisible();
|
||||||
|
|
||||||
|
await userEvent.click(input);
|
||||||
|
|
||||||
|
const datePicker = await canvas.findByRole('dialog');
|
||||||
|
expect(datePicker).toBeVisible();
|
||||||
|
|
||||||
|
const dayToChoose = await within(datePicker).findByRole('option', {
|
||||||
|
name: 'Choose Saturday, December 7th, 2024',
|
||||||
|
});
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
userEvent.click(dayToChoose),
|
||||||
|
|
||||||
|
waitForElementToBeRemoved(datePicker),
|
||||||
|
waitFor(() => {
|
||||||
|
expect(args.onPersist).toHaveBeenCalledWith(
|
||||||
|
expect.stringMatching(/^2024-12-07/),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
waitFor(() => {
|
||||||
|
expect(canvas.getByDisplayValue('12/07/2024')).toBeVisible();
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ResetsDateByClickingButton: Story = {
|
||||||
|
args: {
|
||||||
|
label: 'Created At',
|
||||||
|
defaultValue: '2024-12-09T13:20:19.631Z',
|
||||||
|
onPersist: fn(),
|
||||||
|
},
|
||||||
|
play: async ({ canvasElement, args }) => {
|
||||||
|
const canvas = within(canvasElement);
|
||||||
|
|
||||||
|
const input = await canvas.findByPlaceholderText('mm/dd/yyyy');
|
||||||
|
expect(input).toBeVisible();
|
||||||
|
|
||||||
|
await userEvent.click(input);
|
||||||
|
|
||||||
|
const datePicker = await canvas.findByRole('dialog');
|
||||||
|
expect(datePicker).toBeVisible();
|
||||||
|
|
||||||
|
const clearButton = await canvas.findByText('Clear');
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
userEvent.click(clearButton),
|
||||||
|
|
||||||
|
waitForElementToBeRemoved(datePicker),
|
||||||
|
waitFor(() => {
|
||||||
|
expect(args.onPersist).toHaveBeenCalledWith(null);
|
||||||
|
}),
|
||||||
|
waitFor(() => {
|
||||||
|
expect(input).toHaveDisplayValue('');
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ResetsDateByErasingInputContent: Story = {
|
||||||
|
args: {
|
||||||
|
label: 'Created At',
|
||||||
|
defaultValue: '2024-12-09T13:20:19.631Z',
|
||||||
|
onPersist: fn(),
|
||||||
|
},
|
||||||
|
play: async ({ canvasElement, args }) => {
|
||||||
|
const canvas = within(canvasElement);
|
||||||
|
|
||||||
|
const input = await canvas.findByPlaceholderText('mm/dd/yyyy');
|
||||||
|
expect(input).toBeVisible();
|
||||||
|
|
||||||
|
expect(input).toHaveDisplayValue('12/09/2024');
|
||||||
|
|
||||||
|
await userEvent.clear(input);
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
userEvent.type(input, '{Enter}'),
|
||||||
|
|
||||||
|
waitForElementToBeRemoved(() => canvas.queryByRole('dialog')),
|
||||||
|
waitFor(() => {
|
||||||
|
expect(args.onPersist).toHaveBeenCalledWith(null);
|
||||||
|
}),
|
||||||
|
waitFor(() => {
|
||||||
|
expect(input).toHaveDisplayValue('');
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DefaultsToMinValueWhenTypingReallyOldDate: Story = {
|
||||||
|
args: {
|
||||||
|
label: 'Created At',
|
||||||
|
defaultValue: undefined,
|
||||||
|
onPersist: fn(),
|
||||||
|
},
|
||||||
|
play: async ({ canvasElement, args }) => {
|
||||||
|
const canvas = within(canvasElement);
|
||||||
|
|
||||||
|
const input = await canvas.findByPlaceholderText('mm/dd/yyyy');
|
||||||
|
expect(input).toBeVisible();
|
||||||
|
|
||||||
|
await userEvent.click(input);
|
||||||
|
|
||||||
|
const datePicker = await canvas.findByRole('dialog');
|
||||||
|
expect(datePicker).toBeVisible();
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
userEvent.type(input, '02/02/1500{Enter}'),
|
||||||
|
|
||||||
|
waitFor(() => {
|
||||||
|
expect(args.onPersist).toHaveBeenCalledWith(MIN_DATE.toISOString());
|
||||||
|
}),
|
||||||
|
waitFor(() => {
|
||||||
|
expect(input).toHaveDisplayValue(
|
||||||
|
parseDateToString({
|
||||||
|
date: MIN_DATE,
|
||||||
|
isDateTimeInput: false,
|
||||||
|
userTimezone: undefined,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
waitFor(() => {
|
||||||
|
const expectedDate = DateTime.fromJSDate(MIN_DATE)
|
||||||
|
.toLocal()
|
||||||
|
.set({
|
||||||
|
day: MIN_DATE.getUTCDate(),
|
||||||
|
month: MIN_DATE.getUTCMonth() + 1,
|
||||||
|
year: MIN_DATE.getUTCFullYear(),
|
||||||
|
hour: 0,
|
||||||
|
minute: 0,
|
||||||
|
second: 0,
|
||||||
|
millisecond: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
const selectedDay = within(datePicker).getByRole('option', {
|
||||||
|
selected: true,
|
||||||
|
name: (accessibleName) => {
|
||||||
|
// The name looks like "Choose Sunday, December 31st, 1899"
|
||||||
|
return accessibleName.includes(expectedDate.toFormat('yyyy'));
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(selectedDay).toBeVisible();
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DefaultsToMaxValueWhenTypingReallyFarDate: Story = {
|
||||||
|
args: {
|
||||||
|
label: 'Created At',
|
||||||
|
defaultValue: undefined,
|
||||||
|
onPersist: fn(),
|
||||||
|
},
|
||||||
|
play: async ({ canvasElement, args }) => {
|
||||||
|
const canvas = within(canvasElement);
|
||||||
|
|
||||||
|
const input = await canvas.findByPlaceholderText('mm/dd/yyyy');
|
||||||
|
expect(input).toBeVisible();
|
||||||
|
|
||||||
|
await userEvent.click(input);
|
||||||
|
|
||||||
|
const datePicker = await canvas.findByRole('dialog');
|
||||||
|
expect(datePicker).toBeVisible();
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
userEvent.type(input, '02/02/2500{Enter}'),
|
||||||
|
|
||||||
|
waitFor(() => {
|
||||||
|
expect(args.onPersist).toHaveBeenCalledWith(MAX_DATE.toISOString());
|
||||||
|
}),
|
||||||
|
waitFor(() => {
|
||||||
|
expect(input).toHaveDisplayValue(
|
||||||
|
parseDateToString({
|
||||||
|
date: MAX_DATE,
|
||||||
|
isDateTimeInput: false,
|
||||||
|
userTimezone: undefined,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
waitFor(() => {
|
||||||
|
const expectedDate = DateTime.fromJSDate(MAX_DATE)
|
||||||
|
.toLocal()
|
||||||
|
.set({
|
||||||
|
day: MAX_DATE.getUTCDate(),
|
||||||
|
month: MAX_DATE.getUTCMonth() + 1,
|
||||||
|
year: MAX_DATE.getUTCFullYear(),
|
||||||
|
hour: 0,
|
||||||
|
minute: 0,
|
||||||
|
second: 0,
|
||||||
|
millisecond: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
const selectedDay = within(datePicker).getByRole('option', {
|
||||||
|
selected: true,
|
||||||
|
name: (accessibleName) => {
|
||||||
|
// The name looks like "Choose Thursday, December 30th, 2100"
|
||||||
|
return accessibleName.includes(expectedDate.toFormat('yyyy'));
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(selectedDay).toBeVisible();
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SwitchesToStandaloneVariable: Story = {
|
||||||
|
args: {
|
||||||
|
label: 'Created At',
|
||||||
|
defaultValue: undefined,
|
||||||
|
onPersist: fn(),
|
||||||
|
VariablePicker: ({ onVariableSelect }) => {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
onVariableSelect('{{test}}');
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Add variable
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
play: async ({ canvasElement }) => {
|
||||||
|
const canvas = within(canvasElement);
|
||||||
|
|
||||||
|
const addVariableButton = await canvas.findByText('Add variable');
|
||||||
|
await userEvent.click(addVariableButton);
|
||||||
|
|
||||||
|
const variableTag = await canvas.findByText('test');
|
||||||
|
expect(variableTag).toBeVisible();
|
||||||
|
|
||||||
|
const removeVariableButton = canvas.getByTestId(/^remove-icon/);
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
userEvent.click(removeVariableButton),
|
||||||
|
|
||||||
|
waitForElementToBeRemoved(variableTag),
|
||||||
|
waitFor(() => {
|
||||||
|
const input = canvas.getByPlaceholderText('mm/dd/yyyy');
|
||||||
|
expect(input).toBeVisible();
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ClickingOutsideDoesNotResetInputState: Story = {
|
||||||
|
args: {
|
||||||
|
label: 'Created At',
|
||||||
|
defaultValue: '2024-12-09T13:20:19.631Z',
|
||||||
|
onPersist: fn(),
|
||||||
|
},
|
||||||
|
play: async ({ canvasElement, args }) => {
|
||||||
|
const defaultValueAsDisplayString = parseDateToString({
|
||||||
|
date: new Date(args.defaultValue!),
|
||||||
|
isDateTimeInput: false,
|
||||||
|
userTimezone: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
const canvas = within(canvasElement);
|
||||||
|
|
||||||
|
const input = await canvas.findByPlaceholderText('mm/dd/yyyy');
|
||||||
|
expect(input).toBeVisible();
|
||||||
|
expect(input).toHaveDisplayValue(defaultValueAsDisplayString);
|
||||||
|
|
||||||
|
await userEvent.type(input, '{Backspace}{Backspace}');
|
||||||
|
|
||||||
|
const datePicker = await canvas.findByRole('dialog');
|
||||||
|
expect(datePicker).toBeVisible();
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
userEvent.click(canvasElement),
|
||||||
|
|
||||||
|
waitForElementToBeRemoved(datePicker),
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(args.onPersist).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
expect(input).toHaveDisplayValue(defaultValueAsDisplayString.slice(0, -2));
|
||||||
|
},
|
||||||
|
};
|
||||||
@ -4,8 +4,8 @@ import { useDateField } from '@/object-record/record-field/meta-types/hooks/useD
|
|||||||
import { DateInput } from '@/ui/field/input/components/DateInput';
|
import { DateInput } from '@/ui/field/input/components/DateInput';
|
||||||
import { isDefined } from '~/utils/isDefined';
|
import { isDefined } from '~/utils/isDefined';
|
||||||
|
|
||||||
import { usePersistField } from '../../../hooks/usePersistField';
|
|
||||||
import { FieldInputClickOutsideEvent } from '@/object-record/record-field/meta-types/input/components/DateTimeFieldInput';
|
import { FieldInputClickOutsideEvent } from '@/object-record/record-field/meta-types/input/components/DateTimeFieldInput';
|
||||||
|
import { usePersistField } from '../../../hooks/usePersistField';
|
||||||
|
|
||||||
type FieldInputEvent = (persist: () => void) => void;
|
type FieldInputEvent = (persist: () => void) => void;
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { useRef, useState } from 'react';
|
|
||||||
import { Nullable } from 'twenty-ui';
|
import { Nullable } from 'twenty-ui';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -10,8 +9,9 @@ import {
|
|||||||
} from '@/ui/input/components/internal/date/components/InternalDatePicker';
|
} from '@/ui/input/components/internal/date/components/InternalDatePicker';
|
||||||
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
|
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
|
||||||
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
|
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
|
||||||
|
import { useRef, useState } from 'react';
|
||||||
|
|
||||||
const StyledCalendarContainer = styled.div`
|
export const StyledCalendarContainer = styled.div`
|
||||||
background: ${({ theme }) => theme.background.transparent.secondary};
|
background: ${({ theme }) => theme.background.transparent.secondary};
|
||||||
backdrop-filter: ${({ theme }) => theme.blur.medium};
|
backdrop-filter: ${({ theme }) => theme.blur.medium};
|
||||||
border: 1px solid ${({ theme }) => theme.border.color.light};
|
border: 1px solid ${({ theme }) => theme.border.color.light};
|
||||||
@ -32,6 +32,7 @@ export type DateInputProps = {
|
|||||||
isDateTimeInput?: boolean;
|
isDateTimeInput?: boolean;
|
||||||
onClear?: () => void;
|
onClear?: () => void;
|
||||||
onSubmit?: (newDate: Nullable<Date>) => void;
|
onSubmit?: (newDate: Nullable<Date>) => void;
|
||||||
|
hideHeaderInput?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DateInput = ({
|
export const DateInput = ({
|
||||||
@ -44,6 +45,7 @@ export const DateInput = ({
|
|||||||
isDateTimeInput,
|
isDateTimeInput,
|
||||||
onClear,
|
onClear,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
|
hideHeaderInput,
|
||||||
}: DateInputProps) => {
|
}: DateInputProps) => {
|
||||||
const [internalValue, setInternalValue] = useState(value);
|
const [internalValue, setInternalValue] = useState(value);
|
||||||
|
|
||||||
@ -97,6 +99,7 @@ export const DateInput = ({
|
|||||||
onEnter={onEnter}
|
onEnter={onEnter}
|
||||||
onEscape={onEscape}
|
onEscape={onEscape}
|
||||||
onClear={handleClear}
|
onClear={handleClear}
|
||||||
|
hideHeaderInput={hideHeaderInput}
|
||||||
/>
|
/>
|
||||||
</StyledCalendarContainer>
|
</StyledCalendarContainer>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -38,6 +38,7 @@ type AbsoluteDatePickerHeaderProps = {
|
|||||||
nextMonthButtonDisabled: boolean;
|
nextMonthButtonDisabled: boolean;
|
||||||
isDateTimeInput?: boolean;
|
isDateTimeInput?: boolean;
|
||||||
timeZone: string;
|
timeZone: string;
|
||||||
|
hideInput?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const AbsoluteDatePickerHeader = ({
|
export const AbsoluteDatePickerHeader = ({
|
||||||
@ -51,6 +52,7 @@ export const AbsoluteDatePickerHeader = ({
|
|||||||
nextMonthButtonDisabled,
|
nextMonthButtonDisabled,
|
||||||
isDateTimeInput,
|
isDateTimeInput,
|
||||||
timeZone,
|
timeZone,
|
||||||
|
hideInput = false,
|
||||||
}: AbsoluteDatePickerHeaderProps) => {
|
}: AbsoluteDatePickerHeaderProps) => {
|
||||||
const endOfDayDateTimeInLocalTimezone = DateTime.now().set({
|
const endOfDayDateTimeInLocalTimezone = DateTime.now().set({
|
||||||
day: date.getDate(),
|
day: date.getDate(),
|
||||||
@ -66,12 +68,15 @@ export const AbsoluteDatePickerHeader = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<DateTimeInput
|
{!hideInput && (
|
||||||
date={date}
|
<DateTimeInput
|
||||||
isDateTimeInput={isDateTimeInput}
|
date={date}
|
||||||
onChange={onChange}
|
isDateTimeInput={isDateTimeInput}
|
||||||
userTimezone={timeZone}
|
onChange={onChange}
|
||||||
/>
|
userTimezone={timeZone}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<StyledCustomDatePickerHeader>
|
<StyledCustomDatePickerHeader>
|
||||||
<Select
|
<Select
|
||||||
dropdownId={MONTH_AND_YEAR_DROPDOWN_MONTH_SELECT_ID}
|
dropdownId={MONTH_AND_YEAR_DROPDOWN_MONTH_SELECT_ID}
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
import { css } from '@emotion/react';
|
import { css } from '@emotion/react';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { DateTime } from 'luxon';
|
|
||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
import { useIMask } from 'react-imask';
|
import { useIMask } from 'react-imask';
|
||||||
|
|
||||||
@ -10,6 +9,10 @@ import { DATE_TIME_BLOCKS } from '@/ui/input/components/internal/date/constants/
|
|||||||
import { DATE_TIME_MASK } from '@/ui/input/components/internal/date/constants/DateTimeMask';
|
import { DATE_TIME_MASK } from '@/ui/input/components/internal/date/constants/DateTimeMask';
|
||||||
import { MAX_DATE } from '@/ui/input/components/internal/date/constants/MaxDate';
|
import { MAX_DATE } from '@/ui/input/components/internal/date/constants/MaxDate';
|
||||||
import { MIN_DATE } from '@/ui/input/components/internal/date/constants/MinDate';
|
import { MIN_DATE } from '@/ui/input/components/internal/date/constants/MinDate';
|
||||||
|
import { parseDateToString } from '@/ui/input/components/internal/date/utils/parseDateToString';
|
||||||
|
import { parseStringToDate } from '@/ui/input/components/internal/date/utils/parseStringToDate';
|
||||||
|
import { isNull } from '@sniptt/guards';
|
||||||
|
import { isDefined } from 'twenty-ui';
|
||||||
|
|
||||||
const StyledInputContainer = styled.div`
|
const StyledInputContainer = styled.div`
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@ -53,53 +56,29 @@ export const DateTimeInput = ({
|
|||||||
isDateTimeInput,
|
isDateTimeInput,
|
||||||
userTimezone,
|
userTimezone,
|
||||||
}: DateTimeInputProps) => {
|
}: DateTimeInputProps) => {
|
||||||
const parsingFormat = isDateTimeInput ? 'MM/dd/yyyy HH:mm' : 'MM/dd/yyyy';
|
|
||||||
|
|
||||||
const [hasError, setHasError] = useState(false);
|
const [hasError, setHasError] = useState(false);
|
||||||
|
|
||||||
const parseDateToString = useCallback(
|
const handleParseDateToString = useCallback(
|
||||||
(date: any) => {
|
(date: any) => {
|
||||||
const dateParsed = DateTime.fromJSDate(date, { zone: userTimezone });
|
return parseDateToString({
|
||||||
|
date,
|
||||||
const dateWithoutTime = DateTime.fromJSDate(date)
|
isDateTimeInput: isDateTimeInput === true,
|
||||||
.toLocal()
|
userTimezone,
|
||||||
.set({
|
});
|
||||||
day: date.getUTCDate(),
|
|
||||||
month: date.getUTCMonth() + 1,
|
|
||||||
year: date.getUTCFullYear(),
|
|
||||||
hour: 0,
|
|
||||||
minute: 0,
|
|
||||||
second: 0,
|
|
||||||
millisecond: 0,
|
|
||||||
});
|
|
||||||
|
|
||||||
const formattedDate = isDateTimeInput
|
|
||||||
? dateParsed.setZone(userTimezone).toFormat(parsingFormat)
|
|
||||||
: dateWithoutTime.toFormat(parsingFormat);
|
|
||||||
|
|
||||||
return formattedDate;
|
|
||||||
},
|
},
|
||||||
[parsingFormat, isDateTimeInput, userTimezone],
|
[isDateTimeInput, userTimezone],
|
||||||
);
|
);
|
||||||
|
|
||||||
const parseStringToDate = (str: string) => {
|
const handleParseStringToDate = (str: string) => {
|
||||||
setHasError(false);
|
const date = parseStringToDate({
|
||||||
|
dateAsString: str,
|
||||||
|
isDateTimeInput: isDateTimeInput === true,
|
||||||
|
userTimezone,
|
||||||
|
});
|
||||||
|
|
||||||
const parsedDate = isDateTimeInput
|
setHasError(isNull(date) === true);
|
||||||
? DateTime.fromFormat(str, parsingFormat, { zone: userTimezone })
|
|
||||||
: DateTime.fromFormat(str, parsingFormat, { zone: 'utc' });
|
|
||||||
|
|
||||||
const isValid = parsedDate.isValid;
|
return date;
|
||||||
|
|
||||||
if (!isValid) {
|
|
||||||
setHasError(true);
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const jsDate = parsedDate.toJSDate();
|
|
||||||
|
|
||||||
return jsDate;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const pattern = isDateTimeInput ? DATE_TIME_MASK : DATE_MASK;
|
const pattern = isDateTimeInput ? DATE_TIME_MASK : DATE_MASK;
|
||||||
@ -112,14 +91,18 @@ export const DateTimeInput = ({
|
|||||||
blocks,
|
blocks,
|
||||||
min: MIN_DATE,
|
min: MIN_DATE,
|
||||||
max: MAX_DATE,
|
max: MAX_DATE,
|
||||||
format: parseDateToString,
|
format: handleParseDateToString,
|
||||||
parse: parseStringToDate,
|
parse: handleParseStringToDate,
|
||||||
lazy: false,
|
lazy: false,
|
||||||
autofix: true,
|
autofix: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
onComplete: (value) => {
|
onComplete: (value) => {
|
||||||
const parsedDate = parseStringToDate(value);
|
const parsedDate = parseStringToDate({
|
||||||
|
dateAsString: value,
|
||||||
|
isDateTimeInput: isDateTimeInput === true,
|
||||||
|
userTimezone,
|
||||||
|
});
|
||||||
|
|
||||||
onChange?.(parsedDate);
|
onChange?.(parsedDate);
|
||||||
},
|
},
|
||||||
@ -130,8 +113,18 @@ export const DateTimeInput = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setValue(parseDateToString(date));
|
if (!isDefined(date)) {
|
||||||
}, [date, setValue, parseDateToString]);
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setValue(
|
||||||
|
parseDateToString({
|
||||||
|
date: date,
|
||||||
|
isDateTimeInput: isDateTimeInput === true,
|
||||||
|
userTimezone,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}, [date, setValue, isDateTimeInput, userTimezone]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledInputContainer>
|
<StyledInputContainer>
|
||||||
|
|||||||
@ -9,11 +9,11 @@ import {
|
|||||||
StyledHoverableMenuItemBase,
|
StyledHoverableMenuItemBase,
|
||||||
} from 'twenty-ui';
|
} from 'twenty-ui';
|
||||||
|
|
||||||
import { DateTimeInput } from '@/ui/input/components/internal/date/components/DateTimeInput';
|
|
||||||
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
|
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
|
||||||
import { isDefined } from '~/utils/isDefined';
|
import { isDefined } from '~/utils/isDefined';
|
||||||
|
|
||||||
import { AbsoluteDatePickerHeader } from '@/ui/input/components/internal/date/components/AbsoluteDatePickerHeader';
|
import { AbsoluteDatePickerHeader } from '@/ui/input/components/internal/date/components/AbsoluteDatePickerHeader';
|
||||||
|
import { DateTimeInput } from '@/ui/input/components/internal/date/components/DateTimeInput';
|
||||||
import { RelativeDatePickerHeader } from '@/ui/input/components/internal/date/components/RelativeDatePickerHeader';
|
import { RelativeDatePickerHeader } from '@/ui/input/components/internal/date/components/RelativeDatePickerHeader';
|
||||||
import { getHighlightedDates } from '@/ui/input/components/internal/date/utils/getHighlightedDates';
|
import { getHighlightedDates } from '@/ui/input/components/internal/date/utils/getHighlightedDates';
|
||||||
import { UserContext } from '@/users/contexts/UserContext';
|
import { UserContext } from '@/users/contexts/UserContext';
|
||||||
@ -276,6 +276,7 @@ const StyledButton = styled(MenuItemLeftContent)`
|
|||||||
|
|
||||||
type InternalDatePickerProps = {
|
type InternalDatePickerProps = {
|
||||||
isRelative?: boolean;
|
isRelative?: boolean;
|
||||||
|
hideHeaderInput?: boolean;
|
||||||
date: Date | null;
|
date: Date | null;
|
||||||
relativeDate?: {
|
relativeDate?: {
|
||||||
direction: VariableDateViewFilterValueDirection;
|
direction: VariableDateViewFilterValueDirection;
|
||||||
@ -317,6 +318,7 @@ export const InternalDatePicker = ({
|
|||||||
relativeDate,
|
relativeDate,
|
||||||
onRelativeDateChange,
|
onRelativeDateChange,
|
||||||
highlightedDateRange,
|
highlightedDateRange,
|
||||||
|
hideHeaderInput,
|
||||||
}: InternalDatePickerProps) => {
|
}: InternalDatePickerProps) => {
|
||||||
const internalDate = date ?? new Date();
|
const internalDate = date ?? new Date();
|
||||||
|
|
||||||
@ -510,6 +512,7 @@ export const InternalDatePicker = ({
|
|||||||
nextMonthButtonDisabled={nextMonthButtonDisabled}
|
nextMonthButtonDisabled={nextMonthButtonDisabled}
|
||||||
isDateTimeInput={isDateTimeInput}
|
isDateTimeInput={isDateTimeInput}
|
||||||
timeZone={timeZone}
|
timeZone={timeZone}
|
||||||
|
hideInput={hideHeaderInput}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1 @@
|
|||||||
|
export const DATE_PARSER_FORMAT = 'MM/dd/yyyy';
|
||||||
@ -0,0 +1 @@
|
|||||||
|
export const DATE_TIME_PARSER_FORMAT = 'MM/dd/yyyy HH:mm';
|
||||||
@ -0,0 +1,39 @@
|
|||||||
|
import { DATE_PARSER_FORMAT } from '@/ui/input/components/internal/date/constants/DateParserFormat';
|
||||||
|
import { DATE_TIME_PARSER_FORMAT } from '@/ui/input/components/internal/date/constants/DateTimeParserFormat';
|
||||||
|
import { DateTime } from 'luxon';
|
||||||
|
|
||||||
|
type ParseDateToStringArgs = {
|
||||||
|
date: Date;
|
||||||
|
isDateTimeInput: boolean;
|
||||||
|
userTimezone: string | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const parseDateToString = ({
|
||||||
|
date,
|
||||||
|
isDateTimeInput,
|
||||||
|
userTimezone,
|
||||||
|
}: ParseDateToStringArgs) => {
|
||||||
|
const parsingFormat = isDateTimeInput
|
||||||
|
? DATE_TIME_PARSER_FORMAT
|
||||||
|
: DATE_PARSER_FORMAT;
|
||||||
|
|
||||||
|
const dateParsed = DateTime.fromJSDate(date, { zone: userTimezone });
|
||||||
|
|
||||||
|
const dateWithoutTime = DateTime.fromJSDate(date)
|
||||||
|
.toLocal()
|
||||||
|
.set({
|
||||||
|
day: date.getUTCDate(),
|
||||||
|
month: date.getUTCMonth() + 1,
|
||||||
|
year: date.getUTCFullYear(),
|
||||||
|
hour: 0,
|
||||||
|
minute: 0,
|
||||||
|
second: 0,
|
||||||
|
millisecond: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
const formattedDate = isDateTimeInput
|
||||||
|
? dateParsed.setZone(userTimezone).toFormat(parsingFormat)
|
||||||
|
: dateWithoutTime.toFormat(parsingFormat);
|
||||||
|
|
||||||
|
return formattedDate;
|
||||||
|
};
|
||||||
@ -0,0 +1,33 @@
|
|||||||
|
import { DATE_PARSER_FORMAT } from '@/ui/input/components/internal/date/constants/DateParserFormat';
|
||||||
|
import { DATE_TIME_PARSER_FORMAT } from '@/ui/input/components/internal/date/constants/DateTimeParserFormat';
|
||||||
|
import { DateTime } from 'luxon';
|
||||||
|
|
||||||
|
type ParseStringToDateArgs = {
|
||||||
|
dateAsString: string;
|
||||||
|
isDateTimeInput: boolean;
|
||||||
|
userTimezone: string | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const parseStringToDate = ({
|
||||||
|
dateAsString,
|
||||||
|
isDateTimeInput,
|
||||||
|
userTimezone,
|
||||||
|
}: ParseStringToDateArgs) => {
|
||||||
|
const parsingFormat = isDateTimeInput
|
||||||
|
? DATE_TIME_PARSER_FORMAT
|
||||||
|
: DATE_PARSER_FORMAT;
|
||||||
|
|
||||||
|
const parsedDate = isDateTimeInput
|
||||||
|
? DateTime.fromFormat(dateAsString, parsingFormat, { zone: userTimezone })
|
||||||
|
: DateTime.fromFormat(dateAsString, parsingFormat, { zone: 'utc' });
|
||||||
|
|
||||||
|
const isValid = parsedDate.isValid;
|
||||||
|
|
||||||
|
if (!isValid) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const jsDate = parsedDate.toJSDate();
|
||||||
|
|
||||||
|
return jsDate;
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user