mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-21 20:47:08 -05:00
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:
parent
8cd9b4051c
commit
e5d6777211
13 changed files with 168 additions and 93 deletions
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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,
|
||||
]
|
||||
)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 () {})
|
||||
|
|
|
@ -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(),
|
||||
|
|
3
services/web/test/frontend/helpers/sleep.ts
Normal file
3
services/web/test/frontend/helpers/sleep.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
export const sleep = (timeout: number) => {
|
||||
return new Promise(resolve => setTimeout(resolve, timeout))
|
||||
}
|
3
services/web/types/helpers/date.ts
Normal file
3
services/web/types/helpers/date.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
import { Brand } from './brand'
|
||||
|
||||
export type DateString = Brand<string, 'DateString'>
|
31
services/web/types/review-panel/comment-thread.ts
Normal file
31
services/web/types/review-panel/comment-thread.ts
Normal 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
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue