From a0b56dcd84ed7530f0b6e9e6d0696c67be205127 Mon Sep 17 00:00:00 2001 From: Devessier Date: Thu, 5 Dec 2024 18:25:23 +0100 Subject: [PATCH 01/24] feat: make a first implementation for the date field Be sure to check edge cases. Previously, the component was unmounted when the field was saved. Now, we keep it displayed. We need to figure out how to solve the involved edge cases. --- .../components/FormFieldInput.tsx | 9 + .../components/FormDateFieldInput.tsx | 298 ++++++++++++++++++ .../FormFieldInputInputContainer.tsx | 1 + .../input/components/DateFieldInput.tsx | 30 +- .../input/components/DateTimeFieldInput.tsx | 32 +- .../ui/field/input/components/DateInput.tsx | 51 ++- .../components/AbsoluteDatePickerHeader.tsx | 17 +- .../date/components/DateTimeInput.tsx | 95 +----- .../date/components/InternalDatePicker.tsx | 5 +- .../internal/date/hooks/useDateTimeInput.tsx | 124 ++++++++ 10 files changed, 520 insertions(+), 142 deletions(-) create mode 100644 packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormDateFieldInput.tsx create mode 100644 packages/twenty-front/src/modules/ui/input/components/internal/date/hooks/useDateTimeInput.tsx diff --git a/packages/twenty-front/src/modules/object-record/record-field/components/FormFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/components/FormFieldInput.tsx index 8a11b321550e..38084a4d26ae 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/components/FormFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/components/FormFieldInput.tsx @@ -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 { FormFullNameFieldInput } from '@/object-record/record-field/form-types/components/FormFullNameFieldInput'; 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 { FormSelectFieldInput } from '@/object-record/record-field/form-types/components/FormSelectFieldInput'; 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 { isFieldFullName } from '@/object-record/record-field/types/guards/isFieldFullName'; 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 { isFieldSelect } from '@/object-record/record-field/types/guards/isFieldSelect'; import { isFieldText } from '@/object-record/record-field/types/guards/isFieldText'; @@ -98,5 +100,12 @@ export const FormFieldInput = ({ onPersist={onPersist} VariablePicker={VariablePicker} /> + ) : isFieldDate(field) ? ( + ) : null; }; diff --git a/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormDateFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormDateFieldInput.tsx new file mode 100644 index 000000000000..495926fd64c8 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormDateFieldInput.tsx @@ -0,0 +1,298 @@ +import { StyledFormFieldInputContainer } from '@/object-record/record-field/form-types/components/StyledFormFieldInputContainer'; +import { StyledFormFieldInputInputContainer } from '@/object-record/record-field/form-types/components/StyledFormFieldInputInputContainer'; +import { StyledFormFieldInputRowContainer } from '@/object-record/record-field/form-types/components/StyledFormFieldInputRowContainer'; +import { VariableChip } from '@/object-record/record-field/form-types/components/VariableChip'; +import { VariablePickerComponent } from '@/object-record/record-field/form-types/types/VariablePickerComponent'; +import { DateInput } from '@/ui/field/input/components/DateInput'; +import { InputLabel } from '@/ui/input/components/InputLabel'; +import { useDateTimeInput } from '@/ui/input/components/internal/date/hooks/useDateTimeInput'; +import { isStandaloneVariableString } from '@/workflow/utils/isStandaloneVariableString'; +import { css } from '@emotion/react'; +import styled from '@emotion/styled'; +import { useId, useMemo, useRef, useState } from 'react'; +import { isDefined, Nullable, TEXT_INPUT_STYLE } from 'twenty-ui'; + +const StyledDisplayModeContainer = styled.button` + width: 100%; + align-items: center; + display: flex; + cursor: pointer; + border: none; + background: transparent; + font-family: inherit; + padding-inline: ${({ theme }) => theme.spacing(2)}; + text-align: left; + + &:hover, + &[data-open='true'] { + background-color: ${({ theme }) => theme.background.transparent.lighter}; + } +`; + +const StyledInputContainer = styled(StyledFormFieldInputInputContainer)` + display: grid; + grid-template-columns: 1fr; + grid-template-rows: 1fr 0px; + overflow: visible; +`; + +const StyledDateInput = styled.input<{ hasError?: boolean }>` + ${TEXT_INPUT_STYLE} + + ${({ hasError, theme }) => + hasError && + css` + color: ${theme.color.red}; + `}; +`; + +// Inspired by the StyledInlineCellInput component. +const StyledDateInputContainer = styled.div` + position: relative; + z-index: 1000; +`; + +type DraftValue = + | { + type: 'static'; + value: string | null; + editingMode: 'view' | 'edit'; + } + | { + type: 'variable'; + value: string; + }; + +// eslint-disable-next-line prefer-arrow/prefer-arrow-functions +function assertStaticDraftValue( + draftValue: DraftValue, +): asserts draftValue is DraftValue & { type: 'static' } { + if (draftValue.type !== 'static') { + throw new Error('Expected the draftValue to be static'); + } +} + +type FormDateFieldInputProps = { + label?: string; + defaultValue: string | undefined; + onPersist: (value: string | null) => void; + VariablePicker?: VariablePickerComponent; +}; + +export const FormDateFieldInput = ({ + label, + defaultValue, + onPersist, + VariablePicker, +}: FormDateFieldInputProps) => { + const inputId = useId(); + + const [draftValue, setDraftValue] = useState( + isStandaloneVariableString(defaultValue) + ? { + type: 'variable', + value: defaultValue, + } + : { + type: 'static', + value: defaultValue ?? null, + editingMode: 'view', + }, + ); + + const dateValue = useMemo( + () => (isDefined(draftValue.value) ? new Date(draftValue.value) : null), + [draftValue.value], + ); + + const persistDate = (newDate: Nullable) => { + console.log('persisting date', newDate); + + if (!isDefined(newDate)) { + onPersist(null); + } else { + const newDateISO = newDate.toISOString(); + + onPersist(newDateISO); + } + }; + + const handleChange = (newDate: Nullable) => { + assertStaticDraftValue(draftValue); + + setDraftValue({ + ...draftValue, + value: newDate?.toDateString() ?? null, + }); + + persistDate(newDate); + }; + + const handleEnter = (newDate: Nullable) => { + setDraftValue({ + type: 'static', + value: newDate?.toDateString() ?? null, + editingMode: 'view', + }); + + setTemporaryValue(newDate); + + persistDate(newDate); + }; + + const handleEscape = (newDate: Nullable) => { + setDraftValue({ + type: 'static', + value: newDate?.toDateString() ?? null, + editingMode: 'view', + }); + + setTemporaryValue(newDate); + + persistDate(newDate); + }; + + const handleClickOutside = ( + _event: MouseEvent | TouchEvent, + newDate: Nullable, + ) => { + setDraftValue({ + type: 'static', + value: newDate?.toDateString() ?? null, + editingMode: 'view', + }); + + setTemporaryValue(newDate); + + persistDate(newDate); + }; + + const handleClear = () => { + setDraftValue({ + type: 'static', + value: null, + editingMode: 'view', + }); + + setTemporaryValue(null); + + persistDate(null); + }; + + const handleSubmit = (newDate: Nullable) => { + setDraftValue({ + type: 'static', + value: newDate?.toDateString() ?? null, + editingMode: 'view', + }); + + setTemporaryValue(newDate); + + persistDate(newDate); + }; + + const datePickerWrapperRef = useRef(null); + + const [temporaryValue, setTemporaryValue] = + useState>(dateValue); + + const inputDateTimeDate = useMemo(() => dateValue ?? new Date(), [dateValue]); + const { + ref: dateInputRef, + value: dateInputValue, + hasError: dateInputHasError, + } = useDateTimeInput({ + date: inputDateTimeDate, + onChange: (newDate) => { + setTemporaryValue(newDate); + + handleChange(newDate); + }, + }); + + const handleVariableTagInsert = (variableName: string) => { + setDraftValue({ + type: 'variable', + value: variableName, + }); + + onPersist(variableName); + }; + + const handleUnlinkVariable = () => { + setDraftValue({ + type: 'static', + value: null, + editingMode: 'view', + }); + + setTemporaryValue(null); + + onPersist(null); + }; + + return ( + + {label ? {label} : null} + + + + {draftValue.type === 'static' ? ( + <> + { + setDraftValue({ + type: 'static', + editingMode: 'edit', + value: draftValue.value, + }); + }} + onChange={() => {}} + /> + + {draftValue.editingMode === 'edit' ? ( + +
+ +
+
+ ) : null} + + ) : ( + + )} +
+ + {VariablePicker ? ( + + ) : null} +
+
+ ); +}; diff --git a/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormFieldInputInputContainer.tsx b/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormFieldInputInputContainer.tsx index 36dbe3d861d4..cc84f24ac03c 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormFieldInputInputContainer.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormFieldInputInputContainer.tsx @@ -28,6 +28,7 @@ const StyledFormFieldInputInputContainer = styled.div<{ display: flex; overflow: ${({ multiline }) => (multiline ? 'auto' : 'hidden')}; width: 100%; + position: relative; `; export const FormFieldInputInputContainer = StyledFormFieldInputInputContainer; diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/DateFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/DateFieldInput.tsx index 0e67f4baa436..631c57bb18ac 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/DateFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/DateFieldInput.tsx @@ -4,6 +4,7 @@ import { useDateField } from '@/object-record/record-field/meta-types/hooks/useD import { DateInput } from '@/ui/field/input/components/DateInput'; import { isDefined } from '~/utils/isDefined'; +import { useRef, useState } from 'react'; import { usePersistField } from '../../../hooks/usePersistField'; import { FieldInputClickOutsideEvent } from '@/object-record/record-field/meta-types/input/components/DateTimeFieldInput'; @@ -67,16 +68,25 @@ export const DateFieldInput = ({ const dateValue = fieldValue ? new Date(fieldValue) : null; + const wrapperRef = useRef(null); + + const [temporaryValue, setTemporaryValue] = + useState>(dateValue); + return ( - +
+ +
); }; diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/DateTimeFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/DateTimeFieldInput.tsx index 15f85c61c276..cc5c638a5a27 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/DateTimeFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/DateTimeFieldInput.tsx @@ -2,6 +2,7 @@ import { Nullable } from 'twenty-ui'; import { DateInput } from '@/ui/field/input/components/DateInput'; +import { useRef, useState } from 'react'; import { usePersistField } from '../../../hooks/usePersistField'; import { useDateTimeField } from '../../hooks/useDateTimeField'; @@ -69,17 +70,26 @@ export const DateTimeFieldInput = ({ const dateValue = fieldValue ? new Date(fieldValue) : null; + const wrapperRef = useRef(null); + + const [temporaryValue, setTemporaryValue] = + useState>(dateValue); + return ( - +
+ +
); }; diff --git a/packages/twenty-front/src/modules/ui/field/input/components/DateInput.tsx b/packages/twenty-front/src/modules/ui/field/input/components/DateInput.tsx index 858436a9744e..0fce5dcb228e 100644 --- a/packages/twenty-front/src/modules/ui/field/input/components/DateInput.tsx +++ b/packages/twenty-front/src/modules/ui/field/input/components/DateInput.tsx @@ -1,5 +1,4 @@ import styled from '@emotion/styled'; -import { useRef, useState } from 'react'; import { Nullable } from 'twenty-ui'; import { @@ -20,7 +19,6 @@ const StyledCalendarContainer = styled.div` `; export type DateInputProps = { - value: Nullable; onEnter: (newDate: Nullable) => void; onEscape: (newDate: Nullable) => void; onClickOutside: ( @@ -32,10 +30,13 @@ export type DateInputProps = { isDateTimeInput?: boolean; onClear?: () => void; onSubmit?: (newDate: Nullable) => void; + hideHeaderInput?: boolean; + temporaryValue: Nullable; + setTemporaryValue: (newValue: Nullable) => void; + wrapperRef: React.RefObject; }; export const DateInput = ({ - value, onEnter, onEscape, onClickOutside, @@ -44,23 +45,23 @@ export const DateInput = ({ isDateTimeInput, onClear, onSubmit, + hideHeaderInput, + wrapperRef, + temporaryValue, + setTemporaryValue, }: DateInputProps) => { - const [internalValue, setInternalValue] = useState(value); - - const wrapperRef = useRef(null); - const handleChange = (newDate: Date | null) => { - setInternalValue(newDate); + setTemporaryValue(newDate); onChange?.(newDate); }; const handleClear = () => { - setInternalValue(null); + setTemporaryValue(null); onClear?.(); }; const handleMouseSelect = (newDate: Date | null) => { - setInternalValue(newDate); + setTemporaryValue(newDate); onSubmit?.(newDate); }; @@ -77,28 +78,26 @@ export const DateInput = ({ listenerId: 'DateInput', callback: (event) => { event.stopImmediatePropagation(); - closeDropdownYearSelect(); closeDropdownMonthSelect(); closeDropdown(); - onClickOutside(event, internalValue); + onClickOutside(event, temporaryValue); }, }); return ( -
- - - -
+ + + ); }; diff --git a/packages/twenty-front/src/modules/ui/input/components/internal/date/components/AbsoluteDatePickerHeader.tsx b/packages/twenty-front/src/modules/ui/input/components/internal/date/components/AbsoluteDatePickerHeader.tsx index 6ffeb3fc3da6..c701a46814c8 100644 --- a/packages/twenty-front/src/modules/ui/input/components/internal/date/components/AbsoluteDatePickerHeader.tsx +++ b/packages/twenty-front/src/modules/ui/input/components/internal/date/components/AbsoluteDatePickerHeader.tsx @@ -38,6 +38,7 @@ type AbsoluteDatePickerHeaderProps = { nextMonthButtonDisabled: boolean; isDateTimeInput?: boolean; timeZone: string; + hideInput?: boolean; }; export const AbsoluteDatePickerHeader = ({ @@ -51,6 +52,7 @@ export const AbsoluteDatePickerHeader = ({ nextMonthButtonDisabled, isDateTimeInput, timeZone, + hideInput, }: AbsoluteDatePickerHeaderProps) => { const endOfDayDateTimeInLocalTimezone = DateTime.now().set({ day: date.getDate(), @@ -66,12 +68,15 @@ export const AbsoluteDatePickerHeader = ({ return ( <> - + {hideInput !== true ? ( + + ) : null} + Date: Fri, 13 Dec 2024 13:47:53 +0100 Subject: [PATCH 20/24] fix: keep the input state when clicking outside of the picker or pressing Esc --- .../components/FormDateFieldInput.tsx | 42 ++++--------------- .../FormDateFieldInput.stories.tsx | 36 ++++++++++++++++ 2 files changed, 43 insertions(+), 35 deletions(-) diff --git a/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormDateFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormDateFieldInput.tsx index 25e452c9e980..663502ca92c3 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormDateFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormDateFieldInput.tsx @@ -142,51 +142,22 @@ export const FormDateFieldInput = ({ const handlePickerEnter = () => {}; - const handlePickerEscape = (newDate: Nullable) => { + const handlePickerEscape = () => { + // FIXME: Escape key is not handled properly by the underlying DateInput component. We need to solve that. + setDraftValue({ type: 'static', - value: newDate?.toDateString() ?? null, + value: draftValue.value, mode: 'view', }); - - setTemporaryValue(newDate); - - setInputDateTime( - isDefined(newDate) - ? parseDateToString({ - date: newDate, - isDateTimeInput: false, - userTimezone: timeZone, - }) - : '', - ); - - persistDate(newDate); }; - const handlePickerClickOutside = ( - _event: MouseEvent | TouchEvent, - newDate: Nullable, - ) => { + const handlePickerClickOutside = () => { setDraftValue({ type: 'static', - value: newDate?.toDateString() ?? null, + value: draftValue.value, mode: 'view', }); - - setTemporaryValue(newDate); - - setInputDateTime( - isDefined(newDate) - ? parseDateToString({ - date: newDate, - isDateTimeInput: false, - userTimezone: timeZone, - }) - : '', - ); - - persistDate(newDate); }; const handlePickerClear = () => { @@ -204,6 +175,7 @@ export const FormDateFieldInput = ({ }; const handlePickerSubmit = (newDate: Nullable) => { + // 2 setDraftValue({ type: 'static', value: newDate?.toDateString() ?? null, diff --git a/packages/twenty-front/src/modules/object-record/record-field/form-types/components/__stories__/FormDateFieldInput.stories.tsx b/packages/twenty-front/src/modules/object-record/record-field/form-types/components/__stories__/FormDateFieldInput.stories.tsx index 2e85b40c8495..39a313fb809c 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/form-types/components/__stories__/FormDateFieldInput.stories.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/form-types/components/__stories__/FormDateFieldInput.stories.tsx @@ -332,3 +332,39 @@ export const SwitchesToStandaloneVariable: Story = { ]); }, }; + +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)); + }, +}; From f35ff7e3fb5f2599fb58dcb9e7be5b4bcce91f6d Mon Sep 17 00:00:00 2001 From: Devessier Date: Fri, 13 Dec 2024 14:23:59 +0100 Subject: [PATCH 21/24] fix: use renamed components --- .../form-types/components/FormDateFieldInput.tsx | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormDateFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormDateFieldInput.tsx index 663502ca92c3..60c0985d9f7f 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormDateFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormDateFieldInput.tsx @@ -1,6 +1,6 @@ -import { StyledFormFieldInputContainer } from '@/object-record/record-field/form-types/components/StyledFormFieldInputContainer'; -import { StyledFormFieldInputInputContainer } from '@/object-record/record-field/form-types/components/StyledFormFieldInputInputContainer'; -import { StyledFormFieldInputRowContainer } from '@/object-record/record-field/form-types/components/StyledFormFieldInputRowContainer'; +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 { DateInput } from '@/ui/field/input/components/DateInput'; @@ -23,7 +23,7 @@ import { } from 'react'; import { isDefined, Nullable, TEXT_INPUT_STYLE } from 'twenty-ui'; -const StyledInputContainer = styled(StyledFormFieldInputInputContainer)` +const StyledInputContainer = styled(FormFieldInputInputContainer)` display: grid; grid-template-columns: 1fr; grid-template-rows: 1fr 0px; @@ -282,10 +282,10 @@ export const FormDateFieldInput = ({ }; return ( - + {label ? {label} : null} - + ) : null} - - + + ); }; From 00d6cccb59fca0315829a86a0f299e49f89bcefe Mon Sep 17 00:00:00 2001 From: Devessier Date: Mon, 16 Dec 2024 17:36:58 +0100 Subject: [PATCH 22/24] revert: revert updates on the DateInput component --- .../ui/field/input/components/DateInput.tsx | 52 ++++++++++--------- 1 file changed, 28 insertions(+), 24 deletions(-) diff --git a/packages/twenty-front/src/modules/ui/field/input/components/DateInput.tsx b/packages/twenty-front/src/modules/ui/field/input/components/DateInput.tsx index 0fce5dcb228e..5d587ee303b5 100644 --- a/packages/twenty-front/src/modules/ui/field/input/components/DateInput.tsx +++ b/packages/twenty-front/src/modules/ui/field/input/components/DateInput.tsx @@ -9,8 +9,9 @@ import { } from '@/ui/input/components/internal/date/components/InternalDatePicker'; import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown'; 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}; backdrop-filter: ${({ theme }) => theme.blur.medium}; border: 1px solid ${({ theme }) => theme.border.color.light}; @@ -19,6 +20,7 @@ const StyledCalendarContainer = styled.div` `; export type DateInputProps = { + value: Nullable; onEnter: (newDate: Nullable) => void; onEscape: (newDate: Nullable) => void; onClickOutside: ( @@ -31,12 +33,10 @@ export type DateInputProps = { onClear?: () => void; onSubmit?: (newDate: Nullable) => void; hideHeaderInput?: boolean; - temporaryValue: Nullable; - setTemporaryValue: (newValue: Nullable) => void; - wrapperRef: React.RefObject; }; export const DateInput = ({ + value, onEnter, onEscape, onClickOutside, @@ -46,22 +46,23 @@ export const DateInput = ({ onClear, onSubmit, hideHeaderInput, - wrapperRef, - temporaryValue, - setTemporaryValue, }: DateInputProps) => { + const [internalValue, setInternalValue] = useState(value); + + const wrapperRef = useRef(null); + const handleChange = (newDate: Date | null) => { - setTemporaryValue(newDate); + setInternalValue(newDate); onChange?.(newDate); }; const handleClear = () => { - setTemporaryValue(null); + setInternalValue(null); onClear?.(); }; const handleMouseSelect = (newDate: Date | null) => { - setTemporaryValue(newDate); + setInternalValue(newDate); onSubmit?.(newDate); }; @@ -78,26 +79,29 @@ export const DateInput = ({ listenerId: 'DateInput', callback: (event) => { event.stopImmediatePropagation(); + closeDropdownYearSelect(); closeDropdownMonthSelect(); closeDropdown(); - onClickOutside(event, temporaryValue); + onClickOutside(event, internalValue); }, }); return ( - - - +
+ + + +
); }; From 246a4fb6f4472665947076c6c97e76ff46adaf07 Mon Sep 17 00:00:00 2001 From: Devessier Date: Mon, 16 Dec 2024 17:49:33 +0100 Subject: [PATCH 23/24] refactor: directly use the InternalDatePicker instead of trying to make the DateInput component fit my needs --- .../components/FormDateFieldInput.tsx | 79 +++++++++++++------ 1 file changed, 56 insertions(+), 23 deletions(-) diff --git a/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormDateFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormDateFieldInput.tsx index 60c0985d9f7f..751741c6d85d 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormDateFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormDateFieldInput.tsx @@ -3,12 +3,20 @@ import { FormFieldInputInputContainer } from '@/object-record/record-field/form- 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 { DateInput } from '@/ui/field/input/components/DateInput'; +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'; @@ -95,11 +103,11 @@ export const FormDateFieldInput = ({ ? new Date(draftValue.value) : null; - const datePickerWrapperRef = useRef(null); - - const [temporaryValue, setTemporaryValue] = + const [pickerDate, setPickerDate] = useState>(draftValueAsDate); + const datePickerWrapperRef = useRef(null); + const [inputDateTime, setInputDateTime] = useState( isDefined(draftValueAsDate) && !isStandaloneVariableString(defaultValue) ? parseDateToString({ @@ -120,6 +128,31 @@ export const FormDateFieldInput = ({ } }; + 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) => { setDraftValue({ type: 'static', @@ -137,6 +170,8 @@ export const FormDateFieldInput = ({ : '', ); + setPickerDate(newDate); + persistDate(newDate); }; @@ -167,22 +202,21 @@ export const FormDateFieldInput = ({ mode: 'view', }); - setTemporaryValue(null); + setPickerDate(null); setInputDateTime(''); persistDate(null); }; - const handlePickerSubmit = (newDate: Nullable) => { - // 2 + const handlePickerMouseSelect = (newDate: Nullable) => { setDraftValue({ type: 'static', value: newDate?.toDateString() ?? null, mode: 'view', }); - setTemporaryValue(newDate); + setPickerDate(newDate); setInputDateTime( isDefined(newDate) @@ -245,7 +279,7 @@ export const FormDateFieldInput = ({ mode: 'edit', }); - setTemporaryValue(validatedDate); + setPickerDate(validatedDate); setInputDateTime( parseDateToString({ @@ -276,7 +310,7 @@ export const FormDateFieldInput = ({ mode: 'view', }); - setTemporaryValue(null); + setPickerDate(null); onPersist(null); }; @@ -304,19 +338,18 @@ export const FormDateFieldInput = ({ {draftValue.mode === 'edit' ? ( - + + + ) : null} From 2d707ea65d87d1dcebfb24023257493b3ec4229f Mon Sep 17 00:00:00 2001 From: Devessier Date: Mon, 16 Dec 2024 17:52:49 +0100 Subject: [PATCH 24/24] revert: go back to previous version with untouched DateInput component --- .../input/components/DateFieldInput.tsx | 32 +++++++------------ .../input/components/DateTimeFieldInput.tsx | 32 +++++++------------ 2 files changed, 22 insertions(+), 42 deletions(-) diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/DateFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/DateFieldInput.tsx index 631c57bb18ac..74ad1528189e 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/DateFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/DateFieldInput.tsx @@ -4,9 +4,8 @@ import { useDateField } from '@/object-record/record-field/meta-types/hooks/useD import { DateInput } from '@/ui/field/input/components/DateInput'; import { isDefined } from '~/utils/isDefined'; -import { useRef, useState } from 'react'; -import { usePersistField } from '../../../hooks/usePersistField'; import { FieldInputClickOutsideEvent } from '@/object-record/record-field/meta-types/input/components/DateTimeFieldInput'; +import { usePersistField } from '../../../hooks/usePersistField'; type FieldInputEvent = (persist: () => void) => void; @@ -68,25 +67,16 @@ export const DateFieldInput = ({ const dateValue = fieldValue ? new Date(fieldValue) : null; - const wrapperRef = useRef(null); - - const [temporaryValue, setTemporaryValue] = - useState>(dateValue); - return ( -
- -
+ ); }; diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/DateTimeFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/DateTimeFieldInput.tsx index cc5c638a5a27..15f85c61c276 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/DateTimeFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/DateTimeFieldInput.tsx @@ -2,7 +2,6 @@ import { Nullable } from 'twenty-ui'; import { DateInput } from '@/ui/field/input/components/DateInput'; -import { useRef, useState } from 'react'; import { usePersistField } from '../../../hooks/usePersistField'; import { useDateTimeField } from '../../hooks/useDateTimeField'; @@ -70,26 +69,17 @@ export const DateTimeFieldInput = ({ const dateValue = fieldValue ? new Date(fieldValue) : null; - const wrapperRef = useRef(null); - - const [temporaryValue, setTemporaryValue] = - useState>(dateValue); - return ( -
- -
+ ); };