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
@@ -8,6 +8,7 @@ export enum LinkType {
LinkedIn = 'linkedin',
Twitter = 'twitter',
Facebook = 'facebook',
Instagram = 'instagram',
}
type SocialLinkProps = {
@@ -137,6 +137,80 @@ describe('getDisplayValueByUrlType', () => {
});
});
describe('instagram', () => {
it('should extract handle from Instagram profile URL with @ prefix', () => {
const result = getDisplayValueByUrlType({
type: LinkType.Instagram,
href: 'https://www.instagram.com/ptcrash',
});
expect(result).toBe('@ptcrash');
});
it('should handle Instagram URL without protocol', () => {
const result = getDisplayValueByUrlType({
type: LinkType.Instagram,
href: 'instagram.com/ptcrash',
});
expect(result).toBe('@ptcrash');
});
it('should handle Instagram URL without www', () => {
const result = getDisplayValueByUrlType({
type: LinkType.Instagram,
href: 'https://instagram.com/ptcrash',
});
expect(result).toBe('@ptcrash');
});
it('should ignore a trailing slash', () => {
const result = getDisplayValueByUrlType({
type: LinkType.Instagram,
href: 'https://instagram.com/ptcrash/',
});
expect(result).toBe('@ptcrash');
});
it('should ignore a query string', () => {
const result = getDisplayValueByUrlType({
type: LinkType.Instagram,
href: 'https://instagram.com/ptcrash?hl=en',
});
expect(result).toBe('@ptcrash');
});
it('should decode URL-encoded characters in the handle', () => {
const result = getDisplayValueByUrlType({
type: LinkType.Instagram,
href: 'https://instagram.com/john%20doe',
});
expect(result).toBe('@john doe');
});
it('should return "Instagram" for a post URL', () => {
const result = getDisplayValueByUrlType({
type: LinkType.Instagram,
href: 'https://instagram.com/p/ABC123',
});
expect(result).toBe('Instagram');
});
it('should return "Instagram" for a reel URL', () => {
const result = getDisplayValueByUrlType({
type: LinkType.Instagram,
href: 'https://instagram.com/reel/xyz',
});
expect(result).toBe('Instagram');
});
it('should return "Instagram" when no handle can be extracted', () => {
const result = getDisplayValueByUrlType({
type: LinkType.Instagram,
href: 'https://www.instagram.com/',
});
expect(result).toBe('Instagram');
});
});
describe('url type', () => {
it('should return undefined for generic url type', () => {
const result = getDisplayValueByUrlType({
@@ -1,6 +1,18 @@
import { type LinkType } from '@ui/navigation';
import { isDefined } from '@ui/utilities/utils/isDefined';
// Instagram path segments that are app routes, not user handles
const INSTAGRAM_RESERVED_PATHS = [
'p',
'reel',
'reels',
'explore',
'stories',
'tv',
'accounts',
'about',
];
type getUrlDisplayValueByUrlTypeProps = {
type: LinkType;
href: string;
@@ -40,4 +52,19 @@ export const getDisplayValueByUrlType = ({
return 'Facebook';
}
}
if (type === 'instagram') {
const matches = href.match(
/(?:https?:\/\/)?(?:www\.)?instagram\.com\/([^/?#]+)/,
);
const handle = matches?.[1];
if (
isDefined(handle) &&
!INSTAGRAM_RESERVED_PATHS.includes(handle.toLowerCase())
) {
return `@${decodeURIComponent(handle)}`;
} else {
return 'Instagram';
}
}
};