diff --git a/packages/twenty-front/src/modules/object-record/record-field/ui/hooks/useRecordFieldInput.ts b/packages/twenty-front/src/modules/object-record/record-field/ui/hooks/useRecordFieldInput.ts index e63b2363e0d..3e7d1b21172 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/ui/hooks/useRecordFieldInput.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/ui/hooks/useRecordFieldInput.ts @@ -7,19 +7,6 @@ export const useRecordFieldInput = () => { const recordFieldInputDraftValueCallbackState = useRecoilComponentCallbackState(recordFieldInputDraftValueComponentState); - const getLatestDraftValue = useRecoilCallback( - ({ snapshot }) => - (instanceId: string) => - snapshot - .getLoadable( - recordFieldInputDraftValueComponentState.atomFamily({ - instanceId, - }), - ) - .getValue() as FieldInputDraftValue, - [], - ); - const setDraftValue = useRecoilCallback( ({ set }) => (newValue: unknown) => { @@ -43,7 +30,6 @@ export const useRecordFieldInput = () => { }; return { - getLatestDraftValue, setDraftValue, isDraftValueEmpty, }; diff --git a/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/hooks/useArrayField.ts b/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/hooks/useArrayField.ts index 61020463a9f..45df25cc059 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/hooks/useArrayField.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/hooks/useArrayField.ts @@ -24,8 +24,7 @@ export const useArrayField = () => { }), ); - const { getLatestDraftValue, setDraftValue } = - useRecordFieldInput(); + const { setDraftValue } = useRecordFieldInput(); const draftValue = useRecoilComponentValue( recordFieldInputDraftValueComponentState, @@ -35,7 +34,6 @@ export const useArrayField = () => { fieldValue, fieldDefinition, draftValue, - getLatestDraftValue, setFieldValue, setDraftValue, }; diff --git a/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/hooks/useEmailsField.ts b/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/hooks/useEmailsField.ts index b5e217b2e37..d680c984620 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/hooks/useEmailsField.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/hooks/useEmailsField.ts @@ -26,8 +26,7 @@ export const useEmailsField = () => { }), ); - const { getLatestDraftValue, setDraftValue } = - useRecordFieldInput(); + const { setDraftValue } = useRecordFieldInput(); const draftValue = useRecoilComponentValue( recordFieldInputDraftValueComponentState, @@ -37,7 +36,6 @@ export const useEmailsField = () => { fieldDefinition, fieldValue, draftValue, - getLatestDraftValue, setDraftValue, setFieldValue, }; diff --git a/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/hooks/useLinksField.ts b/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/hooks/useLinksField.ts index 97444c19c59..17f09c75eee 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/hooks/useLinksField.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/hooks/useLinksField.ts @@ -26,8 +26,7 @@ export const useLinksField = () => { }), ); - const { getLatestDraftValue, setDraftValue } = - useRecordFieldInput(); + const { setDraftValue } = useRecordFieldInput(); const draftValue = useRecoilComponentValue( recordFieldInputDraftValueComponentState, @@ -37,7 +36,6 @@ export const useLinksField = () => { fieldDefinition, fieldValue, draftValue, - getLatestDraftValue, setDraftValue, setFieldValue, }; diff --git a/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/hooks/usePhonesField.ts b/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/hooks/usePhonesField.ts index f0dbe220bfb..8158d3441b8 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/hooks/usePhonesField.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/hooks/usePhonesField.ts @@ -26,8 +26,7 @@ export const usePhonesField = () => { }), ); - const { getLatestDraftValue, setDraftValue } = - useRecordFieldInput(); + const { setDraftValue } = useRecordFieldInput(); const draftValue = useRecoilComponentValue( recordFieldInputDraftValueComponentState, @@ -37,7 +36,6 @@ export const usePhonesField = () => { fieldDefinition, fieldValue, draftValue, - getLatestDraftValue, setDraftValue, setFieldValue, }; diff --git a/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/input/components/ArrayFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/input/components/ArrayFieldInput.tsx index 205842967b3..7a5696edebe 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/input/components/ArrayFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/input/components/ArrayFieldInput.tsx @@ -2,35 +2,38 @@ import { FieldInputEventContext } from '@/object-record/record-field/ui/contexts import { useArrayField } from '@/object-record/record-field/ui/meta-types/hooks/useArrayField'; import { ArrayFieldMenuItem } from '@/object-record/record-field/ui/meta-types/input/components/ArrayFieldMenuItem'; import { MultiItemFieldInput } from '@/object-record/record-field/ui/meta-types/input/components/MultiItemFieldInput'; -import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/ui/states/contexts/RecordFieldComponentInstanceContext'; +import { MULTI_ITEM_FIELD_INPUT_DROPDOWN_ID_PREFIX } from '@/object-record/record-field/ui/meta-types/input/constants/MultiItemFieldInputDropdownClickOutsideId'; import { arraySchema } from '@/object-record/record-field/ui/types/guards/isFieldArrayValue'; -import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow'; import { useContext, useMemo } from 'react'; import { MULTI_ITEM_FIELD_DEFAULT_MAX_VALUES } from 'twenty-shared/constants'; import { isDefined } from 'twenty-shared/utils'; import { FieldMetadataType } from '~/generated-metadata/graphql'; export const ArrayFieldInput = () => { - const { getLatestDraftValue, setDraftValue, draftValue, fieldDefinition } = - useArrayField(); + const { setDraftValue, draftValue, fieldDefinition } = useArrayField(); - const { onEscape, onClickOutside } = useContext(FieldInputEventContext); - const instanceId = useAvailableComponentInstanceIdOrThrow( - RecordFieldComponentInstanceContext, + const { onEscape, onClickOutside, onEnter } = useContext( + FieldInputEventContext, ); const arrayItems = useMemo>( () => (Array.isArray(draftValue) ? draftValue : []), [draftValue], ); + const parseStringArrayToArrayValue = (arrayItems: string[]) => { + const parseResponse = arraySchema.safeParse(arrayItems); + if (parseResponse.success) { + return parseResponse.data; + } + }; const handleChange = (newValue: any[]) => { if (!isDefined(newValue)) setDraftValue(null); - const parseResponse = arraySchema.safeParse(newValue); + const nextValue = parseStringArrayToArrayValue(newValue); - if (parseResponse.success) { - setDraftValue(parseResponse.data); + if (isDefined(nextValue)) { + setDraftValue(nextValue); } }; @@ -38,12 +41,15 @@ export const ArrayFieldInput = () => { _newValue: any, event: MouseEvent | TouchEvent, ) => { - const latestDraftValue = getLatestDraftValue(instanceId); - onClickOutside?.({ newValue: latestDraftValue, event }); + onClickOutside?.({ newValue: draftValue, event }); }; - const handleEscape = (_newValue: any) => { - onEscape?.({ newValue: draftValue }); + const handleEscape = (newValue: string[]) => { + onEscape?.({ newValue: parseStringArrayToArrayValue(newValue) }); + }; + + const handleEnter = (newValue: string[]) => { + onEnter?.({ newValue: parseStringArrayToArrayValue(newValue) }); }; const maxNumberOfValues = @@ -55,6 +61,7 @@ export const ArrayFieldInput = () => { newItemLabel="Add Item" items={arrayItems} onChange={handleChange} + onEnter={handleEnter} onEscape={handleEscape} onClickOutside={handleClickOutside} placeholder="Enter value" @@ -62,7 +69,7 @@ export const ArrayFieldInput = () => { renderItem={({ value, index, handleEdit, handleDelete }) => ( { - const { getLatestDraftValue, setDraftValue, draftValue, fieldDefinition } = - useEmailsField(); + const { setDraftValue, draftValue, fieldDefinition } = useEmailsField(); const { copyToClipboard } = useCopyToClipboard(); const { t } = useLingui(); - const { onEscape, onClickOutside } = useContext(FieldInputEventContext); - const instanceId = useAvailableComponentInstanceIdOrThrow( - RecordFieldComponentInstanceContext, + const { onEscape, onClickOutside, onEnter } = useContext( + FieldInputEventContext, ); const emails = useMemo( @@ -35,10 +33,10 @@ export const EmailsFieldInput = () => { [draftValue?.primaryEmail, draftValue?.additionalEmails], ); - const handleChange = (updatedEmails: string[]) => { - const [nextPrimaryEmail, ...nextAdditionalEmails] = updatedEmails; + const parseStringArrayToEmailsValue = (emails: string[]) => { + const [nextPrimaryEmail, ...nextAdditionalEmails] = emails; - const nextValue = { + const nextValue: FieldEmailsValue = { primaryEmail: nextPrimaryEmail ?? '', additionalEmails: nextAdditionalEmails, }; @@ -46,7 +44,15 @@ export const EmailsFieldInput = () => { const parseResponse = emailsSchema.safeParse(nextValue); if (parseResponse.success) { - setDraftValue(parseResponse.data); + return parseResponse.data; + } + }; + + const handleChange = (updatedEmails: string[]) => { + const nextValue = parseStringArrayToEmailsValue(updatedEmails); + + if (isDefined(nextValue)) { + setDraftValue(nextValue); } }; @@ -75,15 +81,21 @@ export const EmailsFieldInput = () => { }; const handleClickOutside = ( - _newValue: any, + updatedEmails: string[], event: MouseEvent | TouchEvent, ) => { - const latestDraftValue = getLatestDraftValue(instanceId); - onClickOutside?.({ newValue: latestDraftValue, event }); + onClickOutside?.({ + newValue: parseStringArrayToEmailsValue(updatedEmails), + event, + }); }; - const handleEscape = (_newValue: any) => { - onEscape?.({ newValue: draftValue }); + const handleEscape = (updatedEmails: string[]) => { + onEscape?.({ newValue: parseStringArrayToEmailsValue(updatedEmails) }); + }; + + const handleEnter = (updatedEmails: string[]) => { + onEnter?.({ newValue: parseStringArrayToEmailsValue(updatedEmails) }); }; const maxNumberOfValues = @@ -94,6 +106,7 @@ export const EmailsFieldInput = () => { { }) => ( { - const { getLatestDraftValue, draftValue, fieldDefinition, setDraftValue } = - useLinksField(); - const instanceId = useAvailableComponentInstanceIdOrThrow( - RecordFieldComponentInstanceContext, - ); +type LinkRecord = { + url: string | null; + label: string | null; +}; - const { onEscape, onClickOutside } = useContext(FieldInputEventContext); +export const LinksFieldInput = () => { + const { draftValue, fieldDefinition, setDraftValue } = useLinksField(); + + const { onEscape, onClickOutside, onEnter } = useContext( + FieldInputEventContext, + ); const links = useMemo<{ url: string; label: string | null }[]>( () => getFieldLinkDefinedLinks(draftValue), [draftValue], ); - const handleChange = ( - updatedLinks: { url: string | null; label: string | null }[], - ) => { - const nextPrimaryLink = updatedLinks.at(0); - const nextSecondaryLinks = updatedLinks.slice(1); - - const nextValue = { + const parseArrayToLinksValue = (links: LinkRecord[]) => { + const nextPrimaryLink = links.at(0); + const nextSecondaryLinks = links.slice(1); + const nextValue: FieldLinksValue = { primaryLinkUrl: nextPrimaryLink?.url ?? null, primaryLinkLabel: nextPrimaryLink?.label ?? null, secondaryLinks: nextSecondaryLinks, }; - const parseResponse = linksSchema.safeParse(nextValue); - if (parseResponse.success) { - setDraftValue(parseResponse.data); + return parseResponse.data; + } + }; + + const handleChange = ( + updatedLinks: { url: string | null; label: string | null }[], + ) => { + const nextValue = parseArrayToLinksValue(updatedLinks); + + if (isDefined(nextValue)) { + setDraftValue(nextValue); } }; @@ -58,15 +66,18 @@ export const LinksFieldInput = () => { }; const handleClickOutside = ( - _newValue: any, + updatedLinks: LinkRecord[], event: MouseEvent | TouchEvent, ) => { - const latestDraftValue = getLatestDraftValue(instanceId); - onClickOutside?.({ newValue: latestDraftValue, event }); + onClickOutside?.({ newValue: parseArrayToLinksValue(updatedLinks), event }); }; - const handleEscape = (_newValue: any) => { - onEscape?.({ newValue: draftValue }); + const handleEscape = (updatedLinks: LinkRecord[]) => { + onEscape?.({ newValue: parseArrayToLinksValue(updatedLinks) }); + }; + + const handleEnter = (updatedLinks: LinkRecord[]) => { + onEnter?.({ newValue: parseArrayToLinksValue(updatedLinks) }); }; const maxNumberOfValues = @@ -78,6 +89,7 @@ export const LinksFieldInput = () => { items={links} onChange={handleChange} onEscape={handleEscape} + onEnter={handleEnter} onClickOutside={handleClickOutside} placeholder="URL" fieldMetadataType={FieldMetadataType.LINKS} @@ -96,7 +108,7 @@ export const LinksFieldInput = () => { }) => ( = { items: T[]; onChange: (newItemsValue: T[]) => void; - onEscape?: (newItemsValue: T[]) => void; + onEscape: (newItemsValue: T[]) => void; + onEnter: (newItemsValue: T[]) => void; + onClickOutside: (newItemsValue: T[], event: MouseEvent | TouchEvent) => void; + onError?: (hasError: boolean, values: any[]) => void; placeholder: string; validateInput?: (input: string) => { isValid: boolean; errorMessage: string }; formatInput?: (input: string) => T; @@ -39,8 +46,6 @@ type MultiItemFieldInputProps = { newItemLabel?: string; fieldMetadataType: FieldMetadataType; renderInput?: MultiItemBaseInputProps['renderInput']; - onClickOutside?: (newItemsValue: T[], event: MouseEvent | TouchEvent) => void; - onError?: (hasError: boolean, values: any[]) => void; maxItemCount?: number; }; @@ -50,6 +55,8 @@ export const MultiItemFieldInput = ({ items, onChange, onEscape, + onEnter, + onError, placeholder, validateInput, formatInput, @@ -58,36 +65,39 @@ export const MultiItemFieldInput = ({ fieldMetadataType, renderInput, onClickOutside, - onError, maxItemCount, }: MultiItemFieldInputProps) => { const containerRef = useRef(null); - const handleEscape = () => { - onEscape?.(items); - }; - const instanceId = useAvailableComponentInstanceIdOrThrow( RecordFieldComponentInstanceContext, ); + const activeDropdownFocusId = useRecoilValue(activeDropdownFocusIdState); + useListenClickOutside({ refs: [containerRef], callback: (event) => { - handleSubmitInput(); - onClickOutside?.(items, event); + if ( + (isDefined(activeDropdownFocusId) && + activeDropdownFocusId.startsWith( + MULTI_ITEM_FIELD_INPUT_DROPDOWN_ID_PREFIX, + )) || + activeDropdownFocusId === PHONE_COUNTRY_CODE_PICKER_DROPDOWN_ID + ) { + return; + } + handleSubmitChanges(); + onClickOutside(items, event); }, listenerId: instanceId, }); - useHotkeysOnFocusedElement({ - focusId: instanceId, - keys: [Key.Escape], - callback: handleEscape, - dependencies: [handleEscape], - }); - const getItemValueAsString = (index: number): string => { + if (index >= items.length) { + return ''; + } + let item; switch (fieldMetadataType) { case FieldMetadataType.LINKS: @@ -110,22 +120,26 @@ export const MultiItemFieldInput = ({ } }; - const shouldAutoEditSingleItem = items.length === 1 && maxItemCount === 1; + const shouldAutoEnterBecauseOnlyOneItemIsAllowed = maxItemCount === 1; + const shouldAutoEditFirstItemOnOpen = + items.length === 0 || maxItemCount === 1; const [isInputDisplayed, setIsInputDisplayed] = useState( - shouldAutoEditSingleItem, + shouldAutoEditFirstItemOnOpen, ); - const [inputValue, setInputValue] = useState(() => - shouldAutoEditSingleItem ? getItemValueAsString(0) : '', - ); - const [itemToEditIndex, setItemToEditIndex] = useState( - shouldAutoEditSingleItem ? 0 : -1, + + const [inputValue, setInputValue] = useState( + shouldAutoEditFirstItemOnOpen ? getItemValueAsString(0) : '', ); + + const [itemToEditIndex, setItemToEditIndex] = useState(0); + const [isAddingNewItem, setIsAddingNewItem] = useState(false); + const [errorData, setErrorData] = useState({ isValid: true, errorMessage: '', }); - const isAddingNewItem = itemToEditIndex === -1; + const isLimitReached = typeof maxItemCount === 'number' && items.length >= maxItemCount; @@ -146,23 +160,48 @@ export const MultiItemFieldInput = ({ return; } - setItemToEditIndex(-1); + setIsAddingNewItem(true); + setInputValue(''); setIsInputDisplayed(true); }; const handleEditButtonClick = (index: number) => { - setInputValue(getItemValueAsString(index)); setItemToEditIndex(index); + setInputValue(getItemValueAsString(index)); + setIsAddingNewItem(false); setIsInputDisplayed(true); }; - const handleSubmitInput = () => { + const handleAutoEnter = () => { const sanitizedInput = inputValue.trim(); + const newItem = formatInput + ? formatInput(sanitizedInput) + : (sanitizedInput as unknown as T); + + const updatedItems = isAddingNewItem + ? [...items, newItem] + : toSpliced(items, itemToEditIndex, 1, newItem); + + onEnter(updatedItems); + }; + + const handleSubmitChanges = () => { + const sanitizedInput = inputValue.trim(); + + const newItem = formatInput + ? formatInput(sanitizedInput) + : (sanitizedInput as unknown as T); + if (sanitizedInput === '' && isAddingNewItem) { return; } + if (sanitizedInput === '' && shouldAutoEnterBecauseOnlyOneItemIsAllowed) { + onEnter([newItem]); + return; + } + if (sanitizedInput === '' && !isAddingNewItem) { handleDeleteItem(itemToEditIndex); return; @@ -177,23 +216,13 @@ export const MultiItemFieldInput = ({ } } - const newItem = formatInput - ? formatInput(sanitizedInput) - : (sanitizedInput as unknown as T); - - if (!isAddingNewItem && newItem === items[itemToEditIndex]) { - setIsInputDisplayed(false); - setInputValue(''); - return; - } - const updatedItems = isAddingNewItem ? [...items, newItem] : toSpliced(items, itemToEditIndex, 1, newItem); onChange(updatedItems); + setIsAddingNewItem(false); setIsInputDisplayed(false); - setInputValue(''); }; const handleSetPrimaryItem = (index: number) => { @@ -205,30 +234,43 @@ export const MultiItemFieldInput = ({ const updatedItems = toSpliced(items, index, 1); onChange(updatedItems); setIsInputDisplayed(false); - setInputValue(''); - setItemToEditIndex(-1); + setIsAddingNewItem(false); }; + const handleEscape = () => { + onEscape(items); + }; + + useHotkeysOnFocusedElement({ + focusId: instanceId, + keys: [Key.Escape], + callback: handleEscape, + dependencies: [handleEscape], + }); + return ( - {!!items.length && (!shouldAutoEditSingleItem || !isInputDisplayed) && ( - <> - - {items.map((item, index) => - renderItem({ - value: item, - index, - handleEdit: () => handleEditButtonClick(index), - handleSetPrimary: () => handleSetPrimaryItem(index), - handleDelete: () => handleDeleteItem(index), - }), - )} - - {isInputDisplayed || !isLimitReached ? ( - - ) : null} - - )} + {!!items.length && + (!shouldAutoEnterBecauseOnlyOneItemIsAllowed || !isInputDisplayed) && ( + <> + + {items.map((item, index) => + renderItem({ + value: item, + index, + handleEdit: () => handleEditButtonClick(index), + handleSetPrimary: () => handleSetPrimaryItem(index), + handleDelete: () => { + handleDeleteItem(index); + }, + }), + )} + + {isInputDisplayed || !isLimitReached ? ( + + ) : null} + + )} {isInputDisplayed || !items.length ? ( ({ ? handleInputChange(turnIntoEmptyStringIfWhitespacesOnly(value)) : handleInputChange(''); }} - onEnter={handleSubmitInput} + onEnter={() => { + handleSubmitChanges(); + if (shouldAutoEnterBecauseOnlyOneItemIsAllowed) { + handleAutoEnter(); + } + }} hasItem={!!items.length} rightComponent={ items.length ? ( { + handleSubmitChanges(); + if (shouldAutoEnterBecauseOnlyOneItemIsAllowed) { + handleAutoEnter(); + } + }} /> ) : null } diff --git a/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/input/components/MultiItemFieldMenuItem.tsx b/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/input/components/MultiItemFieldMenuItem.tsx index 27e603f3e67..50b0a9e6a85 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/input/components/MultiItemFieldMenuItem.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/input/components/MultiItemFieldMenuItem.tsx @@ -51,35 +51,23 @@ export const MultiItemFieldMenuItem = ({ setIsHovered(false); }; - const handleDeleteClick = (event: React.MouseEvent) => { - event.stopPropagation(); - event.preventDefault(); - + const handleDeleteClick = () => { closeDropdown(dropdownId); setIsHovered(false); onDelete?.(); }; - const handleSetAsPrimaryClick = (event: React.MouseEvent) => { - event.stopPropagation(); - event.preventDefault(); - + const handleSetAsPrimaryClick = () => { closeDropdown(dropdownId); onSetAsPrimary?.(); }; - const handleEditClick = (event: React.MouseEvent) => { - event.stopPropagation(); - event.preventDefault(); - + const handleEditClick = () => { closeDropdown(dropdownId); onEdit?.(); }; - const handleCopyClick = (event: React.MouseEvent) => { - event.stopPropagation(); - event.preventDefault(); - + const handleCopyClick = () => { closeDropdown(dropdownId); onCopy?.(value); }; diff --git a/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/input/components/PhonesFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/input/components/PhonesFieldInput.tsx index c52eff4cd2c..93280b47660 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/input/components/PhonesFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/input/components/PhonesFieldInput.tsx @@ -11,14 +11,18 @@ import 'react-phone-number-input/style.css'; import { MultiItemFieldInput } from './MultiItemFieldInput'; import { FieldInputEventContext } from '@/object-record/record-field/ui/contexts/FieldInputEventContext'; +import { MULTI_ITEM_FIELD_INPUT_DROPDOWN_ID_PREFIX } from '@/object-record/record-field/ui/meta-types/input/constants/MultiItemFieldInputDropdownClickOutsideId'; import { createPhonesFromFieldValue } from '@/object-record/record-field/ui/meta-types/input/utils/phonesUtils'; -import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/ui/states/contexts/RecordFieldComponentInstanceContext'; +import { + type FieldPhonesValue, + type PhoneRecord, +} from '@/object-record/record-field/ui/types/FieldMetadata'; import { phonesSchema } from '@/object-record/record-field/ui/types/guards/isFieldPhonesValue'; import { PhoneCountryPickerDropdownButton } from '@/ui/input/components/internal/phone/components/PhoneCountryPickerDropdownButton'; -import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow'; import { css } from '@emotion/react'; import { useContext } from 'react'; import { MULTI_ITEM_FIELD_DEFAULT_MAX_VALUES } from 'twenty-shared/constants'; +import { isDefined } from 'twenty-shared/utils'; import { TEXT_INPUT_STYLE } from 'twenty-ui/theme'; import { FieldMetadataType } from '~/generated-metadata/graphql'; import { stripSimpleQuotesFromString } from '~/utils/string/stripSimpleQuotesFromString'; @@ -75,15 +79,13 @@ const StyledCustomPhoneInput = styled(ReactPhoneNumberInput)` `; export const PhonesFieldInput = () => { - const { getLatestDraftValue, fieldDefinition, setDraftValue, draftValue } = - usePhonesField(); + const { fieldDefinition, setDraftValue, draftValue } = usePhonesField(); - const { onEscape, onClickOutside } = useContext(FieldInputEventContext); + const { onEscape, onClickOutside, onEnter } = useContext( + FieldInputEventContext, + ); const phones = createPhonesFromFieldValue(draftValue); - const instanceId = useAvailableComponentInstanceIdOrThrow( - RecordFieldComponentInstanceContext, - ); const defaultCountry = stripSimpleQuotesFromString( fieldDefinition?.defaultValue?.primaryPhoneCountryCode, @@ -93,26 +95,26 @@ export const PhonesFieldInput = () => { fieldDefinition.metadata.settings?.maxNumberOfValues ?? MULTI_ITEM_FIELD_DEFAULT_MAX_VALUES; - const handlePhonesChange = ( - updatedPhones: { - number: string; - countryCode: string; - callingCode: string; - }[], - ) => { - const [nextPrimaryPhone, ...nextAdditionalPhones] = updatedPhones; + const parseArrayToPhonesValue = (phones: PhoneRecord[]) => { + const [nextPrimaryPhone, ...nextAdditionalPhones] = phones; - const newValue = { + const nextValue: FieldPhonesValue = { primaryPhoneNumber: nextPrimaryPhone?.number ?? '', primaryPhoneCountryCode: nextPrimaryPhone?.countryCode ?? '', primaryPhoneCallingCode: nextPrimaryPhone?.callingCode ?? '', additionalPhones: nextAdditionalPhones, }; + const parseResponse = phonesSchema.safeParse(nextValue); + if (parseResponse.success) { + return parseResponse.data; + } + }; - const newValidatedPhoneResponse = phonesSchema.safeParse(newValue); + const handlePhonesChange = (updatedPhones: PhoneRecord[]) => { + const nextValue = parseArrayToPhonesValue(updatedPhones); - if (newValidatedPhoneResponse.success) { - setDraftValue(newValidatedPhoneResponse.data); + if (isDefined(nextValue)) { + setDraftValue(nextValue); } }; @@ -134,15 +136,21 @@ export const PhonesFieldInput = () => { }; const handleClickOutside = ( - _newValue: any, + updatedPhones: PhoneRecord[], event: MouseEvent | TouchEvent, ) => { - const latestDraftValue = getLatestDraftValue(instanceId); - onClickOutside?.({ newValue: latestDraftValue, event }); + onClickOutside?.({ + newValue: parseArrayToPhonesValue(updatedPhones), + event, + }); }; - const handleEscape = (_newValue: any) => { - onEscape?.({ newValue: draftValue }); + const handleEscape = (updatedPhones: PhoneRecord[]) => { + onEscape?.({ newValue: parseArrayToPhonesValue(updatedPhones) }); + }; + + const handleEnter = (updatedPhones: PhoneRecord[]) => { + onEnter?.({ newValue: parseArrayToPhonesValue(updatedPhones) }); }; return ( @@ -151,10 +159,19 @@ export const PhonesFieldInput = () => { onChange={handlePhonesChange} onClickOutside={handleClickOutside} onEscape={handleEscape} + onEnter={handleEnter} placeholder="Phone" fieldMetadataType={FieldMetadataType.PHONES} validateInput={validateInput} formatInput={(input) => { + if (input === '') { + return { + number: '', + callingCode: '', + countryCode: '', + }; + } + const phone = parsePhoneNumber(input); if (phone !== undefined) { return { @@ -163,6 +180,7 @@ export const PhonesFieldInput = () => { countryCode: phone.country as string, }; } + return { number: '', callingCode: '', @@ -178,7 +196,7 @@ export const PhonesFieldInput = () => { }) => ( { case RecordFilterOperand.IS_BEFORE: { const pointInTime = new Date(recordFilter.value); + if (!isValid(pointInTime)) { + return ''; + } + const { displayValue } = getDateTimeFilterDisplayValue(pointInTime); return `${displayValue}`; diff --git a/packages/twenty-front/src/modules/ui/input/components/internal/phone/components/PhoneCountryPickerDropdownButton.tsx b/packages/twenty-front/src/modules/ui/input/components/internal/phone/components/PhoneCountryPickerDropdownButton.tsx index 33a0724aff3..abe4ed272a4 100644 --- a/packages/twenty-front/src/modules/ui/input/components/internal/phone/components/PhoneCountryPickerDropdownButton.tsx +++ b/packages/twenty-front/src/modules/ui/input/components/internal/phone/components/PhoneCountryPickerDropdownButton.tsx @@ -7,6 +7,7 @@ import { useEffect, useState } from 'react'; import { PhoneCountryPickerDropdownSelect } from './PhoneCountryPickerDropdownSelect'; +import { PHONE_COUNTRY_CODE_PICKER_DROPDOWN_ID } from '@/ui/input/components/internal/phone/constants/PhoneCountryCodePickerDropdownId'; import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown'; import { isDropdownOpenComponentState } from '@/ui/layout/dropdown/states/isDropdownOpenComponentState'; import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; @@ -77,17 +78,15 @@ export const PhoneCountryPickerDropdownButton = ({ const [selectedCountry, setSelectedCountry] = useState(); - const dropdownId = 'country-picker-dropdown-id'; - const isDropdownOpen = useRecoilComponentValue( isDropdownOpenComponentState, - dropdownId, + PHONE_COUNTRY_CODE_PICKER_DROPDOWN_ID, ); const { closeDropdown } = useCloseDropdown(); const handleChange = (countryCode: string) => { - closeDropdown(dropdownId); + closeDropdown(PHONE_COUNTRY_CODE_PICKER_DROPDOWN_ID); onChange(countryCode); }; @@ -102,7 +101,7 @@ export const PhoneCountryPickerDropdownButton = ({ return ( diff --git a/packages/twenty-front/src/modules/ui/input/components/internal/phone/constants/PhoneCountryCodePickerDropdownId.ts b/packages/twenty-front/src/modules/ui/input/components/internal/phone/constants/PhoneCountryCodePickerDropdownId.ts new file mode 100644 index 00000000000..0d24f89a1f3 --- /dev/null +++ b/packages/twenty-front/src/modules/ui/input/components/internal/phone/constants/PhoneCountryCodePickerDropdownId.ts @@ -0,0 +1,2 @@ +export const PHONE_COUNTRY_CODE_PICKER_DROPDOWN_ID = + 'phone-country-picker-dropdown-id';