feat(front): render Instagram URLs as @handles in link fields (#21642)

LinkedIn and X links already show a readable handle in Twenty's link
fields. Instagram doesn't — it just shows `instagram.com`, which isn't
much help when you're scanning a record.

This adds the same handling for Instagram. `instagram.com/ptcrash` now
shows as `@ptcrash`, in tables, on record pages, and in the edit menu.
Post and reel links (`/p/...`, `/reel/...`) have no handle, so they fall
back to `Instagram`.

How it works:
- `Instagram` added to the `LinkType` enum
- `checkUrlType` detects `instagram.com`
- `getDisplayValueByUrlType` pulls the handle and prefixes `@`
- a shared `isSocialLinkType` helper keeps the three display components
in sync

Tested with unit tests for both helpers, the updated story, and manually
against a record whose Instagram field is
`http://instagram.com/ptcrash`.

Closes #21644

Co-authored-by: Johnny Martin <ptcrash@users.noreply.github.com>
This commit is contained in:
Johnny Martin
2026-06-23 11:25:50 -04:00
committed by GitHub
parent de610bc4e7
commit d4e4e2612b
11 changed files with 192 additions and 17 deletions
@@ -1,6 +1,7 @@
import { isNonEmptyString } from '@sniptt/guards';
import { LinkType, RoundedLink, SocialLink } from 'twenty-ui/navigation';
import { RoundedLink, SocialLink } from 'twenty-ui/navigation';
import { checkUrlType } from '~/utils/checkUrlType';
import { isSocialLinkType } from '~/utils/isSocialLinkType';
import { getSafeUrl } from 'twenty-shared/utils';
type LinkDisplayProps = {
@@ -22,11 +23,7 @@ export const LinkDisplay = ({ value }: LinkDisplayProps) => {
const type = checkUrlType(absoluteUrl);
if (
type === LinkType.LinkedIn ||
type === LinkType.Twitter ||
type === LinkType.Facebook
) {
if (isSocialLinkType(type)) {
return <SocialLink href={absoluteUrl} type={type} label={displayedValue} />;
}
@@ -8,8 +8,9 @@ import {
getUrlHostnameOrThrow,
isDefined,
} from 'twenty-shared/utils';
import { LinkType, RoundedLink, SocialLink } from 'twenty-ui/navigation';
import { RoundedLink, SocialLink } from 'twenty-ui/navigation';
import { checkUrlType } from '~/utils/checkUrlType';
import { isSocialLinkType } from '~/utils/isSocialLinkType';
type LinksDisplayProps = {
value?: FieldLinksValue;
@@ -43,9 +44,7 @@ export const LinksDisplay = ({ value, onLinkClick }: LinksDisplayProps) => {
return (
<ExpandableList>
{links.map(({ url, label, type }, index) =>
type === LinkType.LinkedIn ||
type === LinkType.Twitter ||
type === LinkType.Facebook ? (
isSocialLinkType(type) ? (
<SocialLink
key={index}
href={url}
@@ -1,7 +1,8 @@
import { type MouseEvent } from 'react';
import { LinkType, RoundedLink, SocialLink } from 'twenty-ui/navigation';
import { RoundedLink, SocialLink } from 'twenty-ui/navigation';
import { checkUrlType } from '~/utils/checkUrlType';
import { isSocialLinkType } from '~/utils/isSocialLinkType';
import { getSafeUrl } from 'twenty-shared/utils';
import { EllipsisDisplay } from './EllipsisDisplay';
@@ -20,11 +21,7 @@ export const URLDisplay = ({ value }: URLDisplayProps) => {
const type = checkUrlType(absoluteUrl);
if (
type === LinkType.LinkedIn ||
type === LinkType.Twitter ||
type === LinkType.Facebook
) {
if (isSocialLinkType(type)) {
return (
<EllipsisDisplay>
<SocialLink
@@ -128,6 +128,10 @@ export const SocialMediaLinks: Story = {
primaryLinkLabel: 'Twenty on LinkedIn',
secondaryLinks: [
{ url: 'https://twitter.com/twentycrm', label: 'Twenty on Twitter' },
{
url: 'https://www.instagram.com/twenty_hq',
label: 'Twenty on Instagram',
},
],
},
},
@@ -136,7 +140,7 @@ export const SocialMediaLinks: Story = {
await waitFor(() => {
const links = canvas.queryAllByRole('link');
expect(links).toHaveLength(2);
expect(links).toHaveLength(3);
});
const linkedinLink = await canvas.findByText('twenty');
@@ -152,6 +156,44 @@ export const SocialMediaLinks: Story = {
'href',
'https://twitter.com/twentycrm',
);
const instagramLink = await canvas.findByText('@twenty_hq');
expect(instagramLink).toBeVisible();
expect(instagramLink).toHaveAttribute(
'href',
'https://www.instagram.com/twenty_hq',
);
},
};
export const InstagramLinks: Story = {
args: {
value: {
primaryLinkUrl: 'https://www.instagram.com/twenty_hq',
primaryLinkLabel: 'Twenty on Instagram',
secondaryLinks: [
{ url: 'https://instagram.com/p/ABC123', label: 'A post' },
{ url: 'https://instagram.com/reel/XYZ789', label: 'A reel' },
],
},
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await waitFor(() => {
const links = canvas.queryAllByRole('link');
expect(links).toHaveLength(3);
});
const handleLink = await canvas.findByText('@twenty_hq');
expect(handleLink).toBeVisible();
expect(handleLink).toHaveAttribute(
'href',
'https://www.instagram.com/twenty_hq',
);
const fallbackLinks = await canvas.findAllByText('Instagram');
expect(fallbackLinks).toHaveLength(2);
},
};