فهرست منبع

Merge pull request #13641 from overleaf/ii-review-panel-migration-overview-view

[web] Create overview view shell for review panel

GitOrigin-RevId: 21cc8a744253aec53f089ec6e6c1221d7153f3a4
ilkin-overleaf 3 سال پیش
والد
کامیت
936ab81b65
18فایلهای تغییر یافته به همراه266 افزوده شده و 133 حذف شده
  1. 0 8
      services/web/frontend/js/features/source-editor/components/review-panel/container.tsx
  2. 51 39
      services/web/frontend/js/features/source-editor/components/review-panel/current-file-container.tsx
  3. 10 3
      services/web/frontend/js/features/source-editor/components/review-panel/entries/comment-entry.tsx
  4. 26 0
      services/web/frontend/js/features/source-editor/components/review-panel/hooks/use-collapse-height.ts
  5. 36 36
      services/web/frontend/js/features/source-editor/components/review-panel/overview-container.tsx
  6. 109 0
      services/web/frontend/js/features/source-editor/components/review-panel/overview-file.tsx
  7. 2 7
      services/web/frontend/js/features/source-editor/components/review-panel/review-panel.tsx
  8. 1 1
      services/web/frontend/js/features/source-editor/components/review-panel/toolbar/resolved-comments-dropdown.tsx
  9. 3 16
      services/web/frontend/js/features/source-editor/components/review-panel/toolbar/toggle-menu.tsx
  10. 5 0
      services/web/frontend/js/features/source-editor/context/review-panel/hooks/use-angular-review-panel-state.ts
  11. 6 3
      services/web/frontend/js/features/source-editor/context/review-panel/types/review-panel-state.ts
  12. 1 14
      services/web/frontend/stylesheets/app/editor/review-panel.less
  13. 2 2
      services/web/test/frontend/components/editor-left-menu/editor-left-menu.spec.tsx
  14. 1 1
      services/web/test/frontend/features/editor-left-menu/components/settings/settings-document.test.tsx
  15. 8 0
      services/web/test/frontend/features/review-panel/review-panel.spec.tsx
  16. 3 1
      services/web/types/project-settings.ts
  17. 1 1
      services/web/types/review-panel/entry.ts
  18. 1 1
      services/web/types/review-panel/review-panel.ts

+ 0 - 8
services/web/frontend/js/features/source-editor/components/review-panel/container.tsx

@@ -1,6 +1,3 @@
-import Toggler from './toggler'
-import Toolbar from './toolbar/toolbar'
-import Nav from './nav'
 import classnames from 'classnames'
 
 const reviewPanelClasses = ['ol-cm-review-panel']
@@ -18,11 +15,6 @@ function Container({ children, classNames, ...rest }: ContainerProps) {
       {...rest}
       data-testid="review-panel"
     >
-      <div className="review-panel-tools">
-        <Toolbar />
-        <Nav />
-      </div>
-      <Toggler />
       {children}
     </div>
   )

+ 51 - 39
services/web/frontend/js/features/source-editor/components/review-panel/current-file-container.tsx

@@ -1,4 +1,8 @@
 import { useMemo } from 'react'
+import Container from './container'
+import Toolbar from './toolbar/toolbar'
+import Nav from './nav'
+import Toggler from './toggler'
 import ChangeEntry from './entries/change-entry'
 import AggregateChangeEntry from './entries/aggregate-change-entry'
 import CommentEntry from './entries/comment-entry'
@@ -26,54 +30,62 @@ function CurrentFileContainer() {
   }, [currentDocEntries])
 
   return (
-    <div
-      id="review-panel-current-file"
-      role="tabpanel"
-      tabIndex={0}
-      aria-labelledby="review-panel-tab-current-file"
-    >
+    <Container>
+      <div className="review-panel-tools">
+        <Toolbar />
+        <Nav />
+      </div>
+      <Toggler />
       <div
-        className="rp-entry-list-inner"
-        style={{ height: `${contentHeight}px` }}
+        id="review-panel-current-file"
+        role="tabpanel"
+        tabIndex={0}
+        aria-labelledby="review-panel-tab-current-file"
       >
-        {openDocId &&
-          objectEntries.map(([id, entry]) => {
-            if (!entry.visible) {
-              return null
-            }
+        <div
+          className="rp-entry-list-inner"
+          style={{ height: `${contentHeight}px` }}
+        >
+          {openDocId &&
+            objectEntries.map(([id, entry]) => {
+              if (!entry.visible) {
+                return null
+              }
 
-            if (entry.type === 'insert' || entry.type === 'delete') {
-              return <ChangeEntry key={id} />
-            }
+              if (entry.type === 'insert' || entry.type === 'delete') {
+                return <ChangeEntry key={id} />
+              }
 
-            if (entry.type === 'aggregate-change') {
-              return <AggregateChangeEntry key={id} />
-            }
+              if (entry.type === 'aggregate-change') {
+                return <AggregateChangeEntry key={id} />
+              }
 
-            if (entry.type === 'comment' && !loadingThreads) {
-              return (
-                <CommentEntry
-                  key={id}
-                  docId={openDocId}
-                  entry={entry}
-                  entryId={id}
-                  threads={commentThreads}
-                />
-              )
-            }
+              if (entry.type === 'comment' && !loadingThreads) {
+                return (
+                  <CommentEntry
+                    key={id}
+                    docId={openDocId}
+                    entry={entry}
+                    entryId={id}
+                    permissions={permissions}
+                    threads={commentThreads}
+                  />
+                )
+              }
 
-            if (entry.type === 'add-comment' && permissions.comment) {
-              return <AddCommentEntry key={id} />
-            }
+              if (entry.type === 'add-comment' && permissions.comment) {
+                return <AddCommentEntry key={id} />
+              }
 
-            if (entry.type === 'bulk-actions') {
-              return <BulkActionsEntry key={id} />
-            }
+              if (entry.type === 'bulk-actions') {
+                return <BulkActionsEntry key={id} />
+              }
 
-            return null
-          })}
+              return null
+            })}
+        </div>
       </div>
-    </div>
+    </Container>
   )
 }
 

+ 10 - 3
services/web/frontend/js/features/source-editor/components/review-panel/entries/comment-entry.tsx

@@ -14,22 +14,29 @@ import {
 import classnames from 'classnames'
 import { ReviewPanelCommentEntry } from '../../../../../../../types/review-panel/entry'
 import {
-  DocId,
   ReviewPanelCommentThreads,
+  ReviewPanelPermissions,
   ThreadId,
 } from '../../../../../../../types/review-panel/review-panel'
+import { DocId } from '../../../../../../../types/project-settings'
 
 type CommentEntryProps = {
   docId: DocId
   entry: ReviewPanelCommentEntry
   entryId: ThreadId
+  permissions: ReviewPanelPermissions
   threads: ReviewPanelCommentThreads
 }
 
-function CommentEntry({ docId, entry, entryId, threads }: CommentEntryProps) {
+function CommentEntry({
+  docId,
+  entry,
+  entryId,
+  permissions,
+  threads,
+}: CommentEntryProps) {
   const { t } = useTranslation()
   const {
-    permissions,
     gotoEntry,
     toggleReviewPanel,
     resolveComment,

+ 26 - 0
services/web/frontend/js/features/source-editor/components/review-panel/hooks/use-collapse-height.ts

@@ -0,0 +1,26 @@
+import { useEffect } from 'react'
+
+function useCollapseHeight(
+  elRef: React.MutableRefObject<HTMLElement | null>,
+  shouldCollapse: boolean
+) {
+  useEffect(() => {
+    if (elRef.current) {
+      const neededHeight = elRef.current.scrollHeight
+
+      if (neededHeight > 0) {
+        const height = shouldCollapse ? 0 : neededHeight
+        // This might result in a too big height if the element has css prop of
+        // `box-sizing` set to `content-box`. To fix that, values of props such as
+        // box-sizing, padding and border could be extracted from `height` to compensate.
+        elRef.current.style.height = `${height}px`
+      } else {
+        if (shouldCollapse) {
+          elRef.current.style.height = '0'
+        }
+      }
+    }
+  }, [elRef, shouldCollapse])
+}
+
+export default useCollapseHeight

+ 36 - 36
services/web/frontend/js/features/source-editor/components/review-panel/overview-container.tsx

@@ -1,41 +1,41 @@
+import Container from './container'
+import Toggler from './toggler'
+import Toolbar from './toolbar/toolbar'
+import Nav from './nav'
+import Icon from '../../../../shared/components/icon'
+import OverviewFile from './overview-file'
+import { useReviewPanelValueContext } from '../../context/review-panel/review-panel-context'
+
 function OverviewContainer() {
+  const { loading, docs } = useReviewPanelValueContext()
+
   return (
-    <div
-      className="rp-entry-list"
-      id="review-panel-overview"
-      role="tabpanel"
-      tabIndex={0}
-      aria-labelledby="review-panel-tab-overview"
-    >
-      Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod
-      tempor incididunt ut labore et dolore magna aliqua. Et malesuada fames ac
-      turpis egestas integer eget aliquet nibh. Et leo duis ut diam quam nulla
-      porttitor massa id. Risus quis varius quam quisque id diam vel quam
-      elementum. Nibh venenatis cras sed felis. Sit amet commodo nulla facilisi
-      nullam vehicula ipsum a arcu. Dui ut ornare lectus sit amet est placerat
-      in. Aliquam ultrices sagittis orci a. Leo a diam sollicitudin tempor id eu
-      nisl nunc mi. Quis ipsum suspendisse ultrices gravida dictum fusce. Ut
-      etiam sit amet nisl purus in mollis nunc sed. Rhoncus est pellentesque
-      elit ullamcorper dignissim cras. Faucibus turpis in eu mi bibendum. Proin
-      libero nunc consequat interdum. Ac placerat vestibulum lectus mauris
-      ultrices eros in cursus turpis. Ac felis donec et odio. Nullam ac tortor
-      vitae purus faucibus. Consectetur lorem donec massa sapien faucibus et
-      molestie. Praesent elementum facilisis leo vel fringilla est ullamcorper
-      eget nulla. Adipiscing vitae proin sagittis nisl rhoncus mattis rhoncus
-      urna. Cursus metus aliquam eleifend mi in nulla posuere sollicitudin
-      aliquam. Eget nullam non nisi est sit amet facilisis magna. Donec
-      adipiscing tristique risus nec feugiat in fermentum posuere. Gravida
-      rutrum quisque non tellus orci ac auctor augue. Euismod in pellentesque
-      massa placerat duis ultricies lacus. Pellentesque diam volutpat commodo
-      sed egestas. Tempus iaculis urna id volutpat lacus laoreet. Lorem ipsum
-      dolor sit amet consectetur. Tincidunt id aliquet risus feugiat in ante
-      metus. Risus ultricies tristique nulla aliquet enim tortor at auctor urna.
-      Purus in mollis nunc sed. In ante metus dictum at. Magna eget est lorem
-      ipsum dolor sit. Fusce id velit ut tortor pretium viverra. Augue neque
-      gravida in fermentum et sollicitudin ac. Et malesuada fames ac turpis.
-      Felis bibendum ut tristique et egestas quis ipsum suspendisse ultrices.
-      Varius vel pharetra vel turpis nunc eget.
-    </div>
+    <Container>
+      <Toggler />
+      <Toolbar />
+      <div
+        className="rp-entry-list"
+        id="review-panel-overview"
+        role="tabpanel"
+        tabIndex={0}
+        aria-labelledby="review-panel-tab-overview"
+      >
+        {loading ? (
+          <div className="rp-loading">
+            <Icon type="spinner" spin />
+          </div>
+        ) : (
+          docs?.map(doc => (
+            <OverviewFile
+              key={doc.doc.id}
+              docId={doc.doc.id}
+              docPath={doc.path}
+            />
+          ))
+        )}
+      </div>
+      <Nav />
+    </Container>
   )
 }
 

+ 109 - 0
services/web/frontend/js/features/source-editor/components/review-panel/overview-file.tsx

@@ -0,0 +1,109 @@
+import { useMemo, useRef } from 'react'
+import Icon from '../../../../shared/components/icon'
+import ChangeEntry from './entries/change-entry'
+import AggregateChangeEntry from './entries/aggregate-change-entry'
+import CommentEntry from './entries/comment-entry'
+import {
+  useReviewPanelUpdaterFnsContext,
+  useReviewPanelValueContext,
+} from '../../context/review-panel/review-panel-context'
+import classnames from 'classnames'
+import { ThreadId } from '../../../../../../types/review-panel/review-panel'
+import { MainDocument } from '../../../../../../types/project-settings'
+import { ReviewPanelEntry } from '../../../../../../types/review-panel/entry'
+import useCollapseHeight from './hooks/use-collapse-height'
+
+type OverviewFileProps = {
+  docId: MainDocument['doc']['id']
+  docPath: MainDocument['path']
+}
+
+function OverviewFile({ docId, docPath }: OverviewFileProps) {
+  const { entries, collapsed, commentThreads, permissions } =
+    useReviewPanelValueContext()
+  const { setCollapsed } = useReviewPanelUpdaterFnsContext()
+
+  const docCollapsed = collapsed[docId]
+  const docEntries = useMemo(
+    () => (docId in entries ? entries[docId] : {}),
+    [docId, entries]
+  )
+  const objectEntries = useMemo(() => {
+    const entries = Object.entries(docEntries) as Array<
+      [ThreadId, ReviewPanelEntry]
+    >
+
+    const orderedEntries = entries.sort(([, entryA], [, entryB]) => {
+      return entryA.offset - entryB.offset
+    })
+
+    return orderedEntries
+  }, [docEntries])
+  const entryCount = Object.keys(docEntries).length
+
+  const handleToggleCollapsed = () => {
+    setCollapsed({ ...collapsed, [docId]: !docCollapsed })
+  }
+
+  const entriesContainerRef = useRef<HTMLDivElement | null>(null)
+  useCollapseHeight(entriesContainerRef, docCollapsed)
+
+  return (
+    <div className="rp-overview-file">
+      {entryCount > 0 && (
+        // eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions
+        <div
+          className="rp-overview-file-header"
+          onClick={handleToggleCollapsed}
+        >
+          <span
+            className={classnames('rp-overview-file-header-collapse', {
+              'rp-overview-file-header-collapse-on': docCollapsed,
+            })}
+          >
+            <Icon type="angle-down" />
+          </span>
+          {docPath}
+          {docCollapsed && (
+            <>
+              &nbsp;
+              <span className="rp-overview-file-num-entries">
+                ({entryCount})
+              </span>
+            </>
+          )}
+        </div>
+      )}
+      <div className="rp-overview-file-entries" ref={entriesContainerRef}>
+        {objectEntries.map(([id, entry]) => {
+          if (entry.type === 'insert' || entry.type === 'delete') {
+            return <ChangeEntry key={id} />
+          }
+
+          if (entry.type === 'aggregate-change') {
+            return <AggregateChangeEntry key={id} />
+          }
+
+          if (entry.type === 'comment') {
+            if (!commentThreads[entry.thread_id]?.resolved) {
+              return (
+                <CommentEntry
+                  key={id}
+                  docId={docId}
+                  entry={entry}
+                  entryId={id}
+                  permissions={permissions}
+                  threads={commentThreads}
+                />
+              )
+            }
+          }
+
+          return null
+        })}
+      </div>
+    </div>
+  )
+}
+
+export default OverviewFile

+ 2 - 7
services/web/frontend/js/features/source-editor/components/review-panel/review-panel.tsx

@@ -1,5 +1,4 @@
 import ReactDOM from 'react-dom'
-import Container from './container'
 import CurrentFileContainer from './current-file-container'
 import OverviewContainer from './overview-container'
 import { useCodeMirrorViewContext } from '../codemirror-editor'
@@ -19,13 +18,9 @@ function ReviewPanelView({ parentDomNode }: ReviewPanelViewProps) {
   return ReactDOM.createPortal(
     <>
       {isCurrentFileView(subView) ? (
-        <Container>
-          <CurrentFileContainer />
-        </Container>
+        <CurrentFileContainer />
       ) : (
-        <Container>
-          <OverviewContainer />
-        </Container>
+        <OverviewContainer />
       )}
     </>,
     parentDomNode

+ 1 - 1
services/web/frontend/js/features/source-editor/components/review-panel/toolbar/resolved-comments-dropdown.tsx

@@ -6,11 +6,11 @@ import ResolvedCommentsScroller from './resolved-comments-scroller'
 import classnames from 'classnames'
 import { useReviewPanelValueContext } from '../../../context/review-panel/review-panel-context'
 import {
-  DocId,
   ReviewPanelDocEntries,
   ThreadId,
 } from '../../../../../../../types/review-panel/review-panel'
 import { ReviewPanelResolvedCommentThread } from '../../../../../../../types/review-panel/comment-thread'
+import { DocId } from '../../../../../../../types/project-settings'
 
 export interface FilteredResolvedComments
   extends ReviewPanelResolvedCommentThread {

+ 3 - 16
services/web/frontend/js/features/source-editor/components/review-panel/toolbar/toggle-menu.tsx

@@ -1,4 +1,4 @@
-import { useEffect, useRef } from 'react'
+import { useRef } from 'react'
 import { Trans, useTranslation } from 'react-i18next'
 import Tooltip from '../../../../../shared/components/tooltip'
 import Icon from '../../../../../shared/components/icon'
@@ -9,6 +9,7 @@ import {
   useReviewPanelValueContext,
 } from '../../../context/review-panel/review-panel-context'
 import classnames from 'classnames'
+import useCollapseHeight from '../hooks/use-collapse-height'
 
 function ToggleMenu() {
   const { t } = useTranslation()
@@ -29,21 +30,7 @@ function ToggleMenu() {
   } = useReviewPanelValueContext()
 
   const containerRef = useRef<HTMLUListElement | null>(null)
-
-  useEffect(() => {
-    if (containerRef.current) {
-      const neededHeight = containerRef.current.scrollHeight
-
-      if (neededHeight > 0) {
-        const height = shouldCollapse ? 0 : neededHeight
-        containerRef.current.style.height = `${height}px`
-      } else {
-        if (shouldCollapse) {
-          containerRef.current.style.height = '0'
-        }
-      }
-    }
-  }, [shouldCollapse])
+  useCollapseHeight(containerRef, shouldCollapse)
 
   return (
     <>

+ 5 - 0
services/web/frontend/js/features/source-editor/context/review-panel/hooks/use-angular-review-panel-state.ts

@@ -13,6 +13,9 @@ function useAngularReviewPanelState(): ReviewPanelState {
   const [subView, setSubView] = useScopeValue<ReviewPanel.Value<'subView'>>(
     'reviewPanel.subView'
   )
+  const [loading] = useScopeValue<ReviewPanel.Value<'loading'>>(
+    'reviewPanel.overview.loading'
+  )
   const [collapsed, setCollapsed] = useScopeValue<
     ReviewPanel.Value<'collapsed'>
   >('reviewPanel.overview.docsCollapsedState')
@@ -130,6 +133,7 @@ function useAngularReviewPanelState(): ReviewPanelState {
       submitReply,
       subView,
       wantTrackChanges,
+      loading,
       openDocId,
       toggleTrackChangesForEveryone,
       toggleTrackChangesForUser,
@@ -162,6 +166,7 @@ function useAngularReviewPanelState(): ReviewPanelState {
       submitReply,
       subView,
       wantTrackChanges,
+      loading,
       openDocId,
       toggleTrackChangesForEveryone,
       toggleTrackChangesForUser,

+ 6 - 3
services/web/frontend/js/features/source-editor/context/review-panel/types/review-panel-state.ts

@@ -1,6 +1,5 @@
 import {
   CommentId,
-  DocId,
   ReviewPanelCommentThreads,
   ReviewPanelEntries,
   ReviewPanelPermissions,
@@ -8,11 +7,14 @@ import {
   ThreadId,
 } from '../../../../../../../types/review-panel/review-panel'
 import { ReviewPanelCommentEntry } from '../../../../../../../types/review-panel/entry'
-import { MainDocument } from '../../../../../../../types/project-settings'
+import {
+  DocId,
+  MainDocument,
+} from '../../../../../../../types/project-settings'
 
 export interface ReviewPanelState {
   values: {
-    collapsed: Record<string, boolean>
+    collapsed: Record<DocId, boolean>
     commentThreads: ReviewPanelCommentThreads
     deleteComment: (threadId: ThreadId, commentId: CommentId) => void
     docs: MainDocument[] | undefined
@@ -33,6 +35,7 @@ export interface ReviewPanelState {
     submitReply: (entry: ReviewPanelCommentEntry, replyContent: string) => void
     subView: SubView
     wantTrackChanges: boolean
+    loading: boolean
     openDocId: DocId | null
     toggleTrackChangesForEveryone: (isOn: boolean) => unknown
     toggleTrackChangesForUser: (isOn: boolean, memberId: string) => unknown

+ 1 - 14
services/web/frontend/stylesheets/app/editor/review-panel.less

@@ -1260,6 +1260,7 @@ button when (@is-overleaf-light = true) {
     top: 0;
     display: flex;
     flex-direction: column;
+    height: 100%;
   }
 
   .rp-overview-file {
@@ -1267,20 +1268,6 @@ button when (@is-overleaf-light = true) {
       //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;
-      }
-    }
   }
 
   .rp-nav-item {

+ 2 - 2
services/web/test/frontend/components/editor-left-menu/editor-left-menu.spec.tsx

@@ -352,7 +352,7 @@ describe('<EditorLeftMenu />', function () {
               id: 'id1',
               type: 'doc',
               selected: false,
-            },
+            } as MainDocument['doc'],
           },
           {
             path: 'main2.tex',
@@ -361,7 +361,7 @@ describe('<EditorLeftMenu />', function () {
               id: 'id2',
               type: 'doc',
               selected: false,
-            },
+            } as MainDocument['doc'],
           },
         ]
 

+ 1 - 1
services/web/test/frontend/features/editor-left-menu/components/settings/settings-document.test.tsx

@@ -17,7 +17,7 @@ describe('<SettingsDocument />', function () {
         id: '123abc',
         type: 'doc',
         selected: false,
-      },
+      } as MainDocument['doc'],
     },
   ]
 

+ 8 - 0
services/web/test/frontend/features/review-panel/review-panel.spec.tsx

@@ -192,4 +192,12 @@ describe('<ReviewPanel />', function () {
     // eslint-disable-next-line mocha/no-skipped-tests
     it.skip('resolves comment', function () {})
   })
+
+  describe('overview mode', function () {
+    // eslint-disable-next-line mocha/no-skipped-tests
+    it.skip('shows list of files changed', function () {})
+
+    // eslint-disable-next-line mocha/no-skipped-tests
+    it.skip('renders comments', function () {})
+  })
 })

+ 3 - 1
services/web/types/project-settings.ts

@@ -1,14 +1,16 @@
 import { OverallTheme } from '../frontend/js/features/source-editor/extensions/theme'
+import { Brand } from './helpers/brand'
 
 export type AllowedImageName = {
   imageDesc: string
   imageName: string
 }
 
+export type DocId = Brand<string, 'DocId'>
 export type MainDocument = {
   doc: {
     name: string
-    id: string
+    id: DocId
     type: string
     selected: boolean
   }

+ 1 - 1
services/web/types/review-panel/entry.ts

@@ -8,6 +8,7 @@ interface ReviewPanelEntryScreenPos {
 
 interface ReviewPanelBaseEntry {
   visible: boolean
+  offset: number
 }
 
 export interface ReviewPanelCommentEntry extends ReviewPanelBaseEntry {
@@ -15,7 +16,6 @@ export interface ReviewPanelCommentEntry extends ReviewPanelBaseEntry {
   content: string
   entry_ids: string[]
   focused: boolean
-  offset: number
   screenPos: ReviewPanelEntryScreenPos
   thread_id: ThreadId
   replyContent?: string // angular specific

+ 1 - 1
services/web/types/review-panel/review-panel.ts

@@ -1,4 +1,5 @@
 import { Brand } from '../helpers/brand'
+import { DocId } from '../project-settings'
 import { ReviewPanelEntry } from './entry'
 import { ReviewPanelCommentThread } from './comment-thread'
 
@@ -14,7 +15,6 @@ export interface ReviewPanelPermissions {
 export type ThreadId = Brand<string, 'ThreadId'>
 export type ReviewPanelDocEntries = Record<ThreadId, ReviewPanelEntry>
 
-export type DocId = Brand<string, 'DocId'>
 export type ReviewPanelEntries = Record<DocId, ReviewPanelDocEntries>
 
 export type UserId = Brand<string, 'UserId'>