feat: allow customizing the link fields (#2559)

This commit is contained in:
Michel v. Varendorff
2023-04-26 17:08:47 +02:00
committed by GitHub
parent ddb34c3d83
commit bf6522898d
7 changed files with 91 additions and 22 deletions

View File

@@ -83,6 +83,8 @@ Set this property to `true` to hide this field's gutter within the admin panel.
This allows [fields](/docs/fields/overview) to be saved as extra fields on a link inside the Rich Text Editor. When this is present, the fields will render inside a modal that can be opened by clicking the "edit" button on the link element. This allows [fields](/docs/fields/overview) to be saved as extra fields on a link inside the Rich Text Editor. When this is present, the fields will render inside a modal that can be opened by clicking the "edit" button on the link element.
`link.fields` may either be an array of fields (in which case all fields defined in it will be appended below the default fields) or a function that accepts the default fields as only argument and returns an array defining the entirety of fields to be used (thus providing a mechanism of overriding the default fields).
![RichText link fields](https://payloadcms.com/images/docs/fields/richText/rte-link-fields-modal.jpg) ![RichText link fields](https://payloadcms.com/images/docs/fields/richText/rte-link-fields-modal.jpg)
*RichText link with custom fields* *RichText link with custom fields*

View File

@@ -63,16 +63,16 @@ export const LinkButton: React.FC<{
const locale = useLocale(); const locale = useLocale();
const [initialState, setInitialState] = useState<Fields>({}); const [initialState, setInitialState] = useState<Fields>({});
const { t } = useTranslation(['upload', 'general']); const { t, i18n } = useTranslation(['upload', 'general']);
const editor = useSlate(); const editor = useSlate();
const config = useConfig(); const config = useConfig();
const [fieldSchema] = useState(() => { const [fieldSchema] = useState(() => {
const fields: Field[] = [ const baseFields: Field[] = getBaseFields(config);
...getBaseFields(config),
];
if (customFieldSchema) { const fields = typeof customFieldSchema === 'function' ? customFieldSchema({ defaultFields: baseFields, config, i18n }) : baseFields;
if (Array.isArray(customFieldSchema)) {
fields.push({ fields.push({
name: 'fields', name: 'fields',
type: 'group', type: 'group',

View File

@@ -79,11 +79,11 @@ export const LinkElement: React.FC<{
const [renderPopup, setRenderPopup] = useState(false); const [renderPopup, setRenderPopup] = useState(false);
const [initialState, setInitialState] = useState<Fields>({}); const [initialState, setInitialState] = useState<Fields>({});
const [fieldSchema] = useState(() => { const [fieldSchema] = useState(() => {
const fields: Field[] = [ const baseFields: Field[] = getBaseFields(config);
...getBaseFields(config),
];
if (customFieldSchema) { const fields = typeof customFieldSchema === 'function' ? customFieldSchema({ defaultFields: baseFields, config, i18n }) : baseFields;
if (Array.isArray(customFieldSchema)) {
fields.push({ fields.push({
name: 'fields', name: 'fields',
type: 'group', type: 'group',

View File

@@ -413,7 +413,10 @@ export const richText = baseField.keys({
})), })),
}), }),
link: joi.object({ link: joi.object({
fields: joi.array().items(joi.link('#field')), fields: joi.alternatives(
joi.array().items(joi.link('#field')),
joi.func(),
),
}), }),
}), }),
}); });

View File

@@ -1,9 +1,10 @@
/* eslint-disable no-use-before-define */ /* eslint-disable no-use-before-define */
import { CSSProperties } from 'react'; import { CSSProperties } from 'react';
import { Editor } from 'slate'; import { Editor } from 'slate';
import type { TFunction } from 'i18next'; import type { TFunction, i18n as Ii18n } from 'i18next';
import type { EditorProps } from '@monaco-editor/react'; import type { EditorProps } from '@monaco-editor/react';
import { Operation, Where } from '../../types'; import { Operation, Where } from '../../types';
import { SanitizedConfig } from "../../config/types";
import { TypeWithID } from '../../collections/config/types'; import { TypeWithID } from '../../collections/config/types';
import { PayloadRequest } from '../../express/types'; import { PayloadRequest } from '../../express/types';
import { ConditionalDateProps } from '../../admin/components/elements/DatePicker/types'; import { ConditionalDateProps } from '../../admin/components/elements/DatePicker/types';
@@ -353,7 +354,7 @@ export type RichTextField = FieldBase & {
} }
} }
link?: { link?: {
fields?: Field[]; fields?: Field[] | ((args: {defaultFields: Field[], config: SanitizedConfig, i18n: Ii18n}) => Field[]);
} }
} }
} }

View File

@@ -95,6 +95,55 @@ const RichTextFields: CollectionConfig = {
}, },
}, },
}, },
{
name: 'richTextCustomFields',
type: 'richText',
admin: {
elements: [
'h1',
'h2',
'h3',
'h4',
'h5',
'h6',
'ul',
'ol',
'indent',
'link',
'relationship',
'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.',
},
},
],
},
upload: {
collections: {
uploads: {
fields: [
{
name: 'caption',
type: 'richText',
},
],
},
},
},
},
},
{ {
name: 'richTextReadOnly', name: 'richTextReadOnly',
type: 'richText', type: 'richText',
@@ -401,6 +450,7 @@ export const richTextDoc = {
selectHasMany: ['one', 'five'], selectHasMany: ['one', 'five'],
richText: generateRichText(), richText: generateRichText(),
richTextReadOnly: generateRichText(), richTextReadOnly: generateRichText(),
richTextCustomFields: generateRichText(),
}; };
export default RichTextFields; export default RichTextFields;

View File

@@ -446,7 +446,7 @@ describe('fields', () => {
await navigateToRichTextFields(); await navigateToRichTextFields();
// Open link drawer // Open link drawer
await page.locator('.rich-text__toolbar button:not([disabled]) .link').click(); await page.locator('.rich-text__toolbar button:not([disabled]) .link').first().click();
// find the drawer // find the drawer
const editLinkModal = await page.locator('[id^=drawer_1_rich-text-link-]'); const editLinkModal = await page.locator('[id^=drawer_1_rich-text-link-]');
@@ -479,7 +479,7 @@ describe('fields', () => {
await navigateToRichTextFields(); await navigateToRichTextFields();
// Open link drawer // Open link drawer
await page.locator('.rich-text__toolbar button:not([disabled]) .upload-rich-text-button').click(); await page.locator('.rich-text__toolbar button:not([disabled]) .upload-rich-text-button').first().click();
// open the list select menu // open the list select menu
await page.locator('.list-drawer__select-collection-wrap .rs__control').click(); await page.locator('.list-drawer__select-collection-wrap .rs__control').click();
@@ -493,7 +493,7 @@ describe('fields', () => {
await navigateToRichTextFields(); await navigateToRichTextFields();
// Open link drawer // Open link drawer
await page.locator('.rich-text__toolbar button:not([disabled]) .relationship-rich-text-button').click(); await page.locator('.rich-text__toolbar button:not([disabled]) .relationship-rich-text-button').first().click();
// open the list select menu // open the list select menu
await page.locator('.list-drawer__select-collection-wrap .rs__control').click(); await page.locator('.list-drawer__select-collection-wrap .rs__control').click();
@@ -501,11 +501,24 @@ describe('fields', () => {
const menu = page.locator('.list-drawer__select-collection-wrap .rs__menu'); const menu = page.locator('.list-drawer__select-collection-wrap .rs__menu');
await expect(menu).not.toContainText('Uploads'); await expect(menu).not.toContainText('Uploads');
}); });
test('should respect customizing the default fields', async () => {
await navigateToRichTextFields();
const field = page.locator('.rich-text', { has: page.locator('#field-richTextCustomFields') });
const button = await field.locator('button.rich-text__button.link');
await button.click();
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);
});
}); });
describe('editor', () => { describe('editor', () => {
test('should populate url link', async () => { test('should populate url link', async () => {
navigateToRichTextFields(); await navigateToRichTextFields();
// Open link popup // Open link popup
await page.locator('#field-richText span >> text="render links"').click(); await page.locator('#field-richText span >> text="render links"').click();
@@ -528,7 +541,7 @@ describe('fields', () => {
}); });
test('should populate relationship link', async () => { test('should populate relationship link', async () => {
navigateToRichTextFields(); await navigateToRichTextFields();
// Open link popup // Open link popup
await page.locator('#field-richText span >> text="link to relationships"').click(); await page.locator('#field-richText span >> text="link to relationships"').click();
@@ -551,7 +564,7 @@ describe('fields', () => {
}); });
test('should open upload drawer and render custom relationship fields', async () => { test('should open upload drawer and render custom relationship fields', async () => {
navigateToRichTextFields(); await navigateToRichTextFields();
const field = await page.locator('#field-richText'); const field = await page.locator('#field-richText');
const button = await field.locator('button.rich-text-upload__upload-drawer-toggler'); const button = await field.locator('button.rich-text-upload__upload-drawer-toggler');
@@ -564,7 +577,7 @@ describe('fields', () => {
}); });
test('should open upload document drawer from read-only field', async () => { test('should open upload document drawer from read-only field', async () => {
navigateToRichTextFields(); await navigateToRichTextFields();
const field = await page.locator('#field-richTextReadOnly'); const field = await page.locator('#field-richTextReadOnly');
const button = await field.locator('button.rich-text-upload__doc-drawer-toggler.doc-drawer__toggler'); const button = await field.locator('button.rich-text-upload__doc-drawer-toggler.doc-drawer__toggler');
@@ -575,7 +588,7 @@ describe('fields', () => {
}); });
test('should open relationship document drawer from read-only field', async () => { test('should open relationship document drawer from read-only field', async () => {
navigateToRichTextFields(); await navigateToRichTextFields();
const field = await page.locator('#field-richTextReadOnly'); const field = await page.locator('#field-richTextReadOnly');
const button = await field.locator('button.rich-text-relationship__doc-drawer-toggler.doc-drawer__toggler'); const button = await field.locator('button.rich-text-relationship__doc-drawer-toggler.doc-drawer__toggler');
@@ -586,14 +599,14 @@ describe('fields', () => {
}); });
test('should populate new links', async () => { test('should populate new links', async () => {
navigateToRichTextFields(); await navigateToRichTextFields();
// Highlight existing text // Highlight existing text
const headingElement = await page.locator('#field-richText h1 >> text="Hello, I\'m a rich text field."'); const headingElement = await page.locator('#field-richText h1 >> text="Hello, I\'m a rich text field."');
await headingElement.selectText(); await headingElement.selectText();
// click the toolbar link button // click the toolbar link button
await page.locator('.rich-text__toolbar button:not([disabled]) .link').click(); await page.locator('.rich-text__toolbar button:not([disabled]) .link').first().click();
// find the drawer and confirm the values // find the drawer and confirm the values
const editLinkModal = await page.locator('[id^=drawer_1_rich-text-link-]'); const editLinkModal = await page.locator('[id^=drawer_1_rich-text-link-]');