Просмотр исходного кода

Merge pull request #19346 from overleaf/dp-review-panel-empty-state

Add empty state to review panel

GitOrigin-RevId: 47d7b676e9868942567fc02db234b0827ac86ba3
David 2 лет назад
Родитель
Сommit
0c7a32b4f2

+ 1 - 0
services/web/app/src/Features/Project/ProjectController.js

@@ -335,6 +335,7 @@ const _ProjectController = {
       'pdfjs-40',
       'pdfjs-40',
       'personal-access-token',
       'personal-access-token',
       'revert-file',
       'revert-file',
+      'review-panel-redesign',
       'track-pdf-download',
       'track-pdf-download',
       !anonymous && 'writefull-oauth-promotion',
       !anonymous && 'writefull-oauth-promotion',
       'ieee-stylesheet',
       'ieee-stylesheet',

+ 2 - 0
services/web/frontend/extracted-translations.json

@@ -849,12 +849,14 @@
   "no_borders": "",
   "no_borders": "",
   "no_caption": "",
   "no_caption": "",
   "no_comments": "",
   "no_comments": "",
+  "no_comments_or_suggestions": "",
   "no_existing_password": "",
   "no_existing_password": "",
   "no_folder": "",
   "no_folder": "",
   "no_image_files_found": "",
   "no_image_files_found": "",
   "no_members": "",
   "no_members": "",
   "no_messages": "",
   "no_messages": "",
   "no_new_commits_in_github": "",
   "no_new_commits_in_github": "",
+  "no_one_has_commented_or_left_any_suggestions_yet": "",
   "no_other_projects_found": "",
   "no_other_projects_found": "",
   "no_pdf_error_explanation": "",
   "no_pdf_error_explanation": "",
   "no_pdf_error_reason_no_content": "",
   "no_pdf_error_reason_no_content": "",

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

@@ -3,37 +3,18 @@ import Container from './container'
 import Toolbar from './toolbar/toolbar'
 import Toolbar from './toolbar/toolbar'
 import Nav from './nav'
 import Nav from './nav'
 import Toggler from './toggler'
 import Toggler from './toggler'
-import ChangeEntry from './entries/change-entry'
-import AggregateChangeEntry from './entries/aggregate-change-entry'
-import CommentEntry from './entries/comment-entry'
-import AddCommentEntry from './entries/add-comment-entry'
-import BulkActionsEntry from './entries/bulk-actions-entry/bulk-actions-entry'
 import PositionedEntries from './positioned-entries'
 import PositionedEntries from './positioned-entries'
 import { useReviewPanelValueContext } from '../../context/review-panel/review-panel-context'
 import { useReviewPanelValueContext } from '../../context/review-panel/review-panel-context'
-import { useEditorContext } from '../../../../shared/context/editor-context'
 import useCodeMirrorContentHeight from '../../hooks/use-codemirror-content-height'
 import useCodeMirrorContentHeight from '../../hooks/use-codemirror-content-height'
 import { ReviewPanelEntry } from '../../../../../../types/review-panel/entry'
 import { ReviewPanelEntry } from '../../../../../../types/review-panel/entry'
-import {
-  ReviewPanelDocEntries,
-  ThreadId,
-} from '../../../../../../types/review-panel/review-panel'
-
-const isEntryAThreadId = (
-  entry: keyof ReviewPanelDocEntries
-): entry is ThreadId => entry !== 'add-comment' && entry !== 'bulk-actions'
+import { ReviewPanelDocEntries } from '../../../../../../types/review-panel/review-panel'
+import Entry from './entry'
+import EmptyState from './empty-state'
+import { useFeatureFlag } from '@/shared/context/split-test-context'
 
 
 function CurrentFileContainer() {
 function CurrentFileContainer() {
-  const {
-    commentThreads,
-    entries,
-    openDocId,
-    permissions,
-    loadingThreads,
-    users,
-    nVisibleSelectedChanges: nChanges,
-  } = useReviewPanelValueContext()
+  const { entries, openDocId } = useReviewPanelValueContext()
   const contentHeight = useCodeMirrorContentHeight()
   const contentHeight = useCodeMirrorContentHeight()
-  const { isRestrictedTokenMember } = useEditorContext()
 
 
   const currentDocEntries =
   const currentDocEntries =
     openDocId && openDocId in entries ? entries[openDocId] : undefined
     openDocId && openDocId in entries ? entries[openDocId] : undefined
@@ -44,10 +25,19 @@ function CurrentFileContainer() {
     >
     >
   }, [currentDocEntries])
   }, [currentDocEntries])
 
 
+  const enableEmptyState = useFeatureFlag('review-panel-redesign')
+
+  const showEmptyState =
+    enableEmptyState &&
+    objectEntries.filter(
+      ([key]) => key !== 'add-comment' && key !== 'bulk-actions'
+    ).length === 0
+
   return (
   return (
     <Container className="rp-current-file-container">
     <Container className="rp-current-file-container">
       <div className="review-panel-tools">
       <div className="review-panel-tools">
         <Toolbar />
         <Toolbar />
+        {showEmptyState && <EmptyState />}
         <Nav />
         <Nav />
       </div>
       </div>
       <Toggler />
       <Toggler />
@@ -63,87 +53,7 @@ function CurrentFileContainer() {
         >
         >
           {openDocId &&
           {openDocId &&
             objectEntries.map(([id, entry]) => {
             objectEntries.map(([id, entry]) => {
-              if (!entry.visible) {
-                return null
-              }
-
-              if (
-                isEntryAThreadId(id) &&
-                (entry.type === 'insert' || entry.type === 'delete')
-              ) {
-                return (
-                  <ChangeEntry
-                    key={id}
-                    docId={openDocId}
-                    entryId={id}
-                    permissions={permissions}
-                    user={users[entry.metadata.user_id]}
-                    content={entry.content}
-                    offset={entry.offset}
-                    type={entry.type}
-                    focused={entry.focused}
-                    entryIds={entry.entry_ids}
-                    timestamp={entry.metadata.ts}
-                  />
-                )
-              }
-
-              if (isEntryAThreadId(id) && entry.type === 'aggregate-change') {
-                return (
-                  <AggregateChangeEntry
-                    key={id}
-                    docId={openDocId}
-                    entryId={id}
-                    permissions={permissions}
-                    user={users[entry.metadata.user_id]}
-                    content={entry.content}
-                    replacedContent={entry.metadata.replaced_content}
-                    offset={entry.offset}
-                    focused={entry.focused}
-                    entryIds={entry.entry_ids}
-                    timestamp={entry.metadata.ts}
-                  />
-                )
-              }
-
-              if (
-                isEntryAThreadId(id) &&
-                entry.type === 'comment' &&
-                !loadingThreads
-              ) {
-                return (
-                  <CommentEntry
-                    key={id}
-                    docId={openDocId}
-                    threadId={entry.thread_id}
-                    thread={commentThreads[entry.thread_id]}
-                    entryId={id}
-                    offset={entry.offset}
-                    focused={entry.focused}
-                    permissions={permissions}
-                  />
-                )
-              }
-
-              if (
-                entry.type === 'add-comment' &&
-                permissions.comment &&
-                !isRestrictedTokenMember
-              ) {
-                return <AddCommentEntry key={id} />
-              }
-
-              if (entry.type === 'bulk-actions' && permissions.write) {
-                return (
-                  <BulkActionsEntry
-                    key={id}
-                    entryId={entry.type}
-                    nChanges={nChanges}
-                  />
-                )
-              }
-
-              return null
+              return <Entry key={id} id={id} entry={entry} />
             })}
             })}
         </PositionedEntries>
         </PositionedEntries>
       </div>
       </div>

+ 22 - 0
services/web/frontend/js/features/source-editor/components/review-panel/empty-state.tsx

@@ -0,0 +1,22 @@
+import MaterialIcon from '@/shared/components/material-icon'
+import { useTranslation } from 'react-i18next'
+
+function EmptyState() {
+  const { t } = useTranslation()
+
+  return (
+    <div className="rp-empty-state">
+      <div className="rp-empty-state-inner">
+        <div className="rp-empty-state-comment-icon">
+          <MaterialIcon type="question_answer" />
+        </div>
+        <p>
+          <strong>{t('no_comments_or_suggestions')}</strong>
+        </p>
+        <p>{t('no_one_has_commented_or_left_any_suggestions_yet')}</p>
+      </div>
+    </div>
+  )
+}
+
+export default EmptyState

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

@@ -0,0 +1,109 @@
+import { memo } from 'react'
+import ChangeEntry from './entries/change-entry'
+import AggregateChangeEntry from './entries/aggregate-change-entry'
+import CommentEntry from './entries/comment-entry'
+import AddCommentEntry from './entries/add-comment-entry'
+import BulkActionsEntry from './entries/bulk-actions-entry/bulk-actions-entry'
+import {
+  ReviewPanelDocEntries,
+  ThreadId,
+} from '../../../../../../types/review-panel/review-panel'
+import { useReviewPanelValueContext } from '../../context/review-panel/review-panel-context'
+import { useEditorContext } from '../../../../shared/context/editor-context'
+
+type Props = {
+  entry: ReviewPanelDocEntries[keyof ReviewPanelDocEntries]
+  id: ThreadId | 'add-comment' | 'bulk-actions'
+}
+
+const isEntryAThreadId = (
+  entry: keyof ReviewPanelDocEntries
+): entry is ThreadId => entry !== 'add-comment' && entry !== 'bulk-actions'
+
+function Entry({ entry, id }: Props) {
+  const {
+    commentThreads,
+    openDocId,
+    permissions,
+    loadingThreads,
+    users,
+    nVisibleSelectedChanges: nChanges,
+  } = useReviewPanelValueContext()
+  const { isRestrictedTokenMember } = useEditorContext()
+
+  if (!entry.visible || !openDocId) {
+    return null
+  }
+
+  if (
+    isEntryAThreadId(id) &&
+    (entry.type === 'insert' || entry.type === 'delete')
+  ) {
+    return (
+      <ChangeEntry
+        key={id}
+        docId={openDocId}
+        entryId={id}
+        permissions={permissions}
+        user={users[entry.metadata.user_id]}
+        content={entry.content}
+        offset={entry.offset}
+        type={entry.type}
+        focused={entry.focused}
+        entryIds={entry.entry_ids}
+        timestamp={entry.metadata.ts}
+      />
+    )
+  }
+
+  if (isEntryAThreadId(id) && entry.type === 'aggregate-change') {
+    return (
+      <AggregateChangeEntry
+        key={id}
+        docId={openDocId}
+        entryId={id}
+        permissions={permissions}
+        user={users[entry.metadata.user_id]}
+        content={entry.content}
+        replacedContent={entry.metadata.replaced_content}
+        offset={entry.offset}
+        focused={entry.focused}
+        entryIds={entry.entry_ids}
+        timestamp={entry.metadata.ts}
+      />
+    )
+  }
+
+  if (isEntryAThreadId(id) && entry.type === 'comment' && !loadingThreads) {
+    return (
+      <CommentEntry
+        key={id}
+        docId={openDocId}
+        threadId={entry.thread_id}
+        thread={commentThreads[entry.thread_id]}
+        entryId={id}
+        offset={entry.offset}
+        focused={entry.focused}
+        permissions={permissions}
+      />
+    )
+  }
+
+  if (
+    entry.type === 'add-comment' &&
+    permissions.comment &&
+    !isRestrictedTokenMember
+  ) {
+    return <AddCommentEntry key={id} />
+  }
+
+  if (entry.type === 'bulk-actions' && permissions.write) {
+    return (
+      <BulkActionsEntry key={id} entryId={entry.type} nChanges={nChanges} />
+    )
+  }
+
+  return null
+}
+
+export default memo(Entry)

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

@@ -6,15 +6,33 @@ import Icon from '../../../../shared/components/icon'
 import OverviewFile from './overview-file'
 import OverviewFile from './overview-file'
 import { useReviewPanelValueContext } from '../../context/review-panel/review-panel-context'
 import { useReviewPanelValueContext } from '../../context/review-panel/review-panel-context'
 import { useFileTreeData } from '@/shared/context/file-tree-data-context'
 import { useFileTreeData } from '@/shared/context/file-tree-data-context'
-import { memo } from 'react'
+import { memo, useMemo } from 'react'
+import EmptyState from './empty-state'
+import { useFeatureFlag } from '@/shared/context/split-test-context'
 
 
 function OverviewContainer() {
 function OverviewContainer() {
+  const { entries } = useReviewPanelValueContext()
+
   const { isOverviewLoading } = useReviewPanelValueContext()
   const { isOverviewLoading } = useReviewPanelValueContext()
   const { docs } = useFileTreeData()
   const { docs } = useFileTreeData()
 
 
+  const entryCount = useMemo(() => {
+    return docs
+      ?.map(doc => {
+        const docEntries = entries[doc.doc.id] ?? {}
+        return Object.keys(docEntries).filter(
+          key => key !== 'add-comment' && key !== 'bulk-actions'
+        ).length
+      })
+      .reduce((acc, curr) => acc + curr, 0)
+  }, [docs, entries])
+
+  const enableEmptyState = useFeatureFlag('review-panel-redesign')
+
   return (
   return (
     <Container>
     <Container>
       <Toggler />
       <Toggler />
+      {enableEmptyState && entryCount === 0 && <EmptyState />}
       <Toolbar />
       <Toolbar />
       <div
       <div
         className="rp-entry-list"
         className="rp-entry-list"

+ 42 - 0
services/web/frontend/stylesheets/app/editor/review-panel.less

@@ -1231,3 +1231,45 @@ button when (@is-overleaf-light = true) {
     border: 0;
     border: 0;
   }
   }
 }
 }
+
+.rp-empty-state {
+  position: absolute;
+  top: 0;
+  bottom: 0;
+  left: 0;
+  right: 0;
+
+  .rp-empty-state-inner {
+    position: sticky;
+    top: 50%;
+    transform: translateY(-50%);
+
+    width: 100%;
+    padding-left: 16px;
+    padding-right: 16px;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    flex-direction: column;
+
+    p {
+      margin-bottom: 0;
+      text-align: center;
+    }
+  }
+
+  .rp-empty-state-comment-icon {
+    width: 80px;
+    height: 80px;
+    background-color: white;
+    border-radius: 100%;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    margin-bottom: 16px;
+
+    .material-symbols {
+      font-size: 32px;
+    }
+  }
+}

+ 2 - 0
services/web/locales/en.json

@@ -1243,6 +1243,7 @@
   "no_borders": "No borders",
   "no_borders": "No borders",
   "no_caption": "No caption",
   "no_caption": "No caption",
   "no_comments": "No comments",
   "no_comments": "No comments",
+  "no_comments_or_suggestions": "No comments or suggestions",
   "no_existing_password": "Please use the password reset form to set your password",
   "no_existing_password": "Please use the password reset form to set your password",
   "no_featured_templates": "No featured templates",
   "no_featured_templates": "No featured templates",
   "no_folder": "No folder",
   "no_folder": "No folder",
@@ -1251,6 +1252,7 @@
   "no_members": "No members",
   "no_members": "No members",
   "no_messages": "No messages",
   "no_messages": "No messages",
   "no_new_commits_in_github": "No new commits in GitHub since last merge.",
   "no_new_commits_in_github": "No new commits in GitHub since last merge.",
+  "no_one_has_commented_or_left_any_suggestions_yet": "No one has commented or left any suggestions yet.",
   "no_other_projects_found": "No other projects found, please create another project first",
   "no_other_projects_found": "No other projects found, please create another project first",
   "no_other_sessions": "No other sessions active",
   "no_other_sessions": "No other sessions active",
   "no_pdf_error_explanation": "This compile didn’t produce a PDF. This can happen if:",
   "no_pdf_error_explanation": "This compile didn’t produce a PDF. This can happen if:",