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:
+102
-174
@@ -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}
|
||||
|
||||
+43
-52
@@ -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}
|
||||
|
||||
+3
-1
@@ -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'};
|
||||
|
||||
Reference in New Issue
Block a user