From 19dd4d6c1b08ed3463f9edb6eeaa4a54cb55cbf5 Mon Sep 17 00:00:00 2001 From: Thomas Trompette Date: Wed, 1 Apr 2026 16:44:24 +0200 Subject: [PATCH] Fix workflow date fields (#19210) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Before: broken on forms, missing border right, no fullWidth, not properly saved Capture d’écran 2026-03-31 à 17 10
47 Capture d’écran 2026-03-31 à 17
11 05 Capture d’écran 2026-03-31 à 17 12
15 After: save and validate on edition, fix design Capture d’écran 2026-03-31 à 17 15
08 Capture d’écran 2026-03-31 à 17 15
33 Capture d’écran 2026-03-31 à 17 14
54 Simplifying FormDateInput using the existing DatePicker --- .../components/FormDateFieldInput.tsx | 276 +++++++----------- .../components/FormDateTimeFieldInput.tsx | 95 +++--- .../FormFieldInputInnerContainer.tsx | 4 +- .../date/components/DateTimePickerInput.tsx | 2 +- .../WorkflowEditActionFormBuilder.tsx | 7 +- .../WorkflowEditActionFormFiller.stories.tsx | 75 ----- 6 files changed, 153 insertions(+), 306 deletions(-) diff --git a/packages/twenty-front/src/modules/object-record/record-field/ui/form-types/components/FormDateFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/ui/form-types/components/FormDateFieldInput.tsx index 87b5e773ff..0e64e4d676 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/ui/form-types/components/FormDateFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/ui/form-types/components/FormDateFieldInput.tsx @@ -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( 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>(draftValueAsDate); - const datePickerWrapperRef = useRef(null); - const [inputDate, setInputDate] = useState( - isDefined(draftValueAsDate) && !isStandaloneVariableString(defaultValue) - ? parsePlainDateToDateInputString(draftValueAsDate) - : '', - ); - const persistDate = (newDate: Nullable) => { 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) => { - setInputDate(event.target.value); - }; - - const handleInputKeydown = (event: KeyboardEvent) => { - 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 ( {label ? {label} : null} - - - {draftValue.type === 'static' ? ( - <> - + {draftValue.type === 'static' ? ( + <> + + - - {draftValue.mode === 'edit' ? ( - - - - - - - - ) : null} - - ) : ( - - )} - - - + + {draftValue.mode === 'edit' && !readonly ? ( + + + + + + + + ) : null} + + ) : ( + + )} + {VariablePicker && !readonly ? ( {label} : null} - - - {draftValue.type === 'static' ? ( - <> - - - - {draftValue.mode === 'edit' ? ( - - - - - - - - ) : null} - - ) : ( - - )} - - + + {draftValue.type === 'static' ? ( + <> + + + + {draftValue.mode === 'edit' ? ( + + + + + + + + ) : null} + + ) : ( + + )} + {VariablePicker && !readonly ? ( 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'}; diff --git a/packages/twenty-front/src/modules/ui/input/components/internal/date/components/DateTimePickerInput.tsx b/packages/twenty-front/src/modules/ui/input/components/internal/date/components/DateTimePickerInput.tsx index 07ab9d5746..ec806995dd 100644 --- a/packages/twenty-front/src/modules/ui/input/components/internal/date/components/DateTimePickerInput.tsx +++ b/packages/twenty-front/src/modules/ui/input/components/internal/date/components/DateTimePickerInput.tsx @@ -17,8 +17,8 @@ import { useUserTimezone } from '@/ui/input/components/internal/date/hooks/useUs import { useEffect, useState } from 'react'; import { Temporal } from 'temporal-polyfill'; import { isDefined } from 'twenty-shared/utils'; -import { isDifferentZonedDateTime } from '~/utils/dates/isDifferentZonedDateTime'; import { themeCssVariables } from 'twenty-ui/theme-constants'; +import { isDifferentZonedDateTime } from '~/utils/dates/isDifferentZonedDateTime'; const StyledInputContainer = styled.div` align-items: center; diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/form-action/components/WorkflowEditActionFormBuilder.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/form-action/components/WorkflowEditActionFormBuilder.tsx index 0429bbc584..c5cb34ee67 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/form-action/components/WorkflowEditActionFormBuilder.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/form-action/components/WorkflowEditActionFormBuilder.tsx @@ -14,8 +14,8 @@ import { WorkflowStepFooter } from '@/workflow/workflow-steps/components/Workflo import { WorkflowEditActionFormFieldSettings } from '@/workflow/workflow-steps/workflow-actions/form-action/components/WorkflowEditActionFormFieldSettings'; import { type WorkflowFormActionField } from '@/workflow/workflow-steps/workflow-actions/form-action/types/WorkflowFormActionField'; import { getDefaultFormFieldSettings } from '@/workflow/workflow-steps/workflow-actions/form-action/utils/getDefaultFormFieldSettings'; -import { styled } from '@linaria/react'; import { type OnDragEndResponder } from '@hello-pangea/dnd'; +import { styled } from '@linaria/react'; import { useLingui } from '@lingui/react/macro'; import { isNonEmptyString } from '@sniptt/guards'; import { useContext, useEffect, useState } from 'react'; @@ -30,9 +30,9 @@ import { IconTrash, } from 'twenty-ui/display'; import { LightIconButton } from 'twenty-ui/input'; +import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants'; import { useDebouncedCallback } from 'use-debounce'; import { v4 } from 'uuid'; -import { themeCssVariables, ThemeContext } from 'twenty-ui/theme-constants'; export type WorkflowEditActionFormBuilderProps = { triggerType: WorkflowTriggerType | undefined; @@ -98,9 +98,10 @@ const StyledFieldContainer = styled.div<{ cursor: ${({ readonly }) => (readonly ? 'default' : 'pointer')}; display: flex; font-family: inherit; + height: 100%; padding-left: ${themeCssVariables.spacing[2]}; - padding-right: ${themeCssVariables.spacing[2]}; + padding-right: ${themeCssVariables.spacing[2]}; width: 100%; &:hover, diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/form-action/components/__stories__/WorkflowEditActionFormFiller.stories.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/form-action/components/__stories__/WorkflowEditActionFormFiller.stories.tsx index 12d2d38330..08ff7c59cd 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/form-action/components/__stories__/WorkflowEditActionFormFiller.stories.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/form-action/components/__stories__/WorkflowEditActionFormFiller.stories.tsx @@ -81,43 +81,6 @@ const mockAction: WorkflowFormAction = { }, }; -// TEMP_DISABLED_TEST: Commented out unused mock data -// const mockActionWithDuplicatedRecordFields: WorkflowFormAction = { -// id: 'form-action-1', -// type: 'FORM', -// name: 'Test Form', -// valid: true, -// settings: { -// input: [ -// { -// id: 'field-1', -// name: 'record', -// label: 'Record', -// type: 'RECORD', -// placeholder: 'Select a record', -// settings: { -// objectName: 'company', -// }, -// }, -// { -// id: 'field-2', -// name: 'record', -// label: 'Record', -// type: 'RECORD', -// placeholder: 'Select a record', -// settings: { -// objectName: 'company', -// }, -// }, -// ], -// outputSchema: {}, -// errorHandlingOptions: { -// retryOnFailure: { value: false }, -// continueOnFailure: { value: false }, -// }, -// }, -// }; - export const Default: Story = { args: { action: mockAction, @@ -158,45 +121,7 @@ export const ReadonlyMode: Story = { const numberInput = await canvas.findByPlaceholderText('Enter number'); expect(numberInput).toBeDisabled(); - const dateInput = await canvas.findByPlaceholderText('mm/dd/yyyy'); - expect(dateInput).toBeDisabled(); - const submitButton = canvas.queryByText('Submit'); expect(submitButton).not.toBeInTheDocument(); }, }; - -// TEMP_DISABLED_TEST: Temporarily commented out due to test failure -// export const CanHaveManyRecordFieldsForTheSameRecordType: Story = { -// args: { -// action: mockActionWithDuplicatedRecordFields, -// actionOptions: { -// readonly: false, -// }, -// }, -// play: async ({ canvasElement }) => { -// const canvas = within(canvasElement); - -// const recordSelects = await waitFor(() => { -// const elements = canvas.getAllByText('Select a company'); - -// expect(elements.length).toBe(2); - -// return elements; -// }); - -// for (const recordSelect of recordSelects) { -// expect(recordSelect).toBeVisible(); - -// await userEvent.click(recordSelect); - -// await waitFor(() => { -// expect( -// within(document.body).getByText('Louis Duss'), -// ).toBeVisible(); -// }); - -// await userEvent.click(canvasElement); -// } -// }, -// };