Skip to content

Commit 4800388

Browse files
thaisguigonmagrinjcharlesBochet
authored
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 <[email protected]> Co-authored-by: Charles Bochet <[email protected]>
1 parent beaaf33 commit 4800388

File tree

11 files changed

+159
-63
lines changed

11 files changed

+159
-63
lines changed

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

+35-34
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';
@@ -14,6 +14,7 @@ import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownM
1414
import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
1515
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
1616
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
17+
import { toSpliced } from '~/utils/array/toSpliced';
1718
import { isDefined } from '~/utils/isDefined';
1819

1920
const StyledDropdownMenu = styled(DropdownMenu)`
@@ -35,7 +36,7 @@ export const LinksFieldInput = ({
3536

3637
const containerRef = useRef<HTMLDivElement>(null);
3738

38-
const links = useMemo(
39+
const links = useMemo<{ url: string; label: string }[]>(
3940
() =>
4041
[
4142
fieldValue.primaryLinkUrl
@@ -53,51 +54,47 @@ 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);
8075
setInputValue('');
8176

82-
if (!links.length) {
83-
onSubmit?.(() =>
84-
persistLinksField({
85-
primaryLinkUrl: inputValue,
86-
primaryLinkLabel: '',
87-
secondaryLinks: [],
88-
}),
89-
);
77+
const nextLinks = [...links, { label: '', url: inputValue }];
78+
const [nextPrimaryLink, ...nextSecondaryLinks] = nextLinks;
9079

91-
return;
92-
}
80+
onSubmit?.(() =>
81+
persistLinksField({
82+
primaryLinkUrl: nextPrimaryLink.url ?? '',
83+
primaryLinkLabel: nextPrimaryLink.label ?? '',
84+
secondaryLinks: nextSecondaryLinks,
85+
}),
86+
);
87+
};
9388

89+
const handleDeleteLink = (index: number) => {
9490
onSubmit?.(() =>
9591
persistLinksField({
9692
...fieldValue,
97-
secondaryLinks: [
98-
...(fieldValue.secondaryLinks ?? []),
99-
{ label: '', url: inputValue },
100-
],
93+
secondaryLinks: toSpliced(
94+
fieldValue.secondaryLinks ?? [],
95+
index - 1,
96+
1,
97+
),
10198
}),
10299
);
103100
};
@@ -108,9 +105,13 @@ export const LinksFieldInput = ({
108105
<>
109106
<DropdownMenuItemsContainer>
110107
{links.map(({ label, url }, index) => (
111-
<MenuItem
108+
<LinksFieldMenuItem
112109
key={index}
113-
text={<LinkDisplay value={{ label, url }} />}
110+
dropdownId={`${hotkeyScope}-links-${index}`}
111+
isPrimary={index === 0}
112+
label={label}
113+
onDelete={() => handleDeleteLink(index)}
114+
url={url}
114115
/>
115116
))}
116117
</DropdownMenuItemsContainer>
@@ -124,9 +125,9 @@ export const LinksFieldInput = ({
124125
value={inputValue}
125126
hotkeyScope={hotkeyScope}
126127
onChange={(event) => setInputValue(event.target.value)}
127-
onEnter={handleSubmit}
128+
onEnter={handleAddLink}
128129
rightComponent={
129-
<LightIconButton Icon={IconPlus} onClick={handleSubmit} />
130+
<LightIconButton Icon={IconPlus} onClick={handleAddLink} />
130131
}
131132
/>
132133
) : (
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
@@ -23,7 +23,6 @@ export const MenuItemDraggable = ({
2323
LeftIcon,
2424
accent = 'default',
2525
iconButtons,
26-
isTooltipOpen,
2726
onClick,
2827
text,
2928
isDragDisabled = false,
@@ -37,7 +36,6 @@ export const MenuItemDraggable = ({
3736
onClick={onClick}
3837
accent={accent}
3938
className={className}
40-
isMenuOpen={!!isTooltipOpen}
4139
isIconDisplayedOnHoverOnly={isIconDisplayedOnHoverOnly}
4240
>
4341
<MenuItemLeftContent

0 commit comments

Comments
 (0)