Initial commit
This commit is contained in:
@@ -0,0 +1,539 @@
|
||||
const $ = require('jquery');
|
||||
const _ = require('underscore');
|
||||
const CoreView = require('backbone/core-view');
|
||||
const Table = require('dashboard/components/table/table');
|
||||
const templateSQLViewNotice = require('./sql-view-notice.tpl');
|
||||
const templateSQLLoading = require('./sql-loading.tpl');
|
||||
const templateEmptySQL = require('./empty-sql.tpl');
|
||||
const templateTablePaginationLoaders = require('./table-pagination-loaders.tpl');
|
||||
const RowView = require('./row-view');
|
||||
const checkAndBuildOpts = require('builder/helpers/required-opts');
|
||||
|
||||
const REQUIRED_OPTS = [
|
||||
'configModel'
|
||||
];
|
||||
|
||||
module.exports = Table.extend({
|
||||
|
||||
classLabel: 'cdb.admin.TableView',
|
||||
|
||||
events: CoreView.extendEvents({
|
||||
'click .clearview': '_clearView',
|
||||
'click .sqlview .export_query': '_tableFromQuery',
|
||||
'click .noRows': 'addEmptyRow'
|
||||
}),
|
||||
|
||||
rowView: RowView,
|
||||
|
||||
initialize: function (opts) {
|
||||
checkAndBuildOpts(opts, REQUIRED_OPTS, this);
|
||||
Table.prototype.initialize.call(this);
|
||||
this.options.row_header = true;
|
||||
this.globalError = this.options.globalError;
|
||||
this.vis = this.options.vis;
|
||||
this.user = this.options.user;
|
||||
this._editorsOpened = null;
|
||||
|
||||
this.initializeBindings();
|
||||
|
||||
this.initPaginationAndScroll();
|
||||
},
|
||||
|
||||
/**
|
||||
* Append all the bindings needed for this view
|
||||
* @return undefined
|
||||
*/
|
||||
initializeBindings: function () {
|
||||
_.bindAll(this, 'render', 'rowSaving', 'addEmptyRow',
|
||||
'_checkEmptyTable', '_forceScroll', '_scrollMagic',
|
||||
'rowChanged', 'rowSynched', '_startPagination', '_finishPagination',
|
||||
'rowFailed', 'rowDestroyed', 'emptyTable');
|
||||
|
||||
this.model.data().bind('newPage', this.newPage, this);
|
||||
|
||||
// this.model.data().bind('loadingRows', this._startPagination);
|
||||
this.model.data().bind('endLoadingRows', this._finishPagination);
|
||||
|
||||
this.bind('cellDblClick', this._editCell, this);
|
||||
this.bind('createRow', () => {
|
||||
this._checkEmptyTable();
|
||||
});
|
||||
|
||||
this.model.bind('change:dataSource', this._onSQLView, this);
|
||||
// when model changes the header is re rendered so the notice should be added
|
||||
// this.model.bind('change', this._onSQLView, this);
|
||||
this.model.bind('dataLoaded', () => {
|
||||
// this._checkEmptyTable();
|
||||
this._forceScroll();
|
||||
}, this);
|
||||
|
||||
this.model.bind('change:permission', this._checkEmptyTable, this);
|
||||
|
||||
this.model.bind('change:isSync', this._swicthEnabled, this);
|
||||
this._swicthEnabled();
|
||||
|
||||
// Geocoder binding
|
||||
this.options.geocoder.bind('geocodingComplete geocodingError geocodingCanceled', function () {
|
||||
this.notice(_t('loaded'));
|
||||
}, this);
|
||||
this.add_related_model(this.options.geocoder);
|
||||
},
|
||||
|
||||
initPaginationAndScroll: function () {
|
||||
var topReached = false;
|
||||
var bottomReached = false;
|
||||
|
||||
// Initialize moving header and loaders when scrolls
|
||||
this.scroll_position = { x: $(window).scrollLeft(), y: $(window).scrollTop(), last: 'vertical' };
|
||||
$(window).scroll(this._scrollMagic);
|
||||
|
||||
// Pagination
|
||||
var SCROLL_BACK_PIXELS = 2;
|
||||
this.checkScrollTimer = setInterval(() => {
|
||||
if (!this.$el.is(':visible') || this.model.data().isFetchingPage()) {
|
||||
return;
|
||||
}
|
||||
var pos = $(this).scrollTop();
|
||||
var d = this.model.data();
|
||||
// do not let to fetch previous pages
|
||||
// until the user dont scroll back a little bit
|
||||
// see comments below
|
||||
if (pos > SCROLL_BACK_PIXELS) {
|
||||
topReached = false;
|
||||
}
|
||||
var pageSize = $(window).height() - this.$el.offset().top;
|
||||
var tableHeight = this.$('tbody').height();
|
||||
var realPos = pos + pageSize;
|
||||
if (tableHeight < pageSize) {
|
||||
return;
|
||||
}
|
||||
// do not let to fetch previous pages
|
||||
// until the user dont scroll back a little bit
|
||||
// if we dont do this when the user reach the end of the page
|
||||
// and there are more rows than max_rows, the rows form the beggining
|
||||
// are removed and the scroll keeps at the bottom so a new page is loaded
|
||||
// doing this the user have to move the scroll a little bit (2 px)
|
||||
// in order to load the page again
|
||||
if (realPos < tableHeight - SCROLL_BACK_PIXELS) {
|
||||
bottomReached = false;
|
||||
}
|
||||
if (realPos >= tableHeight) {
|
||||
if (!bottomReached) {
|
||||
// Simulating loadingRows event
|
||||
if (!d.lastPage) this._startPagination('down');
|
||||
|
||||
setTimeout(function () {
|
||||
d.loadPageAtBottom();
|
||||
}, 600);
|
||||
}
|
||||
bottomReached = true;
|
||||
} else if (pos <= 0) {
|
||||
if (!topReached) {
|
||||
// Simulating loadingRows event
|
||||
if (d.pages && d.pages[0] != 0) this._startPagination('up'); // eslint-disable-line eqeqeq
|
||||
|
||||
setTimeout(function () {
|
||||
d.loadPageAtTop();
|
||||
}, 600);
|
||||
}
|
||||
topReached = true;
|
||||
}
|
||||
|
||||
this._setUpPagination(d);
|
||||
}, 300);
|
||||
this.bind('clean', function () {
|
||||
clearInterval(this.checkScrollTimer);
|
||||
}, this);
|
||||
},
|
||||
|
||||
needsRender: function (table) {
|
||||
if (!table) return true;
|
||||
var ca = table.changedAttributes();
|
||||
if (ca.geometry_types && _.keys(ca).length === 1) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
||||
render: function (args) {
|
||||
if (!this.needsRender(args)) return;
|
||||
Table.prototype.render.call(this, args);
|
||||
if (this.model.isInSQLView()) {
|
||||
this._onSQLView();
|
||||
}
|
||||
this._swicthEnabled();
|
||||
this.trigger('render');
|
||||
},
|
||||
|
||||
_renderHeader: function () {
|
||||
var thead = Table.prototype._renderHeader.apply(this);
|
||||
// New custom shadow (better performance)
|
||||
thead.append($('<div>').addClass('shadow'));
|
||||
return thead;
|
||||
},
|
||||
|
||||
addColumn: function (column) {
|
||||
this.newColumnName = 'column_' + new Date().getTime();
|
||||
|
||||
this.model.addColumn(this.newColumnName, 'string');
|
||||
|
||||
this.unbind('render', this._highlightColumn, this);
|
||||
this.bind('render', this._highlightColumn, this);
|
||||
},
|
||||
|
||||
_highlightColumn: function () {
|
||||
if (this.newColumnName) {
|
||||
var $th = this.$("a[href='#" + this.newColumnName + "']").parents('th');
|
||||
var position = $th.index();
|
||||
|
||||
if (position) {
|
||||
setTimeout(function () {
|
||||
var windowWidth = $(window).width();
|
||||
if ($th && $th.position()) {
|
||||
var centerPosition = $th.position().left - windowWidth / 2 + $th.width() / 2;
|
||||
$(window).scrollLeft(centerPosition);
|
||||
}
|
||||
this.$("[data-x='" + position + "']").addClass('is-highlighted');
|
||||
}, 300);
|
||||
|
||||
this.unbind('render', this._highlightColumn, this);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Take care if the table needs space at top and bottom
|
||||
* to show the loaders.
|
||||
*/
|
||||
_setUpPagination: function (d) {
|
||||
var pages = d.pages;
|
||||
|
||||
// Check if the table is not in the first page
|
||||
if (pages.length > 0 && pages[0] > 0) {
|
||||
// In that case, add the paginator-up loader and leave it ready
|
||||
// when it is necessary
|
||||
if (this.$el.find('tfoot.page_loader.up').length == 0) { // eslint-disable-line eqeqeq
|
||||
this.$el.append(templateTablePaginationLoaders({ direction: 'up' }));
|
||||
}
|
||||
// Table now needs some space at the top to show the loader
|
||||
this.$el.parent().addClass('page_up');
|
||||
} else {
|
||||
// Loader is not needed and table doesn't need any space at the top
|
||||
this.$el.parent().removeClass('page_up');
|
||||
}
|
||||
|
||||
// Checks if we are in the last page
|
||||
if (!d.lastPage) {
|
||||
// If not, let's prepare the paginator-down
|
||||
if (this.$el.find('tfoot.page_loader.down').length == 0) { // eslint-disable-line eqeqeq
|
||||
this.$el.append(templateTablePaginationLoaders({ direction: 'down' }));
|
||||
}
|
||||
// Let's say to the table that we have paginator-down
|
||||
this.$el.parent().addClass('page_down');
|
||||
} else {
|
||||
// Loader is not needed and table doesn't need any space at the bottom
|
||||
this.$el.parent().removeClass('page_down');
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* What to do when a pagination starts
|
||||
*/
|
||||
_startPagination: function (updown) {
|
||||
// Loader... move on buddy!
|
||||
this.$el.find('.page_loader.' + updown + '').addClass('active');
|
||||
},
|
||||
|
||||
/**
|
||||
* What to do when a pagination finishes
|
||||
*/
|
||||
_finishPagination: function (page, updown) {
|
||||
// If we are in a different page than 0, and we are paginating up
|
||||
// let's move a little bit the scroll to hide the loader again
|
||||
// HACKY
|
||||
if (page != 0 && updown == 'up') { // eslint-disable-line eqeqeq
|
||||
setTimeout(function () {
|
||||
$(window).scrollTop(180);
|
||||
}, 300);
|
||||
}
|
||||
|
||||
this.$el.find('.page_loader.active').removeClass('active');
|
||||
},
|
||||
|
||||
_onSQLView: function () {
|
||||
// bind each time we change dataSource because table unbind
|
||||
// all the events from sqlView object each time useSQLView is called
|
||||
this.$('.sqlview').remove();
|
||||
|
||||
this.options.sqlView.unbind('reset error', this._renderSQLHeader, this);
|
||||
this.options.sqlView.unbind('loading', this._renderLoading, this);
|
||||
|
||||
this.options.sqlView.bind('loading', this._renderLoading, this);
|
||||
this.options.sqlView.bind('reset', this._renderSQLHeader, this);
|
||||
this.options.sqlView.bind('error', this._renderSQLHeader, this);
|
||||
this._renderSQLHeader();
|
||||
},
|
||||
|
||||
_renderLoading: function (opts) {
|
||||
opts = opts || {};
|
||||
this.cleanEmptyTableInfo();
|
||||
if (!opts.add) {
|
||||
this._renderBodyTemplate(templateSQLLoading);
|
||||
}
|
||||
},
|
||||
|
||||
_renderSQLHeader: function () {
|
||||
if (this.model.isInSQLView()) {
|
||||
var empty = this.isEmptyTable();
|
||||
this.$('thead').find('.sqlview').remove();
|
||||
this.$('thead').append(
|
||||
templateSQLViewNotice({
|
||||
empty: empty,
|
||||
isVisualization: this.vis.isVisualization(),
|
||||
warnMsg: null
|
||||
})
|
||||
);
|
||||
|
||||
this.$('thead > tr').css('height', 64 + 42);
|
||||
if (this.isEmptyTable()) {
|
||||
this.addEmptySQLIfo();
|
||||
}
|
||||
|
||||
this._moveInfo();
|
||||
}
|
||||
},
|
||||
|
||||
// depending if the sync is enabled add or remove a class
|
||||
_swicthEnabled: function () {
|
||||
// Synced?
|
||||
this.$el[ this.model.isSync() ? 'addClass' : 'removeClass' ]('synced');
|
||||
// Visualization?
|
||||
this.$el[ this.vis.isVisualization() ? 'addClass' : 'removeClass' ]('vis');
|
||||
},
|
||||
|
||||
_clearView: function (e) {
|
||||
if (e) e.preventDefault();
|
||||
this.options.layer.clearSQLView();
|
||||
return false;
|
||||
},
|
||||
|
||||
_tableFromQuery: function (e) {
|
||||
throw new Error('Method not migrated, check original implementation');
|
||||
},
|
||||
|
||||
/**
|
||||
* Function to control the scroll in the table (horizontal and vertical)
|
||||
*/
|
||||
_scrollMagic: function (ev) {
|
||||
var actual_scroll_position = { x: $(window).scrollLeft(), y: $(window).scrollTop() };
|
||||
|
||||
if (this.scroll_position.x != actual_scroll_position.x) { // eslint-disable-line eqeqeq
|
||||
this.scroll_position.x = actual_scroll_position.x;
|
||||
this.$el.find('thead').addClass('horizontal');
|
||||
|
||||
// If last change was vertical
|
||||
if (this.scroll_position.last == 'vertical') { // eslint-disable-line eqeqeq
|
||||
this.scroll_position.x = actual_scroll_position.x;
|
||||
|
||||
this.$el.find('thead > tr > th > div > div:not(.dropdown)')
|
||||
.removeAttr('style')
|
||||
.css('top', actual_scroll_position.y + 'px');
|
||||
|
||||
this.scroll_position.last = 'horizontal';
|
||||
}
|
||||
} else if (this.scroll_position.y != actual_scroll_position.y) { // eslint-disable-line eqeqeq
|
||||
this.scroll_position.y = actual_scroll_position.y;
|
||||
this.$el.find('thead').removeClass('horizontal');
|
||||
|
||||
// If last change was horizontal
|
||||
if (this.scroll_position.last == 'horizontal') { // eslint-disable-line eqeqeq
|
||||
this.$el.find('thead > tr > th > div > div:not(.dropdown)')
|
||||
.removeAttr('style')
|
||||
.css({'marginLeft': '-' + actual_scroll_position.x + 'px'});
|
||||
|
||||
this.scroll_position.last = 'vertical';
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Move the info content if the panel is opened or hidden.
|
||||
* - Query info if query is applied
|
||||
* - Query loader if query is appliying in that moment
|
||||
* - Add some padding to last column of the content to show them
|
||||
*/
|
||||
_moveInfo: function (type) {
|
||||
if (type == 'show') { // eslint-disable-line eqeqeq
|
||||
this.$el
|
||||
.removeClass('narrow')
|
||||
.addClass('displaced');
|
||||
} else if (type == 'narrow') { // eslint-disable-line eqeqeq
|
||||
this.$el.addClass('displaced narrow');
|
||||
} else if (type == 'hide') { // eslint-disable-line eqeqeq
|
||||
this.$el.removeClass('displaced narrow');
|
||||
} else {
|
||||
// Check from the beginning if the right menu is openned, isOpen from
|
||||
// the menu is not working properly
|
||||
if ($('.table_panel').length > 0) {
|
||||
var opened = $('.table_panel').css('right').replace('px', '') == 0; // eslint-disable-line eqeqeq
|
||||
if (!opened) {
|
||||
this.$el.removeClass('displaced');
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
_getEditor: function (columnType, opts) {
|
||||
throw new Error('Method not migrated, check original implementation');
|
||||
},
|
||||
|
||||
closeEditor: function () {
|
||||
if (this._editorsOpened) {
|
||||
this._editorsOpened.hide();
|
||||
this._editorsOpened.clean();
|
||||
}
|
||||
},
|
||||
|
||||
_editCell: function (e, cell, x, y) {
|
||||
throw new Error('Method not migrated, check original implementation');
|
||||
},
|
||||
|
||||
headerView: function (column) {
|
||||
throw new Error('Method not migrated, check original implementation');
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks if the table has any rows, and if not, launch the method for showing the appropiate view elements
|
||||
* @method _checkEmptyTable
|
||||
*/
|
||||
_checkEmptyTable: function () {
|
||||
if (this.isEmptyTable()) {
|
||||
this.addEmptyTableInfo();
|
||||
} else {
|
||||
this.cleanEmptyTableInfo();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Force the table to be at the beginning
|
||||
* @method _forceScroll
|
||||
*/
|
||||
_forceScroll: function (ev) {
|
||||
$(window).scrollLeft(0);
|
||||
},
|
||||
|
||||
_renderEmpty: function () {
|
||||
this.addEmptyTableInfo();
|
||||
},
|
||||
|
||||
/**
|
||||
* Adds the view elements associated with no content in the table
|
||||
* @method addemptyTableInfo
|
||||
*/
|
||||
addEmptyTableInfo: function () {
|
||||
throw new Error('Method not migrated, check original implementation');
|
||||
},
|
||||
|
||||
/**
|
||||
* Adds the view elements associated with no content in the table when a SQL is applied
|
||||
* @method addEmptySQLIfo
|
||||
*/
|
||||
addEmptySQLIfo: function () {
|
||||
if (this.model.isInSQLView()) {
|
||||
this._renderBodyTemplate(templateEmptySQL);
|
||||
}
|
||||
},
|
||||
|
||||
_renderBodyTemplate: function (template) {
|
||||
this.$('tbody').html('');
|
||||
this.$('tfoot').remove();
|
||||
this.$el.hide();
|
||||
|
||||
// Check if panel is opened to move the loader some bit left
|
||||
var panel_opened = false;
|
||||
if ($('.table_panel').length > 0) {
|
||||
panel_opened = $('.table_panel').css('right').replace('px', '') == 0; // eslint-disable-line eqeqeq
|
||||
}
|
||||
|
||||
var content = template({ config: this._configModel, panel_opened });
|
||||
var $footer = $('<tfoot class="sql_loader"><tr><td colspan="100">' + content + '</td></tr></tfoot>');
|
||||
|
||||
this.$el.append($footer);
|
||||
this.$el.fadeIn();
|
||||
},
|
||||
|
||||
/**
|
||||
* Removes from the view the no-content elements
|
||||
* @method cleanEmptyTableInfo
|
||||
*/
|
||||
cleanEmptyTableInfo: function () {
|
||||
this.$('tfoot').fadeOut('fast', function () {
|
||||
$(this).remove();
|
||||
});
|
||||
this.$('.noRows').slideUp('fast', function () {
|
||||
$(this).remove();
|
||||
});
|
||||
},
|
||||
|
||||
notice: function (text, type, time) {
|
||||
this.globalError.showError(text, type, time);
|
||||
},
|
||||
|
||||
/**
|
||||
* Add a new row and removes the empty table view elemetns
|
||||
* @method addEmptyRow
|
||||
* @todo: (xabel) refactor this to include a "addRow" method in _models[0]
|
||||
*/
|
||||
addEmptyRow: function () {
|
||||
this.dataModel.addRow({ at: 0 });
|
||||
this.cleanEmptyTableInfo();
|
||||
},
|
||||
|
||||
/**
|
||||
* Captures the saving event from the row and produces a notification
|
||||
* @todo (xabel) i'm pretty sure there has to be a less convulted way of doing this, without capturing a event
|
||||
* to throw another event in the model to be captured by some view
|
||||
*/
|
||||
rowSaving: function () {
|
||||
this.notice('Saving your edit', 'load', -1);
|
||||
},
|
||||
|
||||
/**
|
||||
* Captures the change event from the row and produces a notification
|
||||
* @method rowSynched
|
||||
* @todo (xabel) i'm pretty sure there has to be a less convulted way of doing this, without capturing a event
|
||||
* to throw another event in the model to be captured by some view
|
||||
*/
|
||||
rowSynched: function () {
|
||||
this.notice('Sucessfully saved');
|
||||
},
|
||||
|
||||
/**
|
||||
* Captures the change event from the row and produces a notification
|
||||
* @method rowSynched
|
||||
* @todo (xabel) i'm pretty sure there has to be a less convulted way of doing this, without capturing a event
|
||||
* to throw another event in the model to be captured by some view
|
||||
*/
|
||||
rowFailed: function () {
|
||||
this.notice('Oops, there has been an error saving your changes.', 'error');
|
||||
},
|
||||
|
||||
/**
|
||||
* Captures the destroy event from the row and produces a notification
|
||||
* @method rowDestroyed
|
||||
*/
|
||||
|
||||
rowDestroying: function () {
|
||||
this.notice('Deleting row', 'load', -1);
|
||||
},
|
||||
|
||||
/**
|
||||
* Captures the sync after a destroy event from the row and produces a notification
|
||||
* @method rowDestroyed
|
||||
*/
|
||||
|
||||
rowDestroyed: function () {
|
||||
this.notice('Sucessfully deleted');
|
||||
this._checkEmptyTable();
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user