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:
Dan Stillman 2006-06-22 14:01:54 +00:00
parent 1b74d0b04a
commit 726364d091
4 changed files with 464 additions and 4 deletions

View 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']);
}
}

View file

@ -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();
}
}

View file

@ -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");

View file

@ -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');