mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-21 20:47:08 -05:00
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:
parent
75a86bab87
commit
d89b62e965
7 changed files with 244 additions and 35 deletions
|
@ -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": "",
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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')}
|
||||
<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>
|
||||
)}
|
||||
{t('aggregate_to')}
|
||||
<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')}
|
||||
•
|
||||
{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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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') {
|
||||
|
|
|
@ -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 () {})
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in a new issue