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:
Thaïs
2024-05-07 15:05:18 +02:00
committed by GitHub
parent 8074aae449
commit b0d1cc9dcb
12 changed files with 306 additions and 146 deletions

View File

@ -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}

View File

@ -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';
@ -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;
}
}
};

View File

@ -27,7 +27,7 @@ export const LinkFieldInput = ({
onEnter?.(() =>
persistLinkField({
url: newURL,
label: newURL,
label: '',
}),
);
};
@ -36,7 +36,7 @@ export const LinkFieldInput = ({
onEscape?.(() =>
persistLinkField({
url: newURL,
label: newURL,
label: '',
}),
);
};
@ -48,7 +48,7 @@ export const LinkFieldInput = ({
onClickOutside?.(() =>
persistLinkField({
url: newURL,
label: newURL,
label: '',
}),
);
};
@ -57,7 +57,7 @@ export const LinkFieldInput = ({
onTab?.(() =>
persistLinkField({
url: newURL,
label: newURL,
label: '',
}),
);
};
@ -66,7 +66,7 @@ export const LinkFieldInput = ({
onShiftTab?.(() =>
persistLinkField({
url: newURL,
label: newURL,
label: '',
}),
);
};
@ -74,7 +74,7 @@ export const LinkFieldInput = ({
const handleChange = (newURL: string) => {
setDraftValue({
url: newURL,
label: newURL,
label: '',
});
};

View File

@ -1,99 +1,143 @@
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 { useMemo, useRef, useState } from 'react';
import styled from '@emotion/styled';
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 = {
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?.(() =>
const containerRef = useRef<HTMLDivElement>(null);
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({
primaryLinkUrl: url,
primaryLinkLabel: '',
secondaryLinks: [],
...fieldValue,
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 (
<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>
);
};