feat: remove a link from a Links field (#5313)
Closes #5117 TO FIX in another PR: right now, the "Vertical Dots" LightIconButton inside the Dropdown menu sometimes needs to be clicked twice to open the nested dropdown, not sure why 🤔 Maybe an `event.preventDefault()` is needed somewhere? <img width="369" alt="image" src="https://github.com/twentyhq/twenty/assets/3098428/dd0c771a-c18d-4eb2-8ed6-b107f56711e9"> --------- Co-authored-by: Jérémy Magrin <jeremy.magrin@gmail.com> Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
@ -4,8 +4,8 @@ import { Key } from 'ts-key-enum';
|
||||
import { IconPlus } from 'twenty-ui';
|
||||
|
||||
import { useLinksField } from '@/object-record/record-field/meta-types/hooks/useLinksField';
|
||||
import { LinksFieldMenuItem } from '@/object-record/record-field/meta-types/input/components/LinksFieldMenuItem';
|
||||
import { FieldInputEvent } from '@/object-record/record-field/types/FieldInputEvent';
|
||||
import { LinkDisplay } from '@/ui/field/display/components/LinkDisplay';
|
||||
import { LightIconButton } from '@/ui/input/button/components/LightIconButton';
|
||||
import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu';
|
||||
import { DropdownMenuInput } from '@/ui/layout/dropdown/components/DropdownMenuInput';
|
||||
@ -14,6 +14,7 @@ import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownM
|
||||
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 { toSpliced } from '~/utils/array/toSpliced';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
const StyledDropdownMenu = styled(DropdownMenu)`
|
||||
@ -35,7 +36,7 @@ export const LinksFieldInput = ({
|
||||
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const links = useMemo(
|
||||
const links = useMemo<{ url: string; label: string }[]>(
|
||||
() =>
|
||||
[
|
||||
fieldValue.primaryLinkUrl
|
||||
@ -53,51 +54,47 @@ export const LinksFieldInput = ({
|
||||
],
|
||||
);
|
||||
|
||||
const handleDropdownClose = () => {
|
||||
onCancel?.();
|
||||
};
|
||||
|
||||
useListenClickOutside({
|
||||
refs: [containerRef],
|
||||
callback: (event) => {
|
||||
event.stopImmediatePropagation();
|
||||
|
||||
const isTargetInput =
|
||||
event.target instanceof HTMLInputElement &&
|
||||
event.target.tagName === 'INPUT';
|
||||
|
||||
if (!isTargetInput) {
|
||||
onCancel?.();
|
||||
}
|
||||
},
|
||||
callback: handleDropdownClose,
|
||||
});
|
||||
|
||||
useScopedHotkeys(Key.Escape, handleDropdownClose, hotkeyScope);
|
||||
|
||||
const [isInputDisplayed, setIsInputDisplayed] = useState(false);
|
||||
const [inputValue, setInputValue] = useState('');
|
||||
|
||||
useScopedHotkeys(Key.Escape, onCancel ?? (() => {}), hotkeyScope);
|
||||
|
||||
const handleSubmit = () => {
|
||||
const handleAddLink = () => {
|
||||
if (!inputValue) return;
|
||||
|
||||
setIsInputDisplayed(false);
|
||||
setInputValue('');
|
||||
|
||||
if (!links.length) {
|
||||
onSubmit?.(() =>
|
||||
persistLinksField({
|
||||
primaryLinkUrl: inputValue,
|
||||
primaryLinkLabel: '',
|
||||
secondaryLinks: [],
|
||||
}),
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
const nextLinks = [...links, { label: '', url: inputValue }];
|
||||
const [nextPrimaryLink, ...nextSecondaryLinks] = nextLinks;
|
||||
|
||||
onSubmit?.(() =>
|
||||
persistLinksField({
|
||||
primaryLinkUrl: nextPrimaryLink.url ?? '',
|
||||
primaryLinkLabel: nextPrimaryLink.label ?? '',
|
||||
secondaryLinks: nextSecondaryLinks,
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const handleDeleteLink = (index: number) => {
|
||||
onSubmit?.(() =>
|
||||
persistLinksField({
|
||||
...fieldValue,
|
||||
secondaryLinks: [
|
||||
...(fieldValue.secondaryLinks ?? []),
|
||||
{ label: '', url: inputValue },
|
||||
],
|
||||
secondaryLinks: toSpliced(
|
||||
fieldValue.secondaryLinks ?? [],
|
||||
index - 1,
|
||||
1,
|
||||
),
|
||||
}),
|
||||
);
|
||||
};
|
||||
@ -108,9 +105,13 @@ export const LinksFieldInput = ({
|
||||
<>
|
||||
<DropdownMenuItemsContainer>
|
||||
{links.map(({ label, url }, index) => (
|
||||
<MenuItem
|
||||
<LinksFieldMenuItem
|
||||
key={index}
|
||||
text={<LinkDisplay value={{ label, url }} />}
|
||||
dropdownId={`${hotkeyScope}-links-${index}`}
|
||||
isPrimary={index === 0}
|
||||
label={label}
|
||||
onDelete={() => handleDeleteLink(index)}
|
||||
url={url}
|
||||
/>
|
||||
))}
|
||||
</DropdownMenuItemsContainer>
|
||||
@ -124,9 +125,9 @@ export const LinksFieldInput = ({
|
||||
value={inputValue}
|
||||
hotkeyScope={hotkeyScope}
|
||||
onChange={(event) => setInputValue(event.target.value)}
|
||||
onEnter={handleSubmit}
|
||||
onEnter={handleAddLink}
|
||||
rightComponent={
|
||||
<LightIconButton Icon={IconPlus} onClick={handleSubmit} />
|
||||
<LightIconButton Icon={IconPlus} onClick={handleAddLink} />
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
|
||||
@ -0,0 +1,77 @@
|
||||
import styled from '@emotion/styled';
|
||||
import {
|
||||
IconBookmark,
|
||||
IconComponent,
|
||||
IconDotsVertical,
|
||||
IconTrash,
|
||||
} from 'twenty-ui';
|
||||
|
||||
import { LinkDisplay } from '@/ui/field/display/components/LinkDisplay';
|
||||
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
|
||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
|
||||
import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
|
||||
|
||||
type LinksFieldMenuItemProps = {
|
||||
dropdownId: string;
|
||||
isPrimary?: boolean;
|
||||
label: string;
|
||||
onDelete: () => void;
|
||||
url: string;
|
||||
};
|
||||
|
||||
const StyledIconBookmark = styled(IconBookmark)`
|
||||
color: ${({ theme }) => theme.font.color.light};
|
||||
height: ${({ theme }) => theme.icon.size.sm}px;
|
||||
width: ${({ theme }) => theme.icon.size.sm}px;
|
||||
`;
|
||||
|
||||
export const LinksFieldMenuItem = ({
|
||||
dropdownId,
|
||||
isPrimary,
|
||||
label,
|
||||
onDelete,
|
||||
url,
|
||||
}: LinksFieldMenuItemProps) => {
|
||||
const { isDropdownOpen } = useDropdown(dropdownId);
|
||||
|
||||
return (
|
||||
<MenuItem
|
||||
text={<LinkDisplay value={{ label, url }} />}
|
||||
isIconDisplayedOnHoverOnly={!isPrimary && !isDropdownOpen}
|
||||
iconButtons={[
|
||||
{
|
||||
Wrapper: isPrimary
|
||||
? undefined
|
||||
: ({ iconButton }) => (
|
||||
<Dropdown
|
||||
dropdownId={dropdownId}
|
||||
dropdownHotkeyScope={{
|
||||
scope: dropdownId,
|
||||
}}
|
||||
dropdownPlacement="right-start"
|
||||
dropdownStrategy="fixed"
|
||||
disableBlur
|
||||
clickableComponent={iconButton}
|
||||
dropdownComponents={
|
||||
<DropdownMenuItemsContainer>
|
||||
<MenuItem
|
||||
accent="danger"
|
||||
LeftIcon={IconTrash}
|
||||
text="Delete"
|
||||
onClick={onDelete}
|
||||
/>
|
||||
</DropdownMenuItemsContainer>
|
||||
}
|
||||
/>
|
||||
),
|
||||
Icon: isPrimary
|
||||
? (StyledIconBookmark as IconComponent)
|
||||
: IconDotsVertical,
|
||||
accent: 'tertiary',
|
||||
onClick: isPrimary ? undefined : () => {},
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user