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:
@ -134,13 +134,7 @@ export const FieldInput = ({
|
|||||||
onShiftTab={onShiftTab}
|
onShiftTab={onShiftTab}
|
||||||
/>
|
/>
|
||||||
) : isFieldLinks(fieldDefinition) ? (
|
) : isFieldLinks(fieldDefinition) ? (
|
||||||
<LinksFieldInput
|
<LinksFieldInput onCancel={onCancel} onSubmit={onSubmit} />
|
||||||
onEnter={onEnter}
|
|
||||||
onEscape={onEscape}
|
|
||||||
onClickOutside={onClickOutside}
|
|
||||||
onTab={onTab}
|
|
||||||
onShiftTab={onShiftTab}
|
|
||||||
/>
|
|
||||||
) : isFieldCurrency(fieldDefinition) ? (
|
) : isFieldCurrency(fieldDefinition) ? (
|
||||||
<CurrencyFieldInput
|
<CurrencyFieldInput
|
||||||
onEnter={onEnter}
|
onEnter={onEnter}
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { useContext } from 'react';
|
import { useContext } from 'react';
|
||||||
import { IconComponent, IconPencil } from 'twenty-ui';
|
import { IconComponent, IconPencil } from 'twenty-ui';
|
||||||
|
|
||||||
|
import { isFieldLinks } from '@/object-record/record-field/types/guards/isFieldLinks';
|
||||||
import { isFieldMultiSelect } from '@/object-record/record-field/types/guards/isFieldMultiSelect';
|
import { isFieldMultiSelect } from '@/object-record/record-field/types/guards/isFieldMultiSelect';
|
||||||
import { isFieldRelation } from '@/object-record/record-field/types/guards/isFieldRelation';
|
import { isFieldRelation } from '@/object-record/record-field/types/guards/isFieldRelation';
|
||||||
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
|
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
|
||||||
@ -19,17 +20,12 @@ export const useGetButtonIcon = (): IconComponent | undefined => {
|
|||||||
isFieldLink(fieldDefinition) ||
|
isFieldLink(fieldDefinition) ||
|
||||||
isFieldEmail(fieldDefinition) ||
|
isFieldEmail(fieldDefinition) ||
|
||||||
isFieldPhone(fieldDefinition) ||
|
isFieldPhone(fieldDefinition) ||
|
||||||
isFieldMultiSelect(fieldDefinition)
|
isFieldMultiSelect(fieldDefinition) ||
|
||||||
|
(isFieldRelation(fieldDefinition) &&
|
||||||
|
fieldDefinition.metadata.relationObjectMetadataNameSingular !==
|
||||||
|
'workspaceMember') ||
|
||||||
|
isFieldLinks(fieldDefinition)
|
||||||
) {
|
) {
|
||||||
return IconPencil;
|
return IconPencil;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isFieldRelation(fieldDefinition)) {
|
|
||||||
if (
|
|
||||||
fieldDefinition.metadata.relationObjectMetadataNameSingular !==
|
|
||||||
'workspaceMember'
|
|
||||||
) {
|
|
||||||
return IconPencil;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|||||||
@ -27,7 +27,7 @@ export const LinkFieldInput = ({
|
|||||||
onEnter?.(() =>
|
onEnter?.(() =>
|
||||||
persistLinkField({
|
persistLinkField({
|
||||||
url: newURL,
|
url: newURL,
|
||||||
label: newURL,
|
label: '',
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -36,7 +36,7 @@ export const LinkFieldInput = ({
|
|||||||
onEscape?.(() =>
|
onEscape?.(() =>
|
||||||
persistLinkField({
|
persistLinkField({
|
||||||
url: newURL,
|
url: newURL,
|
||||||
label: newURL,
|
label: '',
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -48,7 +48,7 @@ export const LinkFieldInput = ({
|
|||||||
onClickOutside?.(() =>
|
onClickOutside?.(() =>
|
||||||
persistLinkField({
|
persistLinkField({
|
||||||
url: newURL,
|
url: newURL,
|
||||||
label: newURL,
|
label: '',
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -57,7 +57,7 @@ export const LinkFieldInput = ({
|
|||||||
onTab?.(() =>
|
onTab?.(() =>
|
||||||
persistLinkField({
|
persistLinkField({
|
||||||
url: newURL,
|
url: newURL,
|
||||||
label: newURL,
|
label: '',
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -66,7 +66,7 @@ export const LinkFieldInput = ({
|
|||||||
onShiftTab?.(() =>
|
onShiftTab?.(() =>
|
||||||
persistLinkField({
|
persistLinkField({
|
||||||
url: newURL,
|
url: newURL,
|
||||||
label: newURL,
|
label: '',
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -74,7 +74,7 @@ export const LinkFieldInput = ({
|
|||||||
const handleChange = (newURL: string) => {
|
const handleChange = (newURL: string) => {
|
||||||
setDraftValue({
|
setDraftValue({
|
||||||
url: newURL,
|
url: newURL,
|
||||||
label: newURL,
|
label: '',
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -1,99 +1,143 @@
|
|||||||
import { useLinksField } from '@/object-record/record-field/meta-types/hooks/useLinksField';
|
import { useMemo, useRef, useState } from 'react';
|
||||||
import { FieldInputOverlay } from '@/ui/field/input/components/FieldInputOverlay';
|
import styled from '@emotion/styled';
|
||||||
import { TextInput } from '@/ui/field/input/components/TextInput';
|
import { Key } from 'ts-key-enum';
|
||||||
|
import { IconPlus } from 'twenty-ui';
|
||||||
|
|
||||||
import { FieldInputEvent } from './DateTimeFieldInput';
|
import { useLinksField } from '@/object-record/record-field/meta-types/hooks/useLinksField';
|
||||||
|
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';
|
||||||
|
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||||
|
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
|
||||||
|
import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
|
||||||
|
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
||||||
|
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
|
||||||
|
import { isDefined } from '~/utils/isDefined';
|
||||||
|
|
||||||
|
const StyledDropdownMenu = styled(DropdownMenu)`
|
||||||
|
left: -1px;
|
||||||
|
position: absolute;
|
||||||
|
top: -1px;
|
||||||
|
`;
|
||||||
|
|
||||||
export type LinksFieldInputProps = {
|
export type LinksFieldInputProps = {
|
||||||
onClickOutside?: FieldInputEvent;
|
onCancel?: () => void;
|
||||||
onEnter?: FieldInputEvent;
|
onSubmit?: FieldInputEvent;
|
||||||
onEscape?: FieldInputEvent;
|
|
||||||
onTab?: FieldInputEvent;
|
|
||||||
onShiftTab?: FieldInputEvent;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const LinksFieldInput = ({
|
export const LinksFieldInput = ({
|
||||||
onEnter,
|
onCancel,
|
||||||
onEscape,
|
onSubmit,
|
||||||
onClickOutside,
|
|
||||||
onTab,
|
|
||||||
onShiftTab,
|
|
||||||
}: LinksFieldInputProps) => {
|
}: LinksFieldInputProps) => {
|
||||||
const { draftValue, setDraftValue, hotkeyScope, persistLinksField } =
|
const { persistLinksField, hotkeyScope, fieldValue } = useLinksField();
|
||||||
useLinksField();
|
|
||||||
|
|
||||||
const handleEnter = (url: string) => {
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
onEnter?.(() =>
|
|
||||||
|
const links = useMemo(
|
||||||
|
() =>
|
||||||
|
[
|
||||||
|
fieldValue.primaryLinkUrl
|
||||||
|
? {
|
||||||
|
url: fieldValue.primaryLinkUrl,
|
||||||
|
label: fieldValue.primaryLinkLabel,
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
...(fieldValue.secondaryLinks ?? []),
|
||||||
|
].filter(isDefined),
|
||||||
|
[
|
||||||
|
fieldValue.primaryLinkLabel,
|
||||||
|
fieldValue.primaryLinkUrl,
|
||||||
|
fieldValue.secondaryLinks,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
useListenClickOutside({
|
||||||
|
refs: [containerRef],
|
||||||
|
callback: (event) => {
|
||||||
|
event.stopImmediatePropagation();
|
||||||
|
|
||||||
|
const isTargetInput =
|
||||||
|
event.target instanceof HTMLInputElement &&
|
||||||
|
event.target.tagName === 'INPUT';
|
||||||
|
|
||||||
|
if (!isTargetInput) {
|
||||||
|
onCancel?.();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const [isInputDisplayed, setIsInputDisplayed] = useState(false);
|
||||||
|
const [inputValue, setInputValue] = useState('');
|
||||||
|
|
||||||
|
useScopedHotkeys(Key.Escape, onCancel ?? (() => {}), hotkeyScope);
|
||||||
|
|
||||||
|
const handleSubmit = () => {
|
||||||
|
if (!inputValue) return;
|
||||||
|
|
||||||
|
setIsInputDisplayed(false);
|
||||||
|
setInputValue('');
|
||||||
|
|
||||||
|
if (!links.length) {
|
||||||
|
onSubmit?.(() =>
|
||||||
|
persistLinksField({
|
||||||
|
primaryLinkUrl: inputValue,
|
||||||
|
primaryLinkLabel: '',
|
||||||
|
secondaryLinks: [],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
onSubmit?.(() =>
|
||||||
persistLinksField({
|
persistLinksField({
|
||||||
primaryLinkUrl: url,
|
...fieldValue,
|
||||||
primaryLinkLabel: '',
|
secondaryLinks: [
|
||||||
secondaryLinks: [],
|
...(fieldValue.secondaryLinks ?? []),
|
||||||
|
{ label: '', url: inputValue },
|
||||||
|
],
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleEscape = (url: string) => {
|
|
||||||
onEscape?.(() =>
|
|
||||||
persistLinksField({
|
|
||||||
primaryLinkUrl: url,
|
|
||||||
primaryLinkLabel: '',
|
|
||||||
secondaryLinks: [],
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleClickOutside = (event: MouseEvent | TouchEvent, url: string) => {
|
|
||||||
onClickOutside?.(() =>
|
|
||||||
persistLinksField({
|
|
||||||
primaryLinkUrl: url,
|
|
||||||
primaryLinkLabel: '',
|
|
||||||
secondaryLinks: [],
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleTab = (url: string) => {
|
|
||||||
onTab?.(() =>
|
|
||||||
persistLinksField({
|
|
||||||
primaryLinkUrl: url,
|
|
||||||
primaryLinkLabel: '',
|
|
||||||
secondaryLinks: [],
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleShiftTab = (url: string) => {
|
|
||||||
onShiftTab?.(() =>
|
|
||||||
persistLinksField({
|
|
||||||
primaryLinkUrl: url,
|
|
||||||
primaryLinkLabel: '',
|
|
||||||
secondaryLinks: [],
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleChange = (url: string) => {
|
|
||||||
setDraftValue({
|
|
||||||
primaryLinkUrl: url,
|
|
||||||
primaryLinkLabel: '',
|
|
||||||
secondaryLinks: [],
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FieldInputOverlay>
|
<StyledDropdownMenu ref={containerRef} width={200}>
|
||||||
<TextInput
|
{!!links.length && (
|
||||||
value={draftValue?.primaryLinkUrl ?? ''}
|
<>
|
||||||
autoFocus
|
<DropdownMenuItemsContainer>
|
||||||
placeholder="Links"
|
{links.map(({ label, url }, index) => (
|
||||||
hotkeyScope={hotkeyScope}
|
<MenuItem
|
||||||
onClickOutside={handleClickOutside}
|
key={index}
|
||||||
onEnter={handleEnter}
|
text={<LinkDisplay value={{ label, url }} />}
|
||||||
onEscape={handleEscape}
|
/>
|
||||||
onTab={handleTab}
|
))}
|
||||||
onShiftTab={handleShiftTab}
|
</DropdownMenuItemsContainer>
|
||||||
onChange={handleChange}
|
<DropdownMenuSeparator />
|
||||||
/>
|
</>
|
||||||
</FieldInputOverlay>
|
)}
|
||||||
|
{isInputDisplayed ? (
|
||||||
|
<DropdownMenuInput
|
||||||
|
autoFocus
|
||||||
|
placeholder="URL"
|
||||||
|
value={inputValue}
|
||||||
|
hotkeyScope={hotkeyScope}
|
||||||
|
onChange={(event) => setInputValue(event.target.value)}
|
||||||
|
onEnter={handleSubmit}
|
||||||
|
rightComponent={
|
||||||
|
<LightIconButton Icon={IconPlus} onClick={handleSubmit} />
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<DropdownMenuItemsContainer>
|
||||||
|
<MenuItem
|
||||||
|
onClick={() => setIsInputDisplayed(true)}
|
||||||
|
LeftIcon={IconPlus}
|
||||||
|
text="Add link"
|
||||||
|
/>
|
||||||
|
</DropdownMenuItemsContainer>
|
||||||
|
)}
|
||||||
|
</StyledDropdownMenu>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -8,15 +8,14 @@ import {
|
|||||||
SocialLink,
|
SocialLink,
|
||||||
} from '@/ui/navigation/link/components/SocialLink';
|
} from '@/ui/navigation/link/components/SocialLink';
|
||||||
import { checkUrlType } from '~/utils/checkUrlType';
|
import { checkUrlType } from '~/utils/checkUrlType';
|
||||||
|
import { getAbsoluteUrl } from '~/utils/url/getAbsoluteUrl';
|
||||||
|
import { getUrlHostName } from '~/utils/url/getUrlHostName';
|
||||||
|
|
||||||
import { EllipsisDisplay } from './EllipsisDisplay';
|
import { EllipsisDisplay } from './EllipsisDisplay';
|
||||||
|
|
||||||
const StyledRawLink = styled(RoundedLink)`
|
const StyledRawLink = styled(RoundedLink)`
|
||||||
overflow: hidden;
|
|
||||||
|
|
||||||
a {
|
a {
|
||||||
overflow: hidden;
|
font-size: ${({ theme }) => theme.font.size.md};
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
@ -30,14 +29,8 @@ export const LinkDisplay = ({ value }: LinkDisplayProps) => {
|
|||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
};
|
};
|
||||||
|
|
||||||
const absoluteUrl = value?.url
|
const absoluteUrl = getAbsoluteUrl(value?.url || '');
|
||||||
? value.url.startsWith('http')
|
const displayedValue = value?.label || getUrlHostName(absoluteUrl);
|
||||||
? value.url
|
|
||||||
: 'https://' + value.url
|
|
||||||
: '';
|
|
||||||
|
|
||||||
const displayedValue = value?.label || value?.url || '';
|
|
||||||
|
|
||||||
const type = checkUrlType(absoluteUrl);
|
const type = checkUrlType(absoluteUrl);
|
||||||
|
|
||||||
if (type === LinkType.LinkedIn || type === LinkType.Twitter) {
|
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 { 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';
|
import { getUrlHostName } from '~/utils/url/getUrlHostName';
|
||||||
|
|
||||||
|
const StyledContainer = styled(EllipsisDisplay)`
|
||||||
|
display: flex;
|
||||||
|
gap: ${({ theme }) => theme.spacing(1)};
|
||||||
|
`;
|
||||||
|
|
||||||
type LinksDisplayProps = {
|
type LinksDisplayProps = {
|
||||||
value?: FieldLinksValue;
|
value?: FieldLinksValue;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const LinksDisplay = ({ value }: LinksDisplayProps) => {
|
export const LinksDisplay = ({ value }: LinksDisplayProps) => {
|
||||||
const url = value?.primaryLinkUrl || '';
|
const links = useMemo(
|
||||||
const label = value?.primaryLinkLabel || getUrlHostName(url);
|
() =>
|
||||||
|
[
|
||||||
|
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 styled from '@emotion/styled';
|
||||||
|
|
||||||
|
import { useRegisterInputEvents } from '@/object-record/record-field/meta-types/input/hooks/useRegisterInputEvents';
|
||||||
import { RGBA } from '@/ui/theme/constants/Rgba';
|
import { RGBA } from '@/ui/theme/constants/Rgba';
|
||||||
import { TEXT_INPUT_STYLE } from '@/ui/theme/constants/TextInputStyle';
|
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}
|
${TEXT_INPUT_STYLE}
|
||||||
|
|
||||||
border: 1px solid ${({ theme }) => theme.border.color.medium};
|
border: 1px solid ${({ theme }) => theme.border.color.medium};
|
||||||
@ -19,27 +22,87 @@ const StyledInput = styled.input`
|
|||||||
border-color: ${({ theme }) => theme.color.blue};
|
border-color: ${({ theme }) => theme.color.blue};
|
||||||
box-shadow: 0px 0px 0px 3px ${({ theme }) => RGBA(theme.color.blue, 0.1)};
|
box-shadow: 0px 0px 0px 3px ${({ theme }) => RGBA(theme.color.blue, 0.1)};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
${({ withRightComponent }) =>
|
||||||
|
withRightComponent &&
|
||||||
|
css`
|
||||||
|
padding-right: 32px;
|
||||||
|
`}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const StyledInputContainer = styled.div`
|
const StyledInputContainer = styled.div`
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
padding: ${({ theme }) => theme.spacing(1)};
|
padding: ${({ theme }) => theme.spacing(1)};
|
||||||
|
position: relative;
|
||||||
width: 100%;
|
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<
|
export const DropdownMenuInput = forwardRef<
|
||||||
HTMLInputElement,
|
HTMLInputElement,
|
||||||
InputHTMLAttributes<HTMLInputElement>
|
DropdownMenuInputProps
|
||||||
>(({ autoFocus, value, placeholder, onChange }, ref) => {
|
>(
|
||||||
return (
|
(
|
||||||
<StyledInputContainer>
|
{
|
||||||
<StyledInput
|
autoFocus,
|
||||||
autoFocus={autoFocus}
|
className,
|
||||||
value={value}
|
value,
|
||||||
placeholder={placeholder}
|
placeholder,
|
||||||
onChange={onChange}
|
hotkeyScope = 'dropdown-menu-input',
|
||||||
ref={ref}
|
onChange,
|
||||||
/>
|
onClickOutside,
|
||||||
</StyledInputContainer>
|
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 = {
|
type RoundedLinkProps = {
|
||||||
href: string;
|
href: string;
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
|
className?: string;
|
||||||
onClick?: (event: React.MouseEvent<HTMLElement>) => void;
|
onClick?: (event: React.MouseEvent<HTMLElement>) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -22,14 +23,20 @@ const StyledClickable = styled.div`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
const StyledChip = styled(Chip)`
|
const StyledChip = styled(Chip)`
|
||||||
|
border-color: ${({ theme }) => theme.border.color.strong};
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
padding: ${({ theme }) => theme.spacing(2)};
|
padding: ${({ theme }) => theme.spacing(2)};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const RoundedLink = ({ children, href, onClick }: RoundedLinkProps) => (
|
export const RoundedLink = ({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
href,
|
||||||
|
onClick,
|
||||||
|
}: RoundedLinkProps) => (
|
||||||
<div>
|
<div>
|
||||||
{children !== '' ? (
|
{children !== '' ? (
|
||||||
<StyledClickable>
|
<StyledClickable className={className}>
|
||||||
<ReactLink target="_blank" to={href} onClick={onClick}>
|
<ReactLink target="_blank" to={href} onClick={onClick}>
|
||||||
<StyledChip
|
<StyledChip
|
||||||
label={`${children}`}
|
label={`${children}`}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { MouseEvent } from 'react';
|
import { MouseEvent, ReactNode } from 'react';
|
||||||
import { IconComponent } from 'twenty-ui';
|
import { IconComponent } from 'twenty-ui';
|
||||||
|
|
||||||
import { LightIconButtonGroup } from '@/ui/input/button/components/LightIconButtonGroup';
|
import { LightIconButtonGroup } from '@/ui/input/button/components/LightIconButtonGroup';
|
||||||
@ -18,7 +18,7 @@ export type MenuItemIconButton = {
|
|||||||
export type MenuItemProps = {
|
export type MenuItemProps = {
|
||||||
LeftIcon?: IconComponent | null;
|
LeftIcon?: IconComponent | null;
|
||||||
accent?: MenuItemAccent;
|
accent?: MenuItemAccent;
|
||||||
text: string;
|
text: ReactNode;
|
||||||
iconButtons?: MenuItemIconButton[];
|
iconButtons?: MenuItemIconButton[];
|
||||||
isIconDisplayedOnHoverOnly?: boolean;
|
isIconDisplayedOnHoverOnly?: boolean;
|
||||||
isTooltipOpen?: boolean;
|
isTooltipOpen?: boolean;
|
||||||
|
|||||||
@ -1,4 +1,6 @@
|
|||||||
|
import { ReactNode } from 'react';
|
||||||
import { useTheme } from '@emotion/react';
|
import { useTheme } from '@emotion/react';
|
||||||
|
import { isString } from '@sniptt/guards';
|
||||||
import {
|
import {
|
||||||
IconComponent,
|
IconComponent,
|
||||||
IconGripVertical,
|
IconGripVertical,
|
||||||
@ -14,7 +16,7 @@ type MenuItemLeftContentProps = {
|
|||||||
className?: string;
|
className?: string;
|
||||||
LeftIcon: IconComponent | null | undefined;
|
LeftIcon: IconComponent | null | undefined;
|
||||||
showGrip?: boolean;
|
showGrip?: boolean;
|
||||||
text: string;
|
text: ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const MenuItemLeftContent = ({
|
export const MenuItemLeftContent = ({
|
||||||
@ -38,7 +40,7 @@ export const MenuItemLeftContent = ({
|
|||||||
<LeftIcon size={theme.icon.size.md} stroke={theme.icon.stroke.sm} />
|
<LeftIcon size={theme.icon.size.md} stroke={theme.icon.stroke.sm} />
|
||||||
)}
|
)}
|
||||||
<StyledMenuItemLabel hasLeftIcon={!!LeftIcon}>
|
<StyledMenuItemLabel hasLeftIcon={!!LeftIcon}>
|
||||||
<OverflowingTextWithTooltip text={text} />
|
{isString(text) ? <OverflowingTextWithTooltip text={text} /> : text}
|
||||||
</StyledMenuItemLabel>
|
</StyledMenuItemLabel>
|
||||||
</StyledMenuItemLeftContent>
|
</StyledMenuItemLeftContent>
|
||||||
);
|
);
|
||||||
|
|||||||
9
packages/twenty-front/src/utils/url/getAbsoluteUrl.ts
Normal file
9
packages/twenty-front/src/utils/url/getAbsoluteUrl.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { absoluteUrlSchema } from '~/utils/validation-schemas/absoluteUrlSchema';
|
||||||
|
|
||||||
|
export const getAbsoluteUrl = (url: string) => {
|
||||||
|
try {
|
||||||
|
return absoluteUrlSchema.parse(url);
|
||||||
|
} catch {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -1,8 +1,8 @@
|
|||||||
import { absoluteUrlSchema } from '~/utils/validation-schemas/absoluteUrlSchema';
|
import { getAbsoluteUrl } from '~/utils/url/getAbsoluteUrl';
|
||||||
|
|
||||||
export const getUrlHostName = (url: string) => {
|
export const getUrlHostName = (url: string) => {
|
||||||
try {
|
try {
|
||||||
const absoluteUrl = absoluteUrlSchema.parse(url);
|
const absoluteUrl = getAbsoluteUrl(url);
|
||||||
return new URL(absoluteUrl).hostname.replace(/^www\./i, '');
|
return new URL(absoluteUrl).hostname.replace(/^www\./i, '');
|
||||||
} catch {
|
} catch {
|
||||||
return '';
|
return '';
|
||||||
|
|||||||
Reference in New Issue
Block a user