mirror of
https://github.com/overleaf/overleaf.git
synced 2025-04-20 03:53:45 +00:00
Merge pull request #391 from sharelatex/ja-restore-files
Restore deletes files from their contents in project-history/OL history
This commit is contained in:
commit
f7a3231b42
26 changed files with 639 additions and 173 deletions
|
@ -180,7 +180,7 @@ clean_css:
|
|||
rm -f public/stylesheets/*.css*
|
||||
|
||||
clean_ci:
|
||||
docker-compose down
|
||||
docker-compose down -v
|
||||
|
||||
test: test_unit test_frontend test_acceptance
|
||||
|
||||
|
@ -221,7 +221,7 @@ test_acceptance_module: $(MODULE_MAKEFILES)
|
|||
fi
|
||||
|
||||
test_clean:
|
||||
docker-compose ${DOCKER_COMPOSE_FLAGS} down
|
||||
docker-compose ${DOCKER_COMPOSE_FLAGS} down -v
|
||||
|
||||
ci:
|
||||
MOCHA_ARGS="--reporter tap" \
|
||||
|
|
|
@ -57,26 +57,9 @@ module.exports = EditorHttpController =
|
|||
privilegeLevel
|
||||
)
|
||||
|
||||
restoreDoc: (req, res, next) ->
|
||||
project_id = req.params.Project_id
|
||||
doc_id = req.params.doc_id
|
||||
name = req.body.name
|
||||
|
||||
if !name?
|
||||
return res.sendStatus 400 # Malformed request
|
||||
|
||||
logger.log project_id: project_id, doc_id: doc_id, "restoring doc"
|
||||
ProjectEntityUpdateHandler.restoreDoc project_id, doc_id, name, (err, doc, folder_id) =>
|
||||
return next(error) if error?
|
||||
EditorRealTimeController.emitToRoom(project_id, 'reciveNewDoc', folder_id, doc)
|
||||
res.json {
|
||||
doc_id: doc._id
|
||||
}
|
||||
|
||||
_nameIsAcceptableLength: (name)->
|
||||
return name? and name.length < 150 and name.length != 0
|
||||
|
||||
|
||||
addDoc: (req, res, next) ->
|
||||
project_id = req.params.Project_id
|
||||
name = req.body.name
|
||||
|
|
|
@ -14,8 +14,6 @@ module.exports =
|
|||
webRouter.delete '/project/:Project_id/doc/:entity_id', AuthorizationMiddlewear.ensureUserCanWriteProjectContent, EditorHttpController.deleteDoc
|
||||
webRouter.delete '/project/:Project_id/folder/:entity_id', AuthorizationMiddlewear.ensureUserCanWriteProjectContent, EditorHttpController.deleteFolder
|
||||
|
||||
webRouter.post '/project/:Project_id/doc/:doc_id/restore', AuthorizationMiddlewear.ensureUserCanWriteProjectContent, EditorHttpController.restoreDoc
|
||||
|
||||
# Called by the real-time API to load up the current project state.
|
||||
# This is a post request because it's more than just a getting of data. We take actions
|
||||
# whenever a user joins a project, like updating the deleted status.
|
||||
|
|
|
@ -6,6 +6,7 @@ Errors = require "../Errors/Errors"
|
|||
HistoryManager = require "./HistoryManager"
|
||||
ProjectDetailsHandler = require "../Project/ProjectDetailsHandler"
|
||||
ProjectEntityUpdateHandler = require "../Project/ProjectEntityUpdateHandler"
|
||||
RestoreManager = require "./RestoreManager"
|
||||
|
||||
module.exports = HistoryController =
|
||||
selectHistoryApi: (req, res, next = (error) ->) ->
|
||||
|
@ -71,3 +72,29 @@ module.exports = HistoryController =
|
|||
return res.sendStatus(404) if error instanceof Errors.ProjectHistoryDisabledError
|
||||
return next(error) if error?
|
||||
res.sendStatus 204
|
||||
|
||||
restoreFileFromV2: (req, res, next) ->
|
||||
{project_id} = req.params
|
||||
{version, pathname} = req.body
|
||||
user_id = AuthenticationController.getLoggedInUserId req
|
||||
logger.log {project_id, version, pathname}, "restoring file from v2"
|
||||
RestoreManager.restoreFileFromV2 user_id, project_id, version, pathname, (error, entity) ->
|
||||
return next(error) if error?
|
||||
res.json {
|
||||
type: entity.type,
|
||||
id: entity._id
|
||||
}
|
||||
|
||||
restoreDocFromDeletedDoc: (req, res, next) ->
|
||||
{project_id, doc_id} = req.params
|
||||
{name} = req.body
|
||||
user_id = AuthenticationController.getLoggedInUserId(req)
|
||||
if !name?
|
||||
return res.sendStatus 400 # Malformed request
|
||||
logger.log {project_id, doc_id, user_id}, "restoring doc from v1 deleted doc"
|
||||
RestoreManager.restoreDocFromDeletedDoc user_id, project_id, doc_id, name, (err, doc) =>
|
||||
return next(error) if error?
|
||||
res.json {
|
||||
doc_id: doc._id
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,60 @@
|
|||
Settings = require 'settings-sharelatex'
|
||||
Path = require 'path'
|
||||
FileWriter = require '../../infrastructure/FileWriter'
|
||||
FileSystemImportManager = require '../Uploads/FileSystemImportManager'
|
||||
ProjectEntityHandler = require '../Project/ProjectEntityHandler'
|
||||
ProjectLocator = require '../Project/ProjectLocator'
|
||||
EditorController = require '../Editor/EditorController'
|
||||
Errors = require '../Errors/Errors'
|
||||
moment = require 'moment'
|
||||
|
||||
module.exports = RestoreManager =
|
||||
restoreDocFromDeletedDoc: (user_id, project_id, doc_id, name, callback = (error, doc, folder_id) ->) ->
|
||||
# This is the legacy method for restoring a doc from the SL track-changes/deletedDocs system.
|
||||
# It looks up the deleted doc's contents, and then creates a new doc with the same content.
|
||||
# We don't actually remove the deleted doc entry, just create a new one from its lines.
|
||||
ProjectEntityHandler.getDoc project_id, doc_id, include_deleted: true, (error, lines) ->
|
||||
return callback(error) if error?
|
||||
addDocWithName = (name, callback) ->
|
||||
EditorController.addDoc project_id, null, name, lines, 'restore', user_id, callback
|
||||
RestoreManager._addEntityWithUniqueName addDocWithName, name, callback
|
||||
|
||||
restoreFileFromV2: (user_id, project_id, version, pathname, callback = (error, entity) ->) ->
|
||||
RestoreManager._writeFileVersionToDisk project_id, version, pathname, (error, fsPath) ->
|
||||
return callback(error) if error?
|
||||
basename = Path.basename(pathname)
|
||||
dirname = Path.dirname(pathname)
|
||||
if dirname == '.' # no directory
|
||||
dirname = ''
|
||||
RestoreManager._findOrCreateFolder project_id, dirname, (error, parent_folder_id) ->
|
||||
return callback(error) if error?
|
||||
addEntityWithName = (name, callback) ->
|
||||
FileSystemImportManager.addEntity user_id, project_id, parent_folder_id, name, fsPath, false, callback
|
||||
RestoreManager._addEntityWithUniqueName addEntityWithName, basename, callback
|
||||
|
||||
_findOrCreateFolder: (project_id, dirname, callback = (error, folder_id) ->) ->
|
||||
EditorController.mkdirp project_id, dirname, (error, newFolders, lastFolder) ->
|
||||
return callback(error) if error?
|
||||
return callback(null, lastFolder?._id)
|
||||
|
||||
_addEntityWithUniqueName: (addEntityWithName, basename, callback = (error) ->) ->
|
||||
addEntityWithName basename, (error, entity) ->
|
||||
if error?
|
||||
if error instanceof Errors.InvalidNameError
|
||||
# likely a duplicate name, so try with a prefix
|
||||
date = moment(new Date()).format('Do MMM YY H:mm:ss')
|
||||
# Move extension to the end so the file type is preserved
|
||||
extension = Path.extname(basename)
|
||||
basename = Path.basename(basename, extension)
|
||||
basename = "#{basename} (Restored on #{date})"
|
||||
if extension != ''
|
||||
basename = "#{basename}#{extension}"
|
||||
addEntityWithName basename, callback
|
||||
else
|
||||
callback(error)
|
||||
else
|
||||
callback(null, entity)
|
||||
|
||||
_writeFileVersionToDisk: (project_id, version, pathname, callback = (error, fsPath) ->) ->
|
||||
url = "#{Settings.apis.project_history.url}/project/#{project_id}/version/#{version}/#{encodeURIComponent(pathname)}"
|
||||
FileWriter.writeUrlToDisk project_id, url, callback
|
|
@ -121,15 +121,6 @@ module.exports = ProjectEntityUpdateHandler = self =
|
|||
logger.log project_id: project_id, "removing root doc"
|
||||
Project.update {_id:project_id}, {$unset: {rootDoc_id: true}}, {}, callback
|
||||
|
||||
restoreDoc: (project_id, doc_id, name, callback = (error, doc, folder_id) ->) ->
|
||||
if not SafePath.isCleanFilename name
|
||||
return callback new Errors.InvalidNameError("invalid element name")
|
||||
# getDoc will return the deleted doc's lines, but we don't actually remove
|
||||
# the deleted doc, just create a new one from its lines.
|
||||
ProjectEntityHandler.getDoc project_id, doc_id, include_deleted: true, (error, lines) ->
|
||||
return callback(error) if error?
|
||||
self.addDoc project_id, null, name, lines, callback
|
||||
|
||||
addDoc: wrapWithLock (project_id, folder_id, docName, docLines, userId, callback = (error, doc, folder_id) ->)=>
|
||||
self.addDocWithoutUpdatingHistory.withoutLock project_id, folder_id, docName, docLines, userId, (error, doc, folder_id, path) ->
|
||||
return callback(error) if error?
|
||||
|
|
|
@ -3,21 +3,40 @@ logger = require 'logger-sharelatex'
|
|||
uuid = require 'uuid'
|
||||
_ = require 'underscore'
|
||||
Settings = require 'settings-sharelatex'
|
||||
request = require 'request'
|
||||
|
||||
module.exports =
|
||||
module.exports = FileWriter =
|
||||
writeStreamToDisk: (identifier, stream, callback = (error, fsPath) ->) ->
|
||||
callback = _.once(callback)
|
||||
fsPath = "#{Settings.path.dumpFolder}/#{identifier}_#{uuid.v4()}"
|
||||
|
||||
writeStream = fs.createWriteStream(fsPath)
|
||||
stream.pipe(writeStream)
|
||||
stream.pause()
|
||||
fs.mkdir Settings.path.dumpFolder, (error) ->
|
||||
stream.resume()
|
||||
if error? and error.code != 'EEXIST'
|
||||
# Ignore error about already existing
|
||||
return callback(error)
|
||||
|
||||
stream.on 'error', (err)->
|
||||
logger.err {err, identifier, fsPath}, "[writeStreamToDisk] something went wrong with incoming stream"
|
||||
callback(err)
|
||||
writeStream.on 'error', (err)->
|
||||
logger.err {err, identifier, fsPath}, "[writeStreamToDisk] something went wrong with writing to disk"
|
||||
callback(err)
|
||||
writeStream.on "finish", ->
|
||||
logger.log {identifier, fsPath}, "[writeStreamToDisk] write stream finished"
|
||||
callback null, fsPath
|
||||
writeStream = fs.createWriteStream(fsPath)
|
||||
stream.pipe(writeStream)
|
||||
|
||||
stream.on 'error', (err)->
|
||||
logger.err {err, identifier, fsPath}, "[writeStreamToDisk] something went wrong with incoming stream"
|
||||
callback(err)
|
||||
writeStream.on 'error', (err)->
|
||||
logger.err {err, identifier, fsPath}, "[writeStreamToDisk] something went wrong with writing to disk"
|
||||
callback(err)
|
||||
writeStream.on "finish", ->
|
||||
logger.log {identifier, fsPath}, "[writeStreamToDisk] write stream finished"
|
||||
callback null, fsPath
|
||||
|
||||
writeUrlToDisk: (identifier, url, callback = (error, fsPath) ->) ->
|
||||
callback = _.once(callback)
|
||||
stream = request.get(url)
|
||||
stream.on 'response', (response) ->
|
||||
if 200 <= response.statusCode < 300
|
||||
FileWriter.writeStreamToDisk identifier, stream, callback
|
||||
else
|
||||
err = new Error("bad response from url: #{response.statusCode}")
|
||||
logger.err {err, identifier, url}, err.message
|
||||
callback(err)
|
|
@ -201,8 +201,11 @@ module.exports = class Router
|
|||
webRouter.get "/project/:Project_id/doc/:doc_id/diff", AuthorizationMiddlewear.ensureUserCanReadProject, HistoryController.selectHistoryApi, HistoryController.proxyToHistoryApi
|
||||
webRouter.get "/project/:Project_id/diff", AuthorizationMiddlewear.ensureUserCanReadProject, HistoryController.selectHistoryApi, HistoryController.proxyToHistoryApiAndInjectUserDetails
|
||||
webRouter.post "/project/:Project_id/doc/:doc_id/version/:version_id/restore", AuthorizationMiddlewear.ensureUserCanReadProject, HistoryController.selectHistoryApi, HistoryController.proxyToHistoryApi
|
||||
webRouter.post '/project/:project_id/doc/:doc_id/restore', AuthorizationMiddlewear.ensureUserCanWriteProjectContent, HistoryController.restoreDocFromDeletedDoc
|
||||
webRouter.post "/project/:project_id/restore_file", AuthorizationMiddlewear.ensureUserCanWriteProjectContent, HistoryController.restoreFileFromV2
|
||||
privateApiRouter.post "/project/:Project_id/history/resync", AuthenticationController.httpAuth, HistoryController.resyncProjectHistory
|
||||
|
||||
|
||||
webRouter.get '/Project/:Project_id/download/zip', AuthorizationMiddlewear.ensureUserCanReadProject, ProjectDownloadsController.downloadProject
|
||||
webRouter.get '/project/download/zip', AuthorizationMiddlewear.ensureUserCanReadMultipleProjects, ProjectDownloadsController.downloadMultipleProjects
|
||||
|
||||
|
|
|
@ -11,7 +11,7 @@ aside.file-tree.file-tree-history(ng-controller="FileTreeController", ng-class="
|
|||
.entity
|
||||
.entity-name.entity-name-history(
|
||||
ng-click="history.selection.pathname = pathname",
|
||||
ng-class="{ 'deleted': doc.deleted }"
|
||||
ng-class="{ 'deleted': !!doc.deletedAtV }"
|
||||
)
|
||||
i.fa.fa-fw.fa-pencil
|
||||
span {{ pathname }}
|
||||
|
|
|
@ -122,74 +122,8 @@ div#history(ng-show="ui.view == 'history'")
|
|||
i.fa.fa-spin.fa-refresh
|
||||
| #{translate("loading")}...
|
||||
|
||||
.diff-panel.full-size(ng-controller="HistoryDiffController")
|
||||
.diff(
|
||||
ng-if="!!history.diff && !history.diff.loading && !history.diff.deleted && !history.diff.error && !history.diff.binary"
|
||||
)
|
||||
.toolbar.toolbar-alt
|
||||
span.name
|
||||
| <strong>{{history.diff.highlights.length}} </strong>
|
||||
ng-pluralize(
|
||||
count="history.diff.highlights.length",
|
||||
when="{\
|
||||
'one': 'change',\
|
||||
'other': 'changes'\
|
||||
}"
|
||||
)
|
||||
| in <strong>{{history.diff.pathname}}</strong>
|
||||
.toolbar-right
|
||||
a.btn.btn-danger.btn-sm(
|
||||
href,
|
||||
ng-if="!history.isV2"
|
||||
ng-click="openRestoreDiffModal()"
|
||||
) #{translate("restore_to_before_these_changes")}
|
||||
.deleted-warning(
|
||||
ng-show="history.selection.docs[history.selection.pathname].deleted"
|
||||
) This file was deleted
|
||||
.diff-editor.hide-ace-cursor(
|
||||
ace-editor="history",
|
||||
theme="settings.theme",
|
||||
font-size="settings.fontSize",
|
||||
text="history.diff.text",
|
||||
highlights="history.diff.highlights",
|
||||
read-only="true",
|
||||
resize-on="layout:main:resize",
|
||||
navigate-highlights="true"
|
||||
)
|
||||
|
||||
.diff.diff-binary(ng-show="history.diff.binary")
|
||||
.toolbar.toolbar-alt
|
||||
span.name
|
||||
strong {{history.diff.pathname}}
|
||||
.alert.alert-info We're still working on showing image and binary changes, sorry. Stay tuned!
|
||||
|
||||
.diff-deleted.text-centered(
|
||||
ng-show="history.diff.deleted && !history.diff.restoreDeletedSuccess"
|
||||
)
|
||||
p.text-serif #{translate("file_has_been_deleted", {filename:"{{ history.diff.doc.name }} "})}
|
||||
p
|
||||
a.btn.btn-primary.btn-lg(
|
||||
href,
|
||||
ng-click="restoreDeletedDoc()",
|
||||
ng-disabled="history.diff.restoreInProgress"
|
||||
) #{translate("restore")}
|
||||
|
||||
.diff-deleted.text-centered(
|
||||
ng-show="history.diff.deleted && history.diff.restoreDeletedSuccess"
|
||||
)
|
||||
p.text-serif #{translate("file_restored", {filename:"{{ history.diff.doc.name }} "})}
|
||||
p.text-serif #{translate("file_restored_back_to_editor")}
|
||||
p
|
||||
a.btn.btn-default(
|
||||
href,
|
||||
ng-click="backToEditorAfterRestore()",
|
||||
) #{translate("file_restored_back_to_editor_btn")}
|
||||
|
||||
.loading-panel(ng-show="history.diff.loading")
|
||||
i.fa.fa-spin.fa-refresh
|
||||
| #{translate("loading")}...
|
||||
.error-panel(ng-show="history.diff.error")
|
||||
.alert.alert-danger #{translate("generic_something_went_wrong")}
|
||||
include ./history/diffPanelV1
|
||||
include ./history/diffPanelV2
|
||||
|
||||
script(type="text/ng-template", id="historyRestoreDiffModalTemplate")
|
||||
.modal-header
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
.diff-panel.full-size(ng-if="!history.isV2", ng-controller="HistoryDiffController")
|
||||
.diff(
|
||||
ng-if="!!history.diff && !history.diff.loading && !history.diff.deleted && !history.diff.error && !history.diff.binary"
|
||||
)
|
||||
.toolbar.toolbar-alt
|
||||
span.name
|
||||
| <strong>{{history.diff.highlights.length}} </strong>
|
||||
ng-pluralize(
|
||||
count="history.diff.highlights.length",
|
||||
when="{\
|
||||
'one': 'change',\
|
||||
'other': 'changes'\
|
||||
}"
|
||||
)
|
||||
| in <strong>{{history.diff.pathname}}</strong>
|
||||
.toolbar-right
|
||||
a.btn.btn-danger.btn-sm(
|
||||
href,
|
||||
ng-click="openRestoreDiffModal()"
|
||||
) #{translate("restore_to_before_these_changes")}
|
||||
.diff-editor.hide-ace-cursor(
|
||||
ace-editor="history",
|
||||
theme="settings.theme",
|
||||
font-size="settings.fontSize",
|
||||
text="history.diff.text",
|
||||
highlights="history.diff.highlights",
|
||||
read-only="true",
|
||||
resize-on="layout:main:resize",
|
||||
navigate-highlights="true"
|
||||
)
|
||||
|
||||
.diff-deleted.text-centered(
|
||||
ng-show="history.diff.deleted && !history.diff.restoreDeletedSuccess"
|
||||
)
|
||||
p.text-serif #{translate("file_has_been_deleted", {filename:"{{ history.diff.doc.name }} "})}
|
||||
p
|
||||
a.btn.btn-primary.btn-lg(
|
||||
href,
|
||||
ng-click="restoreDeletedDoc()",
|
||||
ng-disabled="history.diff.restoreInProgress"
|
||||
) #{translate("restore")}
|
||||
|
||||
.diff-deleted.text-centered(
|
||||
ng-show="history.diff.deleted && history.diff.restoreDeletedSuccess"
|
||||
)
|
||||
p.text-serif #{translate("file_restored", {filename:"{{ history.diff.doc.name }} "})}
|
||||
p.text-serif #{translate("file_restored_back_to_editor")}
|
||||
p
|
||||
a.btn.btn-default(
|
||||
href,
|
||||
ng-click="backToEditorAfterRestore()",
|
||||
) #{translate("file_restored_back_to_editor_btn")}
|
||||
|
||||
.loading-panel(ng-show="history.diff.loading")
|
||||
i.fa.fa-spin.fa-refresh
|
||||
| #{translate("loading")}...
|
||||
.error-panel(ng-show="history.diff.error")
|
||||
.alert.alert-danger #{translate("generic_something_went_wrong")}
|
|
@ -0,0 +1,50 @@
|
|||
.diff-panel.full-size(ng-if="history.isV2", ng-controller="HistoryV2DiffController")
|
||||
.diff(
|
||||
ng-if="!!history.diff && !history.diff.loading && !history.diff.error",
|
||||
ng-class="{ 'diff-binary': history.diff.binary }"
|
||||
)
|
||||
.toolbar.toolbar-alt
|
||||
span.name(ng-if="history.diff.binary")
|
||||
strong {{history.diff.pathname}}
|
||||
span.name(ng-if="!history.diff.binary")
|
||||
| <strong>{{history.diff.highlights.length}} </strong>
|
||||
ng-pluralize(
|
||||
count="history.diff.highlights.length",
|
||||
when="{\
|
||||
'one': 'change',\
|
||||
'other': 'changes'\
|
||||
}"
|
||||
)
|
||||
| in <strong>{{history.diff.pathname}}</strong>
|
||||
.toolbar-right(ng-if="history.selection.docs[history.selection.pathname].deletedAtV")
|
||||
button.btn.btn-danger.btn-sm(
|
||||
ng-click="restoreDeletedFile()"
|
||||
ng-show="!restoreState.error"
|
||||
ng-disabled="restoreState.inflight"
|
||||
)
|
||||
i.fa.fa-fw.fa-step-backward
|
||||
span(ng-show="!restoreState.inflight")
|
||||
| Restore this deleted file
|
||||
span(ng-show="restoreState.inflight")
|
||||
| Restoring...
|
||||
span.text-danger(ng-show="restoreState.error")
|
||||
| Error restoring, sorry
|
||||
.diff-editor.hide-ace-cursor(
|
||||
ng-if="!history.diff.binary"
|
||||
ace-editor="history",
|
||||
theme="settings.theme",
|
||||
font-size="settings.fontSize",
|
||||
text="history.diff.text",
|
||||
highlights="history.diff.highlights",
|
||||
read-only="true",
|
||||
resize-on="layout:main:resize",
|
||||
navigate-highlights="true"
|
||||
)
|
||||
.alert.alert-info(ng-if="history.diff.binary")
|
||||
| We're still working on showing image and binary changes, sorry. Stay tuned!
|
||||
|
||||
.loading-panel(ng-show="history.diff.loading")
|
||||
i.fa.fa-spin.fa-refresh
|
||||
| #{translate("loading")}...
|
||||
.error-panel(ng-show="history.diff.error")
|
||||
.alert.alert-danger #{translate("generic_something_went_wrong")}
|
|
@ -5,7 +5,7 @@
|
|||
],
|
||||
"verbose": true,
|
||||
"legacyWatch": true,
|
||||
"exec": "make compile",
|
||||
"exec": "make compile || exit 1",
|
||||
"watch": [
|
||||
"public/coffee/",
|
||||
"public/stylesheets/"
|
||||
|
|
|
@ -8,6 +8,7 @@ define [
|
|||
@openFile(entity)
|
||||
|
||||
openFile: (file) ->
|
||||
@ide.fileTreeManager.selectEntity(file)
|
||||
@$scope.ui.view = "file"
|
||||
@$scope.openFile = null
|
||||
@$scope.$apply()
|
||||
|
|
|
@ -4,6 +4,7 @@ define [
|
|||
"ide/history/util/displayNameForUser"
|
||||
"ide/history/controllers/HistoryListController"
|
||||
"ide/history/controllers/HistoryDiffController"
|
||||
"ide/history/controllers/HistoryV2DiffController"
|
||||
"ide/history/directives/infiniteScroll"
|
||||
], (moment, ColorManager, displayNameForUser) ->
|
||||
class HistoryManager
|
||||
|
|
|
@ -49,6 +49,13 @@ define [
|
|||
diff: null
|
||||
}
|
||||
|
||||
restoreFile: (version, pathname) ->
|
||||
url = "/project/#{@$scope.project_id}/restore_file"
|
||||
@ide.$http.post(url, {
|
||||
version, pathname,
|
||||
_csrf: window.csrfToken
|
||||
})
|
||||
|
||||
MAX_RECENT_UPDATES_TO_SELECT: 5
|
||||
autoSelectRecentUpdates: () ->
|
||||
return if @$scope.history.updates.length == 0
|
||||
|
@ -204,7 +211,7 @@ define [
|
|||
# Map of original pathname -> doc summary
|
||||
docs_summary = Object.create(null)
|
||||
|
||||
updatePathnameWithUpdateVersions = (pathname, update, deleted) ->
|
||||
updatePathnameWithUpdateVersions = (pathname, update, deletedAtV) ->
|
||||
# docs_summary is indexed by the original pathname the doc
|
||||
# had at the start, so we have to look this up from the current
|
||||
# pathname via original_pathname first
|
||||
|
@ -222,8 +229,8 @@ define [
|
|||
doc_summary.toV,
|
||||
update.toV
|
||||
)
|
||||
if deleted?
|
||||
doc_summary.deleted = true
|
||||
if deletedAtV?
|
||||
doc_summary.deletedAtV = deletedAtV
|
||||
|
||||
# Put updates in ascending chronological order
|
||||
updates = updates.slice().reverse()
|
||||
|
@ -241,7 +248,7 @@ define [
|
|||
updatePathnameWithUpdateVersions(add.pathname, update)
|
||||
if project_op.remove?
|
||||
remove = project_op.remove
|
||||
updatePathnameWithUpdateVersions(remove.pathname, update, true)
|
||||
updatePathnameWithUpdateVersions(remove.pathname, update, project_op.atV)
|
||||
|
||||
return docs_summary
|
||||
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
define [
|
||||
"base"
|
||||
], (App) ->
|
||||
App.controller "HistoryV2DiffController", ($scope, ide, event_tracking) ->
|
||||
$scope.restoreState =
|
||||
inflight: false
|
||||
error: false
|
||||
|
||||
$scope.restoreDeletedFile = () ->
|
||||
pathname = $scope.history.selection.pathname
|
||||
return if !pathname?
|
||||
version = $scope.history.selection.docs[pathname]?.deletedAtV
|
||||
return if !version?
|
||||
event_tracking.sendMB "history-v2-restore-deleted"
|
||||
$scope.restoreState.inflight = true
|
||||
ide.historyManager
|
||||
.restoreFile(version, pathname)
|
||||
.then (response) ->
|
||||
{ data } = response
|
||||
openEntity(data)
|
||||
.catch () ->
|
||||
ide.showGenericMessageModal('Sorry, something went wrong with the restore')
|
||||
.finally () ->
|
||||
$scope.restoreState.inflight = false
|
||||
|
||||
openEntity = (data) ->
|
||||
iterations = 0
|
||||
{id, type} = data
|
||||
do tryOpen = () ->
|
||||
if iterations > 5
|
||||
return
|
||||
entity = ide.fileTreeManager.findEntityById(id)
|
||||
if entity? and type == 'doc'
|
||||
ide.editorManager.openDoc(entity)
|
||||
if entity? and type == 'file'
|
||||
ide.binaryFilesManager.openFile(entity)
|
||||
else
|
||||
setTimeout(tryOpen, 500)
|
||||
|
195
services/web/test/acceptance/coffee/RestoringFilesTest.coffee
Normal file
195
services/web/test/acceptance/coffee/RestoringFilesTest.coffee
Normal file
|
@ -0,0 +1,195 @@
|
|||
async = require "async"
|
||||
expect = require("chai").expect
|
||||
_ = require 'underscore'
|
||||
|
||||
ProjectGetter = require "../../../app/js/Features/Project/ProjectGetter.js"
|
||||
|
||||
User = require "./helpers/User"
|
||||
MockProjectHistoryApi = require "./helpers/MockProjectHistoryApi"
|
||||
MockDocstoreApi = require "./helpers/MockDocstoreApi"
|
||||
MockFileStoreApi = require "./helpers/MockFileStoreApi"
|
||||
|
||||
describe "RestoringFiles", ->
|
||||
before (done) ->
|
||||
@owner = new User()
|
||||
@owner.login (error) =>
|
||||
throw error if error?
|
||||
@owner.createProject "example-project", {template: "example"}, (error, @project_id) =>
|
||||
throw error if error?
|
||||
done()
|
||||
|
||||
describe "restoring a deleted doc", ->
|
||||
beforeEach (done) ->
|
||||
@owner.getProject @project_id, (error, project) =>
|
||||
throw error if error?
|
||||
@doc = _.find project.rootFolder[0].docs, (doc) ->
|
||||
doc.name == 'main.tex'
|
||||
@owner.request {
|
||||
method: "DELETE",
|
||||
url: "/project/#{@project_id}/doc/#{@doc._id}",
|
||||
}, (error, response, body) =>
|
||||
throw error if error?
|
||||
expect(response.statusCode).to.equal 204
|
||||
@owner.request {
|
||||
method: "POST",
|
||||
url: "/project/#{@project_id}/doc/#{@doc._id}/restore"
|
||||
json:
|
||||
name: "main.tex"
|
||||
}, (error, response, body) =>
|
||||
throw error if error?
|
||||
expect(response.statusCode).to.equal 200
|
||||
expect(body.doc_id).to.exist
|
||||
@restored_doc_id = body.doc_id
|
||||
done()
|
||||
|
||||
it 'should have restored the doc', (done) ->
|
||||
@owner.getProject @project_id, (error, project) =>
|
||||
throw error if error?
|
||||
restored_doc = _.find project.rootFolder[0].docs, (doc) ->
|
||||
doc.name == 'main.tex'
|
||||
expect(restored_doc._id.toString()).to.equal @restored_doc_id
|
||||
expect(@doc._id).to.not.equal @restored_doc_id
|
||||
# console.log @doc_id, @restored_doc_id, MockDocstoreApi.docs[@project_id]
|
||||
expect(MockDocstoreApi.docs[@project_id][@restored_doc_id].lines).to.deep.equal(
|
||||
MockDocstoreApi.docs[@project_id][@doc._id].lines
|
||||
)
|
||||
done()
|
||||
|
||||
describe "restoring from v2 history", ->
|
||||
describe "restoring a text file", ->
|
||||
beforeEach (done) ->
|
||||
MockProjectHistoryApi.addOldFile(@project_id, 42, "foo.tex", "hello world, this is foo.tex!")
|
||||
@owner.request {
|
||||
method: "POST",
|
||||
url: "/project/#{@project_id}/restore_file",
|
||||
json:
|
||||
pathname: "foo.tex"
|
||||
version: 42
|
||||
}, (error, response, body) ->
|
||||
throw error if error?
|
||||
expect(response.statusCode).to.equal 200
|
||||
done()
|
||||
|
||||
it "should have created a doc", (done) ->
|
||||
@owner.getProject @project_id, (error, project) =>
|
||||
throw error if error?
|
||||
doc = _.find project.rootFolder[0].docs, (doc) ->
|
||||
doc.name == 'foo.tex'
|
||||
doc = MockDocstoreApi.docs[@project_id][doc._id]
|
||||
expect(doc.lines).to.deep.equal [
|
||||
"hello world, this is foo.tex!"
|
||||
]
|
||||
done()
|
||||
|
||||
describe "restoring a binary file", ->
|
||||
beforeEach (done) ->
|
||||
MockProjectHistoryApi.addOldFile(@project_id, 42, "image.png", "Mock image.png content")
|
||||
@owner.request {
|
||||
method: "POST",
|
||||
url: "/project/#{@project_id}/restore_file",
|
||||
json:
|
||||
pathname: "image.png"
|
||||
version: 42
|
||||
}, (error, response, body) ->
|
||||
throw error if error?
|
||||
expect(response.statusCode).to.equal 200
|
||||
done()
|
||||
|
||||
it "should have created a file", (done) ->
|
||||
@owner.getProject @project_id, (error, project) =>
|
||||
throw error if error?
|
||||
file = _.find project.rootFolder[0].fileRefs, (file) ->
|
||||
file.name == 'image.png'
|
||||
file = MockFileStoreApi.files[@project_id][file._id]
|
||||
expect(file.content).to.equal "Mock image.png content"
|
||||
done()
|
||||
|
||||
describe "restoring to a directory that exists", ->
|
||||
beforeEach (done) ->
|
||||
MockProjectHistoryApi.addOldFile(@project_id, 42, "foldername/foo2.tex", "hello world, this is foo-2.tex!")
|
||||
@owner.request.post {
|
||||
uri: "project/#{@project_id}/folder",
|
||||
json:
|
||||
name: 'foldername'
|
||||
}, (error, response, body) =>
|
||||
throw error if error?
|
||||
expect(response.statusCode).to.equal 200
|
||||
@owner.request {
|
||||
method: "POST",
|
||||
url: "/project/#{@project_id}/restore_file",
|
||||
json:
|
||||
pathname: "foldername/foo2.tex"
|
||||
version: 42
|
||||
}, (error, response, body) ->
|
||||
throw error if error?
|
||||
expect(response.statusCode).to.equal 200
|
||||
done()
|
||||
|
||||
it "should have created the doc in the named folder", (done) ->
|
||||
@owner.getProject @project_id, (error, project) =>
|
||||
throw error if error?
|
||||
folder = _.find project.rootFolder[0].folders, (folder) ->
|
||||
folder.name == 'foldername'
|
||||
doc = _.find folder.docs, (doc) ->
|
||||
doc.name == 'foo2.tex'
|
||||
doc = MockDocstoreApi.docs[@project_id][doc._id]
|
||||
expect(doc.lines).to.deep.equal [
|
||||
"hello world, this is foo-2.tex!"
|
||||
]
|
||||
done()
|
||||
|
||||
describe "restoring to a directory that no longer exists", ->
|
||||
beforeEach (done) ->
|
||||
MockProjectHistoryApi.addOldFile(@project_id, 42, "nothere/foo3.tex", "hello world, this is foo-3.tex!")
|
||||
@owner.request {
|
||||
method: "POST",
|
||||
url: "/project/#{@project_id}/restore_file",
|
||||
json:
|
||||
pathname: "nothere/foo3.tex"
|
||||
version: 42
|
||||
}, (error, response, body) ->
|
||||
throw error if error?
|
||||
expect(response.statusCode).to.equal 200
|
||||
done()
|
||||
|
||||
it "should have created the folder and restored the doc to it", (done) ->
|
||||
@owner.getProject @project_id, (error, project) =>
|
||||
throw error if error?
|
||||
folder = _.find project.rootFolder[0].folders, (folder) ->
|
||||
folder.name == 'nothere'
|
||||
expect(folder).to.exist
|
||||
doc = _.find folder.docs, (doc) ->
|
||||
doc.name == 'foo3.tex'
|
||||
doc = MockDocstoreApi.docs[@project_id][doc._id]
|
||||
expect(doc.lines).to.deep.equal [
|
||||
"hello world, this is foo-3.tex!"
|
||||
]
|
||||
done()
|
||||
|
||||
describe "restoring to a filename that already exists", ->
|
||||
it "should have created the file with a timestamp appended", ->
|
||||
beforeEach (done) ->
|
||||
MockProjectHistoryApi.addOldFile(@project_id, 42, "main.tex", "hello world, this is main.tex!")
|
||||
@owner.request {
|
||||
method: "POST",
|
||||
url: "/project/#{@project_id}/restore_file",
|
||||
json:
|
||||
pathname: "main.tex"
|
||||
version: 42
|
||||
}, (error, response, body) ->
|
||||
throw error if error?
|
||||
expect(response.statusCode).to.equal 200
|
||||
done()
|
||||
|
||||
it "should have created the doc in the root folder", (done) ->
|
||||
@owner.getProject @project_id, (error, project) =>
|
||||
throw error if error?
|
||||
doc = _.find project.rootFolder[0].docs, (doc) ->
|
||||
doc.name.match(/main \(Restored on/)
|
||||
expect(doc).to.exist
|
||||
doc = MockDocstoreApi.docs[@project_id][doc._id]
|
||||
expect(doc.lines).to.deep.equal [
|
||||
"hello world, this is main.tex!"
|
||||
]
|
||||
done()
|
||||
|
|
@ -36,7 +36,7 @@ module.exports = MockDocUpdaterApi =
|
|||
res.sendStatus 200
|
||||
|
||||
app.delete "/project/:project_id/doc/:doc_id", (req, res, next) =>
|
||||
res.send 204
|
||||
res.sendStatus 204
|
||||
|
||||
app.listen 3003, (error) ->
|
||||
throw error if error?
|
||||
|
|
|
@ -23,15 +23,23 @@ module.exports = MockDocStoreApi =
|
|||
docs = (doc for doc_id, doc of @docs[req.params.project_id])
|
||||
res.send JSON.stringify docs
|
||||
|
||||
app.get "/project/:project_id/doc/:doc_id", (req, res, next) =>
|
||||
{project_id, doc_id} = req.params
|
||||
doc = @docs[project_id][doc_id]
|
||||
if doc.deleted and !req.query.include_deleted
|
||||
res.sendStatus 404
|
||||
else
|
||||
res.send JSON.stringify doc
|
||||
|
||||
app.delete "/project/:project_id/doc/:doc_id", (req, res, next) =>
|
||||
{project_id, doc_id} = req.params
|
||||
if !@docs[project_id]?
|
||||
res.send 404
|
||||
res.sendStatus 404
|
||||
else if !@docs[project_id][doc_id]?
|
||||
res.send 404
|
||||
res.sendStatus 404
|
||||
else
|
||||
@docs[project_id][doc_id] = undefined
|
||||
res.send 204
|
||||
@docs[project_id][doc_id].deleted = true
|
||||
res.sendStatus 204
|
||||
|
||||
app.listen 3016, (error) ->
|
||||
throw error if error?
|
||||
|
|
|
@ -4,10 +4,23 @@ app = express()
|
|||
module.exports = MockProjectHistoryApi =
|
||||
docs: {}
|
||||
|
||||
oldFiles: {}
|
||||
|
||||
addOldFile: (project_id, version, pathname, content) ->
|
||||
@oldFiles["#{project_id}:#{version}:#{pathname}"] = content
|
||||
|
||||
run: () ->
|
||||
app.post "/project", (req, res, next) =>
|
||||
res.json project: id: 1
|
||||
|
||||
app.get "/project/:project_id/version/:version/:pathname", (req, res, next) =>
|
||||
{project_id, version, pathname} = req.params
|
||||
key = "#{project_id}:#{version}:#{pathname}"
|
||||
if @oldFiles[key]?
|
||||
res.send @oldFiles[key]
|
||||
else
|
||||
res.send 404
|
||||
|
||||
app.listen 3054, (error) ->
|
||||
throw error if error?
|
||||
.on "error", (error) ->
|
||||
|
|
|
@ -164,35 +164,6 @@ describe "EditorHttpController", ->
|
|||
it "should return false in the callback", ->
|
||||
@callback.calledWith(null, null, false).should.equal true
|
||||
|
||||
describe "restoreDoc", ->
|
||||
beforeEach ->
|
||||
@req.params =
|
||||
Project_id: @project_id
|
||||
doc_id: @doc_id
|
||||
@req.body =
|
||||
name: @name = "doc-name"
|
||||
@ProjectEntityUpdateHandler.restoreDoc = sinon.stub().callsArgWith(3, null,
|
||||
@doc = { "mock": "doc", _id: @new_doc_id = "new-doc-id" }
|
||||
@folder_id = "mock-folder-id"
|
||||
)
|
||||
@EditorRealTimeController.emitToRoom = sinon.stub()
|
||||
@EditorHttpController.restoreDoc @req, @res
|
||||
|
||||
it "should restore the doc", ->
|
||||
@ProjectEntityUpdateHandler.restoreDoc
|
||||
.calledWith(@project_id, @doc_id, @name)
|
||||
.should.equal true
|
||||
|
||||
it "should the real-time clients about the new doc", ->
|
||||
@EditorRealTimeController.emitToRoom
|
||||
.calledWith(@project_id, 'reciveNewDoc', @folder_id, @doc)
|
||||
.should.equal true
|
||||
|
||||
it "should return the new doc id", ->
|
||||
@res.json
|
||||
.calledWith(doc_id: @new_doc_id)
|
||||
.should.equal true
|
||||
|
||||
describe "addDoc", ->
|
||||
beforeEach ->
|
||||
@doc = { "mock": "doc" }
|
||||
|
|
|
@ -22,6 +22,7 @@ describe "HistoryController", ->
|
|||
"./HistoryManager": @HistoryManager = {}
|
||||
"../Project/ProjectDetailsHandler": @ProjectDetailsHandler = {}
|
||||
"../Project/ProjectEntityUpdateHandler": @ProjectEntityUpdateHandler = {}
|
||||
"./RestoreManager": @RestoreManager = {}
|
||||
@settings.apis =
|
||||
trackchanges:
|
||||
enabled: false
|
||||
|
|
126
services/web/test/unit/coffee/History/RestoreManagerTests.coffee
Normal file
126
services/web/test/unit/coffee/History/RestoreManagerTests.coffee
Normal file
|
@ -0,0 +1,126 @@
|
|||
SandboxedModule = require('sandboxed-module')
|
||||
assert = require('assert')
|
||||
require('chai').should()
|
||||
expect = require('chai').expect
|
||||
sinon = require('sinon')
|
||||
modulePath = require('path').join __dirname, '../../../../app/js/Features/History/RestoreManager'
|
||||
Errors = require '../../../../app/js/Features/Errors/Errors'
|
||||
tk = require("timekeeper")
|
||||
moment = require('moment')
|
||||
|
||||
describe 'RestoreManager', ->
|
||||
beforeEach ->
|
||||
@RestoreManager = SandboxedModule.require modulePath, requires:
|
||||
'../../infrastructure/FileWriter': @FileWriter = {}
|
||||
'../Uploads/FileSystemImportManager': @FileSystemImportManager = {}
|
||||
'../Project/ProjectLocator': @ProjectLocator = {}
|
||||
'../Errors/Errors': Errors
|
||||
'../Project/ProjectEntityHandler': @ProjectEntityHandler = {}
|
||||
'../Editor/EditorController': @EditorController = {}
|
||||
'logger-sharelatex': @logger = {log: sinon.stub(), err: sinon.stub()}
|
||||
@user_id = 'mock-user-id'
|
||||
@project_id = 'mock-project-id'
|
||||
@version = 42
|
||||
@callback = sinon.stub()
|
||||
tk.freeze Date.now() # freeze the time for these tests
|
||||
|
||||
afterEach ->
|
||||
tk.reset()
|
||||
|
||||
describe 'restoreFileFromV2', ->
|
||||
beforeEach ->
|
||||
@RestoreManager._writeFileVersionToDisk = sinon.stub().yields(null, @fsPath = "/tmp/path/on/disk")
|
||||
@RestoreManager._findFolderOrRootFolderId = sinon.stub().yields(null, @folder_id = 'mock-folder-id')
|
||||
@FileSystemImportManager.addEntity = sinon.stub().yields(null, @entity = 'mock-entity')
|
||||
|
||||
describe "with a file not in a folder", ->
|
||||
beforeEach ->
|
||||
@pathname = 'foo.tex'
|
||||
@RestoreManager.restoreFileFromV2 @user_id, @project_id, @version, @pathname, @callback
|
||||
|
||||
it 'should write the file version to disk', ->
|
||||
@RestoreManager._writeFileVersionToDisk
|
||||
.calledWith(@project_id, @version, @pathname)
|
||||
.should.equal true
|
||||
|
||||
it 'should find the root folder', ->
|
||||
@RestoreManager._findFolderOrRootFolderId
|
||||
.calledWith(@project_id, "")
|
||||
.should.equal true
|
||||
|
||||
it 'should add the entity', ->
|
||||
@FileSystemImportManager.addEntity
|
||||
.calledWith(@user_id, @project_id, @folder_id, 'foo.tex', @fsPath, false)
|
||||
.should.equal true
|
||||
|
||||
it 'should call the callback with the entity', ->
|
||||
@callback.calledWith(null, @entity).should.equal true
|
||||
|
||||
describe "with a file in a folder", ->
|
||||
beforeEach ->
|
||||
@pathname = 'foo/bar.tex'
|
||||
@RestoreManager.restoreFileFromV2 @user_id, @project_id, @version, @pathname, @callback
|
||||
|
||||
it 'should find the folder', ->
|
||||
@RestoreManager._findFolderOrRootFolderId
|
||||
.calledWith(@project_id, "foo")
|
||||
.should.equal true
|
||||
|
||||
it 'should add the entity by its basename', ->
|
||||
@FileSystemImportManager.addEntity
|
||||
.calledWith(@user_id, @project_id, @folder_id, 'bar.tex', @fsPath, false)
|
||||
.should.equal true
|
||||
|
||||
describe '_findFolderOrRootFolderId', ->
|
||||
describe 'with a folder that exists', ->
|
||||
beforeEach ->
|
||||
@ProjectLocator.findElementByPath = sinon.stub().yields(null, {_id: @folder_id = 'mock-folder-id'}, 'folder')
|
||||
@RestoreManager._findFolderOrRootFolderId @project_id, 'folder_name', @callback
|
||||
|
||||
it 'should look up the folder', ->
|
||||
@ProjectLocator.findElementByPath
|
||||
.calledWith({project_id: @project_id, path: 'folder_name'})
|
||||
.should.equal true
|
||||
|
||||
it 'should return the folder_id', ->
|
||||
@callback.calledWith(null, @folder_id).should.equal true
|
||||
|
||||
describe "with a folder that doesn't exist", ->
|
||||
beforeEach ->
|
||||
@ProjectLocator.findElementByPath = sinon.stub().yields(new Errors.NotFoundError())
|
||||
@RestoreManager._findFolderOrRootFolderId @project_id, 'folder_name', @callback
|
||||
|
||||
it 'should return null', ->
|
||||
@callback.calledWith(null, null).should.equal true
|
||||
|
||||
describe '_addEntityWithUniqueName', ->
|
||||
beforeEach ->
|
||||
@addEntityWithName = sinon.stub()
|
||||
@name = 'foo.tex'
|
||||
|
||||
describe 'with a valid name', ->
|
||||
beforeEach ->
|
||||
@addEntityWithName.yields(null, @entity = 'mock-entity')
|
||||
@RestoreManager._addEntityWithUniqueName @addEntityWithName, @name, @callback
|
||||
|
||||
it 'should add the entity', ->
|
||||
@addEntityWithName.calledWith(@name).should.equal true
|
||||
|
||||
it 'should return the entity', ->
|
||||
@callback.calledWith(null, @entity).should.equal true
|
||||
|
||||
describe "with an invalid name", ->
|
||||
beforeEach ->
|
||||
@addEntityWithName.onFirstCall().yields(new Errors.InvalidNameError())
|
||||
@addEntityWithName.onSecondCall().yields(null, @entity = 'mock-entity')
|
||||
@RestoreManager._addEntityWithUniqueName @addEntityWithName, @name, @callback
|
||||
|
||||
it 'should try to add the entity with its original name', ->
|
||||
@addEntityWithName.calledWith('foo.tex').should.equal true
|
||||
|
||||
it 'should try to add the entity with a unique name', ->
|
||||
date = moment(new Date()).format('Do MMM YY H:mm:ss')
|
||||
@addEntityWithName.calledWith("foo (Restored on #{date}).tex").should.equal true
|
||||
|
||||
it 'should return the entity', ->
|
||||
@callback.calledWith(null, @entity).should.equal true
|
|
@ -251,27 +251,6 @@ describe 'ProjectEntityUpdateHandler', ->
|
|||
.calledWith({_id : project_id}, {$unset : {rootDoc_id: true}})
|
||||
.should.equal true
|
||||
|
||||
describe "restoreDoc", ->
|
||||
beforeEach ->
|
||||
@doc = { "mock": "doc" }
|
||||
@ProjectEntityHandler.getDoc = sinon.stub().yields(null, @docLines)
|
||||
@ProjectEntityUpdateHandler.addDoc = sinon.stub().yields(null, @doc, folder_id)
|
||||
|
||||
@ProjectEntityUpdateHandler.restoreDoc project_id, doc_id, @docName, @callback
|
||||
|
||||
it 'should get the doc lines', ->
|
||||
@ProjectEntityHandler.getDoc
|
||||
.calledWith(project_id, doc_id, include_deleted: true)
|
||||
.should.equal true
|
||||
|
||||
it "should add a new doc with these doc lines", ->
|
||||
@ProjectEntityUpdateHandler.addDoc
|
||||
.calledWith(project_id, null, @docName, @docLines)
|
||||
.should.equal true
|
||||
|
||||
it "should call the callback with the new folder and doc", ->
|
||||
@callback.calledWith(null, @doc, folder_id).should.equal true
|
||||
|
||||
describe 'addDoc', ->
|
||||
beforeEach ->
|
||||
@path = "/path/to/doc"
|
||||
|
|
|
@ -130,12 +130,13 @@ define ['ide/history/HistoryV2Manager'], (HistoryV2Manager) ->
|
|||
project_ops: [{
|
||||
remove:
|
||||
pathname: "main.tex"
|
||||
atV: 2
|
||||
}]
|
||||
fromV: 1, toV: 2
|
||||
}])
|
||||
|
||||
expect(result).to.deep.equal({
|
||||
"main.tex": { fromV: 0, toV: 2, deleted: true }
|
||||
"main.tex": { fromV: 0, toV: 2, deletedAtV: 2 }
|
||||
})
|
||||
|
||||
it "should track single deletions", ->
|
||||
|
@ -143,10 +144,11 @@ define ['ide/history/HistoryV2Manager'], (HistoryV2Manager) ->
|
|||
project_ops: [{
|
||||
remove:
|
||||
pathname: "main.tex"
|
||||
atV: 1
|
||||
}]
|
||||
fromV: 0, toV: 1
|
||||
}])
|
||||
|
||||
expect(result).to.deep.equal({
|
||||
"main.tex": { fromV: 0, toV: 1, deleted: true }
|
||||
"main.tex": { fromV: 0, toV: 1, deletedAtV: 1 }
|
||||
})
|
||||
|
|
Loading…
Add table
Reference in a new issue