mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-21 20:47:08 -05:00
Merge pull request #13658 from overleaf/ii-review-panel-migration-change-entry
[web] Create change entries GitOrigin-RevId: 4d49f8b02b9bfcfe470f6db21f5347080ff47562
This commit is contained in:
parent
c165eb80b1
commit
b1f246b9ba
11 changed files with 295 additions and 45 deletions
|
@ -13,6 +13,7 @@
|
|||
"about_to_enable_managed_users": "",
|
||||
"about_to_leave_projects": "",
|
||||
"about_to_trash_projects": "",
|
||||
"accept": "",
|
||||
"accepted_invite": "",
|
||||
"access_denied": "",
|
||||
"account_has_been_link_to_institution_account": "",
|
||||
|
@ -790,6 +791,7 @@
|
|||
"refresh_page_after_starting_free_trial": "",
|
||||
"refreshing": "",
|
||||
"regards": "",
|
||||
"reject": "",
|
||||
"relink_your_account": "",
|
||||
"remote_service_error": "",
|
||||
"remove": "",
|
||||
|
@ -1031,6 +1033,8 @@
|
|||
"track_changes_for_x": "",
|
||||
"track_changes_is_off": "",
|
||||
"track_changes_is_on": "",
|
||||
"tracked_change_added": "",
|
||||
"tracked_change_deleted": "",
|
||||
"trash": "",
|
||||
"trash_projects": "",
|
||||
"trashed": "",
|
||||
|
|
|
@ -8,14 +8,25 @@ 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'
|
||||
import { useReviewPanelValueContext } from '../../context/review-panel/review-panel-context'
|
||||
import {
|
||||
useReviewPanelUpdaterFnsContext,
|
||||
useReviewPanelValueContext,
|
||||
} from '../../context/review-panel/review-panel-context'
|
||||
import useCodeMirrorContentHeight from '../../hooks/use-codemirror-content-height'
|
||||
import { ReviewPanelEntry } from '../../../../../../types/review-panel/entry'
|
||||
import { ThreadId } from '../../../../../../types/review-panel/review-panel'
|
||||
|
||||
function CurrentFileContainer() {
|
||||
const { commentThreads, entries, openDocId, permissions, loadingThreads } =
|
||||
useReviewPanelValueContext()
|
||||
const {
|
||||
commentThreads,
|
||||
entries,
|
||||
openDocId,
|
||||
permissions,
|
||||
loadingThreads,
|
||||
users,
|
||||
toggleReviewPanel,
|
||||
} = useReviewPanelValueContext()
|
||||
const { setEntryHover } = useReviewPanelUpdaterFnsContext()
|
||||
const contentHeight = useCodeMirrorContentHeight()
|
||||
|
||||
console.log('Review panel got content height', contentHeight)
|
||||
|
@ -53,7 +64,18 @@ function CurrentFileContainer() {
|
|||
}
|
||||
|
||||
if (entry.type === 'insert' || entry.type === 'delete') {
|
||||
return <ChangeEntry key={id} />
|
||||
return (
|
||||
<ChangeEntry
|
||||
key={id}
|
||||
docId={openDocId}
|
||||
entry={entry}
|
||||
permissions={permissions}
|
||||
user={users[entry.metadata.user_id]}
|
||||
onMouseEnter={setEntryHover.bind(null, true)}
|
||||
onMouseLeave={setEntryHover.bind(null, false)}
|
||||
onIndicatorClick={toggleReviewPanel}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
if (entry.type === 'aggregate-change') {
|
||||
|
@ -69,6 +91,9 @@ function CurrentFileContainer() {
|
|||
entryId={id}
|
||||
permissions={permissions}
|
||||
threads={commentThreads}
|
||||
onMouseEnter={setEntryHover.bind(null, true)}
|
||||
onMouseLeave={setEntryHover.bind(null, false)}
|
||||
onIndicatorClick={toggleReviewPanel}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,7 +1,177 @@
|
|||
import { useTranslation } from 'react-i18next'
|
||||
import { useState } from 'react'
|
||||
import EntryContainer from './entry-container'
|
||||
import EntryActions from './entry-actions'
|
||||
import Icon from '../../../../../shared/components/icon'
|
||||
import { useReviewPanelValueContext } from '../../../context/review-panel/review-panel-context'
|
||||
import { formatTime } from '../../../../utils/format-date'
|
||||
import classnames from 'classnames'
|
||||
import {
|
||||
ReviewPanelDeleteEntry,
|
||||
ReviewPanelInsertEntry,
|
||||
} from '../../../../../../../types/review-panel/entry'
|
||||
import {
|
||||
ReviewPanelPermissions,
|
||||
ReviewPanelUser,
|
||||
} from '../../../../../../../types/review-panel/review-panel'
|
||||
import { DocId } from '../../../../../../../types/project-settings'
|
||||
|
||||
function ChangeEntry() {
|
||||
return <EntryContainer>Change entry</EntryContainer>
|
||||
type ChangeEntryProps = {
|
||||
docId: DocId
|
||||
entry: ReviewPanelInsertEntry | ReviewPanelDeleteEntry
|
||||
permissions: ReviewPanelPermissions
|
||||
user: ReviewPanelUser | undefined
|
||||
contentLimit?: number
|
||||
onMouseEnter?: () => void
|
||||
onMouseLeave?: () => void
|
||||
onIndicatorClick?: () => void
|
||||
}
|
||||
|
||||
function ChangeEntry({
|
||||
docId,
|
||||
entry,
|
||||
permissions,
|
||||
user,
|
||||
contentLimit = 40,
|
||||
onMouseEnter,
|
||||
onMouseLeave,
|
||||
onIndicatorClick,
|
||||
}: ChangeEntryProps) {
|
||||
const { t } = useTranslation()
|
||||
const { acceptChanges, rejectChanges, handleLayoutChange, gotoEntry } =
|
||||
useReviewPanelValueContext()
|
||||
const [isCollapsed, setIsCollapsed] = useState(false)
|
||||
|
||||
const content = isCollapsed
|
||||
? entry.content.substring(0, contentLimit)
|
||||
: entry.content
|
||||
|
||||
const needsCollapsing = entry.content.length > contentLimit
|
||||
|
||||
const handleEntryClick = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||
const target = e.target as Element
|
||||
|
||||
for (const selector of [
|
||||
'.rp-entry',
|
||||
'.rp-entry-description',
|
||||
'.rp-entry-body',
|
||||
'.rp-entry-action-icon i',
|
||||
]) {
|
||||
if (target.matches(selector)) {
|
||||
gotoEntry(docId, entry.offset)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleToggleCollapse = () => {
|
||||
setIsCollapsed(value => !value)
|
||||
handleLayoutChange()
|
||||
}
|
||||
|
||||
return (
|
||||
<EntryContainer
|
||||
onClick={handleEntryClick}
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}
|
||||
>
|
||||
<div
|
||||
className={classnames(
|
||||
'rp-entry-callout',
|
||||
`rp-entry-callout-${entry.type}`
|
||||
)}
|
||||
style={{
|
||||
top: entry.screenPos
|
||||
? entry.screenPos.y + entry.screenPos.height - 1 + 'px'
|
||||
: undefined,
|
||||
}}
|
||||
/>
|
||||
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */}
|
||||
<div
|
||||
className={classnames('rp-entry-indicator', {
|
||||
'rp-entry-indicator-focused': entry.focused,
|
||||
})}
|
||||
style={{
|
||||
top: entry.screenPos ? entry.screenPos.y + 'px' : undefined,
|
||||
}}
|
||||
onClick={onIndicatorClick}
|
||||
>
|
||||
{entry.type === 'insert' ? (
|
||||
<Icon type="pencil" />
|
||||
) : (
|
||||
<i className="rp-icon-delete" />
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className={classnames('rp-entry', `rp-entry-${entry.type}`, {
|
||||
'rp-entry-focused': entry.focused,
|
||||
})}
|
||||
style={{
|
||||
top: entry.screenPos ? entry.screenPos.y + 'px' : undefined,
|
||||
visibility: entry.visible ? 'visible' : 'hidden',
|
||||
}}
|
||||
>
|
||||
<div className="rp-entry-body">
|
||||
<div className="rp-entry-action-icon">
|
||||
{entry.type === 'insert' ? (
|
||||
<Icon type="pencil" />
|
||||
) : (
|
||||
<i className="rp-icon-delete" />
|
||||
)}
|
||||
</div>
|
||||
<div className="rp-entry-details">
|
||||
<div className="rp-entry-description">
|
||||
<span>
|
||||
{entry.type === 'insert' ? (
|
||||
<>
|
||||
{t('tracked_change_added')}
|
||||
<ins className="rp-content-highlight">{content}</ins>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{t('tracked_change_deleted')}
|
||||
<del className="rp-content-highlight">{content}</del>
|
||||
</>
|
||||
)}
|
||||
{needsCollapsing && (
|
||||
<button
|
||||
className="rp-collapse-toggle btn-inline-link"
|
||||
onClick={handleToggleCollapse}
|
||||
>
|
||||
{isCollapsed
|
||||
? `… (${t('show_all')})`
|
||||
: ` (${t('show_less')})`}
|
||||
</button>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="rp-entry-metadata">
|
||||
{formatTime(entry.metadata.ts, 'MMM d, y h:mm a')}
|
||||
•
|
||||
{user && (
|
||||
<span
|
||||
className="rp-entry-user"
|
||||
style={{ color: `hsl(${user.hue}, 70%, 40%)` }}
|
||||
>
|
||||
{user.name ?? t('anonymous')}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{permissions.write && (
|
||||
<EntryActions>
|
||||
<EntryActions.Button onClick={() => rejectChanges(entry.entry_ids)}>
|
||||
<Icon type="times" /> {t('reject')}
|
||||
</EntryActions.Button>
|
||||
<EntryActions.Button onClick={() => acceptChanges(entry.entry_ids)}>
|
||||
<Icon type="check" /> {t('accept')}
|
||||
</EntryActions.Button>
|
||||
</EntryActions>
|
||||
)}
|
||||
</div>
|
||||
</EntryContainer>
|
||||
)
|
||||
}
|
||||
|
||||
export default ChangeEntry
|
||||
|
|
|
@ -7,10 +7,7 @@ import AutoExpandingTextArea, {
|
|||
resetHeight,
|
||||
} from '../../../../../shared/components/auto-expanding-text-area'
|
||||
import Icon from '../../../../../shared/components/icon'
|
||||
import {
|
||||
useReviewPanelUpdaterFnsContext,
|
||||
useReviewPanelValueContext,
|
||||
} from '../../../context/review-panel/review-panel-context'
|
||||
import { useReviewPanelValueContext } from '../../../context/review-panel/review-panel-context'
|
||||
import classnames from 'classnames'
|
||||
import { ReviewPanelCommentEntry } from '../../../../../../../types/review-panel/entry'
|
||||
import {
|
||||
|
@ -26,6 +23,9 @@ type CommentEntryProps = {
|
|||
entryId: ThreadId
|
||||
permissions: ReviewPanelPermissions
|
||||
threads: ReviewPanelCommentThreads
|
||||
onMouseEnter?: () => void
|
||||
onMouseLeave?: () => void
|
||||
onIndicatorClick?: () => void
|
||||
}
|
||||
|
||||
function CommentEntry({
|
||||
|
@ -34,17 +34,13 @@ function CommentEntry({
|
|||
entryId,
|
||||
permissions,
|
||||
threads,
|
||||
onMouseEnter,
|
||||
onMouseLeave,
|
||||
onIndicatorClick,
|
||||
}: CommentEntryProps) {
|
||||
const { t } = useTranslation()
|
||||
const {
|
||||
gotoEntry,
|
||||
toggleReviewPanel,
|
||||
resolveComment,
|
||||
submitReply,
|
||||
handleLayoutChange,
|
||||
} = useReviewPanelValueContext()
|
||||
const { setEntryHover } = useReviewPanelUpdaterFnsContext()
|
||||
|
||||
const { gotoEntry, resolveComment, submitReply, handleLayoutChange } =
|
||||
useReviewPanelValueContext()
|
||||
const [replyContent, setReplyContent] = useState('')
|
||||
const [animating, setAnimating] = useState(false)
|
||||
const [resolved, setResolved] = useState(false)
|
||||
|
@ -55,16 +51,18 @@ function CommentEntry({
|
|||
|
||||
const handleEntryClick = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||
const target = e.target as Element
|
||||
if (
|
||||
[
|
||||
'rp-entry',
|
||||
'rp-comment-loaded',
|
||||
'rp-comment-content',
|
||||
'rp-comment-reply',
|
||||
'rp-entry-metadata',
|
||||
].some(className => [...target.classList].includes(className))
|
||||
) {
|
||||
|
||||
for (const selector of [
|
||||
'.rp-entry',
|
||||
'.rp-comment-loaded',
|
||||
'.rp-comment-content',
|
||||
'.rp-comment-reply',
|
||||
'.rp-entry-metadata',
|
||||
]) {
|
||||
if (target.matches(selector)) {
|
||||
gotoEntry(docId, entry.offset)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -109,15 +107,16 @@ function CommentEntry({
|
|||
}
|
||||
|
||||
return (
|
||||
<EntryContainer>
|
||||
<EntryContainer
|
||||
onClick={handleEntryClick}
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}
|
||||
>
|
||||
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */}
|
||||
<div
|
||||
className={classnames('rp-comment-wrapper', {
|
||||
'rp-comment-wrapper-resolving': animating,
|
||||
})}
|
||||
onMouseEnter={() => setEntryHover(true)}
|
||||
onMouseLeave={() => setEntryHover(false)}
|
||||
onClick={handleEntryClick}
|
||||
>
|
||||
<div
|
||||
className="rp-entry-callout rp-entry-callout-comment"
|
||||
|
@ -135,7 +134,7 @@ function CommentEntry({
|
|||
style={{
|
||||
top: entry.screenPos ? `${entry.screenPos.y}px` : undefined,
|
||||
}}
|
||||
onClick={toggleReviewPanel}
|
||||
onClick={onIndicatorClick}
|
||||
>
|
||||
<Icon type="comment" />
|
||||
</div>
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
type EntryContainerProps = {
|
||||
children: React.ReactNode
|
||||
}
|
||||
import classnames from 'classnames'
|
||||
|
||||
function EntryContainer({ children }: EntryContainerProps) {
|
||||
return <div className="rp-entry-wrapper">{children}</div>
|
||||
function EntryContainer({ className, ...rest }: React.ComponentProps<'div'>) {
|
||||
return <div className={classnames('rp-entry-wrapper', className)} {...rest} />
|
||||
}
|
||||
|
||||
export default EntryContainer
|
||||
|
|
|
@ -19,7 +19,7 @@ type OverviewFileProps = {
|
|||
}
|
||||
|
||||
function OverviewFile({ docId, docPath }: OverviewFileProps) {
|
||||
const { entries, collapsed, commentThreads, permissions } =
|
||||
const { entries, collapsed, commentThreads, permissions, users } =
|
||||
useReviewPanelValueContext()
|
||||
const { setCollapsed } = useReviewPanelUpdaterFnsContext()
|
||||
|
||||
|
@ -77,7 +77,15 @@ function OverviewFile({ docId, docPath }: OverviewFileProps) {
|
|||
<div className="rp-overview-file-entries" ref={entriesContainerRef}>
|
||||
{objectEntries.map(([id, entry]) => {
|
||||
if (entry.type === 'insert' || entry.type === 'delete') {
|
||||
return <ChangeEntry key={id} />
|
||||
return (
|
||||
<ChangeEntry
|
||||
key={id}
|
||||
docId={docId}
|
||||
entry={entry}
|
||||
permissions={permissions}
|
||||
user={users[entry.metadata.user_id]}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
if (entry.type === 'aggregate-change') {
|
||||
|
|
|
@ -33,6 +33,7 @@ function useAngularReviewPanelState(): ReviewPanelState {
|
|||
|
||||
const [permissions] =
|
||||
useScopeValue<ReviewPanel.Value<'permissions'>>('permissions')
|
||||
const [users] = useScopeValue<ReviewPanel.Value<'users'>>('users', true)
|
||||
const [resolvedComments] = useScopeValue<
|
||||
ReviewPanel.Value<'resolvedComments'>
|
||||
>('reviewPanel.resolvedComments', true)
|
||||
|
@ -90,6 +91,10 @@ function useAngularReviewPanelState(): ReviewPanelState {
|
|||
const [refreshResolvedCommentsDropdown] = useScopeValue<
|
||||
ReviewPanel.Value<'refreshResolvedCommentsDropdown'>
|
||||
>('refreshResolvedCommentsDropdown')
|
||||
const [acceptChanges] =
|
||||
useScopeValue<ReviewPanel.Value<'acceptChanges'>>('acceptChanges')
|
||||
const [rejectChanges] =
|
||||
useScopeValue<ReviewPanel.Value<'rejectChanges'>>('rejectChanges')
|
||||
|
||||
const handleSetSubview = useCallback(
|
||||
(subView: SubView) => {
|
||||
|
@ -126,6 +131,7 @@ function useAngularReviewPanelState(): ReviewPanelState {
|
|||
handleLayoutChange,
|
||||
loadingThreads,
|
||||
permissions,
|
||||
users,
|
||||
resolveComment,
|
||||
resolvedComments,
|
||||
saveEdit,
|
||||
|
@ -147,6 +153,8 @@ function useAngularReviewPanelState(): ReviewPanelState {
|
|||
unresolveComment,
|
||||
deleteThread,
|
||||
refreshResolvedCommentsDropdown,
|
||||
acceptChanges,
|
||||
rejectChanges,
|
||||
}),
|
||||
[
|
||||
collapsed,
|
||||
|
@ -159,6 +167,7 @@ function useAngularReviewPanelState(): ReviewPanelState {
|
|||
handleLayoutChange,
|
||||
loadingThreads,
|
||||
permissions,
|
||||
users,
|
||||
resolveComment,
|
||||
resolvedComments,
|
||||
saveEdit,
|
||||
|
@ -180,6 +189,8 @@ function useAngularReviewPanelState(): ReviewPanelState {
|
|||
unresolveComment,
|
||||
deleteThread,
|
||||
refreshResolvedCommentsDropdown,
|
||||
acceptChanges,
|
||||
rejectChanges,
|
||||
]
|
||||
)
|
||||
|
||||
|
|
|
@ -3,6 +3,7 @@ import {
|
|||
ReviewPanelCommentThreads,
|
||||
ReviewPanelEntries,
|
||||
ReviewPanelPermissions,
|
||||
ReviewPanelUsers,
|
||||
SubView,
|
||||
ThreadId,
|
||||
} from '../../../../../../../types/review-panel/review-panel'
|
||||
|
@ -24,6 +25,7 @@ export interface ReviewPanelState {
|
|||
handleLayoutChange: () => void
|
||||
loadingThreads: boolean
|
||||
permissions: ReviewPanelPermissions
|
||||
users: ReviewPanelUsers
|
||||
resolveComment: (docId: DocId, entryId: ThreadId) => void
|
||||
resolvedComments: ReviewPanelEntries
|
||||
saveEdit: (
|
||||
|
@ -55,6 +57,8 @@ export interface ReviewPanelState {
|
|||
unresolveComment: (threadId: ThreadId) => void
|
||||
deleteThread: (_entryId: unknown, docId: DocId, threadId: ThreadId) => void
|
||||
refreshResolvedCommentsDropdown: () => Promise<void>
|
||||
acceptChanges: (entryIds: unknown) => void
|
||||
rejectChanges: (entryIds: unknown) => void
|
||||
}
|
||||
updaterFns: {
|
||||
handleSetSubview: (subView: SubView) => void
|
||||
|
|
|
@ -193,6 +193,20 @@ describe('<ReviewPanel />', function () {
|
|||
it.skip('resolves comment', function () {})
|
||||
})
|
||||
|
||||
describe('change entries', function () {
|
||||
// eslint-disable-next-line mocha/no-skipped-tests
|
||||
it.skip('renders inserted entries', function () {})
|
||||
|
||||
// eslint-disable-next-line mocha/no-skipped-tests
|
||||
it.skip('renders deleted entries', function () {})
|
||||
|
||||
// eslint-disable-next-line mocha/no-skipped-tests
|
||||
it.skip('accepts change', function () {})
|
||||
|
||||
// eslint-disable-next-line mocha/no-skipped-tests
|
||||
it.skip('rejects change', function () {})
|
||||
})
|
||||
|
||||
describe('overview mode', function () {
|
||||
// eslint-disable-next-line mocha/no-skipped-tests
|
||||
it.skip('shows list of files changed', function () {})
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { ThreadId } from './review-panel'
|
||||
import { ThreadId, UserId } from './review-panel'
|
||||
|
||||
interface ReviewPanelEntryScreenPos {
|
||||
y: number
|
||||
|
@ -14,19 +14,35 @@ interface ReviewPanelBaseEntry {
|
|||
export interface ReviewPanelCommentEntry extends ReviewPanelBaseEntry {
|
||||
type: 'comment'
|
||||
content: string
|
||||
entry_ids: string[]
|
||||
entry_ids: ThreadId[]
|
||||
focused: boolean
|
||||
screenPos: ReviewPanelEntryScreenPos
|
||||
thread_id: ThreadId
|
||||
replyContent?: string // angular specific
|
||||
}
|
||||
|
||||
interface ReviewPanelInsertEntry extends ReviewPanelBaseEntry {
|
||||
export interface ReviewPanelInsertEntry extends ReviewPanelBaseEntry {
|
||||
type: 'insert'
|
||||
content: string
|
||||
entry_ids: ThreadId[]
|
||||
metadata: {
|
||||
ts: Date
|
||||
user_id: UserId
|
||||
}
|
||||
screenPos: ReviewPanelEntryScreenPos
|
||||
focused?: boolean
|
||||
}
|
||||
|
||||
interface ReviewPanelDeleteEntry extends ReviewPanelBaseEntry {
|
||||
export interface ReviewPanelDeleteEntry extends ReviewPanelBaseEntry {
|
||||
type: 'delete'
|
||||
content: string
|
||||
entry_ids: ThreadId[]
|
||||
metadata: {
|
||||
ts: Date
|
||||
user_id: UserId
|
||||
}
|
||||
screenPos: ReviewPanelEntryScreenPos
|
||||
focused?: boolean
|
||||
}
|
||||
|
||||
interface ReviewPanelAggregateChangeEntry extends ReviewPanelBaseEntry {
|
||||
|
|
|
@ -27,6 +27,7 @@ export interface ReviewPanelUser {
|
|||
isSelf: boolean
|
||||
name: string
|
||||
}
|
||||
export type ReviewPanelUsers = Record<UserId, ReviewPanelUser>
|
||||
|
||||
export type CommentId = Brand<string, 'CommentId'>
|
||||
|
||||
|
|
Loading…
Reference in a new issue