feat: add links to Links field (#5223)
Closes #5115, Closes #5116 <img width="242" alt="image" src="https://github.com/twentyhq/twenty/assets/3098428/ab78495a-4216-4243-8de3-53720818a09b"> --------- Co-authored-by: Jérémy Magrin <jeremy.magrin@gmail.com>
This commit is contained in:
@ -8,15 +8,14 @@ import {
|
||||
SocialLink,
|
||||
} from '@/ui/navigation/link/components/SocialLink';
|
||||
import { checkUrlType } from '~/utils/checkUrlType';
|
||||
import { getAbsoluteUrl } from '~/utils/url/getAbsoluteUrl';
|
||||
import { getUrlHostName } from '~/utils/url/getUrlHostName';
|
||||
|
||||
import { EllipsisDisplay } from './EllipsisDisplay';
|
||||
|
||||
const StyledRawLink = styled(RoundedLink)`
|
||||
overflow: hidden;
|
||||
|
||||
a {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-size: ${({ theme }) => theme.font.size.md};
|
||||
white-space: nowrap;
|
||||
}
|
||||
`;
|
||||
@ -30,14 +29,8 @@ export const LinkDisplay = ({ value }: LinkDisplayProps) => {
|
||||
event.stopPropagation();
|
||||
};
|
||||
|
||||
const absoluteUrl = value?.url
|
||||
? value.url.startsWith('http')
|
||||
? value.url
|
||||
: 'https://' + value.url
|
||||
: '';
|
||||
|
||||
const displayedValue = value?.label || value?.url || '';
|
||||
|
||||
const absoluteUrl = getAbsoluteUrl(value?.url || '');
|
||||
const displayedValue = value?.label || getUrlHostName(absoluteUrl);
|
||||
const type = checkUrlType(absoluteUrl);
|
||||
|
||||
if (type === LinkType.LinkedIn || type === LinkType.Twitter) {
|
||||
|
||||
@ -1,14 +1,66 @@
|
||||
import { MouseEventHandler, useMemo } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { FieldLinksValue } from '@/object-record/record-field/types/FieldMetadata';
|
||||
import { LinkDisplay } from '@/ui/field/display/components/LinkDisplay';
|
||||
import { EllipsisDisplay } from '@/ui/field/display/components/EllipsisDisplay';
|
||||
import { RoundedLink } from '@/ui/navigation/link/components/RoundedLink';
|
||||
import {
|
||||
LinkType,
|
||||
SocialLink,
|
||||
} from '@/ui/navigation/link/components/SocialLink';
|
||||
import { checkUrlType } from '~/utils/checkUrlType';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
import { getAbsoluteUrl } from '~/utils/url/getAbsoluteUrl';
|
||||
import { getUrlHostName } from '~/utils/url/getUrlHostName';
|
||||
|
||||
const StyledContainer = styled(EllipsisDisplay)`
|
||||
display: flex;
|
||||
gap: ${({ theme }) => theme.spacing(1)};
|
||||
`;
|
||||
|
||||
type LinksDisplayProps = {
|
||||
value?: FieldLinksValue;
|
||||
};
|
||||
|
||||
export const LinksDisplay = ({ value }: LinksDisplayProps) => {
|
||||
const url = value?.primaryLinkUrl || '';
|
||||
const label = value?.primaryLinkLabel || getUrlHostName(url);
|
||||
const links = useMemo(
|
||||
() =>
|
||||
[
|
||||
value?.primaryLinkUrl
|
||||
? {
|
||||
url: value.primaryLinkUrl,
|
||||
label: value.primaryLinkLabel,
|
||||
}
|
||||
: null,
|
||||
...(value?.secondaryLinks ?? []),
|
||||
]
|
||||
.filter(isDefined)
|
||||
.map(({ url, label }) => {
|
||||
const absoluteUrl = getAbsoluteUrl(url);
|
||||
return {
|
||||
url: absoluteUrl,
|
||||
label: label || getUrlHostName(absoluteUrl),
|
||||
type: checkUrlType(absoluteUrl),
|
||||
};
|
||||
}),
|
||||
[value?.primaryLinkLabel, value?.primaryLinkUrl, value?.secondaryLinks],
|
||||
);
|
||||
|
||||
return <LinkDisplay value={{ url, label }} />;
|
||||
const handleClick: MouseEventHandler = (event) => event.stopPropagation();
|
||||
|
||||
return (
|
||||
<StyledContainer>
|
||||
{links.map(({ url, label, type }, index) =>
|
||||
type === LinkType.LinkedIn || type === LinkType.Twitter ? (
|
||||
<SocialLink key={index} href={url} onClick={handleClick} type={type}>
|
||||
{label}
|
||||
</SocialLink>
|
||||
) : (
|
||||
<RoundedLink key={index} href={url} onClick={handleClick}>
|
||||
{label}
|
||||
</RoundedLink>
|
||||
),
|
||||
)}
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
||||
|
||||
@ -1,10 +1,13 @@
|
||||
import { forwardRef, InputHTMLAttributes } from 'react';
|
||||
import { forwardRef, InputHTMLAttributes, ReactNode, useRef } from 'react';
|
||||
import { css } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { useRegisterInputEvents } from '@/object-record/record-field/meta-types/input/hooks/useRegisterInputEvents';
|
||||
import { RGBA } from '@/ui/theme/constants/Rgba';
|
||||
import { TEXT_INPUT_STYLE } from '@/ui/theme/constants/TextInputStyle';
|
||||
import { useCombinedRefs } from '~/hooks/useCombinedRefs';
|
||||
|
||||
const StyledInput = styled.input`
|
||||
const StyledInput = styled.input<{ withRightComponent?: boolean }>`
|
||||
${TEXT_INPUT_STYLE}
|
||||
|
||||
border: 1px solid ${({ theme }) => theme.border.color.medium};
|
||||
@ -19,27 +22,87 @@ const StyledInput = styled.input`
|
||||
border-color: ${({ theme }) => theme.color.blue};
|
||||
box-shadow: 0px 0px 0px 3px ${({ theme }) => RGBA(theme.color.blue, 0.1)};
|
||||
}
|
||||
|
||||
${({ withRightComponent }) =>
|
||||
withRightComponent &&
|
||||
css`
|
||||
padding-right: 32px;
|
||||
`}
|
||||
`;
|
||||
|
||||
const StyledInputContainer = styled.div`
|
||||
box-sizing: border-box;
|
||||
padding: ${({ theme }) => theme.spacing(1)};
|
||||
position: relative;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledRightContainer = styled.div`
|
||||
position: absolute;
|
||||
right: ${({ theme }) => theme.spacing(2)};
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
`;
|
||||
|
||||
type DropdownMenuInputProps = InputHTMLAttributes<HTMLInputElement> & {
|
||||
hotkeyScope?: string;
|
||||
onClickOutside?: () => void;
|
||||
onEnter?: () => void;
|
||||
onEscape?: () => void;
|
||||
onShiftTab?: () => void;
|
||||
onTab?: () => void;
|
||||
rightComponent?: ReactNode;
|
||||
};
|
||||
|
||||
export const DropdownMenuInput = forwardRef<
|
||||
HTMLInputElement,
|
||||
InputHTMLAttributes<HTMLInputElement>
|
||||
>(({ autoFocus, value, placeholder, onChange }, ref) => {
|
||||
return (
|
||||
<StyledInputContainer>
|
||||
<StyledInput
|
||||
autoFocus={autoFocus}
|
||||
value={value}
|
||||
placeholder={placeholder}
|
||||
onChange={onChange}
|
||||
ref={ref}
|
||||
/>
|
||||
</StyledInputContainer>
|
||||
);
|
||||
});
|
||||
DropdownMenuInputProps
|
||||
>(
|
||||
(
|
||||
{
|
||||
autoFocus,
|
||||
className,
|
||||
value,
|
||||
placeholder,
|
||||
hotkeyScope = 'dropdown-menu-input',
|
||||
onChange,
|
||||
onClickOutside,
|
||||
onEnter = () => {},
|
||||
onEscape = () => {},
|
||||
onShiftTab,
|
||||
onTab,
|
||||
rightComponent,
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const combinedRef = useCombinedRefs(ref, inputRef);
|
||||
|
||||
useRegisterInputEvents({
|
||||
inputRef,
|
||||
inputValue: value,
|
||||
onEnter,
|
||||
onEscape,
|
||||
onClickOutside,
|
||||
onTab,
|
||||
onShiftTab,
|
||||
hotkeyScope,
|
||||
});
|
||||
|
||||
return (
|
||||
<StyledInputContainer className={className}>
|
||||
<StyledInput
|
||||
autoFocus={autoFocus}
|
||||
value={value}
|
||||
placeholder={placeholder}
|
||||
onChange={onChange}
|
||||
ref={combinedRef}
|
||||
withRightComponent={!!rightComponent}
|
||||
/>
|
||||
{!!rightComponent && (
|
||||
<StyledRightContainer>{rightComponent}</StyledRightContainer>
|
||||
)}
|
||||
</StyledInputContainer>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
@ -6,6 +6,7 @@ import { Chip, ChipSize, ChipVariant } from 'twenty-ui';
|
||||
type RoundedLinkProps = {
|
||||
href: string;
|
||||
children?: React.ReactNode;
|
||||
className?: string;
|
||||
onClick?: (event: React.MouseEvent<HTMLElement>) => void;
|
||||
};
|
||||
|
||||
@ -22,14 +23,20 @@ const StyledClickable = styled.div`
|
||||
`;
|
||||
|
||||
const StyledChip = styled(Chip)`
|
||||
border-color: ${({ theme }) => theme.border.color.strong};
|
||||
box-sizing: border-box;
|
||||
padding: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
export const RoundedLink = ({ children, href, onClick }: RoundedLinkProps) => (
|
||||
export const RoundedLink = ({
|
||||
children,
|
||||
className,
|
||||
href,
|
||||
onClick,
|
||||
}: RoundedLinkProps) => (
|
||||
<div>
|
||||
{children !== '' ? (
|
||||
<StyledClickable>
|
||||
<StyledClickable className={className}>
|
||||
<ReactLink target="_blank" to={href} onClick={onClick}>
|
||||
<StyledChip
|
||||
label={`${children}`}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { MouseEvent } from 'react';
|
||||
import { MouseEvent, ReactNode } from 'react';
|
||||
import { IconComponent } from 'twenty-ui';
|
||||
|
||||
import { LightIconButtonGroup } from '@/ui/input/button/components/LightIconButtonGroup';
|
||||
@ -18,7 +18,7 @@ export type MenuItemIconButton = {
|
||||
export type MenuItemProps = {
|
||||
LeftIcon?: IconComponent | null;
|
||||
accent?: MenuItemAccent;
|
||||
text: string;
|
||||
text: ReactNode;
|
||||
iconButtons?: MenuItemIconButton[];
|
||||
isIconDisplayedOnHoverOnly?: boolean;
|
||||
isTooltipOpen?: boolean;
|
||||
|
||||
@ -1,4 +1,6 @@
|
||||
import { ReactNode } from 'react';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import { isString } from '@sniptt/guards';
|
||||
import {
|
||||
IconComponent,
|
||||
IconGripVertical,
|
||||
@ -14,7 +16,7 @@ type MenuItemLeftContentProps = {
|
||||
className?: string;
|
||||
LeftIcon: IconComponent | null | undefined;
|
||||
showGrip?: boolean;
|
||||
text: string;
|
||||
text: ReactNode;
|
||||
};
|
||||
|
||||
export const MenuItemLeftContent = ({
|
||||
@ -38,7 +40,7 @@ export const MenuItemLeftContent = ({
|
||||
<LeftIcon size={theme.icon.size.md} stroke={theme.icon.stroke.sm} />
|
||||
)}
|
||||
<StyledMenuItemLabel hasLeftIcon={!!LeftIcon}>
|
||||
<OverflowingTextWithTooltip text={text} />
|
||||
{isString(text) ? <OverflowingTextWithTooltip text={text} /> : text}
|
||||
</StyledMenuItemLabel>
|
||||
</StyledMenuItemLeftContent>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user