Fix workflow date fields (#19210)

Before: broken on forms, missing border right, no fullWidth, not
properly saved
<img width="220" height="160" alt="Capture d’écran 2026-03-31 à 17 10
47"
src="https://github.com/user-attachments/assets/4143fcb7-909f-42a3-b05e-39185395f657"
/> <img width="231" height="102" alt="Capture d’écran 2026-03-31 à 17
11 05"
src="https://github.com/user-attachments/assets/3989f6b8-ef8a-42a3-9ccc-35a9be1fb67f"
/>
<img width="230" height="75" alt="Capture d’écran 2026-03-31 à 17 12
15"
src="https://github.com/user-attachments/assets/67f5f93f-887f-4e3b-95c2-f5076f41fb21"
/>

After: save and validate on edition, fix design
<img width="231" height="132" alt="Capture d’écran 2026-03-31 à 17 15
08"
src="https://github.com/user-attachments/assets/d1aa0a64-499d-479d-8d5c-e5e104ad6464"
/>
<img width="231" height="75" alt="Capture d’écran 2026-03-31 à 17 15
33"
src="https://github.com/user-attachments/assets/8d668713-8466-4e81-8901-4b12b9271244"
/>
<img width="461" height="156" alt="Capture d’écran 2026-03-31 à 17 14
54"
src="https://github.com/user-attachments/assets/f9d33242-f1cc-48f7-9f63-322799e1f9b8"
/>

Simplifying FormDateInput using the existing DatePicker
This commit is contained in:
Thomas Trompette
2026-04-01 16:44:24 +02:00
committed by GitHub
parent a0c6727a61
commit 19dd4d6c1b
6 changed files with 153 additions and 306 deletions
@@ -1,76 +1,52 @@
import { useDateTimeFormat } from '@/localization/hooks/useDateTimeFormat';
import { FormFieldInputContainer } from '@/object-record/record-field/ui/form-types/components/FormFieldInputContainer';
import { FormFieldInputInnerContainer } from '@/object-record/record-field/ui/form-types/components/FormFieldInputInnerContainer';
import { FormFieldInputRowContainer } from '@/object-record/record-field/ui/form-types/components/FormFieldInputRowContainer';
import { VariableChipStandalone } from '@/object-record/record-field/ui/form-types/components/VariableChipStandalone';
import { type VariablePickerComponent } from '@/object-record/record-field/ui/form-types/types/VariablePickerComponent';
import { InputLabel } from '@/ui/input/components/InputLabel';
import { DatePicker } from '@/ui/input/components/internal/date/components/DatePicker';
import {
DatePicker,
MONTH_AND_YEAR_DROPDOWN_MONTH_SELECT_ID,
MONTH_AND_YEAR_DROPDOWN_YEAR_SELECT_ID,
} from '@/ui/input/components/internal/date/components/DateTimePicker';
import { useParseDateInputStringToPlainDate } from '@/ui/input/components/internal/date/hooks/useParseDateInputStringToPlainDate';
import { useParsePlainDateToDateInputString } from '@/ui/input/components/internal/date/hooks/useParsePlainDateToDateInputString';
} from '@/ui/input/components/internal/date/components/DatePicker';
import { DatePickerInput } from '@/ui/input/components/internal/date/components/DatePickerInput';
import { useUserTimezone } from '@/ui/input/components/internal/date/hooks/useUserTimezone';
import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
import { OverlayContainer } from '@/ui/layout/overlay/components/OverlayContainer';
import { useHotkeysOnFocusedElement } from '@/ui/utilities/hotkey/hooks/useHotkeysOnFocusedElement';
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
import { isStandaloneVariableString } from '@/workflow/utils/isStandaloneVariableString';
import { styled } from '@linaria/react';
import { isNonEmptyString } from '@sniptt/guards';
import {
useId,
useRef,
useState,
type ChangeEvent,
type KeyboardEvent,
} from 'react';
import { useId, useRef, useState } from 'react';
import { Temporal } from 'temporal-polyfill';
import { Key } from 'ts-key-enum';
import { isDefined } from 'twenty-shared/utils';
import { themeCssVariables } from 'twenty-ui/theme-constants';
import { type Nullable } from 'twenty-ui/utilities';
import { getDateFormatStringForDatePickerInputMask } from '~/utils/date-utils';
const StyledInputContainerWrapper = styled.div`
display: grid;
grid-template-columns: 1fr;
grid-template-rows: 1fr 0;
overflow: visible;
position: relative;
`;
const StyledDateInputAbsoluteContainer = styled.div`
position: absolute;
top: ${themeCssVariables.spacing[1]};
`;
const StyledDateInput = styled.input<{ hasError?: boolean }>`
background-color: transparent;
border: none;
color: ${({ hasError }) =>
hasError
? themeCssVariables.color.red
: themeCssVariables.font.color.primary};
color: ${themeCssVariables.font.color.primary};
font-family: ${themeCssVariables.font.family};
font-size: inherit;
font-weight: inherit;
outline: none;
const StyledDateInputTextContainer = styled.div<{ isReadonly?: boolean }>`
align-items: center;
display: flex;
flex: 1;
min-width: 0;
pointer-events: ${({ isReadonly }) => (isReadonly ? 'none' : 'auto')};
user-select: ${({ isReadonly }) => (isReadonly ? 'none' : 'auto')};
width: 100%;
&::placeholder,
&::-webkit-input-placeholder {
color: ${themeCssVariables.font.color.light};
font-family: ${themeCssVariables.font.family};
font-weight: ${themeCssVariables.font.weight.medium};
& input {
color: ${({ isReadonly }) =>
isReadonly
? themeCssVariables.font.color.light
: themeCssVariables.font.color.primary};
}
&:disabled {
color: ${themeCssVariables.font.color.tertiary};
}
padding: ${themeCssVariables.spacing[0]} ${themeCssVariables.spacing[2]};
`;
const StyledDateInputContainer = styled.div`
@@ -104,15 +80,8 @@ export const FormDateFieldInput = ({
onChange,
VariablePicker,
readonly,
placeholder,
}: FormDateFieldInputProps) => {
const instanceId = useId();
const { dateFormat } = useDateTimeFormat();
const { parsePlainDateToDateInputString } =
useParsePlainDateToDateInputString();
const { parseDateInputStringToPlainDate } =
useParseDateInputStringToPlainDate();
const [draftValue, setDraftValue] = useState<DraftValue>(
isStandaloneVariableString(defaultValue)
@@ -127,24 +96,8 @@ export const FormDateFieldInput = ({
},
);
const draftValueAsDate =
isDefined(draftValue.value) &&
isNonEmptyString(draftValue.value) &&
draftValue.type === 'static'
? draftValue.value
: null;
const [pickerDate, setPickerDate] =
useState<Nullable<string>>(draftValueAsDate);
const datePickerWrapperRef = useRef<HTMLDivElement>(null);
const [inputDate, setInputDate] = useState(
isDefined(draftValueAsDate) && !isStandaloneVariableString(defaultValue)
? parsePlainDateToDateInputString(draftValueAsDate)
: '',
);
const persistDate = (newDate: Nullable<string>) => {
if (!isDefined(newDate)) {
onChange(null);
@@ -159,14 +112,9 @@ export const FormDateFieldInput = ({
const displayDatePicker =
draftValue.type === 'static' && draftValue.mode === 'edit';
const defaultPlaceHolder =
getDateFormatStringForDatePickerInputMask(dateFormat);
const placeholderToDisplay = placeholder ?? defaultPlaceHolder;
useListenClickOutside({
refs: [datePickerWrapperRef],
listenerId: 'FormDateTimeFieldInputBase',
listenerId: 'FormDateFieldInputBase',
callback: (event) => {
event.stopImmediatePropagation();
@@ -188,12 +136,6 @@ export const FormDateFieldInput = ({
value: newDate ?? null,
});
setInputDate(
isDefined(newDate) ? parsePlainDateToDateInputString(newDate) : '',
);
setPickerDate(newDate);
persistDate(newDate);
};
@@ -222,10 +164,6 @@ export const FormDateFieldInput = ({
mode: 'view',
});
setPickerDate(null);
setInputDate('');
persistDate(null);
};
@@ -236,58 +174,21 @@ export const FormDateFieldInput = ({
mode: 'view',
});
setPickerDate(newDate);
setInputDate(
isDefined(newDate) ? parsePlainDateToDateInputString(newDate) : '',
);
persistDate(newDate);
};
const handleInputFocus = () => {
const handleInputChange = (newDate: string | null) => {
if (!isDefined(newDate)) {
return;
}
setDraftValue({
type: 'static',
mode: 'edit',
value: draftValue.value,
});
};
const handleInputChange = (event: ChangeEvent<HTMLInputElement>) => {
setInputDate(event.target.value);
};
const handleInputKeydown = (event: KeyboardEvent<HTMLInputElement>) => {
if (event.key !== 'Enter') {
return;
}
const inputDateTime = inputDate.trim();
if (inputDateTime === '') {
handlePickerClear();
return;
}
const parsedInputPlainDate = parseDateInputStringToPlainDate(inputDateTime);
if (!isDefined(parsedInputPlainDate)) {
return;
}
let validatedDate = parsedInputPlainDate;
setDraftValue({
type: 'static',
value: validatedDate,
mode: 'edit',
value: newDate,
});
setPickerDate(validatedDate);
setInputDate(parsePlainDateToDateInputString(validatedDate));
persistDate(validatedDate);
persistDate(newDate);
};
const handleVariableTagInsert = (variableName: string) => {
@@ -296,8 +197,6 @@ export const FormDateFieldInput = ({
value: variableName,
});
setInputDate('');
onChange(variableName);
};
@@ -308,8 +207,6 @@ export const FormDateFieldInput = ({
mode: 'view',
});
setPickerDate(null);
onChange(null);
};
@@ -320,56 +217,87 @@ export const FormDateFieldInput = ({
dependencies: [handlePickerEscape],
});
const { userTimezone } = useUserTimezone();
const isVariable = Boolean(isStandaloneVariableString(defaultValue));
const plainDateValueFromProps =
isVariable ||
!isDefined(defaultValue) ||
defaultValue === '' ||
defaultValue === 'null'
? null
: defaultValue.includes('T')
? Temporal.Instant.from(defaultValue)
.toZonedDateTimeISO(userTimezone)
.toPlainDate()
.toString()
: Temporal.PlainDate.from(defaultValue).toString();
const plainDateValue =
draftValue.type === 'static' && isNonEmptyString(draftValue.value)
? draftValue.value
: plainDateValueFromProps;
const handleMaskedDatePointerDownCapture = () => {
if (readonly) {
return;
}
setDraftValue((previous) =>
previous.type === 'static' && previous.mode === 'view'
? { ...previous, mode: 'edit' }
: previous,
);
};
return (
<FormFieldInputContainer>
{label ? <InputLabel>{label}</InputLabel> : null}
<FormFieldInputRowContainer>
<StyledInputContainerWrapper ref={datePickerWrapperRef}>
<FormFieldInputInnerContainer
formFieldInputInstanceId={instanceId}
hasRightElement={isDefined(VariablePicker) && !readonly}
>
{draftValue.type === 'static' ? (
<>
<StyledDateInput
type="text"
placeholder={placeholderToDisplay}
value={inputDate}
onFocus={handleInputFocus}
<FormFieldInputInnerContainer
ref={datePickerWrapperRef}
formFieldInputInstanceId={instanceId}
hasRightElement={isDefined(VariablePicker) && !readonly}
>
{draftValue.type === 'static' ? (
<>
<StyledDateInputTextContainer
isReadonly={readonly === true}
onPointerDownCapture={handleMaskedDatePointerDownCapture}
>
<DatePickerInput
date={plainDateValue}
onChange={handleInputChange}
onKeyDown={handleInputKeydown}
disabled={readonly}
/>
{draftValue.mode === 'edit' ? (
<StyledDateInputContainer>
<StyledDateInputAbsoluteContainer>
<OverlayContainer>
<DatePicker
instanceId={instanceId}
plainDateString={pickerDate}
onChange={handlePickerChange}
onClose={handlePickerMouseSelect}
onEnter={handlePickerEnter}
onEscape={handlePickerEscape}
onClear={handlePickerClear}
hideHeaderInput
/>
</OverlayContainer>
</StyledDateInputAbsoluteContainer>
</StyledDateInputContainer>
) : null}
</>
) : (
<VariableChipStandalone
rawVariableName={draftValue.value}
onRemove={readonly ? undefined : handleUnlinkVariable}
/>
)}
</FormFieldInputInnerContainer>
</StyledInputContainerWrapper>
</StyledDateInputTextContainer>
{draftValue.mode === 'edit' && !readonly ? (
<StyledDateInputContainer>
<StyledDateInputAbsoluteContainer>
<OverlayContainer>
<DatePicker
instanceId={instanceId}
plainDateString={plainDateValue}
onChange={handlePickerChange}
onClose={handlePickerMouseSelect}
onEnter={handlePickerEnter}
onEscape={handlePickerEscape}
onClear={handlePickerClear}
hideHeaderInput
/>
</OverlayContainer>
</StyledDateInputAbsoluteContainer>
</StyledDateInputContainer>
) : null}
</>
) : (
<VariableChipStandalone
rawVariableName={draftValue.value}
onRemove={readonly ? undefined : handleUnlinkVariable}
/>
)}
</FormFieldInputInnerContainer>
{VariablePicker && !readonly ? (
<VariablePicker
instanceId={instanceId}
@@ -26,14 +26,6 @@ import { isDefined } from 'twenty-shared/utils';
import { themeCssVariables } from 'twenty-ui/theme-constants';
import { type Nullable } from 'twenty-ui/utilities';
const StyledInputContainerWrapper = styled.div`
display: grid;
grid-template-columns: 1fr;
grid-template-rows: 1fr 0;
overflow: visible;
position: relative;
`;
const StyledDateInputAbsoluteContainer = styled.div`
position: absolute;
top: ${themeCssVariables.spacing[1]};
@@ -249,50 +241,49 @@ export const FormDateTimeFieldInput = ({
{label ? <InputLabel>{label}</InputLabel> : null}
<FormFieldInputRowContainer>
<StyledInputContainerWrapper ref={datePickerWrapperRef}>
<FormFieldInputInnerContainer
formFieldInputInstanceId={instanceId}
hasRightElement={isDefined(VariablePicker) && !readonly}
>
{draftValue.type === 'static' ? (
<>
<StyledDateInputTextContainer>
<DateTimePickerInput
date={dateValue}
onChange={handleInputChange}
onFocus={handleInputFocus}
readonly={readonly}
timeZone={timeZone}
/>
</StyledDateInputTextContainer>
{draftValue.mode === 'edit' ? (
<StyledDateInputContainer>
<StyledDateInputAbsoluteContainer>
<OverlayContainer>
<DateTimePicker
instanceId={instanceId}
date={dateValue}
onChange={handlePickerChange}
onClose={handlePickerMouseSelect}
onEnter={handlePickerEnter}
onEscape={handlePickerEscape}
onClear={handlePickerClear}
hideHeaderInput
timeZone={timeZone}
/>
</OverlayContainer>
</StyledDateInputAbsoluteContainer>
</StyledDateInputContainer>
) : null}
</>
) : (
<VariableChipStandalone
rawVariableName={draftValue.value}
onRemove={readonly ? undefined : handleUnlinkVariable}
/>
)}
</FormFieldInputInnerContainer>
</StyledInputContainerWrapper>
<FormFieldInputInnerContainer
ref={datePickerWrapperRef}
formFieldInputInstanceId={instanceId}
hasRightElement={isDefined(VariablePicker) && !readonly}
>
{draftValue.type === 'static' ? (
<>
<StyledDateInputTextContainer>
<DateTimePickerInput
date={dateValue}
onChange={handleInputChange}
onFocus={handleInputFocus}
readonly={readonly}
timeZone={timeZone}
/>
</StyledDateInputTextContainer>
{draftValue.mode === 'edit' ? (
<StyledDateInputContainer>
<StyledDateInputAbsoluteContainer>
<OverlayContainer>
<DateTimePicker
instanceId={instanceId}
date={dateValue}
onChange={handlePickerChange}
onClose={handlePickerMouseSelect}
onEnter={handlePickerEnter}
onEscape={handlePickerEscape}
onClear={handlePickerClear}
hideHeaderInput
timeZone={timeZone}
/>
</OverlayContainer>
</StyledDateInputAbsoluteContainer>
</StyledDateInputContainer>
) : null}
</>
) : (
<VariableChipStandalone
rawVariableName={draftValue.value}
onRemove={readonly ? undefined : handleUnlinkVariable}
/>
)}
</FormFieldInputInnerContainer>
{VariablePicker && !readonly ? (
<VariablePicker
instanceId={instanceId}
@@ -24,7 +24,9 @@ const StyledFormFieldInputInnerContainer = styled.div<
border-bottom-right-radius: ${({ multiline, hasRightElement }) =>
multiline || !hasRightElement ? themeCssVariables.border.radius.sm : '0'};
border-right: ${({ multiline, hasRightElement }) =>
multiline || !hasRightElement ? 'auto' : 'none'};
multiline || !hasRightElement
? `1px solid ${themeCssVariables.border.color.medium}`
: 'none'};
border-top-left-radius: ${themeCssVariables.border.radius.sm};
border-top-right-radius: ${({ multiline, hasRightElement }) =>
multiline || !hasRightElement ? themeCssVariables.border.radius.sm : '0'};