mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-21 20:47:08 -05:00
Connect up document diff viewer to history state
GitOrigin-RevId: 610a254ea77c194969033d0791ecf1129e02c4bf
This commit is contained in:
parent
867b37b76f
commit
11f8905be4
15 changed files with 220 additions and 61 deletions
|
@ -1,7 +1,7 @@
|
|||
import Toolbar from './toolbar'
|
||||
import Main from './main'
|
||||
|
||||
function Editor() {
|
||||
function DiffView() {
|
||||
return (
|
||||
<div className="doc-panel">
|
||||
<div className="history-header toolbar-container">
|
||||
|
@ -14,4 +14,4 @@ function Editor() {
|
|||
)
|
||||
}
|
||||
|
||||
export default Editor
|
||||
export default DiffView
|
|
@ -1,33 +1,20 @@
|
|||
import { FC, useCallback, useRef, useState } from 'react'
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import withErrorBoundary from '../../../../infrastructure/error-boundary'
|
||||
import { ErrorBoundaryFallback } from '../../../../shared/components/error-boundary-fallback'
|
||||
import { EditorState, Extension } from '@codemirror/state'
|
||||
import { EditorView } from '@codemirror/view'
|
||||
import { lineNumbers } from '../../../../../../modules/source-editor/frontend/js/extensions/line-numbers'
|
||||
import { indentationMarkers } from '@replit/codemirror-indentation-markers'
|
||||
import { highlights, setHighlightsEffect } from '../extensions/highlights'
|
||||
import { highlights, setHighlightsEffect } from '../../extensions/highlights'
|
||||
import useScopeValue from '../../../../shared/hooks/use-scope-value'
|
||||
import {
|
||||
FontFamily,
|
||||
LineHeight,
|
||||
OverallTheme,
|
||||
} from '../../../../../../modules/source-editor/frontend/js/extensions/theme'
|
||||
import { theme, Options } from '../extensions/theme'
|
||||
import { theme, Options } from '../../extensions/theme'
|
||||
import { indentUnit } from '@codemirror/language'
|
||||
|
||||
interface Range {
|
||||
from: number
|
||||
to: number
|
||||
}
|
||||
|
||||
type HighlightType = 'addition' | 'deletion'
|
||||
|
||||
export interface Highlight {
|
||||
label: string
|
||||
hue: number
|
||||
range: Range
|
||||
type: HighlightType
|
||||
}
|
||||
import { Highlight } from '../../services/types/doc'
|
||||
|
||||
function extensions(themeOptions: Options): Extension[] {
|
||||
return [
|
||||
|
@ -41,10 +28,13 @@ function extensions(themeOptions: Options): Extension[] {
|
|||
]
|
||||
}
|
||||
|
||||
const DocumentDiffViewer: FC<{ doc: string; highlights: Highlight[] }> = ({
|
||||
function DocumentDiffViewer({
|
||||
doc,
|
||||
highlights,
|
||||
}) => {
|
||||
}: {
|
||||
doc: string
|
||||
highlights: Highlight[]
|
||||
}) {
|
||||
const [fontFamily] = useScopeValue<FontFamily>('settings.fontFamily')
|
||||
const [fontSize] = useScopeValue<number>('settings.fontSize')
|
||||
const [lineHeight] = useScopeValue<LineHeight>('settings.lineHeight')
|
||||
|
@ -62,16 +52,13 @@ const DocumentDiffViewer: FC<{ doc: string; highlights: Highlight[] }> = ({
|
|||
})
|
||||
})
|
||||
|
||||
const viewRef = useRef<EditorView | null>(null)
|
||||
if (viewRef.current === null) {
|
||||
viewRef.current = new EditorView({
|
||||
const view = useRef(
|
||||
new EditorView({
|
||||
state,
|
||||
})
|
||||
}
|
||||
).current
|
||||
|
||||
const view = viewRef.current
|
||||
|
||||
// Append the editor view dom to the container node when mounted
|
||||
// Append the editor view DOM to the container node when mounted
|
||||
const containerRef = useCallback(
|
||||
node => {
|
||||
if (node) {
|
||||
|
@ -81,12 +68,14 @@ const DocumentDiffViewer: FC<{ doc: string; highlights: Highlight[] }> = ({
|
|||
[view]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
view.dispatch({
|
||||
changes: { from: 0, to: view.state.doc.length, insert: doc },
|
||||
effects: setHighlightsEffect.of(highlights),
|
||||
})
|
||||
}, [doc, highlights, view])
|
||||
|
||||
return <div ref={containerRef} style={{ height: '100%' }} />
|
||||
return <div ref={containerRef} className="document-diff-container" />
|
||||
}
|
||||
|
||||
export default withErrorBoundary(DocumentDiffViewer, ErrorBoundaryFallback)
|
|
@ -0,0 +1,68 @@
|
|||
import { useHistoryContext } from '../../context/history-context'
|
||||
import { diffDoc } from '../../services/api'
|
||||
import { useEffect } from 'react'
|
||||
import { DocDiffResponse, Highlight } from '../../services/types/doc'
|
||||
import { highlightsFromDiffResponse } from '../../util/highlights-from-diff-response'
|
||||
import DocumentDiffViewer from './document-diff-viewer'
|
||||
import useAsync from '../../../../shared/hooks/use-async'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
type Diff = {
|
||||
binary: boolean
|
||||
docDiff?: {
|
||||
doc: string
|
||||
highlights: Highlight[]
|
||||
}
|
||||
}
|
||||
|
||||
function Main() {
|
||||
const { t } = useTranslation()
|
||||
const { projectId, updateSelection, fileSelection } = useHistoryContext()
|
||||
const { isLoading, runAsync, data } = useAsync<DocDiffResponse>()
|
||||
let diff: Diff | undefined
|
||||
if (data?.diff) {
|
||||
if ('binary' in data.diff) {
|
||||
diff = { binary: true }
|
||||
} else {
|
||||
diff = { binary: false, docDiff: highlightsFromDiffResponse(data.diff) }
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!updateSelection || !fileSelection || !fileSelection.pathname) {
|
||||
return
|
||||
}
|
||||
|
||||
const { fromV, toV } = updateSelection.update
|
||||
|
||||
// TODO: Error handling
|
||||
runAsync(diffDoc(projectId, fromV, toV, fileSelection.pathname))
|
||||
}, [fileSelection, projectId, runAsync, updateSelection])
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="history-loading-panel">
|
||||
<i className="fa fa-spin fa-refresh" />
|
||||
|
||||
{t('loading')}…
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (!diff) {
|
||||
return <div>No document</div>
|
||||
}
|
||||
|
||||
if (diff.binary) {
|
||||
return <div>Binary file</div>
|
||||
}
|
||||
|
||||
if (diff.docDiff) {
|
||||
const { doc, highlights } = diff.docDiff
|
||||
return <DocumentDiffViewer doc={doc} highlights={highlights} />
|
||||
}
|
||||
|
||||
return <div>No document</div>
|
||||
}
|
||||
|
||||
export default Main
|
|
@ -1,9 +0,0 @@
|
|||
import { useHistoryContext } from '../../context/history-context'
|
||||
|
||||
function Main() {
|
||||
const { fileSelection } = useHistoryContext()
|
||||
|
||||
return <div>Main (editor). File: {fileSelection?.pathname || 'not set'}</div>
|
||||
}
|
||||
|
||||
export default Main
|
|
@ -1,5 +1,5 @@
|
|||
import ChangeList from './change-list/change-list'
|
||||
import Editor from './editor/editor'
|
||||
import DiffView from './diff-view/diff-view'
|
||||
import { useLayoutContext } from '../../../shared/context/layout-context'
|
||||
import { useHistoryContext } from '../context/history-context'
|
||||
|
||||
|
@ -13,7 +13,7 @@ export default function HistoryRoot() {
|
|||
|
||||
return (
|
||||
<div className="history-react">
|
||||
<Editor />
|
||||
<DiffView />
|
||||
<ChangeList />
|
||||
</div>
|
||||
)
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { StateEffect, StateField } from '@codemirror/state'
|
||||
import { Decoration, EditorView, hoverTooltip, Tooltip } from '@codemirror/view'
|
||||
import { Highlight } from '../editor/document-diff-viewer'
|
||||
import { Highlight } from '../services/types/doc'
|
||||
|
||||
export const setHighlightsEffect = StateEffect.define<Highlight[]>()
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
import { getJSON } from '../../../infrastructure/fetch-json'
|
||||
import { FileDiff } from './types/file'
|
||||
import { Update } from './types/update'
|
||||
import { DocDiffResponse } from './types/doc'
|
||||
|
||||
const BATCH_SIZE = 10
|
||||
|
||||
|
@ -27,3 +28,19 @@ export function diffFiles(projectId: string, fromV: number, toV: number) {
|
|||
const diffUrl = `/project/${projectId}/filetree/diff?${queryParamsSerialized}`
|
||||
return getJSON<{ diff: FileDiff[] }>(diffUrl)
|
||||
}
|
||||
|
||||
export function diffDoc(
|
||||
projectId: string,
|
||||
fromV: number,
|
||||
toV: number,
|
||||
pathname: string
|
||||
) {
|
||||
const queryParams: Record<string, string> = {
|
||||
from: fromV.toString(),
|
||||
to: toV.toString(),
|
||||
pathname,
|
||||
}
|
||||
const queryParamsSerialized = new URLSearchParams(queryParams).toString()
|
||||
const diffUrl = `/project/${projectId}/diff?${queryParamsSerialized}`
|
||||
return getJSON<DocDiffResponse>(diffUrl)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
import { Meta } from './shared'
|
||||
|
||||
export interface DocDiffChunk {
|
||||
u?: string
|
||||
i?: string
|
||||
d?: string
|
||||
meta?: Meta
|
||||
}
|
||||
|
||||
export interface BinaryDiffResponse {
|
||||
binary: true
|
||||
}
|
||||
|
||||
export type DocDiffResponse = { diff: DocDiffChunk[] | BinaryDiffResponse }
|
||||
|
||||
interface Range {
|
||||
from: number
|
||||
to: number
|
||||
}
|
||||
|
||||
export interface Highlight {
|
||||
label: string
|
||||
hue: number
|
||||
range: Range
|
||||
type: 'addition' | 'deletion'
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
export interface User {
|
||||
first_name: string
|
||||
last_name: string
|
||||
email: string
|
||||
id: string
|
||||
}
|
||||
|
||||
export interface Meta {
|
||||
users: User[]
|
||||
start_ts: number
|
||||
end_ts: number
|
||||
}
|
|
@ -1,15 +1,4 @@
|
|||
interface User {
|
||||
first_name: string
|
||||
last_name: string
|
||||
email: string
|
||||
id: string
|
||||
}
|
||||
|
||||
interface UpdateMeta {
|
||||
users: User[]
|
||||
start_ts: number
|
||||
end_ts: number
|
||||
}
|
||||
import { Meta } from './shared'
|
||||
|
||||
interface UpdateLabel {
|
||||
id: string
|
||||
|
@ -33,7 +22,7 @@ interface ProjectOp {
|
|||
export interface Update {
|
||||
fromV: number
|
||||
toV: number
|
||||
meta: UpdateMeta
|
||||
meta: Meta
|
||||
labels?: Label[]
|
||||
pathnames: string[]
|
||||
project_ops: ProjectOp[]
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
import displayNameForUser from '../../../ide/history/util/displayNameForUser'
|
||||
import moment from 'moment/moment'
|
||||
import ColorManager from '../../../ide/colors/ColorManager'
|
||||
import { DocDiffChunk, Highlight } from '../services/types/doc'
|
||||
|
||||
export function highlightsFromDiffResponse(chunks: DocDiffChunk[]) {
|
||||
let pos = 0
|
||||
const highlights: Highlight[] = []
|
||||
let doc = ''
|
||||
|
||||
for (const entry of chunks) {
|
||||
const content = entry.u || entry.i || entry.d || ''
|
||||
doc += content
|
||||
const from = pos
|
||||
const to = doc.length
|
||||
pos = to
|
||||
const range = { from, to }
|
||||
|
||||
const isInsertion = typeof entry.i === 'string'
|
||||
const isDeletion = typeof entry.d === 'string'
|
||||
|
||||
if (isInsertion || isDeletion) {
|
||||
const meta = entry.meta
|
||||
if (!meta) {
|
||||
throw new Error('No meta found')
|
||||
}
|
||||
const user = meta.users?.[0]
|
||||
const name = displayNameForUser(user)
|
||||
const date = moment(meta.end_ts).format('Do MMM YYYY, h:mm a')
|
||||
if (isInsertion) {
|
||||
highlights.push({
|
||||
type: 'addition',
|
||||
// There doesn't seem to be a convenient way to make this translatable
|
||||
label: `Added by ${name} on ${date}`,
|
||||
range,
|
||||
hue: ColorManager.getHueForUserId(user.id),
|
||||
})
|
||||
} else if (isDeletion) {
|
||||
highlights.push({
|
||||
type: 'deletion',
|
||||
// There doesn't seem to be a convenient way to make this translatable
|
||||
label: `Deleted by ${name} on ${date}`,
|
||||
range,
|
||||
hue: ColorManager.getHueForUserId(user.id),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { doc, highlights }
|
||||
}
|
|
@ -1,8 +1,7 @@
|
|||
import { ScopeDecorator } from '../decorators/scope'
|
||||
import DocumentDiffViewer, {
|
||||
Highlight,
|
||||
} from '../../js/features/history/components/editor/document-diff-viewer'
|
||||
import DocumentDiffViewer from '../../js/features/history/components/diff-view/document-diff-viewer'
|
||||
import React from 'react'
|
||||
import { Highlight } from '../../js/features/history/services/types/doc'
|
||||
|
||||
export default {
|
||||
title: 'History / Document Diff Viewer',
|
||||
|
|
|
@ -34,6 +34,17 @@ history-root {
|
|||
|
||||
.doc-container {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
|
||||
.document-diff-container {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.cm-editor {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -54,4 +65,10 @@ history-root {
|
|||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.history-loading-panel {
|
||||
padding-top: 10rem;
|
||||
font-family: @font-family-serif;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue