Merge pull request #24332 from overleaf/dp-review-panel

Add review panel to new editor

GitOrigin-RevId: 918a29d81fcfaf60bc4af8a20a25545d79c4a3ed
This commit is contained in:
David
2025-04-10 11:20:37 +01:00
committed by Copybot
parent 36fcc401cc
commit 03d8d358d0
18 changed files with 230 additions and 159 deletions
@@ -3,14 +3,10 @@ import { useFileTreeOpenContext } from '@/features/ide-react/context/file-tree-o
import { useOutlineContext } from '@/features/ide-react/context/outline-context'
import useNestedOutline from '@/features/outline/hooks/use-nested-outline'
import getChildrenLines from '@/features/outline/util/get-children-lines'
import { useCodeMirrorViewContext } from '@/features/source-editor/components/codemirror-context'
import MaterialIcon from '@/shared/components/material-icon'
import { useFileTreeData } from '@/shared/context/file-tree-data-context'
import { getPanel } from '@codemirror/view'
import { Fragment, useMemo } from 'react'
import { Outline } from '@/features/source-editor/utils/tree-operations/outline'
import { createPortal } from 'react-dom'
import { createBreadcrumbsPanel } from '@/features/source-editor/extensions/breadcrumbs-panel'
const constructOutlineHierarchy = (
items: Outline[],
@@ -37,16 +33,6 @@ const constructOutlineHierarchy = (
}
export default function Breadcrumbs() {
const view = useCodeMirrorViewContext()
const panel = getPanel(view, createBreadcrumbsPanel)
if (!panel) {
return null
}
return createPortal(<BreadcrumbsContent />, panel.dom)
}
function BreadcrumbsContent() {
const { openEntity } = useFileTreeOpenContext()
const { fileTreeData } = useFileTreeData()
const outline = useNestedOutline()
@@ -36,7 +36,7 @@ import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
type RailElement = {
icon: AvailableUnfilledIcon
key: RailTabKey
component: ReactElement
component: ReactElement | null
indicator?: ReactElement
title: string
hide?: boolean
@@ -111,7 +111,7 @@ export const RailLayout = () => {
key: 'review-panel',
icon: 'rate_review',
title: t('review_panel'),
component: <>Review panel</>,
component: null,
},
{
key: 'chat',
@@ -169,6 +169,8 @@ export const RailLayout = () => {
[setSelectedTab, selectedTab, setIsOpen, togglePane, railTabs]
)
const isReviewPanelOpen = selectedTab === 'review-panel'
return (
<TabContainer
mountOnEnter // Only render when necessary (so that we can lazy load tab content)
@@ -200,6 +202,7 @@ export const RailLayout = () => {
</div>
<Panel
id="ide-redesign-sidebar-panel"
className={classNames({ hidden: isReviewPanelOpen })}
order={1}
defaultSize={15}
minSize={5}
@@ -211,7 +214,9 @@ export const RailLayout = () => {
>
{isHistoryView && <HistorySidebar />}
<div
className={classNames('ide-rail-content', { hidden: isHistoryView })}
className={classNames('ide-rail-content', {
hidden: isHistoryView,
})}
>
<Tab.Content>
{railTabs
@@ -225,6 +230,7 @@ export const RailLayout = () => {
</div>
</Panel>
<HorizontalResizeHandle
className={classNames({ hidden: isReviewPanelOpen })}
resizable
hitAreaMargins={{ coarse: 0, fine: 0 }}
onDoubleClick={togglePane}
@@ -2,20 +2,15 @@ import ReactDOM from 'react-dom'
import { useCodeMirrorViewContext } from '../../source-editor/components/codemirror-context'
import { memo } from 'react'
import ReviewPanel from './review-panel'
import { useLayoutContext } from '@/shared/context/layout-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'
import TrackChangesOnWidget from './track-changes-on-widget'
import { useEditorManagerContext } from '@/features/ide-react/context/editor-manager-context'
import ReviewModeSwitcher from './review-mode-switcher'
import getMeta from '@/utils/meta'
import useReviewPanelLayout from '../hooks/use-review-panel-layout'
function ReviewPanelContainer() {
const view = useCodeMirrorViewContext()
const ranges = useRangesContext()
const threads = useThreadsContext()
const { reviewPanelOpen } = useLayoutContext()
const { showPanel, mini } = useReviewPanelLayout()
const { wantTrackChanges } = useEditorManagerContext()
const enableReviewerRole = getMeta('ol-isReviewerRoleEnabled')
@@ -23,16 +18,13 @@ function ReviewPanelContainer() {
return null
}
const hasCommentOrChange = hasActiveRange(ranges, threads)
const showPanel = reviewPanelOpen || hasCommentOrChange
const showTrackChangesWidget =
!enableReviewerRole && wantTrackChanges && !reviewPanelOpen
const showTrackChangesWidget = !enableReviewerRole && wantTrackChanges && mini
return ReactDOM.createPortal(
<>
{showTrackChangesWidget && <TrackChangesOnWidget />}
{enableReviewerRole && <ReviewModeSwitcher />}
{showPanel && <ReviewPanel mini={!reviewPanelOpen} />}
{showPanel && <ReviewPanel mini={mini} />}
</>,
view.scrollDOM
)
@@ -6,6 +6,8 @@ import { useLayoutContext } from '@/shared/context/layout-context'
import { useTranslation } from 'react-i18next'
import getMeta from '@/utils/meta'
import { PanelHeading } from '@/shared/components/panel-heading'
import { useRailContext } from '@/features/ide-redesign/contexts/rail-context'
import { useIsNewEditorEnabled } from '@/features/ide-redesign/utils/new-editor-utils'
const isReviewerRoleEnabled = getMeta('ol-isReviewerRoleEnabled')
@@ -13,14 +15,17 @@ const ReviewPanelHeader: FC = () => {
const [trackChangesMenuExpanded, setTrackChangesMenuExpanded] =
useState(false)
const { setReviewPanelOpen } = useLayoutContext()
const { setIsOpen: setRailIsOpen } = useRailContext()
const { t } = useTranslation()
const newEditor = useIsNewEditorEnabled()
const handleClose = newEditor
? () => setRailIsOpen(false)
: () => setReviewPanelOpen(false)
return (
<div className="review-panel-header">
<PanelHeading
title={t('review')}
handleClose={() => setReviewPanelOpen(false)}
>
<PanelHeading title={t('review')} handleClose={handleClose}>
{isReviewerRoleEnabled && <ReviewPanelResolvedThreadsButton />}
</PanelHeading>
{!isReviewerRoleEnabled && (
@@ -6,9 +6,11 @@ import { ReviewPanelOverview } from './review-panel-overview'
import classnames from 'classnames'
import { useReviewPanelStyles } from '@/features/review-panel-new/hooks/use-review-panel-styles'
import { useReviewPanelViewContext } from '../context/review-panel-view-context'
import { useIsNewEditorEnabled } from '@/features/ide-redesign/utils/new-editor-utils'
const ReviewPanel: FC<{ mini?: boolean }> = ({ mini = false }) => {
const choosenSubView = useReviewPanelViewContext()
const newEditor = useIsNewEditorEnabled()
const activeSubView = useMemo(
() => (mini ? 'cur_file' : choosenSubView),
@@ -25,7 +27,7 @@ const ReviewPanel: FC<{ mini?: boolean }> = ({ mini = false }) => {
return (
<div className={className} style={style}>
<div id="review-panel-inner" className="review-panel-inner">
{!mini && <ReviewPanelHeader />}
{!newEditor && !mini && <ReviewPanelHeader />}
{activeSubView === 'cur_file' && <ReviewPanelCurrentFile />}
{activeSubView === 'overview' && <ReviewPanelOverview />}
@@ -35,6 +35,8 @@ import { useEditorManagerContext } from '@/features/ide-react/context/editor-man
import classNames from 'classnames'
import useEventListener from '@/shared/hooks/use-event-listener'
import getMeta from '@/utils/meta'
import { useRailContext } from '@/features/ide-redesign/contexts/rail-context'
import { useIsNewEditorEnabled } from '@/features/ide-redesign/utils/new-editor-utils'
const isReviewerRoleEnabled = getMeta('ol-isReviewerRoleEnabled')
const TRACK_CHANGES_ON_WIDGET_HEIGHT = 25
@@ -49,6 +51,9 @@ const ReviewTooltipMenu: FC = () => {
const [show, setShow] = useState(true)
const { setView } = useReviewPanelViewActionsContext()
const { setReviewPanelOpen } = useLayoutContext()
const { setIsOpen: setRailIsOpen, setSelectedTab: setSelectedRailTab } =
useRailContext()
const newEditor = useIsNewEditorEnabled()
const tooltipState = state.field(reviewTooltipStateField, false)?.tooltip
const previousTooltipState = usePreviousValue(tooltipState)
@@ -65,7 +70,12 @@ const ReviewTooltipMenu: FC = () => {
return
}
setReviewPanelOpen(true)
if (newEditor) {
setSelectedRailTab('review-panel')
setRailIsOpen(true)
} else {
setReviewPanelOpen(true)
}
setView('cur_file')
const effects = isCursorNearViewportEdge(view, main.anchor)
@@ -77,7 +87,15 @@ const ReviewTooltipMenu: FC = () => {
view.dispatch({ effects })
setShow(false)
}, [setReviewPanelOpen, setView, setShow, view])
}, [
setReviewPanelOpen,
setView,
setShow,
view,
setSelectedRailTab,
setRailIsOpen,
newEditor,
])
useEventListener('add-new-review-comment', addComment)
@@ -8,7 +8,7 @@ import {
import { DecorationSet, EditorView } from '@codemirror/view'
import { EditorSelection } from '@codemirror/state'
import _ from 'lodash'
import { useLayoutContext } from '@/shared/context/layout-context'
import useReviewPanelLayout from './use-review-panel-layout'
const useMoreCommments = (
changes: Change<EditOperation>[],
@@ -20,7 +20,8 @@ const useMoreCommments = (
onMoreCommentsBelowClick: null | (() => void)
} => {
const view = useCodeMirrorViewContext()
const { reviewPanelOpen } = useLayoutContext()
const { showPanel, mini } = useReviewPanelLayout()
const reviewPanelOpen = showPanel && !mini
const [positionAbove, setPositionAbove] = useState<number | null>(null)
const [positionBelow, setPositionBelow] = useState<number | null>(null)
@@ -0,0 +1,30 @@
import { useLayoutContext } from '@/shared/context/layout-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'
import { useRailContext } from '@/features/ide-redesign/contexts/rail-context'
import { useIsNewEditorEnabled } from '@/features/ide-redesign/utils/new-editor-utils'
export default function useReviewPanelLayout(): {
showPanel: boolean
showHeader: boolean
mini: boolean
} {
const ranges = useRangesContext()
const threads = useThreadsContext()
const { selectedTab: selectedRailTab, isOpen: railIsOpen } = useRailContext()
const { reviewPanelOpen: reviewPanelOpenOldEditor } = useLayoutContext()
const newEditor = useIsNewEditorEnabled()
const reviewPanelOpen = newEditor
? selectedRailTab === 'review-panel' && railIsOpen
: reviewPanelOpenOldEditor
const hasCommentOrChange = hasActiveRange(ranges, threads)
const showPanel = reviewPanelOpen || !!hasCommentOrChange
const mini = !reviewPanelOpen
const showHeader = showPanel && !mini
return { showPanel, showHeader, mini }
}
@@ -30,13 +30,6 @@ export const useReviewPanelStyles = (mini: boolean) => {
}))
}, [])
useEffect(() => {
setStyles(value => ({
...value,
'--review-panel-width': mini ? '22px' : '230px',
}))
}, [mini])
useEffect(() => {
if ('ResizeObserver' in window) {
const scrollDomObserver = new window.ResizeObserver(entries =>
@@ -17,8 +17,6 @@ import {
CodeMirrorViewContext,
} from './codemirror-context'
import MathPreviewTooltip from './math-preview-tooltip'
import Breadcrumbs from '@/features/ide-redesign/components/breadcrumbs'
import { useIsNewEditorEnabled } from '@/features/ide-redesign/utils/new-editor-utils'
import { useToolbarMenuBarEditorCommands } from '@/features/ide-redesign/hooks/use-toolbar-menu-editor-commands'
// TODO: remove this when definitely no longer used
@@ -68,7 +66,6 @@ function CodeMirrorEditor() {
}
function CodeMirrorEditorComponents() {
const newEditor = useIsNewEditorEnabled()
useToolbarMenuBarEditorCommands()
return (
@@ -83,7 +80,6 @@ function CodeMirrorEditorComponents() {
<Component key={path} />
)
)}
{newEditor && <Breadcrumbs />}
<CodeMirrorCommandTooltip />
<MathPreviewTooltip />
@@ -20,6 +20,11 @@ import { minimumListDepthForSelection } from '../utils/tree-operations/ancestors
import { debugConsole } from '@/utils/debugging'
import { useTranslation } from 'react-i18next'
import { ToggleSearchButton } from '@/features/source-editor/components/toolbar/toggle-search-button'
import ReviewPanelHeader from '@/features/review-panel-new/components/review-panel-header'
import useReviewPanelLayout from '@/features/review-panel-new/hooks/use-review-panel-layout'
import { useIsNewEditorEnabled } from '@/features/ide-redesign/utils/new-editor-utils'
import Breadcrumbs from '@/features/ide-redesign/components/breadcrumbs'
import classNames from 'classnames'
export const CodeMirrorToolbar = () => {
const view = useCodeMirrorViewContext()
@@ -46,6 +51,9 @@ const Toolbar = memo(function Toolbar() {
const listDepth = minimumListDepthForSelection(state)
const newEditor = useIsNewEditorEnabled()
const { showHeader: showReviewPanelHeader } = useReviewPanelLayout()
const {
open: overflowOpen,
onToggle: setOverflowOpen,
@@ -131,50 +139,61 @@ const Toolbar = memo(function Toolbar() {
const showActions = !state.readOnly && !insideTable
return (
<div
role="toolbar"
aria-label={t('toolbar_editor')}
className="ol-cm-toolbar toolbar-editor"
ref={elementRef}
>
<EditorSwitch />
{showActions && (
<ToolbarItems
state={state}
languageName={languageName}
visual={visual}
listDepth={listDepth}
/>
)}
<div className="ol-cm-toolbar-button-group ol-cm-toolbar-stretch">
{showActions && (
<ToolbarOverflow
overflowed={overflowed}
overflowOpen={overflowOpen}
setOverflowOpen={setOverflowOpen}
overflowRef={overflowRef}
>
<>
{newEditor && showReviewPanelHeader && <ReviewPanelHeader />}
<div
id="ol-cm-toolbar-wrapper"
className={classNames('ol-cm-toolbar-wrapper', {
'ol-cm-toolbar-wrapper-indented': newEditor && showReviewPanelHeader,
})}
>
<div
role="toolbar"
aria-label={t('toolbar_editor')}
className="ol-cm-toolbar toolbar-editor"
ref={elementRef}
>
<EditorSwitch />
{showActions && (
<ToolbarItems
state={state}
overflowed={overflowedItemsRef.current}
languageName={languageName}
visual={visual}
listDepth={listDepth}
/>
</ToolbarOverflow>
)}
</div>
)}
<div
className="ol-cm-toolbar-button-group ol-cm-toolbar-end"
ref={handleButtons}
>
<ToggleSearchButton state={state} />
<SwitchToPDFButton />
<DetacherSynctexControl />
<DetachCompileButtonWrapper />
<div className="ol-cm-toolbar-button-group ol-cm-toolbar-stretch">
{showActions && (
<ToolbarOverflow
overflowed={overflowed}
overflowOpen={overflowOpen}
setOverflowOpen={setOverflowOpen}
overflowRef={overflowRef}
>
<ToolbarItems
state={state}
overflowed={overflowedItemsRef.current}
languageName={languageName}
visual={visual}
listDepth={listDepth}
/>
</ToolbarOverflow>
)}
</div>
<div
className="ol-cm-toolbar-button-group ol-cm-toolbar-end"
ref={handleButtons}
>
<ToggleSearchButton state={state} />
<SwitchToPDFButton />
<DetacherSynctexControl />
<DetachCompileButtonWrapper />
</div>
</div>
{newEditor && <Breadcrumbs />}
</div>
</div>
</>
)
})
@@ -1,61 +1,41 @@
import { canUseNewEditor } from '@/features/ide-redesign/utils/new-editor-utils'
import { Compartment, Extension, TransactionSpec } from '@codemirror/state'
import { EditorView, showPanel } from '@codemirror/view'
export function createBreadcrumbsPanel() {
const dom = document.createElement('div')
dom.classList.add('ol-cm-breadcrumbs-portal')
return { dom, top: true }
}
const breadcrumbsTheme = EditorView.baseTheme({
'.ol-cm-breadcrumbs': {
display: 'flex',
alignItems: 'center',
gap: 'var(--spacing-01)',
fontSize: 'var(--font-size-01)',
padding: 'var(--spacing-02)',
overflow: 'auto',
scrollbarWidth: 'thin',
'& > *': {
flexShrink: '0',
},
},
'&light .ol-cm-breadcrumbs': {
color: 'var(--content-secondary)',
backgroundColor: 'var(--bg-light-primary)',
borderBottom: '1px solid #ddd',
},
'&light .ol-cm-breadcrumb-chevron': {
color: 'var(--neutral-30)',
},
'&dark .ol-cm-breadcrumbs': {
color: 'var(--content-secondary-dark)',
backgroundColor: 'var(--bg-dark-primary)',
},
'&dark .ol-cm-breadcrumb-chevron': {
color: 'var(--neutral-50)',
},
})
const breadcrumbsConf = new Compartment()
const breadcrumbsEnabled: Extension = [
showPanel.of(createBreadcrumbsPanel),
breadcrumbsTheme,
]
const breadcrumbsDisabled: Extension = []
export const setBreadcrumbsEnabled = (enabled: boolean): TransactionSpec => ({
effects: breadcrumbsConf.reconfigure(
enabled ? breadcrumbsEnabled : breadcrumbsDisabled
),
})
import { EditorView } from '@codemirror/view'
/**
* A panel which contains the editor breadcrumbs
*/
export const breadcrumbPanel = (enableNewEditor: boolean) => {
const enabled = canUseNewEditor() && enableNewEditor
return breadcrumbsConf.of(enabled ? breadcrumbsEnabled : breadcrumbsDisabled)
export function breadcrumbPanel() {
return [
EditorView.editorAttributes.of({
style: '--breadcrumbs-height: 28px;',
}),
EditorView.baseTheme({
'.ol-cm-breadcrumbs-portal': {
display: 'flex',
pointerEvents: 'none !important',
'& > *': {
pointerEvents: 'all',
},
},
'.ol-cm-breadcrumbs': {
height: 'var(--breadcrumbs-height)',
flex: 1,
display: 'flex',
alignItems: 'center',
gap: 'var(--spacing-01)',
fontSize: 'var(--font-size-01)',
padding: 'var(--spacing-02)',
overflow: 'auto',
scrollbarWidth: 'thin',
'& > *': {
flexShrink: '0',
},
},
'&light .ol-cm-breadcrumb-chevron': {
color: 'var(--neutral-30)',
},
'&dark .ol-cm-breadcrumb-chevron': {
color: 'var(--neutral-50)',
},
}),
]
}
@@ -148,7 +148,7 @@ export const createExtensions = (options: Record<string, any>): Extension[] => [
mathPreview(options.settings.mathPreview),
reviewTooltip(),
toolbarPanel(),
breadcrumbPanel(options.settings.enableNewEditor),
breadcrumbPanel(),
verticalOverflow(),
highlightActiveLine(options.visual.visual),
// The built-in extension that highlights the active line in the gutter.
@@ -23,9 +23,11 @@ export function createToolbarPanel() {
}
const toolbarTheme = EditorView.theme({
'.ol-cm-toolbar': {
'.ol-cm-toolbar-wrapper': {
backgroundColor: 'var(--editor-toolbar-bg)',
color: 'var(--toolbar-btn-color)',
},
'.ol-cm-toolbar': {
flex: 1,
display: 'flex',
overflowX: 'hidden',
@@ -53,7 +53,6 @@ import { useHunspell } from '@/features/source-editor/hooks/use-hunspell'
import { Permissions } from '@/features/ide-react/types/permissions'
import { useEditorManagerContext } from '@/features/ide-react/context/editor-manager-context'
import { useOnlineUsersContext } from '@/features/ide-react/context/online-users-context'
import { setBreadcrumbsEnabled } from '../extensions/breadcrumbs-panel'
function useCodeMirrorScope(view: EditorView) {
const { fileTreeData } = useFileTreeData()
@@ -436,13 +435,6 @@ function useCodeMirrorScope(view: EditorView) {
settingsRef.current.referencesSearchMode = referencesSearchMode
}, [referencesSearchMode])
useEffect(() => {
settingsRef.current.enableNewEditor = enableNewEditor
window.setTimeout(() => {
view.dispatch(setBreadcrumbsEnabled(enableNewEditor))
})
}, [view, enableNewEditor])
const emitSyncToPdf = useScopeEventEmitter('cursor:editor:syncToPdf')
// select and scroll to position on editor:gotoLine event (from synctex)
@@ -11,6 +11,7 @@
}
.panel-heading {
color: var(--panel-heading-color);
display: flex;
align-items: center;
padding: var(--spacing-03) var(--spacing-02);
@@ -16,6 +16,8 @@ $rp-type-blue: #6b7797;
--review-panel-empty-state-bg-color: var(--bg-light-primary);
--review-panel-button-hover-bg-color: var(--bg-light-tertiary);
--review-panel-border-color: var(--border-divider);
--review-panel-width: 230px;
--review-panel-width-mini: 24px;
@include theme('default') {
.ide-redesign-main {
@@ -32,6 +34,42 @@ $rp-type-blue: #6b7797;
}
}
.ide-redesign-main {
.review-panel-container {
order: -1;
}
.review-panel-inner {
border-left: none;
border-right: 1px solid var(--border-divider);
}
.review-panel-header {
border-bottom: none;
flex: 0 0 var(--review-panel-width);
}
.review-panel-more-comments-button-container {
&.upwards {
top: calc(var(--review-panel-top) + 16px);
}
}
.review-panel-mini {
// This needs to have a higher z-index than the gutter
// so that the comment/change hover previews appear in
// front of the gutter
z-index: 201;
.review-panel-entry-hover {
.review-panel-entry-content {
left: auto;
right: -200px;
}
}
}
}
.review-panel-container {
height: 100%;
flex-shrink: 0;
@@ -646,7 +684,7 @@ del.review-panel-content-highlight {
overflow: visible !important;
.review-panel-inner {
width: 24px;
width: var(--review-panel-width-mini);
}
.review-panel-entry {
@@ -32,8 +32,22 @@
--editor-toolbar-bg: var(--bg-dark-primary);
--toolbar-filetree-bg-color: var(--neutral-80);
.cm-panels-top {
border-bottom: none;
.ol-cm-toolbar-portal {
display: flex;
align-items: center;
background-color: var(--editor-toolbar-bg);
.review-panel-header {
align-self: flex-start;
}
}
.ol-cm-toolbar-wrapper {
flex: 1;
}
.ol-cm-toolbar-wrapper-indented {
width: calc(100% - var(--review-panel-width));
}
}
@@ -64,10 +78,6 @@
--toolbar-btn-hover-color: var(--white);
--editor-toolbar-bg: var(--white);
--toolbar-filetree-bg-color: var(--white);
.ol-cm-toolbar-portal {
border-bottom: 1px solid #ddd;
}
}
}