From 38c673d0577678c592b14964a6db08c5a9cf2185 Mon Sep 17 00:00:00 2001 From: Tim Down <158919+timdown@users.noreply.github.com> Date: Tue, 18 Jul 2023 09:35:04 +0100 Subject: [PATCH] Merge pull request #13720 from overleaf/td-review-panel-entry-pos React review panel entry positioning GitOrigin-RevId: c22617b1d3243b7d54b093426358aeb291421b9e --- .../review-panel/current-file-container.tsx | 60 ++- .../editor-widgets/editor-widgets.tsx | 21 +- .../entries/add-comment-entry.tsx | 29 +- .../entries/aggregate-change-entry.tsx | 28 +- .../bulk-actions-entry/bulk-actions-entry.tsx | 5 +- .../review-panel/entries/change-entry.tsx | 28 +- .../review-panel/entries/comment-entry.tsx | 42 +- .../review-panel/entries/comment.tsx | 9 +- .../review-panel/entries/entry-container.tsx | 14 +- .../components/review-panel/nav.tsx | 18 +- .../components/review-panel/overview-file.tsx | 2 + .../review-panel/positioned-entries.tsx | 409 ++++++++++++++++++ .../toolbar/resolved-comments-dropdown.tsx | 7 +- .../review-panel/toolbar/toolbar.tsx | 19 +- .../hooks/use-angular-review-panel-state.ts | 23 +- .../review-panel/types/review-panel-state.ts | 8 +- .../extensions/changes/change-manager.ts | 20 +- .../controllers/ReviewPanelController.js | 83 ++-- .../components/auto-expanding-text-area.tsx | 6 +- services/web/frontend/js/utils/debugging.ts | 7 + .../stylesheets/app/editor/review-panel.less | 27 +- services/web/types/review-panel/entry.ts | 2 +- .../web/types/review-panel/review-panel.ts | 17 +- 23 files changed, 717 insertions(+), 167 deletions(-) create mode 100644 services/web/frontend/js/features/source-editor/components/review-panel/positioned-entries.tsx create mode 100644 services/web/frontend/js/utils/debugging.ts diff --git a/services/web/frontend/js/features/source-editor/components/review-panel/current-file-container.tsx b/services/web/frontend/js/features/source-editor/components/review-panel/current-file-container.tsx index b1fdaee7e2..23ddbac84e 100644 --- a/services/web/frontend/js/features/source-editor/components/review-panel/current-file-container.tsx +++ b/services/web/frontend/js/features/source-editor/components/review-panel/current-file-container.tsx @@ -1,4 +1,4 @@ -import { useMemo } from 'react' +import { useCallback, useMemo } from 'react' import Container from './container' import Toolbar from './toolbar/toolbar' import Nav from './nav' @@ -8,13 +8,21 @@ import AggregateChangeEntry from './entries/aggregate-change-entry' import CommentEntry from './entries/comment-entry' import AddCommentEntry from './entries/add-comment-entry' import BulkActionsEntry from './entries/bulk-actions-entry/bulk-actions-entry' +import PositionedEntries from './positioned-entries' import { useReviewPanelUpdaterFnsContext, useReviewPanelValueContext, } from '../../context/review-panel/review-panel-context' import useCodeMirrorContentHeight from '../../hooks/use-codemirror-content-height' import { ReviewPanelEntry } from '../../../../../../types/review-panel/entry' -import { ThreadId } from '../../../../../../types/review-panel/review-panel' +import { + ReviewPanelDocEntries, + ThreadId, +} from '../../../../../../types/review-panel/review-panel' + +const isEntryAThreadId = ( + entry: keyof ReviewPanelDocEntries +): entry is ThreadId => entry !== 'add-comment' && entry !== 'bulk-actions' function CurrentFileContainer() { const { @@ -36,10 +44,18 @@ function CurrentFileContainer() { const objectEntries = useMemo(() => { return Object.entries(currentDocEntries || {}) as Array< - [ThreadId, ReviewPanelEntry] + [keyof ReviewPanelDocEntries, ReviewPanelEntry] > }, [currentDocEntries]) + const onMouseEnter = useCallback(() => { + setEntryHover(true) + }, [setEntryHover]) + + const onMouseLeave = useCallback(() => { + setEntryHover(false) + }, [setEntryHover]) + return (
@@ -53,9 +69,9 @@ function CurrentFileContainer() { tabIndex={0} aria-labelledby="review-panel-tab-current-file" > -
{openDocId && objectEntries.map(([id, entry]) => { @@ -63,37 +79,46 @@ function CurrentFileContainer() { return null } - if (entry.type === 'insert' || entry.type === 'delete') { + if ( + isEntryAThreadId(id) && + (entry.type === 'insert' || entry.type === 'delete') + ) { return ( ) } - if (entry.type === 'aggregate-change') { + if (isEntryAThreadId(id) && entry.type === 'aggregate-change') { return ( ) } - if (entry.type === 'comment' && !loadingThreads) { + if ( + isEntryAThreadId(id) && + entry.type === 'comment' && + !loadingThreads + ) { return ( ) } if (entry.type === 'add-comment' && permissions.comment) { - return + return } if (entry.type === 'bulk-actions') { @@ -118,6 +143,7 @@ function CurrentFileContainer() { ) @@ -125,7 +151,7 @@ function CurrentFileContainer() { return null })} -
+
) diff --git a/services/web/frontend/js/features/source-editor/components/review-panel/editor-widgets/editor-widgets.tsx b/services/web/frontend/js/features/source-editor/components/review-panel/editor-widgets/editor-widgets.tsx index bf74cf285c..0c6f4ede8f 100644 --- a/services/web/frontend/js/features/source-editor/components/review-panel/editor-widgets/editor-widgets.tsx +++ b/services/web/frontend/js/features/source-editor/components/review-panel/editor-widgets/editor-widgets.tsx @@ -12,8 +12,6 @@ import { useCodeMirrorViewContext } from '../../codemirror-editor' import Modal, { useBulkActionsModal } from '../entries/bulk-actions-entry/modal' import getMeta from '../../../../../utils/meta' import useScopeValue from '../../../../../shared/hooks/use-scope-value' -import { MergeAndOverride } from '../../../../../../../types/utils' -import { ReviewPanelBulkActionsEntry } from '../../../../../../../types/review-panel/entry' function EditorWidgets() { const { t } = useTranslation() @@ -32,30 +30,13 @@ function EditorWidgets() { ) const view = useCodeMirrorViewContext() - type UseReviewPanelValueContextReturnType = ReturnType< - typeof useReviewPanelValueContext - > const { entries, openDocId, nVisibleSelectedChanges: nChanges, wantTrackChanges, permissions, - // Remapping entries as they may contain `add-comment` and `bulk-actions` props along with DocIds - // Ideally the `add-comment` and `bulk-actions` objects should not be within the entries object - // as the doc data, but this is what currently angular returns. - } = useReviewPanelValueContext() as MergeAndOverride< - UseReviewPanelValueContextReturnType, - { - entries: { - // eslint-disable-next-line no-use-before-define - [Entry in UseReviewPanelValueContextReturnType['entries'] as keyof Entry]: Entry & { - 'add-comment': ReviewPanelBulkActionsEntry - 'bulk-actions': ReviewPanelBulkActionsEntry - } - } - } - > + } = useReviewPanelValueContext() const hasTrackChangesFeature = getMeta('ol-hasTrackChangesFeature') diff --git a/services/web/frontend/js/features/source-editor/components/review-panel/entries/add-comment-entry.tsx b/services/web/frontend/js/features/source-editor/components/review-panel/entries/add-comment-entry.tsx index 0e0033038c..02d80a96df 100644 --- a/services/web/frontend/js/features/source-editor/components/review-panel/entries/add-comment-entry.tsx +++ b/services/web/frontend/js/features/source-editor/components/review-panel/entries/add-comment-entry.tsx @@ -1,5 +1,5 @@ import { useTranslation } from 'react-i18next' -import { useState } from 'react' +import { useEffect, useState } from 'react' import EntryContainer from './entry-container' import EntryCallout from './entry-callout' import EntryActions from './entry-actions' @@ -14,34 +14,41 @@ import classnames from 'classnames' import { ReviewPanelAddCommentEntry } from '../../../../../../../types/review-panel/entry' type AddCommentEntryProps = { - entry: ReviewPanelAddCommentEntry + entryId: ReviewPanelAddCommentEntry['type'] } -function AddCommentEntry({ entry }: AddCommentEntryProps) { +function AddCommentEntry({ entryId }: AddCommentEntryProps) { const { t } = useTranslation() - const { isAddingComment, submitNewComment, handleLayoutChange } = - useReviewPanelValueContext() - const { setIsAddingComment } = useReviewPanelUpdaterFnsContext() + const { isAddingComment, submitNewComment } = useReviewPanelValueContext() + const { setIsAddingComment, handleLayoutChange } = + useReviewPanelUpdaterFnsContext() const [content, setContent] = useState('') const handleStartNewComment = () => { setIsAddingComment(true) - handleLayoutChange() + window.setTimeout(handleLayoutChange, 0) } const handleSubmitNewComment = () => { submitNewComment(content) setIsAddingComment(false) setContent('') + window.setTimeout(handleLayoutChange, 0) } const handleCancelNewComment = () => { setIsAddingComment(false) setContent('') - handleLayoutChange() + window.setTimeout(handleLayoutChange, 0) } + useEffect(() => { + return () => { + setIsAddingComment(false) + } + }, [setIsAddingComment]) + const handleCommentKeyPress = ( e: React.KeyboardEvent ) => { @@ -62,16 +69,12 @@ function AddCommentEntry({ entry }: AddCommentEntryProps) { } return ( - +
{isAddingComment ? ( <> diff --git a/services/web/frontend/js/features/source-editor/components/review-panel/entries/aggregate-change-entry.tsx b/services/web/frontend/js/features/source-editor/components/review-panel/entries/aggregate-change-entry.tsx index a3643e559b..cbf458987e 100644 --- a/services/web/frontend/js/features/source-editor/components/review-panel/entries/aggregate-change-entry.tsx +++ b/services/web/frontend/js/features/source-editor/components/review-panel/entries/aggregate-change-entry.tsx @@ -4,19 +4,24 @@ import EntryContainer from './entry-container' import EntryCallout from './entry-callout' import EntryActions from './entry-actions' import Icon from '../../../../../shared/components/icon' -import { useReviewPanelValueContext } from '../../../context/review-panel/review-panel-context' +import { + useReviewPanelUpdaterFnsContext, + useReviewPanelValueContext, +} from '../../../context/review-panel/review-panel-context' import { formatTime } from '../../../../utils/format-date' import classnames from 'classnames' import { ReviewPanelAggregateChangeEntry } from '../../../../../../../types/review-panel/entry' import { ReviewPanelPermissions, ReviewPanelUser, + ThreadId, } from '../../../../../../../types/review-panel/review-panel' import { DocId } from '../../../../../../../types/project-settings' type AggregateChangeEntryProps = { docId: DocId entry: ReviewPanelAggregateChangeEntry + entryId: ThreadId permissions: ReviewPanelPermissions user: ReviewPanelUser | undefined contentLimit?: number @@ -28,6 +33,7 @@ type AggregateChangeEntryProps = { function AggregateChangeEntry({ docId, entry, + entryId, permissions, user, contentLimit = 17, @@ -36,8 +42,9 @@ function AggregateChangeEntry({ onIndicatorClick, }: AggregateChangeEntryProps) { const { t } = useTranslation() - const { acceptChanges, rejectChanges, handleLayoutChange, gotoEntry } = + const { acceptChanges, rejectChanges, gotoEntry } = useReviewPanelValueContext() + const { handleLayoutChange } = useReviewPanelUpdaterFnsContext() const [isDeletionCollapsed, setIsDeletionCollapsed] = useState(true) const [isInsertionCollapsed, setIsInsertionCollapsed] = useState(true) @@ -82,26 +89,17 @@ function AggregateChangeEntry({ return ( - + {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions */}
@@ -110,10 +108,6 @@ function AggregateChangeEntry({ className={classnames('rp-entry', 'rp-entry-aggregate', { 'rp-entry-focused': entry.focused, })} - style={{ - top: entry.screenPos ? entry.screenPos.y + 'px' : undefined, - visibility: entry.visible ? 'visible' : 'hidden', - }} >
diff --git a/services/web/frontend/js/features/source-editor/components/review-panel/entries/bulk-actions-entry/bulk-actions-entry.tsx b/services/web/frontend/js/features/source-editor/components/review-panel/entries/bulk-actions-entry/bulk-actions-entry.tsx index 71261bba8b..302df036bb 100644 --- a/services/web/frontend/js/features/source-editor/components/review-panel/entries/bulk-actions-entry/bulk-actions-entry.tsx +++ b/services/web/frontend/js/features/source-editor/components/review-panel/entries/bulk-actions-entry/bulk-actions-entry.tsx @@ -8,10 +8,11 @@ import { ReviewPanelBulkActionsEntry } from '../../../../../../../../types/revie type BulkActionsEntryProps = { entry: ReviewPanelBulkActionsEntry + entryId: ReviewPanelBulkActionsEntry['type'] nChanges: number } -function BulkActionsEntry({ entry, nChanges }: BulkActionsEntryProps) { +function BulkActionsEntry({ entry, entryId, nChanges }: BulkActionsEntryProps) { const { t } = useTranslation() const { show, @@ -24,7 +25,7 @@ function BulkActionsEntry({ entry, nChanges }: BulkActionsEntryProps) { return ( <> - + {nChanges > 1 && ( <> - + {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */}
{entry.type === 'insert' ? ( @@ -104,10 +102,6 @@ function ChangeEntry({ className={classnames('rp-entry', `rp-entry-${entry.type}`, { 'rp-entry-focused': entry.focused, })} - style={{ - top: entry.screenPos ? entry.screenPos.y + 'px' : undefined, - visibility: entry.visible ? 'visible' : 'hidden', - }} >
diff --git a/services/web/frontend/js/features/source-editor/components/review-panel/entries/comment-entry.tsx b/services/web/frontend/js/features/source-editor/components/review-panel/entries/comment-entry.tsx index 91fb9165ee..5d06dc5766 100644 --- a/services/web/frontend/js/features/source-editor/components/review-panel/entries/comment-entry.tsx +++ b/services/web/frontend/js/features/source-editor/components/review-panel/entries/comment-entry.tsx @@ -1,4 +1,4 @@ -import { useState, useRef } from 'react' +import { useState, useRef, useEffect } from 'react' import { useTranslation } from 'react-i18next' import EntryContainer from './entry-container' import EntryCallout from './entry-callout' @@ -8,7 +8,10 @@ import AutoExpandingTextArea, { resetHeight, } from '../../../../../shared/components/auto-expanding-text-area' import Icon from '../../../../../shared/components/icon' -import { useReviewPanelValueContext } from '../../../context/review-panel/review-panel-context' +import { + useReviewPanelUpdaterFnsContext, + useReviewPanelValueContext, +} from '../../../context/review-panel/review-panel-context' import classnames from 'classnames' import { ReviewPanelCommentEntry } from '../../../../../../../types/review-panel/entry' import { @@ -40,8 +43,9 @@ function CommentEntry({ onIndicatorClick, }: CommentEntryProps) { const { t } = useTranslation() - const { gotoEntry, resolveComment, submitReply, handleLayoutChange } = + const { gotoEntry, resolveComment, submitReply } = useReviewPanelValueContext() + const { handleLayoutChange } = useReviewPanelUpdaterFnsContext() const [replyContent, setReplyContent] = useState('') const [animating, setAnimating] = useState(false) const [resolved, setResolved] = useState(false) @@ -103,12 +107,24 @@ function CommentEntry({ } } + const submitting = Boolean(thread?.submitting) + + // Update the layout when loading finishes + useEffect(() => { + if (!submitting) { + // Ensure everything is rendered in the DOM before updating the layout. + // Having to use a timeout seems less than ideal. + window.setTimeout(handleLayoutChange, 0) + } + }, [submitting, handleLayoutChange]) + if (!thread || resolved) { return null } return ( - + {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */}
@@ -144,13 +150,9 @@ function CommentEntry({ 'rp-entry-focused': entry.focused, 'rp-entry-comment-resolving': animating, })} - style={{ - top: entry.screenPos ? `${entry.screenPos.y}px` : undefined, - visibility: entry.visible ? 'visible' : 'hidden', - }} ref={entryDivRef} > - {!thread.submitting && (!thread || thread.messages.length === 0) && ( + {!submitting && (!thread || thread.messages.length === 0) && (
{t('no_comments')}
)}
@@ -163,7 +165,7 @@ function CommentEntry({ /> ))}
- {thread.submitting && ( + {submitting && (
diff --git a/services/web/frontend/js/features/source-editor/components/review-panel/entries/comment.tsx b/services/web/frontend/js/features/source-editor/components/review-panel/entries/comment.tsx index b847f5614b..ac3a8538bc 100644 --- a/services/web/frontend/js/features/source-editor/components/review-panel/entries/comment.tsx +++ b/services/web/frontend/js/features/source-editor/components/review-panel/entries/comment.tsx @@ -2,7 +2,10 @@ import { useTranslation } from 'react-i18next' import { useState } from 'react' import AutoExpandingTextArea from '../../../../../shared/components/auto-expanding-text-area' import { formatTime } from '../../../../utils/format-date' -import { useReviewPanelValueContext } from '../../../context/review-panel/review-panel-context' +import { + useReviewPanelUpdaterFnsContext, + useReviewPanelValueContext, +} from '../../../context/review-panel/review-panel-context' import { ReviewPanelCommentThread } from '../../../../../../../types/review-panel/comment-thread' import { ReviewPanelCommentThreadMessage, @@ -17,8 +20,8 @@ type CommentProps = { function Comment({ thread, threadId, comment }: CommentProps) { const { t } = useTranslation() - const { deleteComment, handleLayoutChange, saveEdit } = - useReviewPanelValueContext() + const { deleteComment, saveEdit } = useReviewPanelValueContext() + const { handleLayoutChange } = useReviewPanelUpdaterFnsContext() const [deleting, setDeleting] = useState(false) const [editing, setEditing] = useState(false) diff --git a/services/web/frontend/js/features/source-editor/components/review-panel/entries/entry-container.tsx b/services/web/frontend/js/features/source-editor/components/review-panel/entries/entry-container.tsx index 0d5b801d76..3140302aab 100644 --- a/services/web/frontend/js/features/source-editor/components/review-panel/entries/entry-container.tsx +++ b/services/web/frontend/js/features/source-editor/components/review-panel/entries/entry-container.tsx @@ -1,7 +1,17 @@ import classnames from 'classnames' -function EntryContainer({ className, ...rest }: React.ComponentProps<'div'>) { - return
+function EntryContainer({ + id, + className, + ...rest +}: React.ComponentProps<'div'>) { + return ( +
+ ) } export default EntryContainer diff --git a/services/web/frontend/js/features/source-editor/components/review-panel/nav.tsx b/services/web/frontend/js/features/source-editor/components/review-panel/nav.tsx index bd267dcec9..1062a1a36b 100644 --- a/services/web/frontend/js/features/source-editor/components/review-panel/nav.tsx +++ b/services/web/frontend/js/features/source-editor/components/review-panel/nav.tsx @@ -6,14 +6,28 @@ import { useReviewPanelUpdaterFnsContext, } from '../../context/review-panel/review-panel-context' import { isCurrentFileView, isOverviewView } from '../../utils/sub-view' +import { useCallback } from 'react' +import { useResizeObserver } from '../../../../shared/hooks/use-resize-observer' function Nav() { const { t } = useTranslation() const { subView } = useReviewPanelValueContext() - const { handleSetSubview } = useReviewPanelUpdaterFnsContext() + const { handleSetSubview, setNavHeight } = useReviewPanelUpdaterFnsContext() + const handleResize = useCallback( + el => { + // Use requestAnimationFrame to prevent errors like "ResizeObserver loop + // completed with undelivered notifications" that occur if onResize does + // something complicated. The cost of this is that onResize lags one frame + // behind, but it's unlikely to matter. + const height = el.offsetHeight + window.requestAnimationFrame(() => setNavHeight(height)) + }, + [setNavHeight] + ) + const resizeRef = useResizeObserver(handleResize) return ( -
+