From 23907e432e8040acda2a54d2f1784d77178aa40c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Jablo=C3=B1ski?= <43938777+GermanJablo@users.noreply.github.com> Date: Tue, 12 Nov 2024 02:07:50 -0300 Subject: [PATCH] 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 --- .../floatingLinkEditor/LinkEditor/index.tsx | 85 ++++++++++++++----- .../src/features/link/server/i18n.ts | 32 +++++++ test/fields/collections/RichText/e2e.spec.ts | 13 ++- 3 files changed, 108 insertions(+), 22 deletions(-) diff --git a/packages/richtext-lexical/src/features/link/client/plugins/floatingLinkEditor/LinkEditor/index.tsx b/packages/richtext-lexical/src/features/link/client/plugins/floatingLinkEditor/LinkEditor/index.tsx index b5653035b..bdc138549 100644 --- a/packages/richtext-lexical/src/features/link/client/plugins/floatingLinkEditor/LinkEditor/index.tsx +++ b/packages/richtext-lexical/src/features/link/client/plugins/floatingLinkEditor/LinkEditor/index.tsx @@ -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() 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([]) + 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( diff --git a/packages/richtext-lexical/src/features/link/server/i18n.ts b/packages/richtext-lexical/src/features/link/server/i18n.ts index 3728440e0..8b88702b1 100644 --- a/packages/richtext-lexical/src/features/link/server/i18n.ts +++ b/packages/richtext-lexical/src/features/link/server/i18n.ts @@ -3,98 +3,130 @@ import type { GenericLanguages } from '@payloadcms/translations' export const i18n: Partial = { 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: '載入中...', }, } diff --git a/test/fields/collections/RichText/e2e.spec.ts b/test/fields/collections/RichText/e2e.spec.ts index edfe1411c..4bfc8ac3d 100644 --- a/test/fields/collections/RichText/e2e.spec.ts +++ b/test/fields/collections/RichText/e2e.spec.ts @@ -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