Fix bug on Links, Array, Emails, Phones Field inputs (#16603)
The following PR introduced several bugs: https://github.com/twentyhq/twenty/pull/16042 https://github.com/user-attachments/assets/2c0fd211-3579-4a22-9a5f-dcec9cbe6a2e
This commit is contained in:
-14
@@ -7,19 +7,6 @@ export const useRecordFieldInput = <FieldValue>() => {
|
|||||||
const recordFieldInputDraftValueCallbackState =
|
const recordFieldInputDraftValueCallbackState =
|
||||||
useRecoilComponentCallbackState(recordFieldInputDraftValueComponentState);
|
useRecoilComponentCallbackState(recordFieldInputDraftValueComponentState);
|
||||||
|
|
||||||
const getLatestDraftValue = useRecoilCallback(
|
|
||||||
({ snapshot }) =>
|
|
||||||
(instanceId: string) =>
|
|
||||||
snapshot
|
|
||||||
.getLoadable(
|
|
||||||
recordFieldInputDraftValueComponentState.atomFamily({
|
|
||||||
instanceId,
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.getValue() as FieldInputDraftValue<FieldValue>,
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
|
|
||||||
const setDraftValue = useRecoilCallback(
|
const setDraftValue = useRecoilCallback(
|
||||||
({ set }) =>
|
({ set }) =>
|
||||||
(newValue: unknown) => {
|
(newValue: unknown) => {
|
||||||
@@ -43,7 +30,6 @@ export const useRecordFieldInput = <FieldValue>() => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
getLatestDraftValue,
|
|
||||||
setDraftValue,
|
setDraftValue,
|
||||||
isDraftValueEmpty,
|
isDraftValueEmpty,
|
||||||
};
|
};
|
||||||
|
|||||||
+1
-3
@@ -24,8 +24,7 @@ export const useArrayField = () => {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
const { getLatestDraftValue, setDraftValue } =
|
const { setDraftValue } = useRecordFieldInput<FieldArrayValue>();
|
||||||
useRecordFieldInput<FieldArrayValue>();
|
|
||||||
|
|
||||||
const draftValue = useRecoilComponentValue(
|
const draftValue = useRecoilComponentValue(
|
||||||
recordFieldInputDraftValueComponentState,
|
recordFieldInputDraftValueComponentState,
|
||||||
@@ -35,7 +34,6 @@ export const useArrayField = () => {
|
|||||||
fieldValue,
|
fieldValue,
|
||||||
fieldDefinition,
|
fieldDefinition,
|
||||||
draftValue,
|
draftValue,
|
||||||
getLatestDraftValue,
|
|
||||||
setFieldValue,
|
setFieldValue,
|
||||||
setDraftValue,
|
setDraftValue,
|
||||||
};
|
};
|
||||||
|
|||||||
+1
-3
@@ -26,8 +26,7 @@ export const useEmailsField = () => {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
const { getLatestDraftValue, setDraftValue } =
|
const { setDraftValue } = useRecordFieldInput<FieldEmailsValue>();
|
||||||
useRecordFieldInput<FieldEmailsValue>();
|
|
||||||
|
|
||||||
const draftValue = useRecoilComponentValue(
|
const draftValue = useRecoilComponentValue(
|
||||||
recordFieldInputDraftValueComponentState,
|
recordFieldInputDraftValueComponentState,
|
||||||
@@ -37,7 +36,6 @@ export const useEmailsField = () => {
|
|||||||
fieldDefinition,
|
fieldDefinition,
|
||||||
fieldValue,
|
fieldValue,
|
||||||
draftValue,
|
draftValue,
|
||||||
getLatestDraftValue,
|
|
||||||
setDraftValue,
|
setDraftValue,
|
||||||
setFieldValue,
|
setFieldValue,
|
||||||
};
|
};
|
||||||
|
|||||||
+1
-3
@@ -26,8 +26,7 @@ export const useLinksField = () => {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
const { getLatestDraftValue, setDraftValue } =
|
const { setDraftValue } = useRecordFieldInput<FieldLinksValue>();
|
||||||
useRecordFieldInput<FieldLinksValue>();
|
|
||||||
|
|
||||||
const draftValue = useRecoilComponentValue(
|
const draftValue = useRecoilComponentValue(
|
||||||
recordFieldInputDraftValueComponentState,
|
recordFieldInputDraftValueComponentState,
|
||||||
@@ -37,7 +36,6 @@ export const useLinksField = () => {
|
|||||||
fieldDefinition,
|
fieldDefinition,
|
||||||
fieldValue,
|
fieldValue,
|
||||||
draftValue,
|
draftValue,
|
||||||
getLatestDraftValue,
|
|
||||||
setDraftValue,
|
setDraftValue,
|
||||||
setFieldValue,
|
setFieldValue,
|
||||||
};
|
};
|
||||||
|
|||||||
+1
-3
@@ -26,8 +26,7 @@ export const usePhonesField = () => {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
const { getLatestDraftValue, setDraftValue } =
|
const { setDraftValue } = useRecordFieldInput<FieldPhonesValue>();
|
||||||
useRecordFieldInput<FieldPhonesValue>();
|
|
||||||
|
|
||||||
const draftValue = useRecoilComponentValue(
|
const draftValue = useRecoilComponentValue(
|
||||||
recordFieldInputDraftValueComponentState,
|
recordFieldInputDraftValueComponentState,
|
||||||
@@ -37,7 +36,6 @@ export const usePhonesField = () => {
|
|||||||
fieldDefinition,
|
fieldDefinition,
|
||||||
fieldValue,
|
fieldValue,
|
||||||
draftValue,
|
draftValue,
|
||||||
getLatestDraftValue,
|
|
||||||
setDraftValue,
|
setDraftValue,
|
||||||
setFieldValue,
|
setFieldValue,
|
||||||
};
|
};
|
||||||
|
|||||||
+22
-15
@@ -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 { 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 { 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 { 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 { 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 { useContext, useMemo } from 'react';
|
||||||
import { MULTI_ITEM_FIELD_DEFAULT_MAX_VALUES } from 'twenty-shared/constants';
|
import { MULTI_ITEM_FIELD_DEFAULT_MAX_VALUES } from 'twenty-shared/constants';
|
||||||
import { isDefined } from 'twenty-shared/utils';
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||||
|
|
||||||
export const ArrayFieldInput = () => {
|
export const ArrayFieldInput = () => {
|
||||||
const { getLatestDraftValue, setDraftValue, draftValue, fieldDefinition } =
|
const { setDraftValue, draftValue, fieldDefinition } = useArrayField();
|
||||||
useArrayField();
|
|
||||||
|
|
||||||
const { onEscape, onClickOutside } = useContext(FieldInputEventContext);
|
const { onEscape, onClickOutside, onEnter } = useContext(
|
||||||
const instanceId = useAvailableComponentInstanceIdOrThrow(
|
FieldInputEventContext,
|
||||||
RecordFieldComponentInstanceContext,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const arrayItems = useMemo<Array<string>>(
|
const arrayItems = useMemo<Array<string>>(
|
||||||
() => (Array.isArray(draftValue) ? draftValue : []),
|
() => (Array.isArray(draftValue) ? draftValue : []),
|
||||||
[draftValue],
|
[draftValue],
|
||||||
);
|
);
|
||||||
|
const parseStringArrayToArrayValue = (arrayItems: string[]) => {
|
||||||
|
const parseResponse = arraySchema.safeParse(arrayItems);
|
||||||
|
if (parseResponse.success) {
|
||||||
|
return parseResponse.data;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleChange = (newValue: any[]) => {
|
const handleChange = (newValue: any[]) => {
|
||||||
if (!isDefined(newValue)) setDraftValue(null);
|
if (!isDefined(newValue)) setDraftValue(null);
|
||||||
|
|
||||||
const parseResponse = arraySchema.safeParse(newValue);
|
const nextValue = parseStringArrayToArrayValue(newValue);
|
||||||
|
|
||||||
if (parseResponse.success) {
|
if (isDefined(nextValue)) {
|
||||||
setDraftValue(parseResponse.data);
|
setDraftValue(nextValue);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -38,12 +41,15 @@ export const ArrayFieldInput = () => {
|
|||||||
_newValue: any,
|
_newValue: any,
|
||||||
event: MouseEvent | TouchEvent,
|
event: MouseEvent | TouchEvent,
|
||||||
) => {
|
) => {
|
||||||
const latestDraftValue = getLatestDraftValue(instanceId);
|
onClickOutside?.({ newValue: draftValue, event });
|
||||||
onClickOutside?.({ newValue: latestDraftValue, event });
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleEscape = (_newValue: any) => {
|
const handleEscape = (newValue: string[]) => {
|
||||||
onEscape?.({ newValue: draftValue });
|
onEscape?.({ newValue: parseStringArrayToArrayValue(newValue) });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEnter = (newValue: string[]) => {
|
||||||
|
onEnter?.({ newValue: parseStringArrayToArrayValue(newValue) });
|
||||||
};
|
};
|
||||||
|
|
||||||
const maxNumberOfValues =
|
const maxNumberOfValues =
|
||||||
@@ -55,6 +61,7 @@ export const ArrayFieldInput = () => {
|
|||||||
newItemLabel="Add Item"
|
newItemLabel="Add Item"
|
||||||
items={arrayItems}
|
items={arrayItems}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
|
onEnter={handleEnter}
|
||||||
onEscape={handleEscape}
|
onEscape={handleEscape}
|
||||||
onClickOutside={handleClickOutside}
|
onClickOutside={handleClickOutside}
|
||||||
placeholder="Enter value"
|
placeholder="Enter value"
|
||||||
@@ -62,7 +69,7 @@ export const ArrayFieldInput = () => {
|
|||||||
renderItem={({ value, index, handleEdit, handleDelete }) => (
|
renderItem={({ value, index, handleEdit, handleDelete }) => (
|
||||||
<ArrayFieldMenuItem
|
<ArrayFieldMenuItem
|
||||||
key={index}
|
key={index}
|
||||||
dropdownId={`array-field-input-${fieldDefinition.metadata.fieldName}-${index}`}
|
dropdownId={`${MULTI_ITEM_FIELD_INPUT_DROPDOWN_ID_PREFIX}-${fieldDefinition.metadata.fieldName}-${index}`}
|
||||||
value={value}
|
value={value}
|
||||||
onEdit={handleEdit}
|
onEdit={handleEdit}
|
||||||
onDelete={handleDelete}
|
onDelete={handleDelete}
|
||||||
|
|||||||
+30
-17
@@ -1,11 +1,11 @@
|
|||||||
import { FieldInputEventContext } from '@/object-record/record-field/ui/contexts/FieldInputEventContext';
|
import { FieldInputEventContext } from '@/object-record/record-field/ui/contexts/FieldInputEventContext';
|
||||||
import { useEmailsField } from '@/object-record/record-field/ui/meta-types/hooks/useEmailsField';
|
import { useEmailsField } from '@/object-record/record-field/ui/meta-types/hooks/useEmailsField';
|
||||||
import { EmailsFieldMenuItem } from '@/object-record/record-field/ui/meta-types/input/components/EmailsFieldMenuItem';
|
import { EmailsFieldMenuItem } from '@/object-record/record-field/ui/meta-types/input/components/EmailsFieldMenuItem';
|
||||||
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 { recordFieldInputIsFieldInErrorComponentState } from '@/object-record/record-field/ui/states/recordFieldInputIsFieldInErrorComponentState';
|
import { recordFieldInputIsFieldInErrorComponentState } from '@/object-record/record-field/ui/states/recordFieldInputIsFieldInErrorComponentState';
|
||||||
|
import { type FieldEmailsValue } from '@/object-record/record-field/ui/types/FieldMetadata';
|
||||||
import { emailsSchema } from '@/object-record/record-field/ui/types/guards/isFieldEmailsValue';
|
import { emailsSchema } from '@/object-record/record-field/ui/types/guards/isFieldEmailsValue';
|
||||||
import { emailSchema } from '@/object-record/record-field/ui/validation-schemas/emailSchema';
|
import { emailSchema } from '@/object-record/record-field/ui/validation-schemas/emailSchema';
|
||||||
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
|
|
||||||
import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState';
|
import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState';
|
||||||
import { useLingui } from '@lingui/react/macro';
|
import { useLingui } from '@lingui/react/macro';
|
||||||
import { useCallback, useContext, useMemo } from 'react';
|
import { useCallback, useContext, useMemo } from 'react';
|
||||||
@@ -16,14 +16,12 @@ import { useCopyToClipboard } from '~/hooks/useCopyToClipboard';
|
|||||||
import { MultiItemFieldInput } from './MultiItemFieldInput';
|
import { MultiItemFieldInput } from './MultiItemFieldInput';
|
||||||
|
|
||||||
export const EmailsFieldInput = () => {
|
export const EmailsFieldInput = () => {
|
||||||
const { getLatestDraftValue, setDraftValue, draftValue, fieldDefinition } =
|
const { setDraftValue, draftValue, fieldDefinition } = useEmailsField();
|
||||||
useEmailsField();
|
|
||||||
const { copyToClipboard } = useCopyToClipboard();
|
const { copyToClipboard } = useCopyToClipboard();
|
||||||
const { t } = useLingui();
|
const { t } = useLingui();
|
||||||
|
|
||||||
const { onEscape, onClickOutside } = useContext(FieldInputEventContext);
|
const { onEscape, onClickOutside, onEnter } = useContext(
|
||||||
const instanceId = useAvailableComponentInstanceIdOrThrow(
|
FieldInputEventContext,
|
||||||
RecordFieldComponentInstanceContext,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const emails = useMemo<string[]>(
|
const emails = useMemo<string[]>(
|
||||||
@@ -35,10 +33,10 @@ export const EmailsFieldInput = () => {
|
|||||||
[draftValue?.primaryEmail, draftValue?.additionalEmails],
|
[draftValue?.primaryEmail, draftValue?.additionalEmails],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleChange = (updatedEmails: string[]) => {
|
const parseStringArrayToEmailsValue = (emails: string[]) => {
|
||||||
const [nextPrimaryEmail, ...nextAdditionalEmails] = updatedEmails;
|
const [nextPrimaryEmail, ...nextAdditionalEmails] = emails;
|
||||||
|
|
||||||
const nextValue = {
|
const nextValue: FieldEmailsValue = {
|
||||||
primaryEmail: nextPrimaryEmail ?? '',
|
primaryEmail: nextPrimaryEmail ?? '',
|
||||||
additionalEmails: nextAdditionalEmails,
|
additionalEmails: nextAdditionalEmails,
|
||||||
};
|
};
|
||||||
@@ -46,7 +44,15 @@ export const EmailsFieldInput = () => {
|
|||||||
const parseResponse = emailsSchema.safeParse(nextValue);
|
const parseResponse = emailsSchema.safeParse(nextValue);
|
||||||
|
|
||||||
if (parseResponse.success) {
|
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 = (
|
const handleClickOutside = (
|
||||||
_newValue: any,
|
updatedEmails: string[],
|
||||||
event: MouseEvent | TouchEvent,
|
event: MouseEvent | TouchEvent,
|
||||||
) => {
|
) => {
|
||||||
const latestDraftValue = getLatestDraftValue(instanceId);
|
onClickOutside?.({
|
||||||
onClickOutside?.({ newValue: latestDraftValue, event });
|
newValue: parseStringArrayToEmailsValue(updatedEmails),
|
||||||
|
event,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleEscape = (_newValue: any) => {
|
const handleEscape = (updatedEmails: string[]) => {
|
||||||
onEscape?.({ newValue: draftValue });
|
onEscape?.({ newValue: parseStringArrayToEmailsValue(updatedEmails) });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEnter = (updatedEmails: string[]) => {
|
||||||
|
onEnter?.({ newValue: parseStringArrayToEmailsValue(updatedEmails) });
|
||||||
};
|
};
|
||||||
|
|
||||||
const maxNumberOfValues =
|
const maxNumberOfValues =
|
||||||
@@ -94,6 +106,7 @@ export const EmailsFieldInput = () => {
|
|||||||
<MultiItemFieldInput
|
<MultiItemFieldInput
|
||||||
items={emails}
|
items={emails}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
|
onEnter={handleEnter}
|
||||||
onEscape={handleEscape}
|
onEscape={handleEscape}
|
||||||
onClickOutside={handleClickOutside}
|
onClickOutside={handleClickOutside}
|
||||||
placeholder="Email"
|
placeholder="Email"
|
||||||
@@ -108,7 +121,7 @@ export const EmailsFieldInput = () => {
|
|||||||
}) => (
|
}) => (
|
||||||
<EmailsFieldMenuItem
|
<EmailsFieldMenuItem
|
||||||
key={index}
|
key={index}
|
||||||
dropdownId={`emails-${index}`}
|
dropdownId={`${MULTI_ITEM_FIELD_INPUT_DROPDOWN_ID_PREFIX}-${fieldDefinition.metadata.fieldName}-${index}`}
|
||||||
showPrimaryIcon={getShowPrimaryIcon(index)}
|
showPrimaryIcon={getShowPrimaryIcon(index)}
|
||||||
showSetAsPrimaryButton={getShowSetAsPrimaryButton(index)}
|
showSetAsPrimaryButton={getShowSetAsPrimaryButton(index)}
|
||||||
showCopyButton={true}
|
showCopyButton={true}
|
||||||
|
|||||||
+38
-26
@@ -1,48 +1,56 @@
|
|||||||
import { FieldInputEventContext } from '@/object-record/record-field/ui/contexts/FieldInputEventContext';
|
import { FieldInputEventContext } from '@/object-record/record-field/ui/contexts/FieldInputEventContext';
|
||||||
import { useLinksField } from '@/object-record/record-field/ui/meta-types/hooks/useLinksField';
|
import { useLinksField } from '@/object-record/record-field/ui/meta-types/hooks/useLinksField';
|
||||||
import { LinksFieldMenuItem } from '@/object-record/record-field/ui/meta-types/input/components/LinksFieldMenuItem';
|
import { LinksFieldMenuItem } from '@/object-record/record-field/ui/meta-types/input/components/LinksFieldMenuItem';
|
||||||
|
import { MULTI_ITEM_FIELD_INPUT_DROPDOWN_ID_PREFIX } from '@/object-record/record-field/ui/meta-types/input/constants/MultiItemFieldInputDropdownClickOutsideId';
|
||||||
import { getFieldLinkDefinedLinks } from '@/object-record/record-field/ui/meta-types/input/utils/getFieldLinkDefinedLinks';
|
import { getFieldLinkDefinedLinks } from '@/object-record/record-field/ui/meta-types/input/utils/getFieldLinkDefinedLinks';
|
||||||
import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/ui/states/contexts/RecordFieldComponentInstanceContext';
|
|
||||||
import { recordFieldInputIsFieldInErrorComponentState } from '@/object-record/record-field/ui/states/recordFieldInputIsFieldInErrorComponentState';
|
import { recordFieldInputIsFieldInErrorComponentState } from '@/object-record/record-field/ui/states/recordFieldInputIsFieldInErrorComponentState';
|
||||||
|
import { type FieldLinksValue } from '@/object-record/record-field/ui/types/FieldMetadata';
|
||||||
import { linksSchema } from '@/object-record/record-field/ui/types/guards/isFieldLinksValue';
|
import { linksSchema } from '@/object-record/record-field/ui/types/guards/isFieldLinksValue';
|
||||||
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
|
|
||||||
import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState';
|
import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState';
|
||||||
import { useContext, useMemo } from 'react';
|
import { useContext, useMemo } from 'react';
|
||||||
import { MULTI_ITEM_FIELD_DEFAULT_MAX_VALUES } from 'twenty-shared/constants';
|
import { MULTI_ITEM_FIELD_DEFAULT_MAX_VALUES } from 'twenty-shared/constants';
|
||||||
import { absoluteUrlSchema } from 'twenty-shared/utils';
|
import { absoluteUrlSchema, isDefined } from 'twenty-shared/utils';
|
||||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||||
import { MultiItemFieldInput } from './MultiItemFieldInput';
|
import { MultiItemFieldInput } from './MultiItemFieldInput';
|
||||||
|
|
||||||
export const LinksFieldInput = () => {
|
type LinkRecord = {
|
||||||
const { getLatestDraftValue, draftValue, fieldDefinition, setDraftValue } =
|
url: string | null;
|
||||||
useLinksField();
|
label: string | null;
|
||||||
const instanceId = useAvailableComponentInstanceIdOrThrow(
|
};
|
||||||
RecordFieldComponentInstanceContext,
|
|
||||||
);
|
|
||||||
|
|
||||||
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 }[]>(
|
const links = useMemo<{ url: string; label: string | null }[]>(
|
||||||
() => getFieldLinkDefinedLinks(draftValue),
|
() => getFieldLinkDefinedLinks(draftValue),
|
||||||
[draftValue],
|
[draftValue],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleChange = (
|
const parseArrayToLinksValue = (links: LinkRecord[]) => {
|
||||||
updatedLinks: { url: string | null; label: string | null }[],
|
const nextPrimaryLink = links.at(0);
|
||||||
) => {
|
const nextSecondaryLinks = links.slice(1);
|
||||||
const nextPrimaryLink = updatedLinks.at(0);
|
const nextValue: FieldLinksValue = {
|
||||||
const nextSecondaryLinks = updatedLinks.slice(1);
|
|
||||||
|
|
||||||
const nextValue = {
|
|
||||||
primaryLinkUrl: nextPrimaryLink?.url ?? null,
|
primaryLinkUrl: nextPrimaryLink?.url ?? null,
|
||||||
primaryLinkLabel: nextPrimaryLink?.label ?? null,
|
primaryLinkLabel: nextPrimaryLink?.label ?? null,
|
||||||
secondaryLinks: nextSecondaryLinks,
|
secondaryLinks: nextSecondaryLinks,
|
||||||
};
|
};
|
||||||
|
|
||||||
const parseResponse = linksSchema.safeParse(nextValue);
|
const parseResponse = linksSchema.safeParse(nextValue);
|
||||||
|
|
||||||
if (parseResponse.success) {
|
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 = (
|
const handleClickOutside = (
|
||||||
_newValue: any,
|
updatedLinks: LinkRecord[],
|
||||||
event: MouseEvent | TouchEvent,
|
event: MouseEvent | TouchEvent,
|
||||||
) => {
|
) => {
|
||||||
const latestDraftValue = getLatestDraftValue(instanceId);
|
onClickOutside?.({ newValue: parseArrayToLinksValue(updatedLinks), event });
|
||||||
onClickOutside?.({ newValue: latestDraftValue, event });
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleEscape = (_newValue: any) => {
|
const handleEscape = (updatedLinks: LinkRecord[]) => {
|
||||||
onEscape?.({ newValue: draftValue });
|
onEscape?.({ newValue: parseArrayToLinksValue(updatedLinks) });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEnter = (updatedLinks: LinkRecord[]) => {
|
||||||
|
onEnter?.({ newValue: parseArrayToLinksValue(updatedLinks) });
|
||||||
};
|
};
|
||||||
|
|
||||||
const maxNumberOfValues =
|
const maxNumberOfValues =
|
||||||
@@ -78,6 +89,7 @@ export const LinksFieldInput = () => {
|
|||||||
items={links}
|
items={links}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
onEscape={handleEscape}
|
onEscape={handleEscape}
|
||||||
|
onEnter={handleEnter}
|
||||||
onClickOutside={handleClickOutside}
|
onClickOutside={handleClickOutside}
|
||||||
placeholder="URL"
|
placeholder="URL"
|
||||||
fieldMetadataType={FieldMetadataType.LINKS}
|
fieldMetadataType={FieldMetadataType.LINKS}
|
||||||
@@ -96,7 +108,7 @@ export const LinksFieldInput = () => {
|
|||||||
}) => (
|
}) => (
|
||||||
<LinksFieldMenuItem
|
<LinksFieldMenuItem
|
||||||
key={index}
|
key={index}
|
||||||
dropdownId={`links-field-input-${fieldDefinition.metadata.fieldName}-${index}`}
|
dropdownId={`${MULTI_ITEM_FIELD_INPUT_DROPDOWN_ID_PREFIX}-${fieldDefinition.metadata.fieldName}-${index}`}
|
||||||
showPrimaryIcon={getShowPrimaryIcon(index)}
|
showPrimaryIcon={getShowPrimaryIcon(index)}
|
||||||
showSetAsPrimaryButton={getShowSetAsPrimaryButton(index)}
|
showSetAsPrimaryButton={getShowSetAsPrimaryButton(index)}
|
||||||
label={link.label}
|
label={link.label}
|
||||||
|
|||||||
+114
-62
@@ -5,15 +5,19 @@ import {
|
|||||||
MultiItemBaseInput,
|
MultiItemBaseInput,
|
||||||
type MultiItemBaseInputProps,
|
type MultiItemBaseInputProps,
|
||||||
} from '@/object-record/record-field/ui/meta-types/input/components/MultiItemBaseInput';
|
} from '@/object-record/record-field/ui/meta-types/input/components/MultiItemBaseInput';
|
||||||
|
import { MULTI_ITEM_FIELD_INPUT_DROPDOWN_ID_PREFIX } from '@/object-record/record-field/ui/meta-types/input/constants/MultiItemFieldInputDropdownClickOutsideId';
|
||||||
import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/ui/states/contexts/RecordFieldComponentInstanceContext';
|
import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/ui/states/contexts/RecordFieldComponentInstanceContext';
|
||||||
import { type PhoneRecord } from '@/object-record/record-field/ui/types/FieldMetadata';
|
import { type PhoneRecord } from '@/object-record/record-field/ui/types/FieldMetadata';
|
||||||
|
import { PHONE_COUNTRY_CODE_PICKER_DROPDOWN_ID } from '@/ui/input/components/internal/phone/constants/PhoneCountryCodePickerDropdownId';
|
||||||
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
|
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
|
||||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||||
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
|
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
|
||||||
|
import { activeDropdownFocusIdState } from '@/ui/layout/dropdown/states/activeDropdownFocusIdState';
|
||||||
import { useHotkeysOnFocusedElement } from '@/ui/utilities/hotkey/hooks/useHotkeysOnFocusedElement';
|
import { useHotkeysOnFocusedElement } from '@/ui/utilities/hotkey/hooks/useHotkeysOnFocusedElement';
|
||||||
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
|
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
|
||||||
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
|
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
|
||||||
import { CustomError } from 'twenty-shared/utils';
|
import { useRecoilValue } from 'recoil';
|
||||||
|
import { CustomError, isDefined } from 'twenty-shared/utils';
|
||||||
import { IconCheck, IconPlus } from 'twenty-ui/display';
|
import { IconCheck, IconPlus } from 'twenty-ui/display';
|
||||||
import { LightIconButton } from 'twenty-ui/input';
|
import { LightIconButton } from 'twenty-ui/input';
|
||||||
import { MenuItem } from 'twenty-ui/navigation';
|
import { MenuItem } from 'twenty-ui/navigation';
|
||||||
@@ -25,7 +29,10 @@ import { turnIntoEmptyStringIfWhitespacesOnly } from '~/utils/string/turnIntoEmp
|
|||||||
type MultiItemFieldInputProps<T> = {
|
type MultiItemFieldInputProps<T> = {
|
||||||
items: T[];
|
items: T[];
|
||||||
onChange: (newItemsValue: T[]) => void;
|
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;
|
placeholder: string;
|
||||||
validateInput?: (input: string) => { isValid: boolean; errorMessage: string };
|
validateInput?: (input: string) => { isValid: boolean; errorMessage: string };
|
||||||
formatInput?: (input: string) => T;
|
formatInput?: (input: string) => T;
|
||||||
@@ -39,8 +46,6 @@ type MultiItemFieldInputProps<T> = {
|
|||||||
newItemLabel?: string;
|
newItemLabel?: string;
|
||||||
fieldMetadataType: FieldMetadataType;
|
fieldMetadataType: FieldMetadataType;
|
||||||
renderInput?: MultiItemBaseInputProps['renderInput'];
|
renderInput?: MultiItemBaseInputProps['renderInput'];
|
||||||
onClickOutside?: (newItemsValue: T[], event: MouseEvent | TouchEvent) => void;
|
|
||||||
onError?: (hasError: boolean, values: any[]) => void;
|
|
||||||
maxItemCount?: number;
|
maxItemCount?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -50,6 +55,8 @@ export const MultiItemFieldInput = <T,>({
|
|||||||
items,
|
items,
|
||||||
onChange,
|
onChange,
|
||||||
onEscape,
|
onEscape,
|
||||||
|
onEnter,
|
||||||
|
onError,
|
||||||
placeholder,
|
placeholder,
|
||||||
validateInput,
|
validateInput,
|
||||||
formatInput,
|
formatInput,
|
||||||
@@ -58,36 +65,39 @@ export const MultiItemFieldInput = <T,>({
|
|||||||
fieldMetadataType,
|
fieldMetadataType,
|
||||||
renderInput,
|
renderInput,
|
||||||
onClickOutside,
|
onClickOutside,
|
||||||
onError,
|
|
||||||
maxItemCount,
|
maxItemCount,
|
||||||
}: MultiItemFieldInputProps<T>) => {
|
}: MultiItemFieldInputProps<T>) => {
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const handleEscape = () => {
|
|
||||||
onEscape?.(items);
|
|
||||||
};
|
|
||||||
|
|
||||||
const instanceId = useAvailableComponentInstanceIdOrThrow(
|
const instanceId = useAvailableComponentInstanceIdOrThrow(
|
||||||
RecordFieldComponentInstanceContext,
|
RecordFieldComponentInstanceContext,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const activeDropdownFocusId = useRecoilValue(activeDropdownFocusIdState);
|
||||||
|
|
||||||
useListenClickOutside({
|
useListenClickOutside({
|
||||||
refs: [containerRef],
|
refs: [containerRef],
|
||||||
callback: (event) => {
|
callback: (event) => {
|
||||||
handleSubmitInput();
|
if (
|
||||||
onClickOutside?.(items, event);
|
(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,
|
listenerId: instanceId,
|
||||||
});
|
});
|
||||||
|
|
||||||
useHotkeysOnFocusedElement({
|
|
||||||
focusId: instanceId,
|
|
||||||
keys: [Key.Escape],
|
|
||||||
callback: handleEscape,
|
|
||||||
dependencies: [handleEscape],
|
|
||||||
});
|
|
||||||
|
|
||||||
const getItemValueAsString = (index: number): string => {
|
const getItemValueAsString = (index: number): string => {
|
||||||
|
if (index >= items.length) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
let item;
|
let item;
|
||||||
switch (fieldMetadataType) {
|
switch (fieldMetadataType) {
|
||||||
case FieldMetadataType.LINKS:
|
case FieldMetadataType.LINKS:
|
||||||
@@ -110,22 +120,26 @@ export const MultiItemFieldInput = <T,>({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const shouldAutoEditSingleItem = items.length === 1 && maxItemCount === 1;
|
const shouldAutoEnterBecauseOnlyOneItemIsAllowed = maxItemCount === 1;
|
||||||
|
const shouldAutoEditFirstItemOnOpen =
|
||||||
|
items.length === 0 || maxItemCount === 1;
|
||||||
|
|
||||||
const [isInputDisplayed, setIsInputDisplayed] = useState(
|
const [isInputDisplayed, setIsInputDisplayed] = useState(
|
||||||
shouldAutoEditSingleItem,
|
shouldAutoEditFirstItemOnOpen,
|
||||||
);
|
);
|
||||||
const [inputValue, setInputValue] = useState(() =>
|
|
||||||
shouldAutoEditSingleItem ? getItemValueAsString(0) : '',
|
const [inputValue, setInputValue] = useState(
|
||||||
);
|
shouldAutoEditFirstItemOnOpen ? getItemValueAsString(0) : '',
|
||||||
const [itemToEditIndex, setItemToEditIndex] = useState(
|
|
||||||
shouldAutoEditSingleItem ? 0 : -1,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const [itemToEditIndex, setItemToEditIndex] = useState(0);
|
||||||
|
const [isAddingNewItem, setIsAddingNewItem] = useState(false);
|
||||||
|
|
||||||
const [errorData, setErrorData] = useState({
|
const [errorData, setErrorData] = useState({
|
||||||
isValid: true,
|
isValid: true,
|
||||||
errorMessage: '',
|
errorMessage: '',
|
||||||
});
|
});
|
||||||
const isAddingNewItem = itemToEditIndex === -1;
|
|
||||||
const isLimitReached =
|
const isLimitReached =
|
||||||
typeof maxItemCount === 'number' && items.length >= maxItemCount;
|
typeof maxItemCount === 'number' && items.length >= maxItemCount;
|
||||||
|
|
||||||
@@ -146,23 +160,48 @@ export const MultiItemFieldInput = <T,>({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setItemToEditIndex(-1);
|
setIsAddingNewItem(true);
|
||||||
|
setInputValue('');
|
||||||
setIsInputDisplayed(true);
|
setIsInputDisplayed(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleEditButtonClick = (index: number) => {
|
const handleEditButtonClick = (index: number) => {
|
||||||
setInputValue(getItemValueAsString(index));
|
|
||||||
setItemToEditIndex(index);
|
setItemToEditIndex(index);
|
||||||
|
setInputValue(getItemValueAsString(index));
|
||||||
|
setIsAddingNewItem(false);
|
||||||
setIsInputDisplayed(true);
|
setIsInputDisplayed(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmitInput = () => {
|
const handleAutoEnter = () => {
|
||||||
const sanitizedInput = inputValue.trim();
|
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) {
|
if (sanitizedInput === '' && isAddingNewItem) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (sanitizedInput === '' && shouldAutoEnterBecauseOnlyOneItemIsAllowed) {
|
||||||
|
onEnter([newItem]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (sanitizedInput === '' && !isAddingNewItem) {
|
if (sanitizedInput === '' && !isAddingNewItem) {
|
||||||
handleDeleteItem(itemToEditIndex);
|
handleDeleteItem(itemToEditIndex);
|
||||||
return;
|
return;
|
||||||
@@ -177,23 +216,13 @@ export const MultiItemFieldInput = <T,>({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const newItem = formatInput
|
|
||||||
? formatInput(sanitizedInput)
|
|
||||||
: (sanitizedInput as unknown as T);
|
|
||||||
|
|
||||||
if (!isAddingNewItem && newItem === items[itemToEditIndex]) {
|
|
||||||
setIsInputDisplayed(false);
|
|
||||||
setInputValue('');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const updatedItems = isAddingNewItem
|
const updatedItems = isAddingNewItem
|
||||||
? [...items, newItem]
|
? [...items, newItem]
|
||||||
: toSpliced(items, itemToEditIndex, 1, newItem);
|
: toSpliced(items, itemToEditIndex, 1, newItem);
|
||||||
|
|
||||||
onChange(updatedItems);
|
onChange(updatedItems);
|
||||||
|
setIsAddingNewItem(false);
|
||||||
setIsInputDisplayed(false);
|
setIsInputDisplayed(false);
|
||||||
setInputValue('');
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSetPrimaryItem = (index: number) => {
|
const handleSetPrimaryItem = (index: number) => {
|
||||||
@@ -205,30 +234,43 @@ export const MultiItemFieldInput = <T,>({
|
|||||||
const updatedItems = toSpliced(items, index, 1);
|
const updatedItems = toSpliced(items, index, 1);
|
||||||
onChange(updatedItems);
|
onChange(updatedItems);
|
||||||
setIsInputDisplayed(false);
|
setIsInputDisplayed(false);
|
||||||
setInputValue('');
|
setIsAddingNewItem(false);
|
||||||
setItemToEditIndex(-1);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleEscape = () => {
|
||||||
|
onEscape(items);
|
||||||
|
};
|
||||||
|
|
||||||
|
useHotkeysOnFocusedElement({
|
||||||
|
focusId: instanceId,
|
||||||
|
keys: [Key.Escape],
|
||||||
|
callback: handleEscape,
|
||||||
|
dependencies: [handleEscape],
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownContent ref={containerRef}>
|
<DropdownContent ref={containerRef}>
|
||||||
{!!items.length && (!shouldAutoEditSingleItem || !isInputDisplayed) && (
|
{!!items.length &&
|
||||||
<>
|
(!shouldAutoEnterBecauseOnlyOneItemIsAllowed || !isInputDisplayed) && (
|
||||||
<DropdownMenuItemsContainer hasMaxHeight>
|
<>
|
||||||
{items.map((item, index) =>
|
<DropdownMenuItemsContainer hasMaxHeight>
|
||||||
renderItem({
|
{items.map((item, index) =>
|
||||||
value: item,
|
renderItem({
|
||||||
index,
|
value: item,
|
||||||
handleEdit: () => handleEditButtonClick(index),
|
index,
|
||||||
handleSetPrimary: () => handleSetPrimaryItem(index),
|
handleEdit: () => handleEditButtonClick(index),
|
||||||
handleDelete: () => handleDeleteItem(index),
|
handleSetPrimary: () => handleSetPrimaryItem(index),
|
||||||
}),
|
handleDelete: () => {
|
||||||
)}
|
handleDeleteItem(index);
|
||||||
</DropdownMenuItemsContainer>
|
},
|
||||||
{isInputDisplayed || !isLimitReached ? (
|
}),
|
||||||
<DropdownMenuSeparator />
|
)}
|
||||||
) : null}
|
</DropdownMenuItemsContainer>
|
||||||
</>
|
{isInputDisplayed || !isLimitReached ? (
|
||||||
)}
|
<DropdownMenuSeparator />
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
{isInputDisplayed || !items.length ? (
|
{isInputDisplayed || !items.length ? (
|
||||||
<MultiItemBaseInput
|
<MultiItemBaseInput
|
||||||
instanceId={instanceId}
|
instanceId={instanceId}
|
||||||
@@ -243,13 +285,23 @@ export const MultiItemFieldInput = <T,>({
|
|||||||
? handleInputChange(turnIntoEmptyStringIfWhitespacesOnly(value))
|
? handleInputChange(turnIntoEmptyStringIfWhitespacesOnly(value))
|
||||||
: handleInputChange('');
|
: handleInputChange('');
|
||||||
}}
|
}}
|
||||||
onEnter={handleSubmitInput}
|
onEnter={() => {
|
||||||
|
handleSubmitChanges();
|
||||||
|
if (shouldAutoEnterBecauseOnlyOneItemIsAllowed) {
|
||||||
|
handleAutoEnter();
|
||||||
|
}
|
||||||
|
}}
|
||||||
hasItem={!!items.length}
|
hasItem={!!items.length}
|
||||||
rightComponent={
|
rightComponent={
|
||||||
items.length ? (
|
items.length ? (
|
||||||
<LightIconButton
|
<LightIconButton
|
||||||
Icon={isAddingNewItem ? IconPlus : IconCheck}
|
Icon={isAddingNewItem ? IconPlus : IconCheck}
|
||||||
onClick={handleSubmitInput}
|
onClick={() => {
|
||||||
|
handleSubmitChanges();
|
||||||
|
if (shouldAutoEnterBecauseOnlyOneItemIsAllowed) {
|
||||||
|
handleAutoEnter();
|
||||||
|
}
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
) : null
|
) : null
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-16
@@ -51,35 +51,23 @@ export const MultiItemFieldMenuItem = <T,>({
|
|||||||
setIsHovered(false);
|
setIsHovered(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDeleteClick = (event: React.MouseEvent<HTMLDivElement>) => {
|
const handleDeleteClick = () => {
|
||||||
event.stopPropagation();
|
|
||||||
event.preventDefault();
|
|
||||||
|
|
||||||
closeDropdown(dropdownId);
|
closeDropdown(dropdownId);
|
||||||
setIsHovered(false);
|
setIsHovered(false);
|
||||||
onDelete?.();
|
onDelete?.();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSetAsPrimaryClick = (event: React.MouseEvent<HTMLDivElement>) => {
|
const handleSetAsPrimaryClick = () => {
|
||||||
event.stopPropagation();
|
|
||||||
event.preventDefault();
|
|
||||||
|
|
||||||
closeDropdown(dropdownId);
|
closeDropdown(dropdownId);
|
||||||
onSetAsPrimary?.();
|
onSetAsPrimary?.();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleEditClick = (event: React.MouseEvent<HTMLDivElement>) => {
|
const handleEditClick = () => {
|
||||||
event.stopPropagation();
|
|
||||||
event.preventDefault();
|
|
||||||
|
|
||||||
closeDropdown(dropdownId);
|
closeDropdown(dropdownId);
|
||||||
onEdit?.();
|
onEdit?.();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCopyClick = (event: React.MouseEvent<HTMLDivElement>) => {
|
const handleCopyClick = () => {
|
||||||
event.stopPropagation();
|
|
||||||
event.preventDefault();
|
|
||||||
|
|
||||||
closeDropdown(dropdownId);
|
closeDropdown(dropdownId);
|
||||||
onCopy?.(value);
|
onCopy?.(value);
|
||||||
};
|
};
|
||||||
|
|||||||
+44
-26
@@ -11,14 +11,18 @@ import 'react-phone-number-input/style.css';
|
|||||||
import { MultiItemFieldInput } from './MultiItemFieldInput';
|
import { MultiItemFieldInput } from './MultiItemFieldInput';
|
||||||
|
|
||||||
import { FieldInputEventContext } from '@/object-record/record-field/ui/contexts/FieldInputEventContext';
|
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 { 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 { phonesSchema } from '@/object-record/record-field/ui/types/guards/isFieldPhonesValue';
|
||||||
import { PhoneCountryPickerDropdownButton } from '@/ui/input/components/internal/phone/components/PhoneCountryPickerDropdownButton';
|
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 { css } from '@emotion/react';
|
||||||
import { useContext } from 'react';
|
import { useContext } from 'react';
|
||||||
import { MULTI_ITEM_FIELD_DEFAULT_MAX_VALUES } from 'twenty-shared/constants';
|
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 { TEXT_INPUT_STYLE } from 'twenty-ui/theme';
|
||||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||||
import { stripSimpleQuotesFromString } from '~/utils/string/stripSimpleQuotesFromString';
|
import { stripSimpleQuotesFromString } from '~/utils/string/stripSimpleQuotesFromString';
|
||||||
@@ -75,15 +79,13 @@ const StyledCustomPhoneInput = styled(ReactPhoneNumberInput)`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
export const PhonesFieldInput = () => {
|
export const PhonesFieldInput = () => {
|
||||||
const { getLatestDraftValue, fieldDefinition, setDraftValue, draftValue } =
|
const { fieldDefinition, setDraftValue, draftValue } = usePhonesField();
|
||||||
usePhonesField();
|
|
||||||
|
|
||||||
const { onEscape, onClickOutside } = useContext(FieldInputEventContext);
|
const { onEscape, onClickOutside, onEnter } = useContext(
|
||||||
|
FieldInputEventContext,
|
||||||
|
);
|
||||||
|
|
||||||
const phones = createPhonesFromFieldValue(draftValue);
|
const phones = createPhonesFromFieldValue(draftValue);
|
||||||
const instanceId = useAvailableComponentInstanceIdOrThrow(
|
|
||||||
RecordFieldComponentInstanceContext,
|
|
||||||
);
|
|
||||||
|
|
||||||
const defaultCountry = stripSimpleQuotesFromString(
|
const defaultCountry = stripSimpleQuotesFromString(
|
||||||
fieldDefinition?.defaultValue?.primaryPhoneCountryCode,
|
fieldDefinition?.defaultValue?.primaryPhoneCountryCode,
|
||||||
@@ -93,26 +95,26 @@ export const PhonesFieldInput = () => {
|
|||||||
fieldDefinition.metadata.settings?.maxNumberOfValues ??
|
fieldDefinition.metadata.settings?.maxNumberOfValues ??
|
||||||
MULTI_ITEM_FIELD_DEFAULT_MAX_VALUES;
|
MULTI_ITEM_FIELD_DEFAULT_MAX_VALUES;
|
||||||
|
|
||||||
const handlePhonesChange = (
|
const parseArrayToPhonesValue = (phones: PhoneRecord[]) => {
|
||||||
updatedPhones: {
|
const [nextPrimaryPhone, ...nextAdditionalPhones] = phones;
|
||||||
number: string;
|
|
||||||
countryCode: string;
|
|
||||||
callingCode: string;
|
|
||||||
}[],
|
|
||||||
) => {
|
|
||||||
const [nextPrimaryPhone, ...nextAdditionalPhones] = updatedPhones;
|
|
||||||
|
|
||||||
const newValue = {
|
const nextValue: FieldPhonesValue = {
|
||||||
primaryPhoneNumber: nextPrimaryPhone?.number ?? '',
|
primaryPhoneNumber: nextPrimaryPhone?.number ?? '',
|
||||||
primaryPhoneCountryCode: nextPrimaryPhone?.countryCode ?? '',
|
primaryPhoneCountryCode: nextPrimaryPhone?.countryCode ?? '',
|
||||||
primaryPhoneCallingCode: nextPrimaryPhone?.callingCode ?? '',
|
primaryPhoneCallingCode: nextPrimaryPhone?.callingCode ?? '',
|
||||||
additionalPhones: nextAdditionalPhones,
|
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) {
|
if (isDefined(nextValue)) {
|
||||||
setDraftValue(newValidatedPhoneResponse.data);
|
setDraftValue(nextValue);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -134,15 +136,21 @@ export const PhonesFieldInput = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleClickOutside = (
|
const handleClickOutside = (
|
||||||
_newValue: any,
|
updatedPhones: PhoneRecord[],
|
||||||
event: MouseEvent | TouchEvent,
|
event: MouseEvent | TouchEvent,
|
||||||
) => {
|
) => {
|
||||||
const latestDraftValue = getLatestDraftValue(instanceId);
|
onClickOutside?.({
|
||||||
onClickOutside?.({ newValue: latestDraftValue, event });
|
newValue: parseArrayToPhonesValue(updatedPhones),
|
||||||
|
event,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleEscape = (_newValue: any) => {
|
const handleEscape = (updatedPhones: PhoneRecord[]) => {
|
||||||
onEscape?.({ newValue: draftValue });
|
onEscape?.({ newValue: parseArrayToPhonesValue(updatedPhones) });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEnter = (updatedPhones: PhoneRecord[]) => {
|
||||||
|
onEnter?.({ newValue: parseArrayToPhonesValue(updatedPhones) });
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -151,10 +159,19 @@ export const PhonesFieldInput = () => {
|
|||||||
onChange={handlePhonesChange}
|
onChange={handlePhonesChange}
|
||||||
onClickOutside={handleClickOutside}
|
onClickOutside={handleClickOutside}
|
||||||
onEscape={handleEscape}
|
onEscape={handleEscape}
|
||||||
|
onEnter={handleEnter}
|
||||||
placeholder="Phone"
|
placeholder="Phone"
|
||||||
fieldMetadataType={FieldMetadataType.PHONES}
|
fieldMetadataType={FieldMetadataType.PHONES}
|
||||||
validateInput={validateInput}
|
validateInput={validateInput}
|
||||||
formatInput={(input) => {
|
formatInput={(input) => {
|
||||||
|
if (input === '') {
|
||||||
|
return {
|
||||||
|
number: '',
|
||||||
|
callingCode: '',
|
||||||
|
countryCode: '',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const phone = parsePhoneNumber(input);
|
const phone = parsePhoneNumber(input);
|
||||||
if (phone !== undefined) {
|
if (phone !== undefined) {
|
||||||
return {
|
return {
|
||||||
@@ -163,6 +180,7 @@ export const PhonesFieldInput = () => {
|
|||||||
countryCode: phone.country as string,
|
countryCode: phone.country as string,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
number: '',
|
number: '',
|
||||||
callingCode: '',
|
callingCode: '',
|
||||||
@@ -178,7 +196,7 @@ export const PhonesFieldInput = () => {
|
|||||||
}) => (
|
}) => (
|
||||||
<PhonesFieldMenuItem
|
<PhonesFieldMenuItem
|
||||||
key={index}
|
key={index}
|
||||||
dropdownId={`phones-field-input-${fieldDefinition.metadata.fieldName}-${index}`}
|
dropdownId={`${MULTI_ITEM_FIELD_INPUT_DROPDOWN_ID_PREFIX}-${fieldDefinition.metadata.fieldName}-${index}`}
|
||||||
showPrimaryIcon={getShowPrimaryIcon(index)}
|
showPrimaryIcon={getShowPrimaryIcon(index)}
|
||||||
showSetAsPrimaryButton={getShowSetAsPrimaryButton(index)}
|
showSetAsPrimaryButton={getShowSetAsPrimaryButton(index)}
|
||||||
phone={phone}
|
phone={phone}
|
||||||
|
|||||||
+2
@@ -0,0 +1,2 @@
|
|||||||
|
export const MULTI_ITEM_FIELD_INPUT_DROPDOWN_ID_PREFIX =
|
||||||
|
'multi-item-field-input-dropdown';
|
||||||
+4
@@ -97,6 +97,10 @@ export const useGetRecordFilterDisplayValue = () => {
|
|||||||
case RecordFilterOperand.IS_BEFORE: {
|
case RecordFilterOperand.IS_BEFORE: {
|
||||||
const pointInTime = new Date(recordFilter.value);
|
const pointInTime = new Date(recordFilter.value);
|
||||||
|
|
||||||
|
if (!isValid(pointInTime)) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
const { displayValue } = getDateTimeFilterDisplayValue(pointInTime);
|
const { displayValue } = getDateTimeFilterDisplayValue(pointInTime);
|
||||||
|
|
||||||
return `${displayValue}`;
|
return `${displayValue}`;
|
||||||
|
|||||||
+4
-5
@@ -7,6 +7,7 @@ import { useEffect, useState } from 'react';
|
|||||||
|
|
||||||
import { PhoneCountryPickerDropdownSelect } from './PhoneCountryPickerDropdownSelect';
|
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 { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
|
||||||
import { isDropdownOpenComponentState } from '@/ui/layout/dropdown/states/isDropdownOpenComponentState';
|
import { isDropdownOpenComponentState } from '@/ui/layout/dropdown/states/isDropdownOpenComponentState';
|
||||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||||
@@ -77,17 +78,15 @@ export const PhoneCountryPickerDropdownButton = ({
|
|||||||
|
|
||||||
const [selectedCountry, setSelectedCountry] = useState<Country>();
|
const [selectedCountry, setSelectedCountry] = useState<Country>();
|
||||||
|
|
||||||
const dropdownId = 'country-picker-dropdown-id';
|
|
||||||
|
|
||||||
const isDropdownOpen = useRecoilComponentValue(
|
const isDropdownOpen = useRecoilComponentValue(
|
||||||
isDropdownOpenComponentState,
|
isDropdownOpenComponentState,
|
||||||
dropdownId,
|
PHONE_COUNTRY_CODE_PICKER_DROPDOWN_ID,
|
||||||
);
|
);
|
||||||
|
|
||||||
const { closeDropdown } = useCloseDropdown();
|
const { closeDropdown } = useCloseDropdown();
|
||||||
|
|
||||||
const handleChange = (countryCode: string) => {
|
const handleChange = (countryCode: string) => {
|
||||||
closeDropdown(dropdownId);
|
closeDropdown(PHONE_COUNTRY_CODE_PICKER_DROPDOWN_ID);
|
||||||
onChange(countryCode);
|
onChange(countryCode);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -102,7 +101,7 @@ export const PhoneCountryPickerDropdownButton = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Dropdown
|
<Dropdown
|
||||||
dropdownId="country-picker-dropdown-id"
|
dropdownId={PHONE_COUNTRY_CODE_PICKER_DROPDOWN_ID}
|
||||||
clickableComponent={
|
clickableComponent={
|
||||||
<StyledDropdownButtonContainer isUnfolded={isDropdownOpen}>
|
<StyledDropdownButtonContainer isUnfolded={isDropdownOpen}>
|
||||||
<StyledIconContainer>
|
<StyledIconContainer>
|
||||||
|
|||||||
+2
@@ -0,0 +1,2 @@
|
|||||||
|
export const PHONE_COUNTRY_CODE_PICKER_DROPDOWN_ID =
|
||||||
|
'phone-country-picker-dropdown-id';
|
||||||
Reference in New Issue
Block a user