'[% ctx.media_prefix %]/js/dojo/opensrf/md5.js',
'[% ctx.media_prefix %]/js/ui/default/staff/build/js/moment-with-locales.min.js',
'[% ctx.media_prefix %]/js/ui/default/staff/build/js/moment-timezone-with-data.min.js',
+ '[% ctx.media_prefix %]/js/ui/default/staff/build/js/lovefield.min.js',
'[% ctx.media_prefix %]/js/ui/default/common/build/js/jquery.min.js',
'[% ctx.media_prefix %]/js/ui/default/staff/build/js/vendor.bundle.js',
'[% ctx.media_prefix %]/js/ui/default/staff/build/fonts/glyphicons-halflings-regular.woff',
<script src="[% ctx.media_prefix %]/js/ui/default/staff/build/js/moment-timezone-with-data.min.js"></script>
<!--
+ Load the lovefield libs as a standaline file so both the main
+ application and the offline shared worker can reference (and cache)
+ the same file
+-->
+<script src="[% ctx.media_prefix %]/js/ui/default/staff/build/js/lovefield.min.js"></script>
+
+<!--
Load iframeResize via script tag
https://bugs.launchpad.net/evergreen/+bug/1753008
-->
--- /dev/null
+importScripts('/js/ui/default/staff/build/js/lovefield.min.js');
+
+// Collection of schema tracking objects.
+var schemas = {};
+
+// Create the DB schema / tables
+// synchronous
+function createSchema(schemaName) {
+ if (schemas[schemaName]) return;
+
+ var meta = lf.schema.create(schemaName, 2);
+ schemas[schemaName] = {name: schemaName, meta: meta};
+
+ switch (schemaName) {
+ case 'cache':
+ createCacheTables(meta);
+ break;
+ case 'offline':
+ createOfflineTables(meta);
+ break;
+ default:
+ console.error('No schema definition for ' + schemaName);
+ }
+}
+
+// Offline cache tables are globally available in the staff client
+// for on-demand caching.
+function createCacheTables(meta) {
+
+ meta.createTable('Setting').
+ addColumn('name', lf.Type.STRING).
+ addColumn('value', lf.Type.STRING).
+ addPrimaryKey(['name']);
+
+ meta.createTable('Object').
+ addColumn('type', lf.Type.STRING). // class hint
+ addColumn('id', lf.Type.STRING). // obj id
+ addColumn('object', lf.Type.OBJECT).
+ addPrimaryKey(['type','id']);
+
+ meta.createTable('CacheDate').
+ addColumn('type', lf.Type.STRING). // class hint
+ addColumn('cachedate', lf.Type.DATE_TIME). // when was it last updated
+ addPrimaryKey(['type']);
+
+ meta.createTable('StatCat').
+ addColumn('id', lf.Type.INTEGER).
+ addColumn('value', lf.Type.OBJECT).
+ addPrimaryKey(['id']);
+}
+
+// Offline transaction and block list tables. These can be bulky and
+// are only used in the offline UI.
+function createOfflineTables(meta) {
+
+ meta.createTable('OfflineXact').
+ addColumn('seq', lf.Type.INTEGER).
+ addColumn('value', lf.Type.OBJECT).
+ addPrimaryKey(['seq'], true);
+
+ meta.createTable('OfflineBlocks').
+ addColumn('barcode', lf.Type.STRING).
+ addColumn('reason', lf.Type.STRING).
+ addPrimaryKey(['barcode']);
+}
+
+// Connect to the database for a given schema
+function connect(schemaName) {
+
+ var schema = schemas[schemaName];
+ if (!schema) {
+ return Promise.reject('createSchema(' +
+ schemaName + ') call required');
+ }
+
+ if (schema.db) { // already connected.
+ return Promise.resolve();
+ }
+
+ return new Promise(function(resolve, reject) {
+ try {
+ schema.meta.connect().then(
+ function(db) {
+ schema.db = db;
+ resolve();
+ },
+ function(err) {
+ reject('Error connecting to schema ' +
+ schemaName + ' : ' + err);
+ }
+ );
+ } catch (E) {
+ reject('Error connecting to schema ' + schemaName + ' : ' + E);
+ }
+ });
+}
+
+function getTableInfo(schemaName, tableName) {
+ var schema = schemas[schemaName];
+ var info = {};
+
+ if (!schema) {
+ info.error = 'createSchema(' + schemaName + ') call required';
+
+ } else if (!schema.db) {
+ info.error = 'connect(' + schemaName + ') call required';
+
+ } else {
+ info.schema = schema;
+ info.table = schema.meta.getSchema().table(tableName);
+
+ if (!info.table) {
+ info.error = 'no such table ' + tableName;
+ }
+ }
+
+ return info;
+}
+
+// Returns a promise resolved with true on success
+// Note insert .exec() returns rows, but that can get bulky on large
+// inserts, hence the boolean return;
+function insertOrReplace(schemaName, tableName, objects) {
+
+ var info = getTableInfo(schemaName, tableName);
+ if (info.error) { return Promise.reject(info.error); }
+
+ var rows = objects.map(function(r) { return info.table.createRow(r) });
+ return info.schema.db.insertOrReplace().into(info.table)
+ .values(rows).exec().then(function() { return true; });
+}
+
+// Returns a promise resolved with true on success
+// Note insert .exec() returns rows, but that can get bulky on large
+// inserts, hence the boolean return;
+function insert(schemaName, tableName, objects) {
+
+ var info = getTableInfo(schemaName, tableName);
+ if (info.error) { return Promise.reject(info.error); }
+
+ var rows = objects.map(function(r) { return info.table.createRow(r) });
+ return info.schema.db.insert().into(info.table)
+ .values(rows).exec().then(function() { return true; });
+}
+
+// Returns rows where the selected field equals the provided value.
+function selectWhereEqual(schemaName, tableName, field, value) {
+
+ var info = getTableInfo(schemaName, tableName);
+ if (info.error) { return Promise.reject(info.error); }
+
+ return info.schema.db.select().from(info.table)
+ .where(info.table[field].eq(value)).exec();
+}
+
+// Returns rows where the selected field equals the provided value.
+function selectWhereIn(schemaName, tableName, field, value) {
+
+ var info = getTableInfo(schemaName, tableName);
+ if (info.error) { return Promise.reject(info.error); }
+
+ return info.schema.db.select().from(info.table)
+ .where(info.table[field].in(value)).exec();
+}
+
+// Returns all rows in the selected table
+function selectAll(schemaName, tableName) {
+
+ var info = getTableInfo(schemaName, tableName);
+ if (info.error) { return Promise.reject(info.error); }
+
+ return info.schema.db.select().from(info.table).exec();
+}
+
+// Deletes all rows in the selected table.
+function deleteAll(schemaName, tableName) {
+
+ var info = getTableInfo(schemaName, tableName);
+ if (info.error) { return Promise.reject(info.error); }
+
+ return info.schema.db.delete().from(info.table).exec();
+}
+
+// Resolves to true if the selected table contains any rows.
+function hasRows(schemaName, tableName) {
+
+ var info = getTableInfo(schemaName, tableName);
+ if (info.error) { return Promise.reject(info.error); }
+
+ return info.schema.db.select().from(info.table).limit(1).exec()
+ .then(function(rows) { return rows.length > 0 });
+}
+
+
+// Prevent parallel block list building calls, since it does a lot.
+var buildingBlockList = false;
+
+// Fetches the offline block list and rebuilds the offline blocks
+// table from the new data.
+function populateBlockList(authtoken) {
+ if (buildingBlockList) return;
+ buildingBlockList = true;
+
+ var url = '/standalone/list.txt?ses=' +
+ authtoken + '&' + new Date().getTime();
+
+ console.debug('Fetching offline block list from: ' + url);
+
+ return new Promise(function(resolve, reject) {
+
+ var xhttp = new XMLHttpRequest();
+ xhttp.onreadystatechange = function() {
+ if (this.readyState === 4) {
+ if (this.status === 200) {
+ var blocks = xhttp.responseText;
+ var lines = blocks.split('\n');
+ insertOfflineBlocks(lines).then(
+ function() {
+ buildingBlockList = false;
+ resolve();
+ },
+ function(e) {
+ buildingBlockList = false;
+ reject(e);
+ }
+ );
+ } else {
+ reject('Error fetching offline block list');
+ }
+ }
+ };
+
+ xhttp.open('GET', url, true);
+ xhttp.send();
+ });
+}
+
+// Rebuild the offline blocks table with the provided blocks, one per line.
+function insertOfflineBlocks(lines) {
+ console.debug('Fetched ' + lines.length + ' blocks');
+
+ // Clear the table first
+ return deleteAll('offline', 'OfflineBlocks').then(
+ function() {
+
+ console.debug('Cleared existing offline blocks');
+
+ // Create a single batch of rows for insertion.
+ var chunks = [];
+ var currentChunk = [];
+ var chunkSize = 10000;
+ var seen = {bc: {}}; // for easier delete
+
+ chunks.push(currentChunk);
+ lines.forEach(function(line) {
+ // slice/substring instead of split(' ') to handle barcodes
+ // with trailing spaces.
+ var barcode = line.slice(0, -2);
+ var reason = line.substring(line.length - 1);
+
+ // Trim duplicate barcodes, since only one version of each
+ // block per barcode is kept in the offline block list
+ if (seen.bc[barcode]) return;
+ seen.bc[barcode] = true;
+
+ if (currentChunk.length >= chunkSize) {
+ currentChunk = [];
+ chunks.push(currentChunk);
+ }
+
+ currentChunk.push({barcode: barcode, reason: reason});
+ });
+
+ delete seen.bc; // allow this hunk to be reclaimed
+
+ console.debug('offline data broken into ' +
+ chunks.length + ' chunks of size ' + chunkSize);
+
+ return new Promise(function(resolve, reject) {
+ insertOfflineChunks(chunks, 0, resolve, reject);
+ });
+ },
+
+ function(err) {
+ console.error('Error clearing offline table: ' + err);
+ return Promise.reject(err);
+ }
+ );
+}
+
+function insertOfflineChunks(chunks, offset, resolve, reject) {
+ var chunk = chunks[offset];
+ if (!chunk || chunk.length === 0) {
+ console.debug('Block list successfully stored');
+ return resolve();
+ }
+
+ insertOrReplace('offline', 'OfflineBlocks', chunk).then(
+ function() {
+ console.debug('Block list successfully stored chunk ' + offset);
+ insertOfflineChunks(chunks, offset + 1, resolve, reject);
+ },
+ reject
+ );
+}
+
+
+// Routes inbound WebWorker message to the correct handler.
+// Replies include the original request plus added response info.
+function dispatchRequest(port, data) {
+
+ console.debug('Lovefield worker received',
+ 'action=' + (data.action || ''),
+ 'schema=' + (data.schema || ''),
+ 'table=' + (data.table || ''),
+ 'field=' + (data.field || ''),
+ 'value=' + (data.value || '')
+ );
+
+ function replySuccess(result) {
+ data.status = 'OK';
+ data.result = result;
+ port.postMessage(data);
+ }
+
+ function replyError(err) {
+ console.error('shared worker replying with error', err);
+ data.status = 'ERR';
+ data.error = err;
+ port.postMessage(data);
+ }
+
+ switch (data.action) {
+ case 'createSchema':
+ // Schema creation is synchronous and apparently throws
+ // no exceptions, at least until connect() is called.
+ createSchema(data.schema);
+ replySuccess();
+ break;
+
+ case 'connect':
+ connect(data.schema).then(replySuccess, replyError);
+ break;
+
+ case 'insertOrReplace':
+ insertOrReplace(data.schema, data.table, data.rows)
+ .then(replySuccess, replyError);
+ break;
+
+ case 'insert':
+ insert(data.schema, data.table, data.rows)
+ .then(replySuccess, replyError);
+ break;
+
+ case 'selectWhereEqual':
+ selectWhereEqual(data.schema, data.table, data.field, data.value)
+ .then(replySuccess, replyError);
+ break;
+
+ case 'selectWhereIn':
+ selectWhereIn(data.schema, data.table, data.field, data.value)
+ .then(replySuccess, replyError);
+ break;
+
+ case 'selectAll':
+ selectAll(data.schema, data.table).then(replySuccess, replyError);
+ break;
+
+ case 'deleteAll':
+ deleteAll(data.schema, data.table).then(replySuccess, replyError);
+ break;
+
+ case 'hasRows':
+ hasRows(data.schema, data.table).then(replySuccess, replyError);
+ break;
+
+ case 'populateBlockList':
+ populateBlockList(data.authtoken).then(replySuccess, replyError);
+ break;
+
+ default:
+ console.error('no such DB action ' + data.action);
+ }
+}
+
+onconnect = function(e) {
+ var port = e.ports[0];
+ port.addEventListener('message',
+ function(e) {dispatchRequest(port, e.data);});
+ port.start();
+}
+
+
+
* Route resolvers allow us to run async commands
* before the page controller is instantiated.
*/
- var resolver = {delay : ['egCore',
- function(egCore) {
+ var resolver = {delay : ['egCore', 'egLovefield',
+ function(egCore, egLovefield) {
+ // the 'offline' schema is only active in the offline UI.
+ egLovefield.activeSchemas.push('offline');
return egCore.startup.go();
}
]};
])
.controller('OfflineCtrl',
- ['$q','$scope','$window','$location','$rootScope','egCore','egLovefield','$routeParams','$timeout','$http','ngToast','egConfirmDialog','egUnloadPrompt',
- function($q , $scope , $window , $location , $rootScope , egCore , egLovefield , $routeParams , $timeout , $http , ngToast , egConfirmDialog , egUnloadPrompt) {
+ ['$q','$scope','$window','$location','$rootScope','egCore',
+ 'egLovefield','$routeParams','$timeout','$http','ngToast',
+ 'egConfirmDialog','egUnloadPrompt','egProgressDialog',
+ function($q , $scope , $window , $location , $rootScope , egCore ,
+ egLovefield , $routeParams , $timeout , $http , ngToast ,
+ egConfirmDialog , egUnloadPrompt, egProgressDialog) {
// Immediately redirect if we're really offline
if (!$window.navigator.onLine) {
});
$scope.downloadBlockList = function () {
- var url = '/standalone/list.txt?ses='
- + egCore.auth.token()
- + '&' + new Date().getTime();
- return $http.get(url).then(
- function (res) {
- if (res.data) {
- var lines = res.data.split('\n');
- egLovefield.destroyOfflineBlocks().then(function(){
- angular.forEach(lines, function (l) {
- var parts = l.split(' ');
- egLovefield.addOfflineBlock(parts[0], parts[1]);
- });
- return $q.when();
- }).then(function(){
- ngToast.create(egCore.strings.OFFLINE_BLOCKLIST_SUCCESS);
- });
- }
- },function(){
+ egProgressDialog.open();
+ egLovefield.populateBlockList().then(
+ function(){
+ ngToast.create(egCore.strings.OFFLINE_BLOCKLIST_SUCCESS);
+ },
+ function(){
ngToast.warning(egCore.strings.OFFLINE_BLOCKLIST_FAIL);
egCore.audio.play('warning.offline.blocklist_fail');
}
- );
+ )['finally'](egProgressDialog.close);
}
$scope.createOfflineXactBlob = function () {
return egLovefield.reconstituteList('asva');
}).then(function() {
angular.forEach(egCore.env.asv.list, function (s) {
- s.questions( egCore.env.asva.list.filter( function (a) {
+ s.questions( egCore.env.asvq.list.filter( function (q) {
return q.survey().id == s.id();
}));
});
-var osb = lf.schema.create('offline', 2);
-
-osb.createTable('Object').
- addColumn('type', lf.Type.STRING). // class hint
- addColumn('id', lf.Type.STRING). // obj id
- addColumn('object', lf.Type.OBJECT).
- addPrimaryKey(['type','id']);
-
-osb.createTable('CacheDate').
- addColumn('type', lf.Type.STRING). // class hint
- addColumn('cachedate', lf.Type.DATE_TIME). // when was it last updated
- addPrimaryKey(['type']);
-
-osb.createTable('Setting').
- addColumn('name', lf.Type.STRING).
- addColumn('value', lf.Type.STRING).
- addPrimaryKey(['name']);
-
-osb.createTable('StatCat').
- addColumn('id', lf.Type.INTEGER).
- addColumn('value', lf.Type.OBJECT).
- addPrimaryKey(['id']);
-
-osb.createTable('OfflineXact').
- addColumn('seq', lf.Type.INTEGER).
- addColumn('value', lf.Type.OBJECT).
- addPrimaryKey(['seq'], true);
-
-osb.createTable('OfflineBlocks').
- addColumn('barcode', lf.Type.STRING).
- addColumn('reason', lf.Type.STRING).
- addPrimaryKey(['barcode']);
-
/**
* Core Service - egLovefield
*
.factory('egLovefield', ['$q','$rootScope','egCore','$timeout',
function($q , $rootScope , egCore , $timeout) {
- var service = {};
+ var service = {
+ autoId: 0, // each request gets a unique id.
+ cannotConnect: false,
+ pendingRequests: [],
+ activeSchemas: ['cache'], // add 'offline' in the offline UI
+ schemasInProgress: {},
+ connectedSchemas: [],
+ // TODO: relative path would be more portable
+ workerUrl: '/js/ui/default/staff/offline-db-worker.js'
+ };
- function connectOrGo() {
+ service.connectToWorker = function() {
+ if (service.worker) return;
- if (lf.offlineDB) { // offline DB connected
- return $q.when();
+ try {
+ // relative path would be better...
+ service.worker = new SharedWorker(service.workerUrl);
+ } catch (E) {
+ console.error('SharedWorker() not supported', E);
+ service.cannotConnect = true;
+ return;
}
- if (service.cannotConnect) { // connection will never happen
- return $q.reject();
+ service.worker.onerror = function(err) {
+ console.error('Error loading shared worker', err);
+ service.cannotConnect = true;
}
- if (service.connectPromise) { // connection in progress
- return service.connectPromise;
- }
+ // List for responses and resolve the matching pending request.
+ service.worker.port.addEventListener('message', function(evt) {
+ var response = evt.data;
+ var reqId = response.id;
+ var req = service.pendingRequests.filter(
+ function(r) { return r.id === reqId})[0];
- // start a new connection attempt
-
- var deferred = $q.defer();
+ if (!req) {
+ console.error('Recieved response for unknown request ' + reqId);
+ return;
+ }
- //console.debug('attempting offline DB connection');
- try {
- osb.connect().then(
- function(db) {
- console.debug('successfully connected to offline DB');
- service.connectPromise = null;
- lf.offlineDB = db;
- deferred.resolve();
- },
- function(err) {
- // assumes that a single connection failure means
- // a connection will never succeed.
- service.cannotConnect = true;
- console.error('Cannot connect to offline DB: ' + err);
- }
- );
- } catch (e) {
- // .connect() will throw an error if it detects that a connection
- // attempt is already in progress; this can happen with PhantomJS
- console.error('Cannot connect to offline DB: ' + e);
- service.cannotConnect = true;
- }
+ if (response.status === 'OK') {
+ req.deferred.resolve(response.result);
+ } else {
+ console.error('worker request failed with ' + response.error);
+ req.deferred.reject(response.error);
+ }
+ });
- service.connectPromise = deferred.promise;
- return service.connectPromise;
+ service.worker.port.start();
}
- service.isCacheGood = function (type) {
+ service.connectToSchemas = function() {
- return connectOrGo().then(function() {
- var cacheDate = lf.offlineDB.getSchema().table('CacheDate');
+ if (service.cannotConnect) {
+ // This can happen in certain environments
+ return $q.reject();
+ }
+
+ service.connectToWorker(); // no-op if already connected
- return lf.offlineDB.
- select(cacheDate.cachedate).
- from(cacheDate).
- where(cacheDate.type.eq(type)).
- exec().then(function(results) {
- if (results.length == 0) {
- return $q.when(false);
- }
+ var promises = [];
- var now = new Date();
-
- // hard-coded 1 day offline cache timeout
- return $q.when((now.getTime() - results[0]['cachedate'].getTime()) <= 86400000);
- })
+ service.activeSchemas.forEach(function(schema) {
+ promises.push(service.connectToSchema(schema));
});
+
+ return $q.all(promises).then(
+ function() {},
+ function() {service.cannotConnect = true}
+ );
+ }
+
+ // Connects if necessary to the active schemas then relays the request.
+ service.request = function(args) {
+ return service.connectToSchemas().then(
+ function() {
+ return service.relayRequest(args);
+ }
+ );
+ }
+
+ // Send a request to the web worker and register the request for
+ // future resolution.
+ // Store the request ID in the request arguments, so it's included
+ // in the response, and in the pendingRequests list for linking.
+ service.relayRequest = function(args) {
+ var deferred = $q.defer();
+ var reqId = service.autoId++;
+ args.id = reqId;
+ service.pendingRequests.push({id : reqId, deferred: deferred});
+ service.worker.port.postMessage(args);
+ return deferred.promise;
+ }
+
+ // Create and connect to the give schema
+ service.connectToSchema = function(schema) {
+
+ if (service.connectedSchemas.includes(schema)) {
+ // already connected
+ return $q.when();
+ }
+
+ if (service.schemasInProgress[schema]) {
+ return service.schemasInProgress[schema];
+ }
+
+ var deferred = $q.defer();
+
+ service.relayRequest(
+ {schema: schema, action: 'createSchema'})
+ .then(
+ function() {
+ return service.relayRequest(
+ {schema: schema, action: 'connect'});
+ },
+ deferred.reject
+ ).then(
+ function() {
+ service.connectedSchemas.push(schema);
+ delete service.schemasInProgress[schema];
+ deferred.resolve();
+ },
+ deferred.reject
+ );
+
+ return service.schemasInProgress[schema] = deferred.promise;
+ }
+
+ service.isCacheGood = function (type) {
+ return service.request({
+ schema: 'cache',
+ table: 'CacheDate',
+ action: 'selectWhereEqual',
+ field: 'type',
+ value: type
+ }).then(
+ function(result) {
+ var row = result[0];
+ if (!row) { return false; }
+ // hard-coded 1 day offline cache timeout
+ return (new Date().getTime() - row.cachedate.getTime()) <= 86400000;
+ }
+ );
}
service.destroyPendingOfflineXacts = function () {
- return connectOrGo().then(function() {
- var table = lf.offlineDB.getSchema().table('OfflineXact');
- return lf.offlineDB.
- delete().
- from(table).
- exec();
+ return service.request({
+ schema: 'offline',
+ table: 'OfflineXact',
+ action: 'deleteAll'
});
}
service.havePendingOfflineXacts = function () {
- return connectOrGo().then(function() {
- var table = lf.offlineDB.getSchema().table('OfflineXact');
- return lf.offlineDB.
- select(table.reason).
- from(table).
- exec().
- then(function(list) {
- return $q.when(Boolean(list.length > 0))
- });
+ return service.request({
+ schema: 'offline',
+ table: 'OfflineXact',
+ action: 'hasRows'
});
}
service.retrievePendingOfflineXacts = function () {
- return connectOrGo().then(function() {
- var table = lf.offlineDB.getSchema().table('OfflineXact');
- return lf.offlineDB.
- select(table.value).
- from(table).
- exec().
- then(function(list) {
- return $q.when(list.map(function(x) { return x.value }))
- });
- });
- }
-
- service.destroyOfflineBlocks = function () {
- return connectOrGo().then(function() {
- var table = lf.offlineDB.getSchema().table('OfflineBlocks');
- return $q.when(
- lf.offlineDB.
- delete().
- from(table).
- exec()
- );
+ return service.request({
+ schema: 'offline',
+ table: 'OfflineXact',
+ action: 'selectAll'
+ }).then(function(resp) {
+ return resp.map(function(x) { return x.value });
});
}
- service.addOfflineBlock = function (barcode, reason) {
- return connectOrGo().then(function() {
- var table = lf.offlineDB.getSchema().table('OfflineBlocks');
- return $q.when(
- lf.offlineDB.
- insertOrReplace().
- into(table).
- values([ table.createRow({ barcode : barcode, reason : reason }) ]).
- exec()
- );
+ service.populateBlockList = function() {
+ return service.request({
+ action: 'populateBlockList',
+ authtoken: egCore.auth.token()
});
}
// Returns a promise with true for blocked, false for not blocked
service.testOfflineBlock = function (barcode) {
- return connectOrGo().then(function() {
- var table = lf.offlineDB.getSchema().table('OfflineBlocks');
- return lf.offlineDB.
- select(table.reason).
- from(table).
- where(table.barcode.eq(barcode)).
- exec().then(function(list) {
- if(list.length > 0) return $q.when(list[0].reason);
- return $q.when(null);
- });
+ return service.request({
+ schema: 'offline',
+ table: 'OfflineBlocks',
+ action: 'selectWhereEqual',
+ field: 'barcode',
+ value: barcode
+ }).then(function(resp) {
+ if (resp.length === 0) return null;
+ return resp[0].reason;
});
}
service.addOfflineXact = function (obj) {
- return connectOrGo().then(function() {
- var table = lf.offlineDB.getSchema().table('OfflineXact');
- return $q.when(
- lf.offlineDB.
- insertOrReplace().
- into(table).
- values([ table.createRow({ value : obj }) ]).
- exec()
- );
+ return service.request({
+ schema: 'offline',
+ table: 'OfflineXact',
+ action: 'insertOrReplace',
+ rows: [{value: obj}]
});
}
service.setStatCatsCache = function (statcats) {
- if (lf.isOffline) return $q.when();
+ if (lf.isOffline || !statcats || statcats.length === 0)
+ return $q.when();
- return connectOrGo().then(function() {
- var table = lf.offlineDB.getSchema().table('StatCat');
- var rlist = [];
+ var rows = statcats.map(function(cat) {
+ return {id: cat.id(), value: egCore.idl.toHash(cat)}
+ });
- angular.forEach(statcats, function (val) {
- rlist.push(table.createRow({
- id : val.id(),
- value : egCore.idl.toHash(val)
- }));
- });
- return lf.offlineDB.
- insertOrReplace().
- into(table).
- values(rlist).
- exec();
+ return service.request({
+ schema: 'cache',
+ table: 'StatCat',
+ action: 'insertOrReplace',
+ rows: rows
});
}
service.getStatCatsCache = function () {
- return connectOrGo().then(function() {
- var table = lf.offlineDB.getSchema().table('StatCat');
+ return service.request({
+ schema: 'cache',
+ table: 'StatCat',
+ action: 'selectAll'
+ }).then(function(list) {
var result = [];
- return lf.offlineDB.
- select(table.value).
- from(table).
- exec().then(function(list) {
- angular.forEach(list, function (s) {
- var sc = egCore.idl.fromHash('actsc', s.value);
-
- if (angular.isArray(sc.default_entries())) {
- sc.default_entries(
- sc.default_entries().map( function (k) {
- return egCore.idl.fromHash('actsced', k);
- })
- );
- }
-
- if (angular.isArray(sc.entries())) {
- sc.entries(
- sc.entries().map( function (k) {
- return egCore.idl.fromHash('actsce', k);
- })
- );
- }
-
- result.push(sc);
- });
- return $q.when(result);
- });
-
+ list.forEach(function(s) {
+ var sc = egCore.idl.fromHash('actsc', s.value);
+
+ if (Array.isArray(sc.default_entries())) {
+ sc.default_entries(
+ sc.default_entries().map( function (k) {
+ return egCore.idl.fromHash('actsced', k);
+ })
+ );
+ }
+
+ if (Array.isArray(sc.entries())) {
+ sc.entries(
+ sc.entries().map( function (k) {
+ return egCore.idl.fromHash('actsce', k);
+ })
+ );
+ }
+
+ result.push(sc);
+ });
+
+ return result;
});
}
service.setSettingsCache = function (settings) {
if (lf.isOffline) return $q.when();
- return connectOrGo().then(function() {
-
- var table = lf.offlineDB.getSchema().table('Setting');
- var rlist = [];
-
- angular.forEach(settings, function (val, key) {
- rlist.push(
- table.createRow({
- name : key,
- value : JSON.stringify(val)
- })
- );
- });
+ var rows = [];
+ angular.forEach(settings, function (val, key) {
+ rows.push({name : key, value : JSON.stringify(val)});
+ });
- return lf.offlineDB.
- insertOrReplace().
- into(table).
- values(rlist).
- exec();
+ return service.request({
+ schema: 'cache',
+ table: 'Setting',
+ action: 'insertOrReplace',
+ rows: rows
});
}
service.getSettingsCache = function (settings) {
- return connectOrGo().then(function() {
- var table = lf.offlineDB.getSchema().table('Setting');
+ var promise;
+
+ if (settings && settings.length) {
+ promise = service.request({
+ schema: 'cache',
+ table: 'Setting',
+ action: 'selectWhereIn',
+ field: 'name',
+ value: settings
+ });
+ } else {
+ promise = service.request({
+ schema: 'cache',
+ table: 'Setting',
+ action: 'selectAll'
+ });
+ }
- var search_pred = table.name.isNotNull();
- if (settings && settings.length) {
- search_pred = table.name.in(settings);
+ return promise.then(
+ function(resp) {
+ resp.forEach(function(s) { s.value = JSON.parse(s.value); });
+ return resp;
}
-
- return lf.offlineDB.
- select(table.name, table.value).
- from(table).
- where(search_pred).
- exec().then(function(list) {
- angular.forEach(list, function (s) {
- s.value = JSON.parse(s.value)
- });
- return $q.when(list);
- });
- });
+ );
}
service.setListInOfflineCache = function (type, list) {
if (lf.isOffline) return $q.when();
- return connectOrGo().then(function() {
+ return service.isCacheGood(type).then(function(good) {
+ if (good) { return }; // already cached
- service.isCacheGood(type).then(function(good) {
- if (!good) {
- var object = lf.offlineDB.getSchema().table('Object');
- var cacheDate = lf.offlineDB.getSchema().table('CacheDate');
- var pkey = egCore.idl.classes[type].pkey;
-
- angular.forEach(list, function(item) {
- var row = object.createRow({
- type : type,
- id : '' + item[pkey](),
- object : egCore.idl.toHash(item)
- });
- lf.offlineDB.insertOrReplace().into(object).values([row]).exec();
- });
-
- var row = cacheDate.createRow({
- type : type,
- cachedate : new Date()
- });
-
- console.log('egLovefield saving ' + type + ' list');
- lf.offlineDB.insertOrReplace().into(cacheDate).values([row]).exec();
- }
- })
+ var pkey = egCore.idl.classes[type].pkey;
+ var rows = Object.values(list).map(function(item) {
+ return {
+ type: type,
+ id: '' + item[pkey](),
+ object: egCore.idl.toHash(item)
+ };
+ });
+
+ return service.request({
+ schema: 'cache',
+ table: 'Object',
+ action: 'insertOrReplace',
+ rows: rows
+ }).then(function(resp) {
+ return service.request({
+ schema: 'cache',
+ table: 'CacheDate',
+ action: 'insertOrReplace',
+ rows: [{type: type, cachedate : new Date()}]
+ });
+ });
});
}
service.getListFromOfflineCache = function(type) {
- return connectOrGo().then(function() {
-
- var object = lf.offlineDB.getSchema().table('Object');
-
- return lf.offlineDB.
- select(object.object).
- from(object).
- where(object.type.eq(type)).
- exec().then(function(results) {
- return $q.when(results.map(function(item) {
- return egCore.idl.fromHash(type,item['object'])
- }));
- });
+ return service.request({
+ schema: 'cache',
+ table: 'Object',
+ action: 'selectWhereEqual',
+ field: 'type',
+ value: type
+ }).then(function(resp) {
+ return resp.map(function(item) {
+ return egCore.idl.fromHash(type,item['object']);
+ });
});
}
service.reconstituteList = function(type) {
if (lf.isOffline) {
- console.log('egLovefield reading ' + type + ' list');
+ console.debug('egLovefield reading ' + type + ' list');
return service.getListFromOfflineCache(type).then(function (list) {
egCore.env.absorbList(list, type, true)
return $q.when(true);
service.reconstituteTree = function(type) {
if (lf.isOffline) {
- console.log('egLovefield reading ' + type + ' tree');
+ console.debug('egLovefield reading ' + type + ' tree');
var pkey = egCore.idl.classes[type].pkey;
var parent_field = 'parent';
'./node_modules/moment/min/moment-with-locales.min.js',
'./node_modules/moment-timezone/builds/moment-timezone-with-data.min.js',
'./node_modules/iframe-resizer/js/iframeResizer.contentWindow.min.js',
- './node_modules/iframe-resizer/js/iframeResizer.min.js'
+ './node_modules/iframe-resizer/js/iframeResizer.min.js',
+ // lovefield is loaded from multiple locations. Make it stand-alone
+ // so we only need a single copy.
+ './node_modules/lovefield/dist/lovefield.min.js'
]
'angular-tree-control',
'angular-tree-control/context-menu.js',
'angular-order-object-by',
- 'lovefield',
'angular-tablesort'
];