Skip to content

Commit 507770b

Browse files
committed
feat: remove a link from a Links field
Closes #5117
1 parent 8b07898 commit 507770b

File tree

11 files changed

+156
-50
lines changed

11 files changed

+156
-50
lines changed

packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/LinksFieldInput.tsx

+32-21
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ import { Key } from 'ts-key-enum';
44
import { IconPlus } from 'twenty-ui';
55

66
import { useLinksField } from '@/object-record/record-field/meta-types/hooks/useLinksField';
7+
import { LinksFieldMenuItem } from '@/object-record/record-field/meta-types/input/components/LinksFieldMenuItem';
78
import { FieldInputEvent } from '@/object-record/record-field/types/FieldInputEvent';
8-
import { LinkDisplay } from '@/ui/field/display/components/LinkDisplay';
99
import { LightIconButton } from '@/ui/input/button/components/LightIconButton';
1010
import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu';
1111
import { DropdownMenuInput } from '@/ui/layout/dropdown/components/DropdownMenuInput';
@@ -35,13 +35,14 @@ export const LinksFieldInput = ({
3535

3636
const containerRef = useRef<HTMLDivElement>(null);
3737

38-
const links = useMemo(
38+
const links = useMemo<{ url: string; label: string; isPrimary?: boolean }[]>(
3939
() =>
4040
[
4141
fieldValue.primaryLinkUrl
4242
? {
4343
url: fieldValue.primaryLinkUrl,
4444
label: fieldValue.primaryLinkLabel,
45+
isPrimary: true,
4546
}
4647
: null,
4748
...(fieldValue.secondaryLinks ?? []),
@@ -53,27 +54,21 @@ export const LinksFieldInput = ({
5354
],
5455
);
5556

57+
const handleDropdownClose = () => {
58+
onCancel?.();
59+
};
60+
5661
useListenClickOutside({
5762
refs: [containerRef],
58-
callback: (event) => {
59-
event.stopImmediatePropagation();
60-
61-
const isTargetInput =
62-
event.target instanceof HTMLInputElement &&
63-
event.target.tagName === 'INPUT';
64-
65-
if (!isTargetInput) {
66-
onCancel?.();
67-
}
68-
},
63+
callback: handleDropdownClose,
6964
});
7065

66+
useScopedHotkeys(Key.Escape, handleDropdownClose, hotkeyScope);
67+
7168
const [isInputDisplayed, setIsInputDisplayed] = useState(false);
7269
const [inputValue, setInputValue] = useState('');
7370

74-
useScopedHotkeys(Key.Escape, onCancel ?? (() => {}), hotkeyScope);
75-
76-
const handleSubmit = () => {
71+
const handleAddLink = () => {
7772
if (!inputValue) return;
7873

7974
setIsInputDisplayed(false);
@@ -102,15 +97,31 @@ export const LinksFieldInput = ({
10297
);
10398
};
10499

100+
const handleDeleteLink = (index: number) => {
101+
const nextSecondaryLinks = [...(fieldValue.secondaryLinks ?? [])];
102+
nextSecondaryLinks.splice(index - 1, 1);
103+
104+
onSubmit?.(() =>
105+
persistLinksField({
106+
...fieldValue,
107+
secondaryLinks: nextSecondaryLinks,
108+
}),
109+
);
110+
};
111+
105112
return (
106113
<StyledDropdownMenu ref={containerRef} width={200}>
107114
{!!links.length && (
108115
<>
109116
<DropdownMenuItemsContainer>
110-
{links.map(({ label, url }, index) => (
111-
<MenuItem
117+
{links.map(({ isPrimary, label, url }, index) => (
118+
<LinksFieldMenuItem
112119
key={index}
113-
text={<LinkDisplay value={{ label, url }} />}
120+
dropdownId={`${hotkeyScope}-links-${index}`}
121+
isPrimary={isPrimary}
122+
label={label}
123+
onDelete={() => handleDeleteLink(index)}
124+
url={url}
114125
/>
115126
))}
116127
</DropdownMenuItemsContainer>
@@ -124,9 +135,9 @@ export const LinksFieldInput = ({
124135
value={inputValue}
125136
hotkeyScope={hotkeyScope}
126137
onChange={(event) => setInputValue(event.target.value)}
127-
onEnter={handleSubmit}
138+
onEnter={handleAddLink}
128139
rightComponent={
129-
<LightIconButton Icon={IconPlus} onClick={handleSubmit} />
140+
<LightIconButton Icon={IconPlus} onClick={handleAddLink} />
130141
}
131142
/>
132143
) : (
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import styled from '@emotion/styled';
2+
import {
3+
IconBookmark,
4+
IconComponent,
5+
IconDotsVertical,
6+
IconTrash,
7+
} from 'twenty-ui';
8+
9+
import { LinkDisplay } from '@/ui/field/display/components/LinkDisplay';
10+
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
11+
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
12+
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
13+
import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
14+
15+
type LinksFieldMenuItemProps = {
16+
dropdownId: string;
17+
isPrimary?: boolean;
18+
label: string;
19+
onDelete: () => void;
20+
url: string;
21+
};
22+
23+
const StyledIconBookmark = styled(IconBookmark)`
24+
color: ${({ theme }) => theme.font.color.light};
25+
height: ${({ theme }) => theme.icon.size.sm}px;
26+
width: ${({ theme }) => theme.icon.size.sm}px;
27+
`;
28+
29+
export const LinksFieldMenuItem = ({
30+
dropdownId,
31+
isPrimary,
32+
label,
33+
onDelete,
34+
url,
35+
}: LinksFieldMenuItemProps) => {
36+
const { isDropdownOpen } = useDropdown(dropdownId);
37+
38+
return (
39+
<MenuItem
40+
text={<LinkDisplay value={{ label, url }} />}
41+
isIconDisplayedOnHoverOnly={!isPrimary && !isDropdownOpen}
42+
iconButtons={[
43+
{
44+
Wrapper: isPrimary
45+
? undefined
46+
: ({ iconButton }) => (
47+
<Dropdown
48+
dropdownId={dropdownId}
49+
dropdownHotkeyScope={{
50+
scope: dropdownId,
51+
}}
52+
dropdownPlacement="right-start"
53+
dropdownStrategy="fixed"
54+
disableBlur
55+
clickableComponent={iconButton}
56+
dropdownComponents={
57+
<DropdownMenuItemsContainer>
58+
<MenuItem
59+
accent="danger"
60+
LeftIcon={IconTrash}
61+
text="Delete"
62+
onClick={onDelete}
63+
/>
64+
</DropdownMenuItemsContainer>
65+
}
66+
/>
67+
),
68+
Icon: isPrimary
69+
? (StyledIconBookmark as IconComponent)
70+
: IconDotsVertical,
71+
accent: 'tertiary',
72+
onClick: isPrimary ? undefined : () => {},
73+
},
74+
]}
75+
/>
76+
);
77+
};

packages/twenty-front/src/modules/ui/input/button/components/LightIconButtonGroup.tsx

+15-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { MouseEvent } from 'react';
1+
import { FunctionComponent, MouseEvent, ReactElement } from 'react';
22
import styled from '@emotion/styled';
33
import { IconComponent } from 'twenty-ui';
44

@@ -14,7 +14,9 @@ export type LightIconButtonGroupProps = Pick<
1414
'className' | 'size'
1515
> & {
1616
iconButtons: {
17+
Wrapper?: FunctionComponent<{ iconButton: ReactElement }>;
1718
Icon: IconComponent;
19+
accent?: LightIconButtonProps['accent'];
1820
onClick?: (event: MouseEvent<any>) => void;
1921
disabled?: boolean;
2022
}[];
@@ -26,16 +28,26 @@ export const LightIconButtonGroup = ({
2628
className,
2729
}: LightIconButtonGroupProps) => (
2830
<StyledLightIconButtonGroupContainer className={className}>
29-
{iconButtons.map(({ Icon, onClick }, index) => {
30-
return (
31+
{iconButtons.map(({ Wrapper, Icon, accent, onClick }, index) => {
32+
const iconButton = (
3133
<LightIconButton
3234
key={`light-icon-button-${index}`}
3335
Icon={Icon}
36+
accent={accent}
3437
disabled={!onClick}
3538
onClick={onClick}
3639
size={size}
3740
/>
3841
);
42+
43+
return Wrapper ? (
44+
<Wrapper
45+
key={`light-icon-button-wrapper-${index}`}
46+
iconButton={iconButton}
47+
/>
48+
) : (
49+
iconButton
50+
);
3951
})}
4052
</StyledLightIconButtonGroupContainer>
4153
);

packages/twenty-front/src/modules/ui/input/components/internal/date/components/InternalDatePicker.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -460,7 +460,7 @@ export const InternalDatePicker = ({
460460
/>
461461
</div>
462462
{clearable && (
463-
<StyledButtonContainer onClick={handleClear} isMenuOpen={false}>
463+
<StyledButtonContainer onClick={handleClear}>
464464
<StyledButton LeftIcon={IconCalendarX} text="Clear" />
465465
</StyledButtonContainer>
466466
)}

packages/twenty-front/src/modules/ui/layout/dropdown/components/Dropdown.tsx

+3
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ type DropdownProps = {
3636
dropdownPlacement?: Placement;
3737
dropdownMenuWidth?: `${string}px` | `${number}%` | 'auto' | number;
3838
dropdownOffset?: { x?: number; y?: number };
39+
dropdownStrategy?: 'fixed' | 'absolute';
3940
disableBlur?: boolean;
4041
onClickOutside?: () => void;
4142
onClose?: () => void;
@@ -51,6 +52,7 @@ export const Dropdown = ({
5152
dropdownId,
5253
dropdownHotkeyScope,
5354
dropdownPlacement = 'bottom-end',
55+
dropdownStrategy = 'absolute',
5456
dropdownOffset = { x: 0, y: 0 },
5557
disableBlur = false,
5658
onClickOutside,
@@ -75,6 +77,7 @@ export const Dropdown = ({
7577
placement: dropdownPlacement,
7678
middleware: [flip(), ...offsetMiddlewares],
7779
whileElementsMounted: autoUpdate,
80+
strategy: dropdownStrategy,
7881
});
7982

8083
const handleHotkeyTriggered = () => {

packages/twenty-front/src/modules/ui/layout/dropdown/components/DropdownMenu.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@ const StyledDropdownMenu = styled.div<{
2525
2626
flex-direction: column;
2727
z-index: 1;
28-
width: ${({ width }) =>
29-
width ? `${typeof width === 'number' ? `${width}px` : width}` : '160px'};
28+
width: ${({ width = 160 }) =>
29+
typeof width === 'number' ? `${width}px` : width};
3030
`;
3131

3232
export const DropdownMenu = StyledDropdownMenu;

packages/twenty-front/src/modules/ui/layout/dropdown/hooks/useDropdown.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ export const useDropdown = (dropdownId?: string) => {
5252

5353
return {
5454
scopeId,
55-
isDropdownOpen: isDropdownOpen,
55+
isDropdownOpen,
5656
closeDropdown,
5757
toggleDropdown,
5858
openDropdown,

packages/twenty-front/src/modules/ui/navigation/menu-item/components/MenuItem.tsx

+6-5
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
import { MouseEvent, ReactNode } from 'react';
1+
import { FunctionComponent, MouseEvent, ReactElement, ReactNode } from 'react';
22
import { IconComponent } from 'twenty-ui';
33

4+
import { LightIconButtonProps } from '@/ui/input/button/components/LightIconButton';
45
import { LightIconButtonGroup } from '@/ui/input/button/components/LightIconButtonGroup';
56

67
import { MenuItemLeftContent } from '../internals/components/MenuItemLeftContent';
@@ -11,7 +12,9 @@ import {
1112
import { MenuItemAccent } from '../types/MenuItemAccent';
1213

1314
export type MenuItemIconButton = {
15+
Wrapper?: FunctionComponent<{ iconButton: ReactElement }>;
1416
Icon: IconComponent;
17+
accent?: LightIconButtonProps['accent'];
1518
onClick?: (event: MouseEvent<any>) => void;
1619
};
1720

@@ -24,23 +27,22 @@ export type MenuItemProps = {
2427
isTooltipOpen?: boolean;
2528
className?: string;
2629
testId?: string;
27-
onClick?: (event: MouseEvent<HTMLLIElement>) => void;
30+
onClick?: (event: MouseEvent<HTMLDivElement>) => void;
2831
};
2932

3033
export const MenuItem = ({
3134
LeftIcon,
3235
accent = 'default',
3336
text,
3437
iconButtons,
35-
isTooltipOpen,
3638
isIconDisplayedOnHoverOnly = true,
3739
className,
3840
testId,
3941
onClick,
4042
}: MenuItemProps) => {
4143
const showIconButtons = Array.isArray(iconButtons) && iconButtons.length > 0;
4244

43-
const handleMenuItemClick = (event: MouseEvent<HTMLLIElement>) => {
45+
const handleMenuItemClick = (event: MouseEvent<HTMLDivElement>) => {
4446
if (!onClick) return;
4547
event.preventDefault();
4648
event.stopPropagation();
@@ -54,7 +56,6 @@ export const MenuItem = ({
5456
onClick={handleMenuItemClick}
5557
className={className}
5658
accent={accent}
57-
isMenuOpen={!!isTooltipOpen}
5859
isIconDisplayedOnHoverOnly={isIconDisplayedOnHoverOnly}
5960
>
6061
<StyledMenuItemLeftContent>

packages/twenty-front/src/modules/ui/navigation/menu-item/components/MenuItemDraggable.tsx

-2
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ export const MenuItemDraggable = ({
2222
LeftIcon,
2323
accent = 'default',
2424
iconButtons,
25-
isTooltipOpen,
2625
onClick,
2726
text,
2827
isDragDisabled = false,
@@ -35,7 +34,6 @@ export const MenuItemDraggable = ({
3534
onClick={onClick}
3635
accent={accent}
3736
className={className}
38-
isMenuOpen={!!isTooltipOpen}
3937
>
4038
<MenuItemLeftContent
4139
LeftIcon={LeftIcon}

0 commit comments

Comments
 (0)