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

Add empty state to review panel

GitOrigin-RevId: 47d7b676e9868942567fc02db234b0827ac86ba3
This commit is contained in:
David 2024-07-11 14:22:28 +01:00 committed by Copybot
parent fb26087894
commit 0c7a32b4f2
8 changed files with 212 additions and 106 deletions

View file

@ -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',

View file

@ -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": "",

View file

@ -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 { import { ReviewPanelDocEntries } from '../../../../../../types/review-panel/review-panel'
ReviewPanelDocEntries, import Entry from './entry'
ThreadId, import EmptyState from './empty-state'
} from '../../../../../../types/review-panel/review-panel' import { useFeatureFlag } from '@/shared/context/split-test-context'
const isEntryAThreadId = (
entry: keyof ReviewPanelDocEntries
): entry is ThreadId => entry !== 'add-comment' && entry !== 'bulk-actions'
function CurrentFileContainer() { function CurrentFileContainer() {
const { const { entries, openDocId } = useReviewPanelValueContext()
commentThreads,
entries,
openDocId,
permissions,
loadingThreads,
users,
nVisibleSelectedChanges: nChanges,
} = 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 <Entry key={id} id={id} entry={entry} />
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
})} })}
</PositionedEntries> </PositionedEntries>
</div> </div>

View file

@ -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

View file

@ -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)

View file

@ -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"

View file

@ -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;
}
}
}

View file

@ -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 didnt produce a PDF. This can happen if:", "no_pdf_error_explanation": "This compile didnt produce a PDF. This can happen if:",