mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-21 20:47:08 -05:00
Merge branch 'track_changes_integration'
This commit is contained in:
commit
9851321429
21 changed files with 736 additions and 1942 deletions
|
@ -84,6 +84,7 @@ module.exports = (grunt) ->
|
|||
paths:
|
||||
"underscore": "libs/underscore"
|
||||
"jquery": "libs/jquery"
|
||||
"moment": "libs/moment"
|
||||
shim:
|
||||
"libs/backbone":
|
||||
deps: ["libs/underscore"]
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
logger = require "logger-sharelatex"
|
||||
request = require "request"
|
||||
settings = require "settings-sharelatex"
|
||||
|
||||
module.exports = TrackChangesController =
|
||||
proxyToTrackChangesApi: (req, res, next = (error) ->) ->
|
||||
url = settings.apis.trackchanges.url + req.url
|
||||
logger.log url: url, "proxying to track-changes api"
|
||||
getReq = request.get(url)
|
||||
getReq.pipe(res)
|
||||
getReq.on "error", (error) ->
|
||||
logger.error err: error, "track-changes API error"
|
||||
next(error)
|
|
@ -32,6 +32,7 @@ CompileController = require("./Features/Compile/CompileController")
|
|||
HealthCheckController = require("./Features/HealthCheck/HealthCheckController")
|
||||
ProjectDownloadsController = require "./Features/Downloads/ProjectDownloadsController"
|
||||
FileStoreController = require("./Features/FileStore/FileStoreController")
|
||||
TrackChangesController = require("./Features/TrackChanges/TrackChangesController")
|
||||
logger = require("logger-sharelatex")
|
||||
|
||||
httpAuth = require('express').basicAuth (user, pass)->
|
||||
|
@ -122,6 +123,9 @@ module.exports = class Router
|
|||
app.get '/Project/:Project_id/version', SecutiryManager.requestCanAccessProject, versioningController.listVersions
|
||||
app.get '/Project/:Project_id/version/:Version_id', SecutiryManager.requestCanAccessProject, versioningController.getVersion
|
||||
|
||||
app.get "/project/:Project_id/doc/:doc_id/updates", SecutiryManager.requestCanAccessProject, TrackChangesController.proxyToTrackChangesApi
|
||||
app.get "/project/:Project_id/doc/:doc_id/diff", SecutiryManager.requestCanAccessProject, TrackChangesController.proxyToTrackChangesApi
|
||||
|
||||
app.post '/project/:project_id/leave', AuthenticationController.requireLogin(), CollaboratorsController.removeSelfFromProject
|
||||
app.get '/project/:Project_id/collaborators', SecutiryManager.requestCanAccessProject(allow_auth_token: true), CollaboratorsController.getCollaborators
|
||||
|
||||
|
|
|
@ -38,7 +38,8 @@ block content
|
|||
window.requirejs = {
|
||||
"paths" : {
|
||||
"underscore": "libs/underscore",
|
||||
"mathjax": "https://c328740.ssl.cf1.rackcdn.com/mathjax/latest/MathJax.js?config=TeX-AMS_HTML"
|
||||
"mathjax": "https://c328740.ssl.cf1.rackcdn.com/mathjax/latest/MathJax.js?config=TeX-AMS_HTML",
|
||||
"moment": "libs/moment"
|
||||
},
|
||||
"urlArgs" : "fingerprint=#{fingerprint(jsPath + 'ide.js')}",
|
||||
"waitSeconds": 0,
|
||||
|
|
|
@ -147,6 +147,7 @@
|
|||
#editorArea(style='display: none;')
|
||||
#editorSplitter
|
||||
#leftEditorPanel.ui-layout-center
|
||||
#editorWrapper
|
||||
#editor
|
||||
#undoConflictWarning(style="display: none")
|
||||
| <strong>Watch out!</strong> We had to undo some of your collaborators changes before we could undo yours.
|
||||
|
@ -403,11 +404,6 @@
|
|||
div(class='version-message') {{message}}
|
||||
div(class='version-date') {{date}}
|
||||
|
||||
script(type='text/template')#autoCompleteSuggestionTemplate
|
||||
li
|
||||
strong {{base}}
|
||||
| {{completion}}
|
||||
|
||||
script(type='text/template')#versionListTemplate
|
||||
ul#version-list.nav.nav-pills.nav-stacked
|
||||
li.loading Loading...
|
||||
|
@ -427,6 +423,32 @@
|
|||
div
|
||||
a(href="#", title='Show Hot Keys List') Hot keys
|
||||
|
||||
script(type='text/template')#trackChangesPanelTemplate
|
||||
#trackChangesPanel
|
||||
.track-changes-side-bar
|
||||
.track-changes-header
|
||||
h3 Recent changes
|
||||
a(href="#").track-changes-close
|
||||
i.icon-remove
|
||||
.change-list-area
|
||||
.track-changes-diff
|
||||
|
||||
script(type='text/template')#changeListItemTemplate
|
||||
div(class='change-selectors')
|
||||
input(type="radio",name="fromVersion").change-selector-from
|
||||
input(type="radio",name="toVersion").change-selector-to
|
||||
|
||||
div(class='change-description')
|
||||
div(class='change-date') {{date}}
|
||||
div(class='change-name')
|
||||
div.color-square(style="background-color: hsl({{hue}}, 100%, 70%);")
|
||||
span {{name}}
|
||||
|
||||
script(type='text/template')#changeListTemplate
|
||||
ul.change-list.nav.nav-pills.nav-stacked
|
||||
li.loading-changes Loading...
|
||||
li.empty-message You haven't made any changes yet!
|
||||
|
||||
script(type='text/template')#hotKeysListTemplate
|
||||
.hotkeys
|
||||
h3 Common
|
||||
|
|
|
@ -73,7 +73,7 @@ define [
|
|||
if @currentViewState != @viewOptions.splitView
|
||||
@currentViewState = @viewOptions.splitView
|
||||
@leftPanel.prepend(
|
||||
@editorPanel.find("#editor")
|
||||
@editorPanel.find("#editorWrapper")
|
||||
)
|
||||
splitter = @editorPanel.find("#editorSplitter")
|
||||
splitter.show()
|
||||
|
@ -84,7 +84,7 @@ define [
|
|||
@_saveSplitterState()
|
||||
@currentViewState = @viewOptions.flatView
|
||||
@editorPanel.prepend(
|
||||
@editorPanel.find("#editor")
|
||||
@editorPanel.find("#editorWrapper")
|
||||
)
|
||||
@editorPanel.find("#editorSplitter").hide()
|
||||
@aceEditor.resize(true)
|
||||
|
@ -352,3 +352,9 @@ define [
|
|||
getCurrentDocId: () ->
|
||||
@current_doc_id
|
||||
|
||||
show: () ->
|
||||
$("#editor").show()
|
||||
|
||||
hide: () ->
|
||||
$("#editor").hide()
|
||||
|
||||
|
|
|
@ -25,6 +25,7 @@ define [
|
|||
"file-view/FileViewManager"
|
||||
"tour/IdeTour"
|
||||
"analytics/AnalyticsManager"
|
||||
"track-changes/TrackChangesManager"
|
||||
"ace/ace"
|
||||
"libs/jquery.color"
|
||||
"libs/jquery-layout"
|
||||
|
@ -56,7 +57,8 @@ define [
|
|||
BackspaceHighjack,
|
||||
FileViewManager,
|
||||
IdeTour,
|
||||
AnalyticsManager
|
||||
AnalyticsManager,
|
||||
TrackChangesManager
|
||||
) ->
|
||||
|
||||
|
||||
|
@ -117,6 +119,7 @@ define [
|
|||
@cursorManager = new CursorManager(@)
|
||||
@fileViewManager = new FileViewManager(@)
|
||||
@analyticsManager = new AnalyticsManager(@)
|
||||
@trackChangesManager = new TrackChangesManager(@)
|
||||
|
||||
@setLoadingMessage("Connecting")
|
||||
firstConnect = true
|
||||
|
|
|
@ -1,7 +1,38 @@
|
|||
define [
|
||||
"libs/md5"
|
||||
"libs/backbone"
|
||||
], () ->
|
||||
User = Backbone.Model.extend {}, {
|
||||
User = Backbone.Model.extend {
|
||||
gravatarUrl: (size = 32) ->
|
||||
email = @get("email").trim().toLowerCase()
|
||||
hash = CryptoJS.MD5(email)
|
||||
return "//www.gravatar.com/avatar/#{hash}.jpg?size=#{size}&d=mm"
|
||||
|
||||
OWNER_HUE: 200
|
||||
hue: () ->
|
||||
if window.user.id == @get("id")
|
||||
hue = @OWNER_HUE
|
||||
else
|
||||
hash = CryptoJS.MD5(@get("id"))
|
||||
hue = parseInt(hash.toString().slice(0,8), 16) % 320
|
||||
# Avoid 20 degrees either side of the owner
|
||||
if hue > @OWNER_HUE - 20
|
||||
hue = hue + 40
|
||||
return hue
|
||||
|
||||
name: () ->
|
||||
if window.user.id == @get("id")
|
||||
return "you"
|
||||
parts = []
|
||||
first_name = @get("first_name")
|
||||
if first_name? and first_name.length > 0
|
||||
parts.push first_name
|
||||
last_name = @get("last_name")
|
||||
if last_name? and last_name.length > 0
|
||||
parts.push last_name
|
||||
return parts.join(" ")
|
||||
|
||||
}, {
|
||||
findOrBuild : (id, attributes) ->
|
||||
model = @find id
|
||||
if !model?
|
||||
|
|
198
services/web/public/coffee/track-changes/ChangeListView.coffee
Normal file
198
services/web/public/coffee/track-changes/ChangeListView.coffee
Normal file
|
@ -0,0 +1,198 @@
|
|||
define [
|
||||
"moment"
|
||||
"libs/mustache"
|
||||
"libs/backbone"
|
||||
], (moment)->
|
||||
ChangeListView = Backbone.View.extend
|
||||
template: $("#changeListTemplate").html()
|
||||
|
||||
events:
|
||||
"scroll" : "loadUntilFull"
|
||||
|
||||
initialize: () ->
|
||||
@itemViews = []
|
||||
@atEndOfCollection = false
|
||||
|
||||
self = this
|
||||
@collection.on "add", (model) ->
|
||||
self.addItem model
|
||||
@collection.on "reset", (collection) ->
|
||||
self.addItem model for model in collection.models
|
||||
|
||||
@selectedFromIndex = 0
|
||||
@selectedToIndex = 0
|
||||
|
||||
@render()
|
||||
@hideLoading()
|
||||
|
||||
render: ->
|
||||
@$el.html Mustache.to_html @template
|
||||
@$el.css
|
||||
overflow: "scroll"
|
||||
this
|
||||
|
||||
addItem: (model) ->
|
||||
index = @collection.indexOf(model)
|
||||
view = new ChangeListItemView(model : model)
|
||||
@itemViews.push view
|
||||
elementAtIndex = @$(".change-list").children()[index]
|
||||
view.$el.insertBefore(elementAtIndex)
|
||||
|
||||
view.on "click", (e, v) =>
|
||||
@selectedToIndex = index
|
||||
@selectedFromIndex = index
|
||||
@resetAllSelectors()
|
||||
@triggerChangeDiff()
|
||||
|
||||
view.on "selected:to", (e, v) =>
|
||||
@selectedToIndex = index
|
||||
@resetAllSelectors()
|
||||
@triggerChangeDiff()
|
||||
|
||||
view.on "selected:from", (e, v) =>
|
||||
@selectedFromIndex = index
|
||||
@resetAllSelectors()
|
||||
@triggerChangeDiff()
|
||||
|
||||
view.resetSelector(index, @selectedFromIndex, @selectedToIndex)
|
||||
|
||||
resetAllSelectors: () ->
|
||||
for view, i in @itemViews
|
||||
view.resetSelector(i, @selectedFromIndex, @selectedToIndex)
|
||||
|
||||
triggerChangeDiff: () ->
|
||||
@trigger "change_diff", @collection.models[@selectedFromIndex], @collection.models[@selectedToIndex]
|
||||
|
||||
listShorterThanContainer: ->
|
||||
@$el.height() > @$(".change-list").height()
|
||||
|
||||
atEndOfListView: ->
|
||||
@$el.scrollTop() + @$el.height() >= @$(".change-list").height() - 30
|
||||
|
||||
loadUntilFull: (e, callback) ->
|
||||
if (@listShorterThanContainer() or @atEndOfListView()) and not @atEndOfCollection and not @loading
|
||||
@showLoading()
|
||||
@hideEmptyMessage()
|
||||
@collection.fetchNextBatch
|
||||
error: =>
|
||||
@hideLoading()
|
||||
@showEmptyMessageIfCollectionEmpty()
|
||||
callback() if callback?
|
||||
success: (collection, response) =>
|
||||
@hideLoading()
|
||||
if response.updates.length == @collection.batchSize
|
||||
@loadUntilFull(e, callback)
|
||||
else
|
||||
@atEndOfCollection = true
|
||||
@showEmptyMessageIfCollectionEmpty()
|
||||
callback() if callback?
|
||||
|
||||
else
|
||||
callback() if callback?
|
||||
|
||||
showEmptyMessageIfCollectionEmpty: ()->
|
||||
if @collection.isEmpty()
|
||||
@$(".empty-message").show()
|
||||
else
|
||||
@$(".empty-message").hide()
|
||||
|
||||
hideEmptyMessage: () ->
|
||||
@$(".empty-message").hide()
|
||||
|
||||
showLoading: ->
|
||||
@loading = true
|
||||
@$(".loading-changes").show()
|
||||
|
||||
hideLoading: ->
|
||||
@loading = false
|
||||
@$(".loading-changes").hide()
|
||||
|
||||
ChangeListItemView = Backbone.View.extend
|
||||
tagName: "li"
|
||||
|
||||
events:
|
||||
"click .change-description" : "onClick"
|
||||
"click .change-selector-from" : "onFromSelectorClick"
|
||||
"click .change-selector-to" : "onToSelectorClick"
|
||||
|
||||
template : $("#changeListItemTemplate").html()
|
||||
|
||||
initialize: ->
|
||||
@render()
|
||||
|
||||
render: ->
|
||||
@$el.html Mustache.to_html(@template, @modelView())
|
||||
return this
|
||||
|
||||
modelView: ->
|
||||
modelView = {
|
||||
hue: @model.get("user").hue()
|
||||
date: moment(parseInt(@model.get("end_ts"), 10)).calendar()
|
||||
name: @model.get("user").name()
|
||||
}
|
||||
# modelView.start_ts = util.formatDate(modelView.start_ts)
|
||||
# modelView.end_ts = util.formatDate(modelView.end_ts)
|
||||
return modelView
|
||||
|
||||
onClick: (e) ->
|
||||
e.preventDefault()
|
||||
@trigger "click", e, @
|
||||
|
||||
onToSelectorClick: (e) ->
|
||||
@trigger "selected:to", e, @
|
||||
|
||||
onFromSelectorClick: (e) ->
|
||||
@trigger "selected:from", e, @
|
||||
|
||||
isSelectedFrom: () ->
|
||||
@$(".change-selector-from").is(":checked")
|
||||
|
||||
isSelectedTo: () ->
|
||||
@$(".change-selector-to").is(":checked")
|
||||
|
||||
hideFromSelector: () ->
|
||||
@$(".change-selector-from").hide()
|
||||
|
||||
showFromSelector: () ->
|
||||
@$(".change-selector-from").show()
|
||||
|
||||
hideToSelector: () ->
|
||||
@$(".change-selector-to").hide()
|
||||
|
||||
showToSelector: () ->
|
||||
@$(".change-selector-to").show()
|
||||
|
||||
setFromChecked: (checked) ->
|
||||
@$(".change-selector-from").prop("checked", checked)
|
||||
|
||||
setToChecked: (checked) ->
|
||||
@$(".change-selector-to").prop("checked", checked)
|
||||
|
||||
setSelected: () ->
|
||||
@$el.addClass("selected")
|
||||
|
||||
setUnselected: () ->
|
||||
@$el.removeClass("selected")
|
||||
|
||||
resetSelector: (myIndex, selectedFromIndex, selectedToIndex) ->
|
||||
if myIndex >= selectedToIndex
|
||||
@showFromSelector()
|
||||
else
|
||||
@hideFromSelector()
|
||||
|
||||
if myIndex <= selectedFromIndex
|
||||
@showToSelector()
|
||||
else
|
||||
@hideToSelector()
|
||||
|
||||
if selectedToIndex <= myIndex <= selectedFromIndex
|
||||
@setSelected()
|
||||
else
|
||||
@setUnselected()
|
||||
|
||||
@setFromChecked(myIndex == selectedFromIndex)
|
||||
@setToChecked(myIndex == selectedToIndex)
|
||||
|
||||
|
||||
return ChangeListView
|
||||
|
140
services/web/public/coffee/track-changes/DiffView.coffee
Normal file
140
services/web/public/coffee/track-changes/DiffView.coffee
Normal file
|
@ -0,0 +1,140 @@
|
|||
define [
|
||||
"ace/ace"
|
||||
"ace/mode/latex"
|
||||
"ace/range"
|
||||
"libs/backbone"
|
||||
], (Ace, LatexMode, Range)->
|
||||
DiffView = Backbone.View.extend
|
||||
initialize: () ->
|
||||
@model.on "change:diff", () => @render()
|
||||
@render()
|
||||
|
||||
render: ->
|
||||
diff = @model.get("diff")
|
||||
return unless diff?
|
||||
@createAceEditor()
|
||||
@aceEditor.setValue(@getPlainDiffContent())
|
||||
@aceEditor.clearSelection()
|
||||
session = @aceEditor.getSession()
|
||||
session.setMode(new LatexMode.Mode())
|
||||
session.setUseWrapMode(true)
|
||||
@insertMarkers()
|
||||
return @
|
||||
|
||||
createAceEditor: () ->
|
||||
@$el.empty()
|
||||
$editor = $("<div/>")
|
||||
@$el.append($editor)
|
||||
@aceEditor = Ace.edit($editor[0])
|
||||
@aceEditor.setTheme("ace/theme/#{window.userSettings.theme}")
|
||||
@aceEditor.setReadOnly true
|
||||
@aceEditor.setShowPrintMargin(false)
|
||||
|
||||
@aceEditor.on "mousemove", (e) =>
|
||||
position = @aceEditor.renderer.screenToTextCoordinates(e.clientX, e.clientY)
|
||||
e.position = position
|
||||
@updateVisibleNames(e)
|
||||
|
||||
getPlainDiffContent: () ->
|
||||
content = ""
|
||||
for entry in @model.get("diff") or []
|
||||
content += entry.u or entry.i or entry.d or ""
|
||||
return content
|
||||
|
||||
insertMarkers: () ->
|
||||
row = 0
|
||||
column = 0
|
||||
for entry, i in @model.get("diff") or []
|
||||
content = entry.u or entry.i or entry.d
|
||||
lines = content.split("\n")
|
||||
startRow = row
|
||||
startColumn = column
|
||||
if lines.length > 1
|
||||
endRow = startRow + lines.length - 1
|
||||
endColumn = lines[lines.length - 1].length
|
||||
else
|
||||
endRow = startRow
|
||||
endColumn = startColumn + lines[0].length
|
||||
row = endRow
|
||||
column = endColumn
|
||||
|
||||
range = new Range.Range(
|
||||
startRow, startColumn, endRow, endColumn
|
||||
)
|
||||
@addMarker(range, "change-marker-#{i}", entry)
|
||||
|
||||
addMarker: (range, id, entry) ->
|
||||
session = @aceEditor.getSession()
|
||||
markerBackLayer = @aceEditor.renderer.$markerBack
|
||||
markerFrontLayer = @aceEditor.renderer.$markerFront
|
||||
lineHeight = @aceEditor.renderer.lineHeight
|
||||
if entry.i? or entry.d?
|
||||
hue = entry.meta.user.hue()
|
||||
if entry.i?
|
||||
@_addMarkerWithCustomStyle session, markerBackLayer, range, "deleted-change-background", false, """
|
||||
background-color : hsl(#{hue}, 70%, 85%);
|
||||
"""
|
||||
tag = "Added by #{entry.meta.user.name()}"
|
||||
if entry.d?
|
||||
@_addMarkerWithCustomStyle session, markerBackLayer, range, "deleted-change-background", false, """
|
||||
background-color : hsl(#{hue}, 70%, 95%);
|
||||
"""
|
||||
@_addMarkerWithCustomStyle session, markerBackLayer, range, "deleted-change-foreground", true, """
|
||||
height: #{Math.round(lineHeight/2) - 1}px;
|
||||
border-bottom: 2px solid hsl(#{hue}, 70%, 40%);
|
||||
"""
|
||||
tag = "Deleted by #{entry.meta.user.name()}"
|
||||
|
||||
date = moment(parseInt(entry.meta.end_ts, 10)).format("Do MMM YYYY, h:mm:ss a")
|
||||
tag += " on #{date}"
|
||||
@_addNameTag session, id, range, tag, """
|
||||
background-color : hsl(#{hue}, 70%, 95%);
|
||||
"""
|
||||
|
||||
_addMarkerWithCustomStyle: (session, markerLayer, range, klass, foreground, style) ->
|
||||
session.addMarker range, klass, (html, range, left, top, config) ->
|
||||
if range.isMultiLine()
|
||||
markerLayer.drawTextMarker(html, range, klass, config, style)
|
||||
else
|
||||
markerLayer.drawSingleLineMarker(html, range, "#{klass} ace_start", config, 0, style)
|
||||
, foreground
|
||||
|
||||
_addNameTag: (session, id, range, content, style) ->
|
||||
@nameMarkers ||= []
|
||||
@nameMarkers.push
|
||||
range: range
|
||||
id: id
|
||||
startRange = new Range.Range(
|
||||
range.start.row, range.start.column
|
||||
range.start.row, range.start.column + 1
|
||||
)
|
||||
session.addMarker startRange, "change-name-marker", (html, range, left, top, config) ->
|
||||
html.push """
|
||||
<div
|
||||
id = '#{id}'
|
||||
class = 'change-name-marker'
|
||||
style = '
|
||||
height: #{config.lineHeight}px;
|
||||
top: #{top}px;
|
||||
left: #{left}px;
|
||||
'
|
||||
>
|
||||
<div
|
||||
class="name" style="
|
||||
display: none;
|
||||
bottom: #{config.lineHeight + 3}px;
|
||||
#{style}
|
||||
">#{content}</div>
|
||||
</div>
|
||||
"""
|
||||
, true
|
||||
|
||||
updateVisibleNames: (e) ->
|
||||
for marker in @nameMarkers or []
|
||||
if marker.range.contains(e.position.row, e.position.column)
|
||||
$("##{marker.id}").find(".name").show()
|
||||
else
|
||||
$("##{marker.id}").find(".name").hide()
|
||||
|
||||
return DiffView
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
define [
|
||||
"track-changes/models/ChangeList"
|
||||
"track-changes/models/Diff"
|
||||
"track-changes/ChangeListView"
|
||||
"track-changes/DiffView"
|
||||
], (ChangeList, Diff, ChangeListView, DiffView) ->
|
||||
class TrackChangesManager
|
||||
template: $("#trackChangesPanelTemplate").html()
|
||||
|
||||
constructor: (@ide) ->
|
||||
@$el = $(@template)
|
||||
$("#editorWrapper").append(@$el)
|
||||
@hideEl()
|
||||
|
||||
@ide.editor.on "change:doc", () =>
|
||||
@hideEl()
|
||||
|
||||
@$el.find(".track-changes-close").on "click", (e) =>
|
||||
e.preventDefault
|
||||
@hideEl()
|
||||
|
||||
show: () ->
|
||||
@project_id = window.userSettings.project_id
|
||||
@doc_id = @ide.editor.current_doc_id
|
||||
@changes = new ChangeList([], doc_id: @doc_id, project_id: @project_id)
|
||||
|
||||
@changeListView = new ChangeListView(
|
||||
collection : @changes,
|
||||
el : @$el.find(".change-list-area")
|
||||
)
|
||||
@changeListView.render()
|
||||
@changeListView.loadUntilFull()
|
||||
|
||||
@changeListView.on "change_diff", (fromModel, toModel) =>
|
||||
@diff = new Diff({
|
||||
project_id: @project_id
|
||||
doc_id: @doc_id
|
||||
from: fromModel.get("version")
|
||||
to: toModel.get("version")
|
||||
})
|
||||
@diffView = new DiffView(
|
||||
model: @diff
|
||||
el: @$el.find(".track-changes-diff")
|
||||
)
|
||||
@diff.fetch()
|
||||
|
||||
@showEl()
|
||||
|
||||
showEl: ->
|
||||
@ide.editor.hide()
|
||||
@$el.show()
|
||||
|
||||
hideEl: () ->
|
||||
@ide.editor.show()
|
||||
@$el.hide()
|
||||
|
||||
return TrackChangesManager
|
|
@ -0,0 +1,12 @@
|
|||
define [
|
||||
"models/User"
|
||||
"libs/backbone"
|
||||
], (User)->
|
||||
Change = Backbone.Model.extend
|
||||
parse: (change) ->
|
||||
return {
|
||||
start_ts: change.meta.start_ts
|
||||
end_ts: change.meta.end_ts
|
||||
user: User.findOrBuild(change.meta.user.id, change.meta.user)
|
||||
version: change.v
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
define [
|
||||
"track-changes/models/Change"
|
||||
"libs/backbone"
|
||||
], (Change)->
|
||||
ChangeList = Backbone.Collection.extend
|
||||
model: Change
|
||||
batchSize: 3
|
||||
|
||||
initialize: (models, @options) ->
|
||||
|
||||
url: () ->
|
||||
url = "/project/#{@options.project_id}/doc/#{@options.doc_id}/updates?limit=#{@batchSize}"
|
||||
if @models.length > 0
|
||||
last = @models[@models.length - 1]
|
||||
url += "&to=#{last.get("version") - 1}"
|
||||
return url
|
||||
|
||||
parse: (json) ->
|
||||
return json.updates
|
||||
|
||||
fetchNextBatch: (options = {}) ->
|
||||
options.add = true
|
||||
@fetch options
|
||||
|
||||
|
13
services/web/public/coffee/track-changes/models/Diff.coffee
Normal file
13
services/web/public/coffee/track-changes/models/Diff.coffee
Normal file
|
@ -0,0 +1,13 @@
|
|||
define [
|
||||
"models/User"
|
||||
"libs/backbone"
|
||||
], (User) ->
|
||||
Diff = Backbone.Model.extend
|
||||
url: () ->
|
||||
"/project/#{@get("project_id")}/doc/#{@get("doc_id")}/diff?from=#{@get("from")}&to=#{@get("to")}"
|
||||
|
||||
parse: (diff) ->
|
||||
for entry in diff.diff
|
||||
if entry.meta? and entry.meta.user?
|
||||
entry.meta.user = User.findOrBuild(entry.meta.user.id, entry.meta.user)
|
||||
return diff
|
19
services/web/public/js/libs/md5.js
Normal file
19
services/web/public/js/libs/md5.js
Normal file
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
CryptoJS v3.1.2
|
||||
code.google.com/p/crypto-js
|
||||
(c) 2009-2013 by Jeff Mott. All rights reserved.
|
||||
code.google.com/p/crypto-js/wiki/License
|
||||
*/
|
||||
var CryptoJS=CryptoJS||function(s,p){var m={},l=m.lib={},n=function(){},r=l.Base={extend:function(b){n.prototype=this;var h=new n;b&&h.mixIn(b);h.hasOwnProperty("init")||(h.init=function(){h.$super.init.apply(this,arguments)});h.init.prototype=h;h.$super=this;return h},create:function(){var b=this.extend();b.init.apply(b,arguments);return b},init:function(){},mixIn:function(b){for(var h in b)b.hasOwnProperty(h)&&(this[h]=b[h]);b.hasOwnProperty("toString")&&(this.toString=b.toString)},clone:function(){return this.init.prototype.extend(this)}},
|
||||
q=l.WordArray=r.extend({init:function(b,h){b=this.words=b||[];this.sigBytes=h!=p?h:4*b.length},toString:function(b){return(b||t).stringify(this)},concat:function(b){var h=this.words,a=b.words,j=this.sigBytes;b=b.sigBytes;this.clamp();if(j%4)for(var g=0;g<b;g++)h[j+g>>>2]|=(a[g>>>2]>>>24-8*(g%4)&255)<<24-8*((j+g)%4);else if(65535<a.length)for(g=0;g<b;g+=4)h[j+g>>>2]=a[g>>>2];else h.push.apply(h,a);this.sigBytes+=b;return this},clamp:function(){var b=this.words,h=this.sigBytes;b[h>>>2]&=4294967295<<
|
||||
32-8*(h%4);b.length=s.ceil(h/4)},clone:function(){var b=r.clone.call(this);b.words=this.words.slice(0);return b},random:function(b){for(var h=[],a=0;a<b;a+=4)h.push(4294967296*s.random()|0);return new q.init(h,b)}}),v=m.enc={},t=v.Hex={stringify:function(b){var a=b.words;b=b.sigBytes;for(var g=[],j=0;j<b;j++){var k=a[j>>>2]>>>24-8*(j%4)&255;g.push((k>>>4).toString(16));g.push((k&15).toString(16))}return g.join("")},parse:function(b){for(var a=b.length,g=[],j=0;j<a;j+=2)g[j>>>3]|=parseInt(b.substr(j,
|
||||
2),16)<<24-4*(j%8);return new q.init(g,a/2)}},a=v.Latin1={stringify:function(b){var a=b.words;b=b.sigBytes;for(var g=[],j=0;j<b;j++)g.push(String.fromCharCode(a[j>>>2]>>>24-8*(j%4)&255));return g.join("")},parse:function(b){for(var a=b.length,g=[],j=0;j<a;j++)g[j>>>2]|=(b.charCodeAt(j)&255)<<24-8*(j%4);return new q.init(g,a)}},u=v.Utf8={stringify:function(b){try{return decodeURIComponent(escape(a.stringify(b)))}catch(g){throw Error("Malformed UTF-8 data");}},parse:function(b){return a.parse(unescape(encodeURIComponent(b)))}},
|
||||
g=l.BufferedBlockAlgorithm=r.extend({reset:function(){this._data=new q.init;this._nDataBytes=0},_append:function(b){"string"==typeof b&&(b=u.parse(b));this._data.concat(b);this._nDataBytes+=b.sigBytes},_process:function(b){var a=this._data,g=a.words,j=a.sigBytes,k=this.blockSize,m=j/(4*k),m=b?s.ceil(m):s.max((m|0)-this._minBufferSize,0);b=m*k;j=s.min(4*b,j);if(b){for(var l=0;l<b;l+=k)this._doProcessBlock(g,l);l=g.splice(0,b);a.sigBytes-=j}return new q.init(l,j)},clone:function(){var b=r.clone.call(this);
|
||||
b._data=this._data.clone();return b},_minBufferSize:0});l.Hasher=g.extend({cfg:r.extend(),init:function(b){this.cfg=this.cfg.extend(b);this.reset()},reset:function(){g.reset.call(this);this._doReset()},update:function(b){this._append(b);this._process();return this},finalize:function(b){b&&this._append(b);return this._doFinalize()},blockSize:16,_createHelper:function(b){return function(a,g){return(new b.init(g)).finalize(a)}},_createHmacHelper:function(b){return function(a,g){return(new k.HMAC.init(b,
|
||||
g)).finalize(a)}}});var k=m.algo={};return m}(Math);
|
||||
(function(s){function p(a,k,b,h,l,j,m){a=a+(k&b|~k&h)+l+m;return(a<<j|a>>>32-j)+k}function m(a,k,b,h,l,j,m){a=a+(k&h|b&~h)+l+m;return(a<<j|a>>>32-j)+k}function l(a,k,b,h,l,j,m){a=a+(k^b^h)+l+m;return(a<<j|a>>>32-j)+k}function n(a,k,b,h,l,j,m){a=a+(b^(k|~h))+l+m;return(a<<j|a>>>32-j)+k}for(var r=CryptoJS,q=r.lib,v=q.WordArray,t=q.Hasher,q=r.algo,a=[],u=0;64>u;u++)a[u]=4294967296*s.abs(s.sin(u+1))|0;q=q.MD5=t.extend({_doReset:function(){this._hash=new v.init([1732584193,4023233417,2562383102,271733878])},
|
||||
_doProcessBlock:function(g,k){for(var b=0;16>b;b++){var h=k+b,w=g[h];g[h]=(w<<8|w>>>24)&16711935|(w<<24|w>>>8)&4278255360}var b=this._hash.words,h=g[k+0],w=g[k+1],j=g[k+2],q=g[k+3],r=g[k+4],s=g[k+5],t=g[k+6],u=g[k+7],v=g[k+8],x=g[k+9],y=g[k+10],z=g[k+11],A=g[k+12],B=g[k+13],C=g[k+14],D=g[k+15],c=b[0],d=b[1],e=b[2],f=b[3],c=p(c,d,e,f,h,7,a[0]),f=p(f,c,d,e,w,12,a[1]),e=p(e,f,c,d,j,17,a[2]),d=p(d,e,f,c,q,22,a[3]),c=p(c,d,e,f,r,7,a[4]),f=p(f,c,d,e,s,12,a[5]),e=p(e,f,c,d,t,17,a[6]),d=p(d,e,f,c,u,22,a[7]),
|
||||
c=p(c,d,e,f,v,7,a[8]),f=p(f,c,d,e,x,12,a[9]),e=p(e,f,c,d,y,17,a[10]),d=p(d,e,f,c,z,22,a[11]),c=p(c,d,e,f,A,7,a[12]),f=p(f,c,d,e,B,12,a[13]),e=p(e,f,c,d,C,17,a[14]),d=p(d,e,f,c,D,22,a[15]),c=m(c,d,e,f,w,5,a[16]),f=m(f,c,d,e,t,9,a[17]),e=m(e,f,c,d,z,14,a[18]),d=m(d,e,f,c,h,20,a[19]),c=m(c,d,e,f,s,5,a[20]),f=m(f,c,d,e,y,9,a[21]),e=m(e,f,c,d,D,14,a[22]),d=m(d,e,f,c,r,20,a[23]),c=m(c,d,e,f,x,5,a[24]),f=m(f,c,d,e,C,9,a[25]),e=m(e,f,c,d,q,14,a[26]),d=m(d,e,f,c,v,20,a[27]),c=m(c,d,e,f,B,5,a[28]),f=m(f,c,
|
||||
d,e,j,9,a[29]),e=m(e,f,c,d,u,14,a[30]),d=m(d,e,f,c,A,20,a[31]),c=l(c,d,e,f,s,4,a[32]),f=l(f,c,d,e,v,11,a[33]),e=l(e,f,c,d,z,16,a[34]),d=l(d,e,f,c,C,23,a[35]),c=l(c,d,e,f,w,4,a[36]),f=l(f,c,d,e,r,11,a[37]),e=l(e,f,c,d,u,16,a[38]),d=l(d,e,f,c,y,23,a[39]),c=l(c,d,e,f,B,4,a[40]),f=l(f,c,d,e,h,11,a[41]),e=l(e,f,c,d,q,16,a[42]),d=l(d,e,f,c,t,23,a[43]),c=l(c,d,e,f,x,4,a[44]),f=l(f,c,d,e,A,11,a[45]),e=l(e,f,c,d,D,16,a[46]),d=l(d,e,f,c,j,23,a[47]),c=n(c,d,e,f,h,6,a[48]),f=n(f,c,d,e,u,10,a[49]),e=n(e,f,c,d,
|
||||
C,15,a[50]),d=n(d,e,f,c,s,21,a[51]),c=n(c,d,e,f,A,6,a[52]),f=n(f,c,d,e,q,10,a[53]),e=n(e,f,c,d,y,15,a[54]),d=n(d,e,f,c,w,21,a[55]),c=n(c,d,e,f,v,6,a[56]),f=n(f,c,d,e,D,10,a[57]),e=n(e,f,c,d,t,15,a[58]),d=n(d,e,f,c,B,21,a[59]),c=n(c,d,e,f,r,6,a[60]),f=n(f,c,d,e,z,10,a[61]),e=n(e,f,c,d,j,15,a[62]),d=n(d,e,f,c,x,21,a[63]);b[0]=b[0]+c|0;b[1]=b[1]+d|0;b[2]=b[2]+e|0;b[3]=b[3]+f|0},_doFinalize:function(){var a=this._data,k=a.words,b=8*this._nDataBytes,h=8*a.sigBytes;k[h>>>5]|=128<<24-h%32;var l=s.floor(b/
|
||||
4294967296);k[(h+64>>>9<<4)+15]=(l<<8|l>>>24)&16711935|(l<<24|l>>>8)&4278255360;k[(h+64>>>9<<4)+14]=(b<<8|b>>>24)&16711935|(b<<24|b>>>8)&4278255360;a.sigBytes=4*(k.length+1);this._process();a=this._hash;k=a.words;for(b=0;4>b;b++)h=k[b],k[b]=(h<<8|h>>>24)&16711935|(h<<24|h>>>8)&4278255360;return a},clone:function(){var a=t.clone.call(this);a._hash=this._hash.clone();return a}});r.MD5=t._createHelper(q);r.HmacMD5=t._createHmacHelper(q)})(Math);
|
File diff suppressed because one or more lines are too long
|
@ -177,25 +177,13 @@ body.editor {
|
|||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
#editor, #orchard {
|
||||
#editor {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
#orchard-toolbar {
|
||||
height: 28px;
|
||||
padding: 5px;
|
||||
background-color: white;
|
||||
border-bottom: 1px solid #aaa;
|
||||
select {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
#orchard-editor {
|
||||
position: absolute;
|
||||
top: 39px;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
#editorWrapper {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
#rightEditorPanel {
|
||||
border-left: 1px solid #aaa;
|
||||
|
@ -212,6 +200,10 @@ body.editor {
|
|||
bottom: 0;
|
||||
z-index: 100;
|
||||
}
|
||||
#trackChangesPanel {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
#undoConflictWarning {
|
||||
|
@ -964,18 +956,6 @@ i[class*="sprite-"] {
|
|||
background-color: #9a2b1f \9;
|
||||
}
|
||||
|
||||
#orchard-editor {
|
||||
background: white;
|
||||
> .CodeMirror > .CodeMirror-scroll > .CodeMirror-sizer > div > .CodeMirror-lines {
|
||||
background-color: white;
|
||||
margin: auto;
|
||||
max-width: 390pt;
|
||||
min-height: 592pt;
|
||||
margin-top: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
#errorMessages {
|
||||
z-index: 10000;
|
||||
top: 4px;
|
||||
|
@ -998,5 +978,3 @@ i[class*="sprite-"] {
|
|||
margin-bottom: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
@import "./orchard";
|
||||
|
|
|
@ -1,236 +0,0 @@
|
|||
.editor-font {
|
||||
font-size: 11pt;
|
||||
line-height: 1.4em;
|
||||
font-family: "Computer Modern", serif;
|
||||
font-weight: normal;
|
||||
}
|
||||
@font-face {
|
||||
font-family: "Computer Modern";
|
||||
src: url('../font/cmunrm.otf');
|
||||
}
|
||||
@font-face {
|
||||
font-family: "Computer Modern";
|
||||
src: url('../font/cmunrb.otf');
|
||||
font-weight: bold;
|
||||
}
|
||||
@font-face {
|
||||
font-family: "Computer Modern Monospace";
|
||||
src: url('../font/cmuntt.otf');
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
|
||||
.CodeMirror {
|
||||
.CodeMirror-placeholder {
|
||||
color: #999;
|
||||
}
|
||||
height: auto;
|
||||
background: none;
|
||||
.editor-font;
|
||||
|
||||
.cm-command, .cm-bracket {
|
||||
color: green;
|
||||
}
|
||||
.cm-math {
|
||||
color: purple;
|
||||
font-family: "Computer Modern Monospace", monospace;
|
||||
}
|
||||
|
||||
.CodeMirror-lines {
|
||||
padding: 10px;
|
||||
}
|
||||
.oe-chapter {
|
||||
font-weight: bold;
|
||||
font-size: 1.8em !important;
|
||||
line-height: 2em !important;
|
||||
}
|
||||
.oe-section {
|
||||
font-weight: bold;
|
||||
font-size: 1.5em !important;
|
||||
line-height: 2em !important;
|
||||
}
|
||||
.oe-subsection {
|
||||
font-weight: bold;
|
||||
font-size: 1.3em !important;
|
||||
line-height: 2em !important;
|
||||
}
|
||||
.oe-widget-line {
|
||||
display: none;
|
||||
}
|
||||
.oe-figure {
|
||||
margin: 0 5px;
|
||||
padding: 12px 0 4px 0;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
.ui-wrapper {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
.oe-figure-image-wrapper {
|
||||
display: inline-block;
|
||||
}
|
||||
.oe-figure-image-placeholder {
|
||||
background-color: rgb(200,200,200);
|
||||
}
|
||||
.oe-figure-image-placeholder.droppable-hover {
|
||||
background-color: rgb(200,200,255);
|
||||
}
|
||||
.oe-figure-toolbar {
|
||||
display: none;
|
||||
text-align: right;
|
||||
padding: 4px;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.oe-title {
|
||||
.CodeMirror-lines {
|
||||
padding: 0;
|
||||
}
|
||||
.oe-title-title {
|
||||
text-align: center;
|
||||
margin-top: 40px;
|
||||
margin-bottom: 20px;
|
||||
.CodeMirror {
|
||||
font-size: 1.6em;
|
||||
line-height: 1.3em;
|
||||
}
|
||||
}
|
||||
|
||||
.oe-title-author {
|
||||
text-align: center;
|
||||
margin-bottom: 10px;
|
||||
.CodeMirror {
|
||||
font-size: 1.2em;
|
||||
line-height: 1.3em;
|
||||
}
|
||||
}
|
||||
|
||||
.oe-title-abstract {
|
||||
margin-top: 40px;
|
||||
margin-bottom: 30px;
|
||||
.oe-title-abstract-label {
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
margin-bottom: 6px;
|
||||
font-size: 0.9em;
|
||||
line-height: 1.3em;
|
||||
}
|
||||
|
||||
.oe-title-abstract-content {
|
||||
width: 80%;
|
||||
margin: auto;
|
||||
.CodeMirror {
|
||||
font-size: 0.9em;
|
||||
line-height: 1.3em;
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.oe-widget {
|
||||
border: 1px solid transparent;
|
||||
margin: 6px 0;
|
||||
}
|
||||
.oe-widget-selected {
|
||||
border: 1px dashed #bbb;
|
||||
.oe-figure-toolbar {
|
||||
display: block;
|
||||
}
|
||||
.ui-wrapper {
|
||||
border: 1px dashed gray;
|
||||
}
|
||||
}
|
||||
|
||||
.oe-block-math {
|
||||
.MathJax_Display {
|
||||
margin: 12px 0;
|
||||
}
|
||||
|
||||
.oe-block-math-toolbar {
|
||||
text-align: right;
|
||||
padding: 4px;
|
||||
z-index: 100;
|
||||
.bootstrap-select.btn-group {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
.CodeMirror {
|
||||
font-family: "Computer Modern Monospace", monospace;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
.oe-non-editable {
|
||||
color: gray;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.oe-bibliography {
|
||||
padding: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// These are needed by the citation drop down too, so
|
||||
// don't wrap them in .oe-bibliography
|
||||
.oe-bibliography-entry {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.oe-bibliography-year {
|
||||
font-weight: bold;
|
||||
}
|
||||
.oe-bibliography-author {
|
||||
font-weight: bold;
|
||||
}
|
||||
.oe-bibliography-title {
|
||||
display: block;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.oe-bibliography {
|
||||
.oe-bibliography-header {
|
||||
font-weight: bold;
|
||||
font-size: 1.8em !important;
|
||||
line-height: 2em !important;
|
||||
}
|
||||
|
||||
.oe-bibliography-entry-wrapper {
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.CodeMirror {
|
||||
font-family: "Computer Modern Monospace", monospace;
|
||||
color: green;
|
||||
height: 400px;
|
||||
}
|
||||
|
||||
.oe-bibliography-no-entries {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.oe-bibliography-error {
|
||||
color: red;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.bootstrap-select.oe-inline-select {
|
||||
margin: 0;
|
||||
vertical-align: bottom;
|
||||
> .btn {
|
||||
border: none;
|
||||
background: none;
|
||||
box-shadow: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
.editor-font;
|
||||
color: rgb(2, 151, 172);
|
||||
}
|
||||
}
|
116
services/web/public/stylesheets/less/trackchanges.less
Normal file
116
services/web/public/stylesheets/less/trackchanges.less
Normal file
|
@ -0,0 +1,116 @@
|
|||
@changesListWidth: 250px;
|
||||
@changesListPadding: 10px;
|
||||
|
||||
#trackChangesPanel {
|
||||
.track-changes-diff {
|
||||
position: absolute;
|
||||
right: @changesListWidth + 1px;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
padding: 0px 12px;
|
||||
height: 100%;
|
||||
.ace_editor {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.track-changes-side-bar {
|
||||
border-left: 1px solid #999;
|
||||
height: 100%;
|
||||
width: @changesListWidth;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
background-color: white;
|
||||
|
||||
.track-changes-header {
|
||||
background-color: black;
|
||||
h3 {
|
||||
color: white;
|
||||
padding-left: 8px;
|
||||
font-size: 1.2em;
|
||||
}
|
||||
a {
|
||||
color: white;
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
right: 8px;
|
||||
}
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
.change-list-area {
|
||||
overflow: scroll;
|
||||
position: absolute;
|
||||
left: 0px;
|
||||
right: 0px;
|
||||
top: 30px;
|
||||
bottom: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.deleted-change-background, .deleted-change-foreground, .inserted-change, .change-name-marker {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.change-name-marker {
|
||||
.name {
|
||||
font-size: 0.8em;
|
||||
padding: 2px 6px;
|
||||
.border-radius(3px 3px 3px 3px);
|
||||
position: absolute;
|
||||
border: 1px solid #999;
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
ul.change-list {
|
||||
li {
|
||||
padding: 6px 4px;
|
||||
position: relative;
|
||||
border-bottom: 1px solid #ccc;
|
||||
cursor: pointer;
|
||||
.change-selectors {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
left: 6px;
|
||||
.change-selector-from {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
.change-selector-to {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 20px;
|
||||
}
|
||||
}
|
||||
.change-description {
|
||||
padding-left: 42px;
|
||||
}
|
||||
.change-name {
|
||||
font-size: 0.9em;
|
||||
color: #666;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
.color-square {
|
||||
display: inline-block;
|
||||
height: 10px;
|
||||
width: 10px;
|
||||
margin-right: 4px;
|
||||
margin-bottom: -1px;
|
||||
}
|
||||
&:hover {
|
||||
background-color: #eaeaea;
|
||||
}
|
||||
}
|
||||
li.selected {
|
||||
background-color: #eaeaea;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -49,6 +49,7 @@
|
|||
@import "public/stylesheets/less/navbar.less";
|
||||
@import "public/stylesheets/less/footer.less";
|
||||
@import "public/stylesheets/less/list.less";
|
||||
@import "public/stylesheets/less/trackchanges.less";
|
||||
|
||||
|
||||
@import "public/stylesheets/less/fileuploader.less";
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
chai = require('chai')
|
||||
chai.should()
|
||||
sinon = require("sinon")
|
||||
modulePath = "../../../../app/js/Features/TrackChanges/TrackChangesController"
|
||||
SandboxedModule = require('sandboxed-module')
|
||||
|
||||
describe "TrackChangesController", ->
|
||||
beforeEach ->
|
||||
@TrackChangesController = SandboxedModule.require modulePath, requires:
|
||||
"request" : @request = {}
|
||||
"settings-sharelatex": @settings = {}
|
||||
"logger-sharelatex": @logger = {log: sinon.stub(), error: sinon.stub()}
|
||||
|
||||
describe "proxyToTrackChangesApi", ->
|
||||
beforeEach ->
|
||||
@req = { url: "/mock/url" }
|
||||
@res = "mock-res"
|
||||
@next = sinon.stub()
|
||||
@settings.apis =
|
||||
trackchanges:
|
||||
url: "http://trackchanges.example.com"
|
||||
@proxy =
|
||||
events: {}
|
||||
pipe: sinon.stub()
|
||||
on: (event, handler) -> @events[event] = handler
|
||||
@request.get = sinon.stub().returns @proxy
|
||||
@TrackChangesController.proxyToTrackChangesApi @req, @res, @next
|
||||
|
||||
describe "successfully", ->
|
||||
it "should call the track changes api", ->
|
||||
@request.get
|
||||
.calledWith("#{@settings.apis.trackchanges.url}#{@req.url}")
|
||||
.should.equal true
|
||||
|
||||
it "should pipe the response to the client", ->
|
||||
@proxy.pipe
|
||||
.calledWith(@res)
|
||||
.should.equal true
|
||||
|
||||
describe "with an error", ->
|
||||
beforeEach ->
|
||||
@proxy.events["error"].call(@proxy, @error = new Error("oops"))
|
||||
|
||||
it "should pass the error up the call chain", ->
|
||||
@next.calledWith(@error).should.equal true
|
||||
|
Loading…
Reference in a new issue