mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-07 20:31:06 -05:00
Add CommentList to StringFileData (#16568)
* Add CommentList to StringFileData * added more types * use toRaw * using Map rather than array for comments * using Range class * Comment with ranges:Range[] * Revert "Comment with ranges:Range[]" This reverts commit 0783b1837562600637db03cc70c620129061c797. * Comment with ranges:Range[] * remove isDeleted * commentList.toRaw() * using toRaw * commentId to id * Revert "using toRaw" This reverts commit 0c04ca5836f3befd5ec027bad5bf722e8b27f36c. * fix merge * make comment map internal to CommentList * remove unused type * fix parameter name in StringFileData * import types more consistently * more consistent type def GitOrigin-RevId: 2be2225819d8e8ebcf90d08def280377205cb9ec
This commit is contained in:
parent
3de9efb1bb
commit
fc90db231c
10 changed files with 390 additions and 3 deletions
|
@ -12,6 +12,7 @@ const StringFileData = require('./file_data/string_file_data')
|
|||
* @typedef {import("./blob")} Blob
|
||||
* @typedef {import("./types").BlobStore} BlobStore
|
||||
* @typedef {import("./types").StringFileRawData} StringFileRawData
|
||||
* @typedef {import("./types").CommentRawData} CommentRawData
|
||||
* @typedef {import("./operation/text_operation")} TextOperation
|
||||
*/
|
||||
|
||||
|
@ -183,6 +184,15 @@ class File {
|
|||
this.data.edit(textOperation)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the comments for this file.
|
||||
*
|
||||
* @return {CommentRawData[]}
|
||||
*/
|
||||
getComments() {
|
||||
return this.data.getComments()
|
||||
}
|
||||
|
||||
/**
|
||||
* Clone a file.
|
||||
*
|
||||
|
|
36
libraries/overleaf-editor-core/lib/file_data/comment.js
Normal file
36
libraries/overleaf-editor-core/lib/file_data/comment.js
Normal file
|
@ -0,0 +1,36 @@
|
|||
const Range = require('./range')
|
||||
|
||||
/**
|
||||
* @typedef {import("../types").CommentRawData} CommentRawData
|
||||
*/
|
||||
|
||||
class Comment {
|
||||
/**
|
||||
* @param {Range[]} ranges
|
||||
* @param {boolean} [resolved]
|
||||
*/
|
||||
constructor(ranges, resolved = false) {
|
||||
this.ranges = ranges
|
||||
this.resolved = resolved
|
||||
}
|
||||
|
||||
toRaw() {
|
||||
return {
|
||||
resolved: this.resolved,
|
||||
ranges: this.ranges.map(range => range.toRaw()),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {CommentRawData} rawComment
|
||||
* @returns {Comment}
|
||||
*/
|
||||
static fromRaw(rawComment) {
|
||||
return new Comment(
|
||||
rawComment.ranges.map(range => Range.fromRaw(range)),
|
||||
rawComment.resolved
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Comment
|
68
libraries/overleaf-editor-core/lib/file_data/comment_list.js
Normal file
68
libraries/overleaf-editor-core/lib/file_data/comment_list.js
Normal file
|
@ -0,0 +1,68 @@
|
|||
const Comment = require('./comment')
|
||||
|
||||
/**
|
||||
* @typedef {import("../types").CommentRawData} CommentRawData
|
||||
*/
|
||||
|
||||
class CommentList {
|
||||
/**
|
||||
* @param {Map<string, Comment>} comments
|
||||
*/
|
||||
constructor(comments) {
|
||||
this.comments = comments
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {CommentRawData[]}
|
||||
*/
|
||||
getComments() {
|
||||
return Array.from(this.comments).map(([commentId, comment]) => {
|
||||
return {
|
||||
id: commentId,
|
||||
...comment.toRaw(),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} id
|
||||
* @returns {Comment | undefined}
|
||||
*/
|
||||
getComment(id) {
|
||||
return this.comments.get(id)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} id
|
||||
* @param {Comment} newComment
|
||||
*/
|
||||
add(id, newComment) {
|
||||
const existing = this.getComment(id)
|
||||
if (existing) {
|
||||
// todo: merge/split ranges
|
||||
existing.ranges = newComment.ranges
|
||||
} else {
|
||||
this.comments.set(id, newComment)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} id
|
||||
*/
|
||||
delete(id) {
|
||||
return this.comments.delete(id)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {CommentRawData[]} rawComments
|
||||
*/
|
||||
static fromRaw(rawComments) {
|
||||
const comments = new Map()
|
||||
for (const rawComment of rawComments) {
|
||||
comments.set(rawComment.id, Comment.fromRaw(rawComment))
|
||||
}
|
||||
return new CommentList(comments)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = CommentList
|
|
@ -15,6 +15,7 @@ let StringFileData = null
|
|||
|
||||
/**
|
||||
* @typedef {import("../types").BlobStore} BlobStore
|
||||
* @typedef {import("../types").CommentRawData} CommentRawData
|
||||
*/
|
||||
|
||||
/**
|
||||
|
@ -147,6 +148,16 @@ class FileData {
|
|||
async store(blobStore) {
|
||||
throw new Error('store not implemented for ' + JSON.stringify(this))
|
||||
}
|
||||
|
||||
/**
|
||||
* @see File#getComments
|
||||
* @function
|
||||
* @return {CommentRawData[]}
|
||||
* @abstract
|
||||
*/
|
||||
getComments() {
|
||||
throw new Error('getComments not implemented for ' + JSON.stringify(this))
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = FileData
|
||||
|
|
23
libraries/overleaf-editor-core/lib/file_data/range.js
Normal file
23
libraries/overleaf-editor-core/lib/file_data/range.js
Normal file
|
@ -0,0 +1,23 @@
|
|||
class Range {
|
||||
/**
|
||||
* @param {number} pos
|
||||
* @param {number} length
|
||||
*/
|
||||
constructor(pos, length) {
|
||||
this.pos = pos
|
||||
this.length = length
|
||||
}
|
||||
|
||||
toRaw() {
|
||||
return {
|
||||
pos: this.pos,
|
||||
length: this.length
|
||||
}
|
||||
}
|
||||
|
||||
static fromRaw(raw) {
|
||||
return new Range(raw.pos, raw.length)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Range
|
|
@ -3,23 +3,31 @@
|
|||
const assert = require('check-types').assert
|
||||
|
||||
const FileData = require('./')
|
||||
const CommentList = require('./comment_list')
|
||||
|
||||
/**
|
||||
* @typedef {import("../types").StringFileRawData} StringFileRawData
|
||||
* @typedef {import("../types").CommentRawData} CommentRawData
|
||||
*/
|
||||
|
||||
class StringFileData extends FileData {
|
||||
/**
|
||||
* @param {string} content
|
||||
* @param {CommentRawData[]} [rawComments]
|
||||
*/
|
||||
constructor(content) {
|
||||
constructor(content, rawComments = []) {
|
||||
super()
|
||||
assert.string(content)
|
||||
this.content = content
|
||||
this.comments = CommentList.fromRaw(rawComments)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {StringFileRawData} raw
|
||||
* @returns {StringFileData}
|
||||
*/
|
||||
static fromRaw(raw) {
|
||||
return new StringFileData(raw.content)
|
||||
return new StringFileData(raw.content, raw.comments || [])
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -27,7 +35,14 @@ class StringFileData extends FileData {
|
|||
* @returns {StringFileRawData}
|
||||
*/
|
||||
toRaw() {
|
||||
return { content: this.content }
|
||||
const raw = { content: this.content }
|
||||
|
||||
const comments = this.getComments()
|
||||
if (comments.length) {
|
||||
raw.comments = comments
|
||||
}
|
||||
|
||||
return raw
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
|
@ -55,6 +70,11 @@ class StringFileData extends FileData {
|
|||
this.content = textOperation.apply(this.content)
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
getComments() {
|
||||
return this.comments.getComments()
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
async toEager() {
|
||||
return this
|
||||
|
|
|
@ -5,8 +5,18 @@ export type BlobStore = {
|
|||
putString(content: string): Promise<Blob>
|
||||
}
|
||||
|
||||
export type CommentRawData = {
|
||||
id: string
|
||||
ranges: {
|
||||
pos: number
|
||||
length: number
|
||||
}[]
|
||||
resolved?: boolean
|
||||
}
|
||||
|
||||
export type StringFileRawData = {
|
||||
content: string
|
||||
comments?: CommentRawData[]
|
||||
}
|
||||
|
||||
export type RawV2DocVersions = Record<string, { pathname: string; v: number }>
|
||||
|
|
138
libraries/overleaf-editor-core/test/comments_list.test.js
Normal file
138
libraries/overleaf-editor-core/test/comments_list.test.js
Normal file
|
@ -0,0 +1,138 @@
|
|||
'use strict'
|
||||
|
||||
const { expect } = require('chai')
|
||||
const CommentList = require('../lib/file_data/comment_list')
|
||||
const Comment = require('../lib/file_data/comment')
|
||||
const Range = require('../lib/file_data/range')
|
||||
|
||||
describe('commentList', function () {
|
||||
it('checks if toRaw() returns a correct comment list', function () {
|
||||
const commentList = new CommentList(
|
||||
new Map([
|
||||
['comm1', new Comment([new Range(5, 10)])],
|
||||
['comm2', new Comment([new Range(20, 5)])],
|
||||
['comm3', new Comment([new Range(30, 15)])],
|
||||
])
|
||||
)
|
||||
|
||||
expect(commentList.getComments()).to.eql([
|
||||
{ id: 'comm1', ranges: [{ pos: 5, length: 10 }], resolved: false },
|
||||
{ id: 'comm2', ranges: [{ pos: 20, length: 5 }], resolved: false },
|
||||
{
|
||||
id: 'comm3',
|
||||
ranges: [{ pos: 30, length: 15 }],
|
||||
resolved: false,
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it('should get a comment by id', function () {
|
||||
const commentList = new CommentList(
|
||||
new Map([
|
||||
['comm1', new Comment([new Range(5, 10)])],
|
||||
['comm3', new Comment([new Range(30, 15)])],
|
||||
['comm2', new Comment([new Range(20, 5)])],
|
||||
])
|
||||
)
|
||||
|
||||
const comment = commentList.getComment('comm2')
|
||||
expect(comment?.toRaw()).to.eql({
|
||||
ranges: [
|
||||
{
|
||||
pos: 20,
|
||||
length: 5,
|
||||
},
|
||||
],
|
||||
resolved: false,
|
||||
})
|
||||
})
|
||||
|
||||
it('should add new comment to the list', function () {
|
||||
const commentList = new CommentList(
|
||||
new Map([
|
||||
['comm1', new Comment([new Range(5, 10)])],
|
||||
['comm2', new Comment([new Range(20, 5)])],
|
||||
['comm3', new Comment([new Range(30, 15)])],
|
||||
])
|
||||
)
|
||||
|
||||
commentList.add('comm4', new Comment([new Range(40, 10)]))
|
||||
expect(commentList.getComments()).to.eql([
|
||||
{ id: 'comm1', ranges: [{ pos: 5, length: 10 }], resolved: false },
|
||||
{ id: 'comm2', ranges: [{ pos: 20, length: 5 }], resolved: false },
|
||||
{
|
||||
id: 'comm3',
|
||||
ranges: [{ pos: 30, length: 15 }],
|
||||
resolved: false,
|
||||
},
|
||||
{
|
||||
id: 'comm4',
|
||||
ranges: [{ pos: 40, length: 10 }],
|
||||
resolved: false,
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it('should override existing if a new comment has the same id', function () {
|
||||
const commentList = new CommentList(
|
||||
new Map([
|
||||
['comm1', new Comment([new Range(5, 10)])],
|
||||
['comm2', new Comment([new Range(20, 5)])],
|
||||
['comm3', new Comment([new Range(30, 15)])],
|
||||
])
|
||||
)
|
||||
|
||||
commentList.add('comm2', new Comment([new Range(40, 10)]))
|
||||
expect(commentList.getComments()).to.eql([
|
||||
{ id: 'comm1', ranges: [{ pos: 5, length: 10 }], resolved: false },
|
||||
{
|
||||
id: 'comm2',
|
||||
ranges: [{ pos: 40, length: 10 }],
|
||||
resolved: false,
|
||||
},
|
||||
{
|
||||
id: 'comm3',
|
||||
ranges: [{ pos: 30, length: 15 }],
|
||||
resolved: false,
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it('should delete a comment from the list', function () {
|
||||
const commentList = new CommentList(
|
||||
new Map([
|
||||
['comm1', new Comment([new Range(5, 10)])],
|
||||
['comm2', new Comment([new Range(20, 5)])],
|
||||
['comm3', new Comment([new Range(30, 15)])],
|
||||
])
|
||||
)
|
||||
|
||||
commentList.delete('comm3')
|
||||
expect(commentList.getComments()).to.eql([
|
||||
{ id: 'comm1', ranges: [{ pos: 5, length: 10 }], resolved: false },
|
||||
{ id: 'comm2', ranges: [{ pos: 20, length: 5 }], resolved: false },
|
||||
])
|
||||
})
|
||||
|
||||
it('should not throw an error if comment id does not exist', function () {
|
||||
const commentList = new CommentList(
|
||||
new Map([
|
||||
['comm1', new Comment([new Range(5, 10)])],
|
||||
['comm2', new Comment([new Range(20, 5)])],
|
||||
['comm3', new Comment([new Range(30, 15)])],
|
||||
])
|
||||
)
|
||||
|
||||
commentList.delete('comm5')
|
||||
|
||||
expect(commentList.getComments()).to.eql([
|
||||
{ id: 'comm1', ranges: [{ pos: 5, length: 10 }], resolved: false },
|
||||
{ id: 'comm2', ranges: [{ pos: 20, length: 5 }], resolved: false },
|
||||
{
|
||||
id: 'comm3',
|
||||
ranges: [{ pos: 30, length: 15 }],
|
||||
resolved: false,
|
||||
},
|
||||
])
|
||||
})
|
||||
})
|
|
@ -88,4 +88,9 @@ describe('File', function () {
|
|||
expect(clone.getStringLength()).to.equal(0)
|
||||
})
|
||||
})
|
||||
|
||||
it('getComments() returns an empty comment list', function () {
|
||||
const file = File.fromString('foo')
|
||||
expect(file.getComments()).to.eql([])
|
||||
})
|
||||
})
|
||||
|
|
|
@ -34,4 +34,70 @@ describe('StringFileData', function () {
|
|||
expect(fileData.getByteLength()).to.equal(longString.length - 1)
|
||||
expect(fileData.getStringLength()).to.equal(longString.length - 1)
|
||||
})
|
||||
|
||||
it('getComments() should return an empty array', function () {
|
||||
const fileData = new StringFileData('test')
|
||||
expect(fileData.getComments()).to.eql([])
|
||||
})
|
||||
|
||||
it('creates StringFileData with comments', function () {
|
||||
const fileData = new StringFileData('test', [
|
||||
{
|
||||
id: 'comm1',
|
||||
ranges: [
|
||||
{
|
||||
pos: 5,
|
||||
length: 10,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'comm2',
|
||||
ranges: [
|
||||
{
|
||||
pos: 20,
|
||||
length: 5,
|
||||
},
|
||||
],
|
||||
},
|
||||
])
|
||||
|
||||
expect(fileData.getComments()).to.eql([
|
||||
{ id: 'comm1', ranges: [{ pos: 5, length: 10 }], resolved: false },
|
||||
{ id: 'comm2', ranges: [{ pos: 20, length: 5 }], resolved: false },
|
||||
])
|
||||
})
|
||||
|
||||
it('fromRaw() should create StringFileData with comments', function () {
|
||||
const fileData = StringFileData.fromRaw({
|
||||
content: 'test',
|
||||
comments: [
|
||||
{
|
||||
id: 'comm1',
|
||||
ranges: [
|
||||
{
|
||||
pos: 5,
|
||||
length: 10,
|
||||
},
|
||||
],
|
||||
resolved: false,
|
||||
},
|
||||
{
|
||||
id: 'comm2',
|
||||
ranges: [
|
||||
{
|
||||
pos: 20,
|
||||
length: 5,
|
||||
},
|
||||
],
|
||||
resolved: true,
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
expect(fileData.getComments()).to.eql([
|
||||
{ id: 'comm1', ranges: [{ pos: 5, length: 10 }], resolved: false },
|
||||
{ id: 'comm2', ranges: [{ pos: 20, length: 5 }], resolved: true },
|
||||
])
|
||||
})
|
||||
})
|
||||
|
|
Loading…
Reference in a new issue