diff --git a/src/admin/components/forms/field-types/RichText/elements/link/Button/index.tsx b/src/admin/components/forms/field-types/RichText/elements/link/Button/index.tsx index 6b7e18e6f..02b8d48a7 100644 --- a/src/admin/components/forms/field-types/RichText/elements/link/Button/index.tsx +++ b/src/admin/components/forms/field-types/RichText/elements/link/Button/index.tsx @@ -8,10 +8,8 @@ import LinkIcon from '../../../../../../icons/Link'; import reduceFieldsToValues from '../../../../../Form/reduceFieldsToValues'; import { useConfig } from '../../../../../../utilities/Config'; import isElementActive from '../../isActive'; -import { unwrapLink } from '../utilities'; -import { getBaseFields } from '../LinkDrawer/baseFields'; +import { transformExtraFields, unwrapLink } from '../utilities'; import { LinkDrawer } from '../LinkDrawer'; -import { Field } from '../../../../../../../../fields/config/types'; import { Props as RichTextFieldProps } from '../../../types'; import buildStateFromSchema from '../../../../../Form/buildStateFromSchema'; import { useAuth } from '../../../../../../utilities/Auth'; @@ -19,6 +17,9 @@ import { Fields } from '../../../../../Form/types'; import { useLocale } from '../../../../../../utilities/Locale'; import { useDrawerSlug } from '../../../../../../elements/Drawer/useDrawerSlug'; +/** + * This function is called when an new link is created - not when an existing link is edited. + */ const insertLink = (editor, fields) => { const isCollapsed = editor.selection && Range.isCollapsed(editor.selection); const data = reduceFieldsToValues(fields, true); @@ -29,7 +30,7 @@ const insertLink = (editor, fields) => { url: data.url, doc: data.doc, newTab: data.newTab, - fields: data.fields, + fields: data.fields, // Any custom user-added fields are part of data.fields children: [], }; @@ -68,25 +69,7 @@ export const LinkButton: React.FC<{ const config = useConfig(); const [fieldSchema] = useState(() => { - const baseFields: Field[] = getBaseFields(config); - - const fields = typeof customFieldSchema === 'function' ? customFieldSchema({ defaultFields: baseFields, config, i18n }) : baseFields; - - if (Array.isArray(customFieldSchema)) { - fields.push({ - name: 'fields', - type: 'group', - admin: { - style: { - margin: 0, - padding: 0, - borderTop: 0, - borderBottom: 0, - }, - }, - fields: customFieldSchema, - }); - } + const fields = transformExtraFields(customFieldSchema, config, i18n); return fields; }); diff --git a/src/admin/components/forms/field-types/RichText/elements/link/Element/index.tsx b/src/admin/components/forms/field-types/RichText/elements/link/Element/index.tsx index 759b72914..70a49a80c 100644 --- a/src/admin/components/forms/field-types/RichText/elements/link/Element/index.tsx +++ b/src/admin/components/forms/field-types/RichText/elements/link/Element/index.tsx @@ -3,7 +3,7 @@ import { ReactEditor, useSlate } from 'slate-react'; import { Transforms, Node, Editor } from 'slate'; import { useModal } from '@faceless-ui/modal'; import { Trans, useTranslation } from 'react-i18next'; -import { unwrapLink } from '../utilities'; +import { transformExtraFields, unwrapLink } from '../utilities'; import Popup from '../../../../../../elements/Popup'; import { LinkDrawer } from '../LinkDrawer'; import { Fields } from '../../../../../Form/types'; @@ -11,8 +11,6 @@ import buildStateFromSchema from '../../../../../Form/buildStateFromSchema'; import { useAuth } from '../../../../../../utilities/Auth'; import { useLocale } from '../../../../../../utilities/Locale'; import { useConfig } from '../../../../../../utilities/Config'; -import { getBaseFields } from '../LinkDrawer/baseFields'; -import { Field } from '../../../../../../../../fields/config/types'; import reduceFieldsToValues from '../../../../../Form/reduceFieldsToValues'; import deepCopyObject from '../../../../../../../../utilities/deepCopyObject'; import Button from '../../../../../../elements/Button'; @@ -23,6 +21,10 @@ import { useDrawerSlug } from '../../../../../../elements/Drawer/useDrawerSlug'; const baseClass = 'rich-text-link'; +/** + * This function is called when an existing link is edited. + * When a link is first created, another function is called: {@link ../Button/index.tsx#insertLink} + */ const insertChange = (editor, fields, customFieldSchema) => { const data = reduceFieldsToValues(fields, true); @@ -79,25 +81,8 @@ export const LinkElement: React.FC<{ const [renderPopup, setRenderPopup] = useState(false); const [initialState, setInitialState] = useState({}); const [fieldSchema] = useState(() => { - const baseFields: Field[] = getBaseFields(config); + const fields = transformExtraFields(customFieldSchema, config, i18n); - const fields = typeof customFieldSchema === 'function' ? customFieldSchema({ defaultFields: baseFields, config, i18n }) : baseFields; - - if (Array.isArray(customFieldSchema)) { - fields.push({ - name: 'fields', - type: 'group', - admin: { - style: { - margin: 0, - padding: 0, - borderTop: 0, - borderBottom: 0, - }, - }, - fields: customFieldSchema, - }); - } return fields; }); diff --git a/src/admin/components/forms/field-types/RichText/elements/link/utilities.tsx b/src/admin/components/forms/field-types/RichText/elements/link/utilities.tsx index 11fe10153..597b1991c 100644 --- a/src/admin/components/forms/field-types/RichText/elements/link/utilities.tsx +++ b/src/admin/components/forms/field-types/RichText/elements/link/utilities.tsx @@ -1,4 +1,8 @@ import { Editor, Transforms, Range, Element } from 'slate'; +import type { i18n } from 'i18next'; +import type { SanitizedConfig } from 'payload/config'; +import { getBaseFields } from './LinkDrawer/baseFields'; +import { Field } from '../../../../../../../fields/config/types'; export const unwrapLink = (editor: Editor): void => { Transforms.unwrapNodes(editor, { match: (n) => Element.isElement(n) && n.type === 'link' }); @@ -37,3 +41,48 @@ export const withLinks = (incomingEditor: Editor): Editor => { return editor; }; + +/** + * This function is run to enrich the basefields which every link has with potential, custom user-added fields. + */ +export function transformExtraFields(customFieldSchema: Field[] | ((args: { + defaultFields: Field[]; + config: SanitizedConfig; + i18n: i18n; +}) => Field[]), config: SanitizedConfig, i18n: i18n): Field[] { + const baseFields: Field[] = getBaseFields(config); + + const fields = typeof customFieldSchema === 'function' ? customFieldSchema({ defaultFields: baseFields, config, i18n }) : baseFields; + + // Wrap fields which are not part of the base schema in a group named 'fields' - otherwise they will be rendered but not saved + const extraFields = []; + fields.forEach((field) => { + if ('name' in field) { + if (!baseFields.find((baseField) => !('name' in baseField) || baseField.name === field.name)) { + if (field.name !== 'fields' && field.type !== 'group') { + extraFields.push(field); + // Remove from fields from now, as they need to be part of the fields group below + fields.splice(fields.indexOf(field), 1); + } + } + } + }); + + + if (Array.isArray(customFieldSchema) || fields.length > 0) { + fields.push({ + name: 'fields', + type: 'group', + admin: { + style: { + margin: 0, + padding: 0, + borderTop: 0, + borderBottom: 0, + }, + }, + fields: Array.isArray(customFieldSchema) ? customFieldSchema.concat(extraFields) : extraFields, + }); + } + return fields; +} diff --git a/test/fields/collections/RichText/index.ts b/test/fields/collections/RichText/index.ts index 46b6576ad..c17842638 100644 --- a/test/fields/collections/RichText/index.ts +++ b/test/fields/collections/RichText/index.ts @@ -114,21 +114,16 @@ const RichTextFields: CollectionConfig = { 'upload', ], link: { - fields: () => [ - { - required: false, - name: 'rel', - label: 'Rel Attribute', - type: 'select', - hasMany: true, - options: [ - 'noopener', 'noreferrer', 'nofollow', - ], - admin: { - description: 'The rel attribute defines the relationship between a linked resource and the current document. This is a custom link field.', + fields: ({ defaultFields }) => { + return [ + ...defaultFields, + { + label: 'Custom', + name: 'customLinkField', + type: 'text', }, - }, - ], + ]; + }, }, upload: { collections: { diff --git a/test/fields/e2e.spec.ts b/test/fields/e2e.spec.ts index b2509fb83..9cad1805b 100644 --- a/test/fields/e2e.spec.ts +++ b/test/fields/e2e.spec.ts @@ -563,16 +563,41 @@ describe('fields', () => { }); test('should respect customizing the default fields', async () => { + const linkText = 'link'; + const value = 'test value'; await navigateToRichTextFields(); const field = page.locator('.rich-text', { has: page.locator('#field-richTextCustomFields') }); + // open link drawer const button = await field.locator('button.rich-text__button.link'); - await button.click(); + // fill link fields const linkDrawer = await page.locator('[id^=drawer_1_rich-text-link-]'); - await expect(linkDrawer).toBeVisible(); - const fieldCount = await linkDrawer.locator('.render-fields > .field-type').count(); - await expect(fieldCount).toEqual(1); + const fields = await linkDrawer.locator('.render-fields > .field-type'); + await fields.locator('#field-text').fill(linkText); + await fields.locator('#field-url').fill('https://payloadcms.com'); + const input = await fields.locator('#field-fields__customLinkField'); + await input.fill(value); + + // submit link closing drawer + await linkDrawer.locator('button[type="submit"]').click(); + const linkInEditor = field.locator(`.rich-text-link >> text="${linkText}"`); + await saveDocAndAssert(page); + + // open modal again + await linkInEditor.click(); + + const popup = page.locator('.popup--active .rich-text-link__popup'); + await expect(popup).toBeVisible(); + + await popup.locator('.rich-text-link__link-edit').click(); + + const linkDrawer2 = await page.locator('[id^=drawer_1_rich-text-link-]'); + const fields2 = await linkDrawer2.locator('.render-fields > .field-type'); + const input2 = await fields2.locator('#field-fields__customLinkField'); + + + await expect(input2).toHaveValue(value); }); });