Merge pull request #13641 from overleaf/ii-review-panel-migration-overview-view

[web] Create overview view shell for review panel

GitOrigin-RevId: 21cc8a744253aec53f089ec6e6c1221d7153f3a4
This commit is contained in:
ilkin-overleaf 2023-07-04 14:05:12 +03:00 committed by Copybot
parent 87013d20f8
commit 936ab81b65
18 changed files with 274 additions and 141 deletions

View file

@ -1,6 +1,3 @@
import Toggler from './toggler'
import Toolbar from './toolbar/toolbar'
import Nav from './nav'
import classnames from 'classnames'
const reviewPanelClasses = ['ol-cm-review-panel']
@ -18,11 +15,6 @@ function Container({ children, classNames, ...rest }: ContainerProps) {
{...rest}
data-testid="review-panel"
>
<div className="review-panel-tools">
<Toolbar />
<Nav />
</div>
<Toggler />
{children}
</div>
)

View file

@ -1,4 +1,8 @@
import { useMemo } from 'react'
import Container from './container'
import Toolbar from './toolbar/toolbar'
import Nav from './nav'
import Toggler from './toggler'
import ChangeEntry from './entries/change-entry'
import AggregateChangeEntry from './entries/aggregate-change-entry'
import CommentEntry from './entries/comment-entry'
@ -26,54 +30,62 @@ function CurrentFileContainer() {
}, [currentDocEntries])
return (
<div
id="review-panel-current-file"
role="tabpanel"
tabIndex={0}
aria-labelledby="review-panel-tab-current-file"
>
<div
className="rp-entry-list-inner"
style={{ height: `${contentHeight}px` }}
>
{openDocId &&
objectEntries.map(([id, entry]) => {
if (!entry.visible) {
return null
}
if (entry.type === 'insert' || entry.type === 'delete') {
return <ChangeEntry key={id} />
}
if (entry.type === 'aggregate-change') {
return <AggregateChangeEntry key={id} />
}
if (entry.type === 'comment' && !loadingThreads) {
return (
<CommentEntry
key={id}
docId={openDocId}
entry={entry}
entryId={id}
threads={commentThreads}
/>
)
}
if (entry.type === 'add-comment' && permissions.comment) {
return <AddCommentEntry key={id} />
}
if (entry.type === 'bulk-actions') {
return <BulkActionsEntry key={id} />
}
return null
})}
<Container>
<div className="review-panel-tools">
<Toolbar />
<Nav />
</div>
</div>
<Toggler />
<div
id="review-panel-current-file"
role="tabpanel"
tabIndex={0}
aria-labelledby="review-panel-tab-current-file"
>
<div
className="rp-entry-list-inner"
style={{ height: `${contentHeight}px` }}
>
{openDocId &&
objectEntries.map(([id, entry]) => {
if (!entry.visible) {
return null
}
if (entry.type === 'insert' || entry.type === 'delete') {
return <ChangeEntry key={id} />
}
if (entry.type === 'aggregate-change') {
return <AggregateChangeEntry key={id} />
}
if (entry.type === 'comment' && !loadingThreads) {
return (
<CommentEntry
key={id}
docId={openDocId}
entry={entry}
entryId={id}
permissions={permissions}
threads={commentThreads}
/>
)
}
if (entry.type === 'add-comment' && permissions.comment) {
return <AddCommentEntry key={id} />
}
if (entry.type === 'bulk-actions') {
return <BulkActionsEntry key={id} />
}
return null
})}
</div>
</div>
</Container>
)
}

View file

@ -14,22 +14,29 @@ import {
import classnames from 'classnames'
import { ReviewPanelCommentEntry } from '../../../../../../../types/review-panel/entry'
import {
DocId,
ReviewPanelCommentThreads,
ReviewPanelPermissions,
ThreadId,
} from '../../../../../../../types/review-panel/review-panel'
import { DocId } from '../../../../../../../types/project-settings'
type CommentEntryProps = {
docId: DocId
entry: ReviewPanelCommentEntry
entryId: ThreadId
permissions: ReviewPanelPermissions
threads: ReviewPanelCommentThreads
}
function CommentEntry({ docId, entry, entryId, threads }: CommentEntryProps) {
function CommentEntry({
docId,
entry,
entryId,
permissions,
threads,
}: CommentEntryProps) {
const { t } = useTranslation()
const {
permissions,
gotoEntry,
toggleReviewPanel,
resolveComment,

View file

@ -0,0 +1,26 @@
import { useEffect } from 'react'
function useCollapseHeight(
elRef: React.MutableRefObject<HTMLElement | null>,
shouldCollapse: boolean
) {
useEffect(() => {
if (elRef.current) {
const neededHeight = elRef.current.scrollHeight
if (neededHeight > 0) {
const height = shouldCollapse ? 0 : neededHeight
// This might result in a too big height if the element has css prop of
// `box-sizing` set to `content-box`. To fix that, values of props such as
// box-sizing, padding and border could be extracted from `height` to compensate.
elRef.current.style.height = `${height}px`
} else {
if (shouldCollapse) {
elRef.current.style.height = '0'
}
}
}
}, [elRef, shouldCollapse])
}
export default useCollapseHeight

View file

@ -1,41 +1,41 @@
import Container from './container'
import Toggler from './toggler'
import Toolbar from './toolbar/toolbar'
import Nav from './nav'
import Icon from '../../../../shared/components/icon'
import OverviewFile from './overview-file'
import { useReviewPanelValueContext } from '../../context/review-panel/review-panel-context'
function OverviewContainer() {
const { loading, docs } = useReviewPanelValueContext()
return (
<div
className="rp-entry-list"
id="review-panel-overview"
role="tabpanel"
tabIndex={0}
aria-labelledby="review-panel-tab-overview"
>
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod
tempor incididunt ut labore et dolore magna aliqua. Et malesuada fames ac
turpis egestas integer eget aliquet nibh. Et leo duis ut diam quam nulla
porttitor massa id. Risus quis varius quam quisque id diam vel quam
elementum. Nibh venenatis cras sed felis. Sit amet commodo nulla facilisi
nullam vehicula ipsum a arcu. Dui ut ornare lectus sit amet est placerat
in. Aliquam ultrices sagittis orci a. Leo a diam sollicitudin tempor id eu
nisl nunc mi. Quis ipsum suspendisse ultrices gravida dictum fusce. Ut
etiam sit amet nisl purus in mollis nunc sed. Rhoncus est pellentesque
elit ullamcorper dignissim cras. Faucibus turpis in eu mi bibendum. Proin
libero nunc consequat interdum. Ac placerat vestibulum lectus mauris
ultrices eros in cursus turpis. Ac felis donec et odio. Nullam ac tortor
vitae purus faucibus. Consectetur lorem donec massa sapien faucibus et
molestie. Praesent elementum facilisis leo vel fringilla est ullamcorper
eget nulla. Adipiscing vitae proin sagittis nisl rhoncus mattis rhoncus
urna. Cursus metus aliquam eleifend mi in nulla posuere sollicitudin
aliquam. Eget nullam non nisi est sit amet facilisis magna. Donec
adipiscing tristique risus nec feugiat in fermentum posuere. Gravida
rutrum quisque non tellus orci ac auctor augue. Euismod in pellentesque
massa placerat duis ultricies lacus. Pellentesque diam volutpat commodo
sed egestas. Tempus iaculis urna id volutpat lacus laoreet. Lorem ipsum
dolor sit amet consectetur. Tincidunt id aliquet risus feugiat in ante
metus. Risus ultricies tristique nulla aliquet enim tortor at auctor urna.
Purus in mollis nunc sed. In ante metus dictum at. Magna eget est lorem
ipsum dolor sit. Fusce id velit ut tortor pretium viverra. Augue neque
gravida in fermentum et sollicitudin ac. Et malesuada fames ac turpis.
Felis bibendum ut tristique et egestas quis ipsum suspendisse ultrices.
Varius vel pharetra vel turpis nunc eget.
</div>
<Container>
<Toggler />
<Toolbar />
<div
className="rp-entry-list"
id="review-panel-overview"
role="tabpanel"
tabIndex={0}
aria-labelledby="review-panel-tab-overview"
>
{loading ? (
<div className="rp-loading">
<Icon type="spinner" spin />
</div>
) : (
docs?.map(doc => (
<OverviewFile
key={doc.doc.id}
docId={doc.doc.id}
docPath={doc.path}
/>
))
)}
</div>
<Nav />
</Container>
)
}

View file

@ -0,0 +1,109 @@
import { useMemo, useRef } from 'react'
import Icon from '../../../../shared/components/icon'
import ChangeEntry from './entries/change-entry'
import AggregateChangeEntry from './entries/aggregate-change-entry'
import CommentEntry from './entries/comment-entry'
import {
useReviewPanelUpdaterFnsContext,
useReviewPanelValueContext,
} from '../../context/review-panel/review-panel-context'
import classnames from 'classnames'
import { ThreadId } from '../../../../../../types/review-panel/review-panel'
import { MainDocument } from '../../../../../../types/project-settings'
import { ReviewPanelEntry } from '../../../../../../types/review-panel/entry'
import useCollapseHeight from './hooks/use-collapse-height'
type OverviewFileProps = {
docId: MainDocument['doc']['id']
docPath: MainDocument['path']
}
function OverviewFile({ docId, docPath }: OverviewFileProps) {
const { entries, collapsed, commentThreads, permissions } =
useReviewPanelValueContext()
const { setCollapsed } = useReviewPanelUpdaterFnsContext()
const docCollapsed = collapsed[docId]
const docEntries = useMemo(
() => (docId in entries ? entries[docId] : {}),
[docId, entries]
)
const objectEntries = useMemo(() => {
const entries = Object.entries(docEntries) as Array<
[ThreadId, ReviewPanelEntry]
>
const orderedEntries = entries.sort(([, entryA], [, entryB]) => {
return entryA.offset - entryB.offset
})
return orderedEntries
}, [docEntries])
const entryCount = Object.keys(docEntries).length
const handleToggleCollapsed = () => {
setCollapsed({ ...collapsed, [docId]: !docCollapsed })
}
const entriesContainerRef = useRef<HTMLDivElement | null>(null)
useCollapseHeight(entriesContainerRef, docCollapsed)
return (
<div className="rp-overview-file">
{entryCount > 0 && (
// eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions
<div
className="rp-overview-file-header"
onClick={handleToggleCollapsed}
>
<span
className={classnames('rp-overview-file-header-collapse', {
'rp-overview-file-header-collapse-on': docCollapsed,
})}
>
<Icon type="angle-down" />
</span>
{docPath}
{docCollapsed && (
<>
&nbsp;
<span className="rp-overview-file-num-entries">
({entryCount})
</span>
</>
)}
</div>
)}
<div className="rp-overview-file-entries" ref={entriesContainerRef}>
{objectEntries.map(([id, entry]) => {
if (entry.type === 'insert' || entry.type === 'delete') {
return <ChangeEntry key={id} />
}
if (entry.type === 'aggregate-change') {
return <AggregateChangeEntry key={id} />
}
if (entry.type === 'comment') {
if (!commentThreads[entry.thread_id]?.resolved) {
return (
<CommentEntry
key={id}
docId={docId}
entry={entry}
entryId={id}
permissions={permissions}
threads={commentThreads}
/>
)
}
}
return null
})}
</div>
</div>
)
}
export default OverviewFile

View file

@ -1,5 +1,4 @@
import ReactDOM from 'react-dom'
import Container from './container'
import CurrentFileContainer from './current-file-container'
import OverviewContainer from './overview-container'
import { useCodeMirrorViewContext } from '../codemirror-editor'
@ -19,13 +18,9 @@ function ReviewPanelView({ parentDomNode }: ReviewPanelViewProps) {
return ReactDOM.createPortal(
<>
{isCurrentFileView(subView) ? (
<Container>
<CurrentFileContainer />
</Container>
<CurrentFileContainer />
) : (
<Container>
<OverviewContainer />
</Container>
<OverviewContainer />
)}
</>,
parentDomNode

View file

@ -6,11 +6,11 @@ import ResolvedCommentsScroller from './resolved-comments-scroller'
import classnames from 'classnames'
import { useReviewPanelValueContext } from '../../../context/review-panel/review-panel-context'
import {
DocId,
ReviewPanelDocEntries,
ThreadId,
} from '../../../../../../../types/review-panel/review-panel'
import { ReviewPanelResolvedCommentThread } from '../../../../../../../types/review-panel/comment-thread'
import { DocId } from '../../../../../../../types/project-settings'
export interface FilteredResolvedComments
extends ReviewPanelResolvedCommentThread {

View file

@ -1,4 +1,4 @@
import { useEffect, useRef } from 'react'
import { useRef } from 'react'
import { Trans, useTranslation } from 'react-i18next'
import Tooltip from '../../../../../shared/components/tooltip'
import Icon from '../../../../../shared/components/icon'
@ -9,6 +9,7 @@ import {
useReviewPanelValueContext,
} from '../../../context/review-panel/review-panel-context'
import classnames from 'classnames'
import useCollapseHeight from '../hooks/use-collapse-height'
function ToggleMenu() {
const { t } = useTranslation()
@ -29,21 +30,7 @@ function ToggleMenu() {
} = useReviewPanelValueContext()
const containerRef = useRef<HTMLUListElement | null>(null)
useEffect(() => {
if (containerRef.current) {
const neededHeight = containerRef.current.scrollHeight
if (neededHeight > 0) {
const height = shouldCollapse ? 0 : neededHeight
containerRef.current.style.height = `${height}px`
} else {
if (shouldCollapse) {
containerRef.current.style.height = '0'
}
}
}
}, [shouldCollapse])
useCollapseHeight(containerRef, shouldCollapse)
return (
<>

View file

@ -13,6 +13,9 @@ function useAngularReviewPanelState(): ReviewPanelState {
const [subView, setSubView] = useScopeValue<ReviewPanel.Value<'subView'>>(
'reviewPanel.subView'
)
const [loading] = useScopeValue<ReviewPanel.Value<'loading'>>(
'reviewPanel.overview.loading'
)
const [collapsed, setCollapsed] = useScopeValue<
ReviewPanel.Value<'collapsed'>
>('reviewPanel.overview.docsCollapsedState')
@ -130,6 +133,7 @@ function useAngularReviewPanelState(): ReviewPanelState {
submitReply,
subView,
wantTrackChanges,
loading,
openDocId,
toggleTrackChangesForEveryone,
toggleTrackChangesForUser,
@ -162,6 +166,7 @@ function useAngularReviewPanelState(): ReviewPanelState {
submitReply,
subView,
wantTrackChanges,
loading,
openDocId,
toggleTrackChangesForEveryone,
toggleTrackChangesForUser,

View file

@ -1,6 +1,5 @@
import {
CommentId,
DocId,
ReviewPanelCommentThreads,
ReviewPanelEntries,
ReviewPanelPermissions,
@ -8,11 +7,14 @@ import {
ThreadId,
} from '../../../../../../../types/review-panel/review-panel'
import { ReviewPanelCommentEntry } from '../../../../../../../types/review-panel/entry'
import { MainDocument } from '../../../../../../../types/project-settings'
import {
DocId,
MainDocument,
} from '../../../../../../../types/project-settings'
export interface ReviewPanelState {
values: {
collapsed: Record<string, boolean>
collapsed: Record<DocId, boolean>
commentThreads: ReviewPanelCommentThreads
deleteComment: (threadId: ThreadId, commentId: CommentId) => void
docs: MainDocument[] | undefined
@ -33,6 +35,7 @@ export interface ReviewPanelState {
submitReply: (entry: ReviewPanelCommentEntry, replyContent: string) => void
subView: SubView
wantTrackChanges: boolean
loading: boolean
openDocId: DocId | null
toggleTrackChangesForEveryone: (isOn: boolean) => unknown
toggleTrackChangesForUser: (isOn: boolean, memberId: string) => unknown

View file

@ -1260,6 +1260,7 @@ button when (@is-overleaf-light = true) {
top: 0;
display: flex;
flex-direction: column;
height: 100%;
}
.rp-overview-file {
@ -1267,20 +1268,6 @@ button when (@is-overleaf-light = true) {
//height: auto;
transition: height ease-in-out 0.15s; //, display 0.15s 0s;
}
.rp-overview-file-num-entries {
display: none;
}
&.rp-overview-file-collapse {
.rp-overview-file-num-entries {
display: inline;
}
.rp-overview-file-entries {
// height: 0;
}
}
}
.rp-nav-item {

View file

@ -352,7 +352,7 @@ describe('<EditorLeftMenu />', function () {
id: 'id1',
type: 'doc',
selected: false,
},
} as MainDocument['doc'],
},
{
path: 'main2.tex',
@ -361,7 +361,7 @@ describe('<EditorLeftMenu />', function () {
id: 'id2',
type: 'doc',
selected: false,
},
} as MainDocument['doc'],
},
]

View file

@ -17,7 +17,7 @@ describe('<SettingsDocument />', function () {
id: '123abc',
type: 'doc',
selected: false,
},
} as MainDocument['doc'],
},
]

View file

@ -192,4 +192,12 @@ describe('<ReviewPanel />', function () {
// eslint-disable-next-line mocha/no-skipped-tests
it.skip('resolves comment', function () {})
})
describe('overview mode', function () {
// eslint-disable-next-line mocha/no-skipped-tests
it.skip('shows list of files changed', function () {})
// eslint-disable-next-line mocha/no-skipped-tests
it.skip('renders comments', function () {})
})
})

View file

@ -1,14 +1,16 @@
import { OverallTheme } from '../frontend/js/features/source-editor/extensions/theme'
import { Brand } from './helpers/brand'
export type AllowedImageName = {
imageDesc: string
imageName: string
}
export type DocId = Brand<string, 'DocId'>
export type MainDocument = {
doc: {
name: string
id: string
id: DocId
type: string
selected: boolean
}

View file

@ -8,6 +8,7 @@ interface ReviewPanelEntryScreenPos {
interface ReviewPanelBaseEntry {
visible: boolean
offset: number
}
export interface ReviewPanelCommentEntry extends ReviewPanelBaseEntry {
@ -15,7 +16,6 @@ export interface ReviewPanelCommentEntry extends ReviewPanelBaseEntry {
content: string
entry_ids: string[]
focused: boolean
offset: number
screenPos: ReviewPanelEntryScreenPos
thread_id: ThreadId
replyContent?: string // angular specific

View file

@ -1,4 +1,5 @@
import { Brand } from '../helpers/brand'
import { DocId } from '../project-settings'
import { ReviewPanelEntry } from './entry'
import { ReviewPanelCommentThread } from './comment-thread'
@ -14,7 +15,6 @@ export interface ReviewPanelPermissions {
export type ThreadId = Brand<string, 'ThreadId'>
export type ReviewPanelDocEntries = Record<ThreadId, ReviewPanelEntry>
export type DocId = Brand<string, 'DocId'>
export type ReviewPanelEntries = Record<DocId, ReviewPanelDocEntries>
export type UserId = Brand<string, 'UserId'>