espresso-block
Version:
A component of Espresso Grid
685 lines (639 loc) • 21 kB
JavaScript
Application.controller('GridController', [
'$rootScope', '$scope', '$http', 'Transit', 'Data', '$timeout', 'Templates', '$modal',
function ($rootScope, $scope, $http, Transit, Data, $timeout, Templates, $modal) {
//interfaces exposed to the user, and potentially firable from the container
$rootScope.controls = {};
//flags and data used by the template, but not displayed
$rootScope.params = {};
//a reference to the original objects requested by the server
$scope.originalData = [];
//grid data array
$scope.data = [];
//table definitions from an @tables/{endpoint} request, w/ added $scope.table.named attribute(columns index by name)
$scope.table = {};
//search filters array, contains filter objects: {columns:{},operator:{},text:''}
$rootScope.filters = [];
//operators applied to the text of a search filter
$rootScope.operators = {
contains: {
label: 'contains',
prefix: ' LIKE "%',
suffix: '%"',
},
starts: {
label: 'starts with',
prefix: ' LIKE "',
suffix: '%"',
},
ends: {
label: 'ends with',
prefix: ' LIKE "%',
suffix: '"',
},
greater: {
label: 'greater than',
prefix: '>',
suffix: '',
},
less: {
label: 'less than',
prefix: '<',
suffix: '',
},
equals: {
label: 'equals',
prefix: '=\'',
suffix: '\'',
},
not: {
label: 'not equal',
prefix: '!=\'',
suffix: '\'',
}};
$scope.$watch('data', function (current) {
if (current && !angular.equals(current, [])) {
if ($scope.isPaginated(current)) {
var nextBatch = $scope.getPaginationObj(current);
$scope.setNextBatch(nextBatch);
}
else {
$scope.setNextBatch(false);
}
}
});
$scope.$watch('originalData', function (current) {
if (current && !angular.equals(current, [])) {
if ($scope.isPaginated(current)) {
//getting it also removes it, which is desirable here
$scope.getPaginationObj(current);
}
}
});
//expects endpoint
//returns $http promise
$scope.getRows = function (endpoint, filters) {
if (filters) {
if (typeof filters == 'string') {
endpoint += '?' + filters;
}
else {
console.log('filtering by object not implemented');
}
}
return Data.get(endpoint).success(function (rows) {
Transit.ping({
broadcast: 'EventAfterSearch',
data: {rows: rows},
onReply: function (event, data) {
if (data && data.rows) {
$scope.data = data.rows;
$scope.originalData = angular.copy(data.rows);
$scope.refreshColDefs();
}
},
}, window.parent);
$scope.data = rows;
$scope.originalData = angular.copy(rows);
$scope.refreshColDefs();
$(window).resize();
});
};
$scope.refreshColDefs = function () {
if ($scope.isColDefSet) { return; }
$scope.colDefs = [];
if ($rootScope.config.columns) {
$scope.colDefs = $rootScope.config.columns;
}
else {
if ($scope.data[0]) {
var columns = _.keys($scope.data[0]);
angular.forEach(columns, function (column) {
if (column == '@metadata') {return;}
var colDef = null;
var words = _.words(_.humanize(column));
var display = [];
angular.forEach(words, function (word) {
display.push(_.capitalize(word));
});
colDef = {
field: column,
displayName: display.join(' '),
}
if ($scope.table && $scope.table.named) {
colDef.cellClass = 'generic-type-' + $scope.table.named[column].generic_type;
}
if (colDef) {
$scope.colDefs.push(colDef);
}
});
}
else {
console.log('no data, no column keys');
}
}
};
//expects a full url
//returns $http promise
$scope.appendRows = function (url) {
return Data.getUrl(url).success(function (data) {
$scope.data = $scope.data.concat(data);
$scope.originalData = $scope.originalData.concat(angular.copy(data));
});
};
//a collection of columns and the foreign key they are a part of
$scope.foreignColumns = {};
//expects parents array from @tables request
$scope.getForeignKeys = function (parents) {
angular.forEach(parents, function (parent) {
angular.forEach(parent.parent_columns, function (column) {
$scope.foreignColumns[column] = parent;
});
});
};
$scope.parentLookup = function (ngRow, column) {
//grab the index
var index = $scope.data.indexOf(ngRow.entity);
$scope.lookupModal = $modal.open({
templateUrl: 'partials/parentLookup.html',
controller: 'LookupController',
size: 'lg',
resolve: {
relationship: function () { return $scope.foreignColumns[column] }
//row: ngRow.entity,
//index: index
}
});
$scope.lookupModal.result.then(function (record) {
Transit.ping({
broadcast: 'ControlParentLookupSelection',
data: record,
onReply: function (event, data) {
if (data && data.row) {
angular.forEach($scope.foreignColumns[column].child_columns, function (column, i) {
$scope.data[index][column] = record[$scope.foreignColumns[column].parent_columns[i]];
});
$scope.setRowAction(index, 'UPDATE', false);
}
},
}, window.parent);
angular.forEach($scope.foreignColumns[column].child_columns, function (column, i) {
$scope.data[index][column] = record[$scope.foreignColumns[column].parent_columns[i]];
});
$scope.setRowAction(index, 'UPDATE', false);
});
//blur the dom element, required because of strange ng grid behaviors
$timeout(function () {
$('input').blur(); //prevents direct editing
});
Transit.ping({
broadcast: 'CheckParentLookup',
data: {},
onReply: function (event, data) {
if (!data) {
$scope.lookupModal.dismiss();
}
},
}, window.parent);
};
//watcher and modifier of default column definitions
//for example, by default foreign keys ought to behave differently than standard columns
$scope.$watch('colDefs', function (current, previous) {
var colDefKeys = _.indexBy(current, 'field');
angular.forEach(current, function (definition, index) {
//foreign key
if ($scope.foreignColumns[definition.field]) {
current[index].editableCellTemplate = Templates.get('foreignKey', $scope.foreignColumns[definition.field]);
}
});
Transit.broadcast('EventColumnsRefreshed', current);
}, true);
$scope.$on('AuthUpdate', function (event, auth) {
$scope.getRows(auth.endpoint);
Data.get('@tables/' + auth.endpoint).success(function (data) {
$scope.table = data;
$scope.getForeignKeys($scope.table.parents);
angular.forEach($scope.table.columns, function (column, index) {
$scope.table.named = _.indexBy($scope.table.columns, 'name');
});
Transit.broadcast('GridReady', $scope.frameState());
$scope.$emit('GridReady', $scope.frameState());
});
});
$scope.selected = {};
$scope.options = {
data: 'data',
enableCellSelection: true,
enableRowSelection: true,
enableCellEdit: true,
showSelectionCheckbox: true,
showFooter: true,
footerTemplate: 'gridFooter.html',
rowTemplate: '<div ' +
'ng-style="{ \'cursor\': row.cursor }" ' +
'ng-repeat="col in renderedColumns" ' +
'ng-class="col.colIndex()" ' +
'class="ngCell {{col.cellClass}} {{row.entity[\'@metadata\'].action && \'modified\'}}">' +
'<div class="ngVerticalBar" ng-style="{height: rowHeight}" ng-class="{ ngVerticalBarVisible: !$last }"> </div><div ng-cell></div>' +
'</div>',
beforeSelectionChange: function (row) {
Transit.broadcast('EventBeforeSelection', row.entity);
return true;
},
afterSelectionChange: function (row) {
if (row.selected) {
Transit.broadcast('EventRowSelected', row.entity);
var index = $scope.data.indexOf(row.entity);
$scope.selected[index] = $scope.data[index];
}
else {
Transit.broadcast('EventRowDeselected', row.entity);
delete $scope.selected[row.rowIndex];
}
},
columnDefs: 'colDefs'
};
$scope.$on('GridReady', function () {
angular.forEach($rootScope.config.gridOptions, function (element, index) {
$scope.options.ngGrid.config[index] = element;
});
$scope.options.ngGrid.init();
});
$scope.colDefs = [];
$scope.isPaginated = function (data) {
if (data[data.length-1]) {
var lastElement = data[data.length-1];
if (lastElement['@metadata'] && lastElement['@metadata'].next_batch) {
return true;
}
}
return false;
};
$scope.nextBatch = false;
$scope.getPaginationObj = function (data) {
var nextBatch = data.pop()['@metadata'].next_batch;
return nextBatch;
};
$scope.setNextBatch = function (override) {
$scope.nextBatch = override;
};
$scope.populateMetadata = function (row) {
var metadata = {
action: 'INSERT',
//href: Data.auth.apiBase + '/' + Data.auth.endpoint,
};
row['@metadata'] = metadata;
return row;
};
$scope.addRow = function (row) {
var defaultRowAttributes = _.object(_.keys($scope.table.named), []);
angular.forEach(defaultRowAttributes, function (element, index) {
if (!row[index]) {
row[index] = null;
}
});
$scope.populateMetadata(row);
$scope.data.unshift(row);
//populate the original data so the indexes align, side effect is undo doesn't remove inserted objects
$scope.originalData.unshift(angular.copy(row));
Transit.broadcast('EventRowInserted', row);
return row;
};
$scope.isValidRow = function (row) {
return row && row['@metadata'] && row['@metadata'];
};
$scope.isUnsavedInsertedRow = function (row) {
return row['@metadata'].action == 'INSERT';
};
$scope.isSavedInsertedRow = function (row) {
return row['@metadata'].verb == 'INSERT';
};
$scope.isRowUpdating = function (row) {
return row['@metadata'].action == 'UPDATE';
};
$scope.isRowDeleting = function (row) {
return row['@metadata'].action == 'DELETE';
};
$scope.findInserts = function (arr, original) {
var changed = {};
angular.forEach(arr, function (element, index) {
if ($scope.isValidRow(element) && $scope.isUnsavedInsertedRow(element)) {
changed[index] = element;
}
});
return changed;
};
$scope.findUpdates = function (arr, original) {
var changed = {};
angular.forEach(arr, function (element, index) {
if ($scope.isValidRow(element) && $scope.isRowUpdating(element)) {
changed[index] = element;
}
});
return changed;
};
$scope.findDels = function (arr, original) {
var changed = {};
angular.forEach(arr, function (element, index) {
if ($scope.isValidRow(element) && $scope.isRowDeleting(element)) {
changed[index] = element;
}
});
return changed;
};
$scope.save = function (rows) {
return Data.put(Data.auth.endpoint, rows);
};
$scope.replaceLocalInserts = function (inserts, summary) {
angular.forEach(inserts, function (element, index) {
$scope.data.splice(index, 1);
$scope.originalData.splice(index, 1);
});
angular.forEach(summary, function (element, index) {
if ($scope.isSavedInsertedRow(element) && element['@metadata'].resource == Data.auth.endpoint) {
$scope.data.unshift(element);
$scope.originalData.unshift(element);
}
});
$scope.forceGridRefresh();
};
$scope.forceGridRefresh = function () {
$scope.data.push({});
$timeout(function () {
$scope.data.pop();
});
};
$scope.setRowAction = function (index, action, force) {
//if override && action attribute exists
if (!force && $scope.data[index]['@metadata'].action) {
//don't update the action
return;
}
$scope.data[index]['@metadata'].action = action;
};
$scope.$on('ngGridEventStartCellEdit', function (event, data) {
Transit.ping({
broadcast: 'CheckStartEdit',
data: {test:true},
onReply: function (event, data) {
if (data === false) {
$('*').blur();
}
}
}, window.parent);
});
$scope.$on('ngGridEventDigestCell', function (event, data) {
console.log('digesting grid');
});
$scope.$on('ngGridEventEndCellEdit', function (event, data) {
var row = event.targetScope.$eval('row');
var data = {
row: row.entity,
index: $scope.data.indexOf(row.entity),
isSelected: row.selected,
isChanged: false
};
if (!angular.equals(data.row, $scope.originalData[data.index])) {
data.isChanged = true;
$scope.setRowAction(data.index, 'UPDATE', false);
}
Transit.broadcast('EventRowEdit', data);
});
$scope.replaceLocalUpdates = function (updates, summary) {
angular.forEach(summary, function (element, index) {
angular.forEach(updates, function (e, i) {
if ($scope.isValidRow(e) && $scope.isValidRow(element)) {
if (e['@metadata'].href == element['@metadata'].href) {
$scope.data[i] = element;
$scope.originalData[i] = angular.copy(element);
}
}
});
});
$scope.forceGridRefresh();
};
$scope.removeLocalDels = function (dels, summary) {
angular.forEach(summary, function (element, index) {
angular.forEach(dels, function (e, i) {
if (e['@metadata'].action != 'DELETE') { console.log('test'); return;}
if ($scope.isValidRow(e) && $scope.isValidRow(element)) {
if (e['@metadata'].href == element['@metadata'].href) {
$scope.data.splice(i, 1);
$scope.originalData.splice(i, 1);
$scope.selected.splice(i, 1);
}
}
});
});
$scope.forceGridRefresh();
};
$(window).resize(function () {
$scope.$evalAsync(function () {
var controlHeight = 60;
var maxHeight = $scope.data.length * '45';
var height = $(window).height() - controlHeight;
if (maxHeight < height) {
height = maxHeight + controlHeight;
}
if (height < 200) {
height = 200;
}
$('.ngGrid').height(height+15);
});
});
$scope.alertMessage = null;
$scope.prevAlert = null;
$scope.alert = function (message, timeout) {
if (!timeout) { timeout = 2500; }
$scope.alertMessage = message;
if ($scope.prevAlert) {
$timeout.cancel($scope.prevAlert);
}
$scope.prevAlert = $timeout(function () {
$scope.alertMessage = null;
}, timeout);
};
$scope.modal = function (message) {
return $modal.open({
template: message,
windowClass: 'alert-modal',
controller: function () {}
});
};
$rootScope.controls.fetch = function () {
if ($scope.nextBatch) {
$scope.appendRows($scope.nextBatch);
$scope.alert('Fetching ...');
}
};
$scope.isRowInsertAllowed = true;
$rootScope.controls.insert = function (row) {
Transit.ping({
broadcast: 'CheckAddRow',
data: {},
onReply: function (event, data) {
if (!data) {
$scope.isRowInsertAllowed = true;
}
}
});
$timeout(function () {
if (!$scope.isRowInsertAllowed) {return;}
if (!row) {
row = {};
}
$scope.addRow(row);
}, 150);
};
$rootScope.controls.save = function () {
var inserts = $scope.findInserts($scope.data, $scope.originalData);
var updates = $scope.findUpdates($scope.data, $scope.originalData);
var batch = _.extend({}, inserts, updates);
//if we have any batch attributes, save them
if (!angular.equals({}, batch)) {
$scope.alert('Saving ...');
$scope.save(_.values(batch)).success(function (data) {
$scope.replaceLocalInserts(inserts, data.txsummary);
$scope.replaceLocalUpdates(updates, data.txsummary);
console.log(updates, data);
var modified = {};
angular.forEach(data.txsummary, function (element, index) {
if ($scope.isValidRow(element)) {
if (!modified[element['@metadata'].verb]) { modified[element['@metadata'].verb] = 0; }
modified[element['@metadata'].verb]++
}
});
var alertFragments = [];
angular.forEach(modified, function (count, action) {
alertFragments.push(action + ': ' + count);
});
$scope.alert('[Objects Modified] ' + alertFragments.join(', '), 4000);
})['error'](function (err) {
$scope.modal(err.errorMessage);
});
}
};
$rootScope.controls.del = function () {
angular.forEach($scope.selected, function (element, index) {
$scope.setRowAction(index, 'DELETE', false);
});
if (!angular.equals({}, $scope.selected)) {
$scope.save(_.values($scope.selected)).success(function (data) {
$scope.removeLocalDels($scope.selected, data.txsummary);
})['error'](function (err) {
console.log(err);
$scope.modal(err.errorMessage);
});
}
};
$rootScope.controls.search = function () {
if (!$scope.filters.length) {
$rootScope.controls.addFilter();
}
$rootScope.params.filters = !$rootScope.params.filters;
};
$rootScope.controls.undo = function () {
angular.forEach($scope.originalData, function (element, index) {
if ($scope.isValidRow(element)) {
if (element['@metadata'].action && element['@metadata'].action === 'INSERT') {
var i = $scope.originalData.indexOf(element);
$scope.originalData.splice(i, 1);
}
}
});
$scope.data = angular.copy($scope.originalData);
};
$rootScope.controls.addFilter = function () {
$scope.filters.push({
column: $scope.table.columns[0],
operator: $rootScope.operators.contains,
text: ''
});
};
$rootScope.controls.removeFilter = function (index) {
$scope.filters.splice(index, 1);
};
$scope.isSearchRunnable = true;
$rootScope.controls.runSearch = function (filters) {
Transit.ping({
broadcast: 'CheckBeforeSearch',
data: $scope.filters,
onReply: function (event, data) {
if (!data) {
$scope.isSearchRunnable = false;
}
},
}, window.parent);
$timeout(function () {
$scope.alert('Searching ...');
if (!$scope.isSearchRunnable) { return; }
var parsed = 'filter=';
var filterArr = [];
angular.forEach(filters, function (filter, index) {
//column.name + operator.prefix + filter.text + operator.suffix
//customers %{text}%
var text = filter.operator.prefix + filter.text + filter.operator.suffix;
if (typeof filter.text === 'boolean') {
text = text.replace(/'/g, '');
}
filterArr[index] = filter.column.name + text;
});
parsed += filterArr.join(' AND ');
$scope.getRows(Data.auth.endpoint, parsed);
}, 150);
};
//no expectations
Transit.on('ControlSave', $rootScope.controls.save);
//no expectations
Transit.on('ControlDel', $rootScope.controls.del);
//no expectations
Transit.on('ControlFetch', $rootScope.controls.fetch);
//expects data to be row in grid
Transit.on('ControlSelectRow', function (event, data) {
$scope.options.selectRow(data, true);
});
//expects data to be row in grid
Transit.on('ControlDeselectRow', function (event, data) {
$scope.options.selectRow(data, false);
});
//expects data to be $scope.filters object
Transit.on('ControlRunSearch', function (event, data) {
$rootScope.controls.runSearch(data);
});
//expects data.index and a data.row object
Transit.on('ControlUpdateRow', function (event, data) {
$scope.data[data.index] = _.extend($scope.data[data.index], data.row);
});
//expects a new data.row object
Transit.on('ControlInsertRow', function (event, data) {
$scope.controls.insert(data.row);
});
$scope.isColDefSet = false;
//expects data to be ng-grid column definitions
Transit.on('ControlColumnDefinitions', function (event, data) {
$scope.isColDefSet = true;
$scope.colDefs = data;
});
//expects data to be ng-grid column definitions
Transit.on('ControlRawCSS', function (event, data) {
$scope.rawCSS = data;
});
$scope.frameState = function () {
return {
table: $scope.table,
operators: $scope.operators,
columns: $scope.colDefs
};
};
Transit.on('GetFrameState', function (event, data) {
var snapshot = $scope.frameState();
Transit.broadcast(data, snapshot);
});
Transit.on('GetAllRows', function (event, data) {
Transit.broadcast(data, $scope.data);
});
//Transit.on('GetSelectedRows', function (event, data) {});
Transit.on('GetFilters', function (event, data) {
Transit.broadcast(data, $scope.filters);
});
}
]);