Scholar.History -- i.e. undo/redo functionality
Partially integrated into data layer, but I'm waiting to commit that part until I'm sure it won't break everything
This commit is contained in:
parent
1b74d0b04a
commit
726364d091
4 changed files with 464 additions and 4 deletions
429
chrome/chromeFiles/content/scholar/xpcom/history.js
Normal file
429
chrome/chromeFiles/content/scholar/xpcom/history.js
Normal file
|
@ -0,0 +1,429 @@
|
|||
Scholar.History = new function(){
|
||||
this.begin = begin;
|
||||
this.add = add;
|
||||
this.modify = modify;
|
||||
this.remove = remove;
|
||||
this.commit = commit;
|
||||
this.cancel = cancel;
|
||||
this.getPreviousEvent = getPreviousEvent;
|
||||
this.getNextEvent = getNextEvent;
|
||||
this.undo = undo;
|
||||
this.redo = redo;
|
||||
this.clear = clear;
|
||||
this.clearAfter = clearAfter;
|
||||
|
||||
var _firstTime = true;
|
||||
var _currentID = 0;
|
||||
var _activeID;
|
||||
var _activeEvent;
|
||||
var _maxID = 0;
|
||||
|
||||
// event: ('item-add', 'item-delete', 'item-modify', 'collection-add', 'collection-modify', 'collection-delete')
|
||||
// context: (itemCreators.itemID-creatorID.1-1)
|
||||
// action: ('add', 'delete', 'modify')
|
||||
|
||||
/**
|
||||
* Begin a transaction set
|
||||
**/
|
||||
function begin(event, id){
|
||||
if (_activeID){
|
||||
throw('History transaction set already in progress');
|
||||
}
|
||||
|
||||
// If running for the first time this session or we're in the middle of
|
||||
// the history, clear any transaction sets after the current position
|
||||
if (_firstTime || _currentID<_maxID){
|
||||
_firstTime = false;
|
||||
this.clearAfter();
|
||||
}
|
||||
|
||||
Scholar.debug('Beginning history transaction set ' + event);
|
||||
var sql = "INSERT INTO transactionSets (event, id) VALUES "
|
||||
+ "('" + event + "', ";
|
||||
// If integer, insert natively; if array, insert as string
|
||||
sql += (typeof id=='object') ? "'" + id.join('-') + "'" : id;
|
||||
sql += ")";
|
||||
|
||||
Scholar.DB.beginTransaction();
|
||||
_activeID = Scholar.DB.query(sql);
|
||||
_activeEvent = event;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Add an add transaction to the current set
|
||||
**/
|
||||
function add(table, key, keyValues){
|
||||
return _addTransaction('add', table, key, keyValues);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Add a modify transaction to the current set
|
||||
*
|
||||
* _field_ is optional -- otherwise all fields are saved
|
||||
**/
|
||||
function modify(table, key, keyValues, field){
|
||||
return _addTransaction('modify', table, key, keyValues, field);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Add a remove transaction to the current set
|
||||
**/
|
||||
function remove(table, key, keyValues){
|
||||
return _addTransaction('remove', table, key, keyValues);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Commit the current transaction set
|
||||
**/
|
||||
function commit(){
|
||||
Scholar.debug('Committing history transaction set ' + _activeEvent);
|
||||
Scholar.DB.commitTransaction();
|
||||
_currentID = _activeID;
|
||||
_maxID = _activeID;
|
||||
_activeID = null;
|
||||
_activeEvent = null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Cancel the current transaction set
|
||||
**/
|
||||
function cancel(){
|
||||
Scholar.debug('Cancelling history transaction set ' + _activeEvent);
|
||||
Scholar.DB.rollbackTransaction();
|
||||
_activeID = null;
|
||||
_activeEvent = null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the next event to undo, or false if none
|
||||
**/
|
||||
function getPreviousEvent(){
|
||||
if (!_currentID){
|
||||
return false;
|
||||
}
|
||||
|
||||
var sql = "SELECT event FROM transactionSets WHERE transactionSetID="
|
||||
+ _currentID;
|
||||
return Scholar.DB.valueQuery(sql);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the next event to redo, or false if none
|
||||
**/
|
||||
function getNextEvent(){
|
||||
var sql = "SELECT event FROM transactionSets WHERE transactionSetID="
|
||||
+ (_currentID + 1);
|
||||
return Scholar.DB.valueQuery(sql);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Undo the last transaction set
|
||||
**/
|
||||
function undo(){
|
||||
if (!_currentID){
|
||||
throw('No transaction set to undo');
|
||||
return false;
|
||||
}
|
||||
|
||||
var id = _currentID;
|
||||
Scholar.debug('Undoing transaction set ' + id);
|
||||
Scholar.DB.beginTransaction();
|
||||
var undone = _do('undo');
|
||||
_currentID--;
|
||||
Scholar.DB.commitTransaction();
|
||||
_notifyEvent(id);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Redo the next transaction set
|
||||
**/
|
||||
function redo(){
|
||||
var id = _currentID + 1;
|
||||
Scholar.debug('Redoing transaction set ' + id);
|
||||
Scholar.DB.beginTransaction();
|
||||
var redone = _do('redo');
|
||||
_currentID++;
|
||||
Scholar.DB.commitTransaction();
|
||||
_notifyEvent(id);
|
||||
return redone;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Clear the entire history
|
||||
**/
|
||||
function clear(){
|
||||
Scholar.DB.beginTransaction();
|
||||
Scholar.DB.query("DELETE FROM transactionSets");
|
||||
Scholar.DB.query("DELETE FROM transactions");
|
||||
Scholar.DB.query("DELETE FROM transactionLog");
|
||||
_currentID = null;
|
||||
_activeID = null;
|
||||
_activeEvent = null;
|
||||
_maxID = null;
|
||||
Scholar.DB.commitTransaction();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Clear all transactions in history after the current one
|
||||
**/
|
||||
function clearAfter(){
|
||||
Scholar.DB.beginTransaction();
|
||||
var min = Scholar.DB.valueQuery("SELECT MIN(transactionID) FROM "
|
||||
+ "transactions WHERE transactionSetID=" + (_currentID + 1));
|
||||
|
||||
if (!min){
|
||||
Scholar.DB.rollbackTransaction();
|
||||
return;
|
||||
}
|
||||
|
||||
Scholar.DB.query("DELETE FROM transactionLog "
|
||||
+ "WHERE transactionID>=" + min);
|
||||
Scholar.DB.query("DELETE FROM transactions "
|
||||
+ "WHERE transactionID>=" + min);
|
||||
Scholar.DB.query("DELETE FROM transactionSets "
|
||||
+ "WHERE transactionSetID>" + _currentID);
|
||||
|
||||
_maxID = _currentID;
|
||||
_activeID = null;
|
||||
Scholar.DB.commitTransaction();
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
// Private methods
|
||||
//
|
||||
|
||||
function _addTransaction(action, table, key, keyValues, field){
|
||||
if (!_activeID){
|
||||
throw('Cannot add history transaction with no transaction set in progress');
|
||||
}
|
||||
|
||||
if (typeof keyValues == 'object'){
|
||||
keyValues = keyValues.join('-');
|
||||
}
|
||||
|
||||
var contextString = table + '.' + key + '.' + keyValues;
|
||||
var context = _parseContext(contextString);
|
||||
var fromClause = _contextToSQLFrom(context);
|
||||
|
||||
var sql = "INSERT INTO transactions (transactionSetID, context, action) "
|
||||
+ "VALUES (" + _activeID + ", '" + contextString
|
||||
+ "', '" + action + "')";
|
||||
|
||||
var transactionID = Scholar.DB.query(sql);
|
||||
|
||||
switch (action){
|
||||
case 'add':
|
||||
// No need to store an add, since we'll just delete it to reverse
|
||||
break;
|
||||
case 'modify':
|
||||
// Only save one field -- _do() won't know about this, but the
|
||||
// UPDATE statements on the other fields just won't do anything
|
||||
if (field){
|
||||
var sql = "INSERT INTO transactionLog SELECT " + transactionID
|
||||
+ ", '" + field + "', " + field + fromClause;
|
||||
Scholar.DB.query(sql);
|
||||
break;
|
||||
}
|
||||
// Fall through if no field specified and save all
|
||||
case 'remove':
|
||||
var cols = Scholar.DB.getColumns(table);
|
||||
for (var i in cols){
|
||||
// If column is not part of the key, log it
|
||||
if (!Scholar.inArray(cols[i], context['keys'])){
|
||||
var sql = "INSERT INTO transactionLog "
|
||||
+ "SELECT " + transactionID + ", '" + cols[i]
|
||||
+ "', " + cols[i] + fromClause;
|
||||
Scholar.DB.query(sql);
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
Scholar.DB.rollbackTransaction();
|
||||
throw("Invalid history action '" + action + "'");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function _do(mode){
|
||||
switch (mode){
|
||||
case 'undo':
|
||||
var id = _currentID;
|
||||
break;
|
||||
case 'redo':
|
||||
var id = _currentID + 1;
|
||||
break;
|
||||
}
|
||||
|
||||
var sql = "SELECT transactionID, context, action FROM transactions "
|
||||
+ "WHERE transactionSetID=" + id;
|
||||
var transactions = Scholar.DB.query(sql);
|
||||
|
||||
if (!transactions){
|
||||
throw('Transaction set not found for '
|
||||
+ (mode=='undo' ? 'current' : 'next') + id);
|
||||
}
|
||||
|
||||
for (var i in transactions){
|
||||
var transactionID = transactions[i]['transactionID'];
|
||||
var context = _parseContext(transactions[i]['context']);
|
||||
|
||||
// If in redo mode, swap 'add' and 'remove'
|
||||
if (mode=='redo'){
|
||||
switch (transactions[i]['action']){
|
||||
case 'add':
|
||||
transactions[i]['action'] = 'remove';
|
||||
break;
|
||||
case 'remove':
|
||||
transactions[i]['action'] = 'add';
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
switch (transactions[i]['action']){
|
||||
case 'add':
|
||||
var fromClause = _contextToSQLFrom(context);
|
||||
|
||||
// First, store the row we're about to delete for later redo
|
||||
var cols = Scholar.DB.getColumns(context['table']);
|
||||
for (var i in cols){
|
||||
// If column is not part of the key, log it
|
||||
if (!Scholar.inArray(cols[i], context['keys'])){
|
||||
var sql = "INSERT INTO transactionLog "
|
||||
+ "SELECT " + transactionID + ", '" + cols[i]
|
||||
+ "', " + cols[i] + fromClause;
|
||||
Scholar.DB.query(sql);
|
||||
}
|
||||
}
|
||||
|
||||
// And delete the row
|
||||
var sql = "DELETE" + fromClause;
|
||||
Scholar.DB.query(sql);
|
||||
break;
|
||||
|
||||
case 'modify':
|
||||
// Retrieve old values
|
||||
var sql = "SELECT field, value FROM transactionLog "
|
||||
+ "WHERE transactionID=" + transactionID;
|
||||
var oldFieldValues = Scholar.DB.query(sql);
|
||||
|
||||
// Retrieve new values
|
||||
var sql = "SELECT *" + _contextToSQLFrom(context);
|
||||
var newValues = Scholar.DB.rowQuery(sql);
|
||||
|
||||
// Update row with old values
|
||||
var sql = "UPDATE " + context['table'] + " SET ";
|
||||
var values = [];
|
||||
for (var i in oldFieldValues){
|
||||
sql += oldFieldValues[i]['field'] + '=?, ';
|
||||
values.push(oldFieldValues[i]['value']);
|
||||
}
|
||||
sql = sql.substr(0, sql.length-2) + _contextToSQLWhere(context);
|
||||
Scholar.DB.query(sql, values);
|
||||
|
||||
// Update log with new values for later redo
|
||||
for (var i in newValues){
|
||||
if (!Scholar.inArray(i, context['keys'])){
|
||||
var sql = "UPDATE transactionLog SET "
|
||||
+ "value=? WHERE transactionID=? AND field=?";
|
||||
Scholar.DB.query(sql, [i, newValues[i], transactionID]);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 'remove':
|
||||
// Retrieve old values
|
||||
var sql = "SELECT field, value FROM transactionLog "
|
||||
+ "WHERE transactionID=" + transactionID;
|
||||
var oldFieldValues = Scholar.DB.query(sql);
|
||||
|
||||
// Add key to parameters
|
||||
var fields = [], values = [], marks = [];
|
||||
for (var i=0; i<context['keys'].length; i++){
|
||||
fields.push(context['keys'][i]);
|
||||
values.push(context['values'][i]);
|
||||
marks.push('?');
|
||||
}
|
||||
|
||||
// Add other fields to parameters
|
||||
for (var i in oldFieldValues){
|
||||
fields.push(oldFieldValues[i]['field']);
|
||||
values.push(oldFieldValues[i]['value']);
|
||||
marks.push('?');
|
||||
}
|
||||
|
||||
// Insert old values into table
|
||||
var sql = "INSERT INTO " + context['table'] + "("
|
||||
+ fields.join() + ") VALUES (" + marks.join() + ")";
|
||||
Scholar.DB.query(sql, values);
|
||||
|
||||
// Delete restored data from transactionLog
|
||||
var sql = "DELETE FROM transactionLog WHERE transactionID="
|
||||
+ transactionID;
|
||||
Scholar.DB.query(sql);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function _parseContext(context){
|
||||
var parts = context.split('.');
|
||||
var parsed = {
|
||||
table:parts[0],
|
||||
keys:parts[1].split('-'),
|
||||
values:parts[2].split('-')
|
||||
}
|
||||
if (parsed['keys'].length!=parsed['values'].length){
|
||||
throw("Different number of keys and values in _parseContext('"
|
||||
+ context + "')");
|
||||
}
|
||||
|
||||
return parsed;
|
||||
}
|
||||
|
||||
|
||||
function _contextToSQLFrom(parsed){
|
||||
return " FROM " + parsed['table'] + _contextToSQLWhere(parsed);
|
||||
}
|
||||
|
||||
|
||||
function _contextToSQLWhere(parsed){
|
||||
var sql = " WHERE ";
|
||||
for (var i=0; i<parsed['keys'].length; i++){
|
||||
// DEBUG: type?
|
||||
sql += parsed['keys'][i] + "='" + parsed['values'][i] + "' AND ";
|
||||
}
|
||||
return sql.substr(0, sql.length-5);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the ids associated with a particular transaction set
|
||||
**/
|
||||
function _getSetData(transactionSetID){
|
||||
var sql = "SELECT event, id FROM transactionSets WHERE transactionSetID="
|
||||
+ transactionSetID;
|
||||
return Scholar.DB.rowQuery(sql);
|
||||
}
|
||||
|
||||
|
||||
function _notifyEvent(transactionSetID){
|
||||
var data = _getSetData(transactionSetID);
|
||||
var eventParts = data['event'].split('-'); // e.g. modify-item
|
||||
Scholar.Notifier.trigger(eventParts[0], eventParts[1], data['id']);
|
||||
}
|
||||
}
|
|
@ -58,7 +58,7 @@ Scholar.Schema = new function(){
|
|||
|
||||
// If enough time hasn't passed and it's not being forced, don't update
|
||||
if (!force && now < nextCheck){
|
||||
Scholar.debug('Too soon since last update -- not checking repository', 4);
|
||||
Scholar.debug('Not enough time since last update -- not checking repository', 4);
|
||||
// Set the repository timer to the remaining time
|
||||
_setRepositoryTimer(Math.round((nextCheck.getTime() - now.getTime()) / 1000));
|
||||
return false;
|
||||
|
@ -370,7 +370,7 @@ Scholar.Schema = new function(){
|
|||
//
|
||||
// Change this value to match the schema version
|
||||
//
|
||||
var toVersion = 21;
|
||||
var toVersion = 22;
|
||||
|
||||
if (toVersion != _getSchemaSQLVersion()){
|
||||
throw('Schema version does not match version in _migrateSchema()');
|
||||
|
@ -385,7 +385,7 @@ Scholar.Schema = new function(){
|
|||
// Each block performs the changes necessary to move from the
|
||||
// previous revision to that one.
|
||||
for (var i=parseInt(fromVersion) + 1; i<=toVersion; i++){
|
||||
if (i==21){
|
||||
if (i==22){
|
||||
_initializeSchema();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -34,6 +34,10 @@ Cc["@mozilla.org/moz/jssubscript-loader;1"]
|
|||
.getService(Ci.mozIJSSubScriptLoader)
|
||||
.loadSubScript("chrome://scholar/content/xpcom/notifier.js");
|
||||
|
||||
Cc["@mozilla.org/moz/jssubscript-loader;1"]
|
||||
.getService(Ci.mozIJSSubScriptLoader)
|
||||
.loadSubScript("chrome://scholar/content/xpcom/history.js");
|
||||
|
||||
Cc["@mozilla.org/moz/jssubscript-loader;1"]
|
||||
.getService(Ci.mozIJSSubScriptLoader)
|
||||
.loadSubScript("chrome://scholar/content/xpcom/ingester.js");
|
||||
|
|
29
schema.sql
29
schema.sql
|
@ -1,4 +1,4 @@
|
|||
-- 21
|
||||
-- 22
|
||||
|
||||
DROP TABLE IF EXISTS version;
|
||||
CREATE TABLE version (
|
||||
|
@ -151,6 +151,33 @@
|
|||
);
|
||||
|
||||
|
||||
DROP TABLE IF EXISTS transactionSets;
|
||||
CREATE TABLE transactionSets (
|
||||
transactionSetID INTEGER PRIMARY KEY,
|
||||
event TEXT,
|
||||
id INT
|
||||
);
|
||||
|
||||
DROP TABLE IF EXISTS transactions;
|
||||
CREATE TABLE transactions (
|
||||
transactionID INTEGER PRIMARY KEY,
|
||||
transactionSetID INT,
|
||||
context TEXT,
|
||||
action TEXT
|
||||
);
|
||||
DROP INDEX IF EXISTS transactions_transactionSetID;
|
||||
CREATE INDEX transactions_transactionSetID ON transactions(transactionSetID);
|
||||
|
||||
DROP TABLE IF EXISTS transactionLog;
|
||||
CREATE TABLE transactionLog (
|
||||
transactionID INT,
|
||||
field TEXT,
|
||||
value NONE,
|
||||
PRIMARY KEY (transactionID, field, value),
|
||||
FOREIGN KEY (transactionID) REFERENCES transactions(transactionID)
|
||||
);
|
||||
|
||||
|
||||
-- Some sample data
|
||||
INSERT INTO itemTypes VALUES (1,'book');
|
||||
INSERT INTO itemTypes VALUES (2,'journalArticle');
|
||||
|
|
Loading…
Reference in a new issue