mirror of
https://github.com/twentyhq/twenty.git
synced 2024-11-22 03:17:40 +03:00
WIP
This commit is contained in:
parent
d61abf9564
commit
4cc8769d7a
@ -0,0 +1,212 @@
|
||||
import React, { useRef, useState } from 'react';
|
||||
import { Key } from 'ts-key-enum';
|
||||
import { IconCheck, IconPlus, LightIconButton } from 'twenty-ui';
|
||||
|
||||
import { PhoneRecord } from '@/object-record/record-field/types/FieldMetadata';
|
||||
import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu';
|
||||
import {
|
||||
DropdownMenuInput,
|
||||
DropdownMenuInputProps,
|
||||
} from '@/ui/layout/dropdown/components/DropdownMenuInput';
|
||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
|
||||
import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
|
||||
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
||||
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
import { moveArrayItem } from '~/utils/array/moveArrayItem';
|
||||
import { toSpliced } from '~/utils/array/toSpliced';
|
||||
import { turnIntoEmptyStringIfWhitespacesOnly } from '~/utils/string/turnIntoEmptyStringIfWhitespacesOnly';
|
||||
|
||||
type MultiItemFieldInputV2Props<T> = {
|
||||
items: T[];
|
||||
onPersist: (updatedItems: T[]) => void;
|
||||
onCancel?: () => void;
|
||||
placeholder: string;
|
||||
validateInput?: (input: string) => { isValid: boolean; errorMessage: string };
|
||||
formatInput?: (input: string) => T;
|
||||
renderItem: (props: {
|
||||
value: T;
|
||||
index: number;
|
||||
handleEdit: () => void;
|
||||
handleSetPrimary: () => void;
|
||||
handleDelete: () => void;
|
||||
}) => React.ReactNode;
|
||||
hotkeyScope: string;
|
||||
newItemLabel?: string;
|
||||
fieldMetadataType: FieldMetadataType;
|
||||
renderInput?: DropdownMenuInputProps['renderInput'];
|
||||
};
|
||||
|
||||
// Todo: the API of this component does not look healthy: we have renderInput, renderItem, formatInput, ...
|
||||
// This should be refactored with a hook instead that exposes those events in a context around this component and its children.
|
||||
export const MultiItemFieldInputV2 = <T,>({
|
||||
items,
|
||||
onPersist,
|
||||
onCancel,
|
||||
placeholder,
|
||||
validateInput,
|
||||
formatInput,
|
||||
renderItem,
|
||||
hotkeyScope,
|
||||
newItemLabel,
|
||||
fieldMetadataType,
|
||||
renderInput,
|
||||
}: MultiItemFieldInputV2Props<T>) => {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const handleDropdownClose = () => {
|
||||
onCancel?.();
|
||||
};
|
||||
|
||||
useListenClickOutside({
|
||||
refs: [containerRef],
|
||||
callback: handleDropdownClose,
|
||||
});
|
||||
|
||||
useScopedHotkeys(Key.Escape, handleDropdownClose, hotkeyScope);
|
||||
|
||||
const [isInputDisplayed, setIsInputDisplayed] = useState(false);
|
||||
const [inputValue, setInputValue] = useState('');
|
||||
const [itemToEditIndex, setItemToEditIndex] = useState(-1);
|
||||
const [errorData, setErrorData] = useState({
|
||||
isValid: true,
|
||||
errorMessage: '',
|
||||
});
|
||||
const isAddingNewItem = itemToEditIndex === -1;
|
||||
|
||||
const handleOnChange = (value: string) => {
|
||||
setInputValue(value);
|
||||
if (!validateInput) return;
|
||||
|
||||
setErrorData(
|
||||
errorData.isValid ? errorData : { isValid: true, errorMessage: '' },
|
||||
);
|
||||
};
|
||||
|
||||
const handleAddButtonClick = () => {
|
||||
setItemToEditIndex(-1);
|
||||
setIsInputDisplayed(true);
|
||||
};
|
||||
|
||||
// Ugly
|
||||
const handleEditButtonClick = (index: number) => {
|
||||
let item;
|
||||
switch (fieldMetadataType) {
|
||||
case FieldMetadataType.Links:
|
||||
item = items[index] as { label: string; url: string };
|
||||
setInputValue(item.url || '');
|
||||
break;
|
||||
case FieldMetadataType.Phones:
|
||||
item = items[index] as PhoneRecord;
|
||||
setInputValue(item.countryCode + item.number);
|
||||
break;
|
||||
case FieldMetadataType.Emails:
|
||||
item = items[index] as string;
|
||||
setInputValue(item);
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unsupported field type: ${fieldMetadataType}`);
|
||||
}
|
||||
|
||||
setItemToEditIndex(index);
|
||||
setIsInputDisplayed(true);
|
||||
};
|
||||
|
||||
const handleSubmitInput = () => {
|
||||
if (validateInput !== undefined) {
|
||||
const validationData = validateInput(inputValue) ?? { isValid: true };
|
||||
if (!validationData.isValid) {
|
||||
setErrorData(validationData);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const newItem = formatInput
|
||||
? formatInput(inputValue)
|
||||
: (inputValue as unknown as T);
|
||||
|
||||
if (!isAddingNewItem && newItem === items[itemToEditIndex]) {
|
||||
setIsInputDisplayed(false);
|
||||
setInputValue('');
|
||||
return;
|
||||
}
|
||||
|
||||
const updatedItems = isAddingNewItem
|
||||
? [...items, newItem]
|
||||
: toSpliced(items, itemToEditIndex, 1, newItem);
|
||||
|
||||
onPersist(updatedItems);
|
||||
setIsInputDisplayed(false);
|
||||
setInputValue('');
|
||||
};
|
||||
|
||||
const handleSetPrimaryItem = (index: number) => {
|
||||
const updatedItems = moveArrayItem(items, { fromIndex: index, toIndex: 0 });
|
||||
onPersist(updatedItems);
|
||||
};
|
||||
|
||||
const handleDeleteItem = (index: number) => {
|
||||
const updatedItems = toSpliced(items, index, 1);
|
||||
onPersist(updatedItems);
|
||||
};
|
||||
|
||||
return (
|
||||
<DropdownMenu ref={containerRef} width={200}>
|
||||
{!!items.length && (
|
||||
<>
|
||||
<DropdownMenuItemsContainer>
|
||||
{items.map((item, index) =>
|
||||
renderItem({
|
||||
value: item,
|
||||
index,
|
||||
handleEdit: () => handleEditButtonClick(index),
|
||||
handleSetPrimary: () => handleSetPrimaryItem(index),
|
||||
handleDelete: () => handleDeleteItem(index),
|
||||
}),
|
||||
)}
|
||||
</DropdownMenuItemsContainer>
|
||||
<DropdownMenuSeparator />
|
||||
</>
|
||||
)}
|
||||
{isInputDisplayed || !items.length ? (
|
||||
<DropdownMenuInput
|
||||
autoFocus
|
||||
placeholder={placeholder}
|
||||
value={inputValue}
|
||||
hotkeyScope={hotkeyScope}
|
||||
hasError={!errorData.isValid}
|
||||
renderInput={
|
||||
renderInput
|
||||
? (props) =>
|
||||
renderInput({
|
||||
...props,
|
||||
onChange: (newValue) =>
|
||||
setInputValue(newValue as unknown as string),
|
||||
})
|
||||
: undefined
|
||||
}
|
||||
onChange={(event) =>
|
||||
handleOnChange(
|
||||
turnIntoEmptyStringIfWhitespacesOnly(event.target.value),
|
||||
)
|
||||
}
|
||||
onEnter={handleSubmitInput}
|
||||
rightComponent={
|
||||
<LightIconButton
|
||||
Icon={isAddingNewItem ? IconPlus : IconCheck}
|
||||
onClick={handleSubmitInput}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<DropdownMenuItemsContainer>
|
||||
<MenuItem
|
||||
onClick={handleAddButtonClick}
|
||||
LeftIcon={IconPlus}
|
||||
text={newItemLabel || `Add ${placeholder}`}
|
||||
/>
|
||||
</DropdownMenuItemsContainer>
|
||||
)}
|
||||
</DropdownMenu>
|
||||
);
|
||||
};
|
@ -77,6 +77,8 @@ export type DropdownMenuInputProps = HTMLInputProps & {
|
||||
hasError?: boolean;
|
||||
};
|
||||
|
||||
// TODO: refactor this
|
||||
/** @deprecated */
|
||||
export const DropdownMenuInput = forwardRef<
|
||||
HTMLInputElement,
|
||||
DropdownMenuInputProps
|
||||
|
Loading…
Reference in New Issue
Block a user