Skip to content

Commit ab4102c

Browse files
authored
fix(richtext-lexical): internal links render as href="#" in versions view (#15308)
### What Internal links in Lexical rich text fields render as `href="#"` instead of proper admin URLs when viewing document versions. ### Why `LexicalDiffComponent` was passing an empty object to `LinkDiffHTMLConverterAsync`, so the converter had no way to resolve internal document references to clickable admin URLs. ### How Added `internalDocToHref` function to `LexicalDiffComponent` that handles already-populated doc objects, uses the populate function to fetch doc data when needed, and formats admin URLs using `formatAdminURL` utility. Fixes #15250
1 parent f0458fb commit ab4102c

4 files changed

Lines changed: 114 additions & 4 deletions

File tree

packages/richtext-lexical/src/field/Diff/index.tsx

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,14 @@ import { FieldDiffContainer, getHTMLDiffComponents } from '@payloadcms/ui/rsc'
66
import './index.scss'
77
import '../bundled.css'
88

9+
import { formatAdminURL } from 'payload/shared'
910
import React from 'react'
1011

11-
import type { HTMLConvertersFunctionAsync } from '../../features/converters/lexicalToHtml/async/types.js'
12+
import type {
13+
HTMLConvertersFunctionAsync,
14+
HTMLPopulateFn,
15+
} from '../../features/converters/lexicalToHtml/async/types.js'
16+
import type { SerializedLinkNode } from '../../nodeTypes.js'
1217

1318
import { convertLexicalToHTMLAsync } from '../../features/converters/lexicalToHtml/async/index.js'
1419
import { getPayloadPopulateFn } from '../../features/converters/utilities/payloadPopulateFn.js'
@@ -31,9 +36,48 @@ export const LexicalDiffComponent: RichTextFieldDiffServerComponent = async (arg
3136
versionValue: valueTo,
3237
} = args
3338

39+
const internalDocToHref = async ({
40+
linkNode,
41+
populate,
42+
}: {
43+
linkNode: SerializedLinkNode
44+
populate?: HTMLPopulateFn
45+
}) => {
46+
if (!linkNode.fields.doc) {
47+
return '#'
48+
}
49+
50+
const { relationTo, value } = linkNode.fields.doc
51+
52+
let docId: number | string
53+
54+
if (typeof value === 'object' && value !== null) {
55+
docId = value.id
56+
} else if (populate && typeof value !== 'object') {
57+
const doc = await populate({
58+
id: value,
59+
collectionSlug: relationTo,
60+
})
61+
62+
if (!doc || !doc.id) {
63+
return '#'
64+
}
65+
66+
docId = doc.id
67+
} else {
68+
docId = value
69+
}
70+
71+
return formatAdminURL({
72+
adminRoute: req.payload.config.routes.admin,
73+
path: `/collections/${relationTo}/${docId}`,
74+
serverURL: req.payload.config.serverURL,
75+
})
76+
}
77+
3478
const converters: HTMLConvertersFunctionAsync = ({ defaultConverters }) => ({
3579
...defaultConverters,
36-
...LinkDiffHTMLConverterAsync({}),
80+
...LinkDiffHTMLConverterAsync({ internalDocToHref }),
3781
...ListItemDiffHTMLConverterAsync,
3882
...UploadDiffHTMLConverterAsync({ i18n, req }),
3983
...RelationshipDiffHTMLConverterAsync({ i18n, req }),

test/versions/collections/Diff/generateLexicalData.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,51 @@ export function generateLexicalData(args: {
235235
},
236236
id: '67d869aa706b36f346ecffd0',
237237
},
238+
{
239+
detail: 0,
240+
format: 0,
241+
mode: 'normal',
242+
style: '',
243+
text: ' with ',
244+
type: 'text',
245+
version: 1,
246+
},
247+
{
248+
children: [
249+
{
250+
detail: 0,
251+
format: 0,
252+
mode: 'normal',
253+
style: '',
254+
text: args.updated ? 'an updated internal link' : 'an internal link',
255+
type: 'text',
256+
version: 1,
257+
},
258+
],
259+
direction: 'ltr',
260+
format: '',
261+
indent: 0,
262+
type: 'link',
263+
version: 3,
264+
fields: {
265+
doc: {
266+
relationTo: textCollectionSlug,
267+
value: args.textID,
268+
},
269+
newTab: false,
270+
linkType: 'internal',
271+
},
272+
id: '67d869aa706b36f346ecffd1',
273+
},
274+
{
275+
detail: 0,
276+
format: 0,
277+
mode: 'normal',
278+
style: '',
279+
text: '.',
280+
type: 'text',
281+
version: 1,
282+
},
238283
],
239284
direction: 'ltr',
240285
format: '',

test/versions/e2e.spec.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2174,6 +2174,27 @@ describe('Versions', () => {
21742174
await expect(richtextWithCustomDiff.locator('p')).toHaveText('Test')
21752175
})
21762176

2177+
test('correctly renders internal links in richtext fields', async () => {
2178+
await navigateToDiffVersionView()
2179+
2180+
const richtext = page.locator('[data-field-path="richtext"]')
2181+
2182+
const oldDiff = richtext.locator('.html-diff__diff-old')
2183+
const newDiff = richtext.locator('.html-diff__diff-new')
2184+
2185+
const oldInternalLink = oldDiff.locator('a:has-text("an internal link")')
2186+
const newInternalLink = newDiff.locator('a:has-text("an updated internal link")')
2187+
2188+
await expect(oldInternalLink).toHaveCount(1)
2189+
await expect(newInternalLink).toHaveCount(1)
2190+
2191+
await expect(oldInternalLink).not.toHaveAttribute('href', '#')
2192+
await expect(newInternalLink).not.toHaveAttribute('href', '#')
2193+
2194+
await expect(oldInternalLink).toHaveAttribute('href', /\/admin\/collections\/text\/\d+/)
2195+
await expect(newInternalLink).toHaveAttribute('href', /\/admin\/collections\/text\/\d+/)
2196+
})
2197+
21772198
test('correctly renders diff for row fields', async () => {
21782199
await navigateToDiffVersionView()
21792200

test/versions/payload-types.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1543,6 +1543,6 @@ export interface Auth {
15431543

15441544

15451545
declare module 'payload' {
1546-
// @ts-ignore
1546+
// @ts-ignore
15471547
export interface GeneratedTypes extends Config {}
1548-
}
1548+
}

0 commit comments

Comments
 (0)