From 99f54d9ea8266156c35960f30093fcabb3b697ae Mon Sep 17 00:00:00 2001 From: Thomas Trompette Date: Tue, 7 Jul 2026 13:03:29 +0200 Subject: [PATCH] fix(front): honor user-set label on LINKS/URL social links (#22586) ## Problem On LINKS and URL fields, recognized social links **always** rendered the derived handle (e.g. `@cristiano`) and ignored any user-set `label`. This was a regression: `SocialLink` did `getDisplayValueByUrlType(...) ?? label`, and since a provider always matches for social links, `label` was never reached. Adding the Instagram/TikTok/Bluesky providers in v2.16 widened the set of affected links. | Input | Expected | Before | |-------|----------|--------| | `instagram.com/cristiano`, label `Cristiano Ronaldo Official` | `Cristiano Ronaldo Official` | `@cristiano` | ## Fix (display half of #22265) - `SocialLink`: prefer a non-empty `label`; only derive the handle from the URL as fallback (then `href`). Prop widened to `string \| null`. - `LinksDisplay` / `LinkDisplay` / `URLDisplay`: pass the **raw, nullable** label into `SocialLink` instead of a pre-coalesced string, so derivation still works when no label is set. `URLDisplay` passes `label={null}` (URL fields have no label) so handles still render. - Stories: dropped `label` args the old code silently ignored (keeps existing visual snapshots stable) and added a `WithCustomLabel` story asserting precedence. ## What's left (not in this PR) The **label input in the UI** (issue's second half) is intentionally deferred. `MultiItemFieldInput` carries the in-progress edit as a single string and seeds edits with the URL only, so exposing a Label field cleanly requires a small generalization of that shared component (not a JSON-serialization workaround). That change needs manual in-app verification and will be a follow-up. ## Verification - `twenty-ui` typecheck clean; oxlint clean on all changed files; `getDisplayValueByUrlType` tests pass (38). - Added Storybook `play` assertion for the custom-label case. Fixes #22265 (display half). Related: #16414. Review in cubic --- .../field/display/components/LinkDisplay.tsx | 2 +- .../field/display/components/LinksDisplay.tsx | 7 +-- .../field/display/components/URLDisplay.tsx | 2 +- .../__stories__/LinksDisplay.stories.tsx | 49 ++++++++++++++++--- .../src/navigation/SocialLink/SocialLink.tsx | 9 ++-- .../__stories__/SocialLink.stories.tsx | 24 ++++++--- 6 files changed, 71 insertions(+), 22 deletions(-) diff --git a/packages/twenty-front/src/modules/ui/field/display/components/LinkDisplay.tsx b/packages/twenty-front/src/modules/ui/field/display/components/LinkDisplay.tsx index 0712d07dd7..6abb642f18 100644 --- a/packages/twenty-front/src/modules/ui/field/display/components/LinkDisplay.tsx +++ b/packages/twenty-front/src/modules/ui/field/display/components/LinkDisplay.tsx @@ -24,7 +24,7 @@ export const LinkDisplay = ({ value }: LinkDisplayProps) => { const type = checkUrlType(absoluteUrl); if (isSocialLinkType(type)) { - return ; + return ; } return ; diff --git a/packages/twenty-front/src/modules/ui/field/display/components/LinksDisplay.tsx b/packages/twenty-front/src/modules/ui/field/display/components/LinksDisplay.tsx index 33a7d47d8e..e94658526c 100644 --- a/packages/twenty-front/src/modules/ui/field/display/components/LinksDisplay.tsx +++ b/packages/twenty-front/src/modules/ui/field/display/components/LinksDisplay.tsx @@ -35,7 +35,8 @@ export const LinksDisplay = ({ value, onLinkClick }: LinksDisplayProps) => { } return { url: absoluteUrl, - label: label || hostname, + label, + displayLabel: label || hostname, type: checkUrlType(absoluteUrl), }; }); @@ -43,7 +44,7 @@ export const LinksDisplay = ({ value, onLinkClick }: LinksDisplayProps) => { return ( - {links.map(({ url, label, type }, index) => + {links.map(({ url, label, displayLabel, type }, index) => isSocialLinkType(type) ? ( { onLinkClick?.(url, event)} /> ), diff --git a/packages/twenty-front/src/modules/ui/field/display/components/URLDisplay.tsx b/packages/twenty-front/src/modules/ui/field/display/components/URLDisplay.tsx index b33f56ea8a..52b67c0175 100644 --- a/packages/twenty-front/src/modules/ui/field/display/components/URLDisplay.tsx +++ b/packages/twenty-front/src/modules/ui/field/display/components/URLDisplay.tsx @@ -28,7 +28,7 @@ export const URLDisplay = ({ value }: URLDisplayProps) => { href={absoluteUrl} onClick={handleClick} type={type} - label={displayedValue} + label={null} /> ); diff --git a/packages/twenty-front/src/modules/ui/field/display/components/__stories__/LinksDisplay.stories.tsx b/packages/twenty-front/src/modules/ui/field/display/components/__stories__/LinksDisplay.stories.tsx index 0009f1c150..62ced2e32d 100644 --- a/packages/twenty-front/src/modules/ui/field/display/components/__stories__/LinksDisplay.stories.tsx +++ b/packages/twenty-front/src/modules/ui/field/display/components/__stories__/LinksDisplay.stories.tsx @@ -125,12 +125,12 @@ export const SocialMediaLinks: Story = { args: { value: { primaryLinkUrl: 'https://www.linkedin.com/company/twenty', - primaryLinkLabel: 'Twenty on LinkedIn', + primaryLinkLabel: '', secondaryLinks: [ - { url: 'https://twitter.com/twentycrm', label: 'Twenty on Twitter' }, + { url: 'https://twitter.com/twentycrm', label: null }, { url: 'https://www.instagram.com/twenty_hq', - label: 'Twenty on Instagram', + label: null, }, ], }, @@ -170,10 +170,10 @@ export const InstagramLinks: Story = { args: { value: { primaryLinkUrl: 'https://www.instagram.com/twenty_hq', - primaryLinkLabel: 'Twenty on Instagram', + primaryLinkLabel: '', secondaryLinks: [ - { url: 'https://instagram.com/p/ABC123', label: 'A post' }, - { url: 'https://instagram.com/reel/XYZ789', label: 'A reel' }, + { url: 'https://instagram.com/p/ABC123', label: null }, + { url: 'https://instagram.com/reel/XYZ789', label: null }, ], }, }, @@ -197,6 +197,43 @@ export const InstagramLinks: Story = { }, }; +export const SocialLinksWithCustomLabels: Story = { + args: { + value: { + primaryLinkUrl: 'https://www.linkedin.com/company/twenty', + primaryLinkLabel: 'Twenty on LinkedIn', + secondaryLinks: [ + { + url: 'https://www.instagram.com/twenty_hq', + label: 'Twenty on Instagram', + }, + ], + }, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + await waitFor(() => { + const links = canvas.queryAllByRole('link'); + expect(links).toHaveLength(2); + }); + + const linkedinLink = await canvas.findByText('Twenty on LinkedIn'); + expect(linkedinLink).toBeVisible(); + expect(linkedinLink).toHaveAttribute( + 'href', + 'https://www.linkedin.com/company/twenty', + ); + + const instagramLink = await canvas.findByText('Twenty on Instagram'); + expect(instagramLink).toBeVisible(); + expect(instagramLink).toHaveAttribute( + 'href', + 'https://www.instagram.com/twenty_hq', + ); + }, +}; + export const AutomaticLabelFromURL: Story = { args: { value: { diff --git a/packages/twenty-ui/src/navigation/SocialLink/SocialLink.tsx b/packages/twenty-ui/src/navigation/SocialLink/SocialLink.tsx index 7ed28907c4..d1135c4c72 100644 --- a/packages/twenty-ui/src/navigation/SocialLink/SocialLink.tsx +++ b/packages/twenty-ui/src/navigation/SocialLink/SocialLink.tsx @@ -1,19 +1,22 @@ import * as React from 'react'; +import { isNonEmptyString } from '@sniptt/guards'; + import { type LinkType } from '@ui/navigation/SocialLink/LinkType'; import { RoundedLink } from '@ui/navigation/RoundedLink/RoundedLink'; import { getDisplayValueByUrlType } from '@ui/utilities'; type SocialLinkProps = { - label: string; + label?: string | null; href: string; type: LinkType; onClick?: (event: React.MouseEvent) => void; }; export const SocialLink = ({ label, href, onClick, type }: SocialLinkProps) => { - const displayValue = - getDisplayValueByUrlType({ type: type, href: href }) ?? label; + const displayValue = isNonEmptyString(label) + ? label + : (getDisplayValueByUrlType({ type: type, href: href }) ?? href); return ; }; diff --git a/packages/twenty-ui/src/navigation/SocialLink/__stories__/SocialLink.stories.tsx b/packages/twenty-ui/src/navigation/SocialLink/__stories__/SocialLink.stories.tsx index 9d9c256a31..dce54772e5 100644 --- a/packages/twenty-ui/src/navigation/SocialLink/__stories__/SocialLink.stories.tsx +++ b/packages/twenty-ui/src/navigation/SocialLink/__stories__/SocialLink.stories.tsx @@ -11,7 +11,6 @@ const meta: Meta = { decorators: [ComponentWithRouterDecorator], args: { href: 'https://twenty.com', - label: 'Social Link', }, }; @@ -22,7 +21,6 @@ const clickJestFn = fn(); export const LinkedIn: Story = { args: { href: 'https://www.linkedin.com/in/johndoe', - label: 'LinkedIn', onClick: clickJestFn, type: LinkType.LinkedIn, }, @@ -31,7 +29,6 @@ export const LinkedIn: Story = { export const Twitter: Story = { args: { href: 'https://twitter.com/johndoe', - label: 'Twitter', onClick: clickJestFn, type: LinkType.Twitter, }, @@ -49,7 +46,6 @@ export const Twitter: Story = { export const X: Story = { args: { href: 'https://x.com/johndoe', - label: 'X', onClick: clickJestFn, type: LinkType.Twitter, }, @@ -58,7 +54,6 @@ export const X: Story = { export const Facebook: Story = { args: { href: 'https://www.facebook.com/johndoe', - label: 'Facebook', onClick: clickJestFn, type: LinkType.Facebook, }, @@ -67,7 +62,6 @@ export const Facebook: Story = { export const Instagram: Story = { args: { href: 'https://www.instagram.com/johndoe', - label: 'Instagram', onClick: clickJestFn, type: LinkType.Instagram, }, @@ -76,7 +70,6 @@ export const Instagram: Story = { export const TikTok: Story = { args: { href: 'https://www.tiktok.com/@johndoe', - label: 'TikTok', onClick: clickJestFn, type: LinkType.TikTok, }, @@ -85,8 +78,23 @@ export const TikTok: Story = { export const Bluesky: Story = { args: { href: 'https://bsky.app/profile/johndoe.bsky.social', - label: 'Bluesky', onClick: clickJestFn, type: LinkType.Bluesky, }, }; + +export const WithCustomLabel: Story = { + args: { + href: 'https://www.instagram.com/cristiano', + label: 'Cristiano Ronaldo Official', + onClick: clickJestFn, + type: LinkType.Instagram, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + await expect( + canvas.getByText('Cristiano Ronaldo Official'), + ).toBeInTheDocument(); + }, +};