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:
@@ -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
|
||||
|
||||
+43
-1
@@ -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);
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user