feat: allow customizing the link fields (#2559)
This commit is contained in:
committed by
GitHub
parent
ddb34c3d83
commit
bf6522898d
@@ -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 with custom fields*
|
*RichText link with custom fields*
|
||||||
|
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
),
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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[]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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-]');
|
||||||
|
|||||||
Reference in New Issue
Block a user