Skip to content

Commit

Permalink
feat: add links to Links field
Browse files Browse the repository at this point in the history
Closes #5115, Closes #5116
  • Loading branch information
thaisguigon committed Apr 30, 2024
1 parent c498bc1 commit 9770ec9
Show file tree
Hide file tree
Showing 12 changed files with 229 additions and 134 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} />
) : 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 { isFieldRelation } from '@/object-record/record-field/types/guards/isFieldRelation';
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';

Expand All @@ -17,17 +18,12 @@ export const useGetButtonIcon = (): IconComponent | undefined => {
if (
isFieldLink(fieldDefinition) ||
isFieldEmail(fieldDefinition) ||
isFieldPhone(fieldDefinition)
isFieldPhone(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,134 @@
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 { 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;
};

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

const handleEnter = (url: string) => {
onEnter?.(() =>
persistLinksField({
primaryLinkUrl: url,
primaryLinkLabel: '',
secondaryLinks: [],
}),
);
};
export const LinksFieldInput = ({ onCancel }: LinksFieldInputProps) => {
const { persistLinksField, hotkeyScope, fieldValue } = useLinksField();

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

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

const isTargetInput =
event.target instanceof HTMLInputElement &&
event.target.tagName === 'INPUT';

if (!isTargetInput) {
onCancel?.();
}
},
});

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

return;
}

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

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,6 +8,8 @@ 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';

Expand All @@ -30,14 +32,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
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
import { FieldLinksValue } from '@/object-record/record-field/types/FieldMetadata';
import { LinkDisplay } from '@/ui/field/display/components/LinkDisplay';
import { getUrlHostName } from '~/utils/url/getUrlHostName';

type LinksDisplayProps = {
value?: FieldLinksValue;
};

export const LinksDisplay = ({ value }: LinksDisplayProps) => {
const url = value?.primaryLinkUrl || '';
const label = value?.primaryLinkLabel || getUrlHostName(url);
const label = value?.primaryLinkLabel || '';

return <LinkDisplay value={{ url, label }} />;
};

0 comments on commit 9770ec9

Please sign in to comment.