Merge pull request #13671 from overleaf/ii-review-panel-migration-aggregate-change-entry

[web] Create aggregate change entries

GitOrigin-RevId: 685ac40739f3c39665d84bd402ada21e00db5146
This commit is contained in:
Eric Mc Sween 2023-07-06 07:19:04 -04:00 committed by Copybot
parent 75a86bab87
commit d89b62e965
7 changed files with 244 additions and 35 deletions

View file

@ -42,6 +42,8 @@
"additional_licenses": "",
"address_line_1": "",
"address_second_line_optional": "",
"aggregate_changed": "",
"aggregate_to": "",
"all_premium_features": "",
"all_premium_features_including": "",
"all_projects": "",

View file

@ -79,7 +79,18 @@ function CurrentFileContainer() {
}
if (entry.type === 'aggregate-change') {
return <AggregateChangeEntry key={id} />
return (
<AggregateChangeEntry
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 === 'comment' && !loadingThreads) {

View file

@ -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 { ReviewPanelAggregateChangeEntry } from '../../../../../../../types/review-panel/entry'
import {
ReviewPanelPermissions,
ReviewPanelUser,
} from '../../../../../../../types/review-panel/review-panel'
import { DocId } from '../../../../../../../types/project-settings'
function AggregateChangeEntry() {
return <EntryContainer>Aggregate change entry</EntryContainer>
type AggregateChangeEntryProps = {
docId: DocId
entry: ReviewPanelAggregateChangeEntry
permissions: ReviewPanelPermissions
user: ReviewPanelUser | undefined
contentLimit?: number
onMouseEnter?: () => void
onMouseLeave?: () => void
onIndicatorClick?: () => void
}
function AggregateChangeEntry({
docId,
entry,
permissions,
user,
contentLimit = 17,
onMouseEnter,
onMouseLeave,
onIndicatorClick,
}: AggregateChangeEntryProps) {
const { t } = useTranslation()
const { acceptChanges, rejectChanges, handleLayoutChange, gotoEntry } =
useReviewPanelValueContext()
const [isDeletionCollapsed, setIsDeletionCollapsed] = useState(true)
const [isInsertionCollapsed, setIsInsertionCollapsed] = useState(true)
const replacedContent = entry.metadata.replaced_content
const content = entry.content
const deletionNeedsCollapsing = replacedContent.length > contentLimit
const insertionNeedsCollapsing = content.length > contentLimit
const deletionContent = isDeletionCollapsed
? replacedContent.substring(0, contentLimit)
: replacedContent
const insertionContent = isInsertionCollapsed
? content.substring(0, contentLimit)
: content
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 handleDeletionToggleCollapse = () => {
setIsDeletionCollapsed(value => !value)
handleLayoutChange()
}
const handleInsertionToggleCollapse = () => {
setIsInsertionCollapsed(value => !value)
handleLayoutChange()
}
return (
<EntryContainer
onClick={handleEntryClick}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
>
<div
className="rp-entry-callout rp-entry-callout-aggregate"
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}
>
<Icon type="pencil" />
</div>
<div
className={classnames('rp-entry', 'rp-entry-aggregate', {
'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">
<Icon type="pencil" />
</div>
<div className="rp-entry-details">
<div className="rp-entry-description">
{t('aggregate_changed')}&nbsp;
<del className="rp-content-highlight">{deletionContent}</del>
{deletionNeedsCollapsing && (
<button
className="rp-collapse-toggle btn-inline-link"
onClick={handleDeletionToggleCollapse}
>
{isDeletionCollapsed
? `… (${t('show_all')})`
: ` (${t('show_less')})`}
</button>
)}
&nbsp;{t('aggregate_to')}&nbsp;
<ins className="rp-content-highlight">{insertionContent}</ins>
{insertionNeedsCollapsing && (
<button
className="rp-collapse-toggle btn-inline-link"
onClick={handleInsertionToggleCollapse}
>
{isInsertionCollapsed
? `… (${t('show_all')})`
: ` (${t('show_less')})`}
</button>
)}
</div>
<div className="rp-entry-metadata">
{formatTime(entry.metadata.ts, 'MMM d, y h:mm a')}
&nbsp;&bull;&nbsp;
{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 AggregateChangeEntry

View file

@ -40,7 +40,7 @@ function ChangeEntry({
const { t } = useTranslation()
const { acceptChanges, rejectChanges, handleLayoutChange, gotoEntry } =
useReviewPanelValueContext()
const [isCollapsed, setIsCollapsed] = useState(false)
const [isCollapsed, setIsCollapsed] = useState(true)
const content = isCollapsed
? entry.content.substring(0, contentLimit)

View file

@ -89,7 +89,15 @@ function OverviewFile({ docId, docPath }: OverviewFileProps) {
}
if (entry.type === 'aggregate-change') {
return <AggregateChangeEntry key={id} />
return (
<AggregateChangeEntry
key={id}
docId={docId}
entry={entry}
permissions={permissions}
user={users[entry.metadata.user_id]}
/>
)
}
if (entry.type === 'comment') {

View file

@ -195,10 +195,16 @@ describe('<ReviewPanel />', function () {
describe('change entries', function () {
// eslint-disable-next-line mocha/no-skipped-tests
it.skip('renders inserted entries', function () {})
it.skip('renders inserted entries in current file mode', function () {})
// eslint-disable-next-line mocha/no-skipped-tests
it.skip('renders deleted entries', function () {})
it.skip('renders deleted entries in current file mode', function () {})
// eslint-disable-next-line mocha/no-skipped-tests
it.skip('renders inserted entries in overview mode', function () {})
// eslint-disable-next-line mocha/no-skipped-tests
it.skip('renders deleted entries in overview mode', function () {})
// eslint-disable-next-line mocha/no-skipped-tests
it.skip('accepts change', function () {})
@ -207,6 +213,14 @@ describe('<ReviewPanel />', function () {
it.skip('rejects change', function () {})
})
describe('aggregate change entries', function () {
// eslint-disable-next-line mocha/no-skipped-tests
it.skip('renders changed entries in current file mode', function () {})
// eslint-disable-next-line mocha/no-skipped-tests
it.skip('renders changed entries in overview mode', function () {})
})
describe('overview mode', function () {
// eslint-disable-next-line mocha/no-skipped-tests
it.skip('shows list of files changed', function () {})

View file

@ -7,46 +7,50 @@ interface ReviewPanelEntryScreenPos {
}
interface ReviewPanelBaseEntry {
visible: boolean
focused: boolean
offset: number
screenPos: ReviewPanelEntryScreenPos
visible: boolean
}
interface ReviewPanelInsertOrDeleteEntry {
content: string
entry_ids: ThreadId[]
metadata: {
ts: Date
user_id: UserId
}
}
export interface ReviewPanelInsertEntry
extends ReviewPanelBaseEntry,
ReviewPanelInsertOrDeleteEntry {
type: 'insert'
}
export interface ReviewPanelDeleteEntry
extends ReviewPanelBaseEntry,
ReviewPanelInsertOrDeleteEntry {
type: 'delete'
}
export interface ReviewPanelCommentEntry extends ReviewPanelBaseEntry {
type: 'comment'
content: string
entry_ids: ThreadId[]
focused: boolean
screenPos: ReviewPanelEntryScreenPos
thread_id: ThreadId
replyContent?: string // angular specific
}
export interface ReviewPanelInsertEntry extends ReviewPanelBaseEntry {
type: 'insert'
content: string
entry_ids: ThreadId[]
metadata: {
ts: Date
user_id: UserId
}
screenPos: ReviewPanelEntryScreenPos
focused?: boolean
}
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 {
export interface ReviewPanelAggregateChangeEntry extends ReviewPanelBaseEntry {
type: 'aggregate-change'
content: string
entry_ids: ThreadId[]
metadata: {
replaced_content: string
ts: Date
user_id: UserId
}
}
interface ReviewPanelAddCommentEntry extends ReviewPanelBaseEntry {