Merge pull request #13414 from overleaf/ii-review-panel-migration-create-context-api-fix

[web] Create context api for review panel FIX

GitOrigin-RevId: fc6d8adf18d07e71b529a28deab4d49d62c43587
This commit is contained in:
ilkin-overleaf 2023-06-12 13:21:06 +03:00 committed by Copybot
parent 88d0254dde
commit 9b930d2849
11 changed files with 432 additions and 124 deletions

View file

@ -128,7 +128,7 @@
on-indicator-click="toggleReviewPanel();" on-indicator-click="toggleReviewPanel();"
on-mouse-enter="mouseEnterIndicator()" on-mouse-enter="mouseEnterIndicator()"
on-mouse-leave="mouseLeaveIndicator()" on-mouse-leave="mouseLeaveIndicator()"
on-body-click="gotoEntry(editor.open_doc_id, entry)" on-body-click="gotoEntry(editor.open_doc_id, entry.offset)"
permissions="permissions" permissions="permissions"
) )
@ -141,7 +141,7 @@
on-indicator-click="toggleReviewPanel();" on-indicator-click="toggleReviewPanel();"
on-mouse-enter="mouseEnterIndicator()" on-mouse-enter="mouseEnterIndicator()"
on-mouse-leave="mouseLeaveIndicator()" on-mouse-leave="mouseLeaveIndicator()"
on-body-click="gotoEntry(editor.open_doc_id, entry)" on-body-click="gotoEntry(editor.open_doc_id, entry.offset)"
permissions="permissions" permissions="permissions"
) )
@ -149,14 +149,14 @@
comment-entry( comment-entry(
entry="entry" entry="entry"
threads="reviewPanel.commentThreads" threads="reviewPanel.commentThreads"
on-resolve="resolveComment(entry, entry_id)" on-resolve="resolveComment(editor.open_doc_id, entry_id)"
on-reply="submitReply(entry, entry_id);" on-reply="submitReply(entry, entry_id);"
on-indicator-click="toggleReviewPanel();" on-indicator-click="toggleReviewPanel();"
on-mouse-enter="mouseEnterIndicator()" on-mouse-enter="mouseEnterIndicator()"
on-mouse-leave="mouseLeaveIndicator()" on-mouse-leave="mouseLeaveIndicator()"
on-save-edit="saveEdit(entry.thread_id, comment)" on-save-edit="saveEdit(entry.thread_id, comment.id, comment.content)"
on-delete="deleteComment(entry.thread_id, comment)" on-delete="deleteComment(entry.thread_id, comment.id)"
on-body-click="gotoEntry(editor.open_doc_id, entry)" on-body-click="gotoEntry(editor.open_doc_id, entry.offset)"
permissions="permissions" permissions="permissions"
ng-if="!loadingThreads" ng-if="!loadingThreads"
) )
@ -207,7 +207,7 @@
change-entry( change-entry(
entry="entry" entry="entry"
user="users[entry.metadata.user_id]" user="users[entry.metadata.user_id]"
ng-click="gotoEntry(doc.doc.id, entry)" ng-click="gotoEntry(doc.doc.id, entry.offset)"
permissions="permissions" permissions="permissions"
) )
@ -215,7 +215,7 @@
aggregate-change-entry( aggregate-change-entry(
entry="entry" entry="entry"
user="users[entry.metadata.user_id]" user="users[entry.metadata.user_id]"
ng-click="gotoEntry(doc.doc.id, entry)" ng-click="gotoEntry(doc.doc.id, entry.offset)"
permissions="permissions" permissions="permissions"
) )
@ -224,9 +224,9 @@
entry="entry" entry="entry"
threads="reviewPanel.commentThreads" threads="reviewPanel.commentThreads"
on-reply="submitReply(entry, entry_id);" on-reply="submitReply(entry, entry_id);"
on-save-edit="saveEdit(entry.thread_id, comment)" on-save-edit="saveEdit(entry.thread_id, comment.id, comment.content)"
on-delete="deleteComment(entry.thread_id, comment)" on-delete="deleteComment(entry.thread_id, comment.id)"
ng-click="gotoEntry(doc.doc.id, entry)" ng-click="gotoEntry(doc.doc.id, entry.offset)"
permissions="permissions" permissions="permissions"
) )
@ -325,7 +325,7 @@ script(type='text/ng-template', id='aggregateChangeEntryTemplate')
.rp-entry-description .rp-entry-description
| #{translate("aggregate_changed")}  | #{translate("aggregate_changed")} 
del.rp-content-highlight del.rp-content-highlight
| {{ entry.metadata.replaced_content | limitTo:(isDeletionCollapsed ? contentLimit : entry.metadata.replaced_contentlength) }} | {{ entry.metadata.replaced_content | limitTo:(isDeletionCollapsed ? contentLimit : entry.metadata.replaced_content.length) }}
a.rp-collapse-toggle( a.rp-collapse-toggle(
href href
ng-if="deletionNeedsCollapsing" ng-if="deletionNeedsCollapsing"

View file

@ -0,0 +1,19 @@
import classnames from 'classnames'
const reviewPanelClasses = ['ol-cm-review-panel']
type ContainerProps = {
children?: React.ReactNode
classNames?: Record<string, boolean>
style?: React.CSSProperties
}
function Container({ children, classNames, ...rest }: ContainerProps) {
return (
<div className={classnames(...reviewPanelClasses, classNames)} {...rest}>
{children}
</div>
)
}
export default Container

View file

@ -0,0 +1,12 @@
import Container from './container'
function CurrentFileContainer() {
return (
<Container>
<em>ReviewPanelCurrentFileContainer</em>
<div className="review-panel-tools">Tools</div>
</Container>
)
}
export default CurrentFileContainer

View file

@ -0,0 +1 @@
export type SubView = 'cur_file' | 'overview'

View file

@ -0,0 +1,11 @@
import Container from './container'
function OverviewContainer() {
return (
<Container>
<em>ReviewPanelOverviewContainer</em>
</Container>
)
}
export default OverviewContainer

View file

@ -1,5 +1,39 @@
import ReactDOM from 'react-dom'
import { useCodeMirrorViewContext } from '../codemirror-editor'
import {
ReviewPanelProvider,
useReviewPanelValueContext,
} from '../../context/review-panel/review-panel-context'
import CurrentFileContainer from './current-file-container'
import OverviewContainer from './overview-container'
type ReviewPanelViewProps = {
parentDomNode: Element
}
function ReviewPanelView({ parentDomNode }: ReviewPanelViewProps) {
const { subView } = useReviewPanelValueContext()
return ReactDOM.createPortal(
<>
{subView === 'cur_file' ? (
<CurrentFileContainer />
) : (
<OverviewContainer />
)}
</>,
parentDomNode
)
}
function ReviewPanel() { function ReviewPanel() {
return <div>Content to be added.</div> const view = useCodeMirrorViewContext()
return (
<ReviewPanelProvider>
<ReviewPanelView parentDomNode={view.scrollDOM} />
</ReviewPanelProvider>
)
} }
export default ReviewPanel export default ReviewPanel

View file

@ -0,0 +1,33 @@
import { useMemo } from 'react'
import useScopeValue from '../../../../../shared/hooks/use-scope-value'
import { ReviewPanelState } from '../types/review-panel-state'
import * as ReviewPanel from '../types/review-panel-state'
function useAngularReviewPanelState(): ReviewPanelState {
const [subView, setSubView] = useScopeValue<ReviewPanel.Value<'subView'>>(
'reviewPanel.subView'
)
const [collapsed, setCollapsed] = useScopeValue<
ReviewPanel.Value<'collapsed'>
>('reviewPanel.overview.docsCollapsedState')
const values = useMemo<ReviewPanelState['values']>(
() => ({
subView,
collapsed,
}),
[subView, collapsed]
)
const updaterFns = useMemo<ReviewPanelState['updaterFns']>(
() => ({
setSubView,
setCollapsed,
}),
[setSubView, setCollapsed]
)
return { values, updaterFns }
}
export default useAngularReviewPanelState

View file

@ -0,0 +1,47 @@
import { createContext, useContext } from 'react'
import useAngularReviewPanelState from './hooks/use-angular-review-panel'
import { ReviewPanelState } from './types/review-panel-state'
const ReviewPanelValueContext = createContext<
ReviewPanelState['values'] | undefined
>(undefined)
const ReviewPanelUpdaterFnsContext = createContext<
ReviewPanelState['updaterFns'] | undefined
>(undefined)
type ReviewPanelProviderProps = {
children?: React.ReactNode
}
export function ReviewPanelProvider({ children }: ReviewPanelProviderProps) {
const { values, updaterFns } = useAngularReviewPanelState()
return (
<ReviewPanelValueContext.Provider value={values}>
<ReviewPanelUpdaterFnsContext.Provider value={updaterFns}>
{children}
</ReviewPanelUpdaterFnsContext.Provider>
</ReviewPanelValueContext.Provider>
)
}
export function useReviewPanelValueContext() {
const context = useContext(ReviewPanelValueContext)
if (!context) {
throw new Error(
'ReviewPanelValueContext is only available inside ReviewPanelProvider'
)
}
return context
}
export function useReviewPanelUpdaterFnsContext() {
const context = useContext(ReviewPanelUpdaterFnsContext)
if (!context) {
throw new Error(
'ReviewPanelUpdaterFnsContext is only available inside ReviewPanelProvider'
)
}
return context
}

View file

@ -0,0 +1,20 @@
import { SubView } from '../../../components/review-panel/nav'
export interface ReviewPanelState {
values: {
collapsed: Record<string, boolean>
subView: SubView
}
updaterFns: {
setCollapsed: React.Dispatch<
React.SetStateAction<ReviewPanelState['values']['collapsed']>
>
setSubView: React.Dispatch<
React.SetStateAction<ReviewPanelState['values']['subView']>
>
}
}
// Getter for values
export type Value<T extends keyof ReviewPanelState['values']> =
ReviewPanelState['values'][T]

View file

@ -71,7 +71,7 @@ export default App.controller(
PENDING: 'pending', PENDING: 'pending',
} }
$scope.reviewPanel = { ide.$scope.reviewPanel = {
trackChangesState: {}, trackChangesState: {},
trackChangesOnForEveryone: false, trackChangesOnForEveryone: false,
trackChangesOnForGuests: false, trackChangesOnForGuests: false,
@ -111,8 +111,8 @@ export default App.controller(
window.addEventListener('beforeunload', function () { window.addEventListener('beforeunload', function () {
const collapsedStates = {} const collapsedStates = {}
for (const doc in $scope.reviewPanel.overview.docsCollapsedState) { for (const doc in ide.$scope.reviewPanel.overview.docsCollapsedState) {
const state = $scope.reviewPanel.overview.docsCollapsedState[doc] const state = ide.$scope.reviewPanel.overview.docsCollapsedState[doc]
if (state) { if (state) {
collapsedStates[doc] = state collapsedStates[doc] = state
} }
@ -132,7 +132,7 @@ export default App.controller(
) )
$scope.$on('layout:pdf:resize', (event, state) => { $scope.$on('layout:pdf:resize', (event, state) => {
$scope.reviewPanel.layoutToLeft = ide.$scope.reviewPanel.layoutToLeft =
state.east?.size < 220 || state.east?.initClosed state.east?.size < 220 || state.east?.initClosed
$scope.$broadcast('review-panel:layout', false) $scope.$broadcast('review-panel:layout', false)
}) })
@ -156,10 +156,11 @@ export default App.controller(
}) })
$scope.$watch('project.members', function (members) { $scope.$watch('project.members', function (members) {
$scope.reviewPanel.formattedProjectMembers = {} ide.$scope.reviewPanel.formattedProjectMembers = {}
if (($scope.project != null ? $scope.project.owner : undefined) != null) { if (($scope.project != null ? $scope.project.owner : undefined) != null) {
$scope.reviewPanel.formattedProjectMembers[$scope.project.owner._id] = ide.$scope.reviewPanel.formattedProjectMembers[
formatUser($scope.project.owner) $scope.project.owner._id
] = formatUser($scope.project.owner)
} }
if ( if (
($scope.project != null ? $scope.project.members : undefined) != null ($scope.project != null ? $scope.project.members : undefined) != null
@ -168,16 +169,18 @@ export default App.controller(
const result = [] const result = []
for (const member of Array.from(members)) { for (const member of Array.from(members)) {
if (member.privileges === 'readAndWrite') { if (member.privileges === 'readAndWrite') {
if ($scope.reviewPanel.trackChangesState[member._id] == null) { if (
ide.$scope.reviewPanel.trackChangesState[member._id] == null
) {
// An added member will have track changes enabled if track changes is on for everyone // An added member will have track changes enabled if track changes is on for everyone
_setUserTCState( _setUserTCState(
member._id, member._id,
$scope.reviewPanel.trackChangesOnForEveryone, ide.$scope.reviewPanel.trackChangesOnForEveryone,
true true
) )
} }
result.push( result.push(
($scope.reviewPanel.formattedProjectMembers[member._id] = (ide.$scope.reviewPanel.formattedProjectMembers[member._id] =
formatUser(member)) formatUser(member))
) )
} else { } else {
@ -194,9 +197,9 @@ export default App.controller(
content: '', content: '',
} }
$scope.users = {} ide.$scope.users = $scope.users = {}
$scope.reviewPanelEventsBridge = new EventEmitter() ide.$scope.reviewPanelEventsBridge = new EventEmitter()
ide.socket.on('new-comment', function (thread_id, comment) { ide.socket.on('new-comment', function (thread_id, comment) {
const thread = getThread(thread_id) const thread = getThread(thread_id)
@ -241,37 +244,37 @@ export default App.controller(
const rangesTrackers = {} const rangesTrackers = {}
const getDocEntries = function (doc_id) { const getDocEntries = function (doc_id) {
if ($scope.reviewPanel.entries[doc_id] == null) { if (ide.$scope.reviewPanel.entries[doc_id] == null) {
$scope.reviewPanel.entries[doc_id] = {} ide.$scope.reviewPanel.entries[doc_id] = {}
} }
return $scope.reviewPanel.entries[doc_id] return ide.$scope.reviewPanel.entries[doc_id]
} }
const getDocResolvedComments = function (doc_id) { const getDocResolvedComments = function (doc_id) {
if ($scope.reviewPanel.resolvedComments[doc_id] == null) { if (ide.$scope.reviewPanel.resolvedComments[doc_id] == null) {
$scope.reviewPanel.resolvedComments[doc_id] = {} ide.$scope.reviewPanel.resolvedComments[doc_id] = {}
} }
return $scope.reviewPanel.resolvedComments[doc_id] return ide.$scope.reviewPanel.resolvedComments[doc_id]
} }
function getThread(thread_id) { function getThread(thread_id) {
if ($scope.reviewPanel.commentThreads[thread_id] == null) { if (ide.$scope.reviewPanel.commentThreads[thread_id] == null) {
$scope.reviewPanel.commentThreads[thread_id] = { messages: [] } ide.$scope.reviewPanel.commentThreads[thread_id] = { messages: [] }
} }
return $scope.reviewPanel.commentThreads[thread_id] return ide.$scope.reviewPanel.commentThreads[thread_id]
} }
function getChangeTracker(doc_id) { function getChangeTracker(doc_id) {
if (rangesTrackers[doc_id] == null) { if (rangesTrackers[doc_id] == null) {
rangesTrackers[doc_id] = new RangesTracker() rangesTrackers[doc_id] = new RangesTracker()
rangesTrackers[doc_id].resolvedThreadIds = rangesTrackers[doc_id].resolvedThreadIds =
$scope.reviewPanel.resolvedThreadIds ide.$scope.reviewPanel.resolvedThreadIds
} }
return rangesTrackers[doc_id] return rangesTrackers[doc_id]
} }
let scrollbar = {} let scrollbar = {}
$scope.reviewPanelEventsBridge.on( ide.$scope.reviewPanelEventsBridge.on(
'aceScrollbarVisibilityChanged', 'aceScrollbarVisibilityChanged',
function (isVisible, scrollbarWidth) { function (isVisible, scrollbarWidth) {
scrollbar = { isVisible, scrollbarWidth } scrollbar = { isVisible, scrollbarWidth }
@ -282,7 +285,7 @@ export default App.controller(
function updateScrollbar() { function updateScrollbar() {
if ( if (
scrollbar.isVisible && scrollbar.isVisible &&
$scope.reviewPanel.subView === $scope.SubViews.CUR_FILE && ide.$scope.reviewPanel.subView === $scope.SubViews.CUR_FILE &&
!$scope.editor.showRichText !$scope.editor.showRichText
) { ) {
return $reviewPanelEl.css('right', `${scrollbar.scrollbarWidth}px`) return $reviewPanelEl.css('right', `${scrollbar.scrollbarWidth}px`)
@ -310,11 +313,11 @@ export default App.controller(
} }
if (!open) { if (!open) {
// Always show current file when not open, but save current state // Always show current file when not open, but save current state
$scope.reviewPanel.openSubView = $scope.reviewPanel.subView ide.$scope.reviewPanel.openSubView = ide.$scope.reviewPanel.subView
$scope.reviewPanel.subView = $scope.SubViews.CUR_FILE ide.$scope.reviewPanel.subView = $scope.SubViews.CUR_FILE
} else { } else {
// Reset back to what we had when previously open // Reset back to what we had when previously open
$scope.reviewPanel.subView = $scope.reviewPanel.openSubView ide.$scope.reviewPanel.subView = ide.$scope.reviewPanel.openSubView
} }
return $timeout(function () { return $timeout(function () {
$scope.$broadcast('review-panel:toggle') $scope.$broadcast('review-panel:toggle')
@ -342,8 +345,8 @@ export default App.controller(
// replace any outdated info with this // replace any outdated info with this
rangesTrackers[doc.doc_id] = doc.ranges rangesTrackers[doc.doc_id] = doc.ranges
rangesTrackers[doc.doc_id].resolvedThreadIds = rangesTrackers[doc.doc_id].resolvedThreadIds =
$scope.reviewPanel.resolvedThreadIds ide.$scope.reviewPanel.resolvedThreadIds
$scope.reviewPanel.rangesTracker = rangesTrackers[doc.doc_id] ide.$scope.reviewPanel.rangesTracker = rangesTrackers[doc.doc_id]
if (old_doc != null) { if (old_doc != null) {
old_doc.off('flipped_pending_to_inflight') old_doc.off('flipped_pending_to_inflight')
} }
@ -354,7 +357,7 @@ export default App.controller(
$scope.$watch( $scope.$watch(
function () { function () {
const entries = const entries =
$scope.reviewPanel.entries[$scope.editor.open_doc_id] || {} ide.$scope.reviewPanel.entries[$scope.editor.open_doc_id] || {}
const permEntries = {} const permEntries = {}
for (const entry in entries) { for (const entry in entries) {
const entryData = entries[entry] const entryData = entries[entry]
@ -365,7 +368,7 @@ export default App.controller(
return Object.keys(permEntries).length return Object.keys(permEntries).length
}, },
nEntries => nEntries =>
($scope.reviewPanel.hasEntries = (ide.$scope.reviewPanel.hasEntries =
nEntries > 0 && $scope.project.features.trackChangesVisible) nEntries > 0 && $scope.project.features.trackChangesVisible)
) )
@ -385,9 +388,12 @@ export default App.controller(
const result = [] const result = []
for (const doc of Array.from(docs)) { for (const doc of Array.from(docs)) {
if ( if (
$scope.reviewPanel.overview.docsCollapsedState[doc.id] == null ide.$scope.reviewPanel.overview.docsCollapsedState[doc.id] ==
null
) { ) {
$scope.reviewPanel.overview.docsCollapsedState[doc.id] = false ide.$scope.reviewPanel.overview.docsCollapsedState[
doc.id
] = false
} }
if (doc.id !== $scope.editor.open_doc_id) { if (doc.id !== $scope.editor.open_doc_id) {
// this is kept up to date in real-time, don't overwrite // this is kept up to date in real-time, don't overwrite
@ -404,17 +410,17 @@ export default App.controller(
}) })
function refreshOverviewPanel() { function refreshOverviewPanel() {
$scope.reviewPanel.overview.loading = true ide.$scope.reviewPanel.overview.loading = true
return refreshRanges() return refreshRanges()
.then(() => ($scope.reviewPanel.overview.loading = false)) .then(() => (ide.$scope.reviewPanel.overview.loading = false))
.catch(() => ($scope.reviewPanel.overview.loading = false)) .catch(() => (ide.$scope.reviewPanel.overview.loading = false))
} }
$scope.refreshResolvedCommentsDropdown = function () { ide.$scope.refreshResolvedCommentsDropdown = function () {
$scope.reviewPanel.dropdown.loading = true ide.$scope.reviewPanel.dropdown.loading = true
const q = refreshRanges() const q = refreshRanges()
q.then(() => ($scope.reviewPanel.dropdown.loading = false)) q.then(() => (ide.$scope.reviewPanel.dropdown.loading = false))
q.catch(() => ($scope.reviewPanel.dropdown.loading = false)) q.catch(() => (ide.$scope.reviewPanel.dropdown.loading = false))
return q return q
} }
@ -504,7 +510,7 @@ export default App.controller(
let new_comment let new_comment
changed = true changed = true
delete delete_changes[comment.id] delete delete_changes[comment.id]
if ($scope.reviewPanel.resolvedThreadIds[comment.op.t]) { if (ide.$scope.reviewPanel.resolvedThreadIds[comment.op.t]) {
new_comment = new_comment =
resolvedComments[comment.id] != null resolvedComments[comment.id] != null
? resolvedComments[comment.id] ? resolvedComments[comment.id]
@ -567,9 +573,9 @@ export default App.controller(
const doc_id = $scope.editor.open_doc_id const doc_id = $scope.editor.open_doc_id
const entries = getDocEntries(doc_id) const entries = getDocEntries(doc_id)
// All selected changes will be added to this array. // All selected changes will be added to this array.
$scope.reviewPanel.selectedEntryIds = [] ide.$scope.reviewPanel.selectedEntryIds = []
// Count of user-visible changes, i.e. an aggregated change will count as one. // Count of user-visible changes, i.e. an aggregated change will count as one.
$scope.reviewPanel.nVisibleSelectedChanges = 0 ide.$scope.reviewPanel.nVisibleSelectedChanges = 0
delete entries['add-comment'] delete entries['add-comment']
delete entries['bulk-actions'] delete entries['bulk-actions']
@ -591,7 +597,7 @@ export default App.controller(
let isChangeEntryAndWithinSelection = false let isChangeEntryAndWithinSelection = false
if ( if (
entry.type === 'comment' && entry.type === 'comment' &&
!$scope.reviewPanel.resolvedThreadIds[entry.thread_id] !ide.$scope.reviewPanel.resolvedThreadIds[entry.thread_id]
) { ) {
entry.focused = entry.focused =
entry.offset <= selection_offset_start && entry.offset <= selection_offset_start &&
@ -624,9 +630,9 @@ export default App.controller(
if (isChangeEntryAndWithinSelection) { if (isChangeEntryAndWithinSelection) {
for (const entry_id of Array.from(entry.entry_ids)) { for (const entry_id of Array.from(entry.entry_ids)) {
$scope.reviewPanel.selectedEntryIds.push(entry_id) ide.$scope.reviewPanel.selectedEntryIds.push(entry_id)
} }
$scope.reviewPanel.nVisibleSelectedChanges++ ide.$scope.reviewPanel.nVisibleSelectedChanges++
} }
} }
@ -638,17 +644,21 @@ export default App.controller(
} }
) )
$scope.acceptChanges = function (change_ids) { ide.$scope.acceptChanges = function (change_ids) {
_doAcceptChanges(change_ids) _doAcceptChanges(change_ids)
eventTracking.sendMB('rp-changes-accepted', { eventTracking.sendMB('rp-changes-accepted', {
view: $scope.ui.reviewPanelOpen ? $scope.reviewPanel.subView : 'mini', view: $scope.ui.reviewPanelOpen
? ide.$scope.reviewPanel.subView
: 'mini',
}) })
} }
$scope.rejectChanges = function (change_ids) { ide.$scope.rejectChanges = function (change_ids) {
_doRejectChanges(change_ids) _doRejectChanges(change_ids)
eventTracking.sendMB('rp-changes-rejected', { eventTracking.sendMB('rp-changes-rejected', {
view: $scope.ui.reviewPanelOpen ? $scope.reviewPanel.subView : 'mini', view: $scope.ui.reviewPanelOpen
? ide.$scope.reviewPanel.subView
: 'mini',
}) })
} }
@ -657,11 +667,11 @@ export default App.controller(
// the review panel is visible when hovering over its indicator when the // the review panel is visible when hovering over its indicator when the
// review panel is minimized. See issue #8057. // review panel is minimized. See issue #8057.
$scope.mouseEnterIndicator = function () { $scope.mouseEnterIndicator = function () {
$scope.reviewPanel.entryHover = true ide.$scope.reviewPanel.entryHover = true
} }
$scope.mouseLeaveIndicator = function () { $scope.mouseLeaveIndicator = function () {
$scope.reviewPanel.entryHover = false ide.$scope.reviewPanel.entryHover = false
} }
function _doAcceptChanges(change_ids) { function _doAcceptChanges(change_ids) {
@ -679,24 +689,28 @@ export default App.controller(
} }
const bulkAccept = function () { const bulkAccept = function () {
_doAcceptChanges($scope.reviewPanel.selectedEntryIds.slice()) _doAcceptChanges(ide.$scope.reviewPanel.selectedEntryIds.slice())
eventTracking.sendMB('rp-bulk-accept', { eventTracking.sendMB('rp-bulk-accept', {
view: $scope.ui.reviewPanelOpen ? $scope.reviewPanel.subView : 'mini', view: $scope.ui.reviewPanelOpen
nEntries: $scope.reviewPanel.nVisibleSelectedChanges, ? ide.$scope.reviewPanel.subView
: 'mini',
nEntries: ide.$scope.reviewPanel.nVisibleSelectedChanges,
}) })
} }
const bulkReject = function () { const bulkReject = function () {
_doRejectChanges($scope.reviewPanel.selectedEntryIds.slice()) _doRejectChanges(ide.$scope.reviewPanel.selectedEntryIds.slice())
eventTracking.sendMB('rp-bulk-reject', { eventTracking.sendMB('rp-bulk-reject', {
view: $scope.ui.reviewPanelOpen ? $scope.reviewPanel.subView : 'mini', view: $scope.ui.reviewPanelOpen
nEntries: $scope.reviewPanel.nVisibleSelectedChanges, ? ide.$scope.reviewPanel.subView
: 'mini',
nEntries: ide.$scope.reviewPanel.nVisibleSelectedChanges,
}) })
} }
$scope.showBulkAcceptDialog = () => showBulkActionsDialog(true) ide.$scope.showBulkAcceptDialog = () => showBulkActionsDialog(true)
$scope.showBulkRejectDialog = () => showBulkActionsDialog(false) ide.$scope.showBulkRejectDialog = () => showBulkActionsDialog(false)
const showBulkActionsDialog = isAccept => const showBulkActionsDialog = isAccept =>
$modal $modal
@ -708,7 +722,7 @@ export default App.controller(
return isAccept return isAccept
}, },
nChanges() { nChanges() {
return $scope.reviewPanel.nVisibleSelectedChanges return ide.$scope.reviewPanel.nVisibleSelectedChanges
}, },
}, },
scope: $scope.$new(), scope: $scope.$new(),
@ -726,7 +740,7 @@ export default App.controller(
return $scope.toggleReviewPanel() return $scope.toggleReviewPanel()
} }
$scope.addNewComment = function (e) { ide.$scope.addNewComment = function (e) {
e.preventDefault() e.preventDefault()
$scope.$broadcast('comment:start_adding') $scope.$broadcast('comment:start_adding')
return $scope.toggleReviewPanel() return $scope.toggleReviewPanel()
@ -754,7 +768,7 @@ export default App.controller(
return $timeout(() => $scope.$broadcast('review-panel:layout')) return $timeout(() => $scope.$broadcast('review-panel:layout'))
} }
$scope.submitNewComment = function (content) { ide.$scope.submitNewComment = function (content) {
if (content == null || content === '') { if (content == null || content === '') {
return return
} }
@ -798,7 +812,7 @@ export default App.controller(
return $timeout(() => $scope.$broadcast('review-panel:layout')) return $timeout(() => $scope.$broadcast('review-panel:layout'))
} }
$scope.submitReply = function (entry, entry_id) { ide.$scope.submitReply = function (entry, entry_id) {
const { thread_id } = entry const { thread_id } = entry
const content = entry.replyContent const content = entry.replyContent
$http $http
@ -814,7 +828,9 @@ export default App.controller(
) )
const trackingMetadata = { const trackingMetadata = {
view: $scope.ui.reviewPanelOpen ? $scope.reviewPanel.subView : 'mini', view: $scope.ui.reviewPanelOpen
? ide.$scope.reviewPanel.subView
: 'mini',
size: entry.replyContent.length, size: entry.replyContent.length,
thread: thread_id, thread: thread_id,
} }
@ -833,7 +849,8 @@ export default App.controller(
return $scope.$broadcast('review-panel:layout') return $scope.$broadcast('review-panel:layout')
} }
$scope.resolveComment = function (entry, entry_id) { ide.$scope.resolveComment = function (doc_id, entry_id) {
const entry = getDocEntries(doc_id)[entry_id]
entry.focused = false entry.focused = false
$http.post( $http.post(
`/project/${$scope.project_id}/thread/${entry.thread_id}/resolve`, `/project/${$scope.project_id}/thread/${entry.thread_id}/resolve`,
@ -841,11 +858,13 @@ export default App.controller(
) )
_onCommentResolved(entry.thread_id, ide.$scope.user) _onCommentResolved(entry.thread_id, ide.$scope.user)
eventTracking.sendMB('rp-comment-resolve', { eventTracking.sendMB('rp-comment-resolve', {
view: $scope.ui.reviewPanelOpen ? $scope.reviewPanel.subView : 'mini', view: $scope.ui.reviewPanelOpen
? ide.$scope.reviewPanel.subView
: 'mini',
}) })
} }
$scope.unresolveComment = function (thread_id) { ide.$scope.unresolveComment = function (thread_id) {
_onCommentReopened(thread_id) _onCommentReopened(thread_id)
$http.post(`/project/${$scope.project_id}/thread/${thread_id}/reopen`, { $http.post(`/project/${$scope.project_id}/thread/${thread_id}/reopen`, {
_csrf: window.csrfToken, _csrf: window.csrfToken,
@ -861,7 +880,7 @@ export default App.controller(
thread.resolved = true thread.resolved = true
thread.resolved_by_user = formatUser(user) thread.resolved_by_user = formatUser(user)
thread.resolved_at = new Date().toISOString() thread.resolved_at = new Date().toISOString()
$scope.reviewPanel.resolvedThreadIds[thread_id] = true ide.$scope.reviewPanel.resolvedThreadIds[thread_id] = true
$scope.$broadcast('comment:resolve_threads', [thread_id]) $scope.$broadcast('comment:resolve_threads', [thread_id])
dispatchReviewPanelEvent('comment:resolve_threads', [thread_id]) dispatchReviewPanelEvent('comment:resolve_threads', [thread_id])
} }
@ -874,14 +893,14 @@ export default App.controller(
delete thread.resolved delete thread.resolved
delete thread.resolved_by_user delete thread.resolved_by_user
delete thread.resolved_at delete thread.resolved_at
delete $scope.reviewPanel.resolvedThreadIds[thread_id] delete ide.$scope.reviewPanel.resolvedThreadIds[thread_id]
$scope.$broadcast('comment:unresolve_thread', thread_id) $scope.$broadcast('comment:unresolve_thread', thread_id)
dispatchReviewPanelEvent('comment:unresolve_thread', thread_id) dispatchReviewPanelEvent('comment:unresolve_thread', thread_id)
} }
function _onThreadDeleted(thread_id) { function _onThreadDeleted(thread_id) {
delete $scope.reviewPanel.resolvedThreadIds[thread_id] delete ide.$scope.reviewPanel.resolvedThreadIds[thread_id]
delete $scope.reviewPanel.commentThreads[thread_id] delete ide.$scope.reviewPanel.commentThreads[thread_id]
$scope.$broadcast('comment:remove', thread_id) $scope.$broadcast('comment:remove', thread_id)
dispatchReviewPanelEvent('comment:remove', thread_id) dispatchReviewPanelEvent('comment:remove', thread_id)
} }
@ -908,7 +927,7 @@ export default App.controller(
return updateEntries() return updateEntries()
} }
$scope.deleteThread = function (entry_id, doc_id, thread_id) { ide.$scope.deleteThread = function (entry_id, doc_id, thread_id) {
_onThreadDeleted(thread_id) _onThreadDeleted(thread_id)
$http({ $http({
method: 'DELETE', method: 'DELETE',
@ -920,22 +939,22 @@ export default App.controller(
eventTracking.sendMB('rp-comment-delete') eventTracking.sendMB('rp-comment-delete')
} }
$scope.saveEdit = function (thread_id, comment) { ide.$scope.saveEdit = function (thread_id, comment_id, content) {
$http.post( $http.post(
`/project/${$scope.project_id}/thread/${thread_id}/messages/${comment.id}/edit`, `/project/${$scope.project_id}/thread/${thread_id}/messages/${comment_id}/edit`,
{ {
content: comment.content, content,
_csrf: window.csrfToken, _csrf: window.csrfToken,
} }
) )
return $timeout(() => $scope.$broadcast('review-panel:layout')) return $timeout(() => $scope.$broadcast('review-panel:layout'))
} }
$scope.deleteComment = function (thread_id, comment) { ide.$scope.deleteComment = function (thread_id, comment_id) {
_onCommentDeleted(thread_id, comment.id) _onCommentDeleted(thread_id, comment_id)
$http({ $http({
method: 'DELETE', method: 'DELETE',
url: `/project/${$scope.project_id}/thread/${thread_id}/messages/${comment.id}`, url: `/project/${$scope.project_id}/thread/${thread_id}/messages/${comment_id}`,
headers: { headers: {
'X-CSRF-Token': window.csrfToken, 'X-CSRF-Token': window.csrfToken,
}, },
@ -944,17 +963,17 @@ export default App.controller(
} }
$scope.setSubView = function (subView) { $scope.setSubView = function (subView) {
$scope.reviewPanel.subView = subView ide.$scope.reviewPanel.subView = subView
eventTracking.sendMB('rp-subview-change', { subView }) eventTracking.sendMB('rp-subview-change', { subView })
} }
$scope.gotoEntry = (doc_id, entry) => ide.$scope.gotoEntry = (doc_id, entry_offset) =>
ide.editorManager.openDocId(doc_id, { gotoOffset: entry.offset }) ide.editorManager.openDocId(doc_id, { gotoOffset: entry_offset })
$scope.toggleFullTCStateCollapse = function () { $scope.toggleFullTCStateCollapse = function () {
if ($scope.project.features.trackChanges) { if ($scope.project.features.trackChanges) {
return ($scope.reviewPanel.fullTCStateCollapsed = return (ide.$scope.reviewPanel.fullTCStateCollapsed =
!$scope.reviewPanel.fullTCStateCollapsed) !ide.$scope.reviewPanel.fullTCStateCollapsed)
} else { } else {
_sendAnalytics() _sendAnalytics()
return $scope.openTrackChangesUpgradeModal() return $scope.openTrackChangesUpgradeModal()
@ -976,10 +995,10 @@ export default App.controller(
if (isLocal == null) { if (isLocal == null) {
isLocal = false isLocal = false
} }
if ($scope.reviewPanel.trackChangesState[userId] == null) { if (ide.$scope.reviewPanel.trackChangesState[userId] == null) {
$scope.reviewPanel.trackChangesState[userId] = {} ide.$scope.reviewPanel.trackChangesState[userId] = {}
} }
const state = $scope.reviewPanel.trackChangesState[userId] const state = ide.$scope.reviewPanel.trackChangesState[userId]
if ( if (
state.syncState == null || state.syncState == null ||
@ -1006,7 +1025,7 @@ export default App.controller(
if (isLocal == null) { if (isLocal == null) {
isLocal = false isLocal = false
} }
$scope.reviewPanel.trackChangesOnForEveryone = newValue ide.$scope.reviewPanel.trackChangesOnForEveryone = newValue
const { project } = $scope const { project } = $scope
for (const member of Array.from(project.members)) { for (const member of Array.from(project.members)) {
_setUserTCState(member._id, newValue, isLocal) _setUserTCState(member._id, newValue, isLocal)
@ -1019,7 +1038,7 @@ export default App.controller(
if (isLocal == null) { if (isLocal == null) {
isLocal = false isLocal = false
} }
$scope.reviewPanel.trackChangesOnForGuests = newValue ide.$scope.reviewPanel.trackChangesOnForGuests = newValue
if ( if (
currentUserType() === UserTypes.GUEST || currentUserType() === UserTypes.GUEST ||
currentUserType() === UserTypes.ANONYMOUS currentUserType() === UserTypes.ANONYMOUS
@ -1030,15 +1049,15 @@ export default App.controller(
const applyClientTrackChangesStateToServer = function () { const applyClientTrackChangesStateToServer = function () {
const data = {} const data = {}
if ($scope.reviewPanel.trackChangesOnForEveryone) { if (ide.$scope.reviewPanel.trackChangesOnForEveryone) {
data.on = true data.on = true
} else { } else {
data.on_for = {} data.on_for = {}
for (const userId in $scope.reviewPanel.trackChangesState) { for (const userId in ide.$scope.reviewPanel.trackChangesState) {
const userState = $scope.reviewPanel.trackChangesState[userId] const userState = ide.$scope.reviewPanel.trackChangesState[userId]
data.on_for[userId] = userState.value data.on_for[userId] = userState.value
} }
if ($scope.reviewPanel.trackChangesOnForGuests) { if (ide.$scope.reviewPanel.trackChangesOnForGuests) {
data.on_for_guests = true data.on_for_guests = true
} }
} }
@ -1052,7 +1071,7 @@ export default App.controller(
return _setGuestsTCState(state) return _setGuestsTCState(state)
} else { } else {
const { project } = $scope const { project } = $scope
$scope.reviewPanel.trackChangesOnForEveryone = false ide.$scope.reviewPanel.trackChangesOnForEveryone = false
_setGuestsTCState(state.__guests__ === true) _setGuestsTCState(state.__guests__ === true)
for (const member of Array.from(project.members)) { for (const member of Array.from(project.members)) {
_setUserTCState( _setUserTCState(
@ -1069,18 +1088,18 @@ export default App.controller(
} }
} }
$scope.toggleTrackChangesForEveryone = function (onForEveryone) { ide.$scope.toggleTrackChangesForEveryone = function (onForEveryone) {
_setEveryoneTCState(onForEveryone, true) _setEveryoneTCState(onForEveryone, true)
_setGuestsTCState(onForEveryone, true) _setGuestsTCState(onForEveryone, true)
return applyClientTrackChangesStateToServer() return applyClientTrackChangesStateToServer()
} }
$scope.toggleTrackChangesForGuests = function (onForGuests) { ide.$scope.toggleTrackChangesForGuests = function (onForGuests) {
_setGuestsTCState(onForGuests, true) _setGuestsTCState(onForGuests, true)
return applyClientTrackChangesStateToServer() return applyClientTrackChangesStateToServer()
} }
$scope.toggleTrackChangesForUser = function (onForUser, userId) { ide.$scope.toggleTrackChangesForUser = function (onForUser, userId) {
_setUserTCState(userId, onForUser, true) _setUserTCState(userId, onForUser, true)
return applyClientTrackChangesStateToServer() return applyClientTrackChangesStateToServer()
} }
@ -1099,13 +1118,13 @@ export default App.controller(
return return
} }
return $scope.toggleTrackChangesForUser( return $scope.toggleTrackChangesForUser(
!$scope.reviewPanel.trackChangesState[ide.$scope.user.id].value, !ide.$scope.reviewPanel.trackChangesState[ide.$scope.user.id].value,
ide.$scope.user.id ide.$scope.user.id
) )
} }
const setGuestFeatureBasedOnProjectAccessLevel = projectPublicAccessLevel => const setGuestFeatureBasedOnProjectAccessLevel = projectPublicAccessLevel =>
($scope.reviewPanel.trackChangesForGuestsAvailable = (ide.$scope.reviewPanel.trackChangesForGuestsAvailable =
projectPublicAccessLevel === 'tokenBased') projectPublicAccessLevel === 'tokenBased')
const onToggleTrackChangesForGuestsAvailability = function (available) { const onToggleTrackChangesForGuestsAvailability = function (available) {
@ -1113,10 +1132,10 @@ export default App.controller(
if (available) { if (available) {
return return
} }
if (!$scope.reviewPanel.trackChangesOnForGuests) { if (!ide.$scope.reviewPanel.trackChangesOnForGuests) {
return return
} // Already turned off } // Already turned off
if ($scope.reviewPanel.trackChangesOnForEveryone) { if (ide.$scope.reviewPanel.trackChangesOnForEveryone) {
return return
} // Overrides guest setting } // Overrides guest setting
return $scope.toggleTrackChangesForGuests(false) return $scope.toggleTrackChangesForGuests(false)
@ -1153,7 +1172,6 @@ export default App.controller(
let _refreshingRangeUsers = false let _refreshingRangeUsers = false
const _refreshedForUserIds = {} const _refreshedForUserIds = {}
function refreshChangeUsers(refresh_for_user_id) { function refreshChangeUsers(refresh_for_user_id) {
if (refresh_for_user_id != null) { if (refresh_for_user_id != null) {
if (_refreshedForUserIds[refresh_for_user_id] != null) { if (_refreshedForUserIds[refresh_for_user_id] != null) {
@ -1174,7 +1192,7 @@ export default App.controller(
.then(function (response) { .then(function (response) {
const users = response.data const users = response.data
_refreshingRangeUsers = false _refreshingRangeUsers = false
$scope.users = {} ide.$scope.users = $scope.users = {}
// Always include ourself, since if we submit an op, we might need to display info // Always include ourself, since if we submit an op, we might need to display info
// about it locally before it has been flushed through the server // about it locally before it has been flushed through the server
if ( if (
@ -1198,7 +1216,6 @@ export default App.controller(
} }
let _threadsLoaded = false let _threadsLoaded = false
function ensureThreadsAreLoaded() { function ensureThreadsAreLoaded() {
if (_threadsLoaded) { if (_threadsLoaded) {
// We get any updates in real time so only need to load them once. // We get any updates in real time so only need to load them once.
@ -1211,9 +1228,8 @@ export default App.controller(
.then(function (response) { .then(function (response) {
const threads = response.data const threads = response.data
ide.$scope.loadingThreads = false ide.$scope.loadingThreads = false
for (const thread_id in $scope.reviewPanel.resolvedThreadIds) { for (const thread_id in ide.$scope.reviewPanel.resolvedThreadIds) {
const _ = $scope.reviewPanel.resolvedThreadIds[thread_id] delete ide.$scope.reviewPanel.resolvedThreadIds[thread_id]
delete $scope.reviewPanel.resolvedThreadIds[thread_id]
} }
for (const thread_id in threads) { for (const thread_id in threads) {
const thread = threads[thread_id] const thread = threads[thread_id]
@ -1222,11 +1238,11 @@ export default App.controller(
} }
if (thread.resolved_by_user != null) { if (thread.resolved_by_user != null) {
thread.resolved_by_user = formatUser(thread.resolved_by_user) thread.resolved_by_user = formatUser(thread.resolved_by_user)
$scope.reviewPanel.resolvedThreadIds[thread_id] = true ide.$scope.reviewPanel.resolvedThreadIds[thread_id] = true
$scope.$broadcast('comment:resolve_threads', [thread_id]) $scope.$broadcast('comment:resolve_threads', [thread_id])
} }
} }
$scope.reviewPanel.commentThreads = threads ide.$scope.reviewPanel.commentThreads = threads
dispatchReviewPanelEvent('loaded_threads') dispatchReviewPanelEvent('loaded_threads')
return $timeout(() => $scope.$broadcast('review-panel:layout')) return $timeout(() => $scope.$broadcast('review-panel:layout'))
}) })
@ -1293,7 +1309,7 @@ export default App.controller(
switch (type) { switch (type) {
case 'line-height': { case 'line-height': {
$scope.reviewPanel.rendererData.lineHeight = payload ide.$scope.reviewPanel.rendererData.lineHeight = payload
$scope.$broadcast('review-panel:layout') $scope.$broadcast('review-panel:layout')
break break
} }
@ -1325,11 +1341,14 @@ export default App.controller(
} }
case 'toggle-review-panel': { case 'toggle-review-panel': {
ide.toggleReviewPanel() $scope.toggleReviewPanel()
break break
} }
} }
}) })
// Add methods somewhere that React can see them
$scope.reviewPanel.saveEdit = $scope.saveEdit
} }
) )

View file

@ -1109,3 +1109,115 @@ button when (@is-overleaf-light = true) {
.rp-flex-block { .rp-flex-block {
display: block; display: block;
} }
// CM6-specific review panel rules
.ol-cm-review-panel {
display: block;
flex-shrink: 0;
.rp-size-expanded & {
display: flex;
flex-direction: column;
width: @review-panel-width;
overflow: visible;
border-left-width: 1px;
}
.rp-size-mini & {
width: @review-off-width;
z-index: 6;
border-left-width: 1px;
}
background-color: @rp-bg-blue;
border-left: solid 0 @rp-border-grey;
font-family: @font-family-base;
line-height: @line-height-base;
font-size: @rp-base-font-size;
color: @rp-type-blue;
z-index: 6;
position: relative;
box-sizing: content-box;
flex-direction: column;
.review-panel-toolbar {
& .review-panel-toolbar-collapse-button {
border: none;
background: none;
padding: 0;
}
}
.rp-state-current-file & {
.review-panel-tools {
display: flex;
flex-direction: column;
justify-content: space-between;
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
}
.review-panel-toolbar {
position: sticky;
top: 0;
}
.rp-nav {
position: sticky;
bottom: 0;
}
.review-panel-toggler {
position: sticky;
top: 0;
}
}
.rp-state-overview & {
position: sticky;
top: 0;
display: flex;
flex-direction: column;
.review-panel-toggler {
position: absolute;
top: 0;
bottom: 0;
left: 0;
}
}
.rp-nav-item {
background: none;
}
.rp-entry-list {
display: inline-flex;
flex-direction: column;
.rp-overview-file {
.rp-overview-file-entries {
//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;
}
}
}
}
}