Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions pal-demo/collections/Comments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ const Comments: CollectionConfig = {
name: 'field',
type: 'text',
},
{
name: 'author',
type: 'text',
},
{
name: 'range',
type: 'group',
Expand Down
10 changes: 10 additions & 0 deletions pal-demo/payload.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,16 @@ export default buildConfig({
type: 'text',
required: true,
},
{
name: 'first body',
type: 'richText',
required: true,
},
{
name: 'second body',
type: 'richText',
required: true,
},
],
versions: {
drafts: true,
Expand Down
66 changes: 49 additions & 17 deletions src/admin/components/forms/field-types/RichText/RichText.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React, { useState, useCallback, useMemo, useEffect, useRef } from 'react';
import React, { useState, useCallback, useMemo, useRef, useEffect } from 'react';
import isHotkey from 'is-hotkey';
import { createEditor, Transforms, Node, Element as SlateElement, Text, BaseEditor, Range } from 'slate';
import { createEditor, Transforms, Node, NodeEntry, Element as SlateElement, Text, BaseEditor, Range, Editor } from 'slate';
import { ReactEditor, Editable, withReact, Slate } from 'slate-react';
import { HistoryEditor, withHistory } from 'slate-history';
import { richText } from '../../../../../fields/validations';
Expand All @@ -25,6 +25,7 @@ import withEnterBreakOut from './plugins/withEnterBreakOut';

import './index.scss';
import { useCommentsContext } from '../../../views/Comments/context';
import { Comment } from '../../../views/Comments/types'

const defaultElements: RichTextElement[] = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'ul', 'ol', 'indent', 'link', 'relationship', 'upload'];
const defaultLeaves: RichTextLeaf[] = ['bold', 'italic', 'underline', 'strikethrough', 'code'];
Expand Down Expand Up @@ -94,16 +95,17 @@ const RichText: React.FC<Props> = (props) => {
return <div {...attributes}>{children}</div>;
}, [enabledElements, path, props]);


const { setFieldName, setRange, setIsEditing: setIsEditingComment } = useCommentsContext();
const { state, dispatch } = useCommentsContext();

const addComment = (fieldName: string) => (e) => {
e.preventDefault();
setFieldName(fieldName);
setIsEditingComment(true);
dispatch({
type: 'OPEN_COMMENT',
field: fieldName,
range: state.selectedRange,
});
};


const renderLeaf = useCallback(({ attributes, children, leaf }) => {
const matchedLeafName = Object.keys(enabledLeaves).find((leafName) => leaf[leafName]);

Expand All @@ -124,7 +126,12 @@ const RichText: React.FC<Props> = (props) => {
}

return (
<span {...attributes}>{children}</span>
<span
{...attributes}
className={leaf.loaded || leaf.highlighted ? `${baseClass}__${leaf.highlighted ? 'highlight' : 'loaded'}` : ''}
>
{children}
</span>
);
}, [enabledLeaves, path, props]);

Expand Down Expand Up @@ -175,6 +182,34 @@ const RichText: React.FC<Props> = (props) => {
editor.blurSelection = editor.selection;
}, [editor]);

const decorate = useCallback(([node, nodePath]: NodeEntry): Range[] => {
const ranges = [];
const decorateIntersecting = (currentRange: Range, decoration: string): void => {
if (Text.isText(node) && currentRange != null) {
const intersection = Range.intersection(currentRange, Editor.range(editor, nodePath));

if (intersection == null) {
return;
}

const range = {
[decoration]: true,
...intersection,
};

ranges.push(range);
}
};

if (state.selectedField === name) {
decorateIntersecting(state.selectedRange, 'highlighted');
}
state.comments
.filter(({ field }: Comment) => field === name)
.forEach(({ range }: Comment) => decorateIntersecting(range, 'loaded'));
return ranges;
}, [state.selectedRange, state.comments, state.selectedField, editor, name]);

useEffect(() => {
if (!loaded) {
const mergedElements = mergeCustomFunctions(elements, elementTypes);
Expand Down Expand Up @@ -289,6 +324,8 @@ const RichText: React.FC<Props> = (props) => {
spellCheck
readOnly={readOnly}
onBlur={onBlur}
onFocus={() => dispatch({ type: 'FOCUS_FIELD', field: name })}
decorate={decorate}
onKeyDown={(event) => {
if (event.key === 'Enter') {
if (event.shiftKey) {
Expand Down Expand Up @@ -342,9 +379,10 @@ const RichText: React.FC<Props> = (props) => {
}}
onSelect={() => {
const range = editor.selection;
if (!Range.isCollapsed(range)) {
setRange(range);
console.log(range);
if (Range.isExpanded(range)) {
dispatch({ type: 'UPDATE_RANGE', range });
} else if (!state.isEditing) {
dispatch({ type: 'UPDATE_RANGE', range: null });
}
}}
/>
Expand All @@ -355,12 +393,6 @@ const RichText: React.FC<Props> = (props) => {
value={value}
description={description}
/>
<button
type="button"
onClick={addComment(name)}
>
+ Comments
</button>
</div>
</div>
);
Expand Down
8 changes: 8 additions & 0 deletions src/admin/components/forms/field-types/RichText/index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,14 @@
margin-bottom: base(2);
display: flex;

&__highlight {
background-color: $color-green;
}

&__loaded {
background-color: $color-light-gray;
}

&__toolbar {
@include blur-bg(white);
display: flex;
Expand Down
34 changes: 34 additions & 0 deletions src/admin/components/views/Comments/CommentElement/index.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
@import '../../../../scss/styles.scss';

.comment {
$hover-box-shadow: inset 0 0 0 $style-stroke-width lighten($color-dark-gray, 5%);

&__card {
// border: 1px solid $color-light-gray;
color: $color-dark-gray;
margin-bottom: $baseline;
padding: base(0.5) base(0.75);
// border-radius: $style-radius-m;
position: relative;
background: white;

&:last-child {
margin-bottom: 0;
}


&:hover {
box-shadow: $hover-box-shadow;
background: rgba($color-dark-gray, .02);
}
}

&__byline {
position: bottom;
left: base(0.5);
bottom: base(0.5);
font-size: base(0.35);
color: $color-gray;
background: $color-background-gray;
}
}
35 changes: 35 additions & 0 deletions src/admin/components/views/Comments/CommentElement/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import React from 'react';
import { useCommentsContext } from '../context';
import { CommentProps } from './types';
import './index.scss';

const CommentElement: React.FC<CommentProps> = ({ comment: { 'comment-content': content, range, field, author } }) => {
const baseName = 'comment';
const { state, dispatch } = useCommentsContext();
const highlightRange = () => {
if (!state.isEditing) {
dispatch({ type: 'HIGHLIGHT_TEXT', range, field });
}
};
return (
<div
className={`${baseName}__card`}
role="button"
onClick={highlightRange}
onMouseOver={highlightRange}
onFocus={highlightRange}
tabIndex={0}
onKeyDown={(e) => {
if (e.key === 'Enter') {
highlightRange();
}
}}
>
{content}
<br />
{author && (<span className={`${baseName}__byline`}>{author}</span>)}
</div>
);
};

export default CommentElement;
5 changes: 5 additions & 0 deletions src/admin/components/views/Comments/CommentElement/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { Comment } from '../types';

export interface CommentProps {
comment: Comment
}
83 changes: 42 additions & 41 deletions src/admin/components/views/Comments/context/index.tsx
Original file line number Diff line number Diff line change
@@ -1,83 +1,84 @@
import React, { createContext, useCallback, useContext, useEffect, useState } from 'react';
import React, { createContext, useCallback, useContext, useEffect, useReducer, useMemo} from 'react';
import { useRouteMatch } from 'react-router-dom';
import queryString from 'qs';
import { Range } from 'slate';
import { requests } from '../../../../api';
import { useConfig } from '../../../utilities/Config';
import { Comment } from '../types';


type UpdateFn<T> = (t: T) => void
import { CommentsAction, commentsReducer, CommentsState, initCommentsState, Thunk, thunkMiddleware } from './reducer';

interface Context {
comments: Comment[]
range: Range | null
setRange: UpdateFn<Range | null>
isEditing: boolean
setIsEditing: UpdateFn<boolean>
fieldName: string
setFieldName: UpdateFn<string>
reloadComments: () => void
state: CommentsState,
dispatch: React.Dispatch<CommentsAction | Thunk>
reloadComments: () => (dispatch: React.Dispatch<CommentsAction>) => Promise<void>
}

const CommentsContext = createContext({} as Context);

export const useCommentsContext = () => useContext(CommentsContext);

export const CommentsProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [isEditing, setIsEditing] = useState(false);
const [fieldName, setFieldName] = useState('');
const [comments, setComments] = useState<Comment[]>([]);
const [range, setRange] = useState<Range | null>(null);
const [isError, setIsError] = useState(false);
const [isLoading, setIsLoading] = useState(false);

const [state, commentsDispatch] = useReducer(commentsReducer, initCommentsState);
const { serverURL, routes: { api } } = useConfig();
const { params: { id } = {} } = useRouteMatch<Record<string, string>>();


const reloadComments = useCallback(async () => {
const reloadComments = useCallback(() => async (dispatch: React.Dispatch<CommentsAction>) => {
const commentQuery = {
'content-id': {
equals: id,
},
};
if (!id) {
// The currently viewed content may be new and therefore not have an id.
// Load empty array of comments instead.
dispatch({ type: 'SUCCEED_LOAD_COMMENTS', comments: [] });
return;
}
const unwrap = ({ index }: {index: number}) => index;
const dbRangeToSlateRange = (dbRange) => ({
anchor: {
...dbRange.anchor,
path: dbRange.anchor.path.map(unwrap),
},
focus: {
...dbRange.focus,
path: dbRange.focus.path.map(unwrap),
},
});
const dbCommentToSlate = (dbComment) => {
return {
...dbComment,
range: dbRangeToSlateRange(dbComment.range),
};
};
const url = `${serverURL}${api}/comments`;
const search = queryString.stringify({
'fallback-locale': 'null', depth: 0, draft: 'true', where: commentQuery,
});
try {
const response = await requests.get(`${url}?${search}`);

if (response.status > 201) {
setIsError(true);
}
// if (response.status > 201) {
// setIsError(true);
// }

const json = await response.json();
setComments(json.docs ?? []);
const comments = json.docs ? json.docs.map(dbCommentToSlate) : [];
dispatch({ type: 'SUCCEED_LOAD_COMMENTS', comments });

console.log(json);
setIsLoading(false);
// setIsLoading(false);
} catch (error) {
console.log(error);
setIsError(true);
setIsLoading(false);
// setIsError(true);
// setIsLoading(false);
}

console.log(api, serverURL, commentQuery);
}, [api, serverURL, id]);

console.log(comments);
const enhancedDispatch = useMemo(() => thunkMiddleware(commentsDispatch), [commentsDispatch]);


return (
<CommentsContext.Provider value={{
comments,
range,
setRange,
isEditing,
setIsEditing,
fieldName,
setFieldName,
state,
dispatch: enhancedDispatch,
reloadComments,
}}
>
Expand Down
Loading