fix: RichText link custom fields (#2756)
Co-authored-by: Dan Ribbens <dan.ribbens@gmail.com>
This commit is contained in:
@@ -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;
|
||||
});
|
||||
|
||||
@@ -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<Fields>({});
|
||||
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;
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user