From f821e3016d5b75c93cc9ef4d112bd8d5f8c99157 Mon Sep 17 00:00:00 2001 From: Edward Nuno Date: Sun, 29 May 2022 14:11:32 -0700 Subject: [PATCH 1/4] Before adding reducer, dissolve later --- .../forms/field-types/RichText/RichText.tsx | 48 ++++++++++++-- .../forms/field-types/RichText/index.scss | 4 ++ .../views/Comments/CommentElement/index.tsx | 19 ++++++ .../views/Comments/CommentElement/types.ts | 5 ++ .../views/Comments/context/index.tsx | 29 ++++++-- .../views/Comments/context/reducer/actions.ts | 66 +++++++++++++++++++ .../views/Comments/context/reducer/index.ts | 3 + .../Comments/context/reducer/middleware.ts | 18 +++++ .../views/Comments/context/reducer/reducer.ts | 48 ++++++++++++++ src/admin/components/views/Comments/index.tsx | 38 ++++++----- src/admin/components/views/Comments/types.ts | 1 + 11 files changed, 249 insertions(+), 30 deletions(-) create mode 100644 src/admin/components/views/Comments/CommentElement/index.tsx create mode 100644 src/admin/components/views/Comments/CommentElement/types.ts create mode 100644 src/admin/components/views/Comments/context/reducer/actions.ts create mode 100644 src/admin/components/views/Comments/context/reducer/index.ts create mode 100644 src/admin/components/views/Comments/context/reducer/middleware.ts create mode 100644 src/admin/components/views/Comments/context/reducer/reducer.ts diff --git a/src/admin/components/forms/field-types/RichText/RichText.tsx b/src/admin/components/forms/field-types/RichText/RichText.tsx index 49d25456a94..4da838f9341 100644 --- a/src/admin/components/forms/field-types/RichText/RichText.tsx +++ b/src/admin/components/forms/field-types/RichText/RichText.tsx @@ -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'; @@ -94,8 +94,7 @@ const RichText: React.FC = (props) => { return
{children}
; }, [enabledElements, path, props]); - - const { setFieldName, setRange, setIsEditing: setIsEditingComment } = useCommentsContext(); + const { setFieldName, setRange, currentRange, setIsEditing: setIsEditingComment } = useCommentsContext(); const addComment = (fieldName: string) => (e) => { e.preventDefault(); @@ -103,10 +102,10 @@ const RichText: React.FC = (props) => { setIsEditingComment(true); }; - const renderLeaf = useCallback(({ attributes, children, leaf }) => { const matchedLeafName = Object.keys(enabledLeaves).find((leafName) => leaf[leafName]); + console.log('leaf.highlighted:', leaf.highlighted); if (enabledLeaves[matchedLeafName]?.Leaf) { const { Leaf } = enabledLeaves[matchedLeafName]; @@ -124,7 +123,12 @@ const RichText: React.FC = (props) => { } return ( - {children} + + {children} + ); }, [enabledLeaves, path, props]); @@ -175,6 +179,35 @@ const RichText: React.FC = (props) => { editor.blurSelection = editor.selection; }, [editor]); + useEffect(() => { + if (currentRange) { + Transforms.select(editor, currentRange); + ReactEditor.focus(editor); + console.dir(currentRange); + } + }, [editor, currentRange]); + + // TODO(archaengel): Looks like this is higlighting entire ranges if they intersect, not only the + // range from the selected comment. We should rigure out how to massage the ranges to correct this. + const decorate = useCallback(([node, nodePath]: NodeEntry): Range[] => { + if (Text.isText(node) && currentRange != null) { + const intersection = Range.intersection(currentRange, Editor.range(editor, nodePath)); + + if (intersection == null) { + return []; + } + + const range = { + highlighted: true, + ...intersection, + }; + + return [range]; + } + + return []; + }, [currentRange, editor]); + useEffect(() => { if (!loaded) { const mergedElements = mergeCustomFunctions(elements, elementTypes); @@ -289,6 +322,7 @@ const RichText: React.FC = (props) => { spellCheck readOnly={readOnly} onBlur={onBlur} + decorate={decorate} onKeyDown={(event) => { if (event.key === 'Enter') { if (event.shiftKey) { @@ -344,7 +378,7 @@ const RichText: React.FC = (props) => { const range = editor.selection; if (!Range.isCollapsed(range)) { setRange(range); - console.log(range); + console.log('onSelect:', range); } }} /> diff --git a/src/admin/components/forms/field-types/RichText/index.scss b/src/admin/components/forms/field-types/RichText/index.scss index 485a0ef59e2..e75b6156427 100644 --- a/src/admin/components/forms/field-types/RichText/index.scss +++ b/src/admin/components/forms/field-types/RichText/index.scss @@ -4,6 +4,10 @@ margin-bottom: base(2); display: flex; + &__highlight { + background-color: $color-green; + } + &__toolbar { @include blur-bg(white); display: flex; diff --git a/src/admin/components/views/Comments/CommentElement/index.tsx b/src/admin/components/views/Comments/CommentElement/index.tsx new file mode 100644 index 00000000000..601e4de15a7 --- /dev/null +++ b/src/admin/components/views/Comments/CommentElement/index.tsx @@ -0,0 +1,19 @@ +import React from 'react'; +import { useCommentsContext } from '../context'; +import { CommentProps } from './types'; + +const CommentElement: React.FC = ({ comment: { 'comment-content': content, range } }) => { + const { setCurrentRange } = useCommentsContext(); + return ( +
  • + +
  • + ); +}; + +export default CommentElement; diff --git a/src/admin/components/views/Comments/CommentElement/types.ts b/src/admin/components/views/Comments/CommentElement/types.ts new file mode 100644 index 00000000000..95ecc7c2ad6 --- /dev/null +++ b/src/admin/components/views/Comments/CommentElement/types.ts @@ -0,0 +1,5 @@ +import { Comment } from '../types'; + +export interface CommentProps { + comment: Comment +} diff --git a/src/admin/components/views/Comments/context/index.tsx b/src/admin/components/views/Comments/context/index.tsx index 9c455342b10..e656427d9c2 100644 --- a/src/admin/components/views/Comments/context/index.tsx +++ b/src/admin/components/views/Comments/context/index.tsx @@ -13,6 +13,8 @@ interface Context { comments: Comment[] range: Range | null setRange: UpdateFn + currentRange: Range | null + setCurrentRange: UpdateFn isEditing: boolean setIsEditing: UpdateFn fieldName: string @@ -29,6 +31,7 @@ export const CommentsProvider: React.FC<{ children: React.ReactNode }> = ({ chil const [fieldName, setFieldName] = useState(''); const [comments, setComments] = useState([]); const [range, setRange] = useState(null); + const [currentRange, setCurrentRange] = useState(null); const [isError, setIsError] = useState(false); const [isLoading, setIsLoading] = useState(false); @@ -42,6 +45,23 @@ export const CommentsProvider: React.FC<{ children: React.ReactNode }> = ({ chil equals: id, }, }; + 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, @@ -54,26 +74,23 @@ export const CommentsProvider: React.FC<{ children: React.ReactNode }> = ({ chil } const json = await response.json(); - setComments(json.docs ?? []); + setComments(json.docs ? json.docs.map(dbCommentToSlate) : []); - console.log(json); setIsLoading(false); } catch (error) { - console.log(error); setIsError(true); setIsLoading(false); } - - console.log(api, serverURL, commentQuery); }, [api, serverURL, id]); - console.log(comments); return ( +} + +interface FailSaveComment { + type: 'FAIL_SAVE_COMMENT' +} + +interface SucceedSaveComment { + type: 'SUCCEED_SAVE_COMMENT' +} + +interface AttemptLoadComments { + type: 'ATTEMPT_LOAD_COMMENTS' +} + +interface FailLoadComments { + type: 'FAIL_LOAD_COMMENTS' +} + +interface SucceedLoadComments { + type: 'SUCCEED_LOAD_COMMENTS' + comments: Comment[] +} + +interface HighlightComment { + type: 'HIGHLIGHT_COMMENT' + range: Range +} + +export type CommentsAction = + | OpenComment + | UpdateComment + | CancelComment + | DeleteComment + | AttemptSaveComment + | FailSaveComment + | SucceedSaveComment + | AttemptLoadComments + | FailLoadComments + | SucceedLoadComments + | HighlightComment diff --git a/src/admin/components/views/Comments/context/reducer/index.ts b/src/admin/components/views/Comments/context/reducer/index.ts new file mode 100644 index 00000000000..3a74bc73cb2 --- /dev/null +++ b/src/admin/components/views/Comments/context/reducer/index.ts @@ -0,0 +1,3 @@ +export * from './reducer'; +export * from './actions'; +export * from './middleware'; diff --git a/src/admin/components/views/Comments/context/reducer/middleware.ts b/src/admin/components/views/Comments/context/reducer/middleware.ts new file mode 100644 index 00000000000..f357312ae06 --- /dev/null +++ b/src/admin/components/views/Comments/context/reducer/middleware.ts @@ -0,0 +1,18 @@ +import React from 'react'; +import { CommentsAction } from './actions'; +import { CommentsState } from './reducer'; + +interface Store { + dispatch: React.Dispatch + state: CommentsState +} + +type Thunk = (dispatch: React.Dispatch) => void + +export const thunk = (store: Store) => (next: React.Dispatch) => (action: CommentsAction | Thunk) => { + if (typeof action === 'function') { + return action(store.dispatch); + } + + return next(action); +}; diff --git a/src/admin/components/views/Comments/context/reducer/reducer.ts b/src/admin/components/views/Comments/context/reducer/reducer.ts new file mode 100644 index 00000000000..2127178d376 --- /dev/null +++ b/src/admin/components/views/Comments/context/reducer/reducer.ts @@ -0,0 +1,48 @@ +import { Range } from 'slate'; +import { Comment } from '../../types'; +import { CommentsAction } from './actions'; + +export interface CommentsState { + comments: Comment[] + selectedRange: Range | null + selectedField: string | null + text: string | null + isEditing: boolean +} + +export const commentsReducer = (state: CommentsState, action: CommentsAction): CommentsState => { + switch (action.type) { + case 'OPEN_COMMENT': + return ({ + ...state, + selectedRange: action.range, + selectedField: action.field, + isEditing: true, + }); + case 'UPDATE_COMMENT': + return ({ + ...state, + text: action.text, + }); + case 'CANCEL_COMMENT': + return ({ + ...state, + selectedRange: null, + selectedField: null, + text: null, + isEditing: false, + }); + case 'SUCCEED_LOAD_COMMENTS': + return ({ + ...state, + comments: action.comments, + }); + case 'HIGHLIGHT_COMMENT': + return ({ + ...state, + selectedRange: action.range, + }); + default: + return { ...state }; + } +}; diff --git a/src/admin/components/views/Comments/index.tsx b/src/admin/components/views/Comments/index.tsx index db1b290a1b2..3025e67ad7c 100644 --- a/src/admin/components/views/Comments/index.tsx +++ b/src/admin/components/views/Comments/index.tsx @@ -2,11 +2,16 @@ import React, { useState, useEffect, useCallback } from 'react'; import { Range } from 'slate'; import { requests } from '../../../api'; import { useConfig } from '../../utilities/Config'; +import CommentElement from './CommentElement'; import { useCommentsContext } from './context'; import { CommentsProp, Comment } from './types'; -const renderComment = ({ 'comment-content': content }, i: number) =>
  • {content}
  • ; - +const renderComment = (comment: Comment) => ( + +); const CommentsView: React.FC = (props) => { const { @@ -20,26 +25,24 @@ const CommentsView: React.FC = (props) => { fieldName: field, reloadComments, range, + setCurrentRange, } = useCommentsContext(); - const { serverURL, routes: { api } } = useConfig(); - const saveComment = useCallback(async (comment: Comment) => { + const saveComment = useCallback(async (comment: Omit) => { const action = `${serverURL}${api}/comments`; - const indexWrap = (index) => ({ index }); - const slateToPayloadRange = ({ anchor, focus }: Range) => { - return { - anchor: { - ...anchor, - path: anchor.path.map(indexWrap), - }, - focus: { - ...focus, - path: focus.path.map(indexWrap), - }, - }; - }; + const indexWrap = (index: number) => ({ index }); + const slateToPayloadRange = ({ anchor, focus }: Range) => ({ + anchor: { + ...anchor, + path: anchor.path.map(indexWrap), + }, + focus: { + ...focus, + path: focus.path.map(indexWrap), + }, + }); await requests.post(action, { body: JSON.stringify({ @@ -94,6 +97,7 @@ const CommentsView: React.FC = (props) => { placeholder="Enter comment..." value={content} onChange={((e) => setContent(e.target.value))} + onFocus={() => setCurrentRange(range)} />
    ); diff --git a/src/admin/components/forms/field-types/RichText/index.scss b/src/admin/components/forms/field-types/RichText/index.scss index e75b6156427..344168761b1 100644 --- a/src/admin/components/forms/field-types/RichText/index.scss +++ b/src/admin/components/forms/field-types/RichText/index.scss @@ -8,6 +8,10 @@ background-color: $color-green; } + &__loaded { + background-color: $color-light-gray; + } + &__toolbar { @include blur-bg(white); display: flex; diff --git a/src/admin/components/views/Comments/CommentElement/index.scss b/src/admin/components/views/Comments/CommentElement/index.scss new file mode 100644 index 00000000000..59275c4171b --- /dev/null +++ b/src/admin/components/views/Comments/CommentElement/index.scss @@ -0,0 +1,22 @@ +@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; + + &:last-child { + margin-bottom: 0; + } + + &:hover { + box-shadow: $hover-box-shadow; + background: rgba($color-dark-gray, .02); + } + } +} diff --git a/src/admin/components/views/Comments/CommentElement/index.tsx b/src/admin/components/views/Comments/CommentElement/index.tsx index 601e4de15a7..d6118db83b6 100644 --- a/src/admin/components/views/Comments/CommentElement/index.tsx +++ b/src/admin/components/views/Comments/CommentElement/index.tsx @@ -1,18 +1,32 @@ import React from 'react'; import { useCommentsContext } from '../context'; import { CommentProps } from './types'; +import './index.scss'; -const CommentElement: React.FC = ({ comment: { 'comment-content': content, range } }) => { - const { setCurrentRange } = useCommentsContext(); +const CommentElement: React.FC = ({ comment: { 'comment-content': content, range, field } }) => { + const baseName = 'comment'; + const { state, dispatch } = useCommentsContext(); + const highlightRange = () => { + if (!state.isEditing) { + dispatch({ type: 'HIGHLIGHT_TEXT', range, field }); + } + }; return ( -
  • - -
  • +
    { + if (e.key === 'Enter') { + highlightRange(); + } + }} + > + {content} +
    ); }; diff --git a/src/admin/components/views/Comments/context/index.tsx b/src/admin/components/views/Comments/context/index.tsx index e656427d9c2..1a438969963 100644 --- a/src/admin/components/views/Comments/context/index.tsx +++ b/src/admin/components/views/Comments/context/index.tsx @@ -1,25 +1,14 @@ -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) => void +import { CommentsAction, commentsReducer, CommentsState, initCommentsState, Thunk, thunkMiddleware } from './reducer'; interface Context { - comments: Comment[] - range: Range | null - setRange: UpdateFn - currentRange: Range | null - setCurrentRange: UpdateFn - isEditing: boolean - setIsEditing: UpdateFn - fieldName: string - setFieldName: UpdateFn - reloadComments: () => void + state: CommentsState, + dispatch: React.Dispatch + reloadComments: () => (dispatch: React.Dispatch) => Promise } const CommentsContext = createContext({} as Context); @@ -27,24 +16,23 @@ 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([]); - const [range, setRange] = useState(null); - const [currentRange, setCurrentRange] = useState(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>(); - const reloadComments = useCallback(async () => { + const reloadComments = useCallback(() => async (dispatch: React.Dispatch) => { 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: { @@ -69,32 +57,28 @@ export const CommentsProvider: React.FC<{ children: React.ReactNode }> = ({ chil 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 ? json.docs.map(dbCommentToSlate) : []); + const comments = json.docs ? json.docs.map(dbCommentToSlate) : []; + dispatch({ type: 'SUCCEED_LOAD_COMMENTS', comments }); - setIsLoading(false); + // setIsLoading(false); } catch (error) { - setIsError(true); - setIsLoading(false); + // setIsError(true); + // setIsLoading(false); } }, [api, serverURL, id]); + const enhancedDispatch = useMemo(() => thunkMiddleware(commentsDispatch), [commentsDispatch]); + return ( diff --git a/src/admin/components/views/Comments/context/reducer/actions.ts b/src/admin/components/views/Comments/context/reducer/actions.ts index 07df9dbe81e..e6a3c9d770e 100644 --- a/src/admin/components/views/Comments/context/reducer/actions.ts +++ b/src/admin/components/views/Comments/context/reducer/actions.ts @@ -47,11 +47,22 @@ interface SucceedLoadComments { comments: Comment[] } -interface HighlightComment { - type: 'HIGHLIGHT_COMMENT' +interface UpdateRange { + type: 'UPDATE_RANGE' range: Range } +interface HighlightText { + type: 'HIGHLIGHT_TEXT' + range: Range + field: string +} + +interface FocusField { + type: 'FOCUS_FIELD' + field: string +} + export type CommentsAction = | OpenComment | UpdateComment @@ -63,4 +74,6 @@ export type CommentsAction = | AttemptLoadComments | FailLoadComments | SucceedLoadComments - | HighlightComment + | UpdateRange + | HighlightText + | FocusField diff --git a/src/admin/components/views/Comments/context/reducer/middleware.ts b/src/admin/components/views/Comments/context/reducer/middleware.ts index f357312ae06..1adf1eac68f 100644 --- a/src/admin/components/views/Comments/context/reducer/middleware.ts +++ b/src/admin/components/views/Comments/context/reducer/middleware.ts @@ -1,18 +1,12 @@ import React from 'react'; import { CommentsAction } from './actions'; -import { CommentsState } from './reducer'; -interface Store { - dispatch: React.Dispatch - state: CommentsState -} +export type Thunk = (dispatch: React.Dispatch) => void -type Thunk = (dispatch: React.Dispatch) => void - -export const thunk = (store: Store) => (next: React.Dispatch) => (action: CommentsAction | Thunk) => { +export const thunkMiddleware = (dispatch: React.Dispatch) => (action: CommentsAction | Thunk) => { if (typeof action === 'function') { - return action(store.dispatch); + return action(dispatch); } - return next(action); + return dispatch(action); }; diff --git a/src/admin/components/views/Comments/context/reducer/reducer.ts b/src/admin/components/views/Comments/context/reducer/reducer.ts index 2127178d376..13913d69a49 100644 --- a/src/admin/components/views/Comments/context/reducer/reducer.ts +++ b/src/admin/components/views/Comments/context/reducer/reducer.ts @@ -10,38 +10,65 @@ export interface CommentsState { isEditing: boolean } +export const initCommentsState: CommentsState = { + comments: [], + selectedRange: null, + selectedField: null, + text: null, + isEditing: false, +}; + export const commentsReducer = (state: CommentsState, action: CommentsAction): CommentsState => { switch (action.type) { case 'OPEN_COMMENT': - return ({ + return { ...state, selectedRange: action.range, selectedField: action.field, isEditing: true, - }); + }; case 'UPDATE_COMMENT': - return ({ + return { ...state, text: action.text, - }); + }; case 'CANCEL_COMMENT': - return ({ + return { ...state, selectedRange: null, selectedField: null, text: null, isEditing: false, - }); + }; case 'SUCCEED_LOAD_COMMENTS': - return ({ + return { ...state, comments: action.comments, - }); - case 'HIGHLIGHT_COMMENT': - return ({ + }; + case 'SUCCEED_SAVE_COMMENT': + return { + ...state, + selectedRange: null, + selectedField: null, + text: null, + isEditing: false, + }; + case 'UPDATE_RANGE': + return { + ...state, + selectedRange: action.range, + }; + case 'HIGHLIGHT_TEXT': + return { ...state, + selectedField: action.field, selectedRange: action.range, - }); + }; + case 'FOCUS_FIELD': + return { + ...state, + selectedField: action.field, + }; default: return { ...state }; } diff --git a/src/admin/components/views/Comments/index.scss b/src/admin/components/views/Comments/index.scss new file mode 100644 index 00000000000..cb8a9206223 --- /dev/null +++ b/src/admin/components/views/Comments/index.scss @@ -0,0 +1,31 @@ +@import '../../../scss/styles.scss'; + +.comments { + margin-top: base(.5); + padding-left: base(1.5); + padding-right: base(1); + display: flex; + flex-direction: column; + + input { + @include formInput; + margin-top: $baseline; + } + + &__tray { + display: flex; + + > * { + width: calc(50% - #{base(.5)}); + } + + > *:first-child { + margin-right: base(.5); + } + + > *:last-child { + margin-left: base(.5); + } + } + +} diff --git a/src/admin/components/views/Comments/index.tsx b/src/admin/components/views/Comments/index.tsx index 3025e67ad7c..a617cc8fc30 100644 --- a/src/admin/components/views/Comments/index.tsx +++ b/src/admin/components/views/Comments/index.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect, useCallback } from 'react'; +import React, { useRef, useEffect, useCallback } from 'react'; import { Range } from 'slate'; import { requests } from '../../../api'; import { useConfig } from '../../utilities/Config'; @@ -6,6 +6,9 @@ import CommentElement from './CommentElement'; import { useCommentsContext } from './context'; import { CommentsProp, Comment } from './types'; +import './index.scss'; +import Button from '../../elements/Button'; + const renderComment = (comment: Comment) => ( = (props) => { contentId, } = props; + const baseName = 'comments'; + const { - comments, - isEditing, - setIsEditing, - fieldName: field, + state, + dispatch, reloadComments, - range, - setCurrentRange, } = useCommentsContext(); + const inputRef = useRef(null); const { serverURL, routes: { api } } = useConfig(); @@ -55,31 +57,34 @@ const CommentsView: React.FC = (props) => { 'Content-Type': 'application/json', }, }); - }, [serverURL, api]); + dispatch({ type: 'SUCCEED_SAVE_COMMENT' }); + dispatch(reloadComments()); + }, [dispatch, serverURL, api, reloadComments]); useEffect(() => { - reloadComments(); - }, [reloadComments]); + dispatch(reloadComments()); + }, [dispatch, reloadComments]); - const [content, setContent] = useState(''); + useEffect(() => { + if (state.isEditing) { + inputRef.current.focus(); + } + }, [state.isEditing]); const resetState = () => { - setIsEditing(false); - setContent(''); + dispatch({ type: 'CANCEL_COMMENT' }); }; const handleSave = (evt) => { evt.preventDefault(); const comment = { 'content-id': contentId, - field, - 'comment-content': content, - range, + field: state.selectedField, + 'comment-content': state.text, + range: state.selectedRange, }; saveComment(comment); - reloadComments(); - resetState(); }; const handleCancel = (evt) => { @@ -87,36 +92,65 @@ const CommentsView: React.FC = (props) => { resetState(); }; + const openComment = (e) => { + e.preventDefault(); + dispatch({ + type: 'OPEN_COMMENT', + field: state.selectedField, + range: state.selectedRange, + }); + }; + return ( -
      - {comments.map(renderComment)} - {isEditing +
      +
      + {state.comments.map(renderComment)} +
      + {state.selectedRange && !state.isEditing + ? ( + + ) + : null} + {state.isEditing ? ( -
    • + setContent(e.target.value))} - onFocus={() => setCurrentRange(range)} + value={state.text ?? ''} + onChange={(e) => dispatch({ type: 'UPDATE_COMMENT', text: e.target.value })} + onFocus={() => dispatch({ type: 'UPDATE_RANGE', range: state.selectedRange })} /> -
      - - +
      -
    • + ) : null} -
    + ); }; diff --git a/src/admin/components/views/Comments/types.ts b/src/admin/components/views/Comments/types.ts index 5a4f015bfda..1ccd757b349 100644 --- a/src/admin/components/views/Comments/types.ts +++ b/src/admin/components/views/Comments/types.ts @@ -1,13 +1,13 @@ import { Range } from 'slate'; export interface Comment { - id: string, - 'comment-content': string - field: string - 'content-id': string - range: Range + id: string, + 'comment-content': string + field: string + 'content-id': string + range: Range } export type CommentsProp = { - contentId: string + contentId: string } diff --git a/src/admin/components/views/collections/Edit/Default.tsx b/src/admin/components/views/collections/Edit/Default.tsx index e4714638b53..6afbae2402e 100644 --- a/src/admin/components/views/collections/Edit/Default.tsx +++ b/src/admin/components/views/collections/Edit/Default.tsx @@ -1,4 +1,4 @@ -import React, { useCallback } from 'react'; +import React, { useCallback, useEffect } from 'react'; import { Link, useRouteMatch } from 'react-router-dom'; import format from 'date-fns/format'; import { useConfig } from '../../../utilities/Config'; @@ -32,6 +32,7 @@ import { getNextStage } from '../../../utilities/Workflow'; import './index.scss'; import { post } from '../../../../../workflows/baseFields'; import CommentsView from '../../Comments'; +import { useCommentsContext } from '../../Comments/context'; const baseClass = 'collection-edit'; @@ -39,6 +40,7 @@ const DefaultEditView: React.FC = (props) => { const { params: { id } = {} } = useRouteMatch>(); const { admin: { dateFormat }, routes: { admin } } = useConfig(); const { publishedDoc } = useDocumentInfo(); + const { dispatch, reloadComments } = useCommentsContext(); const { collection, @@ -79,6 +81,10 @@ const DefaultEditView: React.FC = (props) => { isEditing && `${baseClass}--is-editing`, ].filter(Boolean).join(' '); + useEffect(() => { + dispatch(reloadComments()); + }, [dispatch, reloadComments]); + const operation = isEditing ? 'update' : 'create'; From b9b604ae2571ae6195f72dd3336e933ccf23583c Mon Sep 17 00:00:00 2001 From: Edward Nuno Date: Tue, 31 May 2022 14:58:27 -0700 Subject: [PATCH 3/4] Adding author and payload styling --- pal-demo/collections/Comments.ts | 4 ++++ .../views/Comments/CommentElement/index.scss | 16 ++++++++++++++-- .../views/Comments/CommentElement/index.tsx | 4 +++- src/admin/components/views/Comments/index.tsx | 4 ++++ src/admin/components/views/Comments/types.ts | 1 + 5 files changed, 26 insertions(+), 3 deletions(-) diff --git a/pal-demo/collections/Comments.ts b/pal-demo/collections/Comments.ts index 9fc65c91be2..93028c73f09 100644 --- a/pal-demo/collections/Comments.ts +++ b/pal-demo/collections/Comments.ts @@ -12,6 +12,10 @@ const Comments: CollectionConfig = { name: 'field', type: 'text', }, + { + name: 'author', + type: 'text', + }, { name: 'range', type: 'group', diff --git a/src/admin/components/views/Comments/CommentElement/index.scss b/src/admin/components/views/Comments/CommentElement/index.scss index 59275c4171b..1ff82509118 100644 --- a/src/admin/components/views/Comments/CommentElement/index.scss +++ b/src/admin/components/views/Comments/CommentElement/index.scss @@ -4,19 +4,31 @@ $hover-box-shadow: inset 0 0 0 $style-stroke-width lighten($color-dark-gray, 5%); &__card { - border: 1px solid $color-light-gray; + // 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; + // 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; + } } diff --git a/src/admin/components/views/Comments/CommentElement/index.tsx b/src/admin/components/views/Comments/CommentElement/index.tsx index d6118db83b6..bad295f12d7 100644 --- a/src/admin/components/views/Comments/CommentElement/index.tsx +++ b/src/admin/components/views/Comments/CommentElement/index.tsx @@ -3,7 +3,7 @@ import { useCommentsContext } from '../context'; import { CommentProps } from './types'; import './index.scss'; -const CommentElement: React.FC = ({ comment: { 'comment-content': content, range, field } }) => { +const CommentElement: React.FC = ({ comment: { 'comment-content': content, range, field, author } }) => { const baseName = 'comment'; const { state, dispatch } = useCommentsContext(); const highlightRange = () => { @@ -26,6 +26,8 @@ const CommentElement: React.FC = ({ comment: { 'comment-content': }} > {content} +
    + {author && ({author})} ); }; diff --git a/src/admin/components/views/Comments/index.tsx b/src/admin/components/views/Comments/index.tsx index a617cc8fc30..cc5e105d437 100644 --- a/src/admin/components/views/Comments/index.tsx +++ b/src/admin/components/views/Comments/index.tsx @@ -8,6 +8,7 @@ import { CommentsProp, Comment } from './types'; import './index.scss'; import Button from '../../elements/Button'; +import { useAuth } from '../../utilities/Auth'; const renderComment = (comment: Comment) => ( = (props) => { dispatch, reloadComments, } = useCommentsContext(); + const { user } = useAuth(); const inputRef = useRef(null); const { serverURL, routes: { api } } = useConfig(); @@ -52,6 +54,7 @@ const CommentsView: React.FC = (props) => { field: comment.field, 'comment-content': comment['comment-content'], range: slateToPayloadRange(comment.range), + author: comment.author, }), headers: { 'Content-Type': 'application/json', @@ -82,6 +85,7 @@ const CommentsView: React.FC = (props) => { field: state.selectedField, 'comment-content': state.text, range: state.selectedRange, + author: user.email, }; saveComment(comment); diff --git a/src/admin/components/views/Comments/types.ts b/src/admin/components/views/Comments/types.ts index 1ccd757b349..de92bd0de80 100644 --- a/src/admin/components/views/Comments/types.ts +++ b/src/admin/components/views/Comments/types.ts @@ -6,6 +6,7 @@ export interface Comment { field: string 'content-id': string range: Range + author: string } export type CommentsProp = { From 63b064178866ec8311ab3003ad9f6a7d091723b0 Mon Sep 17 00:00:00 2001 From: Edward Nuno Date: Fri, 15 Jul 2022 10:29:06 -0700 Subject: [PATCH 4/4] Remove console.log --- src/admin/components/forms/field-types/RichText/RichText.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/admin/components/forms/field-types/RichText/RichText.tsx b/src/admin/components/forms/field-types/RichText/RichText.tsx index 3d4f68dd27c..63a6bc00bab 100644 --- a/src/admin/components/forms/field-types/RichText/RichText.tsx +++ b/src/admin/components/forms/field-types/RichText/RichText.tsx @@ -99,7 +99,6 @@ const RichText: React.FC = (props) => { const addComment = (fieldName: string) => (e) => { e.preventDefault(); - console.log('selectedRange', state.selectedRange); dispatch({ type: 'OPEN_COMMENT', field: fieldName,