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 417bbb66f1..04e858bfc7 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 @@ -18,8 +18,14 @@ import { useHotkeysOnFocusedElement } from '@/ui/utilities/hotkey/hooks/useHotke import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside'; import { isStandaloneVariableString } from '@/workflow/utils/isStandaloneVariableString'; +import { + FloatingPortal, + autoUpdate, + flip, + offset, + useFloating, +} from '@floating-ui/react'; import { styled } from '@linaria/react'; -import { isNonEmptyString } from '@sniptt/guards'; import { useId, useRef, useState } from 'react'; import { Temporal } from 'temporal-polyfill'; import { Key } from 'ts-key-enum'; @@ -27,10 +33,8 @@ import { isDefined } from 'twenty-shared/utils'; import { themeCssVariables } from 'twenty-ui/theme-constants'; import { type Nullable } from 'twenty-ui/utilities'; -const StyledDateInputAbsoluteContainer = styled.div` - position: absolute; - top: ${themeCssVariables.spacing[1]}; -`; +const FORM_DATE_FIELD_PICKER_CLICK_OUTSIDE_ID = + 'form-date-field-picker-floating'; const StyledDateInputTextContainer = styled.div<{ isReadonly?: boolean }>` align-items: center; @@ -49,9 +53,10 @@ const StyledDateInputTextContainer = styled.div<{ isReadonly?: boolean }>` } `; -const StyledDateInputContainer = styled.div` - position: relative; - z-index: 1; +const StyledDatePickerInputWrapper = styled.div` + display: flex; + flex: 1; + min-width: 0; `; type DraftValue = @@ -98,6 +103,16 @@ export const FormDateFieldInput = ({ const datePickerWrapperRef = useRef(null); + const displayDatePicker = + draftValue.type === 'static' && draftValue.mode === 'edit'; + + const { refs, floatingStyles } = useFloating({ + open: displayDatePicker, + placement: 'bottom-start', + middleware: [offset(4), flip()], + whileElementsMounted: autoUpdate, + }); + const persistDate = (newDate: Nullable) => { if (!isDefined(newDate)) { onChange(null); @@ -109,9 +124,6 @@ export const FormDateFieldInput = ({ const { closeDropdown: closeDropdownMonthSelect } = useCloseDropdown(); const { closeDropdown: closeDropdownYearSelect } = useCloseDropdown(); - const displayDatePicker = - draftValue.type === 'static' && draftValue.mode === 'edit'; - useListenClickOutside({ refs: [datePickerWrapperRef], listenerId: 'FormDateFieldInputBase', @@ -124,6 +136,7 @@ export const FormDateFieldInput = ({ }, enabled: displayDatePicker, excludedClickOutsideIds: [ + FORM_DATE_FIELD_PICKER_CLICK_OUTSIDE_ID, MONTH_AND_YEAR_DROPDOWN_MONTH_SELECT_ID, MONTH_AND_YEAR_DROPDOWN_YEAR_SELECT_ID, ], @@ -235,9 +248,7 @@ export const FormDateFieldInput = ({ : Temporal.PlainDate.from(defaultValue).toString(); const plainDateValue = - draftValue.type === 'static' && isNonEmptyString(draftValue.value) - ? draftValue.value - : plainDateValueFromProps; + draftValue.type === 'static' ? draftValue.value : plainDateValueFromProps; const handleMaskedDatePointerDownCapture = () => { if (readonly) { @@ -256,13 +267,13 @@ export const FormDateFieldInput = ({ {label ? {label} : null} - - {draftValue.type === 'static' ? ( - <> + + + {draftValue.type === 'static' ? ( - {draftValue.mode === 'edit' && !readonly ? ( - - - - - - - - ) : null} - - ) : ( - - )} - + ) : ( + + )} + + + {draftValue.type === 'static' && + draftValue.mode === 'edit' && + !readonly ? ( + +
+ + + +
+
+ ) : null} {VariablePicker && !readonly ? ( ` align-items: center; display: flex; + flex: 1; + min-width: 0; + pointer-events: ${({ isReadonly }) => (isReadonly ? 'none' : 'auto')}; + user-select: ${({ isReadonly }) => (isReadonly ? 'none' : 'auto')}; + width: 100%; `; -const StyledDateInputContainer = styled.div` - position: relative; - z-index: 1; +const StyledDatePickerInputWrapper = styled.div` + display: flex; + flex: 1; + min-width: 0; `; type DraftValue = @@ -87,6 +97,16 @@ export const FormDateTimeFieldInput = ({ const datePickerWrapperRef = useRef(null); + const displayDatePicker = + draftValue.type === 'static' && draftValue.mode === 'edit'; + + const { refs, floatingStyles } = useFloating({ + open: displayDatePicker, + placement: 'bottom-start', + middleware: [offset(4), flip()], + whileElementsMounted: autoUpdate, + }); + const persistDate = (newDate: Nullable) => { if (!isDefined(newDate)) { onChange(null); @@ -100,9 +120,6 @@ export const FormDateTimeFieldInput = ({ const { closeDropdown: closeDropdownMonthSelect } = useCloseDropdown(); const { closeDropdown: closeDropdownYearSelect } = useCloseDropdown(); - const displayDatePicker = - draftValue.type === 'static' && draftValue.mode === 'edit'; - useListenClickOutside({ refs: [datePickerWrapperRef], listenerId: 'FormDateTimeFieldInputBase', @@ -115,6 +132,7 @@ export const FormDateTimeFieldInput = ({ }, enabled: displayDatePicker, excludedClickOutsideIds: [ + FORM_DATE_TIME_FIELD_PICKER_CLICK_OUTSIDE_ID, MONTH_AND_YEAR_DROPDOWN_MONTH_SELECT_ID, MONTH_AND_YEAR_DROPDOWN_YEAR_SELECT_ID, ], @@ -170,7 +188,7 @@ export const FormDateTimeFieldInput = ({ persistDate(newDate); }; - const handleInputFocus = () => { + const handleOpenPicker = () => { setDraftValue({ type: 'static', mode: 'edit', @@ -241,49 +259,58 @@ export const FormDateTimeFieldInput = ({ {label ? {label} : null} - - {draftValue.type === 'static' ? ( - <> - + + + {draftValue.type === 'static' ? ( + - {draftValue.mode === 'edit' ? ( - - - - - - - - ) : null} - - ) : ( - - )} - + ) : ( + + )} + + + {draftValue.type === 'static' && draftValue.mode === 'edit' ? ( + +
+ + + +
+
+ ) : null} {VariablePicker && !readonly ? ( | string; + defaultValue?: FieldRelationValue | string | null; onChange: (value: JsonValue) => void; onClear?: () => void; readonly?: boolean; diff --git a/packages/twenty-front/src/modules/object-record/record-field/ui/form-types/components/FormSingleRecordFieldChip.tsx b/packages/twenty-front/src/modules/object-record/record-field/ui/form-types/components/FormSingleRecordFieldChip.tsx index ab8dc1dd0c..030a97773a 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/ui/form-types/components/FormSingleRecordFieldChip.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/ui/form-types/components/FormSingleRecordFieldChip.tsx @@ -8,8 +8,9 @@ import { VariableChipStandalone } from '@/object-record/record-field/ui/form-typ import { type ObjectRecord } from '@/object-record/types/ObjectRecord'; import { isStandaloneVariableString } from '@/workflow/utils/isStandaloneVariableString'; import { styled } from '@linaria/react'; -import { isDefined } from 'twenty-shared/utils'; import { t } from '@lingui/core/macro'; +import { isDefined } from 'twenty-shared/utils'; +import { IconForbid } from 'twenty-ui/display'; import { themeCssVariables } from 'twenty-ui/theme-constants'; const StyledRecordChipContainer = styled.div` @@ -20,6 +21,14 @@ const StyledPlaceholderContainer = styled.div` margin: ${themeCssVariables.spacing[2]}; `; +const StyledNoRecordContainer = styled.div` + align-items: center; + color: ${themeCssVariables.font.color.primary}; + display: flex; + gap: ${themeCssVariables.spacing[1]}; + margin: ${themeCssVariables.spacing[2]}; +`; + type FormSingleRecordFieldChipProps = { draftValue: | { @@ -29,6 +38,10 @@ type FormSingleRecordFieldChipProps = { | { type: 'variable'; value: Variable; + } + | { + type: 'no-record'; + value: null; }; selectedRecord?: ObjectRecord; objectNameSingular: string; @@ -56,6 +69,15 @@ export const FormSingleRecordFieldChip = ({ ); } + if (draftValue.type === 'no-record') { + return ( + + + {t`No record`} + + ); + } + if (draftValue.type === 'static' && isDefined(selectedRecord)) { return ( diff --git a/packages/twenty-front/src/modules/object-record/record-field/ui/form-types/components/FormSingleRecordPicker.tsx b/packages/twenty-front/src/modules/object-record/record-field/ui/form-types/components/FormSingleRecordPicker.tsx index f6ef1c4b74..b66974e597 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/ui/form-types/components/FormSingleRecordPicker.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/ui/form-types/components/FormSingleRecordPicker.tsx @@ -16,7 +16,6 @@ import { useSetAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useSe import { isStandaloneVariableString } from '@/workflow/utils/isStandaloneVariableString'; import { styled } from '@linaria/react'; import { t } from '@lingui/core/macro'; -import { isNonEmptyString } from '@sniptt/guards'; import { useCallback, useContext, useId } from 'react'; import { CustomError, isDefined, isValidUuid } from 'twenty-shared/utils'; import { IconChevronDown, IconForbid } from 'twenty-ui/display'; @@ -45,11 +44,15 @@ type FormSingleRecordPickerValue = | { type: 'variable'; value: Variable; + } + | { + type: 'no-record'; + value: null; }; export type FormSingleRecordPickerProps = { label?: string; - defaultValue?: RecordId | Variable; + defaultValue?: RecordId | Variable | null; onChange: (value: RecordId | Variable | null) => void; onClear?: () => void; objectNameSingulars: string[]; @@ -70,17 +73,12 @@ export const FormSingleRecordPicker = ({ }: FormSingleRecordPickerProps) => { const { theme } = useContext(ThemeContext); - const draftValue: FormSingleRecordPickerValue = isStandaloneVariableString( - defaultValue, - ) - ? { - type: 'variable', - value: defaultValue, - } - : { - type: 'static', - value: (defaultValue as string | undefined) ?? '', - }; + const draftValue: FormSingleRecordPickerValue = + defaultValue === null + ? { type: 'no-record', value: null } + : isStandaloneVariableString(defaultValue) + ? { type: 'variable', value: defaultValue } + : { type: 'static', value: (defaultValue as string | undefined) ?? '' }; if (objectNameSingulars.length === 0) { throw new CustomError( @@ -117,13 +115,22 @@ export const FormSingleRecordPicker = ({ const handleMorphItemSelected = ( selectedMorphItem: RecordPickerPickableMorphItem | null | undefined, ) => { - if (!isNonEmptyString(selectedMorphItem?.recordId)) { - onClear?.(); + if (!isDefined(selectedMorphItem) || selectedMorphItem === null) { + if (defaultValue === null) { + onClear?.(); + } else { + onChange(null); + } + closeDropdown(dropdownId); return; } - onChange(selectedMorphItem.recordId); + if (defaultValue === selectedMorphItem.recordId) { + onClear?.(); + } else { + onChange(selectedMorphItem.recordId); + } closeDropdown(dropdownId); }; @@ -142,8 +149,13 @@ export const FormSingleRecordPicker = ({ ); const handleOpenDropdown = () => { + if (defaultValue === null) { + setSingleRecordPickerSelectedId(undefined); + return; + } + if ( - isDefined(draftValue?.value) && + isDefined(draftValue.value) && !isStandaloneVariableString(draftValue.value) ) { setSingleRecordPickerSelectedId(draftValue.value); @@ -208,7 +220,7 @@ export const FormSingleRecordPicker = ({ focusId={dropdownId} componentInstanceId={dropdownId} EmptyIcon={IconForbid} - emptyLabel={t`No records`} + emptyLabel={t`No record`} onCancel={() => closeDropdown(dropdownId)} onMorphItemSelected={handleMorphItemSelected} objectNameSingulars={objectNameSingulars} diff --git a/packages/twenty-front/src/modules/object-record/record-update-multiple/components/UpdateMultipleRecordsForm.tsx b/packages/twenty-front/src/modules/object-record/record-update-multiple/components/UpdateMultipleRecordsForm.tsx index 06d2033561..cc4e7f3000 100644 --- a/packages/twenty-front/src/modules/object-record/record-update-multiple/components/UpdateMultipleRecordsForm.tsx +++ b/packages/twenty-front/src/modules/object-record/record-update-multiple/components/UpdateMultipleRecordsForm.tsx @@ -65,7 +65,9 @@ export const UpdateMultipleRecordsForm = ({ const value = values[fieldNameOrRelationIdName]; const handleValueChange = (newValue: any) => { - if (isUpdateRecordValueEmpty(newValue)) { + if (newValue === null) { + onChange(fieldNameOrRelationIdName, null); + } else if (isUpdateRecordValueEmpty(newValue)) { onChange(fieldNameOrRelationIdName, undefined); } else { onChange(fieldNameOrRelationIdName, newValue); @@ -79,6 +81,7 @@ export const UpdateMultipleRecordsForm = ({ field={fieldDefinition} defaultValue={value} onChange={handleValueChange} + onClear={() => onChange(fieldNameOrRelationIdName, undefined)} /> ); })} diff --git a/packages/twenty-front/src/modules/ui/input/components/internal/date/components/DatePickerInput.tsx b/packages/twenty-front/src/modules/ui/input/components/internal/date/components/DatePickerInput.tsx index f9479b77ae..26304b43b8 100644 --- a/packages/twenty-front/src/modules/ui/input/components/internal/date/components/DatePickerInput.tsx +++ b/packages/twenty-front/src/modules/ui/input/components/internal/date/components/DatePickerInput.tsx @@ -100,9 +100,13 @@ export const DatePickerInput = ({ ); useEffect(() => { - if (isDefined(date) && internalDate !== date) { + if (internalDate !== date) { setInternalDate(date); - setValue(parsePlainDateToDateInputString(date)); + if (isDefined(date)) { + setValue(parsePlainDateToDateInputString(date)); + } else { + setValue(''); + } } }, [date, internalDate, parsePlainDateToDateInputString, setValue]); 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 ec806995dd..b62d1ee55f 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 @@ -138,6 +138,7 @@ export const DateTimePickerInput = ({ setInternalDate(date); if (!isDefined(date)) { + setValue(''); return; }