diff --git a/front/src/modules/companies/constants/companiesAvailableColumnDefinitions.tsx b/front/src/modules/companies/constants/companiesAvailableColumnDefinitions.tsx index b446fef95..eb44dcb98 100644 --- a/front/src/modules/companies/constants/companiesAvailableColumnDefinitions.tsx +++ b/front/src/modules/companies/constants/companiesAvailableColumnDefinitions.tsx @@ -43,6 +43,7 @@ export const companiesAvailableColumnDefinitions: ColumnDefinition, { @@ -58,6 +59,8 @@ export const companiesAvailableColumnDefinitions: ColumnDefinition, { key: 'accountOwner', @@ -71,6 +74,8 @@ export const companiesAvailableColumnDefinitions: ColumnDefinition, { key: 'createdAt', @@ -83,6 +88,7 @@ export const companiesAvailableColumnDefinitions: ColumnDefinition, { key: 'employees', @@ -97,6 +103,7 @@ export const companiesAvailableColumnDefinitions: ColumnDefinition, { key: 'linkedin', @@ -111,6 +118,7 @@ export const companiesAvailableColumnDefinitions: ColumnDefinition, { key: 'address', @@ -124,6 +132,7 @@ export const companiesAvailableColumnDefinitions: ColumnDefinition, { key: 'idealCustomerProfile', @@ -136,6 +145,8 @@ export const companiesAvailableColumnDefinitions: ColumnDefinition, { key: 'annualRecurringRevenue', @@ -148,6 +159,8 @@ export const companiesAvailableColumnDefinitions: ColumnDefinition, { key: 'xUrl', @@ -162,5 +175,6 @@ export const companiesAvailableColumnDefinitions: ColumnDefinition, ]; diff --git a/front/src/modules/people/constants/peopleAvailableColumnDefinitions.tsx b/front/src/modules/people/constants/peopleAvailableColumnDefinitions.tsx index 818d6bc2b..23c4a9337 100644 --- a/front/src/modules/people/constants/peopleAvailableColumnDefinitions.tsx +++ b/front/src/modules/people/constants/peopleAvailableColumnDefinitions.tsx @@ -42,6 +42,7 @@ export const peopleAvailableColumnDefinitions: ColumnDefinition[] entityType: Entity.Person, }, buttonIcon: IconArrowUpRight, + infoTooltipContent: 'Contact’s first and last name.', basePathToShowPage: '/person/', } satisfies ColumnDefinition, { @@ -56,6 +57,7 @@ export const peopleAvailableColumnDefinitions: ColumnDefinition[] placeHolder: 'Ema​il', // Hack: Fake character to prevent password-manager from filling the field }, buttonIcon: IconPencil, + infoTooltipContent: 'Contact’s Email.', } satisfies ColumnDefinition, { key: 'company', @@ -68,6 +70,7 @@ export const peopleAvailableColumnDefinitions: ColumnDefinition[] fieldName: 'company', relationType: Entity.Company, }, + infoTooltipContent: 'Contact’s company.', } satisfies ColumnDefinition, { key: 'phone', @@ -81,6 +84,7 @@ export const peopleAvailableColumnDefinitions: ColumnDefinition[] placeHolder: 'Phon​e', // Hack: Fake character to prevent password-manager from filling the field }, buttonIcon: IconPencil, + infoTooltipContent: 'Contact’s phone number.', } satisfies ColumnDefinition, { key: 'createdAt', @@ -92,6 +96,7 @@ export const peopleAvailableColumnDefinitions: ColumnDefinition[] metadata: { fieldName: 'createdAt', }, + infoTooltipContent: 'Date when the contact was added.', } satisfies ColumnDefinition, { key: 'city', @@ -104,6 +109,7 @@ export const peopleAvailableColumnDefinitions: ColumnDefinition[] fieldName: 'city', placeHolder: 'Cit​y', // Hack: Fake character to prevent password-manager from filling the field }, + infoTooltipContent: 'Contact’s city.', } satisfies ColumnDefinition, { key: 'jobTitle', @@ -116,6 +122,7 @@ export const peopleAvailableColumnDefinitions: ColumnDefinition[] fieldName: 'jobTitle', placeHolder: 'Job title', }, + infoTooltipContent: 'Contact’s job title.', } satisfies ColumnDefinition, { key: 'linkedin', @@ -129,6 +136,7 @@ export const peopleAvailableColumnDefinitions: ColumnDefinition[] placeHolder: 'LinkedIn', }, buttonIcon: IconPencil, + infoTooltipContent: 'Contact’s Linkedin account.', } satisfies ColumnDefinition, { key: 'x', @@ -142,5 +150,6 @@ export const peopleAvailableColumnDefinitions: ColumnDefinition[] placeHolder: 'X', }, buttonIcon: IconPencil, + infoTooltipContent: 'Contact’s Twitter account.', } satisfies ColumnDefinition, ]; diff --git a/front/src/modules/pipeline/constants/pipelineAvailableFieldDefinitions.tsx b/front/src/modules/pipeline/constants/pipelineAvailableFieldDefinitions.tsx index cce4c5fd3..6ace5e011 100644 --- a/front/src/modules/pipeline/constants/pipelineAvailableFieldDefinitions.tsx +++ b/front/src/modules/pipeline/constants/pipelineAvailableFieldDefinitions.tsx @@ -27,6 +27,8 @@ export const pipelineAvailableFieldDefinitions: BoardFieldDefinition, { key: 'amount', @@ -39,6 +41,7 @@ export const pipelineAvailableFieldDefinitions: BoardFieldDefinition, { key: 'probability', @@ -50,6 +53,8 @@ export const pipelineAvailableFieldDefinitions: BoardFieldDefinition, { key: 'pointOfContact', @@ -64,5 +69,6 @@ export const pipelineAvailableFieldDefinitions: BoardFieldDefinition, ]; diff --git a/front/src/modules/ui/button/components/FloatingIconButton.tsx b/front/src/modules/ui/button/components/FloatingIconButton.tsx index 33e623b87..3f7c7d516 100644 --- a/front/src/modules/ui/button/components/FloatingIconButton.tsx +++ b/front/src/modules/ui/button/components/FloatingIconButton.tsx @@ -21,17 +21,21 @@ export type FloatingIconButtonProps = { disabled?: boolean; focus?: boolean; onClick?: (event: React.MouseEvent) => void; + isActive?: boolean; }; const StyledButton = styled.button< Pick< FloatingIconButtonProps, - 'size' | 'position' | 'applyShadow' | 'applyBlur' | 'focus' + 'size' | 'position' | 'applyShadow' | 'applyBlur' | 'focus' | 'isActive' > >` align-items: center; backdrop-filter: ${({ applyBlur }) => (applyBlur ? 'blur(20px)' : 'none')}; - background: ${({ theme }) => theme.background.primary}; + background: ${({ theme, isActive }) => + !!isActive + ? theme.background.transparent.medium + : theme.background.primary}; border: ${({ focus, theme }) => focus ? `1px solid ${theme.color.blue}` : 'transparent'}; border-radius: ${({ position, theme }) => { @@ -87,7 +91,8 @@ const StyledButton = styled.button< }} &:hover { - background: ${({ theme }) => theme.background.transparent.lighter}; + background: ${({ theme, isActive }) => + !!isActive ?? theme.background.transparent.lighter}; } &:active { @@ -110,6 +115,7 @@ export const FloatingIconButton = ({ disabled = false, focus = false, onClick, + isActive, }: FloatingIconButtonProps) => { const theme = useTheme(); return ( @@ -122,6 +128,7 @@ export const FloatingIconButton = ({ className={className} position={position} onClick={onClick} + isActive={isActive} > {Icon && } diff --git a/front/src/modules/ui/button/components/FloatingIconButtonGroup.tsx b/front/src/modules/ui/button/components/FloatingIconButtonGroup.tsx index 2091b0cdb..0dd48473a 100644 --- a/front/src/modules/ui/button/components/FloatingIconButtonGroup.tsx +++ b/front/src/modules/ui/button/components/FloatingIconButtonGroup.tsx @@ -27,15 +27,17 @@ export type FloatingIconButtonGroupProps = Pick< iconButtons: { Icon: IconComponent; onClick?: (event: MouseEvent) => void; + isActive?: boolean; }[]; }; export const FloatingIconButtonGroup = ({ iconButtons, size, + className, }: FloatingIconButtonGroupProps) => ( - - {iconButtons.map(({ Icon, onClick }, index) => { + + {iconButtons.map(({ Icon, onClick, isActive }, index) => { const position: FloatingIconButtonPosition = iconButtons.length === 1 ? 'standalone' @@ -54,6 +56,7 @@ export const FloatingIconButtonGroup = ({ onClick={onClick} position={position} size={size} + isActive={isActive} /> ); })} diff --git a/front/src/modules/ui/field/types/FieldDefinition.ts b/front/src/modules/ui/field/types/FieldDefinition.ts index 7598a634f..867dcca73 100644 --- a/front/src/modules/ui/field/types/FieldDefinition.ts +++ b/front/src/modules/ui/field/types/FieldDefinition.ts @@ -11,4 +11,5 @@ export type FieldDefinition = { metadata: T; buttonIcon?: IconComponent; basePathToShowPage?: string; + infoTooltipContent?: string; }; diff --git a/front/src/modules/ui/menu-item/components/MenuItem.tsx b/front/src/modules/ui/menu-item/components/MenuItem.tsx index c3a9117c1..2da1bef04 100644 --- a/front/src/modules/ui/menu-item/components/MenuItem.tsx +++ b/front/src/modules/ui/menu-item/components/MenuItem.tsx @@ -20,6 +20,7 @@ export type MenuItemProps = { accent?: MenuItemAccent; text: string; iconButtons?: MenuItemIconButton[]; + isTooltipOpen?: boolean; className?: string; testId?: string; onClick?: () => void; @@ -30,6 +31,7 @@ export const MenuItem = ({ accent = 'default', text, iconButtons, + isTooltipOpen, className, testId, onClick, @@ -42,6 +44,7 @@ export const MenuItem = ({ onClick={onClick} className={className} accent={accent} + isMenuOpen={!!isTooltipOpen} > diff --git a/front/src/modules/ui/menu-item/components/MenuItemDraggable.tsx b/front/src/modules/ui/menu-item/components/MenuItemDraggable.tsx index ee62dc301..ac5bc09ed 100644 --- a/front/src/modules/ui/menu-item/components/MenuItemDraggable.tsx +++ b/front/src/modules/ui/menu-item/components/MenuItemDraggable.tsx @@ -11,6 +11,7 @@ export type MenuItemDraggableProps = { LeftIcon: IconComponent | undefined; accent?: MenuItemAccent; iconButtons?: MenuItemIconButton[]; + isTooltipOpen?: boolean; onClick?: () => void; text: string; isDragDisabled?: boolean; @@ -20,6 +21,7 @@ export const MenuItemDraggable = ({ LeftIcon, accent = 'default', iconButtons, + isTooltipOpen, onClick, text, isDragDisabled = false, @@ -32,17 +34,19 @@ export const MenuItemDraggable = ({ onClick={onClick} accent={accent} className={className} + isMenuOpen={!!isTooltipOpen} > -
- {showIconButtons && ( - - )} -
+ {showIconButtons && ( + + )} ); }; diff --git a/front/src/modules/ui/menu-item/internals/components/StyledMenuItemBase.tsx b/front/src/modules/ui/menu-item/internals/components/StyledMenuItemBase.tsx index 24cb22239..f897a5c64 100644 --- a/front/src/modules/ui/menu-item/internals/components/StyledMenuItemBase.tsx +++ b/front/src/modules/ui/menu-item/internals/components/StyledMenuItemBase.tsx @@ -89,9 +89,11 @@ export const StyledMenuItemRightContent = styled.div` flex-direction: row; `; -export const StyledHoverableMenuItemBase = styled(StyledMenuItemBase)` +export const StyledHoverableMenuItemBase = styled(StyledMenuItemBase)<{ + isMenuOpen: boolean; +}>` & .hoverable-buttons { - opacity: 0; + opacity: ${({ isMenuOpen }) => (isMenuOpen ? 1 : 0)}; pointer-events: none; position: fixed; right: ${({ theme }) => theme.spacing(2)}; diff --git a/front/src/modules/ui/view-bar/components/ViewFieldsVisibilityDropdownSection.tsx b/front/src/modules/ui/view-bar/components/ViewFieldsVisibilityDropdownSection.tsx index 649d06efa..30e9358b4 100644 --- a/front/src/modules/ui/view-bar/components/ViewFieldsVisibilityDropdownSection.tsx +++ b/front/src/modules/ui/view-bar/components/ViewFieldsVisibilityDropdownSection.tsx @@ -1,3 +1,5 @@ +import { useRef, useState } from 'react'; +import { createPortal } from 'react-dom'; import { DropResult, OnDragEndResponder, @@ -9,8 +11,12 @@ import { DraggableList } from '@/ui/draggable-list/components/DraggableList'; import { StyledDropdownMenuItemsContainer } from '@/ui/dropdown/components/StyledDropdownMenuItemsContainer'; import { StyledDropdownMenuSubheader } from '@/ui/dropdown/components/StyledDropdownMenuSubheader'; import { IconMinus, IconPlus } from '@/ui/icon'; +import { IconInfoCircle } from '@/ui/input/constants/icons'; import { MenuItem } from '@/ui/menu-item/components/MenuItem'; import { MenuItemDraggable } from '@/ui/menu-item/components/MenuItemDraggable'; +import { AppTooltip } from '@/ui/tooltip/AppTooltip'; +import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside'; +import { isDefined } from '~/utils/isDefined'; import { ViewFieldForVisibility } from '../types/ViewFieldForVisibility'; @@ -33,8 +39,38 @@ export const ViewFieldsVisibilityDropdownSection = ({ onDragEnd?.(result, provided); }; + const [openToolTipIndex, setOpenToolTipIndex] = useState(); + + const handleInfoButtonClick = (index: number) => { + if (index === openToolTipIndex) setOpenToolTipIndex(undefined); + else setOpenToolTipIndex(index); + }; + const getIconButtons = (index: number, field: ViewFieldForVisibility) => { - if (index !== 0) { + const isFirstColumn = isDraggable && index === 0; + if (isFirstColumn && field.infoTooltipContent) { + return [ + { + Icon: IconInfoCircle, + onClick: () => handleInfoButtonClick(index), + isActive: openToolTipIndex === index, + }, + ]; + } + if (!isFirstColumn && field.infoTooltipContent) { + return [ + { + Icon: IconInfoCircle, + onClick: () => handleInfoButtonClick(index), + isActive: openToolTipIndex === index, + }, + { + Icon: field.isVisible ? IconMinus : IconPlus, + onClick: () => onVisibilityChange(field), + }, + ]; + } + if (!isFirstColumn && !field.infoTooltipContent) { return [ { Icon: field.isVisible ? IconMinus : IconPlus, @@ -44,11 +80,20 @@ export const ViewFieldsVisibilityDropdownSection = ({ } }; + const ref = useRef(null); + + useListenClickOutside({ + refs: [ref], + callback: () => { + setOpenToolTipIndex(undefined); + }, + }); + return ( - <> +
{title} - {isDraggable && ( + {isDraggable ? ( } /> @@ -73,22 +120,31 @@ export const ViewFieldsVisibilityDropdownSection = ({ } /> - )} - {!isDraggable && - fields.map((field) => ( + ) : ( + fields.map((field, index) => ( onVisibilityChange(field), - }, - ]} + iconButtons={getIconButtons(index, field)} + isTooltipOpen={openToolTipIndex === index} text={field.name} + className={`${title}-fixed-item-tooltip-anchor-${index}`} /> - ))} + )) + )} - + {isDefined(openToolTipIndex) && + createPortal( + , + document.body, + )} +
); }; diff --git a/front/src/modules/ui/view-bar/types/ViewFieldForVisibility.ts b/front/src/modules/ui/view-bar/types/ViewFieldForVisibility.ts index 9a6fbbea3..2c2860fd5 100644 --- a/front/src/modules/ui/view-bar/types/ViewFieldForVisibility.ts +++ b/front/src/modules/ui/view-bar/types/ViewFieldForVisibility.ts @@ -3,7 +3,7 @@ import { FieldMetadata } from '@/ui/field/types/FieldMetadata'; export type ViewFieldForVisibility = Pick< FieldDefinition, - 'key' | 'name' | 'Icon' + 'key' | 'name' | 'Icon' | 'infoTooltipContent' > & { isVisible?: boolean; index: number;