feat: allows rich text links to link to other docs
This commit is contained in:
@@ -234,6 +234,7 @@ const RichText: React.FC<Props> = (props) => {
|
||||
if (Button) {
|
||||
return (
|
||||
<Button
|
||||
fieldProps={props}
|
||||
key={i}
|
||||
path={path}
|
||||
/>
|
||||
@@ -253,6 +254,7 @@ const RichText: React.FC<Props> = (props) => {
|
||||
if (Button) {
|
||||
return (
|
||||
<Button
|
||||
fieldProps={props}
|
||||
key={i}
|
||||
path={path}
|
||||
/>
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
import React, { Fragment, useState } from 'react';
|
||||
import { ReactEditor, useSlate } from 'slate-react';
|
||||
import { Transforms, Editor, Range } from 'slate';
|
||||
import { useModal } from '@faceless-ui/modal';
|
||||
import ElementButton from '../Button';
|
||||
import { unwrapLink } from './utilities';
|
||||
import LinkIcon from '../../../../../icons/Link';
|
||||
import { EditModal } from './Modal';
|
||||
import { modalSlug } from './shared';
|
||||
import isElementActive from '../isActive';
|
||||
import { Fields } from '../../../../Form/types';
|
||||
import buildStateFromSchema from '../../../../Form/buildStateFromSchema';
|
||||
import { useAuth } from '../../../../../utilities/Auth';
|
||||
import { useLocale } from '../../../../../utilities/Locale';
|
||||
import { useConfig } from '../../../../../utilities/Config';
|
||||
import { getBaseFields } from './Modal/baseFields';
|
||||
import { Field } from '../../../../../../../fields/config/types';
|
||||
|
||||
export const LinkButton = ({ fieldProps }) => {
|
||||
const customFieldSchema = fieldProps?.admin?.link?.fields;
|
||||
|
||||
const config = useConfig();
|
||||
const editor = useSlate();
|
||||
const { user } = useAuth();
|
||||
const locale = useLocale();
|
||||
const { open, closeAll } = useModal();
|
||||
const [renderModal, setRenderModal] = useState(false);
|
||||
const [initialState, setInitialState] = useState<Fields>({});
|
||||
const [fieldSchema] = useState(() => {
|
||||
const fields: Field[] = [
|
||||
...getBaseFields(config),
|
||||
];
|
||||
|
||||
if (customFieldSchema) {
|
||||
fields.push({
|
||||
name: 'fields',
|
||||
type: 'group',
|
||||
admin: {
|
||||
style: {
|
||||
margin: 0,
|
||||
padding: 0,
|
||||
borderTop: 0,
|
||||
borderBottom: 0,
|
||||
},
|
||||
},
|
||||
fields: customFieldSchema,
|
||||
});
|
||||
}
|
||||
|
||||
return fields;
|
||||
});
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<ElementButton
|
||||
format="link"
|
||||
onClick={async () => {
|
||||
if (isElementActive(editor, 'link')) {
|
||||
unwrapLink(editor);
|
||||
} else {
|
||||
open(modalSlug);
|
||||
setRenderModal(true);
|
||||
|
||||
const isCollapsed = editor.selection && Range.isCollapsed(editor.selection);
|
||||
|
||||
if (!isCollapsed) {
|
||||
const data = {
|
||||
text: Editor.string(editor, editor.selection),
|
||||
};
|
||||
|
||||
const state = await buildStateFromSchema({ fieldSchema, data, user, operation: 'create', locale });
|
||||
setInitialState(state);
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
<LinkIcon />
|
||||
</ElementButton>
|
||||
{renderModal && (
|
||||
<EditModal
|
||||
fieldSchema={fieldSchema}
|
||||
initialState={initialState}
|
||||
close={() => {
|
||||
closeAll();
|
||||
setRenderModal(false);
|
||||
}}
|
||||
handleModalSubmit={(_, data) => {
|
||||
const isCollapsed = editor.selection && Range.isCollapsed(editor.selection);
|
||||
|
||||
const newLink = {
|
||||
type: 'link',
|
||||
linkType: data.linkType,
|
||||
url: data.url,
|
||||
doc: data.doc,
|
||||
newTab: data.newTab,
|
||||
fields: data.fields,
|
||||
children: [],
|
||||
};
|
||||
|
||||
if (isCollapsed) {
|
||||
// If selection anchor and focus are the same,
|
||||
// Just inject a new node with children already set
|
||||
Transforms.insertNodes(editor, {
|
||||
...newLink,
|
||||
children: [{ text: String(data.text) }],
|
||||
});
|
||||
} else {
|
||||
// Otherwise we need to wrap the selected node in a link,
|
||||
// Delete its old text,
|
||||
// Move the selection one position forward into the link,
|
||||
// And insert the text back into the new link
|
||||
Transforms.wrapNodes(editor, newLink, { split: true });
|
||||
Transforms.delete(editor, { at: editor.selection.focus.path, unit: 'word' });
|
||||
Transforms.move(editor, { distance: 1, unit: 'offset' });
|
||||
Transforms.insertText(editor, String(data.text), { at: editor.selection.focus.path });
|
||||
}
|
||||
|
||||
closeAll();
|
||||
setRenderModal(false);
|
||||
|
||||
ReactEditor.focus(editor);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,198 @@
|
||||
import React, { Fragment, useCallback, useEffect, useState } from 'react';
|
||||
import { ReactEditor, useSlate } from 'slate-react';
|
||||
import { Transforms, Node, Editor } from 'slate';
|
||||
import { useModal } from '@faceless-ui/modal';
|
||||
import { unwrapLink } from './utilities';
|
||||
import Popup from '../../../../../elements/Popup';
|
||||
import { EditModal } from './Modal';
|
||||
import { modalSlug } from './shared';
|
||||
import { Fields } from '../../../../Form/types';
|
||||
import buildStateFromSchema from '../../../../Form/buildStateFromSchema';
|
||||
import { useAuth } from '../../../../../utilities/Auth';
|
||||
import { useLocale } from '../../../../../utilities/Locale';
|
||||
import { useConfig } from '../../../../../utilities/Config';
|
||||
import { getBaseFields } from './Modal/baseFields';
|
||||
import { Field } from '../../../../../../../fields/config/types';
|
||||
import reduceFieldsToValues from '../../../../Form/reduceFieldsToValues';
|
||||
import deepCopyObject from '../../../../../../../utilities/deepCopyObject';
|
||||
|
||||
import './index.scss';
|
||||
|
||||
const baseClass = 'rich-text-link';
|
||||
|
||||
// TODO: Multiple modal windows stacked go boom (rip). Edit Upload in fields -> rich text
|
||||
|
||||
export const LinkElement = ({ attributes, children, element, editorRef, fieldProps }) => {
|
||||
const customFieldSchema = fieldProps?.admin?.link?.fields;
|
||||
|
||||
const editor = useSlate();
|
||||
const config = useConfig();
|
||||
const { user } = useAuth();
|
||||
const locale = useLocale();
|
||||
const { open, closeAll } = useModal();
|
||||
const [renderModal, setRenderModal] = useState(false);
|
||||
const [renderPopup, setRenderPopup] = useState(false);
|
||||
const [initialState, setInitialState] = useState<Fields>({});
|
||||
const [fieldSchema] = useState(() => {
|
||||
const fields: Field[] = [
|
||||
...getBaseFields(config),
|
||||
];
|
||||
|
||||
if (customFieldSchema) {
|
||||
fields.push({
|
||||
name: 'fields',
|
||||
type: 'group',
|
||||
admin: {
|
||||
style: {
|
||||
margin: 0,
|
||||
padding: 0,
|
||||
borderTop: 0,
|
||||
borderBottom: 0,
|
||||
},
|
||||
},
|
||||
fields: customFieldSchema,
|
||||
});
|
||||
}
|
||||
|
||||
return fields;
|
||||
});
|
||||
|
||||
const handleTogglePopup = useCallback((render) => {
|
||||
if (!render) {
|
||||
setRenderPopup(render);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const awaitInitialState = async () => {
|
||||
const data = {
|
||||
text: Node.string(element),
|
||||
linkType: element.linkType,
|
||||
url: element.url,
|
||||
doc: element.doc,
|
||||
newTab: element.newTab,
|
||||
fields: deepCopyObject(element.fields),
|
||||
};
|
||||
|
||||
const state = await buildStateFromSchema({ fieldSchema, data, user, operation: 'update', locale });
|
||||
setInitialState(state);
|
||||
};
|
||||
|
||||
awaitInitialState();
|
||||
}, [renderModal, element, fieldSchema, user, locale]);
|
||||
|
||||
return (
|
||||
<span
|
||||
className={baseClass}
|
||||
{...attributes}
|
||||
>
|
||||
<span
|
||||
style={{ userSelect: 'none' }}
|
||||
contentEditable={false}
|
||||
>
|
||||
{renderModal && (
|
||||
<EditModal
|
||||
fieldSchema={fieldSchema}
|
||||
close={() => {
|
||||
closeAll();
|
||||
setRenderModal(false);
|
||||
}}
|
||||
handleModalSubmit={(fields) => {
|
||||
closeAll();
|
||||
setRenderModal(false);
|
||||
|
||||
const data = reduceFieldsToValues(fields, true);
|
||||
|
||||
const [, parentPath] = Editor.above(editor);
|
||||
|
||||
const newNode: Record<string, unknown> = {
|
||||
newTab: data.newTab,
|
||||
url: data.url,
|
||||
linkType: data.linkType,
|
||||
doc: data.doc,
|
||||
};
|
||||
|
||||
if (customFieldSchema) {
|
||||
newNode.fields = data.fields;
|
||||
}
|
||||
|
||||
Transforms.setNodes(
|
||||
editor,
|
||||
newNode,
|
||||
{ at: parentPath },
|
||||
);
|
||||
|
||||
Transforms.delete(editor, { at: editor.selection.focus.path, unit: 'block' });
|
||||
Transforms.move(editor, { distance: 1, unit: 'offset' });
|
||||
Transforms.insertText(editor, String(data.text), { at: editor.selection.focus.path });
|
||||
|
||||
ReactEditor.focus(editor);
|
||||
}}
|
||||
initialState={initialState}
|
||||
/>
|
||||
)}
|
||||
<Popup
|
||||
buttonType="none"
|
||||
size="small"
|
||||
forceOpen={renderPopup}
|
||||
onToggleOpen={handleTogglePopup}
|
||||
horizontalAlign="left"
|
||||
verticalAlign="bottom"
|
||||
boundingRef={editorRef}
|
||||
render={() => (
|
||||
<div className={`${baseClass}__popup`}>
|
||||
{element.linkType === 'internal' && (
|
||||
<Fragment>
|
||||
Linked to doc
|
||||
</Fragment>
|
||||
)}
|
||||
{(element.linkType === 'custom' || !element.linkType) && (
|
||||
<Fragment>
|
||||
Go to link:
|
||||
<a
|
||||
className={`${baseClass}__goto-link`}
|
||||
href={element.url}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
{element.url}
|
||||
</a>
|
||||
</Fragment>
|
||||
)}
|
||||
—
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setRenderPopup(false);
|
||||
open(modalSlug);
|
||||
setRenderModal(true);
|
||||
}}
|
||||
>
|
||||
Change
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
unwrapLink(editor);
|
||||
}}
|
||||
>
|
||||
Remove
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
className={[
|
||||
`${baseClass}__button`,
|
||||
].filter(Boolean).join(' ')}
|
||||
onKeyDown={(e) => { if (e.key === 'Enter') setRenderPopup(true); }}
|
||||
onClick={() => setRenderPopup(true)}
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
</span>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,59 @@
|
||||
import { Config } from '../../../../../../../../config/types';
|
||||
import { Field } from '../../../../../../../../fields/config/types';
|
||||
|
||||
export const getBaseFields = (config: Config): Field[] => [
|
||||
{
|
||||
name: 'text',
|
||||
label: 'Text to display',
|
||||
type: 'text',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'linkType',
|
||||
label: 'Link Type',
|
||||
type: 'radio',
|
||||
required: true,
|
||||
admin: {
|
||||
description: 'Choose between entering a custom text URL or linking to another document.',
|
||||
},
|
||||
defaultValue: 'custom',
|
||||
options: [
|
||||
{
|
||||
label: 'Custom URL',
|
||||
value: 'custom',
|
||||
},
|
||||
{
|
||||
label: 'Internal Link',
|
||||
value: 'internal',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'url',
|
||||
label: 'Enter a URL',
|
||||
type: 'text',
|
||||
required: true,
|
||||
admin: {
|
||||
condition: ({ linkType, url }) => {
|
||||
return (typeof linkType === 'undefined' && url) || linkType === 'custom';
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'doc',
|
||||
label: 'Choose a document to link to',
|
||||
type: 'relationship',
|
||||
required: true,
|
||||
relationTo: config.collections.map(({ slug }) => slug),
|
||||
admin: {
|
||||
condition: ({ linkType }) => {
|
||||
return linkType === 'internal';
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'newTab',
|
||||
label: 'Open in new tab',
|
||||
type: 'checkbox',
|
||||
},
|
||||
];
|
||||
@@ -1,28 +1,25 @@
|
||||
import { Modal } from '@faceless-ui/modal';
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import React from 'react';
|
||||
import { MinimalTemplate } from '../../../../../..';
|
||||
import Button from '../../../../../../elements/Button';
|
||||
import X from '../../../../../../icons/X';
|
||||
import Form from '../../../../../Form';
|
||||
import FormSubmit from '../../../../../Submit';
|
||||
import Checkbox from '../../../../Checkbox';
|
||||
import Text from '../../../../Text';
|
||||
import { Props } from './types';
|
||||
import { modalSlug } from '../shared';
|
||||
import fieldTypes from '../../../..';
|
||||
import RenderFields from '../../../../../RenderFields';
|
||||
|
||||
import './index.scss';
|
||||
|
||||
const baseClass = modalSlug;
|
||||
|
||||
export const EditModal: React.FC<Props> = ({ close, handleModalSubmit, initialData }) => {
|
||||
const inputRef = useRef<HTMLInputElement>();
|
||||
|
||||
useEffect(() => {
|
||||
if (inputRef?.current) {
|
||||
inputRef.current.focus();
|
||||
}
|
||||
}, []);
|
||||
|
||||
export const EditModal: React.FC<Props> = ({
|
||||
close,
|
||||
handleModalSubmit,
|
||||
initialState,
|
||||
fieldSchema,
|
||||
}) => {
|
||||
return (
|
||||
<Modal
|
||||
slug={modalSlug}
|
||||
@@ -40,28 +37,12 @@ export const EditModal: React.FC<Props> = ({ close, handleModalSubmit, initialDa
|
||||
</header>
|
||||
<Form
|
||||
onSubmit={handleModalSubmit}
|
||||
initialData={initialData}
|
||||
initialState={initialState}
|
||||
>
|
||||
<Text
|
||||
inputRef={inputRef}
|
||||
required
|
||||
name="text"
|
||||
label="Text to display"
|
||||
admin={{
|
||||
className: `${baseClass}__input`,
|
||||
}}
|
||||
/>
|
||||
<Text
|
||||
required
|
||||
name="url"
|
||||
label="Enter a URL"
|
||||
admin={{
|
||||
className: `${baseClass}__input`,
|
||||
}}
|
||||
/>
|
||||
<Checkbox
|
||||
label="Open in new tab"
|
||||
name="newTab"
|
||||
<RenderFields
|
||||
fieldTypes={fieldTypes}
|
||||
readOnly={false}
|
||||
fieldSchema={fieldSchema}
|
||||
/>
|
||||
<FormSubmit>
|
||||
Confirm
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { Field } from '../../../../../../../../fields/config/types';
|
||||
import { Fields } from '../../../../../Form/types';
|
||||
|
||||
export type Props = {
|
||||
close: () => void
|
||||
handleModalSubmit: (fields: Fields, data: Record<string, unknown>) => void
|
||||
initialData?: Record<string, unknown>
|
||||
initialState?: Fields
|
||||
fieldSchema: Field[]
|
||||
}
|
||||
|
||||
@@ -1,216 +1,10 @@
|
||||
import React, { Fragment, useCallback, useEffect, useState } from 'react';
|
||||
import { ReactEditor, useSlate } from 'slate-react';
|
||||
import { Transforms, Node, Editor, Range } from 'slate';
|
||||
import { useModal } from '@faceless-ui/modal';
|
||||
import ElementButton from '../Button';
|
||||
import { unwrapLink, withLinks } from './utilities';
|
||||
import LinkIcon from '../../../../../icons/Link';
|
||||
import Popup from '../../../../../elements/Popup';
|
||||
import { EditModal } from './Modal';
|
||||
import { modalSlug } from './shared';
|
||||
import isElementActive from '../isActive';
|
||||
|
||||
import './index.scss';
|
||||
|
||||
const baseClass = 'rich-text-link';
|
||||
|
||||
// TODO: Multiple modal windows stacked go boom (rip). Edit Upload in fields -> rich text
|
||||
|
||||
const Link = ({ attributes, children, element, editorRef }) => {
|
||||
const editor = useSlate();
|
||||
const { open, closeAll } = useModal();
|
||||
const [renderModal, setRenderModal] = useState(false);
|
||||
const [renderPopup, setRenderPopup] = useState(false);
|
||||
const [initialData, setInitialData] = useState<Record<string, unknown>>({});
|
||||
|
||||
const handleTogglePopup = useCallback((render) => {
|
||||
if (!render) {
|
||||
setRenderPopup(render);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setInitialData({
|
||||
newTab: element.newTab,
|
||||
text: Node.string(element),
|
||||
url: element.url,
|
||||
});
|
||||
}, [renderModal, element]);
|
||||
|
||||
return (
|
||||
<span
|
||||
className={baseClass}
|
||||
{...attributes}
|
||||
>
|
||||
<span
|
||||
style={{ userSelect: 'none' }}
|
||||
contentEditable={false}
|
||||
>
|
||||
{renderModal && (
|
||||
<EditModal
|
||||
close={() => {
|
||||
closeAll();
|
||||
setRenderModal(false);
|
||||
}}
|
||||
handleModalSubmit={(_, data) => {
|
||||
closeAll();
|
||||
setRenderModal(false);
|
||||
|
||||
const [, parentPath] = Editor.above(editor);
|
||||
|
||||
Transforms.setNodes(
|
||||
editor,
|
||||
{
|
||||
newTab: data.newTab,
|
||||
url: data.url,
|
||||
},
|
||||
{ at: parentPath },
|
||||
);
|
||||
|
||||
Transforms.delete(editor, { at: editor.selection.focus.path, unit: 'block' });
|
||||
Transforms.move(editor, { distance: 1, unit: 'offset' });
|
||||
Transforms.insertText(editor, String(data.text), { at: editor.selection.focus.path });
|
||||
|
||||
ReactEditor.focus(editor);
|
||||
}}
|
||||
initialData={initialData}
|
||||
/>
|
||||
)}
|
||||
<Popup
|
||||
buttonType="none"
|
||||
size="small"
|
||||
forceOpen={renderPopup}
|
||||
onToggleOpen={handleTogglePopup}
|
||||
horizontalAlign="left"
|
||||
verticalAlign="bottom"
|
||||
boundingRef={editorRef}
|
||||
render={() => (
|
||||
<div className={`${baseClass}__popup`}>
|
||||
Go to link:
|
||||
<a
|
||||
className={`${baseClass}__goto-link`}
|
||||
href={element.url}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
{element.url}
|
||||
</a>
|
||||
—
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setRenderPopup(false);
|
||||
open(modalSlug);
|
||||
setRenderModal(true);
|
||||
}}
|
||||
>
|
||||
Change
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
unwrapLink(editor);
|
||||
}}
|
||||
>
|
||||
Remove
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
className={[
|
||||
`${baseClass}__button`,
|
||||
].filter(Boolean).join(' ')}
|
||||
onKeyDown={(e) => { if (e.key === 'Enter') setRenderPopup(true); }}
|
||||
onClick={() => setRenderPopup(true)}
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
const LinkButton = () => {
|
||||
const editor = useSlate();
|
||||
const { open, closeAll } = useModal();
|
||||
const [renderModal, setRenderModal] = useState(false);
|
||||
const [initialData, setInitialData] = useState<Record<string, unknown>>({});
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<ElementButton
|
||||
format="link"
|
||||
onClick={() => {
|
||||
if (isElementActive(editor, 'link')) {
|
||||
unwrapLink(editor);
|
||||
} else {
|
||||
open(modalSlug);
|
||||
setRenderModal(true);
|
||||
|
||||
const isCollapsed = editor.selection && Range.isCollapsed(editor.selection);
|
||||
|
||||
if (!isCollapsed) {
|
||||
setInitialData({
|
||||
text: Editor.string(editor, editor.selection),
|
||||
});
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
<LinkIcon />
|
||||
</ElementButton>
|
||||
{renderModal && (
|
||||
<EditModal
|
||||
initialData={initialData}
|
||||
close={() => {
|
||||
closeAll();
|
||||
setRenderModal(false);
|
||||
}}
|
||||
handleModalSubmit={(_, data) => {
|
||||
const isCollapsed = editor.selection && Range.isCollapsed(editor.selection);
|
||||
|
||||
const newLink = {
|
||||
type: 'link',
|
||||
url: data.url,
|
||||
newTab: data.newTab,
|
||||
children: [],
|
||||
};
|
||||
|
||||
if (isCollapsed) {
|
||||
// If selection anchor and focus are the same,
|
||||
// Just inject a new node with children already set
|
||||
Transforms.insertNodes(editor, {
|
||||
...newLink,
|
||||
children: [{ text: String(data.text) }],
|
||||
});
|
||||
} else {
|
||||
// Otherwise we need to wrap the selected node in a link,
|
||||
// Delete its old text,
|
||||
// Move the selection one position forward into the link,
|
||||
// And insert the text back into the new link
|
||||
Transforms.wrapNodes(editor, newLink, { split: true });
|
||||
Transforms.delete(editor, { at: editor.selection.focus.path, unit: 'word' });
|
||||
Transforms.move(editor, { distance: 1, unit: 'offset' });
|
||||
Transforms.insertText(editor, String(data.text), { at: editor.selection.focus.path });
|
||||
}
|
||||
|
||||
closeAll();
|
||||
setRenderModal(false);
|
||||
|
||||
ReactEditor.focus(editor);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
import { withLinks } from './utilities';
|
||||
import { LinkButton } from './Button';
|
||||
import { LinkElement } from './Element';
|
||||
|
||||
const link = {
|
||||
Button: LinkButton,
|
||||
Element: Link,
|
||||
Element: LinkElement,
|
||||
plugins: [
|
||||
withLinks,
|
||||
],
|
||||
|
||||
@@ -10,7 +10,6 @@ import Button from '../../../../../../../elements/Button';
|
||||
import RenderFields from '../../../../../../RenderFields';
|
||||
import fieldTypes from '../../../../..';
|
||||
import Form from '../../../../../../Form';
|
||||
import reduceFieldsToValues from '../../../../../../Form/reduceFieldsToValues';
|
||||
import Submit from '../../../../../../Submit';
|
||||
import { Field } from '../../../../../../../../../fields/config/types';
|
||||
import { useLocale } from '../../../../../../../utilities/Locale';
|
||||
@@ -34,9 +33,9 @@ export const EditModal: React.FC<Props> = ({ slug, closeModal, relatedCollection
|
||||
const { user } = useAuth();
|
||||
const locale = useLocale();
|
||||
|
||||
const handleUpdateEditData = useCallback((fields) => {
|
||||
const handleUpdateEditData = useCallback((_, data) => {
|
||||
const newNode = {
|
||||
fields: reduceFieldsToValues(fields, true),
|
||||
fields: data,
|
||||
};
|
||||
|
||||
const elementPath = ReactEditor.findPath(editor, element);
|
||||
@@ -90,7 +89,6 @@ export const EditModal: React.FC<Props> = ({ slug, closeModal, relatedCollection
|
||||
fieldTypes={fieldTypes}
|
||||
fieldSchema={fieldSchema}
|
||||
/>
|
||||
|
||||
<Submit>
|
||||
Save changes
|
||||
</Submit>
|
||||
|
||||
@@ -346,6 +346,9 @@ export const richText = baseField.keys({
|
||||
fields: joi.array().items(joi.link('#field')),
|
||||
})),
|
||||
}),
|
||||
link: joi.object({
|
||||
fields: joi.array().items(joi.link('#field')),
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
||||
|
||||
@@ -293,6 +293,9 @@ export type RichTextField = FieldBase & {
|
||||
}
|
||||
}
|
||||
}
|
||||
link?: {
|
||||
fields?: Field[];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { Field, fieldAffectsData } from '../../config/types';
|
||||
import { PayloadRequest } from '../../../express/types';
|
||||
import { traverseFields } from './traverseFields';
|
||||
import richTextRelationshipPromise from '../../richText/relationshipPromise';
|
||||
import richTextRelationshipPromise from '../../richText/richTextRelationshipPromise';
|
||||
import relationshipPopulationPromise from './relationshipPopulationPromise';
|
||||
|
||||
type Args = {
|
||||
@@ -47,11 +47,11 @@ export const promise = async ({
|
||||
}
|
||||
|
||||
const hasLocalizedValue = flattenLocales
|
||||
&& fieldAffectsData(field)
|
||||
&& (typeof siblingDoc[field.name] === 'object' && siblingDoc[field.name] !== null)
|
||||
&& field.name
|
||||
&& field.localized
|
||||
&& req.locale !== 'all';
|
||||
&& fieldAffectsData(field)
|
||||
&& (typeof siblingDoc[field.name] === 'object' && siblingDoc[field.name] !== null)
|
||||
&& field.name
|
||||
&& field.localized
|
||||
&& req.locale !== 'all';
|
||||
|
||||
if (hasLocalizedValue) {
|
||||
let localizedValue = siblingDoc[field.name][req.locale];
|
||||
@@ -110,8 +110,8 @@ export const promise = async ({
|
||||
await priorHook;
|
||||
|
||||
const shouldRunHookOnAllLocales = field.localized
|
||||
&& (req.locale === 'all' || !flattenLocales)
|
||||
&& typeof siblingDoc[field.name] === 'object';
|
||||
&& (req.locale === 'all' || !flattenLocales)
|
||||
&& typeof siblingDoc[field.name] === 'object';
|
||||
|
||||
if (shouldRunHookOnAllLocales) {
|
||||
const hookPromises = Object.entries(siblingDoc[field.name]).map(([locale, value]) => (async () => {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { Field, fieldHasSubFields, fieldIsArrayType, fieldAffectsData } from '../config/types';
|
||||
import { PayloadRequest } from '../../express/types';
|
||||
import { populate } from './populate';
|
||||
import { recurseRichText } from './relationshipPromise';
|
||||
import { recurseRichText } from './richTextRelationshipPromise';
|
||||
|
||||
type NestedRichTextFieldsArgs = {
|
||||
promises: Promise<void>[]
|
||||
|
||||
@@ -36,12 +36,26 @@ export const recurseRichText = ({
|
||||
}: RecurseRichTextArgs): void => {
|
||||
if (Array.isArray(children)) {
|
||||
(children as any[]).forEach((element) => {
|
||||
const collection = req.payload.collections[element?.relationTo];
|
||||
|
||||
if ((element.type === 'relationship' || element.type === 'upload')
|
||||
&& element?.value?.id
|
||||
&& collection
|
||||
&& (depth && currentDepth <= depth)) {
|
||||
const collection = req.payload.collections[element?.relationTo];
|
||||
|
||||
if (collection) {
|
||||
promises.push(populate({
|
||||
req,
|
||||
id: element.value.id,
|
||||
data: element,
|
||||
key: 'value',
|
||||
overrideAccess,
|
||||
depth,
|
||||
currentDepth,
|
||||
field,
|
||||
collection,
|
||||
showHiddenFields,
|
||||
}));
|
||||
}
|
||||
|
||||
if (element.type === 'upload' && Array.isArray(field.admin?.upload?.collections?.[element?.relationTo]?.fields)) {
|
||||
recurseNestedFields({
|
||||
promises,
|
||||
@@ -54,18 +68,40 @@ export const recurseRichText = ({
|
||||
showHiddenFields,
|
||||
});
|
||||
}
|
||||
promises.push(populate({
|
||||
req,
|
||||
id: element.value.id,
|
||||
data: element,
|
||||
key: 'value',
|
||||
overrideAccess,
|
||||
depth,
|
||||
currentDepth,
|
||||
field,
|
||||
collection,
|
||||
showHiddenFields,
|
||||
}));
|
||||
}
|
||||
|
||||
if (element.type === 'link') {
|
||||
if (element?.doc?.value && element?.doc?.relationTo) {
|
||||
const collection = req.payload.collections[element?.doc?.relationTo];
|
||||
|
||||
if (collection) {
|
||||
promises.push(populate({
|
||||
req,
|
||||
id: element.doc.value,
|
||||
data: element.doc,
|
||||
key: 'value',
|
||||
overrideAccess,
|
||||
depth,
|
||||
currentDepth,
|
||||
field,
|
||||
collection,
|
||||
showHiddenFields,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
if (Array.isArray(field.admin?.link?.fields)) {
|
||||
recurseNestedFields({
|
||||
promises,
|
||||
data: element.fields || {},
|
||||
fields: field.admin?.link?.fields,
|
||||
req,
|
||||
overrideAccess,
|
||||
depth,
|
||||
currentDepth,
|
||||
showHiddenFields,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (element?.children) {
|
||||
@@ -42,7 +42,7 @@ import formatName from '../utilities/formatName';
|
||||
import combineParentName from '../utilities/combineParentName';
|
||||
import withNullableType from './withNullableType';
|
||||
import { toWords } from '../../utilities/formatLabels';
|
||||
import createRichTextRelationshipPromise from '../../fields/richText/relationshipPromise';
|
||||
import createRichTextRelationshipPromise from '../../fields/richText/richTextRelationshipPromise';
|
||||
import formatOptions from '../utilities/formatOptions';
|
||||
import { Payload } from '../..';
|
||||
import buildWhereInputType from './buildWhereInputType';
|
||||
|
||||
@@ -43,6 +43,22 @@ const RichTextFields: CollectionConfig = {
|
||||
type: 'richText',
|
||||
required: true,
|
||||
admin: {
|
||||
link: {
|
||||
fields: [
|
||||
{
|
||||
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: {
|
||||
|
||||
Reference in New Issue
Block a user