From 2350c4df10856d60a0d2f53459bf1c13f227c559 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tha=C3=AFs=20Guigon?= Date: Mon, 6 May 2024 20:14:42 +0200 Subject: [PATCH] feat: remove a link from a Links field Closes #5117 --- .../input/components/LinksFieldInput.tsx | 53 ++++++++----- .../input/components/LinksFieldMenuItem.tsx | 77 +++++++++++++++++++ .../components/LightIconButtonGroup.tsx | 18 ++++- .../date/components/InternalDatePicker.tsx | 2 +- .../layout/dropdown/components/Dropdown.tsx | 3 + .../dropdown/components/DropdownMenu.tsx | 4 +- .../ui/layout/dropdown/hooks/useDropdown.ts | 2 +- .../menu-item/components/MenuItem.tsx | 11 +-- .../components/MenuItemDraggable.tsx | 2 - .../components/StyledMenuItemBase.tsx | 33 ++++---- .../display/icon/components/TablerIcons.ts | 1 + 11 files changed, 156 insertions(+), 50 deletions(-) create mode 100644 packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/LinksFieldMenuItem.tsx diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/LinksFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/LinksFieldInput.tsx index 46bfe68dc924..20f4d398dfb0 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/LinksFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/LinksFieldInput.tsx @@ -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'; @@ -35,13 +35,14 @@ export const LinksFieldInput = ({ const containerRef = useRef(null); - const links = useMemo( + const links = useMemo<{ url: string; label: string; isPrimary?: boolean }[]>( () => [ fieldValue.primaryLinkUrl ? { url: fieldValue.primaryLinkUrl, label: fieldValue.primaryLinkLabel, + isPrimary: true, } : null, ...(fieldValue.secondaryLinks ?? []), @@ -53,27 +54,21 @@ 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); @@ -102,15 +97,31 @@ export const LinksFieldInput = ({ ); }; + const handleDeleteLink = (index: number) => { + const nextSecondaryLinks = [...(fieldValue.secondaryLinks ?? [])]; + nextSecondaryLinks.splice(index - 1, 1); + + onSubmit?.(() => + persistLinksField({ + ...fieldValue, + secondaryLinks: nextSecondaryLinks, + }), + ); + }; + return ( {!!links.length && ( <> - {links.map(({ label, url }, index) => ( - ( + } + dropdownId={`${hotkeyScope}-links-${index}`} + isPrimary={isPrimary} + label={label} + onDelete={() => handleDeleteLink(index)} + url={url} /> ))} @@ -124,9 +135,9 @@ export const LinksFieldInput = ({ value={inputValue} hotkeyScope={hotkeyScope} onChange={(event) => setInputValue(event.target.value)} - onEnter={handleSubmit} + onEnter={handleAddLink} rightComponent={ - + } /> ) : ( diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/LinksFieldMenuItem.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/LinksFieldMenuItem.tsx new file mode 100644 index 000000000000..b4f1f2facefd --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/LinksFieldMenuItem.tsx @@ -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 ( + } + isIconDisplayedOnHoverOnly={!isPrimary && !isDropdownOpen} + iconButtons={[ + { + Wrapper: isPrimary + ? undefined + : ({ iconButton }) => ( + + + + } + /> + ), + Icon: isPrimary + ? (StyledIconBookmark as IconComponent) + : IconDotsVertical, + accent: 'tertiary', + onClick: isPrimary ? undefined : () => {}, + }, + ]} + /> + ); +}; diff --git a/packages/twenty-front/src/modules/ui/input/button/components/LightIconButtonGroup.tsx b/packages/twenty-front/src/modules/ui/input/button/components/LightIconButtonGroup.tsx index fd3f20ce15ed..7c0459252dc8 100644 --- a/packages/twenty-front/src/modules/ui/input/button/components/LightIconButtonGroup.tsx +++ b/packages/twenty-front/src/modules/ui/input/button/components/LightIconButtonGroup.tsx @@ -1,4 +1,4 @@ -import { MouseEvent } from 'react'; +import { FunctionComponent, MouseEvent, ReactElement } from 'react'; import styled from '@emotion/styled'; import { IconComponent } from 'twenty-ui'; @@ -14,7 +14,9 @@ export type LightIconButtonGroupProps = Pick< 'className' | 'size' > & { iconButtons: { + Wrapper?: FunctionComponent<{ iconButton: ReactElement }>; Icon: IconComponent; + accent?: LightIconButtonProps['accent']; onClick?: (event: MouseEvent) => void; disabled?: boolean; }[]; @@ -26,16 +28,26 @@ export const LightIconButtonGroup = ({ className, }: LightIconButtonGroupProps) => ( - {iconButtons.map(({ Icon, onClick }, index) => { - return ( + {iconButtons.map(({ Wrapper, Icon, accent, onClick }, index) => { + const iconButton = ( ); + + return Wrapper ? ( + + ) : ( + iconButton + ); })} ); diff --git a/packages/twenty-front/src/modules/ui/input/components/internal/date/components/InternalDatePicker.tsx b/packages/twenty-front/src/modules/ui/input/components/internal/date/components/InternalDatePicker.tsx index 368c7eafde3e..6f96eaee0040 100644 --- a/packages/twenty-front/src/modules/ui/input/components/internal/date/components/InternalDatePicker.tsx +++ b/packages/twenty-front/src/modules/ui/input/components/internal/date/components/InternalDatePicker.tsx @@ -460,7 +460,7 @@ export const InternalDatePicker = ({ /> {clearable && ( - + )} diff --git a/packages/twenty-front/src/modules/ui/layout/dropdown/components/Dropdown.tsx b/packages/twenty-front/src/modules/ui/layout/dropdown/components/Dropdown.tsx index 70c58355d4da..5964f92cc798 100644 --- a/packages/twenty-front/src/modules/ui/layout/dropdown/components/Dropdown.tsx +++ b/packages/twenty-front/src/modules/ui/layout/dropdown/components/Dropdown.tsx @@ -36,6 +36,7 @@ type DropdownProps = { dropdownPlacement?: Placement; dropdownMenuWidth?: `${string}px` | `${number}%` | 'auto' | number; dropdownOffset?: { x?: number; y?: number }; + dropdownStrategy?: 'fixed' | 'absolute'; disableBlur?: boolean; onClickOutside?: () => void; onClose?: () => void; @@ -51,6 +52,7 @@ export const Dropdown = ({ dropdownId, dropdownHotkeyScope, dropdownPlacement = 'bottom-end', + dropdownStrategy = 'absolute', dropdownOffset = { x: 0, y: 0 }, disableBlur = false, onClickOutside, @@ -75,6 +77,7 @@ export const Dropdown = ({ placement: dropdownPlacement, middleware: [flip(), ...offsetMiddlewares], whileElementsMounted: autoUpdate, + strategy: dropdownStrategy, }); const handleHotkeyTriggered = () => { diff --git a/packages/twenty-front/src/modules/ui/layout/dropdown/components/DropdownMenu.tsx b/packages/twenty-front/src/modules/ui/layout/dropdown/components/DropdownMenu.tsx index 8acfcadfaee7..4804056675cf 100644 --- a/packages/twenty-front/src/modules/ui/layout/dropdown/components/DropdownMenu.tsx +++ b/packages/twenty-front/src/modules/ui/layout/dropdown/components/DropdownMenu.tsx @@ -25,8 +25,8 @@ const StyledDropdownMenu = styled.div<{ flex-direction: column; z-index: 1; - width: ${({ width }) => - width ? `${typeof width === 'number' ? `${width}px` : width}` : '160px'}; + width: ${({ width = 160 }) => + typeof width === 'number' ? `${width}px` : width}; `; export const DropdownMenu = StyledDropdownMenu; diff --git a/packages/twenty-front/src/modules/ui/layout/dropdown/hooks/useDropdown.ts b/packages/twenty-front/src/modules/ui/layout/dropdown/hooks/useDropdown.ts index f3b196f41808..7712852c804e 100644 --- a/packages/twenty-front/src/modules/ui/layout/dropdown/hooks/useDropdown.ts +++ b/packages/twenty-front/src/modules/ui/layout/dropdown/hooks/useDropdown.ts @@ -52,7 +52,7 @@ export const useDropdown = (dropdownId?: string) => { return { scopeId, - isDropdownOpen: isDropdownOpen, + isDropdownOpen, closeDropdown, toggleDropdown, openDropdown, diff --git a/packages/twenty-front/src/modules/ui/navigation/menu-item/components/MenuItem.tsx b/packages/twenty-front/src/modules/ui/navigation/menu-item/components/MenuItem.tsx index 6c8b70b22d74..36449c7c568f 100644 --- a/packages/twenty-front/src/modules/ui/navigation/menu-item/components/MenuItem.tsx +++ b/packages/twenty-front/src/modules/ui/navigation/menu-item/components/MenuItem.tsx @@ -1,6 +1,7 @@ -import { MouseEvent, ReactNode } from 'react'; +import { FunctionComponent, MouseEvent, ReactElement, ReactNode } from 'react'; import { IconComponent } from 'twenty-ui'; +import { LightIconButtonProps } from '@/ui/input/button/components/LightIconButton'; import { LightIconButtonGroup } from '@/ui/input/button/components/LightIconButtonGroup'; import { MenuItemLeftContent } from '../internals/components/MenuItemLeftContent'; @@ -11,7 +12,9 @@ import { import { MenuItemAccent } from '../types/MenuItemAccent'; export type MenuItemIconButton = { + Wrapper?: FunctionComponent<{ iconButton: ReactElement }>; Icon: IconComponent; + accent?: LightIconButtonProps['accent']; onClick?: (event: MouseEvent) => void; }; @@ -24,7 +27,7 @@ export type MenuItemProps = { isTooltipOpen?: boolean; className?: string; testId?: string; - onClick?: (event: MouseEvent) => void; + onClick?: (event: MouseEvent) => void; }; export const MenuItem = ({ @@ -32,7 +35,6 @@ export const MenuItem = ({ accent = 'default', text, iconButtons, - isTooltipOpen, isIconDisplayedOnHoverOnly = true, className, testId, @@ -40,7 +42,7 @@ export const MenuItem = ({ }: MenuItemProps) => { const showIconButtons = Array.isArray(iconButtons) && iconButtons.length > 0; - const handleMenuItemClick = (event: MouseEvent) => { + const handleMenuItemClick = (event: MouseEvent) => { if (!onClick) return; event.preventDefault(); event.stopPropagation(); @@ -54,7 +56,6 @@ export const MenuItem = ({ onClick={handleMenuItemClick} className={className} accent={accent} - isMenuOpen={!!isTooltipOpen} isIconDisplayedOnHoverOnly={isIconDisplayedOnHoverOnly} > diff --git a/packages/twenty-front/src/modules/ui/navigation/menu-item/components/MenuItemDraggable.tsx b/packages/twenty-front/src/modules/ui/navigation/menu-item/components/MenuItemDraggable.tsx index 773de35e02d5..641bebee1618 100644 --- a/packages/twenty-front/src/modules/ui/navigation/menu-item/components/MenuItemDraggable.tsx +++ b/packages/twenty-front/src/modules/ui/navigation/menu-item/components/MenuItemDraggable.tsx @@ -22,7 +22,6 @@ export const MenuItemDraggable = ({ LeftIcon, accent = 'default', iconButtons, - isTooltipOpen, onClick, text, isDragDisabled = false, @@ -35,7 +34,6 @@ export const MenuItemDraggable = ({ onClick={onClick} accent={accent} className={className} - isMenuOpen={!!isTooltipOpen} > ` +export const StyledMenuItemBase = styled.div` --horizontal-padding: ${({ theme }) => theme.spacing(1)}; --vertical-padding: ${({ theme }) => theme.spacing(2)}; @@ -101,23 +101,26 @@ export const StyledMenuItemRightContent = styled.div` `; export const StyledHoverableMenuItemBase = styled(StyledMenuItemBase)<{ - isMenuOpen: boolean; isIconDisplayedOnHoverOnly?: boolean; }>` + ${({ isIconDisplayedOnHoverOnly, theme }) => + isIconDisplayedOnHoverOnly && + css` + & .hoverable-buttons { + opacity: 0; + position: fixed; + right: ${theme.spacing(2)}; + } + + &:hover { + & .hoverable-buttons { + opacity: 1; + position: static; + } + } + `}; + & .hoverable-buttons { - pointer-events: none; - position: fixed; - right: ${({ theme }) => theme.spacing(2)}; - opacity: ${({ isIconDisplayedOnHoverOnly }) => - isIconDisplayedOnHoverOnly ? 0 : 1}; transition: opacity ${({ theme }) => theme.animation.duration.instant}s ease; } - - &:hover { - & .hoverable-buttons { - opacity: 1; - pointer-events: auto; - position: static; - } - } `; diff --git a/packages/twenty-ui/src/display/icon/components/TablerIcons.ts b/packages/twenty-ui/src/display/icon/components/TablerIcons.ts index cac8c23a527b..13d12b7fe297 100644 --- a/packages/twenty-ui/src/display/icon/components/TablerIcons.ts +++ b/packages/twenty-ui/src/display/icon/components/TablerIcons.ts @@ -19,6 +19,7 @@ export { IconBell, IconBolt, IconBook2, + IconBookmark, IconBox, IconBrandGithub, IconBrandGoogle,