This commit is contained in:
Nick Winter 2014-05-08 11:36:26 -07:00
commit 775a2a87e2
14 changed files with 383 additions and 393 deletions

View file

@ -1,378 +0,0 @@
// There's no reason that this file is in JavaScript instead of CoffeeScript.
// We should convert it and update the brunch config.
// If we wanted to be more robust, we could use this: https://github.com/padolsey/operative/blob/master/src/operative.js
if(typeof window !== 'undefined' || !self.importScripts)
throw "Attempt to load worker_world into main window instead of web worker.";
// Taken from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/bind
// This is here for running simuations in enviroments lacking function.bind (PhantomJS mostly)
if (!Function.prototype.bind) {
Function.prototype.bind = function (oThis) {
if (typeof this !== "function") {
// closest thing possible to the ECMAScript 5 internal IsCallable function
throw new TypeError("Function.prototype.bind (Shim) - target is not callable");
}
var aArgs = Array.prototype.slice.call(arguments, 1),
fToBind = this,
fNOP = function () {},
fBound = function () {
return fToBind.apply(this instanceof fNOP && oThis
? this
: oThis,
aArgs.concat(Array.prototype.slice.call(arguments)));
};
fNOP.prototype = this.prototype;
fBound.prototype = new fNOP();
return fBound;
};
}
// assign global window so that Brunch's require (in world.js) can go into it
self.window = self;
self.workerID = "DebugWorker";
self.logLimit = 2000;
self.logsLogged = 0;
var console = {
log: function() {
if(self.logsLogged++ == self.logLimit)
self.postMessage({type: 'console-log', args: ["Log limit " + self.logLimit + " reached; shutting up."], id: self.workerID});
else if(self.logsLogged < self.logLimit) {
args = [].slice.call(arguments);
for(var i = 0; i < args.length; ++i) {
if(args[i] && args[i].constructor) {
if(args[i].constructor.className === "Thang" || args[i].isComponent)
args[i] = args[i].toString();
}
}
try {
self.postMessage({type: 'console-log', args: args, id: self.workerID});
}
catch(error) {
self.postMessage({type: 'console-log', args: ["Could not post log: " + args, error.toString(), error.stack, error.stackTrace], id: self.workerID});
}
}
}}; // so that we don't crash when debugging statements happen
console.error = console.info = console.log;
self.console = console;
// We could do way more from this: http://stackoverflow.com/questions/10653809/making-webworkers-a-safe-environment
Object.defineProperty(self, "XMLHttpRequest", {
get: function() { throw new Error("Access to XMLHttpRequest is forbidden."); },
configurable: false
});
self.transferableSupported = function transferableSupported() {
// Not in IE, even in IE 11
try {
var ab = new ArrayBuffer(1);
worker.postMessage(ab, [ab]);
return ab.byteLength == 0;
} catch(error) {
return false;
}
return false;
}
importScripts('/javascripts/world.js');
var World = self.require('lib/world/world');
var GoalManager = self.require('lib/world/GoalManager');
serializedClasses = {
"Thang": self.require('lib/world/thang'),
"Vector": self.require('lib/world/vector'),
"Rectangle": self.require('lib/world/rectangle')
}
self.getCurrentFrame = function getCurrentFrame(args) { return self.world.frames.length; };
//optimize this later
self.currentUserCodeMapCopy = {};
self.currentWorldFrame = 0;
self.maxSerializationDepth = 3;
self.stringifyValue = function(value, depth) {
var brackets, i, isArray, isObject, key, prefix, s, sep, size, v, values, _i, _j, _len, _len1, _ref, _ref1, _ref2, _ref3;
if (!value || _.isString(value)) {
return value;
}
if (_.isFunction(value)) {
if (depth === 2) {
return void 0;
} else {
return "<Function>";
}
}
if (value === this.thang && depth) {
return "<this " + value.id + ">";
}
if (depth === 2) {
if (((_ref = value.constructor) != null ? _ref.className : void 0) === "Thang") {
value = "<" + (value.type || value.spriteName) + " - " + value.id + ", " + (value.pos ? value.pos.toString() : 'non-physical') + ">";
} else {
value = value.toString();
}
return value;
}
isArray = _.isArray(value);
isObject = _.isObject(value);
if (!(isArray || isObject)) {
return value.toString();
}
brackets = isArray ? ["[", "]"] : ["{", "}"];
size = _.size(value);
if (!size) {
return brackets.join("");
}
values = [];
if (isArray) {
for (_i = 0, _len = value.length; _i < _len; _i++) {
v = value[_i];
s = this.stringifyValue(v, depth + 1);
if (s !== void 0) {
values.push("" + s);
}
}
} else {
_ref2 = (_ref1 = value.apiProperties) != null ? _ref1 : _.keys(value);
for (_j = 0, _len1 = _ref2.length; _j < _len1; _j++) {
key = _ref2[_j];
if (key[0] === "_") continue;
s = this.stringifyValue(value[key], depth + 1);
if (s !== void 0) {
values.push(key + ": " + s);
}
}
}
sep = '\n' + ((function() {
var _k, _results;
_results = [];
for (i = _k = 0; 0 <= depth ? _k < depth : _k > depth; i = 0 <= depth ? ++_k : --_k) {
_results.push(" ");
}
return _results;
})()).join('');
prefix = (_ref3 = value.constructor) != null ? _ref3.className : void 0;
if (isArray) {
if (prefix == null) {
prefix = "Array";
}
}
if (isObject) {
if (prefix == null) {
prefix = "Object";
}
}
prefix = prefix ? prefix + " " : "";
return "" + prefix + brackets[0] + sep + " " + (values.join(sep + ' ')) + sep + brackets[1];
};
var cache = {};
self.invalidateCache = function () {
cache = {};
};
self.retrieveValueFromCache = function (thangID, spellID, variableChain, frame) {
var frameCache, thangCache, spellCache;
if ((frameCache = cache[frame]) && (thangCache = frameCache[thangID]) && (spellCache = thangCache[spellID]))
return spellCache[variableChain.join()];
return undefined;
};
self.updateCache = function (thangID, spellID, variableChain, frame, value) {
var key, keys, currentObject;
keys = [frame,thangID, spellID, variableChain.join()];
currentObject = cache;
for (var i = 0, len = keys.length - 1; i < len; i++)
{
key = keys[i];
if (!(key in currentObject))
currentObject[key] = {};
currentObject = currentObject[key];
}
currentObject[keys[keys.length - 1]] = value;
};
self.retrieveValueFromFrame = function retrieveValueFromFrame(args) {
var cacheValue;
if (args.frame === self.currentWorldFrame && (cacheValue = self.retrieveValueFromCache(args.currentThangID, args.currentSpellID, args.variableChain, args.frame)))
return self.postMessage({type: 'debug-value-return', serialized: {"key": args.variableChain.join("."), "value": cacheValue}});
var retrieveProperty = function retrieveProperty(currentThangID, currentSpellID, variableChain)
{
var prop;
var value;
var keys = [];
for (var i = 0, len = variableChain.length; i < len; i++) {
prop = variableChain[i];
if (prop === "this")
{
value = self.world.thangMap[currentThangID];
}
else if (i === 0)
{
try
{
var flowStates = self.world.userCodeMap[currentThangID][currentSpellID].flow.states;
//we have to go to the second last flowState as we run the world for one additional frame
//to collect the flow
value = _.last(flowStates[flowStates.length - 2].statements).variables[prop];
}
catch (e)
{
value = undefined;
}
}
else
{
value = value[prop];
}
keys.push(prop);
if (!value) break;
var classOfValue;
if (classOfValue = serializedClasses[value.CN])
{
if (value.CN === "Thang")
{
var thang = self.world.thangMap[value.id];
value = thang || "<Thang " + value.id + " (non-existent)>"
}
else
{
value = classOfValue.deserializeFromAether(value);
}
}
}
var serializedProperty = {
"key": keys.join("."),
"value": self.stringifyValue(value,0)
};
self.updateCache(currentThangID,currentSpellID,variableChain, args.frame, serializedProperty.value);
self.postMessage({type: 'debug-value-return', serialized: serializedProperty});
};
self.enableFlowOnThangSpell(args.currentThangID, args.currentSpellID, args.userCodeMap);
self.setupWorldToRunUntilFrame(args);
self.world.loadFramesUntilFrame(
args.frame,
retrieveProperty.bind({},args.currentThangID, args.currentSpellID, args.variableChain),
self.onWorldError,
self.onWorldLoadProgress
);
};
self.enableFlowOnThangSpell = function enableFlowOnThang(thangID, spellID, userCodeMap) {
try {
if (userCodeMap[thangID][spellID].originalOptions.includeFlow === true &&
userCodeMap[thangID][spellID].originalOptions.noSerializationInFlow === true)
return;
else
{
userCodeMap[thangID][spellID].originalOptions.includeFlow = true;
userCodeMap[thangID][spellID].originalOptions.noSerializationInFlow = true;
var temporaryAether = Aether.deserialize(userCodeMap[thangID][spellID]);
temporaryAether.transpile(temporaryAether.raw);
userCodeMap[thangID][spellID] = temporaryAether.serialize();
}
}
catch (e) {
console.log("there was an error enabling flow on thang spell:" + e)
}
};
self.setupWorldToRunUntilFrame = function setupWorldToRunUntilFrame(args) {
self.postedErrors = {};
self.t0 = new Date();
self.firstWorld = args.firstWorld;
self.postedErrors = false;
self.logsLogged = 0;
var stringifiedUserCodeMap = JSON.stringify(args.userCodeMap);
var userCodeMapHasChanged = ! _.isEqual(self.currentUserCodeMapCopy, stringifiedUserCodeMap);
self.currentUserCodeMapCopy = stringifiedUserCodeMap;
if (!self.world || userCodeMapHasChanged || args.frame != self.currentWorldFrame) {
self.invalidateCache();
try {
self.world = new World(args.worldName, args.userCodeMap);
if (args.level)
self.world.loadFromLevel(args.level, true);
self.goalManager = new GoalManager(self.world);
self.goalManager.setGoals(args.goals);
self.goalManager.setCode(args.userCodeMap);
self.goalManager.worldGenerationWillBegin();
self.world.setGoalManager(self.goalManager);
}
catch (error) {
self.onWorldError(error);
return;
}
Math.random = self.world.rand.randf; // so user code is predictable
self.world.totalFrames = args.frame; //hack to work around error checking
self.currentWorldFrame = args.frame;
}
};
self.runWorldUntilFrame = function runWorldUntilFrame(args) {
self.setupWorldToRunUntilFrame(args);
self.world.loadFramesUntilFrame(args.frame, self.onWorldLoaded, self.onWorldError, self.onWorldLoadProgress);
};
self.onWorldLoaded = function onWorldLoaded() {
console.log("World loaded!");
};
self.onWorldError = function onWorldError(error) {
if(error instanceof Aether.problems.UserCodeProblem) {
if(!self.postedErrors[error.key]) {
var problem = error.serialize();
self.postMessage({type: 'user-code-problem', problem: problem});
self.postedErrors[error.key] = problem;
}
}
else {
console.log("Non-UserCodeError:", error.toString() + "\n" + error.stack || error.stackTrace);
}
/* We don't actually have the recoverable property any more; hmm
if(!self.firstWorld && !error.recoverable) {
self.abort();
return false;
}
*/
return true;
};
self.onWorldLoadProgress = function onWorldLoadProgress(progress) {
self.postMessage({type: 'world-load-progress-changed', progress: progress});
};
self.abort = function abort() {
if(self.world && self.world.name) {
console.log("About to abort:", self.world.name, typeof self.world.abort);
if(typeof self.world !== "undefined")
self.world.abort();
self.world = null;
}
self.postMessage({type: 'abort'});
};
self.reportIn = function reportIn() {
self.postMessage({type: 'reportIn'});
}
self.addEventListener('message', function(event) {
self[event.data.func](event.data.args);
});
self.postMessage({type: 'worker-initialized'});

View file

@ -82,6 +82,278 @@ self.transferableSupported = function transferableSupported() {
var World = self.require('lib/world/world');
var GoalManager = self.require('lib/world/GoalManager');
var serializedClasses = {
"Thang": self.require('lib/world/thang'),
"Vector": self.require('lib/world/vector'),
"Rectangle": self.require('lib/world/rectangle')
};
self.currentUserCodeMapCopy = "";
self.currentDebugWorldFrame = 0;
self.stringifyValue = function(value, depth) {
var brackets, i, isArray, isObject, key, prefix, s, sep, size, v, values, _i, _j, _len, _len1, _ref, _ref1, _ref2, _ref3;
if (!value || _.isString(value)) {
return value;
}
if (_.isFunction(value)) {
if (depth === 2) {
return void 0;
} else {
return "<Function>";
}
}
if (value === this.thang && depth) {
return "<this " + value.id + ">";
}
if (depth === 2) {
if (((_ref = value.constructor) != null ? _ref.className : void 0) === "Thang") {
value = "<" + (value.type || value.spriteName) + " - " + value.id + ", " + (value.pos ? value.pos.toString() : 'non-physical') + ">";
} else {
value = value.toString();
}
return value;
}
isArray = _.isArray(value);
isObject = _.isObject(value);
if (!(isArray || isObject)) {
return value.toString();
}
brackets = isArray ? ["[", "]"] : ["{", "}"];
size = _.size(value);
if (!size) {
return brackets.join("");
}
values = [];
if (isArray) {
for (_i = 0, _len = value.length; _i < _len; _i++) {
v = value[_i];
s = this.stringifyValue(v, depth + 1);
if (s !== void 0) {
values.push("" + s);
}
}
} else {
_ref2 = (_ref1 = value.apiProperties) != null ? _ref1 : _.keys(value);
for (_j = 0, _len1 = _ref2.length; _j < _len1; _j++) {
key = _ref2[_j];
if (key[0] === "_") continue;
s = this.stringifyValue(value[key], depth + 1);
if (s !== void 0) {
values.push(key + ": " + s);
}
}
}
sep = '\n' + ((function() {
var _k, _results;
_results = [];
for (i = _k = 0; 0 <= depth ? _k < depth : _k > depth; i = 0 <= depth ? ++_k : --_k) {
_results.push(" ");
}
return _results;
})()).join('');
prefix = (_ref3 = value.constructor) != null ? _ref3.className : void 0;
if (isArray) {
if (prefix == null) {
prefix = "Array";
}
}
if (isObject) {
if (prefix == null) {
prefix = "Object";
}
}
prefix = prefix ? prefix + " " : "";
return "" + prefix + brackets[0] + sep + " " + (values.join(sep + ' ')) + sep + brackets[1];
};
var cache = {};
self.invalidateCache = function () {
cache = {};
};
self.retrieveValueFromCache = function (thangID, spellID, variableChain, frame) {
var frameCache, thangCache, spellCache;
if ((frameCache = cache[frame]) && (thangCache = frameCache[thangID]) && (spellCache = thangCache[spellID]))
return spellCache[variableChain.join()];
return undefined;
};
self.updateCache = function (thangID, spellID, variableChain, frame, value) {
var key, keys, currentObject;
keys = [frame,thangID, spellID, variableChain.join()];
currentObject = cache;
for (var i = 0, len = keys.length - 1; i < len; i++)
{
key = keys[i];
if (!(key in currentObject))
currentObject[key] = {};
currentObject = currentObject[key];
}
currentObject[keys[keys.length - 1]] = value;
};
self.retrieveValueFromFrame = function retrieveValueFromFrame(args) {
var cacheValue;
if (args.frame === self.currentDebugWorldFrame && (cacheValue = self.retrieveValueFromCache(args.currentThangID, args.currentSpellID, args.variableChain, args.frame)))
return self.postMessage({type: 'debug-value-return', serialized: {"key": args.variableChain.join("."), "value": cacheValue}});
var retrieveProperty = function retrieveProperty(currentThangID, currentSpellID, variableChain)
{
var prop;
var value;
var keys = [];
for (var i = 0, len = variableChain.length; i < len; i++) {
prop = variableChain[i];
if (prop === "this")
{
value = self.debugWorld.thangMap[currentThangID];
}
else if (i === 0)
{
try
{
var flowStates = self.debugWorld.userCodeMap[currentThangID][currentSpellID].flow.states;
//we have to go to the second last flowState as we run the world for one additional frame
//to collect the flow
value = _.last(flowStates[flowStates.length - 2].statements).variables[prop];
}
catch (e)
{
value = undefined;
}
}
else
{
value = value[prop];
}
keys.push(prop);
if (!value) break;
var classOfValue;
if (classOfValue = serializedClasses[value.CN])
{
if (value.CN === "Thang")
{
var thang = self.debugWorld.thangMap[value.id];
value = thang || "<Thang " + value.id + " (non-existent)>"
}
else
{
value = classOfValue.deserializeFromAether(value);
}
}
}
var serializedProperty = {
"key": keys.join("."),
"value": self.stringifyValue(value,0)
};
self.updateCache(currentThangID,currentSpellID,variableChain, args.frame, serializedProperty.value);
self.postMessage({type: 'debug-value-return', serialized: serializedProperty});
};
self.enableFlowOnThangSpell(args.currentThangID, args.currentSpellID, args.userCodeMap);
self.setupDebugWorldToRunUntilFrame(args);
self.debugWorld.loadFramesUntilFrame(
args.frame,
retrieveProperty.bind({},args.currentThangID, args.currentSpellID, args.variableChain),
self.onDebugWorldError,
self.onDebugWorldProgress
);
};
self.enableFlowOnThangSpell = function (thangID, spellID, userCodeMap) {
try {
if (userCodeMap[thangID][spellID].originalOptions.includeFlow === true &&
userCodeMap[thangID][spellID].originalOptions.noSerializationInFlow === true)
return;
else
{
userCodeMap[thangID][spellID].originalOptions.includeFlow = true;
userCodeMap[thangID][spellID].originalOptions.noSerializationInFlow = true;
var temporaryAether = Aether.deserialize(userCodeMap[thangID][spellID]);
temporaryAether.transpile(temporaryAether.raw);
userCodeMap[thangID][spellID] = temporaryAether.serialize();
}
}
catch (e) {
console.log("there was an error enabling flow on thang spell:" + e)
}
};
self.setupDebugWorldToRunUntilFrame = function (args) {
self.debugPostedErrors = {};
self.debugt0 = new Date();
self.debugPostedErrors = false;
self.logsLogged = 0;
var stringifiedUserCodeMap = JSON.stringify(args.userCodeMap);
var userCodeMapHasChanged = ! _.isEqual(self.currentUserCodeMapCopy, stringifiedUserCodeMap);
self.currentUserCodeMapCopy = stringifiedUserCodeMap;
if (!self.debugWorld || userCodeMapHasChanged || args.frame != self.currentDebugWorldFrame) {
self.invalidateCache();
try {
self.debugWorld = new World(args.worldName, args.userCodeMap);
if (args.level)
self.debugWorld.loadFromLevel(args.level, true);
self.debugGoalManager = new GoalManager(self.debugWorld);
self.debugGoalManager.setGoals(args.goals);
self.debugGoalManager.setCode(args.userCodeMap);
self.debugGoalManager.worldGenerationWillBegin();
self.debugWorld.setGoalManager(self.debugGoalManager);
}
catch (error) {
self.onDebugWorldError(error);
return;
}
Math.random = self.debugWorld.rand.randf; // so user code is predictable
self.debugWorld.totalFrames = args.frame; //hack to work around error checking
self.currentDebugWorldFrame = args.frame;
}
};
self.runDebugWorldUntilFrame = function (args) {
self.setupDebugWorldToRunUntilFrame(args);
self.debugWorld.loadFramesUntilFrame(args.frame, self.onDebugWorldLoaded, self.onDebugWorldError, self.onDebugWorldProgress);
};
self.onDebugWorldLoaded = function onDebugWorldLoaded() {
console.log("World loaded!");
};
self.onDebugWorldError = function onDebugWorldError(error) {
if(error instanceof Aether.problems.UserCodeProblem) {
if(!self.debugPostedErrors[error.key]) {
var problem = error.serialize();
self.postMessage({type: 'user-code-problem', problem: problem});
self.debugPostedErrors[error.key] = problem;
}
}
else {
console.log("Non-UserCodeError:", error.toString() + "\n" + error.stack || error.stackTrace);
}
return true;
};
self.onDebugWorldProgress = function onDebugWorldProgress(progress) {
self.postMessage({type: 'debug-world-load-progress-changed', progress: progress});
};
self.debugAbort = function () {
if(self.debugWorld && self.debugWorld.name) {
console.log("About to abort:", self.debugWorld.name, typeof self.debugWorld.abort);
if(typeof self.debugWorld !== "undefined")
self.debugWorld.abort();
self.debugWorld = null;
}
self.postMessage({type: 'debugAbort'});
};
self.runWorld = function runWorld(args) {
self.postedErrors = {};

View file

@ -57,7 +57,7 @@ module.exports = class God
worker
createDebugWorker: ->
worker = new Worker '/javascripts/workers/worker_debug.js'
worker = new Worker '/javascripts/workers/worker_world.js'
worker.creationTime = new Date()
worker.addEventListener 'message', @onDebugWorkerMessage
worker
@ -146,7 +146,6 @@ module.exports = class God
worldName: @level.name
userCodeMap: @currentUserCodeMap
level: @level
firstWorld: @firstWorld
goals: @goalManager?.getGoals()
frame: args.frame
currentThangID: args.thangID

View file

@ -712,3 +712,14 @@
files: "Files"
top_simulators: "Top Simulators"
source_document: "Source Document"
document: "Document" # note to diplomats: not a physical document, a document in MongoDB, ie a record in a database
delta:
added: "Added"
modified: "Modified"
deleted: "Deleted"
moved_index: "Moved Index"
text_diff: "Text Diff"
merge_conflict_with: "MERGE CONFLICT WITH"
no_changes: "No Changes"

View file

@ -162,6 +162,10 @@ class CocoModel extends Backbone.Model
getDelta: ->
differ = deltasLib.makeJSONDiffer()
differ.diff @_revertAttributes, @attributes
getDeltaWith: (otherModel) ->
differ = deltasLib.makeJSONDiffer()
differ.diff @attributes, otherModel.attributes
applyDelta: (delta) ->
newAttributes = $.extend(true, {}, @attributes)
@ -172,6 +176,10 @@ class CocoModel extends Backbone.Model
delta = @getDelta()
deltasLib.expandDelta(delta, @_revertAttributes, @schema())
getExpandedDeltaWith: (otherModel) ->
delta = @getDeltaWith(otherModel)
deltasLib.expandDelta(delta, @attributes, @schema())
watch: (doWatch=true) ->
$.ajax("#{@urlRoot}/#{@id}/watch", {type:'PUT', data:{on:doWatch}})
@watching = -> doWatch

View file

@ -31,8 +31,9 @@ module.exports = class SuperModel extends Backbone.Model
else
@registerModel(model)
console.debug 'Registering model', model.getURL()
return @addModelResource(model, name, fetchOptions, value).load()
res = @addModelResource(model, name, fetchOptions, value)
if not res.isLoaded then res.load()
return res
loadCollection: (collection, name, fetchOptions, value=1) ->
url = collection.getURL()
@ -52,7 +53,9 @@ module.exports = class SuperModel extends Backbone.Model
@listenToOnce collection, 'sync', (c) ->
console.debug 'Registering collection', url
@registerCollection c
return @addModelResource(collection, name, fetchOptions, value).load()
res = @addModelResource(collection, name, fetchOptions, value)
res.load() if not (res.isLoading or res.isLoaded)
return res
# replace or overwrite
shouldSaveBackups: (model) -> false

View file

@ -37,10 +37,12 @@ mixin deltaPanel(delta, conflict)
if delta.conflict && !conflict
.panel-body
strong MERGE CONFLICT WITH
strong(data-i18n="delta.merge_conflict_with") MERGE CONFLICT WITH
+deltaPanel(delta.conflict, true)
.panel-group(id='delta-accordion-'+(counter))
for delta in deltas
+deltaPanel(delta)
if !deltas.length
alert.alert-warning(data-i18n="delta.no_changes") No changes

View file

@ -24,7 +24,7 @@ block header
li
a(href="#editor-level-settings-tab-view", data-toggle="tab", data-i18n="editor.level_tab_settings") Settings
li
a(href="#editor-level-components-tab-view", data-toggle="tab", data-i18n="editor.level_tab_components") Components
a(href="#editor-level-components-tab-view", data-toggle="tab", data-i18n="editor.level_tab_components")#components-tab Components
li
a(href="#editor-level-systems-tab-view", data-toggle="tab", data-i18n="editor.level_tab_systems") Systems
li

View file

@ -10,15 +10,21 @@ block modal-body-content
if dataList
table.table.table-condensed
tr
th
th(data-i18n="general.name") Name
th(data-i18n="general.version") Version
th(data-i18n="general.commit_msg") Commit Message
for data in dataList
tr
td
input(type="checkbox", value=data._id).select
td
a(href="/editor/#{page}/#{data.slug || data._id}")
| #{data.name}
td #{data.version.major}.#{data.version.minor}
td #{data.commitMessage}
div.delta-container
div.delta-view
block modal-footer-content

View file

@ -9,19 +9,63 @@ TEXTDIFF_OPTIONS =
viewType: 1
module.exports = class DeltaView extends CocoView
###
Takes a CocoModel instance (model) and displays changes since the
last save (attributes vs _revertAttributes).
* If headModel is included, will look for and display conflicts with the changes in model.
* If comparisonModel is included, will show deltas between model and comparisonModel instead
of changes within model itself.
###
@deltaCounter: 0
className: "delta-view"
template: template
constructor: (options) ->
super(options)
@model = options.model
@headModel = options.headModel
@expandedDeltas = @model.getExpandedDelta()
@expandedDeltas = []
@skipPaths = options.skipPaths
for modelName in ['model', 'headModel', 'comparisonModel']
continue unless m = options[modelName]
@[modelName] = @supermodel.loadModel(m, 'document').model
@buildDeltas() if @supermodel.finished()
console.log 'done constructing'
onLoaded: ->
@buildDeltas()
super()
buildDeltas: ->
if @comparisonModel
@expandedDeltas = @model.getExpandedDeltaWith(@comparisonModel)
else
@expandedDeltas = @model.getExpandedDelta()
@filterExpandedDeltas()
if @headModel
@headDeltas = @headModel.getExpandedDelta()
@conflicts = deltasLib.getConflicts(@headDeltas, @expandedDeltas)
filterExpandedDeltas: ->
return unless @skipPaths
for path, i in @skipPaths
@skipPaths[i] = [path] if _.isString(path)
newDeltas = []
for delta in @expandedDeltas
skip = false
for skipPath in @skipPaths
if _.isEqual _.first(delta.dataPath, skipPath.length), skipPath
skip = true
break
newDeltas.push delta unless skip
@expandedDeltas = newDeltas
getRenderData: ->
c = super()
c.deltas = @expandedDeltas

View file

@ -14,7 +14,6 @@ module.exports = class ComponentsTabView extends View
className: 'tab-pane'
subscriptions:
'level-thangs-changed': 'onLevelThangsChanged'
'edit-level-component': 'editLevelComponent'
'level-component-edited': 'onLevelComponentEdited'
'level-component-editing-ended': 'onLevelComponentEditingEnded'
@ -24,8 +23,8 @@ module.exports = class ComponentsTabView extends View
'click #create-new-component-button-no-select': 'createNewLevelComponent'
onLoaded: ->
onLevelThangsChanged: (e) ->
thangsData = e.thangsData
refreshLevelThangsTreema: (thangsData) ->
presentComponents = {}
for thang in thangsData
for component in thang.components

View file

@ -29,6 +29,7 @@ module.exports = class EditorLevelView extends View
'click #fork-level-start-button': 'startForkingLevel'
'click #level-history-button': 'showVersionHistory'
'click #patches-tab': -> @patchesView.load()
'click #components-tab': -> @componentsTab.refreshLevelThangsTreema @level.get('thangs')
'click #level-patch-button': 'startPatchingLevel'
'click #level-watch-button': 'toggleWatchLevel'
'click #pop-level-i18n-button': -> @level.populateI18N()

View file

@ -50,7 +50,9 @@ module.exports = class EmployersView extends View
renderCandidatesAndSetupScrolling: =>
@render()
$(".nano").nanoScroller()
if window.location.hash.length is 25
if window.history?.state?.lastViewedCandidateID
$(".nano").nanoScroller({scrollTo:$("#" + window.history.state.lastViewedCandidateID)})
else if window.location.hash.length is 25
$(".nano").nanoScroller({scrollTo:$(window.location.hash)})
sortTable: ->
@ -175,8 +177,13 @@ module.exports = class EmployersView extends View
onCandidateClicked: (e) ->
id = $(e.target).closest('tr').data('candidate-id')
window.location.hash = id
if id
if window.history
oldState = _.cloneDeep window.history.state ? {}
oldState["lastViewedCandidateID"] = id
window.history.replaceState(oldState,"")
else
window.location.hash = id
url = "/account/profile/#{id}"
app.router.navigate url, {trigger: true}
else

View file

@ -1,6 +1,7 @@
ModalView = require 'views/kinds/ModalView'
template = require 'templates/modal/versions'
tableTemplate = require 'templates/kinds/table'
DeltaView = require 'views/editor/delta'
class VersionsViewCollection extends Backbone.Collection
url: ""
@ -20,6 +21,9 @@ module.exports = class VersionsModalView extends ModalView
id: ""
url: ""
page: ""
events:
'change input.select': 'onSelectionChanged'
constructor: (options, @ID, @model) ->
super options
@ -36,6 +40,18 @@ module.exports = class VersionsModalView extends ModalView
@startsLoading = false
@render()
onSelectionChanged: ->
rows = @$el.find 'input.select:checked'
deltaEl = @$el.find '.delta-view'
@deltaView?.destroy()
deltaEl.empty()
if rows.length isnt 2 then return
laterVersion = new @model(_id:$(rows[0]).val())
earlierVersion = new @model(_id:$(rows[1]).val())
@deltaView = new DeltaView({model:earlierVersion, comparisonModel:laterVersion, skipPaths:['_id','version', 'commitMessage', 'parent', 'created', 'slug', 'index']})
@insertSubView(@deltaView, deltaEl)
getRenderData: (context={}) ->
context = super(context)
context.page = @page