Merge pull request #13628 from overleaf/ii-review-panel-migration-resolved-comments-entry

[web] Add resolved comments entries functionality

GitOrigin-RevId: f0a8365b00c0861be12347aeaf486f7c02faf8e5
This commit is contained in:
ilkin-overleaf 2023-07-03 11:55:39 +03:00 committed by Copybot
parent 8cd9b4051c
commit e5d6777211
13 changed files with 168 additions and 93 deletions

View file

@ -40,6 +40,7 @@ function CommentEntry({ docId, entry, entryId, threads }: CommentEntryProps) {
const [replyContent, setReplyContent] = useState('')
const [animating, setAnimating] = useState(false)
const [resolved, setResolved] = useState(false)
const entryDivRef = useRef<HTMLDivElement | null>(null)
const thread =
@ -68,6 +69,8 @@ function CommentEntry({ docId, entry, entryId, threads }: CommentEntryProps) {
}
setTimeout(() => {
setAnimating(false)
setResolved(true)
resolveComment(docId, entryId)
}, 350)
}
@ -94,7 +97,7 @@ function CommentEntry({ docId, entry, entryId, threads }: CommentEntryProps) {
}
}
if (!thread) {
if (!thread || resolved) {
return null
}

View file

@ -3,8 +3,8 @@ import { useState } from 'react'
import AutoExpandingTextArea from '../../../../../shared/components/auto-expanding-text-area'
import { formatTime } from '../../../../utils/format-date'
import { useReviewPanelValueContext } from '../../../context/review-panel/review-panel-context'
import { ReviewPanelCommentThread } from '../../../../../../../types/review-panel/comment-thread'
import {
ReviewPanelCommentThread,
ReviewPanelCommentThreadMessage,
ThreadId,
} from '../../../../../../../types/review-panel/review-panel'

View file

@ -3,6 +3,7 @@ import { useState } from 'react'
import Linkify from 'react-linkify'
import { formatTime } from '../../../../utils/format-date'
import { useReviewPanelValueContext } from '../../../context/review-panel/review-panel-context'
import { FilteredResolvedComments } from '../toolbar/resolved-comments-dropdown'
function LinkDecorator(
decoratedHref: string,
@ -17,26 +18,7 @@ function LinkDecorator(
}
type ResolvedCommentEntryProps = {
thread: {
resolved_at: number
entryId: string
docName: string
content: string
messages: Array<{
id: string
user: {
id: string
hue: string
name: string
}
content: string
timestamp: string
}>
resolved_by_user: {
name: string
hue: string
}
} // TODO extract type
thread: FilteredResolvedComments
contentLimit?: number
}
@ -45,7 +27,8 @@ function ResolvedCommentEntry({
contentLimit = 40,
}: ResolvedCommentEntryProps) {
const { t } = useTranslation()
const { permissions } = useReviewPanelValueContext()
const { permissions, unresolveComment, deleteThread } =
useReviewPanelValueContext()
const [isCollapsed, setIsCollapsed] = useState(false)
const needsCollapsing = thread.content.length > contentLimit
const content = isCollapsed
@ -53,11 +36,11 @@ function ResolvedCommentEntry({
: thread.content
const handleUnresolve = () => {
// TODO unresolve comment
unresolveComment(thread.threadId)
}
const handleDelete = () => {
// TODO delete thread
deleteThread(undefined, thread.docId, thread.threadId)
}
return (

View file

@ -1,16 +1,85 @@
import { useTranslation } from 'react-i18next'
import { useState } from 'react'
import { useState, useMemo, useCallback } from 'react'
import Icon from '../../../../../shared/components/icon'
import Tooltip from '../../../../../shared/components/tooltip'
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'
export interface FilteredResolvedComments
extends ReviewPanelResolvedCommentThread {
content: string
threadId: ThreadId
entryId: string
docId: DocId
docName: string | null
}
function ResolvedCommentsDropdown() {
const { t } = useTranslation()
const [isOpen, setIsOpen] = useState(false)
// TODO setIsLoading
// eslint-disable-next-line no-unused-vars
const [isLoading, setIsLoading] = useState(false)
const {
docs,
commentThreads,
resolvedComments,
refreshResolvedCommentsDropdown,
} = useReviewPanelValueContext()
const handleResolvedCommentsClick = () => {
setIsOpen(isOpen => {
if (!isOpen) {
setIsLoading(true)
refreshResolvedCommentsDropdown().finally(() => setIsLoading(false))
}
return !isOpen
})
}
const getDocNameById = useCallback(
(docId: DocId) => {
return docs?.find(doc => doc.doc.id === docId)?.doc.name || null
},
[docs]
)
const filteredResolvedComments = useMemo(() => {
const comments: FilteredResolvedComments[] = []
for (const [docId, docEntries] of Object.entries(resolvedComments) as Array<
[DocId, ReviewPanelDocEntries]
>) {
for (const [entryId, entry] of Object.entries(docEntries)) {
if (entry.type === 'comment') {
const threadId = entry.thread_id
const thread =
threadId in commentThreads
? commentThreads[entry.thread_id]
: undefined
if (thread?.resolved) {
comments.push({
...thread,
content: entry.content,
threadId,
entryId,
docId,
docName: getDocNameById(docId),
})
}
}
}
}
return comments
}, [commentThreads, getDocNameById, resolvedComments])
return (
<div className="resolved-comments">
@ -29,7 +98,7 @@ function ResolvedCommentsDropdown() {
>
<button
className="resolved-comments-toggle"
onClick={() => setIsOpen(value => !value)}
onClick={handleResolvedCommentsClick}
aria-label={t('resolved_comments')}
>
<Icon type="inbox" />
@ -47,30 +116,7 @@ function ResolvedCommentsDropdown() {
</div>
) : (
<ResolvedCommentsScroller
resolvedComments={[
{
resolved_at: 12345,
entryId: '123',
docName: 'demo name',
content: 'demo content',
messages: [
{
id: '123',
user: {
id: '123',
hue: 'abcde',
name: 'demo name',
},
content: 'demo content',
timestamp: '12345',
},
],
resolved_by_user: {
name: 'demo',
hue: 'abcde',
},
},
]}
resolvedComments={filteredResolvedComments}
/>
)}
</div>

View file

@ -1,29 +1,10 @@
import { useTranslation } from 'react-i18next'
import { useMemo } from 'react'
import ResolvedCommentEntry from '../entries/resolved-comment-entry'
import moment from 'moment'
import { FilteredResolvedComments } from './resolved-comments-dropdown'
type ResolvedCommentsScrollerProps = {
resolvedComments: Array<{
resolved_at: number
entryId: string
docName: string
content: string
messages: Array<{
id: string
user: {
id: string
hue: string
name: string
}
content: string
timestamp: string
}>
resolved_by_user: {
name: string
hue: string
}
}> // TODO extract type
resolvedComments: FilteredResolvedComments[]
}
function ResolvedCommentsScroller({
@ -31,12 +12,10 @@ function ResolvedCommentsScroller({
}: ResolvedCommentsScrollerProps) {
const { t } = useTranslation()
// TODO remove momentjs
const sortedResolvedComments = useMemo(() => {
return [...resolvedComments].sort(
(a, b) =>
moment(b.resolved_at).valueOf() - moment(a.resolved_at).valueOf()
)
return [...resolvedComments].sort((a, b) => {
return Date.parse(b.resolved_at) - Date.parse(a.resolved_at)
})
}, [resolvedComments])
return (

View file

@ -20,14 +20,19 @@ function useAngularReviewPanelState(): ReviewPanelState {
'reviewPanel.commentThreads',
true
)
const [docs] = useScopeValue<ReviewPanel.Value<'docs'>>('docs')
const [entries] = useScopeValue<ReviewPanel.Value<'entries'>>(
'reviewPanel.entries'
'reviewPanel.entries',
true
)
const [loadingThreads] =
useScopeValue<ReviewPanel.Value<'loadingThreads'>>('loadingThreads')
const [permissions] =
useScopeValue<ReviewPanel.Value<'permissions'>>('permissions')
const [resolvedComments] = useScopeValue<
ReviewPanel.Value<'resolvedComments'>
>('reviewPanel.resolvedComments', true)
const [wantTrackChanges] = useScopeValue<
ReviewPanel.Value<'wantTrackChanges'>
@ -75,6 +80,13 @@ function useAngularReviewPanelState(): ReviewPanelState {
const [toggleReviewPanel] =
useScopeValue<ReviewPanel.Value<'toggleReviewPanel'>>('toggleReviewPanel')
const [unresolveComment] =
useScopeValue<ReviewPanel.Value<'unresolveComment'>>('unresolveComment')
const [deleteThread] =
useScopeValue<ReviewPanel.Value<'deleteThread'>>('deleteThread')
const [refreshResolvedCommentsDropdown] = useScopeValue<
ReviewPanel.Value<'refreshResolvedCommentsDropdown'>
>('refreshResolvedCommentsDropdown')
const handleSetSubview = useCallback(
(subView: SubView) => {
@ -104,6 +116,7 @@ function useAngularReviewPanelState(): ReviewPanelState {
collapsed,
commentThreads,
deleteComment,
docs,
entries,
entryHover,
gotoEntry,
@ -111,6 +124,7 @@ function useAngularReviewPanelState(): ReviewPanelState {
loadingThreads,
permissions,
resolveComment,
resolvedComments,
saveEdit,
shouldCollapse,
submitReply,
@ -126,11 +140,15 @@ function useAngularReviewPanelState(): ReviewPanelState {
trackChangesForGuestsAvailable,
formattedProjectMembers,
toggleReviewPanel,
unresolveComment,
deleteThread,
refreshResolvedCommentsDropdown,
}),
[
collapsed,
commentThreads,
deleteComment,
docs,
entries,
entryHover,
gotoEntry,
@ -138,6 +156,7 @@ function useAngularReviewPanelState(): ReviewPanelState {
loadingThreads,
permissions,
resolveComment,
resolvedComments,
saveEdit,
shouldCollapse,
submitReply,
@ -153,6 +172,9 @@ function useAngularReviewPanelState(): ReviewPanelState {
trackChangesForGuestsAvailable,
formattedProjectMembers,
toggleReviewPanel,
unresolveComment,
deleteThread,
refreshResolvedCommentsDropdown,
]
)

View file

@ -8,12 +8,14 @@ import {
ThreadId,
} from '../../../../../../../types/review-panel/review-panel'
import { ReviewPanelCommentEntry } from '../../../../../../../types/review-panel/entry'
import { MainDocument } from '../../../../../../../types/project-settings'
export interface ReviewPanelState {
values: {
collapsed: Record<string, boolean>
commentThreads: ReviewPanelCommentThreads
deleteComment: (threadId: ThreadId, commentId: CommentId) => void
docs: MainDocument[] | undefined
entries: ReviewPanelEntries
entryHover: boolean
gotoEntry: (docId: DocId, entryOffset: number) => void
@ -21,6 +23,7 @@ export interface ReviewPanelState {
loadingThreads: boolean
permissions: ReviewPanelPermissions
resolveComment: (docId: DocId, entryId: ThreadId) => void
resolvedComments: ReviewPanelEntries
saveEdit: (
threadId: ThreadId,
commentId: CommentId,
@ -46,6 +49,9 @@ export interface ReviewPanelState {
}
>
toggleReviewPanel: () => void
unresolveComment: (threadId: ThreadId) => void
deleteThread: (_entryId: unknown, docId: DocId, threadId: ThreadId) => void
refreshResolvedCommentsDropdown: () => Promise<void>
}
updaterFns: {
handleSetSubview: (subView: SubView) => void

View file

@ -42,7 +42,10 @@ describe('<ReviewPanel />', function () {
})
// eslint-disable-next-line mocha/no-skipped-tests
it.skip('opens dropdown', function () {})
it.skip('opens dropdown', function () {
cy.findByRole('button', { name: /resolved comments/i }).click()
// TODO dropdown opens/closes
})
// eslint-disable-next-line mocha/no-skipped-tests
it.skip('renders list of resolved comments', function () {})

View file

@ -1,5 +1,6 @@
import { docId, mockDoc } from './mock-doc'
import { Folder } from '../../../../../types/folder'
import { sleep } from '../../../helpers/sleep'
export const rootFolderId = '012345678901234567890123'
export const figuresFolderId = '123456789012345678901234'
@ -79,12 +80,14 @@ export const mockScope = (content?: string) => {
formattedProjectMembers: {},
fullTCStateCollapsed: true,
entries: {},
resolvedComments: {},
},
ui: {
reviewPanelOpen: true,
},
toggleReviewPanel: cy.stub(),
toggleTrackChangesForEveryone: cy.stub(),
refreshResolvedCommentsDropdown: cy.stub(() => sleep(1000)),
onlineUserCursorHighlights: {},
permissionsLevel: 'owner',
$on: cy.stub(),

View file

@ -0,0 +1,3 @@
export const sleep = (timeout: number) => {
return new Promise(resolve => setTimeout(resolve, timeout))
}

View file

@ -0,0 +1,3 @@
import { Brand } from './brand'
export type DateString = Brand<string, 'DateString'>

View file

@ -0,0 +1,31 @@
import {
ReviewPanelCommentThreadMessage,
ReviewPanelUser,
UserId,
} from './review-panel'
import { DateString } from '../helpers/date'
interface ReviewPanelCommentThreadBase {
messages: Array<ReviewPanelCommentThreadMessage>
submitting?: boolean // angular specific (to be made into a local state)
}
interface ReviewPanelUnresolvedCommentThread
extends ReviewPanelCommentThreadBase {
resolved?: never
resolved_at?: never
resolved_by_user_id?: never
resolved_by_user?: never
}
export interface ReviewPanelResolvedCommentThread
extends ReviewPanelCommentThreadBase {
resolved: boolean
resolved_at: DateString
resolved_by_user_id: UserId
resolved_by_user: ReviewPanelUser
}
export type ReviewPanelCommentThread =
| ReviewPanelUnresolvedCommentThread
| ReviewPanelResolvedCommentThread

View file

@ -1,5 +1,6 @@
import { Brand } from '../helpers/brand'
import { ReviewPanelEntry } from './entry'
import { ReviewPanelCommentThread } from './comment-thread'
export type SubView = 'cur_file' | 'overview'
@ -11,14 +12,14 @@ export interface ReviewPanelPermissions {
}
export type ThreadId = Brand<string, 'ThreadId'>
type ReviewPanelDocEntries = Record<ThreadId, ReviewPanelEntry>
export type ReviewPanelDocEntries = Record<ThreadId, ReviewPanelEntry>
export type DocId = Brand<string, 'DocId'>
export type ReviewPanelEntries = Record<DocId, ReviewPanelDocEntries>
type UserId = Brand<string, 'UserId'>
export type UserId = Brand<string, 'UserId'>
interface ReviewPanelUser {
export interface ReviewPanelUser {
avatar_text: string
email: string
hue: number
@ -28,6 +29,7 @@ interface ReviewPanelUser {
}
export type CommentId = Brand<string, 'CommentId'>
export interface ReviewPanelCommentThreadMessage {
content: string
id: CommentId
@ -36,15 +38,6 @@ export interface ReviewPanelCommentThreadMessage {
user_id: UserId
}
export interface ReviewPanelCommentThread {
messages: Array<ReviewPanelCommentThreadMessage>
// resolved: boolean
// resolved_at: number
// resolved_by_user_id: string
// resolved_by_user: ReviewPanelUser
submitting?: boolean // angular specific (to be made into a local state)
}
export type ReviewPanelCommentThreads = Record<
ThreadId,
ReviewPanelCommentThread