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:
@@ -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';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user