Skip to content

Commit

Permalink
feat: adds rich text editor upload element
Browse files Browse the repository at this point in the history
  • Loading branch information
jmikrut committed Sep 24, 2021
1 parent 9247d29 commit aa76950
Show file tree
Hide file tree
Showing 24 changed files with 633 additions and 8 deletions.
2 changes: 2 additions & 0 deletions src/admin/components/elements/Button/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@ import { Props } from './types';
import plus from '../../icons/Plus';
import x from '../../icons/X';
import chevron from '../../icons/Chevron';
import edit from '../../icons/Edit';

import './index.scss';

const icons = {
plus,
x,
chevron,
edit,
};

const baseClass = 'btn';
Expand Down
2 changes: 1 addition & 1 deletion src/admin/components/elements/Button/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export type Props = {
children?: React.ReactNode,
onClick?: (event: MouseEvent) => void,
disabled?: boolean,
icon?: React.ReactNode | ['chevron' | 'x' | 'plus'],
icon?: React.ReactNode | ['chevron' | 'x' | 'plus' | 'edit'],
iconStyle?: 'with-border' | 'without-border' | 'none',
buttonStyle?: 'primary' | 'secondary' | 'transparent' | 'error' | 'none' | 'icon-label',
round?: boolean,
Expand Down
2 changes: 2 additions & 0 deletions src/admin/components/elements/Popup/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const baseClass = 'popup';

const Popup: React.FC<Props> = (props) => {
const {
className,
render,
size = 'small',
color = 'light',
Expand Down Expand Up @@ -87,6 +88,7 @@ const Popup: React.FC<Props> = (props) => {

const classes = [
baseClass,
className,
`${baseClass}--size-${size}`,
`${baseClass}--color-${color}`,
`${baseClass}--v-align-${verticalAlign}`,
Expand Down
1 change: 1 addition & 0 deletions src/admin/components/elements/Popup/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export type Props = {
className?: string
render?: (any) => void,
children?: React.ReactNode,
horizontalAlign?: 'left' | 'center' | 'right',
Expand Down
2 changes: 2 additions & 0 deletions src/admin/components/elements/UploadCard/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,15 @@ const baseClass = 'upload-card';

const UploadCard: React.FC<Props> = (props) => {
const {
className,
onClick,
doc,
collection,
} = props;

const classes = [
baseClass,
className,
typeof onClick === 'function' && `${baseClass}--has-on-click`,
].filter(Boolean).join(' ');

Expand Down
1 change: 1 addition & 0 deletions src/admin/components/elements/UploadCard/types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { SanitizedCollectionConfig } from '../../../../collections/config/types';

export type Props = {
className?: string
collection: SanitizedCollectionConfig,
doc: Record<string, unknown>
onClick?: () => void,
Expand Down
10 changes: 7 additions & 3 deletions src/admin/components/forms/field-types/RichText/RichText.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import mergeCustomFunctions from './mergeCustomFunctions';

import './index.scss';

const defaultElements: RichTextElement[] = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'ul', 'ol', 'link', 'relationship'];
const defaultElements: RichTextElement[] = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'ul', 'ol', 'link', 'relationship', 'upload'];
const defaultLeaves: RichTextLeaf[] = ['bold', 'italic', 'underline', 'strikethrough', 'code'];
const enterBreakOutTypes = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'];

Expand Down Expand Up @@ -68,14 +68,15 @@ const RichText: React.FC<Props> = (props) => {
<Element
attributes={attributes}
element={element}
path={path}
>
{children}
</Element>
);
}

return <div {...attributes}>{children}</div>;
}, [enabledElements]);
}, [enabledElements, path]);

const renderLeaf = useCallback(({ attributes, children, leaf }) => {
const matchedLeafName = Object.keys(enabledLeaves).find((leafName) => leaf[leafName]);
Expand All @@ -87,6 +88,7 @@ const RichText: React.FC<Props> = (props) => {
<Leaf
attributes={attributes}
leaf={leaf}
path={path}
>
{children}
</Leaf>
Expand All @@ -96,7 +98,7 @@ const RichText: React.FC<Props> = (props) => {
return (
<span {...attributes}>{children}</span>
);
}, [enabledLeaves]);
}, [enabledLeaves, path]);

const memoizedValidate = useCallback((value) => {
const validationResult = validate(value, { required });
Expand Down Expand Up @@ -287,6 +289,8 @@ const RichText: React.FC<Props> = (props) => {

Transforms.setNodes(editor, { type: 'p' });
}
} else if (editor.isVoid(selectedElement)) {
Transforms.removeNodes(editor);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import ol from './ol';
import ul from './ul';
import li from './li';
import relationship from './relationship';
import upload from './upload';

export default {
h1,
Expand All @@ -22,4 +23,5 @@ export default {
ul,
li,
relationship,
upload,
};
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,9 @@
text-overflow: ellipsis;
overflow: hidden;
}

&--selected {
box-shadow: $focus-box-shadow;
outline: none;
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React, { useState } from 'react';
import { useFocused, useSelected } from 'slate-react';
import { useConfig } from '@payloadcms/config-provider';
import RelationshipIcon from '../../../../../../icons/Relationship';
import usePayloadAPI from '../../../../../../../hooks/usePayloadAPI';
Expand All @@ -11,10 +12,13 @@ const initialParams = {
depth: 0,
};

const Element = ({ attributes, children, element }) => {
const Element = (props) => {
const { attributes, children, element } = props;
const { relationTo, value } = element;
const { collections, serverURL, routes: { api } } = useConfig();
const [relatedCollection] = useState(() => collections.find((coll) => coll.slug === relationTo));
const selected = useSelected();
const focused = useFocused();

const [{ data }] = usePayloadAPI(
`${serverURL}${api}/${relatedCollection.slug}/${value?.id}`,
Expand All @@ -23,7 +27,10 @@ const Element = ({ attributes, children, element }) => {

return (
<div
className={baseClass}
className={[
baseClass,
(selected && focused) && `${baseClass}--selected`,
].filter(Boolean).join(' ')}
contentEditable={false}
{...attributes}
>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import React, { Fragment, useState, useEffect } from 'react';
import { useConfig, useAuth } from '@payloadcms/config-provider';
import { useWatchForm } from '../../../../../../Form/context';
import Relationship from '../../../../../Relationship';
import Select from '../../../../../Select';

const createOptions = (collections, permissions) => collections.reduce((options, collection) => {
if (permissions?.collections?.[collection.slug]?.read?.permission && collection?.admin?.enableRichTextRelationship && collection.upload) {
return [
...options,
{
label: collection.labels.plural,
value: collection.slug,
},
];
}

return options;
}, []);

const UploadFields = () => {
const { collections } = useConfig();
const { permissions } = useAuth();

const [options, setOptions] = useState(() => createOptions(collections, permissions));

const { getData } = useWatchForm();
const { relationTo } = getData();

useEffect(() => {
setOptions(createOptions(collections, permissions));
}, [collections, permissions]);

return (
<Fragment>
<Select
required
label="Relation To"
name="relationTo"
options={options}
/>
{relationTo && (
<Relationship
label="Upload"
name="value"
relationTo={relationTo}
required
/>
)}
</Fragment>
);
};

export default UploadFields;
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
@import '../../../../../../../scss/styles.scss';

.upload-rich-text-button {
.btn {
margin-right: base(1);
}

&__modal {
display: flex;
align-items: center;
height: 100%;

&.payload__modal-item--enterDone {
@include blur-bg;
}
}

&__header {
width: 100%;
margin-bottom: $baseline;
display: flex;
justify-content: space-between;

h3 {
margin: 0;
}

svg {
width: base(1.5);
height: base(1.5);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import React, { Fragment, useCallback, useEffect, useState } from 'react';
import { Modal, useModal } from '@faceless-ui/modal';
import { Transforms } from 'slate';
import { ReactEditor, useSlate } from 'slate-react';
import { useConfig } from '@payloadcms/config-provider';
import ElementButton from '../../Button';
import UploadIcon from '../../../../../../icons/Upload';
import Form from '../../../../../Form';
import MinimalTemplate from '../../../../../../templates/Minimal';
import Button from '../../../../../../elements/Button';
import Submit from '../../../../../Submit';
import X from '../../../../../../icons/X';
import Fields from './Fields';
import { requests } from '../../../../../../../api';

import './index.scss';

const initialFormData = {};

const baseClass = 'upload-rich-text-button';

const insertUpload = (editor, { value, relationTo }) => {
const text = { text: ' ' };

const upload = {
type: 'upload',
value,
relationTo,
children: [
text,
],
};

const nodes = [upload, { children: [{ text: '' }] }];

if (editor.blurSelection) {
Transforms.select(editor, editor.blurSelection);
}

Transforms.insertNodes(editor, nodes);

const currentPath = editor.selection.anchor.path[0];
const newSelection = { anchor: { path: [currentPath + 1, 0], offset: 0 }, focus: { path: [currentPath + 1, 0], offset: 0 } };

Transforms.select(editor, newSelection);
ReactEditor.focus(editor);
};

const UploadButton: React.FC<{path: string}> = ({ path }) => {
const { open, closeAll } = useModal();
const editor = useSlate();
const { serverURL, routes: { api }, collections } = useConfig();
const [renderModal, setRenderModal] = useState(false);
const [loading, setLoading] = useState(false);
const [hasEnabledCollections] = useState(() => collections.find(({ upload, admin: { enableRichTextRelationship } }) => upload && enableRichTextRelationship));
const modalSlug = `${path}-add-upload`;

const handleAddUpload = useCallback(async (_, { relationTo, value }) => {
setLoading(true);

const res = await requests.get(`${serverURL}${api}/${relationTo}/${value}?depth=0`);
const json = await res.json();

insertUpload(editor, { value: { id: json.id }, relationTo });
closeAll();
setRenderModal(false);
setLoading(false);
}, [editor, closeAll, api, serverURL]);

useEffect(() => {
if (renderModal) {
open(modalSlug);
}
}, [renderModal, open, modalSlug]);

if (!hasEnabledCollections) return null;

return (
<Fragment>
<ElementButton
className={baseClass}
format="upload"
onClick={() => setRenderModal(true)}
>
<UploadIcon />
</ElementButton>
{renderModal && (
<Modal
slug={modalSlug}
className={`${baseClass}__modal`}
>
<MinimalTemplate>
<header className={`${baseClass}__header`}>
<h3>Add Upload</h3>
<Button
buttonStyle="none"
onClick={() => {
closeAll();
setRenderModal(false);
}}
>
<X />
</Button>
</header>
<Form
onSubmit={handleAddUpload}
initialData={initialFormData}
disabled={loading}
>
<Fields />
<Submit>
Add upload
</Submit>
</Form>
</MinimalTemplate>
</Modal>
)}
</Fragment>
);
};

export default UploadButton;
Loading

0 comments on commit aa76950

Please sign in to comment.