|
|
@@ -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']>(
|
|
|
() => ({
|