From b1f246b9ba64ec8ef7cc82649af8ad6a8d497d79 Mon Sep 17 00:00:00 2001 From: Davinder Singh Date: Wed, 5 Jul 2023 15:07:36 +0100 Subject: [PATCH] Merge pull request #13658 from overleaf/ii-review-panel-migration-change-entry [web] Create change entries GitOrigin-RevId: 4d49f8b02b9bfcfe470f6db21f5347080ff47562 --- .../web/frontend/extracted-translations.json | 4 + .../review-panel/current-file-container.tsx | 33 +++- .../review-panel/entries/change-entry.tsx | 174 +++++++++++++++++- .../review-panel/entries/comment-entry.tsx | 55 +++--- .../review-panel/entries/entry-container.tsx | 8 +- .../components/review-panel/overview-file.tsx | 12 +- .../hooks/use-angular-review-panel-state.ts | 11 ++ .../review-panel/types/review-panel-state.ts | 4 + .../review-panel/review-panel.spec.tsx | 14 ++ services/web/types/review-panel/entry.ts | 24 ++- .../web/types/review-panel/review-panel.ts | 1 + 11 files changed, 295 insertions(+), 45 deletions(-) diff --git a/services/web/frontend/extracted-translations.json b/services/web/frontend/extracted-translations.json index 8a61400235..bdc0d57913 100644 --- a/services/web/frontend/extracted-translations.json +++ b/services/web/frontend/extracted-translations.json @@ -13,6 +13,7 @@ "about_to_enable_managed_users": "", "about_to_leave_projects": "", "about_to_trash_projects": "", + "accept": "", "accepted_invite": "", "access_denied": "", "account_has_been_link_to_institution_account": "", @@ -790,6 +791,7 @@ "refresh_page_after_starting_free_trial": "", "refreshing": "", "regards": "", + "reject": "", "relink_your_account": "", "remote_service_error": "", "remove": "", @@ -1031,6 +1033,8 @@ "track_changes_for_x": "", "track_changes_is_off": "", "track_changes_is_on": "", + "tracked_change_added": "", + "tracked_change_deleted": "", "trash": "", "trash_projects": "", "trashed": "", 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 bfb01124ba..c2de44f5a7 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 @@ -8,14 +8,25 @@ 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' -import { useReviewPanelValueContext } from '../../context/review-panel/review-panel-context' +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' function CurrentFileContainer() { - const { commentThreads, entries, openDocId, permissions, loadingThreads } = - useReviewPanelValueContext() + const { + commentThreads, + entries, + openDocId, + permissions, + loadingThreads, + users, + toggleReviewPanel, + } = useReviewPanelValueContext() + const { setEntryHover } = useReviewPanelUpdaterFnsContext() const contentHeight = useCodeMirrorContentHeight() console.log('Review panel got content height', contentHeight) @@ -53,7 +64,18 @@ function CurrentFileContainer() { } if (entry.type === 'insert' || entry.type === 'delete') { - return + return ( + + ) } if (entry.type === 'aggregate-change') { @@ -69,6 +91,9 @@ function CurrentFileContainer() { entryId={id} permissions={permissions} threads={commentThreads} + onMouseEnter={setEntryHover.bind(null, true)} + onMouseLeave={setEntryHover.bind(null, false)} + onIndicatorClick={toggleReviewPanel} /> ) } diff --git a/services/web/frontend/js/features/source-editor/components/review-panel/entries/change-entry.tsx b/services/web/frontend/js/features/source-editor/components/review-panel/entries/change-entry.tsx index 17bf349112..25f8f993d6 100644 --- a/services/web/frontend/js/features/source-editor/components/review-panel/entries/change-entry.tsx +++ b/services/web/frontend/js/features/source-editor/components/review-panel/entries/change-entry.tsx @@ -1,7 +1,177 @@ +import { useTranslation } from 'react-i18next' +import { useState } from 'react' import EntryContainer from './entry-container' +import EntryActions from './entry-actions' +import Icon from '../../../../../shared/components/icon' +import { useReviewPanelValueContext } from '../../../context/review-panel/review-panel-context' +import { formatTime } from '../../../../utils/format-date' +import classnames from 'classnames' +import { + ReviewPanelDeleteEntry, + ReviewPanelInsertEntry, +} from '../../../../../../../types/review-panel/entry' +import { + ReviewPanelPermissions, + ReviewPanelUser, +} from '../../../../../../../types/review-panel/review-panel' +import { DocId } from '../../../../../../../types/project-settings' -function ChangeEntry() { - return Change entry +type ChangeEntryProps = { + docId: DocId + entry: ReviewPanelInsertEntry | ReviewPanelDeleteEntry + permissions: ReviewPanelPermissions + user: ReviewPanelUser | undefined + contentLimit?: number + onMouseEnter?: () => void + onMouseLeave?: () => void + onIndicatorClick?: () => void +} + +function ChangeEntry({ + docId, + entry, + permissions, + user, + contentLimit = 40, + onMouseEnter, + onMouseLeave, + onIndicatorClick, +}: ChangeEntryProps) { + const { t } = useTranslation() + const { acceptChanges, rejectChanges, handleLayoutChange, gotoEntry } = + useReviewPanelValueContext() + const [isCollapsed, setIsCollapsed] = useState(false) + + const content = isCollapsed + ? entry.content.substring(0, contentLimit) + : entry.content + + const needsCollapsing = entry.content.length > contentLimit + + const handleEntryClick = (e: React.MouseEvent) => { + const target = e.target as Element + + for (const selector of [ + '.rp-entry', + '.rp-entry-description', + '.rp-entry-body', + '.rp-entry-action-icon i', + ]) { + if (target.matches(selector)) { + gotoEntry(docId, entry.offset) + break + } + } + } + + const handleToggleCollapse = () => { + setIsCollapsed(value => !value) + handleLayoutChange() + } + + return ( + +
+ {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */} +
+ {entry.type === 'insert' ? ( + + ) : ( + + )} +
+
+
+
+ {entry.type === 'insert' ? ( + + ) : ( + + )} +
+
+
+ + {entry.type === 'insert' ? ( + <> + {t('tracked_change_added')}  + {content} + + ) : ( + <> + {t('tracked_change_deleted')}  + {content} + + )} + {needsCollapsing && ( + + )} + +
+
+ {formatTime(entry.metadata.ts, 'MMM d, y h:mm a')} +  •  + {user && ( + + {user.name ?? t('anonymous')} + + )} +
+
+
+ {permissions.write && ( + + rejectChanges(entry.entry_ids)}> + {t('reject')} + + acceptChanges(entry.entry_ids)}> + {t('accept')} + + + )} +
+ + ) } export default ChangeEntry 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 9871b37c3b..fa277f0085 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 @@ -7,10 +7,7 @@ import AutoExpandingTextArea, { resetHeight, } from '../../../../../shared/components/auto-expanding-text-area' import Icon from '../../../../../shared/components/icon' -import { - useReviewPanelUpdaterFnsContext, - useReviewPanelValueContext, -} from '../../../context/review-panel/review-panel-context' +import { useReviewPanelValueContext } from '../../../context/review-panel/review-panel-context' import classnames from 'classnames' import { ReviewPanelCommentEntry } from '../../../../../../../types/review-panel/entry' import { @@ -26,6 +23,9 @@ type CommentEntryProps = { entryId: ThreadId permissions: ReviewPanelPermissions threads: ReviewPanelCommentThreads + onMouseEnter?: () => void + onMouseLeave?: () => void + onIndicatorClick?: () => void } function CommentEntry({ @@ -34,17 +34,13 @@ function CommentEntry({ entryId, permissions, threads, + onMouseEnter, + onMouseLeave, + onIndicatorClick, }: CommentEntryProps) { const { t } = useTranslation() - const { - gotoEntry, - toggleReviewPanel, - resolveComment, - submitReply, - handleLayoutChange, - } = useReviewPanelValueContext() - const { setEntryHover } = useReviewPanelUpdaterFnsContext() - + const { gotoEntry, resolveComment, submitReply, handleLayoutChange } = + useReviewPanelValueContext() const [replyContent, setReplyContent] = useState('') const [animating, setAnimating] = useState(false) const [resolved, setResolved] = useState(false) @@ -55,16 +51,18 @@ function CommentEntry({ const handleEntryClick = (e: React.MouseEvent) => { const target = e.target as Element - if ( - [ - 'rp-entry', - 'rp-comment-loaded', - 'rp-comment-content', - 'rp-comment-reply', - 'rp-entry-metadata', - ].some(className => [...target.classList].includes(className)) - ) { - gotoEntry(docId, entry.offset) + + for (const selector of [ + '.rp-entry', + '.rp-comment-loaded', + '.rp-comment-content', + '.rp-comment-reply', + '.rp-entry-metadata', + ]) { + if (target.matches(selector)) { + gotoEntry(docId, entry.offset) + break + } } } @@ -109,15 +107,16 @@ function CommentEntry({ } return ( - + {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */}
setEntryHover(true)} - onMouseLeave={() => setEntryHover(false)} - onClick={handleEntryClick} >
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 8c51136143..0d5b801d76 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,9 +1,7 @@ -type EntryContainerProps = { - children: React.ReactNode -} +import classnames from 'classnames' -function EntryContainer({ children }: EntryContainerProps) { - return
{children}
+function EntryContainer({ className, ...rest }: React.ComponentProps<'div'>) { + return
} export default EntryContainer diff --git a/services/web/frontend/js/features/source-editor/components/review-panel/overview-file.tsx b/services/web/frontend/js/features/source-editor/components/review-panel/overview-file.tsx index 1349c931d3..08533f09ad 100644 --- a/services/web/frontend/js/features/source-editor/components/review-panel/overview-file.tsx +++ b/services/web/frontend/js/features/source-editor/components/review-panel/overview-file.tsx @@ -19,7 +19,7 @@ type OverviewFileProps = { } function OverviewFile({ docId, docPath }: OverviewFileProps) { - const { entries, collapsed, commentThreads, permissions } = + const { entries, collapsed, commentThreads, permissions, users } = useReviewPanelValueContext() const { setCollapsed } = useReviewPanelUpdaterFnsContext() @@ -77,7 +77,15 @@ function OverviewFile({ docId, docPath }: OverviewFileProps) {
{objectEntries.map(([id, entry]) => { if (entry.type === 'insert' || entry.type === 'delete') { - return + return ( + + ) } if (entry.type === 'aggregate-change') { diff --git a/services/web/frontend/js/features/source-editor/context/review-panel/hooks/use-angular-review-panel-state.ts b/services/web/frontend/js/features/source-editor/context/review-panel/hooks/use-angular-review-panel-state.ts index afb46ac797..82566600dd 100644 --- a/services/web/frontend/js/features/source-editor/context/review-panel/hooks/use-angular-review-panel-state.ts +++ b/services/web/frontend/js/features/source-editor/context/review-panel/hooks/use-angular-review-panel-state.ts @@ -33,6 +33,7 @@ function useAngularReviewPanelState(): ReviewPanelState { const [permissions] = useScopeValue>('permissions') + const [users] = useScopeValue>('users', true) const [resolvedComments] = useScopeValue< ReviewPanel.Value<'resolvedComments'> >('reviewPanel.resolvedComments', true) @@ -90,6 +91,10 @@ function useAngularReviewPanelState(): ReviewPanelState { const [refreshResolvedCommentsDropdown] = useScopeValue< ReviewPanel.Value<'refreshResolvedCommentsDropdown'> >('refreshResolvedCommentsDropdown') + const [acceptChanges] = + useScopeValue>('acceptChanges') + const [rejectChanges] = + useScopeValue>('rejectChanges') const handleSetSubview = useCallback( (subView: SubView) => { @@ -126,6 +131,7 @@ function useAngularReviewPanelState(): ReviewPanelState { handleLayoutChange, loadingThreads, permissions, + users, resolveComment, resolvedComments, saveEdit, @@ -147,6 +153,8 @@ function useAngularReviewPanelState(): ReviewPanelState { unresolveComment, deleteThread, refreshResolvedCommentsDropdown, + acceptChanges, + rejectChanges, }), [ collapsed, @@ -159,6 +167,7 @@ function useAngularReviewPanelState(): ReviewPanelState { handleLayoutChange, loadingThreads, permissions, + users, resolveComment, resolvedComments, saveEdit, @@ -180,6 +189,8 @@ function useAngularReviewPanelState(): ReviewPanelState { unresolveComment, deleteThread, refreshResolvedCommentsDropdown, + acceptChanges, + rejectChanges, ] ) diff --git a/services/web/frontend/js/features/source-editor/context/review-panel/types/review-panel-state.ts b/services/web/frontend/js/features/source-editor/context/review-panel/types/review-panel-state.ts index d62e4e0152..d022ef625e 100644 --- a/services/web/frontend/js/features/source-editor/context/review-panel/types/review-panel-state.ts +++ b/services/web/frontend/js/features/source-editor/context/review-panel/types/review-panel-state.ts @@ -3,6 +3,7 @@ import { ReviewPanelCommentThreads, ReviewPanelEntries, ReviewPanelPermissions, + ReviewPanelUsers, SubView, ThreadId, } from '../../../../../../../types/review-panel/review-panel' @@ -24,6 +25,7 @@ export interface ReviewPanelState { handleLayoutChange: () => void loadingThreads: boolean permissions: ReviewPanelPermissions + users: ReviewPanelUsers resolveComment: (docId: DocId, entryId: ThreadId) => void resolvedComments: ReviewPanelEntries saveEdit: ( @@ -55,6 +57,8 @@ export interface ReviewPanelState { unresolveComment: (threadId: ThreadId) => void deleteThread: (_entryId: unknown, docId: DocId, threadId: ThreadId) => void refreshResolvedCommentsDropdown: () => Promise + acceptChanges: (entryIds: unknown) => void + rejectChanges: (entryIds: unknown) => void } updaterFns: { handleSetSubview: (subView: SubView) => void diff --git a/services/web/test/frontend/features/review-panel/review-panel.spec.tsx b/services/web/test/frontend/features/review-panel/review-panel.spec.tsx index 767c0055e2..1c822086bc 100644 --- a/services/web/test/frontend/features/review-panel/review-panel.spec.tsx +++ b/services/web/test/frontend/features/review-panel/review-panel.spec.tsx @@ -193,6 +193,20 @@ describe('', function () { it.skip('resolves comment', function () {}) }) + describe('change entries', function () { + // eslint-disable-next-line mocha/no-skipped-tests + it.skip('renders inserted entries', function () {}) + + // eslint-disable-next-line mocha/no-skipped-tests + it.skip('renders deleted entries', function () {}) + + // eslint-disable-next-line mocha/no-skipped-tests + it.skip('accepts change', function () {}) + + // eslint-disable-next-line mocha/no-skipped-tests + it.skip('rejects change', function () {}) + }) + describe('overview mode', function () { // eslint-disable-next-line mocha/no-skipped-tests it.skip('shows list of files changed', function () {}) diff --git a/services/web/types/review-panel/entry.ts b/services/web/types/review-panel/entry.ts index 055604c80b..2f2a5fb45a 100644 --- a/services/web/types/review-panel/entry.ts +++ b/services/web/types/review-panel/entry.ts @@ -1,4 +1,4 @@ -import { ThreadId } from './review-panel' +import { ThreadId, UserId } from './review-panel' interface ReviewPanelEntryScreenPos { y: number @@ -14,19 +14,35 @@ interface ReviewPanelBaseEntry { export interface ReviewPanelCommentEntry extends ReviewPanelBaseEntry { type: 'comment' content: string - entry_ids: string[] + entry_ids: ThreadId[] focused: boolean screenPos: ReviewPanelEntryScreenPos thread_id: ThreadId replyContent?: string // angular specific } -interface ReviewPanelInsertEntry extends ReviewPanelBaseEntry { +export interface ReviewPanelInsertEntry extends ReviewPanelBaseEntry { type: 'insert' + content: string + entry_ids: ThreadId[] + metadata: { + ts: Date + user_id: UserId + } + screenPos: ReviewPanelEntryScreenPos + focused?: boolean } -interface ReviewPanelDeleteEntry extends ReviewPanelBaseEntry { +export interface ReviewPanelDeleteEntry extends ReviewPanelBaseEntry { type: 'delete' + content: string + entry_ids: ThreadId[] + metadata: { + ts: Date + user_id: UserId + } + screenPos: ReviewPanelEntryScreenPos + focused?: boolean } interface ReviewPanelAggregateChangeEntry extends ReviewPanelBaseEntry { diff --git a/services/web/types/review-panel/review-panel.ts b/services/web/types/review-panel/review-panel.ts index 0a14870bfa..c03b48ae0c 100644 --- a/services/web/types/review-panel/review-panel.ts +++ b/services/web/types/review-panel/review-panel.ts @@ -27,6 +27,7 @@ export interface ReviewPanelUser { isSelf: boolean name: string } +export type ReviewPanelUsers = Record export type CommentId = Brand