فهرست منبع

Merge pull request #15397 from overleaf/ii-ide-page-prototype-review-panel

Init review panel for React IDE page

GitOrigin-RevId: fc23201055ae892c5c1d5cb88e472a0bb0cd6c25
ilkin-overleaf 2 سال پیش
والد
کامیت
0c403bf8e3

+ 3 - 1
services/web/frontend/js/features/ide-react/context/ide-react-context.tsx

@@ -9,6 +9,7 @@ import React, {
 } from 'react'
 import { ReactScopeValueStore } from '@/features/ide-react/scope-value-store/react-scope-value-store'
 import populateLayoutScope from '@/features/ide-react/scope-adapters/layout-context-adapter'
+import populateReviewPanelScope from '@/features/ide-react/scope-adapters/review-panel-context-adapter'
 import { IdeProvider } from '@/shared/context/ide-context'
 import {
   createIdeEventEmitter,
@@ -22,8 +23,8 @@ import { postJSON } from '@/infrastructure/fetch-json'
 import { EventLog } from '@/features/ide-react/editor/event-log'
 import { populateSettingsScope } from '@/features/ide-react/scope-adapters/settings-adapter'
 import { populateOnlineUsersScope } from '@/features/ide-react/context/online-users-context'
-import { ReactScopeEventEmitter } from '@/features/ide-react/scope-event-emitter/react-scope-event-emitter'
 import { populateReferenceScope } from '@/features/ide-react/context/references-context'
+import { ReactScopeEventEmitter } from '@/features/ide-react/scope-event-emitter/react-scope-event-emitter'
 
 type IdeReactContextValue = {
   projectId: string
@@ -75,6 +76,7 @@ function createReactScopeValueStore() {
   populateOnlineUsersScope(scopeStore)
   populateReferenceScope(scopeStore)
   populateFileTreeScope(scopeStore)
+  populateReviewPanelScope(scopeStore)
 
   scopeStore.allowNonExistentPath('hasLintingError')
   scopeStore.allowNonExistentPath('loadingThreads')

+ 273 - 0
services/web/frontend/js/features/ide-react/context/review-panel/hooks/use-review-panel-state.ts

@@ -0,0 +1,273 @@
+import { useState, useMemo, useCallback } from 'react'
+import useScopeValue from '../../../../../shared/hooks/use-scope-value'
+import { sendMB } from '../../../../../infrastructure/event-tracking'
+import { dispatchReviewPanelLayout as handleLayoutChange } from '@/features/source-editor/extensions/changes/change-manager'
+import { useProjectContext } from '@/shared/context/project-context'
+import { useLayoutContext } from '@/shared/context/layout-context'
+import { ReviewPanelStateReactIde } from '../types/review-panel-state'
+import * as ReviewPanel from '../types/review-panel-state'
+import {
+  SubView,
+  ThreadId,
+} from '../../../../../../../types/review-panel/review-panel'
+
+function useReviewPanelState(): ReviewPanelStateReactIde {
+  const { reviewPanelOpen, setReviewPanelOpen } = useLayoutContext()
+  const {
+    features: { trackChangesVisible },
+  } = useProjectContext()
+
+  const [subView, setSubView] = useScopeValue<ReviewPanel.Value<'subView'>>(
+    'reviewPanel.subView'
+  )
+  const [loading] = useScopeValue<ReviewPanel.Value<'loading'>>(
+    'reviewPanel.overview.loading'
+  )
+  const [nVisibleSelectedChanges] = useScopeValue<
+    ReviewPanel.Value<'nVisibleSelectedChanges'>
+  >('reviewPanel.nVisibleSelectedChanges')
+  const [collapsed, setCollapsed] = useScopeValue<
+    ReviewPanel.Value<'collapsed'>
+  >('reviewPanel.overview.docsCollapsedState')
+  const [commentThreads] = useScopeValue<ReviewPanel.Value<'commentThreads'>>(
+    'reviewPanel.commentThreads',
+    true
+  )
+  const [docs] = useScopeValue<ReviewPanel.Value<'docs'>>('docs')
+  const [entries] = useScopeValue<ReviewPanel.Value<'entries'>>(
+    'reviewPanel.entries',
+    true
+  )
+  const [loadingThreads] =
+    useScopeValue<ReviewPanel.Value<'loadingThreads'>>('loadingThreads')
+
+  const [permissions] =
+    useScopeValue<ReviewPanel.Value<'permissions'>>('permissions')
+  const [users] = useScopeValue<ReviewPanel.Value<'users'>>('users', true)
+  const [resolvedComments] = useScopeValue<
+    ReviewPanel.Value<'resolvedComments'>
+  >('reviewPanel.resolvedComments', true)
+
+  const [wantTrackChanges] = useScopeValue<
+    ReviewPanel.Value<'wantTrackChanges'>
+  >('editor.wantTrackChanges')
+  const [openDocId] =
+    useScopeValue<ReviewPanel.Value<'openDocId'>>('editor.open_doc_id')
+  const [shouldCollapse, setShouldCollapse] = useScopeValue<
+    ReviewPanel.Value<'shouldCollapse'>
+  >('reviewPanel.fullTCStateCollapsed')
+  const [lineHeight] = useScopeValue<number>(
+    'reviewPanel.rendererData.lineHeight'
+  )
+
+  const [toggleTrackChangesForEveryone] = useScopeValue<
+    ReviewPanel.UpdaterFn<'toggleTrackChangesForEveryone'>
+  >('toggleTrackChangesForEveryone')
+  const [toggleTrackChangesForUser] = useScopeValue<
+    ReviewPanel.UpdaterFn<'toggleTrackChangesForUser'>
+  >('toggleTrackChangesForUser')
+  const [toggleTrackChangesForGuests] = useScopeValue<
+    ReviewPanel.UpdaterFn<'toggleTrackChangesForGuests'>
+  >('toggleTrackChangesForGuests')
+
+  const [trackChangesState] = useScopeValue<
+    ReviewPanel.Value<'trackChangesState'>
+  >('reviewPanel.trackChangesState')
+  const [trackChangesOnForEveryone] = useScopeValue<
+    ReviewPanel.Value<'trackChangesOnForEveryone'>
+  >('reviewPanel.trackChangesOnForEveryone')
+  const [trackChangesOnForGuests] = useScopeValue<
+    ReviewPanel.Value<'trackChangesOnForGuests'>
+  >('reviewPanel.trackChangesOnForGuests')
+  const [trackChangesForGuestsAvailable] = useScopeValue<
+    ReviewPanel.Value<'trackChangesForGuestsAvailable'>
+  >('reviewPanel.trackChangesForGuestsAvailable')
+  const [resolveComment] =
+    useScopeValue<ReviewPanel.UpdaterFn<'resolveComment'>>('resolveComment')
+  const [submitNewComment] =
+    useScopeValue<ReviewPanel.UpdaterFn<'submitNewComment'>>('submitNewComment')
+  const [deleteComment] =
+    useScopeValue<ReviewPanel.UpdaterFn<'deleteComment'>>('deleteComment')
+  const [gotoEntry] =
+    useScopeValue<ReviewPanel.UpdaterFn<'gotoEntry'>>('gotoEntry')
+  const [saveEdit] =
+    useScopeValue<ReviewPanel.UpdaterFn<'saveEdit'>>('saveEdit')
+  const [submitReplyAngular] =
+    useScopeValue<
+      (entry: { thread_id: ThreadId; replyContent: string }) => void
+    >('submitReply')
+
+  const [formattedProjectMembers] = useScopeValue<
+    ReviewPanel.Value<'formattedProjectMembers'>
+  >('reviewPanel.formattedProjectMembers')
+
+  const toggleReviewPanel = useCallback(() => {
+    if (!trackChangesVisible) {
+      return
+    }
+    setReviewPanelOpen(value => !value)
+    sendMB('rp-toggle-panel', {
+      value: reviewPanelOpen,
+    })
+  }, [reviewPanelOpen, setReviewPanelOpen, trackChangesVisible])
+
+  const [unresolveComment] =
+    useScopeValue<ReviewPanel.UpdaterFn<'unresolveComment'>>('unresolveComment')
+  const [deleteThread] =
+    useScopeValue<ReviewPanel.UpdaterFn<'deleteThread'>>('deleteThread')
+  const [refreshResolvedCommentsDropdown] = useScopeValue<
+    ReviewPanel.UpdaterFn<'refreshResolvedCommentsDropdown'>
+  >('refreshResolvedCommentsDropdown')
+  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 })
+    },
+    [setSubView]
+  )
+
+  const submitReply = useCallback(
+    (threadId: ThreadId, replyContent: string) => {
+      submitReplyAngular({ thread_id: threadId, replyContent })
+    },
+    [submitReplyAngular]
+  )
+
+  const [entryHover, setEntryHover] = useState(false)
+  const [isAddingComment, setIsAddingComment] = useState(false)
+  const [navHeight, setNavHeight] = useState(0)
+  const [toolbarHeight, setToolbarHeight] = useState(0)
+  const [layoutSuspended, setLayoutSuspended] = useState(false)
+
+  const values = useMemo<ReviewPanelStateReactIde['values']>(
+    () => ({
+      collapsed,
+      commentThreads,
+      docs,
+      entries,
+      entryHover,
+      isAddingComment,
+      loadingThreads,
+      nVisibleSelectedChanges,
+      permissions,
+      users,
+      resolvedComments,
+      shouldCollapse,
+      navHeight,
+      toolbarHeight,
+      subView,
+      wantTrackChanges,
+      loading,
+      openDocId,
+      lineHeight,
+      trackChangesState,
+      trackChangesOnForEveryone,
+      trackChangesOnForGuests,
+      trackChangesForGuestsAvailable,
+      formattedProjectMembers,
+      layoutSuspended,
+    }),
+    [
+      collapsed,
+      commentThreads,
+      docs,
+      entries,
+      entryHover,
+      isAddingComment,
+      loadingThreads,
+      nVisibleSelectedChanges,
+      permissions,
+      users,
+      resolvedComments,
+      shouldCollapse,
+      navHeight,
+      toolbarHeight,
+      subView,
+      wantTrackChanges,
+      loading,
+      openDocId,
+      lineHeight,
+      trackChangesState,
+      trackChangesOnForEveryone,
+      trackChangesOnForGuests,
+      trackChangesForGuestsAvailable,
+      formattedProjectMembers,
+      layoutSuspended,
+    ]
+  )
+
+  const updaterFns = useMemo<ReviewPanelStateReactIde['updaterFns']>(
+    () => ({
+      handleSetSubview,
+      handleLayoutChange,
+      gotoEntry,
+      resolveComment,
+      submitReply,
+      acceptChanges,
+      rejectChanges,
+      toggleReviewPanel,
+      bulkAcceptActions,
+      bulkRejectActions,
+      saveEdit,
+      submitNewComment,
+      deleteComment,
+      unresolveComment,
+      refreshResolvedCommentsDropdown,
+      deleteThread,
+      toggleTrackChangesForEveryone,
+      toggleTrackChangesForUser,
+      toggleTrackChangesForGuests,
+      setEntryHover,
+      setCollapsed,
+      setShouldCollapse,
+      setIsAddingComment,
+      setNavHeight,
+      setToolbarHeight,
+      setLayoutSuspended,
+    }),
+    [
+      handleSetSubview,
+      gotoEntry,
+      resolveComment,
+      submitReply,
+      acceptChanges,
+      rejectChanges,
+      toggleReviewPanel,
+      bulkAcceptActions,
+      bulkRejectActions,
+      saveEdit,
+      submitNewComment,
+      deleteComment,
+      unresolveComment,
+      refreshResolvedCommentsDropdown,
+      deleteThread,
+      toggleTrackChangesForEveryone,
+      toggleTrackChangesForUser,
+      toggleTrackChangesForGuests,
+      setCollapsed,
+      setEntryHover,
+      setShouldCollapse,
+      setIsAddingComment,
+      setNavHeight,
+      setToolbarHeight,
+      setLayoutSuspended,
+    ]
+  )
+
+  return { values, updaterFns }
+}
+
+export default useReviewPanelState

+ 43 - 0
services/web/frontend/js/features/ide-react/context/review-panel/review-panel-context.tsx

@@ -0,0 +1,43 @@
+import { useContext, createContext } from 'react'
+import useReviewPanelState from '@/features/ide-react/context/review-panel/hooks/use-review-panel-state'
+import { ReviewPanelStateReactIde } from '@/features/ide-react/context/review-panel/types/review-panel-state'
+
+export const ReviewPanelReactIdeValueContext = createContext<
+  ReviewPanelStateReactIde['values'] | undefined
+>(undefined)
+
+export const ReviewPanelReactIdeUpdaterFnsContext = createContext<
+  ReviewPanelStateReactIde['updaterFns'] | undefined
+>(undefined)
+
+export const ReviewPanelReactIdeProvider: React.FC = ({ children }) => {
+  const { values, updaterFns } = useReviewPanelState()
+
+  return (
+    <ReviewPanelReactIdeValueContext.Provider value={values}>
+      <ReviewPanelReactIdeUpdaterFnsContext.Provider value={updaterFns}>
+        {children}
+      </ReviewPanelReactIdeUpdaterFnsContext.Provider>
+    </ReviewPanelReactIdeValueContext.Provider>
+  )
+}
+
+export function useReviewPanelReactIdeValueContext() {
+  const context = useContext(ReviewPanelReactIdeValueContext)
+  if (!context) {
+    throw new Error(
+      'ReviewPanelReactIdeValueContext is only available inside ReviewPanelReactIdeProvider'
+    )
+  }
+  return context
+}
+
+export function useReviewPanelReactIdeUpdaterFnsContext() {
+  const context = useContext(ReviewPanelReactIdeUpdaterFnsContext)
+  if (!context) {
+    throw new Error(
+      'ReviewPanelReactIdeUpdaterFnsContext is only available inside ReviewPanelReactIdeProvider'
+    )
+  }
+  return context
+}

+ 11 - 0
services/web/frontend/js/features/ide-react/context/review-panel/types/review-panel-state.ts

@@ -0,0 +1,11 @@
+import { ReviewPanelState } from '@/features/source-editor/context/review-panel/types/review-panel-state'
+
+export interface ReviewPanelStateReactIde extends ReviewPanelState {}
+
+// Getter for values
+export type Value<T extends keyof ReviewPanelStateReactIde['values']> =
+  ReviewPanelStateReactIde['values'][T]
+
+// Getter for stable functions
+export type UpdaterFn<T extends keyof ReviewPanelStateReactIde['updaterFns']> =
+  ReviewPanelStateReactIde['updaterFns'][T]

+ 47 - 0
services/web/frontend/js/features/ide-react/scope-adapters/review-panel-context-adapter.ts

@@ -0,0 +1,47 @@
+import { ReactScopeValueStore } from '@/features/ide-react/scope-value-store/react-scope-value-store'
+
+export default function populateReviewPanelScope(store: ReactScopeValueStore) {
+  store.set('reviewPanel.overview.docsCollapsedState', {})
+  store.set('reviewPanel.subView', 'cur_file')
+  store.set('reviewPanel.overview.loading', false)
+  store.set('reviewPanel.nVisibleSelectedChanges', 0)
+  store.set('reviewPanel.commentThreads', {})
+  store.set('docs', undefined)
+  store.set('reviewPanel.entries', {})
+  store.set('loadingThreads', true)
+  store.set('permissions', {
+    read: false,
+    write: false,
+    admin: false,
+    comment: false,
+  })
+  store.set('users', {})
+  store.set('reviewPanel.resolvedComments', {})
+  store.set('editor.wantTrackChanges', false)
+  store.set('editor.open_doc_id', null)
+  store.set('reviewPanel.fullTCStateCollapsed', true)
+  store.set('reviewPanel.rendererData.lineHeight', 0)
+  store.set('reviewPanel.trackChangesState', {})
+  store.set('reviewPanel.trackChangesOnForEveryone', false)
+  store.set('reviewPanel.trackChangesOnForGuests', false)
+  store.set('reviewPanel.trackChangesForGuestsAvailable', false)
+  store.set('reviewPanel.formattedProjectMembers', {})
+  store.set('toggleTrackChangesForEveryone', () => {})
+  store.set('toggleTrackChangesForUser', () => {})
+  store.set('toggleTrackChangesForGuests', () => {})
+  store.set('resolveComment', () => {})
+  store.set('submitNewComment', async () => {})
+  store.set('deleteComment', () => {})
+  store.set('gotoEntry', () => {})
+  store.set('saveEdit', () => {})
+  store.set('toggleReviewPanel', () => {})
+  store.set('unresolveComment', () => {})
+  store.set('deleteThread', () => {})
+  store.set('refreshResolvedCommentsDropdown', async () => {})
+  store.set('acceptChanges', () => {})
+  store.set('rejectChanges', () => {})
+  store.set('bulkAcceptActions', () => {})
+  store.set('bulkRejectActions', () => {})
+  store.set('submitReply', () => {})
+  store.set('addNewComment', () => {})
+}

+ 1 - 1
services/web/frontend/js/features/source-editor/components/codemirror-editor.tsx

@@ -64,7 +64,7 @@ function CodeMirrorEditor() {
         <CodeMirrorSearch />
         <CodeMirrorToolbar />
         <CodeMirrorCommandTooltip />
-        {isReviewPanelReact && !isReactIde && <ReviewPanel />}
+        {(isReviewPanelReact || isReactIde) && <ReviewPanel />}
         {sourceEditorComponents.map(
           ({ import: { default: Component }, path }) => (
             <Component key={path} />

+ 38 - 4
services/web/frontend/js/features/source-editor/components/review-panel/review-panel.tsx

@@ -7,16 +7,22 @@ import {
   ReviewPanelProvider,
   useReviewPanelValueContext,
 } from '../../context/review-panel/review-panel-context'
+import { ReviewPanelReactIdeProvider } from '@/features/ide-react/context/review-panel/review-panel-context'
 import { isCurrentFileView } from '../../utils/sub-view'
+import { useLayoutContext } from '@/shared/context/layout-context'
+import { useIdeContext } from '@/shared/context/ide-context'
+import classnames from 'classnames'
 
 type ReviewPanelViewProps = {
   parentDomNode: Element
 }
 
 function ReviewPanelView({ parentDomNode }: ReviewPanelViewProps) {
-  const { subView } = useReviewPanelValueContext()
+  const { subView, loadingThreads } = useReviewPanelValueContext()
+  const { reviewPanelOpen } = useLayoutContext()
+  const { isReactIde } = useIdeContext()
 
-  return ReactDOM.createPortal(
+  const content = (
     <>
       <EditorWidgets />
       {isCurrentFileView(subView) ? (
@@ -24,15 +30,43 @@ function ReviewPanelView({ parentDomNode }: ReviewPanelViewProps) {
       ) : (
         <OverviewContainer />
       )}
-    </>,
+    </>
+  )
+
+  return ReactDOM.createPortal(
+    isReactIde ? (
+      <div
+        className={classnames('review-panel', {
+          'rp-state-current-file': subView === 'cur_file',
+          'rp-state-current-file-expanded':
+            subView === 'cur_file' && reviewPanelOpen,
+          'rp-state-current-file-mini':
+            subView === 'cur_file' && !reviewPanelOpen,
+          'rp-state-overview': subView === 'overview',
+          // 'rp-size-mini': ui.miniReviewPanelVisible,
+          'rp-size-expanded': reviewPanelOpen,
+          // 'rp-layout-left': reviewPanel.layoutToLeft,
+          'rp-loading-threads': loadingThreads,
+        })}
+      >
+        {content}
+      </div>
+    ) : (
+      content
+    ),
     parentDomNode
   )
 }
 
 function ReviewPanel() {
   const view = useCodeMirrorViewContext()
+  const { isReactIde } = useIdeContext()
 
-  return (
+  return isReactIde ? (
+    <ReviewPanelReactIdeProvider>
+      <ReviewPanelView parentDomNode={view.scrollDOM} />
+    </ReviewPanelReactIdeProvider>
+  ) : (
     <ReviewPanelProvider>
       <ReviewPanelView parentDomNode={view.scrollDOM} />
     </ReviewPanelProvider>

+ 15 - 2
services/web/frontend/js/features/source-editor/context/review-panel/review-panel-context.tsx

@@ -1,5 +1,10 @@
 import { createContext, useContext } from 'react'
 import useAngularReviewPanelState from './hooks/use-angular-review-panel-state'
+import {
+  ReviewPanelReactIdeUpdaterFnsContext,
+  ReviewPanelReactIdeValueContext,
+} from '@/features/ide-react/context/review-panel/review-panel-context'
+import { useIdeContext } from '@/shared/context/ide-context'
 import { ReviewPanelState } from './types/review-panel-state'
 
 const ReviewPanelValueContext = createContext<
@@ -27,7 +32,11 @@ export function ReviewPanelProvider({ children }: ReviewPanelProviderProps) {
 }
 
 export function useReviewPanelValueContext() {
-  const context = useContext(ReviewPanelValueContext)
+  const contextAngularIde = useContext(ReviewPanelValueContext)
+  const contextReactIde = useContext(ReviewPanelReactIdeValueContext)
+  const { isReactIde } = useIdeContext()
+  const context = isReactIde ? contextReactIde : contextAngularIde
+
   if (!context) {
     throw new Error(
       'ReviewPanelValueContext is only available inside ReviewPanelProvider'
@@ -37,7 +46,11 @@ export function useReviewPanelValueContext() {
 }
 
 export function useReviewPanelUpdaterFnsContext() {
-  const context = useContext(ReviewPanelUpdaterFnsContext)
+  const contextAngularIde = useContext(ReviewPanelUpdaterFnsContext)
+  const contextReactIde = useContext(ReviewPanelReactIdeUpdaterFnsContext)
+  const { isReactIde } = useIdeContext()
+  const context = isReactIde ? contextReactIde : contextAngularIde
+
   if (!context) {
     throw new Error(
       'ReviewPanelUpdaterFnsContext is only available inside ReviewPanelProvider'

+ 9 - 0
services/web/frontend/stylesheets/app/editor/ide-react.less

@@ -12,6 +12,15 @@
     position: relative;
     height: 100%;
   }
+
+  .review-panel {
+    height: 100%;
+  }
+
+  .rp-state-overview {
+    position: sticky;
+    top: 0;
+  }
 }
 
 .ide-react-main {