Phone-onclickoutside (#11350)

Fixes : For phones I have to press "enter" to validate my changes but
for other fields it's saved automatically when I leave the cell

Bug related to onClickOutside on the MultiItemFieldMenuItem component
that shows phones (but also emails...)

Seen with @bonapara : we keep a consitent behaviour meaning
- saving input on click outside when primary item is being edited
- not saving input on click outside when other items are being edited



Fixes https://github.com/twentyhq/twenty/issues/11246
This commit is contained in:
Guillim
2025-04-03 09:57:02 +02:00
committed by GitHub
parent bea75b9532
commit 8abec309e0
25 changed files with 82 additions and 69 deletions

View File

@ -73,10 +73,7 @@ export const FieldInput = ({
) : isFieldRelationFromManyObjects(fieldDefinition) ? (
<RelationFromManyFieldInput onSubmit={onSubmit} />
) : isFieldPhones(fieldDefinition) ? (
<PhonesFieldInput
onCancel={onCancel}
onClickOutside={(event) => onClickOutside?.(() => {}, event)}
/>
<PhonesFieldInput onCancel={onCancel} onClickOutside={onClickOutside} />
) : isFieldText(fieldDefinition) ? (
<TextFieldInput
onEnter={onEnter}

View File

@ -2,12 +2,11 @@ import { useAddressField } from '@/object-record/record-field/meta-types/hooks/u
import { FieldAddressDraftValue } from '@/object-record/record-field/types/FieldInputDraftValue';
import { AddressInput } from '@/ui/field/input/components/AddressInput';
import { usePersistField } from '../../../hooks/usePersistField';
import {
FieldInputClickOutsideEvent,
FieldInputEvent,
} from './DateTimeFieldInput';
} from '@/object-record/record-field/types/FieldInputEvent';
import { usePersistField } from '../../../hooks/usePersistField';
export type AddressFieldInputProps = {
onClickOutside?: FieldInputClickOutsideEvent;

View File

@ -27,7 +27,10 @@ export const ArrayFieldInput = ({
items={arrayItems}
onPersist={persistArrayField}
onCancel={onCancel}
onClickOutside={onClickOutside}
onClickOutside={(persist, event) => {
onClickOutside?.(event);
persist();
}}
placeholder="Enter value"
fieldMetadataType={FieldMetadataType.ARRAY}
renderItem={({ value, index, handleEdit, handleDelete }) => (

View File

@ -1,10 +1,9 @@
import { BooleanInput } from '@/ui/field/input/components/BooleanInput';
import { FieldInputEvent } from '@/object-record/record-field/types/FieldInputEvent';
import { usePersistField } from '../../../hooks/usePersistField';
import { useBooleanField } from '../../hooks/useBooleanField';
import { FieldInputEvent } from './DateTimeFieldInput';
export type BooleanFieldInputProps = {
onSubmit?: FieldInputEvent;
readonly?: boolean;

View File

@ -6,11 +6,11 @@ import { CurrencyInput } from '@/ui/field/input/components/CurrencyInput';
import { useCurrencyField } from '../../hooks/useCurrencyField';
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
import {
FieldInputClickOutsideEvent,
FieldInputEvent,
} from './DateTimeFieldInput';
} from '@/object-record/record-field/types/FieldInputEvent';
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
type CurrencyFieldInputProps = {
onClickOutside?: FieldInputClickOutsideEvent;

View File

@ -3,9 +3,9 @@ import { Nullable } from 'twenty-ui';
import { useDateField } from '@/object-record/record-field/meta-types/hooks/useDateField';
import { DateInput } from '@/ui/field/input/components/DateInput';
import { FieldInputClickOutsideEvent } from '@/object-record/record-field/meta-types/input/components/DateTimeFieldInput';
import { usePersistField } from '../../../hooks/usePersistField';
import { FieldInputClickOutsideEvent } from '@/object-record/record-field/types/FieldInputEvent';
import { isDefined } from 'twenty-shared/utils';
import { usePersistField } from '../../../hooks/usePersistField';
type FieldInputEvent = (persist: () => void) => void;

View File

@ -2,15 +2,12 @@ import { Nullable } from 'twenty-ui';
import { DateInput } from '@/ui/field/input/components/DateInput';
import { FieldInputEvent } from '@/object-record/record-field/meta-types/input/components/NumberFieldInput';
import { FieldInputClickOutsideEvent } from '@/object-record/record-field/types/FieldInputEvent';
import { usePersistField } from '../../../hooks/usePersistField';
import { useDateTimeField } from '../../hooks/useDateTimeField';
export type FieldInputEvent = (persist: () => void) => void;
export type FieldInputClickOutsideEvent = (
persist: () => void,
event: MouseEvent | TouchEvent,
) => void;
export type DateTimeFieldInputProps = {
onClickOutside?: FieldInputClickOutsideEvent;
onEnter?: FieldInputEvent;

View File

@ -4,9 +4,9 @@ import { recordFieldInputIsFieldInErrorComponentState } from '@/object-record/re
import { emailSchema } from '@/object-record/record-field/validation-schemas/emailSchema';
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
import { useCallback, useMemo } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { FieldMetadataType } from '~/generated-metadata/graphql';
import { MultiItemFieldInput } from './MultiItemFieldInput';
import { isDefined } from 'twenty-shared/utils';
type EmailsFieldInputProps = {
onCancel?: () => void;
@ -59,7 +59,10 @@ export const EmailsFieldInput = ({
items={emails}
onPersist={handlePersistEmails}
onCancel={onCancel}
onClickOutside={onClickOutside}
onClickOutside={(persist, event) => {
onClickOutside?.(event);
persist();
}}
placeholder="Email"
fieldMetadataType={FieldMetadataType.EMAILS}
validateInput={validateInput}

View File

@ -8,7 +8,7 @@ import { isDoubleTextFieldEmpty } from '@/object-record/record-field/meta-types/
import {
FieldInputClickOutsideEvent,
FieldInputEvent,
} from './DateTimeFieldInput';
} from '@/object-record/record-field/types/FieldInputEvent';
type FullNameFieldInputProps = {
onClickOutside?: FieldInputClickOutsideEvent;

View File

@ -62,7 +62,10 @@ export const LinksFieldInput = ({
items={links}
onPersist={handlePersistLinks}
onCancel={onCancel}
onClickOutside={onClickOutside}
onClickOutside={(persist, event) => {
onClickOutside?.(event);
persist();
}}
placeholder="URL"
fieldMetadataType={FieldMetadataType.LINKS}
validateInput={(input) => ({

View File

@ -66,7 +66,7 @@ const StyledErrorDiv = styled.div`
type HTMLInputProps = InputHTMLAttributes<HTMLInputElement>;
export type MultiItemBaseInputProps = HTMLInputProps & {
export type MultiItemBaseInputProps = Omit<HTMLInputProps, 'onChange'> & {
hotkeyScope?: string;
onClickOutside?: () => void;
onEnter?: () => void;
@ -76,13 +76,14 @@ export type MultiItemBaseInputProps = HTMLInputProps & {
rightComponent?: ReactNode;
renderInput?: (props: {
value: HTMLInputProps['value'];
onChange: HTMLInputProps['onChange'];
onChange: (value: string) => void;
autoFocus: HTMLInputProps['autoFocus'];
placeholder: HTMLInputProps['placeholder'];
}) => React.ReactNode;
error?: string | null;
hasError?: boolean;
hasItem: boolean;
onChange: (value: string) => void;
};
export const MultiItemBaseInput = forwardRef<
@ -140,7 +141,7 @@ export const MultiItemBaseInput = forwardRef<
autoFocus={autoFocus}
value={value}
placeholder={placeholder}
onChange={onChange}
onChange={(event) => onChange(event.target.value)}
ref={combinedRef}
withRightComponent={!!rightComponent}
hasItem={hasItem}

View File

@ -6,6 +6,7 @@ import {
MultiItemBaseInput,
MultiItemBaseInputProps,
} from '@/object-record/record-field/meta-types/input/components/MultiItemBaseInput';
import { FieldInputClickOutsideEvent } from '@/object-record/record-field/types/FieldInputEvent';
import { PhoneRecord } from '@/object-record/record-field/types/FieldMetadata';
import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
@ -35,7 +36,7 @@ type MultiItemFieldInputProps<T> = {
newItemLabel?: string;
fieldMetadataType: FieldMetadataType;
renderInput?: MultiItemBaseInputProps['renderInput'];
onClickOutside?: (event: MouseEvent | TouchEvent) => void;
onClickOutside?: FieldInputClickOutsideEvent;
onError?: (hasError: boolean, values: any[]) => void;
};
@ -64,7 +65,13 @@ export const MultiItemFieldInput = <T,>({
useListenClickOutside({
refs: [containerRef],
callback: (event) => {
onClickOutside?.(event);
const isEditing = inputValue !== '';
const isPrimaryItem = items.length === 0;
if (isEditing && isPrimaryItem) {
handleSubmitInput();
}
onClickOutside?.(() => {}, event);
},
listenerId: hotkeyScope,
});
@ -195,22 +202,13 @@ export const MultiItemFieldInput = <T,>({
value={inputValue}
hotkeyScope={hotkeyScope}
hasError={!errorData.isValid}
renderInput={
renderInput
? (props) =>
renderInput({
...props,
onChange: (newValue) =>
setInputValue(newValue as unknown as string),
})
: undefined
}
renderInput={renderInput}
onEscape={handleDropdownClose}
onChange={(event) =>
handleOnChange(
turnIntoEmptyStringIfWhitespacesOnly(event.target.value),
)
}
onChange={(value) => {
value
? handleOnChange(turnIntoEmptyStringIfWhitespacesOnly(value))
: handleOnChange('');
}}
onEnter={handleSubmitInput}
hasItem={!!items.length}
rightComponent={

View File

@ -1,6 +1,6 @@
import { TextInput } from '@/ui/field/input/components/TextInput';
import { FieldInputClickOutsideEvent } from '@/object-record/record-field/meta-types/input/components/DateTimeFieldInput';
import { FieldInputClickOutsideEvent } from '@/object-record/record-field/types/FieldInputEvent';
import { FieldInputContainer } from '@/ui/field/input/components/FieldInputContainer';
import { useNumberField } from '../../hooks/useNumberField';

View File

@ -9,6 +9,7 @@ import { TEXT_INPUT_STYLE } from 'twenty-ui';
import { MultiItemFieldInput } from './MultiItemFieldInput';
import { createPhonesFromFieldValue } from '@/object-record/record-field/meta-types/input/utils/phonesUtils';
import { FieldInputClickOutsideEvent } from '@/object-record/record-field/types/FieldInputEvent';
import { PhoneCountryPickerDropdownButton } from '@/ui/input/components/internal/phone/components/PhoneCountryPickerDropdownButton';
import { css } from '@emotion/react';
import { FieldMetadataType } from '~/generated-metadata/graphql';
@ -61,7 +62,7 @@ const StyledCustomPhoneInput = styled(ReactPhoneNumberInput)`
type PhonesFieldInputProps = {
onCancel?: () => void;
onClickOutside?: (event: MouseEvent | TouchEvent) => void;
onClickOutside?: FieldInputClickOutsideEvent;
};
export const PhonesFieldInput = ({

View File

@ -1,11 +1,10 @@
import { FieldRatingValue } from '@/object-record/record-field/types/FieldMetadata';
import { RatingInput } from '@/ui/field/input/components/RatingInput';
import { FieldInputEvent } from '@/object-record/record-field/types/FieldInputEvent';
import { usePersistField } from '../../../hooks/usePersistField';
import { useRatingField } from '../../hooks/useRatingField';
import { FieldInputEvent } from './DateTimeFieldInput';
export type RatingFieldInputProps = {
onSubmit?: FieldInputEvent;
readonly?: boolean;

View File

@ -1,11 +1,10 @@
import { TextAreaInput } from '@/ui/field/input/components/TextAreaInput';
import { useJsonField } from '../../hooks/useJsonField';
import {
FieldInputClickOutsideEvent,
FieldInputEvent,
} from './DateTimeFieldInput';
} from '@/object-record/record-field/types/FieldInputEvent';
import { useJsonField } from '../../hooks/useJsonField';
type RawJsonFieldInputProps = {
onClickOutside?: FieldInputClickOutsideEvent;

View File

@ -5,12 +5,12 @@ import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadata
import { useAddNewRecordAndOpenRightDrawer } from '@/object-record/record-field/meta-types/input/hooks/useAddNewRecordAndOpenRightDrawer';
import { recordFieldInputLayoutDirectionComponentState } from '@/object-record/record-field/states/recordFieldInputLayoutDirectionComponentState';
import { recordFieldInputLayoutDirectionLoadingComponentState } from '@/object-record/record-field/states/recordFieldInputLayoutDirectionLoadingComponentState';
import { FieldInputEvent } from '@/object-record/record-field/types/FieldInputEvent';
import { SingleRecordPicker } from '@/object-record/record-picker/single-record-picker/components/SingleRecordPicker';
import { SingleRecordPickerRecord } from '@/object-record/record-picker/single-record-picker/types/SingleRecordPickerRecord';
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { IconForbid } from 'twenty-ui';
import { FieldInputEvent } from './DateTimeFieldInput';
export type RelationToOneFieldInputProps = {
onSubmit?: FieldInputEvent;

View File

@ -1,8 +1,8 @@
import { BLOCK_SCHEMA } from '@/activities/blocks/constants/Schema';
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
import { useRichTextField } from '@/object-record/record-field/meta-types/hooks/useRichTextField';
import { FieldInputClickOutsideEvent } from '@/object-record/record-field/meta-types/input/components/DateTimeFieldInput';
import { useRegisterInputEvents } from '@/object-record/record-field/meta-types/input/hooks/useRegisterInputEvents';
import { FieldInputClickOutsideEvent } from '@/object-record/record-field/types/FieldInputEvent';
import { BlockEditor } from '@/ui/input/editor/components/BlockEditor';
import { BlockEditorComponentInstanceContext } from '@/ui/input/editor/contexts/BlockEditorCompoponeInstanceContext';
import { PartialBlock } from '@blocknote/core';

View File

@ -3,12 +3,12 @@ import { TextAreaInput } from '@/ui/field/input/components/TextAreaInput';
import { usePersistField } from '../../../hooks/usePersistField';
import { useTextField } from '../../hooks/useTextField';
import { FieldInputContainer } from '@/ui/field/input/components/FieldInputContainer';
import { turnIntoUndefinedIfWhitespacesOnly } from '~/utils/string/turnIntoUndefinedIfWhitespacesOnly';
import {
FieldInputClickOutsideEvent,
FieldInputEvent,
} from './DateTimeFieldInput';
} from '@/object-record/record-field/types/FieldInputEvent';
import { FieldInputContainer } from '@/ui/field/input/components/FieldInputContainer';
import { turnIntoUndefinedIfWhitespacesOnly } from '~/utils/string/turnIntoUndefinedIfWhitespacesOnly';
export type TextFieldInputProps = {
onClickOutside?: FieldInputClickOutsideEvent;

View File

@ -1 +1,6 @@
export type FieldInputEvent = (persist: () => void) => void;
export type FieldInputClickOutsideEvent = (
persist: () => void,
event: MouseEvent | TouchEvent,
) => void;

View File

@ -6,12 +6,15 @@ import { FieldInput } from '@/object-record/record-field/components/FieldInput';
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
import { FieldFocusContextProvider } from '@/object-record/record-field/contexts/FieldFocusContextProvider';
import { useGetButtonIcon } from '@/object-record/record-field/hooks/useGetButtonIcon';
import { FieldInputEvent } from '@/object-record/record-field/types/FieldInputEvent';
import {
FieldInputClickOutsideEvent,
FieldInputEvent,
} from '@/object-record/record-field/types/FieldInputEvent';
import { useIsFieldInputOnly } from '@/object-record/record-field/hooks/useIsFieldInputOnly';
import { useIsFieldValueReadOnly } from '@/object-record/record-field/hooks/useIsFieldValueReadOnly';
import { useOpenFieldInputEditMode } from '@/object-record/record-field/hooks/useOpenFieldInputEditMode';
import { FieldInputClickOutsideEvent } from '@/object-record/record-field/meta-types/input/components/DateTimeFieldInput';
import { FieldDefinition } from '@/object-record/record-field/types/FieldDefinition';
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
import { isFieldRelation } from '@/object-record/record-field/types/guards/isFieldRelation';

View File

@ -1,7 +1,9 @@
import { FieldInput } from '@/object-record/record-field/components/FieldInput';
import { useIsFieldValueReadOnly } from '@/object-record/record-field/hooks/useIsFieldValueReadOnly';
import { FieldInputClickOutsideEvent } from '@/object-record/record-field/meta-types/input/components/DateTimeFieldInput';
import { FieldInputEvent } from '@/object-record/record-field/types/FieldInputEvent';
import {
FieldInputClickOutsideEvent,
FieldInputEvent,
} from '@/object-record/record-field/types/FieldInputEvent';
import { useRecordTableBodyContextOrThrow } from '@/object-record/record-table/contexts/RecordTableBodyContext';
import { TableHotkeyScope } from '@/object-record/record-table/types/TableHotkeyScope';
import { currentHotkeyScopeState } from '@/ui/utilities/hotkey/states/internal/currentHotkeyScopeState';

View File

@ -3,11 +3,13 @@ import { useContext } from 'react';
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
import { FieldFocusContextProvider } from '@/object-record/record-field/contexts/FieldFocusContextProvider';
import { useIsFieldInputOnly } from '@/object-record/record-field/hooks/useIsFieldInputOnly';
import { FieldInputEvent } from '@/object-record/record-field/types/FieldInputEvent';
import {
FieldInputClickOutsideEvent,
FieldInputEvent,
} from '@/object-record/record-field/types/FieldInputEvent';
import { useInlineCell } from '../../record-inline-cell/hooks/useInlineCell';
import { FieldInputClickOutsideEvent } from '@/object-record/record-field/meta-types/input/components/DateTimeFieldInput';
import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext';
import { RecordTitleCellContainer } from '@/object-record/record-title-cell/components/RecordTitleCellContainer';
import {

View File

@ -1,12 +1,14 @@
import { usePersistField } from '@/object-record/record-field/hooks/usePersistField';
import { useTextField } from '@/object-record/record-field/meta-types/hooks/useTextField';
import { FieldInputClickOutsideEvent } from '@/object-record/record-field/meta-types/input/components/DateTimeFieldInput';
import { useRegisterInputEvents } from '@/object-record/record-field/meta-types/input/hooks/useRegisterInputEvents';
import { FieldInputEvent } from '@/object-record/record-field/types/FieldInputEvent';
import {
FieldInputClickOutsideEvent,
FieldInputEvent,
} from '@/object-record/record-field/types/FieldInputEvent';
import { TextInputV2 } from '@/ui/input/components/TextInputV2';
import { useRef } from 'react';
import { turnIntoUndefinedIfWhitespacesOnly } from '~/utils/string/turnIntoUndefinedIfWhitespacesOnly';
import { isDefined } from 'twenty-shared/utils';
import { turnIntoUndefinedIfWhitespacesOnly } from '~/utils/string/turnIntoUndefinedIfWhitespacesOnly';
type RecordTitleCellTextFieldInputProps = {
onClickOutside?: FieldInputClickOutsideEvent;

View File

@ -1,12 +1,12 @@
import { useFullNameField } from '@/object-record/record-field/meta-types/hooks/useFullNameField';
import {
FieldInputClickOutsideEvent,
FieldInputEvent,
} from '@/object-record/record-field/meta-types/input/components/DateTimeFieldInput';
import { FIRST_NAME_PLACEHOLDER_WITH_SPECIAL_CHARACTER_TO_AVOID_PASSWORD_MANAGERS } from '@/object-record/record-field/meta-types/input/constants/FirstNamePlaceholder';
import { LAST_NAME_PLACEHOLDER_WITH_SPECIAL_CHARACTER_TO_AVOID_PASSWORD_MANAGERS } from '@/object-record/record-field/meta-types/input/constants/LastNamePlaceholder';
import { isDoubleTextFieldEmpty } from '@/object-record/record-field/meta-types/input/utils/isDoubleTextFieldEmpty';
import { FieldDoubleText } from '@/object-record/record-field/types/FieldDoubleText';
import {
FieldInputClickOutsideEvent,
FieldInputEvent,
} from '@/object-record/record-field/types/FieldInputEvent';
import { RecordTitleDoubleTextInput } from './RecordTitleDoubleTextInput';
type RecordTitleFullNameFieldInputProps = {