diff --git a/app/lib/deltas.coffee b/app/lib/deltas.coffee new file mode 100644 index 000000000..abf3dddf5 --- /dev/null +++ b/app/lib/deltas.coffee @@ -0,0 +1,89 @@ +# path: an array of indexes to navigate into a JSON object +# left: + +module.exports.interpretDelta = (delta, path, left, schema) -> + # takes a single delta and converts into an object that can be + # easily formatted into something human readable. + + betterDelta = { action:'???', delta: delta } + + betterPath = [] + parentLeft = left + parentSchema = schema + for key in path + # TODO: A smarter way of getting child schemas + childSchema = parentSchema?.items or parentSchema?.properties?[key] or {} + childLeft = parentLeft?[key] + betterKey = null + betterKey ?= childLeft.name or childLeft.id if childLeft + betterKey ?= "#{childSchema.title} ##{key+1}" if childSchema.title and _.isNumber(key) + betterKey ?= "#{childSchema.title}" if childSchema.title + betterKey ?= _.string.titleize key + betterPath.push betterKey + parentLeft = childLeft + parentSchema = childSchema + + betterDelta.path = betterPath.join(' :: ') + betterDelta.schema = childSchema + betterDelta.left = childLeft + betterDelta.right = jsondiffpatch.patch childLeft, delta + + if _.isArray(delta) and delta.length is 1 + betterDelta.action = 'added' + betterDelta.newValue = delta[0] + + if _.isArray(delta) and delta.length is 2 + betterDelta.action = 'modified' + betterDelta.oldValue = delta[0] + betterDelta.newValue = delta[1] + + if _.isArray(delta) and delta.length is 3 and delta[1] is 0 and delta[2] is 0 + betterDelta.action = 'deleted' + betterDelta.oldValue = delta[0] + + if _.isPlainObject(delta) and delta._t is 'a' + betterDelta.action = 'modified-array' + + if _.isPlainObject(delta) and delta._t isnt 'a' + betterDelta.action = 'modified-object' + + if _.isArray(delta) and delta.length is 3 and delta[1] is 0 and delta[2] is 3 + betterDelta.action = 'moved-index' + betterDelta.destinationIndex = delta[1] + + if _.isArray(delta) and delta.length is 3 and delta[1] is 0 and delta[2] is 2 + betterDelta.action = 'text-diff' + betterDelta.unidiff = delta[0] + left = betterDelta.left.trim().split('\n') + right = betterDelta.right.trim().split('\n') + shifted = popped = false + while left.length > 5 and right.length > 5 and left[0] is right[0] and left[1] is right[1] + left.shift() + right.shift() + shifted = true + while left.length > 5 and right.length > 5 and left[left.length-1] is right[right.length-1] and left[left.length-2] is right[right.length-2] + left.pop() + right.pop() + popped = true + left.push('...') and right.push('...') if popped + left.unshift('...') and right.unshift('...') if shifted + betterDelta.trimmedLeft = left.join('\n') + betterDelta.trimmedRight = right.join('\n') + + + betterDelta + +module.exports.flattenDelta = flattenDelta = (delta, path=null) -> + # takes a single delta and returns an array of deltas + + path ?= [] + + return [{path:path, delta:delta}] if _.isArray delta + + results = [] + affectingArray = delta._t is 'a' + for index, childDelta of delta + continue if index is '_t' + index = parseInt(index.replace('_', '')) if affectingArray + results = results.concat flattenDelta(childDelta, path.concat([index])) + results \ No newline at end of file diff --git a/app/models/CocoModel.coffee b/app/models/CocoModel.coffee index 862ba72fd..6edd0d634 100644 --- a/app/models/CocoModel.coffee +++ b/app/models/CocoModel.coffee @@ -215,6 +215,11 @@ class CocoModel extends Backbone.Model return true if permission.access in ['owner', 'write'] return false - + + getDelta: -> + jsd = jsondiffpatch.create({ + objectHash: (obj) -> obj.name || obj.id || obj._id || JSON.stringify(_.keys(obj)) + }) + jsd.diff @_revertAttributes, @attributes module.exports = CocoModel diff --git a/app/styles/editor/delta.sass b/app/styles/editor/delta.sass new file mode 100644 index 000000000..b7c4cdd56 --- /dev/null +++ b/app/styles/editor/delta.sass @@ -0,0 +1,35 @@ +#delta-list-view + width: 600px + .panel-heading + font-size: 13px + padding: 4px + .row + padding: 5px 10px + + .delta-added + border-color: green + strong + color: green + .panel-heading + background-color: lighten(green, 70%) + + .delta-modified + border-color: darkgoldenrod + strong + color: darkgoldenrod + .panel-heading + background-color: lighten(darkgoldenrod, 40%) + + .delta-text-diff + border-color: blue + strong + color: blue + .panel-heading + background-color: lighten(blue, 45%) + + .delta-deleted + border-color: red + strong + color: red + .panel-heading + background-color: lighten(red, 42%) diff --git a/app/templates/editor/delta.jade b/app/templates/editor/delta.jade new file mode 100644 index 000000000..33b85ba15 --- /dev/null +++ b/app/templates/editor/delta.jade @@ -0,0 +1,35 @@ +- var i = 0 +.panel-group#accordion + for delta in deltas + .delta.panel.panel-default(class='delta-'+delta.action) + .panel-heading + if delta.action === 'added' + strong(data-i18n="delta.added") Added + if delta.action === 'modified' + strong(data-i18n="delta.modified") Modified + if delta.action === 'deleted' + strong(data-i18n="delta.deleted") Deleted + if delta.action === 'moved-index' + strong(data-i18n="delta.modified_array") Moved Index + if delta.action === 'text-diff' + strong(data-i18n="delta.text_diff") Text Diff + span + a(data-toggle="collapse" data-parent="#accordion" href="#collapse-"+i) + span= delta.path + + .panel-collapse.collapse(id="collapse-"+i) + .panel-body.row + if delta.action === 'added' + .new-value.col-md-12= delta.right + if delta.action === 'modified' + .old-value.col-md-6= delta.left + .new-value.col-md-6= delta.right + if delta.action === 'deleted' + .col-md-12 + div.old-value= delta.left + if delta.action === 'text-diff' + .col-md-6 + pre= delta.trimmedLeft + .col-md-6 + pre= delta.trimmedRight + - i += 1 \ No newline at end of file diff --git a/app/views/editor/delta.coffee b/app/views/editor/delta.coffee new file mode 100644 index 000000000..46c637f36 --- /dev/null +++ b/app/views/editor/delta.coffee @@ -0,0 +1,43 @@ +CocoView = require 'views/kinds/CocoView' +template = require 'templates/editor/delta' +deltaLib = require 'lib/deltas' + +module.exports = class DeltaListView extends CocoView + id: "delta-list-view" + template: template + + constructor: (options) -> + super(options) + @delta = options.delta + @schema = options.schema or {} + @left = options.left + + getRenderData: -> + c = super() + deltas = deltaLib.flattenDelta @delta + deltas = (deltaLib.interpretDelta(d.delta, d.path, @left, @schema) for d in deltas) + c.deltas = deltas + @processedDeltas = deltas + c + + afterRender: -> + deltas = @$el.find('.delta') + for delta, i in deltas + deltaEl = $(delta) + deltaData = @processedDeltas[i] + console.log 'delta', deltaEl, deltaData + if _.isObject(deltaData.left) and leftEl = deltaEl.find('.old-value') + options = + data: deltaData.left + schema: deltaData.schema + readOnly: true + treema = TreemaNode.make(leftEl, options) + treema.build() + + if _.isObject(deltaData.right) and rightEl = deltaEl.find('.old-value') + options = + data: deltaData.right + schema: deltaData.schema + readOnly: true + treema = TreemaNode.make(rightEl, options) + treema.build() diff --git a/bower.json b/bower.json index 0a6b42f4a..84e10d876 100644 --- a/bower.json +++ b/bower.json @@ -52,6 +52,9 @@ }, "underscore.string": { "main": "lib/underscore.string.js" + }, + "jsondiffpatch": { + "main": ["build/bundle-full.js", "build/formatters.js", "src/formatters/html.css"] } } }