mirror of
https://github.com/overleaf/overleaf.git
synced 2024-10-17 21:05:04 -04:00
Merge pull request #20008 from overleaf/ae-review-panel-empty-state
Improve calculations of empty state, mini state and sizes variables in review panel GitOrigin-RevId: 41bcb3b67c9f0019c11b4de0e4590b0407e04e66
This commit is contained in:
parent
989c48978a
commit
e61eb1b220
11 changed files with 341 additions and 245 deletions
|
@ -4,7 +4,6 @@ import {
|
||||||
useCodeMirrorViewContext,
|
useCodeMirrorViewContext,
|
||||||
} from '@/features/source-editor/components/codemirror-editor'
|
} from '@/features/source-editor/components/codemirror-editor'
|
||||||
import { EditorSelection } from '@codemirror/state'
|
import { EditorSelection } from '@codemirror/state'
|
||||||
import { PANEL_WIDTH } from './review-panel'
|
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { useThreadsActionsContext } from '../context/threads-context'
|
import { useThreadsActionsContext } from '../context/threads-context'
|
||||||
|
|
||||||
|
@ -49,7 +48,7 @@ export const ReviewPanelAddComment: FC = () => {
|
||||||
return (
|
return (
|
||||||
<form
|
<form
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
style={{ display: 'flex', flexDirection: 'column', width: PANEL_WIDTH }}
|
className="review-panel-add-comment-form"
|
||||||
ref={handleElement}
|
ref={handleElement}
|
||||||
>
|
>
|
||||||
{/* eslint-disable-next-line jsx-a11y/no-autofocus */}
|
{/* eslint-disable-next-line jsx-a11y/no-autofocus */}
|
||||||
|
|
|
@ -4,19 +4,30 @@ import { memo } from 'react'
|
||||||
import ReviewPanel from './review-panel'
|
import ReviewPanel from './review-panel'
|
||||||
import { useLayoutContext } from '@/shared/context/layout-context'
|
import { useLayoutContext } from '@/shared/context/layout-context'
|
||||||
import { useRangesContext } from '../context/ranges-context'
|
import { useRangesContext } from '../context/ranges-context'
|
||||||
|
import { useThreadsContext } from '@/features/review-panel-new/context/threads-context'
|
||||||
|
import { hasActiveRange } from '@/features/review-panel-new/utils/has-active-range'
|
||||||
|
|
||||||
function ReviewPanelContainer() {
|
function ReviewPanelContainer() {
|
||||||
const view = useCodeMirrorViewContext()
|
const view = useCodeMirrorViewContext()
|
||||||
const ranges = useRangesContext()
|
const ranges = useRangesContext()
|
||||||
|
const threads = useThreadsContext()
|
||||||
const { reviewPanelOpen } = useLayoutContext()
|
const { reviewPanelOpen } = useLayoutContext()
|
||||||
|
|
||||||
const mini = !reviewPanelOpen && !!ranges?.total
|
if (!view) {
|
||||||
|
|
||||||
if (!view || (!reviewPanelOpen && !mini)) {
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
return ReactDOM.createPortal(<ReviewPanel mini={mini} />, view.scrollDOM)
|
// the full-width review panel
|
||||||
|
if (reviewPanelOpen) {
|
||||||
|
return ReactDOM.createPortal(<ReviewPanel />, view.scrollDOM)
|
||||||
|
}
|
||||||
|
|
||||||
|
// the mini review panel
|
||||||
|
if (hasActiveRange(ranges, threads)) {
|
||||||
|
return ReactDOM.createPortal(<ReviewPanel mini />, view.scrollDOM)
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
export default memo(ReviewPanelContainer)
|
export default memo(ReviewPanelContainer)
|
||||||
|
|
|
@ -16,7 +16,6 @@ import {
|
||||||
DeleteOperation,
|
DeleteOperation,
|
||||||
EditOperation,
|
EditOperation,
|
||||||
} from '../../../../../types/change'
|
} from '../../../../../types/change'
|
||||||
import { editorVerticalTopPadding } from '@/features/source-editor/extensions/vertical-overflow'
|
|
||||||
import {
|
import {
|
||||||
useCodeMirrorStateContext,
|
useCodeMirrorStateContext,
|
||||||
useCodeMirrorViewContext,
|
useCodeMirrorViewContext,
|
||||||
|
@ -27,18 +26,14 @@ import { isDeleteChange, isInsertChange } from '@/utils/operations'
|
||||||
import Icon from '@/shared/components/icon'
|
import Icon from '@/shared/components/icon'
|
||||||
import { positionItems } from '../utils/position-items'
|
import { positionItems } from '../utils/position-items'
|
||||||
import { canAggregate } from '../utils/can-aggregate'
|
import { canAggregate } from '../utils/can-aggregate'
|
||||||
import { isInViewport } from '../utils/is-in-viewport'
|
|
||||||
import ReviewPanelEmptyState from './review-panel-empty-state'
|
import ReviewPanelEmptyState from './review-panel-empty-state'
|
||||||
import useEventListener from '@/shared/hooks/use-event-listener'
|
import useEventListener from '@/shared/hooks/use-event-listener'
|
||||||
|
import { hasActiveRange } from '@/features/review-panel-new/utils/has-active-range'
|
||||||
|
|
||||||
type Positions = Map<string, number>
|
type AggregatedRanges = {
|
||||||
type Aggregates = Map<string, Change<DeleteOperation>>
|
|
||||||
|
|
||||||
type RangesWithPositions = {
|
|
||||||
changes: Change<EditOperation>[]
|
changes: Change<EditOperation>[]
|
||||||
comments: Change<CommentOperation>[]
|
comments: Change<CommentOperation>[]
|
||||||
positions: Positions
|
aggregates: Map<string, Change<DeleteOperation>>
|
||||||
aggregates: Aggregates
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const ReviewPanelCurrentFile: FC = () => {
|
const ReviewPanelCurrentFile: FC = () => {
|
||||||
|
@ -47,24 +42,7 @@ const ReviewPanelCurrentFile: FC = () => {
|
||||||
const threads = useThreadsContext()
|
const threads = useThreadsContext()
|
||||||
const state = useCodeMirrorStateContext()
|
const state = useCodeMirrorStateContext()
|
||||||
|
|
||||||
const [rangesWithPositions, setRangesWithPositions] =
|
const [aggregatedRanges, setAggregatedRanges] = useState<AggregatedRanges>()
|
||||||
useState<RangesWithPositions>()
|
|
||||||
|
|
||||||
const contentRect = view.contentDOM.getBoundingClientRect()
|
|
||||||
|
|
||||||
const editorPaddingTop = editorVerticalTopPadding(view)
|
|
||||||
const topDiff = contentRect.top - editorPaddingTop
|
|
||||||
const docLength = state.doc.length
|
|
||||||
|
|
||||||
const screenPosition = useCallback(
|
|
||||||
(change: Change): number | undefined => {
|
|
||||||
const pos = Math.min(change.op.p, docLength)
|
|
||||||
const coords = view.coordsAtPos(pos)
|
|
||||||
|
|
||||||
return coords ? Math.round(coords.top - topDiff) : undefined
|
|
||||||
},
|
|
||||||
[docLength, topDiff, view]
|
|
||||||
)
|
|
||||||
|
|
||||||
const selectionCoords = useMemo(
|
const selectionCoords = useMemo(
|
||||||
() =>
|
() =>
|
||||||
|
@ -85,7 +63,7 @@ const ReviewPanelCurrentFile: FC = () => {
|
||||||
)
|
)
|
||||||
|
|
||||||
if (extents) {
|
if (extents) {
|
||||||
previousFocusedItem.current = extents.focusedItemIndex
|
previousFocusedItem.current = extents.activeItemIndex
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
@ -124,62 +102,84 @@ const ReviewPanelCurrentFile: FC = () => {
|
||||||
}
|
}
|
||||||
}, [updatePositions])
|
}, [updatePositions])
|
||||||
|
|
||||||
const buildEntries = useCallback(() => {
|
const buildAggregatedRanges = useCallback(() => {
|
||||||
if (ranges) {
|
if (ranges) {
|
||||||
|
const output: AggregatedRanges = {
|
||||||
|
aggregates: new Map(),
|
||||||
|
changes: [],
|
||||||
|
comments: [],
|
||||||
|
}
|
||||||
|
|
||||||
|
let precedingChange: Change<EditOperation> | null = null
|
||||||
|
|
||||||
|
for (const change of ranges.changes) {
|
||||||
|
if (
|
||||||
|
precedingChange &&
|
||||||
|
isInsertChange(precedingChange) &&
|
||||||
|
isDeleteChange(change) &&
|
||||||
|
canAggregate(change, precedingChange)
|
||||||
|
) {
|
||||||
|
output.aggregates.set(precedingChange.id, change)
|
||||||
|
} else {
|
||||||
|
output.changes.push(change)
|
||||||
|
}
|
||||||
|
|
||||||
|
precedingChange = change
|
||||||
|
}
|
||||||
|
|
||||||
|
if (threads) {
|
||||||
|
for (const comment of ranges.comments) {
|
||||||
|
if (!threads[comment.op.t]?.resolved) {
|
||||||
|
output.comments.push(comment)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setAggregatedRanges(output)
|
||||||
|
}
|
||||||
|
}, [threads, ranges])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
buildAggregatedRanges()
|
||||||
|
}, [buildAggregatedRanges])
|
||||||
|
|
||||||
|
useEventListener('editor:viewport-changed', buildAggregatedRanges)
|
||||||
|
|
||||||
|
const [positions, setPositions] = useState<Map<string, number>>(new Map())
|
||||||
|
|
||||||
|
const positionsRef = useRef<Map<string, number>>(new Map())
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (aggregatedRanges) {
|
||||||
view.requestMeasure({
|
view.requestMeasure({
|
||||||
key: 'review-panel-position',
|
key: 'review-panel-position',
|
||||||
read(view): RangesWithPositions {
|
read(view) {
|
||||||
const isVisible = isInViewport(view)
|
const contentRect = view.contentDOM.getBoundingClientRect()
|
||||||
|
const docLength = view.state.doc.length
|
||||||
|
|
||||||
const output: RangesWithPositions = {
|
const screenPosition = (change: Change): number | undefined => {
|
||||||
positions: new Map(),
|
const pos = Math.min(change.op.p, docLength) // TODO: needed?
|
||||||
aggregates: new Map(),
|
const coords = view.coordsAtPos(pos)
|
||||||
changes: [],
|
|
||||||
comments: [],
|
return coords ? Math.round(coords.top - contentRect.top) : undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
let precedingChange: Change<EditOperation> | null = null
|
for (const change of aggregatedRanges.changes) {
|
||||||
|
const position = screenPosition(change)
|
||||||
for (const change of ranges.changes) {
|
if (position) {
|
||||||
if (isVisible(change)) {
|
positionsRef.current.set(change.id, position)
|
||||||
if (
|
|
||||||
precedingChange &&
|
|
||||||
isInsertChange(precedingChange) &&
|
|
||||||
isDeleteChange(change) &&
|
|
||||||
canAggregate(change, precedingChange)
|
|
||||||
) {
|
|
||||||
output.aggregates.set(precedingChange.id, change)
|
|
||||||
} else {
|
|
||||||
output.changes.push(change)
|
|
||||||
|
|
||||||
const position = screenPosition(change)
|
|
||||||
if (position) {
|
|
||||||
output.positions.set(change.id, position)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
precedingChange = change
|
|
||||||
}
|
|
||||||
|
|
||||||
if (threads) {
|
|
||||||
for (const comment of ranges.comments) {
|
|
||||||
if (isVisible(comment)) {
|
|
||||||
output.comments.push(comment)
|
|
||||||
if (!threads[comment.op.t]?.resolved) {
|
|
||||||
const position = screenPosition(comment)
|
|
||||||
if (position) {
|
|
||||||
output.positions.set(comment.id, position)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return output
|
for (const comment of aggregatedRanges.comments) {
|
||||||
|
const position = screenPosition(comment)
|
||||||
|
if (position) {
|
||||||
|
positionsRef.current.set(comment.id, position)
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
write(positionedRanges) {
|
write() {
|
||||||
setRangesWithPositions(positionedRanges)
|
setPositions(positionsRef.current)
|
||||||
window.setTimeout(() => {
|
window.setTimeout(() => {
|
||||||
containerRef.current?.dispatchEvent(
|
containerRef.current?.dispatchEvent(
|
||||||
new Event('review-panel:position')
|
new Event('review-panel:position')
|
||||||
|
@ -188,28 +188,22 @@ const ReviewPanelCurrentFile: FC = () => {
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}, [screenPosition, threads, view, ranges])
|
}, [view, aggregatedRanges])
|
||||||
|
|
||||||
useEffect(() => {
|
const showEmptyState = useMemo(
|
||||||
buildEntries()
|
() => hasActiveRange(ranges, threads) === false,
|
||||||
}, [buildEntries])
|
[ranges, threads]
|
||||||
|
)
|
||||||
|
|
||||||
useEventListener('editor:viewport-changed', buildEntries)
|
if (!aggregatedRanges) {
|
||||||
|
|
||||||
if (!rangesWithPositions) {
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
const showEmptyState =
|
|
||||||
threads &&
|
|
||||||
rangesWithPositions.changes.length === 0 &&
|
|
||||||
rangesWithPositions.comments.length === 0
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={handleContainer}>
|
<div ref={handleContainer}>
|
||||||
{selectionCoords && (
|
{selectionCoords && (
|
||||||
<div
|
<div
|
||||||
className="review-panel-entry"
|
className="review-panel-entry review-panel-entry-action"
|
||||||
style={{ position: 'absolute' }}
|
style={{ position: 'absolute' }}
|
||||||
data-top={selectionCoords.top + view.scrollDOM.scrollTop - 70}
|
data-top={selectionCoords.top + view.scrollDOM.scrollTop - 70}
|
||||||
data-pos={state.selection.main.head}
|
data-pos={state.selection.main.head}
|
||||||
|
@ -225,22 +219,28 @@ const ReviewPanelCurrentFile: FC = () => {
|
||||||
|
|
||||||
{showEmptyState && <ReviewPanelEmptyState />}
|
{showEmptyState && <ReviewPanelEmptyState />}
|
||||||
|
|
||||||
{rangesWithPositions.changes.map(change => (
|
{aggregatedRanges.changes.map(
|
||||||
<ReviewPanelChange
|
change =>
|
||||||
key={change.id}
|
positions.has(change.id) && (
|
||||||
change={change}
|
<ReviewPanelChange
|
||||||
top={rangesWithPositions.positions.get(change.id)}
|
key={change.id}
|
||||||
aggregate={rangesWithPositions.aggregates.get(change.id)}
|
change={change}
|
||||||
/>
|
top={positions.get(change.id)}
|
||||||
))}
|
aggregate={aggregatedRanges.aggregates.get(change.id)}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
|
||||||
{rangesWithPositions.comments.map(comment => (
|
{aggregatedRanges.comments.map(
|
||||||
<ReviewPanelComment
|
comment =>
|
||||||
key={comment.id}
|
positions.has(comment.id) && (
|
||||||
comment={comment}
|
<ReviewPanelComment
|
||||||
top={rangesWithPositions.positions.get(comment.id)}
|
key={comment.id}
|
||||||
/>
|
comment={comment}
|
||||||
))}
|
top={positions.get(comment.id)}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,16 +6,13 @@ import { Button } from 'react-bootstrap'
|
||||||
import MaterialIcon from '@/shared/components/material-icon'
|
import MaterialIcon from '@/shared/components/material-icon'
|
||||||
import { useLayoutContext } from '@/shared/context/layout-context'
|
import { useLayoutContext } from '@/shared/context/layout-context'
|
||||||
|
|
||||||
const ReviewPanelHeader: FC<{
|
const ReviewPanelHeader: FC = () => {
|
||||||
top: number
|
|
||||||
width: number
|
|
||||||
}> = ({ top, width }) => {
|
|
||||||
const [trackChangesMenuExpanded, setTrackChangesMenuExpanded] =
|
const [trackChangesMenuExpanded, setTrackChangesMenuExpanded] =
|
||||||
useState(false)
|
useState(false)
|
||||||
const { setReviewPanelOpen } = useLayoutContext()
|
const { setReviewPanelOpen } = useLayoutContext()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="review-panel-header" style={{ top, width }}>
|
<div className="review-panel-header">
|
||||||
<div className="review-panel-heading">
|
<div className="review-panel-heading">
|
||||||
<div className="review-panel-label">Review</div>
|
<div className="review-panel-label">Review</div>
|
||||||
<Button
|
<Button
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { FC, useMemo } from 'react'
|
import { FC, Fragment, useMemo } from 'react'
|
||||||
import { useFileTreeData } from '@/shared/context/file-tree-data-context'
|
import { useFileTreeData } from '@/shared/context/file-tree-data-context'
|
||||||
import { Ranges, useRangesContext } from '../context/ranges-context'
|
import { Ranges, useRangesContext } from '../context/ranges-context'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
@ -59,14 +59,10 @@ export const ReviewPanelOverview: FC = () => {
|
||||||
const ranges = rangesForDocs.get(doc.doc.id)
|
const ranges = rangesForDocs.get(doc.doc.id)
|
||||||
return (
|
return (
|
||||||
ranges && (
|
ranges && (
|
||||||
<>
|
<Fragment key={doc.doc.id}>
|
||||||
<ReviewPanelOverviewFile
|
<ReviewPanelOverviewFile doc={doc} ranges={ranges} />
|
||||||
key={doc.doc.id}
|
|
||||||
doc={doc}
|
|
||||||
ranges={ranges}
|
|
||||||
/>
|
|
||||||
<div className="review-panel-overfile-divider" />
|
<div className="review-panel-overfile-divider" />
|
||||||
</>
|
</Fragment>
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
|
|
|
@ -1,69 +1,32 @@
|
||||||
import { FC, memo, useState } from 'react'
|
import { FC, memo, useState } from 'react'
|
||||||
import {
|
|
||||||
useCodeMirrorStateContext,
|
|
||||||
useCodeMirrorViewContext,
|
|
||||||
} from '@/features/source-editor/components/codemirror-editor'
|
|
||||||
import ReviewPanelTabs from './review-panel-tabs'
|
import ReviewPanelTabs from './review-panel-tabs'
|
||||||
import ReviewPanelHeader from './review-panel-header'
|
import ReviewPanelHeader from './review-panel-header'
|
||||||
import ReviewPanelCurrentFile from './review-panel-current-file'
|
import ReviewPanelCurrentFile from './review-panel-current-file'
|
||||||
import { ReviewPanelOverview } from './review-panel-overview'
|
import { ReviewPanelOverview } from './review-panel-overview'
|
||||||
import classnames from 'classnames'
|
import classnames from 'classnames'
|
||||||
|
import { useReviewPanelStyles } from '@/features/review-panel-new/hooks/use-review-panel-styles'
|
||||||
|
|
||||||
export type SubView = 'cur_file' | 'overview'
|
export type SubView = 'cur_file' | 'overview'
|
||||||
|
|
||||||
export const PANEL_WIDTH = 230
|
const ReviewPanel: FC<{ mini?: boolean }> = ({ mini = false }) => {
|
||||||
export const PANEL_MINI_WIDTH = 20
|
|
||||||
|
|
||||||
const ReviewPanel: FC<{ mini: boolean }> = ({ mini }) => {
|
|
||||||
const view = useCodeMirrorViewContext()
|
|
||||||
// eslint-disable-next-line no-unused-vars
|
|
||||||
const _state = useCodeMirrorStateContext() // needs to update on editor state changes
|
|
||||||
|
|
||||||
const [subView, setSubView] = useState<SubView>('cur_file')
|
const [subView, setSubView] = useState<SubView>('cur_file')
|
||||||
|
|
||||||
const contentRect = view.contentDOM.getBoundingClientRect()
|
const style = useReviewPanelStyles(mini)
|
||||||
const scrollRect = view.scrollDOM.getBoundingClientRect()
|
|
||||||
|
const className = classnames('review-panel-new', 'review-panel-container', {
|
||||||
|
'review-panel-mini': mini,
|
||||||
|
'review-panel-subview-overview': subView === 'overview',
|
||||||
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className={className} style={style}>
|
||||||
className="review-panel-container"
|
<div className="review-panel-inner">
|
||||||
style={{
|
{!mini && <ReviewPanelHeader />}
|
||||||
overflowY: subView === 'overview' ? 'hidden' : undefined,
|
|
||||||
position: subView === 'overview' ? 'sticky' : 'relative',
|
|
||||||
top: subView === 'overview' ? 0 : undefined,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className={classnames('review-panel-new', {
|
|
||||||
'review-panel-mini': mini,
|
|
||||||
})}
|
|
||||||
style={{
|
|
||||||
flexShrink: 0,
|
|
||||||
minHeight: subView === 'cur_file' ? contentRect.height : 'auto',
|
|
||||||
height: subView === 'overview' ? '100%' : undefined,
|
|
||||||
overflow: subView === 'overview' ? 'hidden' : undefined,
|
|
||||||
width: mini ? PANEL_MINI_WIDTH : PANEL_WIDTH,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{!mini && (
|
|
||||||
<ReviewPanelHeader top={scrollRect.top - 40} width={PANEL_WIDTH} />
|
|
||||||
)}
|
|
||||||
|
|
||||||
{subView === 'cur_file' && <ReviewPanelCurrentFile />}
|
{subView === 'cur_file' && <ReviewPanelCurrentFile />}
|
||||||
{subView === 'overview' && <ReviewPanelOverview />}
|
{subView === 'overview' && <ReviewPanelOverview />}
|
||||||
|
|
||||||
<div
|
<div className="review-panel-footer">
|
||||||
className="review-panel-footer"
|
|
||||||
style={{
|
|
||||||
position: 'fixed',
|
|
||||||
top: scrollRect.bottom - 66,
|
|
||||||
zIndex: 1,
|
|
||||||
background: '#fafafa',
|
|
||||||
borderTop: 'solid 1px #d9d9d9',
|
|
||||||
width: PANEL_WIDTH,
|
|
||||||
display: mini ? 'none' : 'flex',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ReviewPanelTabs subView={subView} setSubView={setSubView} />
|
<ReviewPanelTabs subView={subView} setSubView={setSubView} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -0,0 +1,54 @@
|
||||||
|
import { CSSProperties, useCallback, useEffect, useState } from 'react'
|
||||||
|
import { useCodeMirrorViewContext } from '@/features/source-editor/components/codemirror-editor'
|
||||||
|
|
||||||
|
export const useReviewPanelStyles = (mini: boolean) => {
|
||||||
|
const view = useCodeMirrorViewContext()
|
||||||
|
const [styles, setStyles] = useState<CSSProperties>()
|
||||||
|
|
||||||
|
const updateScrollDomVariables = useCallback((element: HTMLDivElement) => {
|
||||||
|
const { top, bottom } = element.getBoundingClientRect()
|
||||||
|
|
||||||
|
setStyles(value => ({
|
||||||
|
...value,
|
||||||
|
'--review-panel-top': `${top}px`,
|
||||||
|
'--review-panel-bottom': `${bottom}px`,
|
||||||
|
}))
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const updateContentDomVariables = useCallback((element: HTMLDivElement) => {
|
||||||
|
const { height } = element.getBoundingClientRect()
|
||||||
|
|
||||||
|
setStyles(value => ({
|
||||||
|
...value,
|
||||||
|
'--review-panel-height': `${height}px`,
|
||||||
|
}))
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setStyles(value => ({
|
||||||
|
...value,
|
||||||
|
'--review-panel-width': mini ? '22px' : '230px',
|
||||||
|
}))
|
||||||
|
}, [mini])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if ('ResizeObserver' in window) {
|
||||||
|
const scrollDomObserver = new window.ResizeObserver(entries =>
|
||||||
|
updateScrollDomVariables(entries[0]?.target as HTMLDivElement)
|
||||||
|
)
|
||||||
|
scrollDomObserver.observe(view.scrollDOM)
|
||||||
|
|
||||||
|
const contentDomObserver = new window.ResizeObserver(entries =>
|
||||||
|
updateContentDomVariables(entries[0]?.target as HTMLDivElement)
|
||||||
|
)
|
||||||
|
contentDomObserver.observe(view.contentDOM)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
scrollDomObserver.disconnect()
|
||||||
|
contentDomObserver.disconnect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [view, updateScrollDomVariables, updateContentDomVariables])
|
||||||
|
|
||||||
|
return styles
|
||||||
|
}
|
|
@ -0,0 +1,25 @@
|
||||||
|
import { Ranges } from '@/features/review-panel-new/context/ranges-context'
|
||||||
|
import { Threads } from '@/features/review-panel-new/context/threads-context'
|
||||||
|
|
||||||
|
export const hasActiveRange = (
|
||||||
|
ranges: Ranges | undefined,
|
||||||
|
threads: Threads | undefined
|
||||||
|
): boolean | undefined => {
|
||||||
|
if (!ranges || !threads) {
|
||||||
|
// data isn't loaded yet
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ranges.changes.length > 0) {
|
||||||
|
// at least one tracked change
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const thread of Object.values(threads)) {
|
||||||
|
if (!thread.resolved) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
|
@ -1,7 +0,0 @@
|
||||||
import { EditorView } from '@codemirror/view'
|
|
||||||
import { Change } from '../../../../../types/change'
|
|
||||||
|
|
||||||
export const isInViewport =
|
|
||||||
(view: EditorView) =>
|
|
||||||
(change: Change): boolean =>
|
|
||||||
change.op.p >= view.viewport.from && change.op.p <= view.viewport.to
|
|
|
@ -15,34 +15,44 @@ export const positionItems = debounce(
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let focusedItemIndex = items.findIndex(item =>
|
let activeItemIndex = items.findIndex(item =>
|
||||||
item.classList.contains('review-panel-entry-focused')
|
item.classList.contains('review-panel-entry-action')
|
||||||
)
|
)
|
||||||
if (focusedItemIndex === -1) {
|
|
||||||
|
if (activeItemIndex === -1) {
|
||||||
|
// if there is no action available
|
||||||
|
// check if there is a focused entry
|
||||||
|
activeItemIndex = items.findIndex(item =>
|
||||||
|
item.classList.contains('review-panel-entry-focused')
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (activeItemIndex === -1) {
|
||||||
// if entry was not focused manually
|
// if entry was not focused manually
|
||||||
// check if there is an entry in selection and use that as the focused item
|
// check if there is an entry in selection and use that as the focused item
|
||||||
focusedItemIndex = items.findIndex(item =>
|
activeItemIndex = items.findIndex(item =>
|
||||||
item.classList.contains('review-panel-entry-highlighted')
|
item.classList.contains('review-panel-entry-highlighted')
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (focusedItemIndex === -1) {
|
|
||||||
focusedItemIndex = previousFocusedItemIndex
|
if (activeItemIndex === -1) {
|
||||||
|
activeItemIndex = previousFocusedItemIndex
|
||||||
}
|
}
|
||||||
|
|
||||||
const focusedItem = items[focusedItemIndex]
|
const activeItem = items[activeItemIndex]
|
||||||
if (!focusedItem) {
|
if (!activeItem) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const focusedItemTop = getTopPosition(focusedItem, focusedItemIndex === 0)
|
const activeItemTop = getTopPosition(activeItem, activeItemIndex === 0)
|
||||||
|
|
||||||
focusedItem.style.top = `${focusedItemTop}px`
|
activeItem.style.top = `${activeItemTop}px`
|
||||||
focusedItem.style.visibility = 'visible'
|
activeItem.style.visibility = 'visible'
|
||||||
const focusedItemRect = focusedItem.getBoundingClientRect()
|
const focusedItemRect = activeItem.getBoundingClientRect()
|
||||||
|
|
||||||
// above the focused item
|
// above the active item
|
||||||
let topLimit = focusedItemTop
|
let topLimit = activeItemTop
|
||||||
for (let i = focusedItemIndex - 1; i >= 0; i--) {
|
for (let i = activeItemIndex - 1; i >= 0; i--) {
|
||||||
const item = items[i]
|
const item = items[i]
|
||||||
const rect = item.getBoundingClientRect()
|
const rect = item.getBoundingClientRect()
|
||||||
let top = getTopPosition(item, i === 0)
|
let top = getTopPosition(item, i === 0)
|
||||||
|
@ -55,9 +65,9 @@ export const positionItems = debounce(
|
||||||
topLimit = top
|
topLimit = top
|
||||||
}
|
}
|
||||||
|
|
||||||
// below the focused item
|
// below the active item
|
||||||
let bottomLimit = focusedItemTop + focusedItemRect.height
|
let bottomLimit = activeItemTop + focusedItemRect.height
|
||||||
for (let i = focusedItemIndex + 1; i < items.length; i++) {
|
for (let i = activeItemIndex + 1; i < items.length; i++) {
|
||||||
const item = items[i]
|
const item = items[i]
|
||||||
const rect = item.getBoundingClientRect()
|
const rect = item.getBoundingClientRect()
|
||||||
let top = getTopPosition(item, false)
|
let top = getTopPosition(item, false)
|
||||||
|
@ -70,7 +80,7 @@ export const positionItems = debounce(
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
focusedItemIndex,
|
activeItemIndex,
|
||||||
min: topLimit,
|
min: topLimit,
|
||||||
max: bottomLimit,
|
max: bottomLimit,
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,17 +1,22 @@
|
||||||
.review-panel-container {
|
|
||||||
height: 100%;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.review-panel-new {
|
.review-panel-new {
|
||||||
z-index: 6;
|
&.review-panel-container {
|
||||||
flex-shrink: 0;
|
height: 100%;
|
||||||
background-color: @neutral-10;
|
flex-shrink: 0;
|
||||||
border-left: solid 0 @neutral-20;
|
position: relative;
|
||||||
font-family: @font-family-base;
|
}
|
||||||
line-height: @line-height-base;
|
|
||||||
font-size: @font-size-01;
|
.review-panel-inner {
|
||||||
box-sizing: content-box;
|
z-index: 6;
|
||||||
|
flex-shrink: 0;
|
||||||
|
background-color: @neutral-10;
|
||||||
|
border-left: solid 0 @neutral-20;
|
||||||
|
font-family: @font-family-base;
|
||||||
|
line-height: @line-height-base;
|
||||||
|
font-size: @font-size-01;
|
||||||
|
box-sizing: content-box;
|
||||||
|
width: var(--review-panel-width);
|
||||||
|
min-height: var(--review-panel-height);
|
||||||
|
}
|
||||||
|
|
||||||
.review-panel-entry {
|
.review-panel-entry {
|
||||||
background-color: white;
|
background-color: white;
|
||||||
|
@ -38,6 +43,7 @@
|
||||||
margin-left: @spacing-01;
|
margin-left: @spacing-01;
|
||||||
border: 1px solid @blue-50;
|
border: 1px solid @blue-50;
|
||||||
}
|
}
|
||||||
|
|
||||||
.review-panel-entry-header {
|
.review-panel-entry-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
@ -46,9 +52,11 @@
|
||||||
color: @blue;
|
color: @blue;
|
||||||
font-size: 110%;
|
font-size: 110%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.review-panel-entry-time {
|
.review-panel-entry-time {
|
||||||
color: @content-secondary;
|
color: @content-secondary;
|
||||||
}
|
}
|
||||||
|
|
||||||
.review-panel-entry-actions {
|
.review-panel-entry-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
@ -73,16 +81,19 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.review-panel-change-body {
|
.review-panel-change-body {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
color: @content-secondary;
|
color: @content-secondary;
|
||||||
gap: @spacing-02;
|
gap: @spacing-02;
|
||||||
}
|
}
|
||||||
|
|
||||||
.review-panel-content-highlight {
|
.review-panel-content-highlight {
|
||||||
color: @content-primary;
|
color: @content-primary;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
del.review-panel-content-highlight {
|
del.review-panel-content-highlight {
|
||||||
text-decoration: line-through;
|
text-decoration: line-through;
|
||||||
}
|
}
|
||||||
|
@ -92,14 +103,17 @@
|
||||||
padding: @spacing-02;
|
padding: @spacing-02;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.review-panel-entry-icon-accept {
|
.review-panel-entry-icon-accept {
|
||||||
background-color: @green-10;
|
background-color: @green-10;
|
||||||
color: @green-50;
|
color: @green-50;
|
||||||
}
|
}
|
||||||
|
|
||||||
.review-panel-entry-icon-reject {
|
.review-panel-entry-icon-reject {
|
||||||
background-color: @red-10;
|
background-color: @red-10;
|
||||||
color: @red-50;
|
color: @red-50;
|
||||||
}
|
}
|
||||||
|
|
||||||
.review-panel-entry-icon-changed {
|
.review-panel-entry-icon-changed {
|
||||||
background-color: @neutral-20;
|
background-color: @neutral-20;
|
||||||
color: @content-secondary;
|
color: @content-secondary;
|
||||||
|
@ -107,7 +121,8 @@
|
||||||
|
|
||||||
.review-panel-header {
|
.review-panel-header {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: calc(var(--review-panel-top) - 40px);
|
||||||
|
width: var(--review-panel-width);
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
@ -291,19 +306,22 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: @spacing-04;
|
gap: @spacing-04;
|
||||||
}
|
}
|
||||||
|
|
||||||
.review-panel-comment {
|
.review-panel-comment {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.review-panel-comment-reply-divider {
|
.review-panel-comment-reply-divider {
|
||||||
border-left: 2px solid @yellow-20;
|
border-left: 2px solid @yellow-20;
|
||||||
}
|
}
|
||||||
|
|
||||||
.review-panel-comment-body {
|
.review-panel-comment-body {
|
||||||
font-size: @font-size-02;
|
font-size: @font-size-02;
|
||||||
color: @content-primary;
|
color: @content-primary;
|
||||||
overflow-wrap: anywhere;
|
overflow-wrap: anywhere;
|
||||||
}
|
}
|
||||||
|
|
||||||
.review-panel-content-expandable {
|
.review-panel-content-expandable {
|
||||||
display: -webkit-box;
|
display: -webkit-box;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
-webkit-box-orient: vertical;
|
-webkit-box-orient: vertical;
|
||||||
|
@ -311,6 +329,7 @@
|
||||||
line-clamp: 3;
|
line-clamp: 3;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.review-panel-content-expanded {
|
.review-panel-content-expanded {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
@ -330,6 +349,12 @@
|
||||||
max-height: 400px;
|
max-height: 400px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.review-panel-add-comment-form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: var(--review-panel-width);
|
||||||
|
}
|
||||||
|
|
||||||
.review-panel-empty-state {
|
.review-panel-empty-state {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
|
@ -419,46 +444,69 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.review-panel-footer {
|
.review-panel-footer {
|
||||||
|
position: fixed;
|
||||||
|
top: calc(var(--review-panel-bottom) - 66px);
|
||||||
|
width: var(--review-panel-width);
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
}
|
background: @rp-bg-dim-blue;
|
||||||
}
|
border-top: 1px solid #d9d9d9;
|
||||||
|
|
||||||
.review-panel-new.review-panel-mini {
|
|
||||||
width: 22px !important;
|
|
||||||
overflow: visible !important;
|
|
||||||
|
|
||||||
.review-panel-entry {
|
|
||||||
margin-left: 2px;
|
|
||||||
background-color: transparent;
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.review-panel-entry-indicator {
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
top: 7px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
width: 16px;
|
|
||||||
height: 16px;
|
|
||||||
color: @content-secondary;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.review-panel-entry-content {
|
&.review-panel-subview-overview {
|
||||||
display: none;
|
&.review-panel-container {
|
||||||
background: white;
|
overflow-y: hidden;
|
||||||
border: 1px solid @rp-border-grey;
|
position: sticky;
|
||||||
border-radius: @border-radius-base-new;
|
|
||||||
width: 200px;
|
|
||||||
padding: @spacing-02;
|
|
||||||
}
|
|
||||||
|
|
||||||
.review-panel-entry:hover {
|
|
||||||
.review-panel-entry-content {
|
|
||||||
display: initial;
|
|
||||||
position: absolute;
|
|
||||||
left: -200px;
|
|
||||||
top: 0;
|
top: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.review-panel-inner {
|
||||||
|
min-height: auto;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.review-panel-mini {
|
||||||
|
overflow: visible !important;
|
||||||
|
|
||||||
|
.review-panel-entry {
|
||||||
|
margin-left: 2px;
|
||||||
|
background-color: transparent;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.review-panel-entry-indicator {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 7px;
|
||||||
|
display: flex;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
color: @content-secondary;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.review-panel-entry-content {
|
||||||
|
display: none;
|
||||||
|
background: white;
|
||||||
|
border: 1px solid @rp-border-grey;
|
||||||
|
border-radius: @border-radius-base-new;
|
||||||
|
width: 200px;
|
||||||
|
padding: @spacing-02;
|
||||||
|
}
|
||||||
|
|
||||||
|
.review-panel-entry:hover {
|
||||||
|
.review-panel-entry-content {
|
||||||
|
display: initial;
|
||||||
|
position: absolute;
|
||||||
|
left: -200px;
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.review-panel-footer {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue