mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-07 20:31:06 -05:00
Merge pull request #15999 from overleaf/ii-ide-page-prototype-review-panel-accept-reject-changes
[web] React ide page accept/reject changes GitOrigin-RevId: 0bb8e3759c7edbef16be04b2f200ae3686c3a53c
This commit is contained in:
parent
ebca8c1919
commit
de945a432d
6 changed files with 228 additions and 46 deletions
|
@ -6,7 +6,10 @@ import useSocketListener from '@/features/ide-react/hooks/use-socket-listener'
|
|||
import useAsync from '@/shared/hooks/use-async'
|
||||
import useAbortController from '@/shared/hooks/use-abort-controller'
|
||||
import { sendMB } from '../../../../../infrastructure/event-tracking'
|
||||
import { dispatchReviewPanelLayout as handleLayoutChange } from '@/features/source-editor/extensions/changes/change-manager'
|
||||
import {
|
||||
dispatchReviewPanelLayout as handleLayoutChange,
|
||||
UpdateType,
|
||||
} from '@/features/source-editor/extensions/changes/change-manager'
|
||||
import { useProjectContext } from '@/shared/context/project-context'
|
||||
import { useLayoutContext } from '@/shared/context/layout-context'
|
||||
import { useUserContext } from '@/shared/context/user-context'
|
||||
|
@ -33,12 +36,15 @@ import { PublicAccessLevel } from '../../../../../../../types/public-access-leve
|
|||
import { ReviewPanelStateReactIde } from '../types/review-panel-state'
|
||||
import {
|
||||
DeepReadonly,
|
||||
Entries,
|
||||
MergeAndOverride,
|
||||
} from '../../../../../../../types/utils'
|
||||
import { ReviewPanelCommentThread } from '../../../../../../../types/review-panel/comment-thread'
|
||||
import { DocId } from '../../../../../../../types/project-settings'
|
||||
import {
|
||||
ReviewPanelAddCommentEntry,
|
||||
ReviewPanelAggregateChangeEntry,
|
||||
ReviewPanelBulkActionsEntry,
|
||||
ReviewPanelChangeEntry,
|
||||
ReviewPanelCommentEntry,
|
||||
ReviewPanelEntry,
|
||||
|
@ -130,9 +136,13 @@ function useReviewPanelState(): ReviewPanelStateReactIde {
|
|||
const [loading] = useScopeValue<ReviewPanel.Value<'loading'>>(
|
||||
'reviewPanel.overview.loading'
|
||||
)
|
||||
const [nVisibleSelectedChanges] = useScopeValue<
|
||||
ReviewPanel.Value<'nVisibleSelectedChanges'>
|
||||
>('reviewPanel.nVisibleSelectedChanges')
|
||||
// All selected changes. If an aggregated change (insertion + deletion) is selected, the two ids
|
||||
// will be present. The length of this array will differ from the count below (see explanation).
|
||||
const selectedEntryIds = useRef<ThreadId[]>([])
|
||||
// A count of user-facing selected changes. An aggregated change (insertion + deletion) will count
|
||||
// as only one.
|
||||
const [nVisibleSelectedChanges, setNVisibleSelectedChanges] =
|
||||
useState<ReviewPanel.Value<'nVisibleSelectedChanges'>>(0)
|
||||
const [collapsed, setCollapsed] = usePersistedState<
|
||||
ReviewPanel.Value<'collapsed'>
|
||||
>(`docs_collapsed_state:${projectId}`, {}, false, true)
|
||||
|
@ -333,7 +343,9 @@ function useReviewPanelState(): ReviewPanelStateReactIde {
|
|||
// Assume we'll delete everything until we see it, then we'll remove it from this object
|
||||
const deleteChanges = new Set<keyof ReviewPanelDocEntries>()
|
||||
|
||||
for (const [id, change] of Object.entries(docEntries)) {
|
||||
for (const [id, change] of Object.entries(docEntries) as Entries<
|
||||
typeof docEntries
|
||||
>) {
|
||||
if (
|
||||
'entry_ids' in change &&
|
||||
id !== 'add-comment' &&
|
||||
|
@ -344,7 +356,9 @@ function useReviewPanelState(): ReviewPanelStateReactIde {
|
|||
}
|
||||
}
|
||||
}
|
||||
for (const [, change] of Object.entries(docResolvedComments)) {
|
||||
for (const [, change] of Object.entries(docResolvedComments) as Entries<
|
||||
typeof docResolvedComments
|
||||
>) {
|
||||
if ('entry_ids' in change) {
|
||||
for (const entryId of change.entry_ids) {
|
||||
deleteChanges.add(entryId)
|
||||
|
@ -383,10 +397,9 @@ function useReviewPanelState(): ReviewPanelStateReactIde {
|
|||
offset: change.op.p,
|
||||
metadata: change.metadata,
|
||||
}
|
||||
const newEntryEntries = Object.entries(newEntry) as [
|
||||
[keyof typeof newEntry, typeof newEntry[keyof typeof newEntry]]
|
||||
]
|
||||
for (const [key, value] of newEntryEntries) {
|
||||
for (const [key, value] of Object.entries(newEntry) as Entries<
|
||||
typeof newEntry
|
||||
>) {
|
||||
const entriesTyped = docEntries[change.id] as Record<any, any>
|
||||
entriesTyped[key] = value
|
||||
}
|
||||
|
@ -417,9 +430,6 @@ function useReviewPanelState(): ReviewPanelStateReactIde {
|
|||
content: comment.op.c,
|
||||
offset: comment.op.p,
|
||||
}
|
||||
const newEntryEntries = Object.entries(newEntry) as [
|
||||
[keyof typeof newEntry, typeof newEntry[keyof typeof newEntry]]
|
||||
]
|
||||
|
||||
let newComment: any
|
||||
if (localResolvedThreadIds[comment.op.t]) {
|
||||
|
@ -432,7 +442,9 @@ function useReviewPanelState(): ReviewPanelStateReactIde {
|
|||
delete docResolvedComments[comment.id]
|
||||
}
|
||||
|
||||
for (const [key, value] of newEntryEntries) {
|
||||
for (const [key, value] of Object.entries(newEntry) as Entries<
|
||||
typeof newEntry
|
||||
>) {
|
||||
newComment[key] = value
|
||||
}
|
||||
}
|
||||
|
@ -827,6 +839,8 @@ function useReviewPanelState(): ReviewPanelStateReactIde {
|
|||
(entry: { thread_id: ThreadId; replyContent: string }) => void
|
||||
>('submitReply')
|
||||
|
||||
const view = reviewPanelOpen ? subView : 'mini'
|
||||
|
||||
const toggleReviewPanel = useCallback(() => {
|
||||
if (!trackChangesVisible) {
|
||||
return
|
||||
|
@ -870,18 +884,9 @@ function useReviewPanelState(): ReviewPanelStateReactIde {
|
|||
|
||||
postJSON(`/project/${projectId}/thread/${entry.thread_id}/resolve`)
|
||||
onCommentResolved(entry.thread_id, user)
|
||||
sendMB('rp-comment-resolve', {
|
||||
view: reviewPanelOpen ? subView : 'mini',
|
||||
})
|
||||
sendMB('rp-comment-resolve', { view })
|
||||
},
|
||||
[
|
||||
getDocEntries,
|
||||
onCommentResolved,
|
||||
projectId,
|
||||
reviewPanelOpen,
|
||||
subView,
|
||||
user,
|
||||
]
|
||||
[getDocEntries, onCommentResolved, projectId, user, view]
|
||||
)
|
||||
|
||||
const onCommentReopened = useCallback(
|
||||
|
@ -979,6 +984,47 @@ function useReviewPanelState(): ReviewPanelStateReactIde {
|
|||
[onCommentDeleted, projectId]
|
||||
)
|
||||
|
||||
const doAcceptChanges = useCallback(
|
||||
(entryIds: ThreadId[]) => {
|
||||
const url = `/project/${projectId}/doc/${currentDocumentId}/changes/accept`
|
||||
postJSON(url, { body: { change_ids: entryIds } }).catch(
|
||||
debugConsole.error
|
||||
)
|
||||
dispatchReviewPanelEvent('changes:accept', entryIds)
|
||||
},
|
||||
[currentDocumentId, projectId]
|
||||
)
|
||||
|
||||
const acceptChanges = useCallback(
|
||||
(entryIds: ThreadId[]) => {
|
||||
doAcceptChanges(entryIds)
|
||||
sendMB('rp-changes-accepted', { view })
|
||||
},
|
||||
[doAcceptChanges, view]
|
||||
)
|
||||
|
||||
const doRejectChanges = useCallback((entryIds: ThreadId[]) => {
|
||||
dispatchReviewPanelEvent('changes:reject', entryIds)
|
||||
}, [])
|
||||
|
||||
const rejectChanges = useCallback(
|
||||
(entryIds: ThreadId[]) => {
|
||||
doRejectChanges(entryIds)
|
||||
sendMB('rp-changes-rejected', { view })
|
||||
},
|
||||
[doRejectChanges, view]
|
||||
)
|
||||
|
||||
const bulkAcceptActions = useCallback(() => {
|
||||
doAcceptChanges(selectedEntryIds.current)
|
||||
sendMB('rp-bulk-accept', { view, nEntries: nVisibleSelectedChanges })
|
||||
}, [doAcceptChanges, nVisibleSelectedChanges, view])
|
||||
|
||||
const bulkRejectActions = useCallback(() => {
|
||||
doRejectChanges(selectedEntryIds.current)
|
||||
sendMB('rp-bulk-reject', { view, nEntries: nVisibleSelectedChanges })
|
||||
}, [doRejectChanges, nVisibleSelectedChanges, view])
|
||||
|
||||
const refreshRanges = useCallback(() => {
|
||||
type Doc = {
|
||||
id: DocId
|
||||
|
@ -1020,19 +1066,6 @@ function useReviewPanelState(): ReviewPanelStateReactIde {
|
|||
updateEntries,
|
||||
])
|
||||
|
||||
const [acceptChanges] =
|
||||
useScopeValue<ReviewPanel.UpdaterFn<'acceptChanges'>>('acceptChanges')
|
||||
const [rejectChanges] =
|
||||
useScopeValue<ReviewPanel.UpdaterFn<'rejectChanges'>>('rejectChanges')
|
||||
const [bulkAcceptActions] =
|
||||
useScopeValue<ReviewPanel.UpdaterFn<'bulkAcceptActions'>>(
|
||||
'bulkAcceptActions'
|
||||
)
|
||||
const [bulkRejectActions] =
|
||||
useScopeValue<ReviewPanel.UpdaterFn<'bulkRejectActions'>>(
|
||||
'bulkRejectActions'
|
||||
)
|
||||
|
||||
const handleSetSubview = useCallback((subView: SubView) => {
|
||||
setSubView(subView)
|
||||
sendMB('rp-subview-change', { subView })
|
||||
|
@ -1075,9 +1108,132 @@ function useReviewPanelState(): ReviewPanelStateReactIde {
|
|||
handleLayoutChange()
|
||||
}
|
||||
|
||||
const editorFocusChanged = (
|
||||
selectionOffsetStart: number,
|
||||
selectionOffsetEnd: number,
|
||||
selection: boolean,
|
||||
updateType: UpdateType
|
||||
) => {
|
||||
let tempEntries = {
|
||||
...getDocEntries(currentDocumentId),
|
||||
}
|
||||
// All selected changes will be added to this array.
|
||||
selectedEntryIds.current = []
|
||||
// Count of user-visible changes, i.e. an aggregated change will count as one.
|
||||
let tempNVisibleSelectedChanges = 0
|
||||
|
||||
const offset = selectionOffsetStart
|
||||
const length = selectionOffsetEnd - selectionOffsetStart
|
||||
|
||||
// Recreate the add comment and bulk actions entries only when
|
||||
// necessary. This is to avoid the UI thinking that these entries have
|
||||
// changed and getting into an infinite loop.
|
||||
if (selection) {
|
||||
const existingAddComment = tempEntries[
|
||||
'add-comment'
|
||||
] as ReviewPanelAddCommentEntry
|
||||
if (
|
||||
!existingAddComment ||
|
||||
existingAddComment.offset !== offset ||
|
||||
existingAddComment.length !== length
|
||||
) {
|
||||
tempEntries['add-comment'] = {
|
||||
type: 'add-comment',
|
||||
offset,
|
||||
length,
|
||||
} as ReviewPanelAddCommentEntry
|
||||
}
|
||||
const existingBulkActions = tempEntries[
|
||||
'bulk-actions'
|
||||
] as ReviewPanelBulkActionsEntry
|
||||
if (
|
||||
!existingBulkActions ||
|
||||
existingBulkActions.offset !== offset ||
|
||||
existingBulkActions.length !== length
|
||||
) {
|
||||
tempEntries['bulk-actions'] = {
|
||||
type: 'bulk-actions',
|
||||
offset,
|
||||
length,
|
||||
} as ReviewPanelBulkActionsEntry
|
||||
}
|
||||
} else {
|
||||
delete (tempEntries as Partial<typeof tempEntries>)['add-comment']
|
||||
delete (tempEntries as Partial<typeof tempEntries>)['bulk-actions']
|
||||
}
|
||||
|
||||
for (const [key, entry] of Object.entries(tempEntries) as Entries<
|
||||
typeof tempEntries
|
||||
>) {
|
||||
let isChangeEntryAndWithinSelection = false
|
||||
if (entry.type === 'comment' && !resolvedThreadIds[entry.thread_id]) {
|
||||
tempEntries = {
|
||||
...tempEntries,
|
||||
[key]: {
|
||||
...tempEntries[key],
|
||||
focused:
|
||||
entry.offset <= selectionOffsetStart &&
|
||||
selectionOffsetStart <= entry.offset + entry.content.length,
|
||||
},
|
||||
}
|
||||
} else if (
|
||||
entry.type === 'insert' ||
|
||||
entry.type === 'aggregate-change'
|
||||
) {
|
||||
isChangeEntryAndWithinSelection =
|
||||
entry.offset >= selectionOffsetStart &&
|
||||
entry.offset + entry.content.length <= selectionOffsetEnd
|
||||
tempEntries = {
|
||||
...tempEntries,
|
||||
[key]: {
|
||||
...tempEntries[key],
|
||||
focused:
|
||||
entry.offset <= selectionOffsetStart &&
|
||||
selectionOffsetStart <= entry.offset + entry.content.length,
|
||||
},
|
||||
}
|
||||
} else if (entry.type === 'delete') {
|
||||
isChangeEntryAndWithinSelection =
|
||||
selectionOffsetStart <= entry.offset &&
|
||||
entry.offset <= selectionOffsetEnd
|
||||
tempEntries = {
|
||||
...tempEntries,
|
||||
[key]: {
|
||||
...tempEntries[key],
|
||||
focused: entry.offset === selectionOffsetStart,
|
||||
},
|
||||
}
|
||||
} else if (
|
||||
['add-comment', 'bulk-actions'].includes(entry.type) &&
|
||||
selection
|
||||
) {
|
||||
tempEntries = {
|
||||
...tempEntries,
|
||||
[key]: { ...tempEntries[key], focused: true },
|
||||
}
|
||||
}
|
||||
if (isChangeEntryAndWithinSelection) {
|
||||
const entryIds = 'entry_ids' in entry ? entry.entry_ids : []
|
||||
for (const entryId of entryIds) {
|
||||
selectedEntryIds.current.push(entryId)
|
||||
}
|
||||
tempNVisibleSelectedChanges++
|
||||
}
|
||||
}
|
||||
setEntries(prev => ({ ...prev, [currentDocumentId]: tempEntries }))
|
||||
setNVisibleSelectedChanges(tempNVisibleSelectedChanges)
|
||||
dispatchReviewPanelEvent('recalculate-screen-positions', {
|
||||
entries: tempEntries,
|
||||
updateType,
|
||||
})
|
||||
// Ensure that watchers, such as the React-based review panel component,
|
||||
// are informed of the changes to entries
|
||||
handleLayoutChange()
|
||||
}
|
||||
|
||||
const handleEditorEvents = (e: Event) => {
|
||||
const event = e as CustomEvent
|
||||
const { type } = event.detail
|
||||
const { type, payload } = event.detail
|
||||
|
||||
switch (type) {
|
||||
case 'track-changes:changed': {
|
||||
|
@ -1085,6 +1241,12 @@ function useReviewPanelState(): ReviewPanelStateReactIde {
|
|||
break
|
||||
}
|
||||
|
||||
case 'focus:changed': {
|
||||
const { from, to, empty, updateType } = payload
|
||||
editorFocusChanged(from, to, !empty, updateType)
|
||||
break
|
||||
}
|
||||
|
||||
case 'toggle-track-changes': {
|
||||
toggleTrackChangesFromKbdShortcut()
|
||||
break
|
||||
|
@ -1099,6 +1261,8 @@ function useReviewPanelState(): ReviewPanelStateReactIde {
|
|||
}
|
||||
}, [
|
||||
currentDocumentId,
|
||||
getDocEntries,
|
||||
resolvedThreadIds,
|
||||
toggleTrackChangesForUser,
|
||||
trackChanges,
|
||||
trackChangesState,
|
||||
|
@ -1112,6 +1276,21 @@ function useReviewPanelState(): ReviewPanelStateReactIde {
|
|||
useSocketListener(socket, 'resolve-thread', onCommentResolved)
|
||||
useSocketListener(socket, 'edit-message', onCommentEdited)
|
||||
useSocketListener(socket, 'delete-message', onCommentDeleted)
|
||||
useSocketListener(
|
||||
socket,
|
||||
'accept-changes',
|
||||
useCallback(
|
||||
(docId: DocId, entryIds: ThreadId[]) => {
|
||||
if (docId !== currentDocumentId) {
|
||||
getChangeTracker(docId).removeChangeIds(entryIds)
|
||||
} else {
|
||||
dispatchReviewPanelEvent('changes:accept', entryIds)
|
||||
}
|
||||
updateEntries(docId)
|
||||
},
|
||||
[currentDocumentId, getChangeTracker, updateEntries]
|
||||
)
|
||||
)
|
||||
|
||||
const values = useMemo<ReviewPanelStateReactIde['values']>(
|
||||
() => ({
|
||||
|
|
|
@ -8,10 +8,7 @@ export default function populateReviewPanelScope(store: ReactScopeValueStore) {
|
|||
store.set('reviewPanel.rendererData.lineHeight', 0)
|
||||
store.set('submitNewComment', async () => {})
|
||||
store.set('gotoEntry', () => {})
|
||||
store.set('acceptChanges', () => {})
|
||||
store.set('rejectChanges', () => {})
|
||||
store.set('bulkAcceptActions', () => {})
|
||||
store.set('bulkRejectActions', () => {})
|
||||
store.set('saveEdit', () => {})
|
||||
store.set('submitReply', () => {})
|
||||
store.set('addNewComment', () => {})
|
||||
}
|
||||
|
|
|
@ -58,8 +58,8 @@ export interface ReviewPanelState {
|
|||
resolveComment: (docId: DocId, entryId: ThreadId) => void
|
||||
deleteComment: (threadId: ThreadId, commentId: CommentId) => void
|
||||
submitReply: (threadId: ThreadId, replyContent: string) => void
|
||||
acceptChanges: (entryIds: unknown) => void
|
||||
rejectChanges: (entryIds: unknown) => void
|
||||
acceptChanges: (entryIds: ThreadId[]) => void
|
||||
rejectChanges: (entryIds: ThreadId[]) => void
|
||||
toggleTrackChangesForEveryone: (onForEveryone: boolean) => void
|
||||
toggleTrackChangesForUser: (onForUser: boolean, userId: UserId) => void
|
||||
toggleTrackChangesForGuests: (onForGuests: boolean) => void
|
||||
|
@ -97,6 +97,7 @@ export interface ReviewPanelState {
|
|||
>
|
||||
}
|
||||
}
|
||||
|
||||
/* eslint-enable no-use-before-define */
|
||||
|
||||
// Getter for values
|
||||
|
|
|
@ -63,7 +63,7 @@ export type ChangeManager = {
|
|||
destroy: () => void
|
||||
}
|
||||
|
||||
type UpdateType =
|
||||
export type UpdateType =
|
||||
| 'edit'
|
||||
| 'selectionChange'
|
||||
| 'geometryChange'
|
||||
|
|
|
@ -61,6 +61,7 @@ export interface ReviewPanelAggregateChangeEntry extends ReviewPanelBaseEntry {
|
|||
|
||||
export interface ReviewPanelAddCommentEntry extends ReviewPanelBaseEntry {
|
||||
type: 'add-comment'
|
||||
length: number
|
||||
}
|
||||
|
||||
export interface ReviewPanelBulkActionsEntry extends ReviewPanelBaseEntry {
|
||||
|
|
|
@ -18,3 +18,7 @@ export type DeepReadonly<T> = T extends (infer R)[]
|
|||
export type DeepPartial<T> = Partial<{ [P in keyof T]: DeepPartial<T[P]> }>
|
||||
|
||||
export type MergeAndOverride<Parent, Own> = Own & Omit<Parent, keyof Own>
|
||||
|
||||
export type Entries<T extends object> = [keyof T, T[keyof T]][]
|
||||
|
||||
export type Keys<T extends object> = (keyof T)[]
|
||||
|
|
Loading…
Reference in a new issue