- {selectionCoords && (
-
- )}
+ {addCommentEntries.map(entry => {
+ const { id, from, to, value, top } = entry
+ return (
+
+ )
+ })}
{showEmptyState &&
}
diff --git a/services/web/frontend/js/features/review-panel-new/components/review-panel-entry.tsx b/services/web/frontend/js/features/review-panel-new/components/review-panel-entry.tsx
index 527912c637..59d262a4ae 100644
--- a/services/web/frontend/js/features/review-panel-new/components/review-panel-entry.tsx
+++ b/services/web/frontend/js/features/review-panel-new/components/review-panel-entry.tsx
@@ -14,7 +14,8 @@ export const ReviewPanelEntry: FC<{
op: AnyOperation
top?: number
className?: string
-}> = ({ children, position, top, op, className }) => {
+ selectLineOnFocus?: boolean
+}> = ({ children, position, top, op, className, selectLineOnFocus }) => {
const state = useCodeMirrorStateContext()
const view = useCodeMirrorViewContext()
const [focused, setFocused] = useState(false)
@@ -25,13 +26,15 @@ export const ReviewPanelEntry: FC<{
setTimeout(() => {
// without setTimeout, error "EditorView.update are not allowed while an update is in progress" can occur
// this can be avoided by using onClick rather than onFocus but it will then not pick up
or events for focusing entries
- view.dispatch({
- selection: EditorSelection.cursor(position),
- effects: EditorView.scrollIntoView(position, { y: 'center' }),
- })
+ if (selectLineOnFocus) {
+ view.dispatch({
+ selection: EditorSelection.cursor(position),
+ effects: EditorView.scrollIntoView(position, { y: 'center' }),
+ })
+ }
}, 0)
setFocused(true)
- }, [view, position])
+ }, [view, position, selectLineOnFocus])
return (
>
+ ) => void
+) {
+ const [content, setContent] = useState('')
+
+ const handleKeyPress = useCallback(
+ (e: React.KeyboardEvent
) => {
+ if (e.key === 'Enter' && !e.shiftKey && !e.ctrlKey && !e.metaKey) {
+ e.preventDefault()
+ handleSubmit(content, setContent)
+ }
+ },
+ [content, handleSubmit]
+ )
+
+ const handleChange = useCallback(
+ (e: React.ChangeEvent) => {
+ setContent(e.target.value)
+ },
+ []
+ )
+
+ return { handleChange, handleKeyPress, content }
+}
diff --git a/services/web/frontend/js/features/source-editor/components/codemirror-editor.tsx b/services/web/frontend/js/features/source-editor/components/codemirror-editor.tsx
index 3503222aaf..5b420ab240 100644
--- a/services/web/frontend/js/features/source-editor/components/codemirror-editor.tsx
+++ b/services/web/frontend/js/features/source-editor/components/codemirror-editor.tsx
@@ -15,11 +15,12 @@ import { CodeMirrorToolbar } from './codemirror-toolbar'
import { CodemirrorOutline } from './codemirror-outline'
import { CodeMirrorCommandTooltip } from './codemirror-command-tooltip'
import { dispatchTimer } from '../../../infrastructure/cm6-performance'
-
import importOverleafModules from '../../../../macros/import-overleaf-module.macro'
import { FigureModal } from './figure-modal/figure-modal'
import { ReviewPanelProviders } from '@/features/review-panel-new/context/review-panel-providers'
import { ReviewPanelMigration } from '@/features/source-editor/components/review-panel/review-panel-migration'
+import AddCommentTooltip from '@/features/review-panel-new/components/add-comment-tooltip'
+import { useFeatureFlag } from '@/shared/context/split-test-context'
const sourceEditorComponents = importOverleafModules(
'sourceEditorComponents'
@@ -37,6 +38,8 @@ function CodeMirrorEditor() {
const isMounted = useIsMounted()
+ const newReviewPanel = useFeatureFlag('review-panel-redesign')
+
// create the view using the initial state and intercept transactions
const viewRef = useRef(null)
if (viewRef.current === null) {
@@ -75,7 +78,9 @@ function CodeMirrorEditor() {
)}
+ {newReviewPanel && }
+
{sourceEditorComponents.map(
({ import: { default: Component }, path }) => (
diff --git a/services/web/frontend/js/features/source-editor/extensions/add-comment.ts b/services/web/frontend/js/features/source-editor/extensions/add-comment.ts
new file mode 100644
index 0000000000..8da9c876bd
--- /dev/null
+++ b/services/web/frontend/js/features/source-editor/extensions/add-comment.ts
@@ -0,0 +1,134 @@
+import {
+ Decoration,
+ DecorationSet,
+ EditorView,
+ showTooltip,
+ Tooltip,
+} from '@codemirror/view'
+import {
+ EditorState,
+ Extension,
+ StateField,
+ StateEffect,
+ Range,
+ SelectionRange,
+} from '@codemirror/state'
+import { isSplitTestEnabled } from '@/utils/splitTestUtils'
+import { v4 as uuid } from 'uuid'
+
+export const addNewCommentRangeEffect = StateEffect.define>()
+
+export const removeNewCommentRangeEffect = StateEffect.define()
+
+export const buildAddNewCommentRangeEffect = (range: SelectionRange) => {
+ return addNewCommentRangeEffect.of(
+ Decoration.mark({
+ tagName: 'span',
+ class: `ol-cm-change ol-cm-change-c`,
+ opType: 'c',
+ id: uuid(),
+ }).range(range.from, range.to)
+ )
+}
+
+export const addComment = (): Extension => {
+ if (!isSplitTestEnabled('review-panel-redesign')) {
+ return []
+ }
+
+ return [addCommentTheme, addCommentStateField]
+}
+
+export const addCommentStateField = StateField.define<{
+ tooltip: Tooltip | null
+ ranges: DecorationSet
+}>({
+ create() {
+ return { tooltip: null, ranges: Decoration.none }
+ },
+
+ update(field, tr) {
+ let { tooltip, ranges } = field
+
+ ranges = ranges.map(tr.changes)
+
+ for (const effect of tr.effects) {
+ if (effect.is(removeNewCommentRangeEffect)) {
+ const rangeToRemove = effect.value
+ ranges = ranges.update({
+ // eslint-disable-next-line no-unused-vars
+ filter: (from, to, value) => {
+ return value.spec.id !== rangeToRemove.spec.id
+ },
+ })
+ }
+
+ if (effect.is(addNewCommentRangeEffect)) {
+ const rangeToAdd = effect.value
+ ranges = ranges.update({
+ add: [rangeToAdd],
+ })
+ }
+ }
+
+ if (tr.docChanged || tr.selection) {
+ tooltip = buildTooltip(tr.state)
+ }
+
+ return { tooltip, ranges }
+ },
+
+ provide: field => [
+ EditorView.decorations.from(field, field => field.ranges),
+ showTooltip.compute([field], state => state.field(field).tooltip),
+ ],
+})
+
+function buildTooltip(state: EditorState): Tooltip | null {
+ const range = state.selection.main
+ if (range.empty) {
+ return null
+ }
+
+ return {
+ pos: range.assoc < 0 ? range.to : range.from,
+ above: true,
+ strictSide: true,
+ arrow: false,
+ create() {
+ const dom = document.createElement('div')
+ dom.className = 'review-panel-add-comment-tooltip-container'
+ return { dom, overlap: true, offset: { x: 0, y: 8 } }
+ },
+ }
+}
+
+/**
+ * Styles for the tooltip
+ */
+const addCommentTheme = EditorView.baseTheme({
+ '.review-panel-add-comment-tooltip-container.cm-tooltip': {
+ backgroundColor: 'transparent',
+ border: 'none',
+ },
+
+ '&light': {
+ '& .review-panel-add-comment-tooltip': {
+ backgroundColor: 'white',
+ border: '1px solid #e7e9ee',
+ '&:hover': {
+ backgroundColor: '#e7e9ee',
+ },
+ },
+ },
+
+ '&dark': {
+ '& .review-panel-add-comment-tooltip': {
+ backgroundColor: '#1b222c',
+ border: '1px solid #2f3a4c',
+ '&:hover': {
+ backgroundColor: '#2f3a4c',
+ },
+ },
+ },
+})
diff --git a/services/web/frontend/js/features/source-editor/extensions/index.ts b/services/web/frontend/js/features/source-editor/extensions/index.ts
index 0f171f74af..9e663d47b6 100644
--- a/services/web/frontend/js/features/source-editor/extensions/index.ts
+++ b/services/web/frontend/js/features/source-editor/extensions/index.ts
@@ -52,6 +52,7 @@ import { mathPreview } from './math-preview'
import { isSplitTestEnabled } from '@/utils/splitTestUtils'
import { ranges } from './ranges'
import { trackDetachedComments } from './track-detached-comments'
+import { addComment } from './add-comment'
const moduleExtensions: Array<() => Extension> = importOverleafModules(
'sourceEditorExtensions'
@@ -133,6 +134,7 @@ export const createExtensions = (options: Record): Extension[] => [
trackDetachedComments(options.currentDoc),
visual(options.visual),
mathPreview(options.settings.mathPreview),
+ addComment(),
toolbarPanel(),
verticalOverflow(),
highlightActiveLine(options.visual.visual),
diff --git a/services/web/frontend/js/shared/context/layout-context.tsx b/services/web/frontend/js/shared/context/layout-context.tsx
index 3736419a56..5e1606a476 100644
--- a/services/web/frontend/js/shared/context/layout-context.tsx
+++ b/services/web/frontend/js/shared/context/layout-context.tsx
@@ -16,6 +16,7 @@ import { DetachRole } from './detach-context'
import { debugConsole } from '@/utils/debugging'
import { BinaryFile } from '@/features/file-view/types/binary-file'
import useScopeEventEmitter from '@/shared/hooks/use-scope-event-emitter'
+import useEventListener from '../hooks/use-event-listener'
export type IdeLayout = 'sideBySide' | 'flat'
export type IdeView = 'editor' | 'file' | 'pdf' | 'history'
@@ -165,6 +166,17 @@ export const LayoutProvider: FC = ({ children }) => {
changeLayout,
])
+ const handleSetReviewPanelOpenEvent = useCallback(
+ (e: Event) => {
+ const event = e as CustomEvent<{ isOpen: boolean }>
+ const { isOpen } = event.detail
+ setReviewPanelOpen(isOpen)
+ },
+ [setReviewPanelOpen]
+ )
+
+ useEventListener('set-review-panel-open', handleSetReviewPanelOpenEvent)
+
const value = useMemo(
() => ({
reattach,
diff --git a/services/web/frontend/stylesheets/app/editor/review-panel-new.less b/services/web/frontend/stylesheets/app/editor/review-panel-new.less
index ad3270730c..9c1645aa19 100644
--- a/services/web/frontend/stylesheets/app/editor/review-panel-new.less
+++ b/services/web/frontend/stylesheets/app/editor/review-panel-new.less
@@ -349,12 +349,6 @@
max-height: 400px;
}
- .review-panel-add-comment-form {
- display: flex;
- flex-direction: column;
- width: var(--review-panel-width);
- }
-
.review-panel-empty-state {
position: absolute;
top: 0;
@@ -453,6 +447,33 @@
display: flex;
}
+ .review-panel-add-comment-textarea {
+ padding: 2px 6px;
+ resize: vertical;
+ min-height: 44px;
+ }
+
+ .review-panel-add-comment {
+ position: absolute;
+ z-index: 2;
+ }
+
+ .review-panel-add-comment-buttons {
+ display: flex;
+ justify-content: flex-end;
+ gap: 8px;
+ }
+
+ .review-panel-add-comment-cancel-button {
+ background-color: transparent;
+
+ &:hover,
+ &:focus {
+ background-color: @neutral-20;
+ color: @content-primary;
+ }
+ }
+
&.review-panel-subview-overview {
&.review-panel-container {
overflow-y: hidden;
@@ -510,3 +531,12 @@
}
}
}
+
+.review-panel-add-comment-tooltip {
+ height: 24px;
+ border-radius: 12px;
+ padding: 2px 12px;
+ display: flex;
+ align-items: center;
+ gap: 2px;
+}