Skip to content

Commit

Permalink
feat: add links to Links field (#5223)
Browse files Browse the repository at this point in the history
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>
  • Loading branch information
thaisguigon and magrinj committed May 7, 2024
1 parent 8074aae commit b0d1cc9
Show file tree
Hide file tree
Showing 12 changed files with 301 additions and 141 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -134,13 +134,7 @@ export const FieldInput = ({
onShiftTab={onShiftTab}
/>
) : isFieldLinks(fieldDefinition) ? (
<LinksFieldInput
onEnter={onEnter}
onEscape={onEscape}
onClickOutside={onClickOutside}
onTab={onTab}
onShiftTab={onShiftTab}
/>
<LinksFieldInput onCancel={onCancel} onSubmit={onSubmit} />
) : isFieldCurrency(fieldDefinition) ? (
<CurrencyFieldInput
onEnter={onEnter}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useContext } from 'react';
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 { isFieldRelation } from '@/object-record/record-field/types/guards/isFieldRelation';
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
Expand All @@ -19,17 +20,12 @@ export const useGetButtonIcon = (): IconComponent | undefined => {
isFieldLink(fieldDefinition) ||
isFieldEmail(fieldDefinition) ||
isFieldPhone(fieldDefinition) ||
isFieldMultiSelect(fieldDefinition)
isFieldMultiSelect(fieldDefinition) ||
(isFieldRelation(fieldDefinition) &&
fieldDefinition.metadata.relationObjectMetadataNameSingular !==
'workspaceMember') ||
isFieldLinks(fieldDefinition)
) {
return IconPencil;
}

if (isFieldRelation(fieldDefinition)) {
if (
fieldDefinition.metadata.relationObjectMetadataNameSingular !==
'workspaceMember'
) {
return IconPencil;
}
}
};
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export const LinkFieldInput = ({
onEnter?.(() =>
persistLinkField({
url: newURL,
label: newURL,
label: '',
}),
);
};
Expand All @@ -36,7 +36,7 @@ export const LinkFieldInput = ({
onEscape?.(() =>
persistLinkField({
url: newURL,
label: newURL,
label: '',
}),
);
};
Expand All @@ -48,7 +48,7 @@ export const LinkFieldInput = ({
onClickOutside?.(() =>
persistLinkField({
url: newURL,
label: newURL,
label: '',
}),
);
};
Expand All @@ -57,7 +57,7 @@ export const LinkFieldInput = ({
onTab?.(() =>
persistLinkField({
url: newURL,
label: newURL,
label: '',
}),
);
};
Expand All @@ -66,15 +66,15 @@ export const LinkFieldInput = ({
onShiftTab?.(() =>
persistLinkField({
url: newURL,
label: newURL,
label: '',
}),
);
};

const handleChange = (newURL: string) => {
setDraftValue({
url: newURL,
label: newURL,
label: '',
});
};

Expand Down
Original file line number Diff line number Diff line change
@@ -1,99 +1,143 @@
import { useMemo, useRef, useState } from 'react';
import styled from '@emotion/styled';
import { Key } from 'ts-key-enum';
import { IconPlus } from 'twenty-ui';

import { useLinksField } from '@/object-record/record-field/meta-types/hooks/useLinksField';
import { FieldInputOverlay } from '@/ui/field/input/components/FieldInputOverlay';
import { TextInput } from '@/ui/field/input/components/TextInput';
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';

import { FieldInputEvent } from './DateTimeFieldInput';
const StyledDropdownMenu = styled(DropdownMenu)`
left: -1px;
position: absolute;
top: -1px;
`;

export type LinksFieldInputProps = {
onClickOutside?: FieldInputEvent;
onEnter?: FieldInputEvent;
onEscape?: FieldInputEvent;
onTab?: FieldInputEvent;
onShiftTab?: FieldInputEvent;
onCancel?: () => void;
onSubmit?: FieldInputEvent;
};

export const LinksFieldInput = ({
onEnter,
onEscape,
onClickOutside,
onTab,
onShiftTab,
onCancel,
onSubmit,
}: LinksFieldInputProps) => {
const { draftValue, setDraftValue, hotkeyScope, persistLinksField } =
useLinksField();
const { persistLinksField, hotkeyScope, fieldValue } = useLinksField();

const handleEnter = (url: string) => {
onEnter?.(() =>
persistLinksField({
primaryLinkUrl: url,
primaryLinkLabel: '',
secondaryLinks: [],
}),
);
};
const containerRef = useRef<HTMLDivElement>(null);

const handleEscape = (url: string) => {
onEscape?.(() =>
persistLinksField({
primaryLinkUrl: url,
primaryLinkLabel: '',
secondaryLinks: [],
}),
);
};
const links = useMemo(
() =>
[
fieldValue.primaryLinkUrl
? {
url: fieldValue.primaryLinkUrl,
label: fieldValue.primaryLinkLabel,
}
: null,
...(fieldValue.secondaryLinks ?? []),
].filter(isDefined),
[
fieldValue.primaryLinkLabel,
fieldValue.primaryLinkUrl,
fieldValue.secondaryLinks,
],
);

const handleClickOutside = (event: MouseEvent | TouchEvent, url: string) => {
onClickOutside?.(() =>
persistLinksField({
primaryLinkUrl: url,
primaryLinkLabel: '',
secondaryLinks: [],
}),
);
};
useListenClickOutside({
refs: [containerRef],
callback: (event) => {
event.stopImmediatePropagation();

const handleTab = (url: string) => {
onTab?.(() =>
persistLinksField({
primaryLinkUrl: url,
primaryLinkLabel: '',
secondaryLinks: [],
}),
);
};
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 handleShiftTab = (url: string) => {
onShiftTab?.(() =>
const handleSubmit = () => {
if (!inputValue) return;

setIsInputDisplayed(false);
setInputValue('');

if (!links.length) {
onSubmit?.(() =>
persistLinksField({
primaryLinkUrl: inputValue,
primaryLinkLabel: '',
secondaryLinks: [],
}),
);

return;
}

onSubmit?.(() =>
persistLinksField({
primaryLinkUrl: url,
primaryLinkLabel: '',
secondaryLinks: [],
...fieldValue,
secondaryLinks: [
...(fieldValue.secondaryLinks ?? []),
{ label: '', url: inputValue },
],
}),
);
};

const handleChange = (url: string) => {
setDraftValue({
primaryLinkUrl: url,
primaryLinkLabel: '',
secondaryLinks: [],
});
};

return (
<FieldInputOverlay>
<TextInput
value={draftValue?.primaryLinkUrl ?? ''}
autoFocus
placeholder="Links"
hotkeyScope={hotkeyScope}
onClickOutside={handleClickOutside}
onEnter={handleEnter}
onEscape={handleEscape}
onTab={handleTab}
onShiftTab={handleShiftTab}
onChange={handleChange}
/>
</FieldInputOverlay>
<StyledDropdownMenu ref={containerRef} width={200}>
{!!links.length && (
<>
<DropdownMenuItemsContainer>
{links.map(({ label, url }, index) => (
<MenuItem
key={index}
text={<LinkDisplay value={{ label, url }} />}
/>
))}
</DropdownMenuItemsContainer>
<DropdownMenuSeparator />
</>
)}
{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>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
`;
Expand All @@ -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) {
Expand Down

0 comments on commit b0d1cc9

Please sign in to comment.