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:  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:
@@ -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 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(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),
|
||||
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(
|
||||
|
||||
@@ -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: '載入中...',
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user