feat(richtext-lexical): add useAsTitle to the popup links label (#8718)

Now we show not only the collection being linked to, but also the
document title:

![image](https://github.com/user-attachments/assets/5ba5713a-b051-4f11-ae2a-d5b50a25966b)

Previously this example was just displayed as: `Linked to Users`

- I've added a loading state in case the request is slow (verified with
fake slow connection).
- I have verified that if the `useAsTitle` is not defined, it correctly
fallbacks to the id

Please let me know if the same needs to be done with Slate.

---------

Co-authored-by: Alessio Gravili <alessio@gravili.de>
This commit is contained in:
Germán Jabloñski
2024-11-12 02:07:50 -03:00
committed by GitHub
parent a30eeaf644
commit 23907e432e
3 changed files with 108 additions and 22 deletions

View File

@@ -11,8 +11,10 @@ import {
formatDrawerSlug,
useConfig,
useEditDepth,
useLocale,
useTranslation,
} from '@payloadcms/ui'
import { requests } from '@payloadcms/ui/shared'
import {
$getSelection,
$isLineBreakNode,
@@ -50,7 +52,7 @@ export function LinkEditor({ anchorElem }: { anchorElem: HTMLElement }): React.R
const { config } = useConfig()
const { i18n, t } = useTranslation()
const { i18n, t } = useTranslation<object, 'lexical:link:loadingWithEllipsis'>()
const [stateData, setStateData] = useState<
({ id?: string; text: string } & LinkFields) | undefined
@@ -59,6 +61,7 @@ export function LinkEditor({ anchorElem }: { anchorElem: HTMLElement }): React.R
const editDepth = useEditDepth()
const [isLink, setIsLink] = useState(false)
const [selectedNodes, setSelectedNodes] = useState<LexicalNode[]>([])
const locale = useLocale()
const [isAutoLink, setIsAutoLink] = useState(false)
@@ -87,7 +90,7 @@ export function LinkEditor({ anchorElem }: { anchorElem: HTMLElement }): React.R
let selectedNodeDomRect: DOMRect | undefined
if (!$isRangeSelection(selection) || !selection) {
setNotLink()
void setNotLink()
return
}
@@ -114,41 +117,72 @@ export function LinkEditor({ anchorElem }: { anchorElem: HTMLElement }): React.R
return
}
const fields = focusLinkParent.getFields()
// Initial state:
const data: { text: string } & LinkFields = {
...focusLinkParent.getFields(),
...fields,
id: focusLinkParent.getID(),
text: focusLinkParent.getTextContent(),
}
if (focusLinkParent.getFields()?.linkType === 'custom') {
setLinkUrl(focusLinkParent.getFields()?.url ?? null)
if (fields?.linkType === 'custom') {
setLinkUrl(fields?.url ?? null)
setLinkLabel(null)
} else {
// internal link
setLinkUrl(
`${config.routes.admin === '/' ? '' : config.routes.admin}/collections/${focusLinkParent.getFields()?.doc?.relationTo}/${
focusLinkParent.getFields()?.doc?.value
`${config.routes.admin === '/' ? '' : config.routes.admin}/collections/${fields?.doc?.relationTo}/${
fields?.doc?.value
}`,
)
const relatedField = config.collections.find(
(coll) => coll.slug === focusLinkParent.getFields()?.doc?.relationTo,
)
const relatedField = config.collections.find((coll) => coll.slug === fields?.doc?.relationTo)
if (!relatedField) {
// Usually happens if the user removed all default fields. In this case, we let them specify the label or do not display the label at all.
// label could be a virtual field the user added. This is useful if they want to use the link feature for things other than links.
setLinkLabel(
focusLinkParent.getFields()?.label ? String(focusLinkParent.getFields()?.label) : null,
)
setLinkUrl(
focusLinkParent.getFields()?.url ? String(focusLinkParent.getFields()?.url) : null,
)
setLinkLabel(fields?.label ? String(fields?.label) : null)
setLinkUrl(fields?.url ? String(fields?.url) : null)
} else {
const label = t('fields:linkedTo', {
label: getTranslation(relatedField.labels.singular, i18n),
const id = typeof fields.doc?.value === 'object' ? fields.doc.value.id : fields.doc?.value
const collection = fields.doc?.relationTo
if (!id || !collection) {
throw new Error(`Focus link parent is missing doc.value or doc.relationTo`)
}
const loadingLabel = t('fields:linkedTo', {
label: `${getTranslation(relatedField.labels.singular, i18n)} - ${t('lexical:link:loadingWithEllipsis', i18n)}`,
}).replace(/<[^>]*>?/g, '')
setLinkLabel(label)
setLinkLabel(loadingLabel)
requests
.get(`${config.serverURL}${config.routes.api}/${collection}/${id}`, {
headers: {
'Accept-Language': i18n.language,
},
params: {
depth: 0,
locale: locale?.code,
},
})
.then(async (res) => {
if (!res.ok) {
throw new Error(`HTTP error! Status: ${res.status}`)
}
const data = await res.json()
const useAsTitle = relatedField?.admin?.useAsTitle || 'id'
const title = data[useAsTitle]
const label = t('fields:linkedTo', {
label: `${getTranslation(relatedField.labels.singular, i18n)} - ${title}`,
}).replace(/<[^>]*>?/g, '')
setLinkLabel(label)
})
.catch(() => {
const label = t('fields:linkedTo', {
label: `${getTranslation(relatedField.labels.singular, i18n)} - ${t('general:untitled', i18n)} - ID: ${id}`,
}).replace(/<[^>]*>?/g, '')
setLinkLabel(label)
})
}
}
@@ -196,7 +230,18 @@ export function LinkEditor({ anchorElem }: { anchorElem: HTMLElement }): React.R
}
return true
}, [editor, setNotLink, config.routes.admin, config.collections, t, i18n, anchorElem])
}, [
editor,
setNotLink,
config.routes.admin,
config.routes.api,
config.collections,
config.serverURL,
t,
i18n,
locale?.code,
anchorElem,
])
useEffect(() => {
return mergeRegister(

View File

@@ -3,98 +3,130 @@ import type { GenericLanguages } from '@payloadcms/translations'
export const i18n: Partial<GenericLanguages> = {
ar: {
label: 'رابط',
loadingWithEllipsis: 'جار التحميل...',
},
az: {
label: 'Keçid',
loadingWithEllipsis: 'Yüklənir...',
},
bg: {
label: 'Връзка',
loadingWithEllipsis: 'Зарежда се...',
},
cs: {
label: 'Odkaz',
loadingWithEllipsis: 'Načítání...',
},
de: {
label: 'Verknüpfung',
loadingWithEllipsis: 'Laden...',
},
en: {
label: 'Link',
loadingWithEllipsis: 'Loading...',
},
es: {
label: 'Enlace',
loadingWithEllipsis: 'Cargando...',
},
fa: {
label: 'پیوند',
loadingWithEllipsis: 'در حال بارگذاری...',
},
fr: {
label: 'Lien',
loadingWithEllipsis: 'Chargement...',
},
he: {
label: 'קישור',
loadingWithEllipsis: 'טוען...',
},
hr: {
label: 'Poveznica',
loadingWithEllipsis: 'Učitavanje...',
},
hu: {
label: 'Hivatkozás',
loadingWithEllipsis: 'Betöltés...',
},
it: {
label: 'Collegamento',
loadingWithEllipsis: 'Caricamento...',
},
ja: {
label: 'リンク',
loadingWithEllipsis: '読み込み中...',
},
ko: {
label: '링크',
loadingWithEllipsis: '로딩 중...',
},
my: {
label: 'လင့်',
loadingWithEllipsis: 'ဖွင့်နေသည်...',
},
nb: {
label: 'Lenke',
loadingWithEllipsis: 'Laster...',
},
nl: {
label: 'Link',
loadingWithEllipsis: 'Laden...',
},
pl: {
label: 'Łącze',
loadingWithEllipsis: 'Ładowanie...',
},
pt: {
label: 'Ligação',
loadingWithEllipsis: 'Carregando...',
},
ro: {
label: 'Legătură',
loadingWithEllipsis: 'Se încarcă...',
},
rs: {
label: 'Veza',
loadingWithEllipsis: 'Учитавање...',
},
'rs-latin': {
label: 'Veza',
loadingWithEllipsis: 'Učitavanje...',
},
ru: {
label: 'Ссылка',
loadingWithEllipsis: 'Загрузка...',
},
sk: {
label: 'Odkaz',
loadingWithEllipsis: 'Načítava sa...',
},
sv: {
label: 'Länk',
loadingWithEllipsis: 'Laddar...',
},
th: {
label: 'ลิงค์',
loadingWithEllipsis: 'กำลังโหลด...',
},
tr: {
label: 'Bağlantı',
loadingWithEllipsis: 'Yükleniyor...',
},
uk: {
label: 'Посилання',
loadingWithEllipsis: 'Завантаження...',
},
vi: {
label: 'Liên kết',
loadingWithEllipsis: 'Đang tải...',
},
zh: {
label: '链接',
loadingWithEllipsis: '加载中...',
},
'zh-TW': {
label: '連結',
loadingWithEllipsis: '載入中...',
},
}

View File

@@ -238,12 +238,15 @@ describe('Rich Text', () => {
test('should only list RTE enabled collections in link drawer', async () => {
await navigateToRichTextFields()
await wait(1000)
await page.locator('.rich-text__toolbar button:not([disabled]) .link').first().click()
const editLinkModal = page.locator('[id^=drawer_1_rich-text-link-]')
await expect(editLinkModal).toBeVisible()
await wait(1000)
await editLinkModal.locator('label[for="field-linkType-internal-2"]').click()
await editLinkModal.locator('.relationship__wrap .rs__control').click()
@@ -255,6 +258,7 @@ describe('Rich Text', () => {
test('should only list non-upload collections in relationship drawer', async () => {
await navigateToRichTextFields()
await wait(1000)
// Open link drawer
await page
@@ -262,7 +266,7 @@ describe('Rich Text', () => {
.first()
.click()
await wait(300)
await wait(1000)
// open the list select menu
await page.locator('.list-drawer__select-collection-wrap .rs__control').click()
@@ -277,12 +281,15 @@ describe('Rich Text', () => {
const linkText = 'link'
const value = 'test value'
await navigateToRichTextFields()
await wait(1000)
const field = page.locator('.rich-text', {
has: page.locator('#field-richTextCustomFields'),
})
// open link drawer
const button = field.locator('button.rich-text__button.link')
await button.click()
await wait(1000)
// fill link fields
const linkDrawer = page.locator('[id^=drawer_1_rich-text-link-]')
@@ -292,11 +299,13 @@ describe('Rich Text', () => {
const input = fields.locator('#field-fields__customLinkField')
await input.fill(value)
await wait(300)
await wait(1000)
// submit link closing drawer
await linkDrawer.locator('button[type="submit"]').click()
const linkInEditor = field.locator(`.rich-text-link >> text="${linkText}"`)
await wait(300)
await saveDocAndAssert(page)
// open modal again